Compare commits
	
		
			30 Commits
		
	
	
		
			3.2.0+132
			...
			406e5187a8
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						406e5187a8
	
				 | 
					
					
						|||
| 
						
						
							
						
						9bdd08d8dd
	
				 | 
					
					
						|||
| 
						
						
							
						
						d737232dcf
	
				 | 
					
					
						|||
| 
						
						
							
						
						c9d751479e
	
				 | 
					
					
						|||
| 
						
						
							
						
						a2c2bfe585
	
				 | 
					
					
						|||
| 
						
						
							
						
						c7f9da0dee
	
				 | 
					
					
						|||
| 
						 | 
					a243cda1df | ||
| 
						 | 
					7b238f32fd | ||
| 
						
						
							
						
						313af28d7f
	
				 | 
					
					
						|||
| 
						
						
							
						
						c64e1e208c
	
				 | 
					
					
						|||
| 
						
						
							
						
						c9b07a9a2a
	
				 | 
					
					
						|||
| 55c0e355f1 | |||
| be414891ec | |||
| 787876ab6a | |||
| 
						
						
							
						
						8578cde620
	
				 | 
					
					
						|||
| 
						
						
							
						
						14d55d45a8
	
				 | 
					
					
						|||
| 
						
						
							
						
						724391584e
	
				 | 
					
					
						|||
| 3a5e45808a | |||
| 
						
						
							
						
						488055955c
	
				 | 
					
					
						|||
| 
						 | 
					313ebc64cc | ||
| 
						 | 
					1ed8b1d0c1 | ||
| 4af816d931 | |||
| 1c058a4323 | |||
| 461ed1fcda | |||
| 
						
						
							
						
						5363afa558
	
				 | 
					
					
						|||
| 
						
						
							
						
						f0d2737da8
	
				 | 
					
					
						|||
| 
						
						
							
						
						1f2f80aa3e
	
				 | 
					
					
						|||
| 
						
						
							
						
						240a872e65
	
				 | 
					
					
						|||
| c1ec6f0849 | |||
| ab42686d4d | 
@@ -447,6 +447,8 @@
 | 
				
			|||||||
  "lastActiveAt": "Last active at {}",
 | 
					  "lastActiveAt": "Last active at {}",
 | 
				
			||||||
  "authDeviceLogout": "Logout",
 | 
					  "authDeviceLogout": "Logout",
 | 
				
			||||||
  "authDeviceLogoutHint": "Are you sure you want to logout this device? This will also disable the push notification to this device.",
 | 
					  "authDeviceLogoutHint": "Are you sure you want to logout this device? This will also disable the push notification to this device.",
 | 
				
			||||||
 | 
					  "authDeviceChallenges": "Device Usage",
 | 
				
			||||||
 | 
					  "authDeviceHint": "Swipe left to edit label, swipe right to logout device.",
 | 
				
			||||||
  "typingHint": {
 | 
					  "typingHint": {
 | 
				
			||||||
    "one": "{} is typing...",
 | 
					    "one": "{} is typing...",
 | 
				
			||||||
    "other": "{} are typing..."
 | 
					    "other": "{} are typing..."
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -149,9 +149,9 @@ PODS:
 | 
				
			|||||||
  - flutter_udid (0.0.1):
 | 
					  - flutter_udid (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - SAMKeychain
 | 
					    - SAMKeychain
 | 
				
			||||||
  - flutter_webrtc (1.1.0):
 | 
					  - flutter_webrtc (1.2.0):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - WebRTC-SDK (= 137.7151.03)
 | 
					    - WebRTC-SDK (= 137.7151.04)
 | 
				
			||||||
  - gal (1.0.0):
 | 
					  - gal (1.0.0):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
@@ -219,7 +219,7 @@ PODS:
 | 
				
			|||||||
  - livekit_client (2.5.0):
 | 
					  - livekit_client (2.5.0):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - flutter_webrtc
 | 
					    - flutter_webrtc
 | 
				
			||||||
    - WebRTC-SDK (= 137.7151.03)
 | 
					    - WebRTC-SDK (= 137.7151.04)
 | 
				
			||||||
  - local_auth_darwin (0.0.1):
 | 
					  - local_auth_darwin (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
@@ -299,7 +299,7 @@ PODS:
 | 
				
			|||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - wakelock_plus (0.0.1):
 | 
					  - wakelock_plus (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - WebRTC-SDK (137.7151.03)
 | 
					  - WebRTC-SDK (137.7151.04)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
DEPENDENCIES:
 | 
					DEPENDENCIES:
 | 
				
			||||||
  - Alamofire
 | 
					  - Alamofire
 | 
				
			||||||
@@ -499,7 +499,7 @@ SPEC CHECKSUMS:
 | 
				
			|||||||
  flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
 | 
					  flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
 | 
				
			||||||
  flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
 | 
					  flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
 | 
				
			||||||
  flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
 | 
					  flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
 | 
				
			||||||
  flutter_webrtc: b0b2e04411747142962164a1cfa43a1af9a0afac
 | 
					  flutter_webrtc: c3e21fc0dcd9d8eb246ae4d5256fcbeb2f5ecd22
 | 
				
			||||||
  gal: baecd024ebfd13c441269ca7404792a7152fde89
 | 
					  gal: baecd024ebfd13c441269ca7404792a7152fde89
 | 
				
			||||||
  GoogleAdsOnDeviceConversion: 9090c435cde08903e8dd1ba2c77fbec9e46d9afe
 | 
					  GoogleAdsOnDeviceConversion: 9090c435cde08903e8dd1ba2c77fbec9e46d9afe
 | 
				
			||||||
  GoogleAppMeasurement: 09f341dfa8527d1612a09cbfe809a242c0b737af
 | 
					  GoogleAppMeasurement: 09f341dfa8527d1612a09cbfe809a242c0b737af
 | 
				
			||||||
@@ -508,8 +508,8 @@ SPEC CHECKSUMS:
 | 
				
			|||||||
  image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
 | 
					  image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
 | 
				
			||||||
  irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
 | 
					  irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
 | 
				
			||||||
  Kingfisher: ff0d31a1f07bdff6a1ebb3ba08b8e6e567b6500c
 | 
					  Kingfisher: ff0d31a1f07bdff6a1ebb3ba08b8e6e567b6500c
 | 
				
			||||||
  livekit_client: f810c81bbbc229a84f60b09e66603ac4e93f7599
 | 
					  livekit_client: a6f5fa86ac28ccd7ded53626a5379961db311ab4
 | 
				
			||||||
  local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
 | 
					  local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
 | 
				
			||||||
  media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
 | 
					  media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
 | 
				
			||||||
  media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
 | 
					  media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
 | 
				
			||||||
  nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
 | 
					  nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
 | 
				
			||||||
@@ -536,7 +536,7 @@ SPEC CHECKSUMS:
 | 
				
			|||||||
  url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
 | 
					  url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
 | 
				
			||||||
  volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
 | 
					  volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
 | 
				
			||||||
  wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
 | 
					  wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
 | 
				
			||||||
  WebRTC-SDK: 69d4e56b0b4b27d788e87bab9b9a1326ed05b1e3
 | 
					  WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
PODFILE CHECKSUM: c818292390b02fa379036ea099713a332bd7193f
 | 
					PODFILE CHECKSUM: c818292390b02fa379036ea099713a332bd7193f
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -566,7 +566,7 @@
 | 
				
			|||||||
			);
 | 
								);
 | 
				
			||||||
			runOnlyForDeploymentPostprocessing = 0;
 | 
								runOnlyForDeploymentPostprocessing = 0;
 | 
				
			||||||
			shellPath = /bin/sh;
 | 
								shellPath = /bin/sh;
 | 
				
			||||||
			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
 | 
								shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n";
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
		4815E0A19398E51078F4160D /* [CP] Check Pods Manifest.lock */ = {
 | 
							4815E0A19398E51078F4160D /* [CP] Check Pods Manifest.lock */ = {
 | 
				
			||||||
			isa = PBXShellScriptBuildPhase;
 | 
								isa = PBXShellScriptBuildPhase;
 | 
				
			||||||
@@ -883,6 +883,7 @@
 | 
				
			|||||||
				);
 | 
									);
 | 
				
			||||||
				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
 | 
									PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
 | 
				
			||||||
				PRODUCT_NAME = "$(TARGET_NAME)";
 | 
									PRODUCT_NAME = "$(TARGET_NAME)";
 | 
				
			||||||
 | 
									SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)";
 | 
				
			||||||
				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
 | 
									SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
 | 
				
			||||||
				SWIFT_VERSION = 5.0;
 | 
									SWIFT_VERSION = 5.0;
 | 
				
			||||||
				VERSIONING_SYSTEM = "apple-generic";
 | 
									VERSIONING_SYSTEM = "apple-generic";
 | 
				
			||||||
@@ -1096,6 +1097,7 @@
 | 
				
			|||||||
				SKIP_INSTALL = YES;
 | 
									SKIP_INSTALL = YES;
 | 
				
			||||||
				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
 | 
									SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
 | 
				
			||||||
				SWIFT_EMIT_LOC_STRINGS = YES;
 | 
									SWIFT_EMIT_LOC_STRINGS = YES;
 | 
				
			||||||
 | 
									SWIFT_ENABLE_EXPLICIT_MODULES = NO;
 | 
				
			||||||
				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
 | 
									SWIFT_OPTIMIZATION_LEVEL = "-Onone";
 | 
				
			||||||
				SWIFT_VERSION = 5.0;
 | 
									SWIFT_VERSION = 5.0;
 | 
				
			||||||
				TARGETED_DEVICE_FAMILY = "1,2";
 | 
									TARGETED_DEVICE_FAMILY = "1,2";
 | 
				
			||||||
@@ -1137,6 +1139,7 @@
 | 
				
			|||||||
				PRODUCT_NAME = "$(TARGET_NAME)";
 | 
									PRODUCT_NAME = "$(TARGET_NAME)";
 | 
				
			||||||
				SKIP_INSTALL = YES;
 | 
									SKIP_INSTALL = YES;
 | 
				
			||||||
				SWIFT_EMIT_LOC_STRINGS = YES;
 | 
									SWIFT_EMIT_LOC_STRINGS = YES;
 | 
				
			||||||
 | 
									SWIFT_ENABLE_EXPLICIT_MODULES = NO;
 | 
				
			||||||
				SWIFT_VERSION = 5.0;
 | 
									SWIFT_VERSION = 5.0;
 | 
				
			||||||
				TARGETED_DEVICE_FAMILY = "1,2";
 | 
									TARGETED_DEVICE_FAMILY = "1,2";
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
@@ -1177,6 +1180,7 @@
 | 
				
			|||||||
				PRODUCT_NAME = "$(TARGET_NAME)";
 | 
									PRODUCT_NAME = "$(TARGET_NAME)";
 | 
				
			||||||
				SKIP_INSTALL = YES;
 | 
									SKIP_INSTALL = YES;
 | 
				
			||||||
				SWIFT_EMIT_LOC_STRINGS = YES;
 | 
									SWIFT_EMIT_LOC_STRINGS = YES;
 | 
				
			||||||
 | 
									SWIFT_ENABLE_EXPLICIT_MODULES = NO;
 | 
				
			||||||
				SWIFT_VERSION = 5.0;
 | 
									SWIFT_VERSION = 5.0;
 | 
				
			||||||
				TARGETED_DEVICE_FAMILY = "1,2";
 | 
									TARGETED_DEVICE_FAMILY = "1,2";
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
@@ -1434,6 +1438,7 @@
 | 
				
			|||||||
				);
 | 
									);
 | 
				
			||||||
				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
 | 
									PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
 | 
				
			||||||
				PRODUCT_NAME = "$(TARGET_NAME)";
 | 
									PRODUCT_NAME = "$(TARGET_NAME)";
 | 
				
			||||||
 | 
									SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)";
 | 
				
			||||||
				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
 | 
									SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
 | 
				
			||||||
				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
 | 
									SWIFT_OPTIMIZATION_LEVEL = "-Onone";
 | 
				
			||||||
				SWIFT_VERSION = 5.0;
 | 
									SWIFT_VERSION = 5.0;
 | 
				
			||||||
@@ -1462,6 +1467,7 @@
 | 
				
			|||||||
				);
 | 
									);
 | 
				
			||||||
				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
 | 
									PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
 | 
				
			||||||
				PRODUCT_NAME = "$(TARGET_NAME)";
 | 
									PRODUCT_NAME = "$(TARGET_NAME)";
 | 
				
			||||||
 | 
									SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)";
 | 
				
			||||||
				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
 | 
									SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
 | 
				
			||||||
				SWIFT_VERSION = 5.0;
 | 
									SWIFT_VERSION = 5.0;
 | 
				
			||||||
				VERSIONING_SYSTEM = "apple-generic";
 | 
									VERSIONING_SYSTEM = "apple-generic";
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -181,7 +181,7 @@ class IslandApp extends HookConsumerWidget {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() {
 | 
					    useEffect(() {
 | 
				
			||||||
      if (!kIsWeb && Platform.isLinux) {
 | 
					      if (!kIsWeb && (Platform.isLinux || Platform.isWindows)) {
 | 
				
			||||||
        return null;
 | 
					        return null;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,11 +14,11 @@ sealed class AppToken with _$AppToken {
 | 
				
			|||||||
@freezed
 | 
					@freezed
 | 
				
			||||||
sealed class GeoIpLocation with _$GeoIpLocation {
 | 
					sealed class GeoIpLocation with _$GeoIpLocation {
 | 
				
			||||||
  const factory GeoIpLocation({
 | 
					  const factory GeoIpLocation({
 | 
				
			||||||
    required double latitude,
 | 
					    required double? latitude,
 | 
				
			||||||
    required double longitude,
 | 
					    required double? longitude,
 | 
				
			||||||
    required String countryCode,
 | 
					    required String? countryCode,
 | 
				
			||||||
    required String country,
 | 
					    required String? country,
 | 
				
			||||||
    required String city,
 | 
					    required String? city,
 | 
				
			||||||
  }) = _GeoIpLocation;
 | 
					  }) = _GeoIpLocation;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory GeoIpLocation.fromJson(Map<String, dynamic> json) =>
 | 
					  factory GeoIpLocation.fromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
@@ -29,7 +29,7 @@ sealed class GeoIpLocation with _$GeoIpLocation {
 | 
				
			|||||||
sealed class SnAuthChallenge with _$SnAuthChallenge {
 | 
					sealed class SnAuthChallenge with _$SnAuthChallenge {
 | 
				
			||||||
  const factory SnAuthChallenge({
 | 
					  const factory SnAuthChallenge({
 | 
				
			||||||
    required String id,
 | 
					    required String id,
 | 
				
			||||||
    required DateTime expiredAt,
 | 
					    required DateTime? expiredAt,
 | 
				
			||||||
    required int stepRemain,
 | 
					    required int stepRemain,
 | 
				
			||||||
    required int stepTotal,
 | 
					    required int stepTotal,
 | 
				
			||||||
    required int failedAttempts,
 | 
					    required int failedAttempts,
 | 
				
			||||||
@@ -57,7 +57,7 @@ sealed class SnAuthSession with _$SnAuthSession {
 | 
				
			|||||||
    required String id,
 | 
					    required String id,
 | 
				
			||||||
    required String? label,
 | 
					    required String? label,
 | 
				
			||||||
    required DateTime lastGrantedAt,
 | 
					    required DateTime lastGrantedAt,
 | 
				
			||||||
    required DateTime expiredAt,
 | 
					    required DateTime? expiredAt,
 | 
				
			||||||
    required String accountId,
 | 
					    required String accountId,
 | 
				
			||||||
    required String challengeId,
 | 
					    required String challengeId,
 | 
				
			||||||
    required SnAuthChallenge challenge,
 | 
					    required SnAuthChallenge challenge,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -272,7 +272,7 @@ as String,
 | 
				
			|||||||
/// @nodoc
 | 
					/// @nodoc
 | 
				
			||||||
mixin _$GeoIpLocation {
 | 
					mixin _$GeoIpLocation {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 double get latitude; double get longitude; String get countryCode; String get country; String get city;
 | 
					 double? get latitude; double? get longitude; String? get countryCode; String? get country; String? get city;
 | 
				
			||||||
/// Create a copy of GeoIpLocation
 | 
					/// Create a copy of GeoIpLocation
 | 
				
			||||||
/// 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)
 | 
				
			||||||
@@ -305,7 +305,7 @@ abstract mixin class $GeoIpLocationCopyWith<$Res>  {
 | 
				
			|||||||
  factory $GeoIpLocationCopyWith(GeoIpLocation value, $Res Function(GeoIpLocation) _then) = _$GeoIpLocationCopyWithImpl;
 | 
					  factory $GeoIpLocationCopyWith(GeoIpLocation value, $Res Function(GeoIpLocation) _then) = _$GeoIpLocationCopyWithImpl;
 | 
				
			||||||
@useResult
 | 
					@useResult
 | 
				
			||||||
$Res call({
 | 
					$Res call({
 | 
				
			||||||
 double latitude, double longitude, String countryCode, String country, String city
 | 
					 double? latitude, double? longitude, String? countryCode, String? country, String? city
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -322,14 +322,14 @@ class _$GeoIpLocationCopyWithImpl<$Res>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/// Create a copy of GeoIpLocation
 | 
					/// Create a copy of GeoIpLocation
 | 
				
			||||||
/// 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? latitude = null,Object? longitude = null,Object? countryCode = null,Object? country = null,Object? city = null,}) {
 | 
					@pragma('vm:prefer-inline') @override $Res call({Object? latitude = freezed,Object? longitude = freezed,Object? countryCode = freezed,Object? country = freezed,Object? city = freezed,}) {
 | 
				
			||||||
  return _then(_self.copyWith(
 | 
					  return _then(_self.copyWith(
 | 
				
			||||||
latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable
 | 
					latitude: freezed == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as double,longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable
 | 
					as double?,longitude: freezed == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as double,countryCode: null == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
 | 
					as double?,countryCode: freezed == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,country: null == country ? _self.country : country // ignore: cast_nullable_to_non_nullable
 | 
					as String?,country: freezed == country ? _self.country : country // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,city: null == city ? _self.city : city // ignore: cast_nullable_to_non_nullable
 | 
					as String?,city: freezed == city ? _self.city : city // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,
 | 
					as String?,
 | 
				
			||||||
  ));
 | 
					  ));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -411,7 +411,7 @@ return $default(_that);case _:
 | 
				
			|||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( double latitude,  double longitude,  String countryCode,  String country,  String city)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
					@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( double? latitude,  double? longitude,  String? countryCode,  String? country,  String? city)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
				
			||||||
switch (_that) {
 | 
					switch (_that) {
 | 
				
			||||||
case _GeoIpLocation() when $default != null:
 | 
					case _GeoIpLocation() when $default != null:
 | 
				
			||||||
return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);case _:
 | 
					return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);case _:
 | 
				
			||||||
@@ -432,7 +432,7 @@ return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_
 | 
				
			|||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( double latitude,  double longitude,  String countryCode,  String country,  String city)  $default,) {final _that = this;
 | 
					@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( double? latitude,  double? longitude,  String? countryCode,  String? country,  String? city)  $default,) {final _that = this;
 | 
				
			||||||
switch (_that) {
 | 
					switch (_that) {
 | 
				
			||||||
case _GeoIpLocation():
 | 
					case _GeoIpLocation():
 | 
				
			||||||
return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);}
 | 
					return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);}
 | 
				
			||||||
@@ -449,7 +449,7 @@ return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_
 | 
				
			|||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( double latitude,  double longitude,  String countryCode,  String country,  String city)?  $default,) {final _that = this;
 | 
					@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( double? latitude,  double? longitude,  String? countryCode,  String? country,  String? city)?  $default,) {final _that = this;
 | 
				
			||||||
switch (_that) {
 | 
					switch (_that) {
 | 
				
			||||||
case _GeoIpLocation() when $default != null:
 | 
					case _GeoIpLocation() when $default != null:
 | 
				
			||||||
return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);case _:
 | 
					return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);case _:
 | 
				
			||||||
@@ -467,11 +467,11 @@ class _GeoIpLocation implements GeoIpLocation {
 | 
				
			|||||||
  const _GeoIpLocation({required this.latitude, required this.longitude, required this.countryCode, required this.country, required this.city});
 | 
					  const _GeoIpLocation({required this.latitude, required this.longitude, required this.countryCode, required this.country, required this.city});
 | 
				
			||||||
  factory _GeoIpLocation.fromJson(Map<String, dynamic> json) => _$GeoIpLocationFromJson(json);
 | 
					  factory _GeoIpLocation.fromJson(Map<String, dynamic> json) => _$GeoIpLocationFromJson(json);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@override final  double latitude;
 | 
					@override final  double? latitude;
 | 
				
			||||||
@override final  double longitude;
 | 
					@override final  double? longitude;
 | 
				
			||||||
@override final  String countryCode;
 | 
					@override final  String? countryCode;
 | 
				
			||||||
@override final  String country;
 | 
					@override final  String? country;
 | 
				
			||||||
@override final  String city;
 | 
					@override final  String? city;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Create a copy of GeoIpLocation
 | 
					/// Create a copy of GeoIpLocation
 | 
				
			||||||
/// with the given fields replaced by the non-null parameter values.
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
@@ -506,7 +506,7 @@ abstract mixin class _$GeoIpLocationCopyWith<$Res> implements $GeoIpLocationCopy
 | 
				
			|||||||
  factory _$GeoIpLocationCopyWith(_GeoIpLocation value, $Res Function(_GeoIpLocation) _then) = __$GeoIpLocationCopyWithImpl;
 | 
					  factory _$GeoIpLocationCopyWith(_GeoIpLocation value, $Res Function(_GeoIpLocation) _then) = __$GeoIpLocationCopyWithImpl;
 | 
				
			||||||
@override @useResult
 | 
					@override @useResult
 | 
				
			||||||
$Res call({
 | 
					$Res call({
 | 
				
			||||||
 double latitude, double longitude, String countryCode, String country, String city
 | 
					 double? latitude, double? longitude, String? countryCode, String? country, String? city
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -523,14 +523,14 @@ class __$GeoIpLocationCopyWithImpl<$Res>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/// Create a copy of GeoIpLocation
 | 
					/// Create a copy of GeoIpLocation
 | 
				
			||||||
/// 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? latitude = null,Object? longitude = null,Object? countryCode = null,Object? country = null,Object? city = null,}) {
 | 
					@override @pragma('vm:prefer-inline') $Res call({Object? latitude = freezed,Object? longitude = freezed,Object? countryCode = freezed,Object? country = freezed,Object? city = freezed,}) {
 | 
				
			||||||
  return _then(_GeoIpLocation(
 | 
					  return _then(_GeoIpLocation(
 | 
				
			||||||
latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable
 | 
					latitude: freezed == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as double,longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable
 | 
					as double?,longitude: freezed == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as double,countryCode: null == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
 | 
					as double?,countryCode: freezed == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,country: null == country ? _self.country : country // ignore: cast_nullable_to_non_nullable
 | 
					as String?,country: freezed == country ? _self.country : country // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,city: null == city ? _self.city : city // ignore: cast_nullable_to_non_nullable
 | 
					as String?,city: freezed == city ? _self.city : city // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,
 | 
					as String?,
 | 
				
			||||||
  ));
 | 
					  ));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -541,7 +541,7 @@ as String,
 | 
				
			|||||||
/// @nodoc
 | 
					/// @nodoc
 | 
				
			||||||
mixin _$SnAuthChallenge {
 | 
					mixin _$SnAuthChallenge {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 String get id; DateTime get expiredAt; int get stepRemain; int get stepTotal; int get failedAttempts; int get type; List<String> get blacklistFactors; List<dynamic> get audiences; List<dynamic> get scopes; String get ipAddress; String get userAgent; String? get nonce; GeoIpLocation? get location; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
 | 
					 String get id; DateTime? get expiredAt; int get stepRemain; int get stepTotal; int get failedAttempts; int get type; List<String> get blacklistFactors; List<dynamic> get audiences; List<dynamic> get scopes; String get ipAddress; String get userAgent; String? get nonce; GeoIpLocation? get location; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
 | 
				
			||||||
/// Create a copy of SnAuthChallenge
 | 
					/// Create a copy of SnAuthChallenge
 | 
				
			||||||
/// 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)
 | 
				
			||||||
@@ -574,7 +574,7 @@ abstract mixin class $SnAuthChallengeCopyWith<$Res>  {
 | 
				
			|||||||
  factory $SnAuthChallengeCopyWith(SnAuthChallenge value, $Res Function(SnAuthChallenge) _then) = _$SnAuthChallengeCopyWithImpl;
 | 
					  factory $SnAuthChallengeCopyWith(SnAuthChallenge value, $Res Function(SnAuthChallenge) _then) = _$SnAuthChallengeCopyWithImpl;
 | 
				
			||||||
@useResult
 | 
					@useResult
 | 
				
			||||||
$Res call({
 | 
					$Res call({
 | 
				
			||||||
 String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
 | 
					 String id, DateTime? expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -591,11 +591,11 @@ class _$SnAuthChallengeCopyWithImpl<$Res>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/// Create a copy of SnAuthChallenge
 | 
					/// Create a copy of SnAuthChallenge
 | 
				
			||||||
/// 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? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
 | 
					@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? expiredAt = freezed,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = 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,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
 | 
					as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable
 | 
					as DateTime?,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable
 | 
					as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable
 | 
					as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
 | 
					as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
@@ -704,7 +704,7 @@ return $default(_that);case _:
 | 
				
			|||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  DateTime expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  GeoIpLocation? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
					@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  DateTime? expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  GeoIpLocation? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
				
			||||||
switch (_that) {
 | 
					switch (_that) {
 | 
				
			||||||
case _SnAuthChallenge() when $default != null:
 | 
					case _SnAuthChallenge() when $default != null:
 | 
				
			||||||
return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
 | 
					return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
 | 
				
			||||||
@@ -725,7 +725,7 @@ return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.
 | 
				
			|||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  DateTime expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  GeoIpLocation? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this;
 | 
					@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  DateTime? expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  GeoIpLocation? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this;
 | 
				
			||||||
switch (_that) {
 | 
					switch (_that) {
 | 
				
			||||||
case _SnAuthChallenge():
 | 
					case _SnAuthChallenge():
 | 
				
			||||||
return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);}
 | 
					return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);}
 | 
				
			||||||
@@ -742,7 +742,7 @@ return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.
 | 
				
			|||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  DateTime expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  GeoIpLocation? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this;
 | 
					@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  DateTime? expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  GeoIpLocation? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this;
 | 
				
			||||||
switch (_that) {
 | 
					switch (_that) {
 | 
				
			||||||
case _SnAuthChallenge() when $default != null:
 | 
					case _SnAuthChallenge() when $default != null:
 | 
				
			||||||
return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
 | 
					return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
 | 
				
			||||||
@@ -761,7 +761,7 @@ class _SnAuthChallenge implements SnAuthChallenge {
 | 
				
			|||||||
  factory _SnAuthChallenge.fromJson(Map<String, dynamic> json) => _$SnAuthChallengeFromJson(json);
 | 
					  factory _SnAuthChallenge.fromJson(Map<String, dynamic> json) => _$SnAuthChallengeFromJson(json);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@override final  String id;
 | 
					@override final  String id;
 | 
				
			||||||
@override final  DateTime expiredAt;
 | 
					@override final  DateTime? expiredAt;
 | 
				
			||||||
@override final  int stepRemain;
 | 
					@override final  int stepRemain;
 | 
				
			||||||
@override final  int stepTotal;
 | 
					@override final  int stepTotal;
 | 
				
			||||||
@override final  int failedAttempts;
 | 
					@override final  int failedAttempts;
 | 
				
			||||||
@@ -829,7 +829,7 @@ abstract mixin class _$SnAuthChallengeCopyWith<$Res> implements $SnAuthChallenge
 | 
				
			|||||||
  factory _$SnAuthChallengeCopyWith(_SnAuthChallenge value, $Res Function(_SnAuthChallenge) _then) = __$SnAuthChallengeCopyWithImpl;
 | 
					  factory _$SnAuthChallengeCopyWith(_SnAuthChallenge value, $Res Function(_SnAuthChallenge) _then) = __$SnAuthChallengeCopyWithImpl;
 | 
				
			||||||
@override @useResult
 | 
					@override @useResult
 | 
				
			||||||
$Res call({
 | 
					$Res call({
 | 
				
			||||||
 String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
 | 
					 String id, DateTime? expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -846,11 +846,11 @@ class __$SnAuthChallengeCopyWithImpl<$Res>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/// Create a copy of SnAuthChallenge
 | 
					/// Create a copy of SnAuthChallenge
 | 
				
			||||||
/// 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? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
 | 
					@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? expiredAt = freezed,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
 | 
				
			||||||
  return _then(_SnAuthChallenge(
 | 
					  return _then(_SnAuthChallenge(
 | 
				
			||||||
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,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
 | 
					as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable
 | 
					as DateTime?,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable
 | 
					as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable
 | 
					as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
 | 
					as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
@@ -888,7 +888,7 @@ $GeoIpLocationCopyWith<$Res>? get location {
 | 
				
			|||||||
/// @nodoc
 | 
					/// @nodoc
 | 
				
			||||||
mixin _$SnAuthSession {
 | 
					mixin _$SnAuthSession {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 String get id; String? get label; DateTime get lastGrantedAt; DateTime get expiredAt; String get accountId; String get challengeId; SnAuthChallenge get challenge; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
 | 
					 String get id; String? get label; DateTime get lastGrantedAt; DateTime? get expiredAt; String get accountId; String get challengeId; SnAuthChallenge get challenge; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
 | 
				
			||||||
/// Create a copy of SnAuthSession
 | 
					/// Create a copy of SnAuthSession
 | 
				
			||||||
/// 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)
 | 
				
			||||||
@@ -921,7 +921,7 @@ abstract mixin class $SnAuthSessionCopyWith<$Res>  {
 | 
				
			|||||||
  factory $SnAuthSessionCopyWith(SnAuthSession value, $Res Function(SnAuthSession) _then) = _$SnAuthSessionCopyWithImpl;
 | 
					  factory $SnAuthSessionCopyWith(SnAuthSession value, $Res Function(SnAuthSession) _then) = _$SnAuthSessionCopyWithImpl;
 | 
				
			||||||
@useResult
 | 
					@useResult
 | 
				
			||||||
$Res call({
 | 
					$Res call({
 | 
				
			||||||
 String id, String? label, DateTime lastGrantedAt, DateTime expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
 | 
					 String id, String? label, DateTime lastGrantedAt, DateTime? expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -938,13 +938,13 @@ class _$SnAuthSessionCopyWithImpl<$Res>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/// Create a copy of SnAuthSession
 | 
					/// Create a copy of SnAuthSession
 | 
				
			||||||
/// 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? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = null,Object? accountId = null,Object? challengeId = null,Object? challenge = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
 | 
					@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = freezed,Object? accountId = null,Object? challengeId = null,Object? challenge = 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,label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
 | 
					as String,label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String?,lastGrantedAt: null == lastGrantedAt ? _self.lastGrantedAt : lastGrantedAt // ignore: cast_nullable_to_non_nullable
 | 
					as String?,lastGrantedAt: null == lastGrantedAt ? _self.lastGrantedAt : lastGrantedAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as DateTime,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
 | 
					as DateTime,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as DateTime,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
 | 
					as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,challengeId: null == challengeId ? _self.challengeId : challengeId // ignore: cast_nullable_to_non_nullable
 | 
					as String,challengeId: null == challengeId ? _self.challengeId : challengeId // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,challenge: null == challenge ? _self.challenge : challenge // ignore: cast_nullable_to_non_nullable
 | 
					as String,challenge: null == challenge ? _self.challenge : challenge // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as SnAuthChallenge,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
 | 
					as SnAuthChallenge,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
@@ -1041,7 +1041,7 @@ return $default(_that);case _:
 | 
				
			|||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String? label,  DateTime lastGrantedAt,  DateTime expiredAt,  String accountId,  String challengeId,  SnAuthChallenge challenge,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
					@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String? label,  DateTime lastGrantedAt,  DateTime? expiredAt,  String accountId,  String challengeId,  SnAuthChallenge challenge,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
				
			||||||
switch (_that) {
 | 
					switch (_that) {
 | 
				
			||||||
case _SnAuthSession() when $default != null:
 | 
					case _SnAuthSession() when $default != null:
 | 
				
			||||||
return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
 | 
					return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
 | 
				
			||||||
@@ -1062,7 +1062,7 @@ return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.a
 | 
				
			|||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String? label,  DateTime lastGrantedAt,  DateTime expiredAt,  String accountId,  String challengeId,  SnAuthChallenge challenge,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this;
 | 
					@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String? label,  DateTime lastGrantedAt,  DateTime? expiredAt,  String accountId,  String challengeId,  SnAuthChallenge challenge,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this;
 | 
				
			||||||
switch (_that) {
 | 
					switch (_that) {
 | 
				
			||||||
case _SnAuthSession():
 | 
					case _SnAuthSession():
 | 
				
			||||||
return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);}
 | 
					return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);}
 | 
				
			||||||
@@ -1079,7 +1079,7 @@ return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.a
 | 
				
			|||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String? label,  DateTime lastGrantedAt,  DateTime expiredAt,  String accountId,  String challengeId,  SnAuthChallenge challenge,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this;
 | 
					@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String? label,  DateTime lastGrantedAt,  DateTime? expiredAt,  String accountId,  String challengeId,  SnAuthChallenge challenge,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this;
 | 
				
			||||||
switch (_that) {
 | 
					switch (_that) {
 | 
				
			||||||
case _SnAuthSession() when $default != null:
 | 
					case _SnAuthSession() when $default != null:
 | 
				
			||||||
return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
 | 
					return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
 | 
				
			||||||
@@ -1100,7 +1100,7 @@ class _SnAuthSession implements SnAuthSession {
 | 
				
			|||||||
@override final  String id;
 | 
					@override final  String id;
 | 
				
			||||||
@override final  String? label;
 | 
					@override final  String? label;
 | 
				
			||||||
@override final  DateTime lastGrantedAt;
 | 
					@override final  DateTime lastGrantedAt;
 | 
				
			||||||
@override final  DateTime expiredAt;
 | 
					@override final  DateTime? expiredAt;
 | 
				
			||||||
@override final  String accountId;
 | 
					@override final  String accountId;
 | 
				
			||||||
@override final  String challengeId;
 | 
					@override final  String challengeId;
 | 
				
			||||||
@override final  SnAuthChallenge challenge;
 | 
					@override final  SnAuthChallenge challenge;
 | 
				
			||||||
@@ -1141,7 +1141,7 @@ abstract mixin class _$SnAuthSessionCopyWith<$Res> implements $SnAuthSessionCopy
 | 
				
			|||||||
  factory _$SnAuthSessionCopyWith(_SnAuthSession value, $Res Function(_SnAuthSession) _then) = __$SnAuthSessionCopyWithImpl;
 | 
					  factory _$SnAuthSessionCopyWith(_SnAuthSession value, $Res Function(_SnAuthSession) _then) = __$SnAuthSessionCopyWithImpl;
 | 
				
			||||||
@override @useResult
 | 
					@override @useResult
 | 
				
			||||||
$Res call({
 | 
					$Res call({
 | 
				
			||||||
 String id, String? label, DateTime lastGrantedAt, DateTime expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
 | 
					 String id, String? label, DateTime lastGrantedAt, DateTime? expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1158,13 +1158,13 @@ class __$SnAuthSessionCopyWithImpl<$Res>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/// Create a copy of SnAuthSession
 | 
					/// Create a copy of SnAuthSession
 | 
				
			||||||
/// 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? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = null,Object? accountId = null,Object? challengeId = null,Object? challenge = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
 | 
					@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = freezed,Object? accountId = null,Object? challengeId = null,Object? challenge = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
 | 
				
			||||||
  return _then(_SnAuthSession(
 | 
					  return _then(_SnAuthSession(
 | 
				
			||||||
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,label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
 | 
					as String,label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String?,lastGrantedAt: null == lastGrantedAt ? _self.lastGrantedAt : lastGrantedAt // ignore: cast_nullable_to_non_nullable
 | 
					as String?,lastGrantedAt: null == lastGrantedAt ? _self.lastGrantedAt : lastGrantedAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as DateTime,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
 | 
					as DateTime,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as DateTime,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
 | 
					as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,challengeId: null == challengeId ? _self.challengeId : challengeId // ignore: cast_nullable_to_non_nullable
 | 
					as String,challengeId: null == challengeId ? _self.challengeId : challengeId // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,challenge: null == challenge ? _self.challenge : challenge // ignore: cast_nullable_to_non_nullable
 | 
					as String,challenge: null == challenge ? _self.challenge : challenge // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as SnAuthChallenge,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
 | 
					as SnAuthChallenge,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,11 +15,11 @@ Map<String, dynamic> _$AppTokenToJson(_AppToken instance) => <String, dynamic>{
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
_GeoIpLocation _$GeoIpLocationFromJson(Map<String, dynamic> json) =>
 | 
					_GeoIpLocation _$GeoIpLocationFromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
    _GeoIpLocation(
 | 
					    _GeoIpLocation(
 | 
				
			||||||
      latitude: (json['latitude'] as num).toDouble(),
 | 
					      latitude: (json['latitude'] as num?)?.toDouble(),
 | 
				
			||||||
      longitude: (json['longitude'] as num).toDouble(),
 | 
					      longitude: (json['longitude'] as num?)?.toDouble(),
 | 
				
			||||||
      countryCode: json['country_code'] as String,
 | 
					      countryCode: json['country_code'] as String?,
 | 
				
			||||||
      country: json['country'] as String,
 | 
					      country: json['country'] as String?,
 | 
				
			||||||
      city: json['city'] as String,
 | 
					      city: json['city'] as String?,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Map<String, dynamic> _$GeoIpLocationToJson(_GeoIpLocation instance) =>
 | 
					Map<String, dynamic> _$GeoIpLocationToJson(_GeoIpLocation instance) =>
 | 
				
			||||||
@@ -34,7 +34,10 @@ Map<String, dynamic> _$GeoIpLocationToJson(_GeoIpLocation instance) =>
 | 
				
			|||||||
_SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) =>
 | 
					_SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
    _SnAuthChallenge(
 | 
					    _SnAuthChallenge(
 | 
				
			||||||
      id: json['id'] as String,
 | 
					      id: json['id'] as String,
 | 
				
			||||||
      expiredAt: DateTime.parse(json['expired_at'] as String),
 | 
					      expiredAt:
 | 
				
			||||||
 | 
					          json['expired_at'] == null
 | 
				
			||||||
 | 
					              ? null
 | 
				
			||||||
 | 
					              : DateTime.parse(json['expired_at'] as String),
 | 
				
			||||||
      stepRemain: (json['step_remain'] as num).toInt(),
 | 
					      stepRemain: (json['step_remain'] as num).toInt(),
 | 
				
			||||||
      stepTotal: (json['step_total'] as num).toInt(),
 | 
					      stepTotal: (json['step_total'] as num).toInt(),
 | 
				
			||||||
      failedAttempts: (json['failed_attempts'] as num).toInt(),
 | 
					      failedAttempts: (json['failed_attempts'] as num).toInt(),
 | 
				
			||||||
@@ -66,7 +69,7 @@ _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) =>
 | 
				
			|||||||
Map<String, dynamic> _$SnAuthChallengeToJson(_SnAuthChallenge instance) =>
 | 
					Map<String, dynamic> _$SnAuthChallengeToJson(_SnAuthChallenge instance) =>
 | 
				
			||||||
    <String, dynamic>{
 | 
					    <String, dynamic>{
 | 
				
			||||||
      'id': instance.id,
 | 
					      'id': instance.id,
 | 
				
			||||||
      'expired_at': instance.expiredAt.toIso8601String(),
 | 
					      'expired_at': instance.expiredAt?.toIso8601String(),
 | 
				
			||||||
      'step_remain': instance.stepRemain,
 | 
					      'step_remain': instance.stepRemain,
 | 
				
			||||||
      'step_total': instance.stepTotal,
 | 
					      'step_total': instance.stepTotal,
 | 
				
			||||||
      'failed_attempts': instance.failedAttempts,
 | 
					      'failed_attempts': instance.failedAttempts,
 | 
				
			||||||
@@ -89,7 +92,10 @@ _SnAuthSession _$SnAuthSessionFromJson(Map<String, dynamic> json) =>
 | 
				
			|||||||
      id: json['id'] as String,
 | 
					      id: json['id'] as String,
 | 
				
			||||||
      label: json['label'] as String?,
 | 
					      label: json['label'] as String?,
 | 
				
			||||||
      lastGrantedAt: DateTime.parse(json['last_granted_at'] as String),
 | 
					      lastGrantedAt: DateTime.parse(json['last_granted_at'] as String),
 | 
				
			||||||
      expiredAt: DateTime.parse(json['expired_at'] as String),
 | 
					      expiredAt:
 | 
				
			||||||
 | 
					          json['expired_at'] == null
 | 
				
			||||||
 | 
					              ? null
 | 
				
			||||||
 | 
					              : DateTime.parse(json['expired_at'] as String),
 | 
				
			||||||
      accountId: json['account_id'] as String,
 | 
					      accountId: json['account_id'] as String,
 | 
				
			||||||
      challengeId: json['challenge_id'] as String,
 | 
					      challengeId: json['challenge_id'] as String,
 | 
				
			||||||
      challenge: SnAuthChallenge.fromJson(
 | 
					      challenge: SnAuthChallenge.fromJson(
 | 
				
			||||||
@@ -108,7 +114,7 @@ Map<String, dynamic> _$SnAuthSessionToJson(_SnAuthSession instance) =>
 | 
				
			|||||||
      'id': instance.id,
 | 
					      'id': instance.id,
 | 
				
			||||||
      'label': instance.label,
 | 
					      'label': instance.label,
 | 
				
			||||||
      'last_granted_at': instance.lastGrantedAt.toIso8601String(),
 | 
					      'last_granted_at': instance.lastGrantedAt.toIso8601String(),
 | 
				
			||||||
      'expired_at': instance.expiredAt.toIso8601String(),
 | 
					      'expired_at': instance.expiredAt?.toIso8601String(),
 | 
				
			||||||
      'account_id': instance.accountId,
 | 
					      'account_id': instance.accountId,
 | 
				
			||||||
      'challenge_id': instance.challengeId,
 | 
					      'challenge_id': instance.challengeId,
 | 
				
			||||||
      'challenge': instance.challenge.toJson(),
 | 
					      'challenge': instance.challenge.toJson(),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,9 @@ import 'package:shelf/shelf.dart';
 | 
				
			|||||||
import 'package:shelf/shelf_io.dart' as shelf_io;
 | 
					import 'package:shelf/shelf_io.dart' as shelf_io;
 | 
				
			||||||
import 'package:shelf_web_socket/shelf_web_socket.dart';
 | 
					import 'package:shelf_web_socket/shelf_web_socket.dart';
 | 
				
			||||||
import 'package:web_socket_channel/web_socket_channel.dart';
 | 
					import 'package:web_socket_channel/web_socket_channel.dart';
 | 
				
			||||||
import 'package:path/path.dart' as path;
 | 
					
 | 
				
			||||||
 | 
					// Conditional imports for IPC server - use web stubs on web platform
 | 
				
			||||||
 | 
					import 'ipc_server.dart' if (dart.library.html) 'ipc_server.web.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const String kRpcLogPrefix = 'arRPC.websocket';
 | 
					const String kRpcLogPrefix = 'arRPC.websocket';
 | 
				
			||||||
const String kRpcIpcLogPrefix = 'arRPC.ipc';
 | 
					const String kRpcIpcLogPrefix = 'arRPC.ipc';
 | 
				
			||||||
@@ -43,14 +45,14 @@ class IpcErrorCodes {
 | 
				
			|||||||
  static const int invalidEncoding = 4005;
 | 
					  static const int invalidEncoding = 4005;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Reference https://github.com/OpenAsar/arrpc/blob/main/src/transports/ipc.js
 | 
				
			||||||
class ActivityRpcServer {
 | 
					class ActivityRpcServer {
 | 
				
			||||||
  static const List<int> portRange = [6463, 6472]; // Ports 6463–6472
 | 
					  static const List<int> portRange = [6463, 6472]; // Ports 6463–6472
 | 
				
			||||||
  Map<String, Function>
 | 
					  Map<String, Function>
 | 
				
			||||||
  handlers; // {connection: (socket), message: (socket, data), close: (socket)}
 | 
					  handlers; // {connection: (socket), message: (socket, data), close: (socket)}
 | 
				
			||||||
  HttpServer? _httpServer;
 | 
					  HttpServer? _httpServer;
 | 
				
			||||||
  ServerSocket? _ipcServer;
 | 
					  IpcServer? _ipcServer;
 | 
				
			||||||
  final List<WebSocketChannel> _wsSockets = [];
 | 
					  final List<WebSocketChannel> _wsSockets = [];
 | 
				
			||||||
  final List<_IpcSocketWrapper> _ipcSockets = [];
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ActivityRpcServer(this.handlers);
 | 
					  ActivityRpcServer(this.handlers);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -58,109 +60,20 @@ class ActivityRpcServer {
 | 
				
			|||||||
    handlers = newHandlers;
 | 
					    handlers = newHandlers;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Encode IPC packet
 | 
					  // Start the server
 | 
				
			||||||
  static Uint8List encodeIpcPacket(int type, Map<String, dynamic> data) {
 | 
					 | 
				
			||||||
    final jsonData = jsonEncode(data);
 | 
					 | 
				
			||||||
    final dataBytes = utf8.encode(jsonData);
 | 
					 | 
				
			||||||
    final dataSize = dataBytes.length;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final buffer = ByteData(8 + dataSize);
 | 
					 | 
				
			||||||
    buffer.setInt32(0, type, Endian.little);
 | 
					 | 
				
			||||||
    buffer.setInt32(4, dataSize, Endian.little);
 | 
					 | 
				
			||||||
    buffer.buffer.asUint8List().setRange(8, 8 + dataSize, dataBytes);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return buffer.buffer.asUint8List();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<String> _getMacOsSystemTmpDir() async {
 | 
					 | 
				
			||||||
    final result = await Process.run('getconf', ['DARWIN_USER_TEMP_DIR']);
 | 
					 | 
				
			||||||
    return (result.stdout as String).trim();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Find available IPC socket path
 | 
					 | 
				
			||||||
  Future<String> _findAvailableIpcPath() async {
 | 
					 | 
				
			||||||
    // Build list of directories to try, with macOS-specific handling
 | 
					 | 
				
			||||||
    final baseDirs = <String>[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (Platform.isMacOS) {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        final macTempDir = await _getMacOsSystemTmpDir();
 | 
					 | 
				
			||||||
        if (macTempDir.isNotEmpty) {
 | 
					 | 
				
			||||||
          baseDirs.add(macTempDir);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      } catch (e) {
 | 
					 | 
				
			||||||
        developer.log(
 | 
					 | 
				
			||||||
          'Failed to get macOS system temp dir: $e',
 | 
					 | 
				
			||||||
          name: kRpcIpcLogPrefix,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Add other standard directories
 | 
					 | 
				
			||||||
    final otherDirs = [
 | 
					 | 
				
			||||||
      Platform.environment['XDG_RUNTIME_DIR'], // User runtime directory
 | 
					 | 
				
			||||||
      Platform.environment['TMPDIR'], // App container temp (fallback)
 | 
					 | 
				
			||||||
      Platform.environment['TMP'],
 | 
					 | 
				
			||||||
      Platform.environment['TEMP'],
 | 
					 | 
				
			||||||
      '/tmp', // System temp directory - most compatible
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    baseDirs.addAll(
 | 
					 | 
				
			||||||
      otherDirs.where((dir) => dir != null && dir.isNotEmpty).cast<String>(),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (final baseDir in baseDirs) {
 | 
					 | 
				
			||||||
      for (int i = 0; i < 10; i++) {
 | 
					 | 
				
			||||||
        final socketPath = path.join(baseDir, '$kIpcBasePath-$i');
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
          final socket = await ServerSocket.bind(
 | 
					 | 
				
			||||||
            InternetAddress(socketPath, type: InternetAddressType.unix),
 | 
					 | 
				
			||||||
            0,
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          socket.close();
 | 
					 | 
				
			||||||
          // Clean up the test socket
 | 
					 | 
				
			||||||
          try {
 | 
					 | 
				
			||||||
            await File(socketPath).delete();
 | 
					 | 
				
			||||||
          } catch (_) {}
 | 
					 | 
				
			||||||
          developer.log(
 | 
					 | 
				
			||||||
            'IPC socket will be created at: $socketPath',
 | 
					 | 
				
			||||||
            name: kRpcIpcLogPrefix,
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          return socketPath;
 | 
					 | 
				
			||||||
        } catch (e) {
 | 
					 | 
				
			||||||
          // Path not available, try next
 | 
					 | 
				
			||||||
          if (i == 0) {
 | 
					 | 
				
			||||||
            // Log only for the first attempt per directory
 | 
					 | 
				
			||||||
            developer.log(
 | 
					 | 
				
			||||||
              'IPC path $socketPath not available: $e',
 | 
					 | 
				
			||||||
              name: kRpcIpcLogPrefix,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          continue;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    throw Exception(
 | 
					 | 
				
			||||||
      'No available IPC socket paths found in any temp directory',
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Start the WebSocket server
 | 
					 | 
				
			||||||
  Future<void> start() async {
 | 
					  Future<void> start() async {
 | 
				
			||||||
    int port = portRange[0];
 | 
					    int port = portRange[0];
 | 
				
			||||||
    bool wsSuccess = false;
 | 
					    bool wsSuccess = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Start WebSocket server
 | 
					    // Start WebSocket server
 | 
				
			||||||
    while (port <= portRange[1]) {
 | 
					    while (port <= portRange[1]) {
 | 
				
			||||||
      developer.log('trying port $port', name: kRpcLogPrefix);
 | 
					      developer.log('Trying port $port', name: kRpcLogPrefix);
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        // Start HTTP server
 | 
					 | 
				
			||||||
        _httpServer = await HttpServer.bind(InternetAddress.loopbackIPv4, port);
 | 
					        _httpServer = await HttpServer.bind(InternetAddress.loopbackIPv4, port);
 | 
				
			||||||
        developer.log('listening on $port', name: kRpcLogPrefix);
 | 
					        developer.log('Listening on $port', name: kRpcLogPrefix);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Handle WebSocket upgrades
 | 
					 | 
				
			||||||
        shelf_io.serveRequests(_httpServer!, (Request request) async {
 | 
					        shelf_io.serveRequests(_httpServer!, (Request request) async {
 | 
				
			||||||
          developer.log('new request', name: kRpcLogPrefix);
 | 
					          developer.log('New request', name: kRpcLogPrefix);
 | 
				
			||||||
          if (request.headers['upgrade']?.toLowerCase() == 'websocket') {
 | 
					          if (request.headers['upgrade']?.toLowerCase() == 'websocket') {
 | 
				
			||||||
            final handler = webSocketHandler((WebSocketChannel channel, _) {
 | 
					            final handler = webSocketHandler((WebSocketChannel channel, _) {
 | 
				
			||||||
              _wsSockets.add(channel);
 | 
					              _wsSockets.add(channel);
 | 
				
			||||||
@@ -169,7 +82,7 @@ class ActivityRpcServer {
 | 
				
			|||||||
            return handler(request);
 | 
					            return handler(request);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          developer.log(
 | 
					          developer.log(
 | 
				
			||||||
            'new request disposed due to not websocket',
 | 
					            'New request disposed due to not websocket',
 | 
				
			||||||
            name: kRpcLogPrefix,
 | 
					            name: kRpcLogPrefix,
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
          return Response.notFound('Not a WebSocket request');
 | 
					          return Response.notFound('Not a WebSocket request');
 | 
				
			||||||
@@ -178,12 +91,12 @@ class ActivityRpcServer {
 | 
				
			|||||||
        break;
 | 
					        break;
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        if (e is SocketException && e.osError?.errorCode == 98) {
 | 
					        if (e is SocketException && e.osError?.errorCode == 98) {
 | 
				
			||||||
          // EADDRINUSE
 | 
					 | 
				
			||||||
          developer.log('$port in use!', name: kRpcLogPrefix);
 | 
					          developer.log('$port in use!', name: kRpcLogPrefix);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          developer.log('http error: $e', name: kRpcLogPrefix);
 | 
					          developer.log('HTTP error: $e', name: kRpcLogPrefix);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        port++;
 | 
					        port++;
 | 
				
			||||||
 | 
					        await Future.delayed(Duration(milliseconds: 100)); // Add delay
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -193,27 +106,24 @@ class ActivityRpcServer {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Start IPC server (skip on macOS due to sandboxing)
 | 
					    // Start IPC server
 | 
				
			||||||
    final shouldStartIpc = !Platform.isMacOS;
 | 
					    final shouldStartIpc = !Platform.isMacOS && !kIsWeb;
 | 
				
			||||||
    if (shouldStartIpc) {
 | 
					    if (shouldStartIpc) {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        final ipcPath = await _findAvailableIpcPath();
 | 
					        _ipcServer = MultiPlatformIpcServer();
 | 
				
			||||||
        _ipcServer = await ServerSocket.bind(
 | 
					 | 
				
			||||||
          InternetAddress(ipcPath, type: InternetAddressType.unix),
 | 
					 | 
				
			||||||
          0,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        developer.log('IPC listening at $ipcPath', name: kRpcIpcLogPrefix);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        _ipcServer!.listen((Socket socket) {
 | 
					        // Set up IPC handlers
 | 
				
			||||||
          _onIpcConnection(socket);
 | 
					        _ipcServer!.handlePacket = (socket, packet, _) {
 | 
				
			||||||
        });
 | 
					          _handleIpcPacket(socket, packet);
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await _ipcServer!.start();
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        developer.log('IPC server error: $e', name: kRpcIpcLogPrefix);
 | 
					        developer.log('IPC server error: $e', name: kRpcIpcLogPrefix);
 | 
				
			||||||
        // Continue without IPC if it fails
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      developer.log(
 | 
					      developer.log(
 | 
				
			||||||
        'IPC server disabled on macOS in production mode due to sandboxing',
 | 
					        'IPC server disabled on macOS or web in production mode',
 | 
				
			||||||
        name: kRpcIpcLogPrefix,
 | 
					        name: kRpcIpcLogPrefix,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -223,24 +133,23 @@ class ActivityRpcServer {
 | 
				
			|||||||
  Future<void> stop() async {
 | 
					  Future<void> stop() async {
 | 
				
			||||||
    // Stop WebSocket server
 | 
					    // Stop WebSocket server
 | 
				
			||||||
    for (var socket in _wsSockets) {
 | 
					    for (var socket in _wsSockets) {
 | 
				
			||||||
      await socket.sink.close();
 | 
					      try {
 | 
				
			||||||
 | 
					        await socket.sink.close();
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        developer.log('Error closing WebSocket: $e', name: kRpcLogPrefix);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    _wsSockets.clear();
 | 
					    _wsSockets.clear();
 | 
				
			||||||
    await _httpServer?.close();
 | 
					    await _httpServer?.close(force: true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Stop IPC server
 | 
					    // Stop IPC server
 | 
				
			||||||
    for (var socket in _ipcSockets) {
 | 
					    await _ipcServer?.stop();
 | 
				
			||||||
      socket.close();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    _ipcSockets.clear();
 | 
					 | 
				
			||||||
    await _ipcServer?.close();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    developer.log('servers stopped', name: kRpcLogPrefix);
 | 
					    developer.log('Servers stopped', name: kRpcLogPrefix);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Handle new WebSocket connection
 | 
					  // Handle new WebSocket connection
 | 
				
			||||||
  void _onWsConnection(WebSocketChannel socket, Request request) {
 | 
					  void _onWsConnection(WebSocketChannel socket, Request request) {
 | 
				
			||||||
    // Parse query parameters
 | 
					 | 
				
			||||||
    final uri = request.url;
 | 
					    final uri = request.url;
 | 
				
			||||||
    final params = uri.queryParameters;
 | 
					    final params = uri.queryParameters;
 | 
				
			||||||
    final ver = int.tryParse(params['v'] ?? '1') ?? 1;
 | 
					    final ver = int.tryParse(params['v'] ?? '1') ?? 1;
 | 
				
			||||||
@@ -249,43 +158,38 @@ class ActivityRpcServer {
 | 
				
			|||||||
    final origin = request.headers['origin'] ?? '';
 | 
					    final origin = request.headers['origin'] ?? '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    developer.log(
 | 
					    developer.log(
 | 
				
			||||||
      'new WS connection! origin: $origin, params: $params',
 | 
					      'New WS connection! origin: $origin, params: $params',
 | 
				
			||||||
      name: kRpcLogPrefix,
 | 
					      name: kRpcLogPrefix,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Validate origin
 | 
					 | 
				
			||||||
    if (origin.isNotEmpty &&
 | 
					    if (origin.isNotEmpty &&
 | 
				
			||||||
        ![
 | 
					        ![
 | 
				
			||||||
          'https://discord.com',
 | 
					          'https://discord.com',
 | 
				
			||||||
          'https://ptb.discord.com',
 | 
					          'https://ptb.discord.com',
 | 
				
			||||||
          'https://canary.discord.com',
 | 
					          'https://canary.discord.com',
 | 
				
			||||||
        ].contains(origin)) {
 | 
					        ].contains(origin)) {
 | 
				
			||||||
      developer.log('disallowed origin: $origin', name: kRpcLogPrefix);
 | 
					      developer.log('Disallowed origin: $origin', name: kRpcLogPrefix);
 | 
				
			||||||
      socket.sink.close();
 | 
					      socket.sink.close();
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Validate encoding
 | 
					 | 
				
			||||||
    if (encoding != 'json') {
 | 
					    if (encoding != 'json') {
 | 
				
			||||||
      developer.log(
 | 
					      developer.log(
 | 
				
			||||||
        'unsupported encoding requested: $encoding',
 | 
					        'Unsupported encoding requested: $encoding',
 | 
				
			||||||
        name: kRpcLogPrefix,
 | 
					        name: kRpcLogPrefix,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      socket.sink.close();
 | 
					      socket.sink.close();
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Validate version
 | 
					 | 
				
			||||||
    if (ver != 1) {
 | 
					    if (ver != 1) {
 | 
				
			||||||
      developer.log('unsupported version requested: $ver', name: kRpcLogPrefix);
 | 
					      developer.log('Unsupported version requested: $ver', name: kRpcLogPrefix);
 | 
				
			||||||
      socket.sink.close();
 | 
					      socket.sink.close();
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Store client info on socket
 | 
					 | 
				
			||||||
    final socketWithMeta = _WsSocketWrapper(socket, clientId, encoding);
 | 
					    final socketWithMeta = _WsSocketWrapper(socket, clientId, encoding);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Set up event listeners
 | 
					 | 
				
			||||||
    socket.stream.listen(
 | 
					    socket.stream.listen(
 | 
				
			||||||
      (data) => _onWsMessage(socketWithMeta, data),
 | 
					      (data) => _onWsMessage(socketWithMeta, data),
 | 
				
			||||||
      onError: (e) {
 | 
					      onError: (e) {
 | 
				
			||||||
@@ -298,36 +202,27 @@ class ActivityRpcServer {
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Notify handler of new connection
 | 
					 | 
				
			||||||
    handlers['connection']?.call(socketWithMeta);
 | 
					    handlers['connection']?.call(socketWithMeta);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Handle new IPC connection
 | 
					 | 
				
			||||||
  void _onIpcConnection(Socket socket) {
 | 
					 | 
				
			||||||
    developer.log('new IPC connection!', name: kRpcIpcLogPrefix);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final socketWrapper = _IpcSocketWrapper(socket);
 | 
					 | 
				
			||||||
    _ipcSockets.add(socketWrapper);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Set up event listeners
 | 
					 | 
				
			||||||
    socket.listen(
 | 
					 | 
				
			||||||
      (data) => _onIpcData(socketWrapper, data),
 | 
					 | 
				
			||||||
      onError: (e) {
 | 
					 | 
				
			||||||
        developer.log('IPC socket error: $e', name: kRpcIpcLogPrefix);
 | 
					 | 
				
			||||||
        socket.close();
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      onDone: () {
 | 
					 | 
				
			||||||
        developer.log('IPC socket closed', name: kRpcIpcLogPrefix);
 | 
					 | 
				
			||||||
        handlers['close']?.call(socketWrapper);
 | 
					 | 
				
			||||||
        _ipcSockets.remove(socketWrapper);
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Handle incoming WebSocket message
 | 
					  // Handle incoming WebSocket message
 | 
				
			||||||
  void _onWsMessage(_WsSocketWrapper socket, dynamic data) {
 | 
					  Future<void> _onWsMessage(_WsSocketWrapper socket, dynamic data) async {
 | 
				
			||||||
 | 
					    if (data is! String) {
 | 
				
			||||||
 | 
					      developer.log(
 | 
				
			||||||
 | 
					        'Invalid WebSocket message: not a string',
 | 
				
			||||||
 | 
					        name: kRpcLogPrefix,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      final jsonData = jsonDecode(data as String);
 | 
					      final jsonData = await compute(jsonDecode, data);
 | 
				
			||||||
 | 
					      if (jsonData is! Map<String, dynamic>) {
 | 
				
			||||||
 | 
					        developer.log(
 | 
				
			||||||
 | 
					          'Invalid WebSocket message: not a JSON object',
 | 
				
			||||||
 | 
					          name: kRpcLogPrefix,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      developer.log('WS message: $jsonData', name: kRpcLogPrefix);
 | 
					      developer.log('WS message: $jsonData', name: kRpcLogPrefix);
 | 
				
			||||||
      handlers['message']?.call(socket, jsonData);
 | 
					      handlers['message']?.call(socket, jsonData);
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
@@ -335,22 +230,8 @@ class ActivityRpcServer {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Handle incoming IPC data
 | 
					 | 
				
			||||||
  void _onIpcData(_IpcSocketWrapper socket, List<int> data) {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      socket.addData(data);
 | 
					 | 
				
			||||||
      final packets = socket.readPackets();
 | 
					 | 
				
			||||||
      for (final packet in packets) {
 | 
					 | 
				
			||||||
        _handleIpcPacket(socket, packet);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      developer.log('IPC data error: $e', name: kRpcIpcLogPrefix);
 | 
					 | 
				
			||||||
      socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString());
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Handle IPC packet
 | 
					  // Handle IPC packet
 | 
				
			||||||
  void _handleIpcPacket(_IpcSocketWrapper socket, _IpcPacket packet) {
 | 
					  void _handleIpcPacket(IpcSocketWrapper socket, IpcPacket packet) {
 | 
				
			||||||
    switch (packet.type) {
 | 
					    switch (packet.type) {
 | 
				
			||||||
      case IpcTypes.ping:
 | 
					      case IpcTypes.ping:
 | 
				
			||||||
        developer.log('IPC ping received', name: kRpcIpcLogPrefix);
 | 
					        developer.log('IPC ping received', name: kRpcIpcLogPrefix);
 | 
				
			||||||
@@ -359,7 +240,6 @@ class ActivityRpcServer {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      case IpcTypes.pong:
 | 
					      case IpcTypes.pong:
 | 
				
			||||||
        developer.log('IPC pong received', name: kRpcIpcLogPrefix);
 | 
					        developer.log('IPC pong received', name: kRpcIpcLogPrefix);
 | 
				
			||||||
        // Handle pong if needed
 | 
					 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      case IpcTypes.handshake:
 | 
					      case IpcTypes.handshake:
 | 
				
			||||||
@@ -388,13 +268,12 @@ class ActivityRpcServer {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Handle IPC handshake
 | 
					  // Handle IPC handshake
 | 
				
			||||||
  void _onIpcHandshake(_IpcSocketWrapper socket, Map<String, dynamic> params) {
 | 
					  void _onIpcHandshake(IpcSocketWrapper socket, Map<String, dynamic> params) {
 | 
				
			||||||
    developer.log('IPC handshake: $params', name: kRpcIpcLogPrefix);
 | 
					    developer.log('IPC handshake: $params', name: kRpcIpcLogPrefix);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final ver = int.tryParse(params['v']?.toString() ?? '1') ?? 1;
 | 
					    final ver = int.tryParse(params['v']?.toString() ?? '1') ?? 1;
 | 
				
			||||||
    final clientId = params['client_id']?.toString() ?? '';
 | 
					    final clientId = params['client_id']?.toString() ?? '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Validate version
 | 
					 | 
				
			||||||
    if (ver != 1) {
 | 
					    if (ver != 1) {
 | 
				
			||||||
      developer.log(
 | 
					      developer.log(
 | 
				
			||||||
        'IPC unsupported version requested: $ver',
 | 
					        'IPC unsupported version requested: $ver',
 | 
				
			||||||
@@ -404,7 +283,6 @@ class ActivityRpcServer {
 | 
				
			|||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Validate client ID
 | 
					 | 
				
			||||||
    if (clientId.isEmpty) {
 | 
					    if (clientId.isEmpty) {
 | 
				
			||||||
      developer.log('IPC client ID required', name: kRpcIpcLogPrefix);
 | 
					      developer.log('IPC client ID required', name: kRpcIpcLogPrefix);
 | 
				
			||||||
      socket.closeWithCode(IpcErrorCodes.invalidClientId);
 | 
					      socket.closeWithCode(IpcErrorCodes.invalidClientId);
 | 
				
			||||||
@@ -413,7 +291,6 @@ class ActivityRpcServer {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    socket.clientId = clientId;
 | 
					    socket.clientId = clientId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Notify handler of new connection
 | 
					 | 
				
			||||||
    handlers['connection']?.call(socket);
 | 
					    handlers['connection']?.call(socket);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -432,74 +309,6 @@ class _WsSocketWrapper {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// IPC wrapper
 | 
					 | 
				
			||||||
class _IpcSocketWrapper {
 | 
					 | 
				
			||||||
  final Socket socket;
 | 
					 | 
				
			||||||
  String clientId = '';
 | 
					 | 
				
			||||||
  bool handshook = false;
 | 
					 | 
				
			||||||
  final List<int> _buffer = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  _IpcSocketWrapper(this.socket);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void addData(List<int> data) {
 | 
					 | 
				
			||||||
    _buffer.addAll(data);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void send(Map<String, dynamic> msg) {
 | 
					 | 
				
			||||||
    developer.log('IPC sending: $msg', name: kRpcIpcLogPrefix);
 | 
					 | 
				
			||||||
    final packet = ActivityRpcServer.encodeIpcPacket(IpcTypes.frame, msg);
 | 
					 | 
				
			||||||
    socket.add(packet);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void sendPong(dynamic data) {
 | 
					 | 
				
			||||||
    final packet = ActivityRpcServer.encodeIpcPacket(IpcTypes.pong, data ?? {});
 | 
					 | 
				
			||||||
    socket.add(packet);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void close() {
 | 
					 | 
				
			||||||
    socket.close();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void closeWithCode(int code, [String message = '']) {
 | 
					 | 
				
			||||||
    final closeData = {'code': code, 'message': message};
 | 
					 | 
				
			||||||
    final packet = ActivityRpcServer.encodeIpcPacket(IpcTypes.close, closeData);
 | 
					 | 
				
			||||||
    socket.add(packet);
 | 
					 | 
				
			||||||
    socket.close();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  List<_IpcPacket> readPackets() {
 | 
					 | 
				
			||||||
    final packets = <_IpcPacket>[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    while (_buffer.length >= 8) {
 | 
					 | 
				
			||||||
      final buffer = Uint8List.fromList(_buffer);
 | 
					 | 
				
			||||||
      final byteData = ByteData.view(buffer.buffer);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      final type = byteData.getInt32(0, Endian.little);
 | 
					 | 
				
			||||||
      final dataSize = byteData.getInt32(4, Endian.little);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (_buffer.length < 8 + dataSize) break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      final dataBytes = _buffer.sublist(8, 8 + dataSize);
 | 
					 | 
				
			||||||
      final jsonStr = utf8.decode(dataBytes);
 | 
					 | 
				
			||||||
      final jsonData = jsonDecode(jsonStr);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      packets.add(_IpcPacket(type, jsonData));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      _buffer.removeRange(0, 8 + dataSize);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return packets;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// IPC Packet structure
 | 
					 | 
				
			||||||
class _IpcPacket {
 | 
					 | 
				
			||||||
  final int type;
 | 
					 | 
				
			||||||
  final Map<String, dynamic> data;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  _IpcPacket(this.type, this.data);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// State management for server status and activities
 | 
					// State management for server status and activities
 | 
				
			||||||
class ServerState {
 | 
					class ServerState {
 | 
				
			||||||
  final String status;
 | 
					  final String status;
 | 
				
			||||||
@@ -522,7 +331,6 @@ class ServerStateNotifier extends StateNotifier<ServerState> {
 | 
				
			|||||||
    : super(ServerState(status: 'Server not started'));
 | 
					    : super(ServerState(status: 'Server not started'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> start() async {
 | 
					  Future<void> start() async {
 | 
				
			||||||
    // Only start server on desktop platforms
 | 
					 | 
				
			||||||
    if (!Platform.isAndroid && !Platform.isIOS && !kIsWeb) {
 | 
					    if (!Platform.isAndroid && !Platform.isIOS && !kIsWeb) {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        await server.start();
 | 
					        await server.start();
 | 
				
			||||||
@@ -554,9 +362,8 @@ final rpcServerStateProvider =
 | 
				
			|||||||
          final clientId =
 | 
					          final clientId =
 | 
				
			||||||
              socket is _WsSocketWrapper
 | 
					              socket is _WsSocketWrapper
 | 
				
			||||||
                  ? socket.clientId
 | 
					                  ? socket.clientId
 | 
				
			||||||
                  : (socket as _IpcSocketWrapper).clientId;
 | 
					                  : (socket as IpcSocketWrapper).clientId;
 | 
				
			||||||
          notifier.updateStatus('Client connected (ID: $clientId)');
 | 
					          notifier.updateStatus('Client connected (ID: $clientId)');
 | 
				
			||||||
          // Send READY event
 | 
					 | 
				
			||||||
          socket.send({
 | 
					          socket.send({
 | 
				
			||||||
            'cmd': 'DISPATCH',
 | 
					            'cmd': 'DISPATCH',
 | 
				
			||||||
            'data': {
 | 
					            'data': {
 | 
				
			||||||
@@ -575,7 +382,7 @@ final rpcServerStateProvider =
 | 
				
			|||||||
              },
 | 
					              },
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            'evt': 'READY',
 | 
					            'evt': 'READY',
 | 
				
			||||||
            'nonce': '12345', // Should be dynamic
 | 
					            'nonce': '12345',
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        'message': (socket, dynamic data) async {
 | 
					        'message': (socket, dynamic data) async {
 | 
				
			||||||
@@ -583,7 +390,6 @@ final rpcServerStateProvider =
 | 
				
			|||||||
            notifier.addActivity(
 | 
					            notifier.addActivity(
 | 
				
			||||||
              'Activity: ${data['args']['activity']['details'] ?? 'Unknown'}',
 | 
					              'Activity: ${data['args']['activity']['details'] ?? 'Unknown'}',
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
            // Call setRemoteActivityStatus
 | 
					 | 
				
			||||||
            final label = data['args']['activity']['details'] ?? 'Unknown';
 | 
					            final label = data['args']['activity']['details'] ?? 'Unknown';
 | 
				
			||||||
            final appId = socket.clientId;
 | 
					            final appId = socket.clientId;
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
@@ -594,7 +400,6 @@ final rpcServerStateProvider =
 | 
				
			|||||||
                name: kRpcLogPrefix,
 | 
					                name: kRpcLogPrefix,
 | 
				
			||||||
              );
 | 
					              );
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            // Echo back success
 | 
					 | 
				
			||||||
            socket.send({
 | 
					            socket.send({
 | 
				
			||||||
              'cmd': 'SET_ACTIVITY',
 | 
					              'cmd': 'SET_ACTIVITY',
 | 
				
			||||||
              'data': data['args']['activity'],
 | 
					              'data': data['args']['activity'],
 | 
				
			||||||
							
								
								
									
										297
									
								
								lib/pods/activity/ipc_server.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										297
									
								
								lib/pods/activity/ipc_server.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,297 @@
 | 
				
			|||||||
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					import 'dart:convert';
 | 
				
			||||||
 | 
					import 'dart:developer' as developer;
 | 
				
			||||||
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					import 'dart:typed_data';
 | 
				
			||||||
 | 
					import 'package:dart_ipc/dart_ipc.dart';
 | 
				
			||||||
 | 
					import 'package:path/path.dart' as path;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const String kRpcIpcLogPrefix = 'arRPC.ipc';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IPC Packet Types
 | 
				
			||||||
 | 
					class IpcTypes {
 | 
				
			||||||
 | 
					  static const int handshake = 0;
 | 
				
			||||||
 | 
					  static const int frame = 1;
 | 
				
			||||||
 | 
					  static const int close = 2;
 | 
				
			||||||
 | 
					  static const int ping = 3;
 | 
				
			||||||
 | 
					  static const int pong = 4;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IPC Close Codes
 | 
				
			||||||
 | 
					class IpcCloseCodes {
 | 
				
			||||||
 | 
					  static const int closeNormal = 1000;
 | 
				
			||||||
 | 
					  static const int closeUnsupported = 1003;
 | 
				
			||||||
 | 
					  static const int closeAbnormal = 1006;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IPC Error Codes
 | 
				
			||||||
 | 
					class IpcErrorCodes {
 | 
				
			||||||
 | 
					  static const int invalidClientId = 4000;
 | 
				
			||||||
 | 
					  static const int invalidOrigin = 4001;
 | 
				
			||||||
 | 
					  static const int rateLimited = 4002;
 | 
				
			||||||
 | 
					  static const int tokenRevoked = 4003;
 | 
				
			||||||
 | 
					  static const int invalidVersion = 4004;
 | 
				
			||||||
 | 
					  static const int invalidEncoding = 4005;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IPC Packet structure
 | 
				
			||||||
 | 
					class IpcPacket {
 | 
				
			||||||
 | 
					  final int type;
 | 
				
			||||||
 | 
					  final Map<String, dynamic> data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  IpcPacket(this.type, this.data);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Abstract base class for IPC server
 | 
				
			||||||
 | 
					abstract class IpcServer {
 | 
				
			||||||
 | 
					  final List<IpcSocketWrapper> _sockets = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Encode IPC packet
 | 
				
			||||||
 | 
					  static Uint8List encodeIpcPacket(int type, Map<String, dynamic> data) {
 | 
				
			||||||
 | 
					    final jsonData = jsonEncode(data);
 | 
				
			||||||
 | 
					    final dataBytes = utf8.encode(jsonData);
 | 
				
			||||||
 | 
					    final dataSize = dataBytes.length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final buffer = ByteData(8 + dataSize);
 | 
				
			||||||
 | 
					    buffer.setInt32(0, type, Endian.little);
 | 
				
			||||||
 | 
					    buffer.setInt32(4, dataSize, Endian.little);
 | 
				
			||||||
 | 
					    buffer.buffer.asUint8List().setRange(8, 8 + dataSize, dataBytes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return buffer.buffer.asUint8List();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> start();
 | 
				
			||||||
 | 
					  Future<void> stop();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void addSocket(IpcSocketWrapper socket) {
 | 
				
			||||||
 | 
					    _sockets.add(socket);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void removeSocket(IpcSocketWrapper socket) {
 | 
				
			||||||
 | 
					    _sockets.remove(socket);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<IpcSocketWrapper> get sockets => _sockets;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void Function(
 | 
				
			||||||
 | 
					    IpcSocketWrapper socket,
 | 
				
			||||||
 | 
					    IpcPacket packet,
 | 
				
			||||||
 | 
					    Map<String, Function> handlers,
 | 
				
			||||||
 | 
					  )?
 | 
				
			||||||
 | 
					  handlePacket;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Abstract base class for IPC socket wrapper
 | 
				
			||||||
 | 
					abstract class IpcSocketWrapper {
 | 
				
			||||||
 | 
					  String clientId = '';
 | 
				
			||||||
 | 
					  bool handshook = false;
 | 
				
			||||||
 | 
					  final List<int> _buffer = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void addData(List<int> data) {
 | 
				
			||||||
 | 
					    _buffer.addAll(data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void send(Map<String, dynamic> msg);
 | 
				
			||||||
 | 
					  void sendPong(dynamic data);
 | 
				
			||||||
 | 
					  void close();
 | 
				
			||||||
 | 
					  void closeWithCode(int code, [String message = '']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<IpcPacket> readPackets() {
 | 
				
			||||||
 | 
					    final packets = <IpcPacket>[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    while (_buffer.length >= 8) {
 | 
				
			||||||
 | 
					      final buffer = Uint8List.fromList(_buffer);
 | 
				
			||||||
 | 
					      final byteData = ByteData.view(buffer.buffer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      final type = byteData.getInt32(0, Endian.little);
 | 
				
			||||||
 | 
					      final dataSize = byteData.getInt32(4, Endian.little);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (_buffer.length < 8 + dataSize) break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      final dataBytes = _buffer.sublist(8, 8 + dataSize);
 | 
				
			||||||
 | 
					      final jsonStr = utf8.decode(dataBytes);
 | 
				
			||||||
 | 
					      final jsonData = jsonDecode(jsonStr);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      packets.add(IpcPacket(type, jsonData));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      _buffer.removeRange(0, 8 + dataSize);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return packets;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Multiplatform IPC Server implementation using dart_ipc
 | 
				
			||||||
 | 
					class MultiPlatformIpcServer extends IpcServer {
 | 
				
			||||||
 | 
					  StreamSubscription? _serverSubscription;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<void> start() async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final ipcPath = Platform.isWindows
 | 
				
			||||||
 | 
					          ? r'\\.\pipe\discord-ipc-0'
 | 
				
			||||||
 | 
					          : await _findAvailableUnixIpcPath();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      final serverSocket = await bind(ipcPath);
 | 
				
			||||||
 | 
					      developer.log(
 | 
				
			||||||
 | 
					        'IPC listening at $ipcPath',
 | 
				
			||||||
 | 
					        name: kRpcIpcLogPrefix,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      _serverSubscription = serverSocket.listen((socket) {
 | 
				
			||||||
 | 
					        final socketWrapper = MultiPlatformIpcSocketWrapper(socket);
 | 
				
			||||||
 | 
					        addSocket(socketWrapper);
 | 
				
			||||||
 | 
					        developer.log(
 | 
				
			||||||
 | 
					          'New IPC connection!',
 | 
				
			||||||
 | 
					          name: kRpcIpcLogPrefix,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        _handleIpcData(socketWrapper);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      throw Exception('Failed to start IPC server: $e');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<void> stop() async {
 | 
				
			||||||
 | 
					    for (var socket in sockets) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        socket.close();
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        developer.log('Error closing IPC socket: $e', name: kRpcIpcLogPrefix);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    sockets.clear();
 | 
				
			||||||
 | 
					    _serverSubscription?.cancel();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Handle incoming IPC data
 | 
				
			||||||
 | 
					  void _handleIpcData(MultiPlatformIpcSocketWrapper socket) {
 | 
				
			||||||
 | 
					    final startTime = DateTime.now();
 | 
				
			||||||
 | 
					    socket.socket.listen((data) {
 | 
				
			||||||
 | 
					      final readStart = DateTime.now();
 | 
				
			||||||
 | 
					      socket.addData(data);
 | 
				
			||||||
 | 
					      final readDuration = DateTime.now().difference(readStart).inMicroseconds;
 | 
				
			||||||
 | 
					      developer.log(
 | 
				
			||||||
 | 
					        'Read data took $readDuration microseconds',
 | 
				
			||||||
 | 
					        name: kRpcIpcLogPrefix,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      final packets = socket.readPackets();
 | 
				
			||||||
 | 
					      for (final packet in packets) {
 | 
				
			||||||
 | 
					        handlePacket?.call(socket, packet, {});
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }, onDone: () {
 | 
				
			||||||
 | 
					      developer.log('IPC connection closed', name: kRpcIpcLogPrefix);
 | 
				
			||||||
 | 
					      socket.close();
 | 
				
			||||||
 | 
					    }, onError: (e) {
 | 
				
			||||||
 | 
					      developer.log('IPC data error: $e', name: kRpcIpcLogPrefix);
 | 
				
			||||||
 | 
					      socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString());
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    final totalDuration = DateTime.now().difference(startTime).inMicroseconds;
 | 
				
			||||||
 | 
					    developer.log(
 | 
				
			||||||
 | 
					      '_handleIpcData took $totalDuration microseconds',
 | 
				
			||||||
 | 
					      name: kRpcIpcLogPrefix,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<String> _getMacOsSystemTmpDir() async {
 | 
				
			||||||
 | 
					    final result = await Process.run('getconf', ['DARWIN_USER_TEMP_DIR']);
 | 
				
			||||||
 | 
					    return (result.stdout as String).trim();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Find available IPC socket path for Unix-like systems
 | 
				
			||||||
 | 
					  Future<String> _findAvailableUnixIpcPath() async {
 | 
				
			||||||
 | 
					    // Build list of directories to try, with macOS-specific handling
 | 
				
			||||||
 | 
					    final baseDirs = <String>[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (Platform.isMacOS) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        final macTempDir = await _getMacOsSystemTmpDir();
 | 
				
			||||||
 | 
					        if (macTempDir.isNotEmpty) {
 | 
				
			||||||
 | 
					          baseDirs.add(macTempDir);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        developer.log(
 | 
				
			||||||
 | 
					          'Failed to get macOS system temp dir: $e',
 | 
				
			||||||
 | 
					          name: kRpcIpcLogPrefix,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Add other standard directories
 | 
				
			||||||
 | 
					    final otherDirs = [
 | 
				
			||||||
 | 
					      Platform.environment['XDG_RUNTIME_DIR'],
 | 
				
			||||||
 | 
					      Platform.environment['TMPDIR'],
 | 
				
			||||||
 | 
					      Platform.environment['TMP'],
 | 
				
			||||||
 | 
					      Platform.environment['TEMP'],
 | 
				
			||||||
 | 
					      '/tmp',
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    baseDirs.addAll(
 | 
				
			||||||
 | 
					      otherDirs.where((dir) => dir != null && dir.isNotEmpty).cast<String>(),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (final baseDir in baseDirs) {
 | 
				
			||||||
 | 
					      for (int i = 0; i < 10; i++) {
 | 
				
			||||||
 | 
					        final socketPath = path.join(baseDir, 'discord-ipc-$i');
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          final socket = await bind(socketPath);
 | 
				
			||||||
 | 
					          socket.close();
 | 
				
			||||||
 | 
					          try {
 | 
				
			||||||
 | 
					            await File(socketPath).delete();
 | 
				
			||||||
 | 
					          } catch (_) {}
 | 
				
			||||||
 | 
					          developer.log(
 | 
				
			||||||
 | 
					            'IPC socket will be created at: $socketPath',
 | 
				
			||||||
 | 
					            name: kRpcIpcLogPrefix,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          return socketPath;
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					          if (i == 0) {
 | 
				
			||||||
 | 
					            developer.log(
 | 
				
			||||||
 | 
					              'IPC path $socketPath not available: $e',
 | 
				
			||||||
 | 
					              name: kRpcIpcLogPrefix,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    throw Exception(
 | 
				
			||||||
 | 
					      'No available IPC socket paths found in any temp directory',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Multiplatform IPC Socket Wrapper
 | 
				
			||||||
 | 
					class MultiPlatformIpcSocketWrapper extends IpcSocketWrapper {
 | 
				
			||||||
 | 
					  final dynamic socket;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MultiPlatformIpcSocketWrapper(this.socket);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void send(Map<String, dynamic> msg) {
 | 
				
			||||||
 | 
					    developer.log('IPC sending: $msg', name: kRpcIpcLogPrefix);
 | 
				
			||||||
 | 
					    final packet = IpcServer.encodeIpcPacket(IpcTypes.frame, msg);
 | 
				
			||||||
 | 
					    socket.add(packet);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void sendPong(dynamic data) {
 | 
				
			||||||
 | 
					    final packet = IpcServer.encodeIpcPacket(IpcTypes.pong, data ?? {});
 | 
				
			||||||
 | 
					    socket.add(packet);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void close() {
 | 
				
			||||||
 | 
					    socket.close();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void closeWithCode(int code, [String message = '']) {
 | 
				
			||||||
 | 
					    final closeData = {'code': code, 'message': message};
 | 
				
			||||||
 | 
					    final packet = IpcServer.encodeIpcPacket(IpcTypes.close, closeData);
 | 
				
			||||||
 | 
					    socket.add(packet);
 | 
				
			||||||
 | 
					    socket.close();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										61
									
								
								lib/pods/activity/ipc_server.web.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								lib/pods/activity/ipc_server.web.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
				
			|||||||
 | 
					// Stub implementation for web platform
 | 
				
			||||||
 | 
					// This file provides empty implementations to avoid import errors on web
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IPC Packet Types
 | 
				
			||||||
 | 
					class IpcTypes {
 | 
				
			||||||
 | 
					  static const int handshake = 0;
 | 
				
			||||||
 | 
					  static const int frame = 1;
 | 
				
			||||||
 | 
					  static const int close = 2;
 | 
				
			||||||
 | 
					  static const int ping = 3;
 | 
				
			||||||
 | 
					  static const int pong = 4;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IPC Close Codes
 | 
				
			||||||
 | 
					class IpcCloseCodes {
 | 
				
			||||||
 | 
					  static const int closeNormal = 1000;
 | 
				
			||||||
 | 
					  static const int closeUnsupported = 1003;
 | 
				
			||||||
 | 
					  static const int closeAbnormal = 1006;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IPC Error Codes
 | 
				
			||||||
 | 
					class IpcErrorCodes {
 | 
				
			||||||
 | 
					  static const int invalidClientId = 4000;
 | 
				
			||||||
 | 
					  static const int invalidOrigin = 4001;
 | 
				
			||||||
 | 
					  static const int rateLimited = 4002;
 | 
				
			||||||
 | 
					  static const int tokenRevoked = 4003;
 | 
				
			||||||
 | 
					  static const int invalidVersion = 4004;
 | 
				
			||||||
 | 
					  static const int invalidEncoding = 4005;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IPC Packet structure
 | 
				
			||||||
 | 
					class IpcPacket {
 | 
				
			||||||
 | 
					  final int type;
 | 
				
			||||||
 | 
					  final Map<String, dynamic> data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  IpcPacket(this.type, this.data);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class IpcServer {
 | 
				
			||||||
 | 
					  Future<void> start() async {}
 | 
				
			||||||
 | 
					  Future<void> stop() async {}
 | 
				
			||||||
 | 
					  void Function(dynamic, dynamic, dynamic)? handlePacket;
 | 
				
			||||||
 | 
					  void addSocket(dynamic socket) {}
 | 
				
			||||||
 | 
					  void removeSocket(dynamic socket) {}
 | 
				
			||||||
 | 
					  List<dynamic> get sockets => [];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class IpcSocketWrapper {
 | 
				
			||||||
 | 
					  String clientId = '';
 | 
				
			||||||
 | 
					  bool handshook = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void addData(List<int> data) {}
 | 
				
			||||||
 | 
					  void send(Map<String, dynamic> msg) {}
 | 
				
			||||||
 | 
					  void sendPong(dynamic data) {}
 | 
				
			||||||
 | 
					  void close() {}
 | 
				
			||||||
 | 
					  void closeWithCode(int code, [String message = '']) {}
 | 
				
			||||||
 | 
					  List<dynamic> readPackets() => [];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MultiPlatformIpcServer extends IpcServer {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MultiPlatformIpcSocketWrapper extends IpcSocketWrapper {}
 | 
				
			||||||
@@ -30,7 +30,7 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
 | 
				
			|||||||
      final user = SnAccount.fromJson(response.data);
 | 
					      final user = SnAccount.fromJson(response.data);
 | 
				
			||||||
      state = AsyncValue.data(user);
 | 
					      state = AsyncValue.data(user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (kIsWeb || !Platform.isLinux) {
 | 
					      if (kIsWeb || !(Platform.isLinux || Platform.isWindows)) {
 | 
				
			||||||
        FirebaseAnalytics.instance.setUserId(id: user.id);
 | 
					        FirebaseAnalytics.instance.setUserId(id: user.id);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (error, stackTrace) {
 | 
					    } catch (error, stackTrace) {
 | 
				
			||||||
@@ -44,9 +44,12 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
 | 
				
			|||||||
                      : 'failedToLoadUserInfoNetwork')
 | 
					                      : 'failedToLoadUserInfoNetwork')
 | 
				
			||||||
                  .tr()
 | 
					                  .tr()
 | 
				
			||||||
                  .trim(),
 | 
					                  .trim(),
 | 
				
			||||||
              '${error.response!.statusCode}\n${error.response?.headers}',
 | 
					              '',
 | 
				
			||||||
              jsonEncode(error.response?.data),
 | 
					              '${error.response?.statusCode ?? 'Network Error'}',
 | 
				
			||||||
            ].join('\n\n'),
 | 
					              if (error.response?.headers != null) error.response?.headers,
 | 
				
			||||||
 | 
					              if (error.response?.data != null)
 | 
				
			||||||
 | 
					                jsonEncode(error.response?.data),
 | 
				
			||||||
 | 
					            ].join('\n'),
 | 
				
			||||||
            iconStyle: IconStyle.error,
 | 
					            iconStyle: IconStyle.error,
 | 
				
			||||||
            neutralButtonTitle: 'retry'.tr(),
 | 
					            neutralButtonTitle: 'retry'.tr(),
 | 
				
			||||||
            negativeButtonTitle: 'okay'.tr(),
 | 
					            negativeButtonTitle: 'okay'.tr(),
 | 
				
			||||||
@@ -87,7 +90,7 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
 | 
				
			|||||||
    final prefs = _ref.read(sharedPreferencesProvider);
 | 
					    final prefs = _ref.read(sharedPreferencesProvider);
 | 
				
			||||||
    await prefs.remove(kTokenPairStoreKey);
 | 
					    await prefs.remove(kTokenPairStoreKey);
 | 
				
			||||||
    _ref.invalidate(tokenProvider);
 | 
					    _ref.invalidate(tokenProvider);
 | 
				
			||||||
    if (kIsWeb || !Platform.isLinux) {
 | 
					    if (kIsWeb || !(Platform.isLinux || Platform.isWindows)) {
 | 
				
			||||||
      FirebaseAnalytics.instance.setUserId(id: null);
 | 
					      FirebaseAnalytics.instance.setUserId(id: null);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -89,8 +89,7 @@ bool get _supportsAnalytics =>
 | 
				
			|||||||
    kIsWeb ||
 | 
					    kIsWeb ||
 | 
				
			||||||
    Platform.isAndroid ||
 | 
					    Platform.isAndroid ||
 | 
				
			||||||
    Platform.isIOS ||
 | 
					    Platform.isIOS ||
 | 
				
			||||||
    Platform.isMacOS ||
 | 
					    Platform.isMacOS;
 | 
				
			||||||
    Platform.isWindows;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Provider for the router
 | 
					// Provider for the router
 | 
				
			||||||
final routerProvider = Provider<GoRouter>((ref) {
 | 
					final routerProvider = Provider<GoRouter>((ref) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
 | 
				
			|||||||
import 'package:flutter/services.dart';
 | 
					import 'package:flutter/services.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/services/udid.native.dart';
 | 
					import 'package:island/services/udid.dart' as udid;
 | 
				
			||||||
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';
 | 
				
			||||||
@@ -68,7 +68,7 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
 | 
				
			|||||||
    try {
 | 
					    try {
 | 
				
			||||||
      final deviceInfoPlugin = DeviceInfoPlugin();
 | 
					      final deviceInfoPlugin = DeviceInfoPlugin();
 | 
				
			||||||
      _deviceInfo = await deviceInfoPlugin.deviceInfo;
 | 
					      _deviceInfo = await deviceInfoPlugin.deviceInfo;
 | 
				
			||||||
      _deviceUdid = await getUdid();
 | 
					      _deviceUdid = await udid.getUdid();
 | 
				
			||||||
      if (mounted) {
 | 
					      if (mounted) {
 | 
				
			||||||
        setState(() {});
 | 
					        setState(() {});
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -174,12 +174,20 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
 | 
				
			|||||||
                            context,
 | 
					                            context,
 | 
				
			||||||
                            title: 'Device Information',
 | 
					                            title: 'Device Information',
 | 
				
			||||||
                            children: [
 | 
					                            children: [
 | 
				
			||||||
                              _buildInfoItem(
 | 
					                              FutureBuilder<String>(
 | 
				
			||||||
                                context,
 | 
					                                future: udid.getDeviceName(),
 | 
				
			||||||
                                icon: Symbols.label,
 | 
					                                builder: (context, snapshot) {
 | 
				
			||||||
                                label: 'aboutDeviceName'.tr(),
 | 
					                                  final value =
 | 
				
			||||||
                                value:
 | 
					                                      snapshot.hasData
 | 
				
			||||||
                                    _deviceInfo?.data['name'] ?? 'unknown'.tr(),
 | 
					                                          ? snapshot.data!
 | 
				
			||||||
 | 
					                                          : 'unknown'.tr();
 | 
				
			||||||
 | 
					                                  return _buildInfoItem(
 | 
				
			||||||
 | 
					                                    context,
 | 
				
			||||||
 | 
					                                    icon: Symbols.label,
 | 
				
			||||||
 | 
					                                    label: 'aboutDeviceName'.tr(),
 | 
				
			||||||
 | 
					                                    value: value,
 | 
				
			||||||
 | 
					                                  );
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
                              ),
 | 
					                              ),
 | 
				
			||||||
                              _buildInfoItem(
 | 
					                              _buildInfoItem(
 | 
				
			||||||
                                context,
 | 
					                                context,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,7 +32,7 @@ import 'package:island/widgets/content/cloud_files.dart';
 | 
				
			|||||||
import 'package:island/widgets/content/markdown.dart';
 | 
					import 'package:island/widgets/content/markdown.dart';
 | 
				
			||||||
import 'package:island/widgets/safety/abuse_report_helper.dart';
 | 
					import 'package:island/widgets/safety/abuse_report_helper.dart';
 | 
				
			||||||
import 'package:material_symbols_icons/symbols.dart';
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:palette_generator/palette_generator.dart';
 | 
					import 'package:island/services/color_extraction.dart';
 | 
				
			||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
import 'package:share_plus/share_plus.dart';
 | 
					import 'package:share_plus/share_plus.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
@@ -581,14 +581,14 @@ Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async {
 | 
				
			|||||||
  try {
 | 
					  try {
 | 
				
			||||||
    final account = await ref.watch(accountProvider(uname).future);
 | 
					    final account = await ref.watch(accountProvider(uname).future);
 | 
				
			||||||
    if (account.profile.background == null) return null;
 | 
					    if (account.profile.background == null) return null;
 | 
				
			||||||
    final palette = await PaletteGenerator.fromImageProvider(
 | 
					    final colors = await ColorExtractionService.getColorsFromImage(
 | 
				
			||||||
      CloudImageWidget.provider(
 | 
					      CloudImageWidget.provider(
 | 
				
			||||||
        fileId: account.profile.background!.id,
 | 
					        fileId: account.profile.background!.id,
 | 
				
			||||||
        serverUrl: ref.watch(serverUrlProvider),
 | 
					        serverUrl: ref.watch(serverUrlProvider),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    final dominantColor = palette.dominantColor?.color;
 | 
					    if (colors.isEmpty) return null;
 | 
				
			||||||
    if (dominantColor == null) return null;
 | 
					    final dominantColor = colors.first;
 | 
				
			||||||
    return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
 | 
					    return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
 | 
				
			||||||
  } catch (_) {
 | 
					  } catch (_) {
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,17 +1,10 @@
 | 
				
			|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:island/pods/network.dart';
 | 
					 | 
				
			||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
part 'captcha.config.g.dart';
 | 
					part 'captcha.config.g.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@riverpod
 | 
					@riverpod
 | 
				
			||||||
Future<String> captchaUrl(Ref ref) async {
 | 
					Future<String> captchaUrl(Ref ref) async {
 | 
				
			||||||
  final apiClient = ref.watch(apiClientProvider);
 | 
					  const baseUrl = "https://solian.app";
 | 
				
			||||||
  final resp = await apiClient.get('/.well-known/services');
 | 
					  return '$baseUrl/auth/captcha';
 | 
				
			||||||
  final serviceMapping = await resp.data;
 | 
					 | 
				
			||||||
  var baseUrl = serviceMapping['DysonNetwork.Pass'] as String;
 | 
					 | 
				
			||||||
  // The backend using self-signed certicates on development
 | 
					 | 
				
			||||||
  // Which mobile simulator might not accept, use this to avoid errors
 | 
					 | 
				
			||||||
  if (baseUrl.contains('https://localhost')) baseUrl = 'http://localhost:5216';
 | 
					 | 
				
			||||||
  return '$baseUrl/captcha';
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,6 @@
 | 
				
			|||||||
import 'dart:convert';
 | 
					import 'dart:convert';
 | 
				
			||||||
import 'dart:io';
 | 
					 | 
				
			||||||
import 'dart:math' as math;
 | 
					import 'dart:math' as math;
 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:animations/animations.dart';
 | 
					import 'package:animations/animations.dart';
 | 
				
			||||||
import 'package:device_info_plus/device_info_plus.dart';
 | 
					 | 
				
			||||||
import 'package:dio/dio.dart';
 | 
					import 'package:dio/dio.dart';
 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/foundation.dart';
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
@@ -42,22 +39,6 @@ final Map<int, (String, String, IconData)> kFactorTypes = {
 | 
				
			|||||||
  4: ('authFactorPin', 'authFactorPinDescription', Symbols.nest_secure_alarm),
 | 
					  4: ('authFactorPin', 'authFactorPinDescription', Symbols.nest_secure_alarm),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Future<String?> getDeviceName() async {
 | 
					 | 
				
			||||||
  if (kIsWeb) return null;
 | 
					 | 
				
			||||||
  String? name;
 | 
					 | 
				
			||||||
  if (Platform.isIOS) {
 | 
					 | 
				
			||||||
    final deviceInfo = await DeviceInfoPlugin().iosInfo;
 | 
					 | 
				
			||||||
    name = deviceInfo.name;
 | 
					 | 
				
			||||||
  } else if (Platform.isAndroid) {
 | 
					 | 
				
			||||||
    final deviceInfo = await DeviceInfoPlugin().androidInfo;
 | 
					 | 
				
			||||||
    name = deviceInfo.name;
 | 
					 | 
				
			||||||
  } else if (Platform.isWindows) {
 | 
					 | 
				
			||||||
    final deviceInfo = await DeviceInfoPlugin().windowsInfo;
 | 
					 | 
				
			||||||
    name = deviceInfo.computerName;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  return name;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class LoginScreen extends HookConsumerWidget {
 | 
					class LoginScreen extends HookConsumerWidget {
 | 
				
			||||||
  const LoginScreen({super.key});
 | 
					  const LoginScreen({super.key});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -289,7 +289,6 @@ class MessagesNotifier extends _$MessagesNotifier {
 | 
				
			|||||||
  bool? _withAttachments;
 | 
					  bool? _withAttachments;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  late final String _roomId;
 | 
					  late final String _roomId;
 | 
				
			||||||
  int _currentPage = 0;
 | 
					 | 
				
			||||||
  static const int _pageSize = 20;
 | 
					  static const int _pageSize = 20;
 | 
				
			||||||
  bool _hasMore = true;
 | 
					  bool _hasMore = true;
 | 
				
			||||||
  bool _isSyncing = false;
 | 
					  bool _isSyncing = false;
 | 
				
			||||||
@@ -565,47 +564,46 @@ class MessagesNotifier extends _$MessagesNotifier {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> loadInitial() async {
 | 
					Future<void> loadInitial() async {
 | 
				
			||||||
    developer.log('Loading initial messages', name: 'MessagesNotifier');
 | 
					  developer.log('Loading initial messages', name: 'MessagesNotifier');
 | 
				
			||||||
    if (_searchQuery == null || _searchQuery!.isEmpty) {
 | 
					  if (_searchQuery == null || _searchQuery!.isEmpty) {
 | 
				
			||||||
      syncMessages();
 | 
					    syncMessages();
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    final messages = await _getCachedMessages(offset: 0, take: 100);
 | 
					 | 
				
			||||||
    _currentPage = 0;
 | 
					 | 
				
			||||||
    _hasMore = messages.length == _pageSize;
 | 
					 | 
				
			||||||
    state = AsyncValue.data(messages);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> loadMore() async {
 | 
					  final messages = await _getCachedMessages(offset: 0, take: _pageSize);
 | 
				
			||||||
    if (!_hasMore || state is AsyncLoading) return;
 | 
					 | 
				
			||||||
    developer.log('Loading more messages', name: 'MessagesNotifier');
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					  _hasMore = messages.length == _pageSize;
 | 
				
			||||||
      final currentMessages = state.value ?? [];
 | 
					 | 
				
			||||||
      _currentPage++;
 | 
					 | 
				
			||||||
      final newMessages = await listMessages(
 | 
					 | 
				
			||||||
        offset: _currentPage * _pageSize,
 | 
					 | 
				
			||||||
        take: _pageSize,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (newMessages.isEmpty || newMessages.length < _pageSize) {
 | 
					  state = AsyncValue.data(messages);
 | 
				
			||||||
        _hasMore = false;
 | 
					}
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      state = AsyncValue.data(
 | 
					Future<void> loadMore() async {
 | 
				
			||||||
        _sortMessages([...currentMessages, ...newMessages]),
 | 
					  if (!_hasMore || state is AsyncLoading) return;
 | 
				
			||||||
      );
 | 
					  developer.log('Loading more messages', name: 'MessagesNotifier');
 | 
				
			||||||
    } catch (err, stackTrace) {
 | 
					
 | 
				
			||||||
      developer.log(
 | 
					  try {
 | 
				
			||||||
        'Error loading more messages',
 | 
					    final currentMessages = state.value ?? [];
 | 
				
			||||||
        name: 'MessagesNotifier',
 | 
					    final offset = currentMessages.length;
 | 
				
			||||||
        error: err,
 | 
					
 | 
				
			||||||
        stackTrace: stackTrace,
 | 
					    final newMessages = await listMessages(offset: offset, take: _pageSize);
 | 
				
			||||||
      );
 | 
					
 | 
				
			||||||
      showErrorAlert(err);
 | 
					    if (newMessages.isEmpty || newMessages.length < _pageSize) {
 | 
				
			||||||
      _currentPage--;
 | 
					      _hasMore = false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    state = AsyncValue.data(
 | 
				
			||||||
 | 
					      _sortMessages([...currentMessages, ...newMessages]),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  } catch (err, stackTrace) {
 | 
				
			||||||
 | 
					    developer.log(
 | 
				
			||||||
 | 
					      'Error loading more messages',
 | 
				
			||||||
 | 
					      name: 'MessagesNotifier',
 | 
				
			||||||
 | 
					      error: err,
 | 
				
			||||||
 | 
					      stackTrace: stackTrace,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    showErrorAlert(err);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> sendMessage(
 | 
					  Future<void> sendMessage(
 | 
				
			||||||
    String content,
 | 
					    String content,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,7 +39,7 @@ class NotificationUnreadCountNotifier
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      final client = ref.read(apiClientProvider);
 | 
					      final client = ref.read(apiClientProvider);
 | 
				
			||||||
      final response = await client.get('/pusher/notifications/count');
 | 
					      final response = await client.get('/ring/notifications/count');
 | 
				
			||||||
      return (response.data as num).toInt();
 | 
					      return (response.data as num).toInt();
 | 
				
			||||||
    } catch (_) {
 | 
					    } catch (_) {
 | 
				
			||||||
      return 0;
 | 
					      return 0;
 | 
				
			||||||
@@ -89,7 +89,7 @@ class NotificationListNotifier extends _$NotificationListNotifier
 | 
				
			|||||||
    final queryParams = {'offset': offset, 'take': _pageSize};
 | 
					    final queryParams = {'offset': offset, 'take': _pageSize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final response = await client.get(
 | 
					    final response = await client.get(
 | 
				
			||||||
      '/pusher/notifications',
 | 
					      '/ring/notifications',
 | 
				
			||||||
      queryParameters: queryParams,
 | 
					      queryParameters: queryParams,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    final total = int.parse(response.headers.value('X-Total') ?? '0');
 | 
					    final total = int.parse(response.headers.value('X-Total') ?? '0');
 | 
				
			||||||
@@ -121,7 +121,7 @@ class NotificationScreen extends HookConsumerWidget {
 | 
				
			|||||||
    Future<void> markAllRead() async {
 | 
					    Future<void> markAllRead() async {
 | 
				
			||||||
      showLoadingModal(context);
 | 
					      showLoadingModal(context);
 | 
				
			||||||
      final apiClient = ref.watch(apiClientProvider);
 | 
					      final apiClient = ref.watch(apiClientProvider);
 | 
				
			||||||
      await apiClient.post('/pusher/notifications/all/read');
 | 
					      await apiClient.post('/ring/notifications/all/read');
 | 
				
			||||||
      if (!context.mounted) return;
 | 
					      if (!context.mounted) return;
 | 
				
			||||||
      hideLoadingModal(context);
 | 
					      hideLoadingModal(context);
 | 
				
			||||||
      ref.invalidate(notificationListNotifierProvider);
 | 
					      ref.invalidate(notificationListNotifierProvider);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,6 @@ import 'dart:async';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
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_hooks/flutter_hooks.dart';
 | 
					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';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,7 +21,7 @@ import 'package:island/widgets/content/cloud_files.dart';
 | 
				
			|||||||
import 'package:island/widgets/content/markdown.dart';
 | 
					import 'package:island/widgets/content/markdown.dart';
 | 
				
			||||||
import 'package:island/widgets/post/post_list.dart';
 | 
					import 'package:island/widgets/post/post_list.dart';
 | 
				
			||||||
import 'package:material_symbols_icons/symbols.dart';
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:palette_generator/palette_generator.dart';
 | 
					import 'package:island/services/color_extraction.dart';
 | 
				
			||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -278,14 +278,14 @@ Future<Color?> publisherAppbarForcegroundColor(Ref ref, String pubName) async {
 | 
				
			|||||||
  try {
 | 
					  try {
 | 
				
			||||||
    final publisher = await ref.watch(publisherProvider(pubName).future);
 | 
					    final publisher = await ref.watch(publisherProvider(pubName).future);
 | 
				
			||||||
    if (publisher.background == null) return null;
 | 
					    if (publisher.background == null) return null;
 | 
				
			||||||
    final palette = await PaletteGenerator.fromImageProvider(
 | 
					    final colors = await ColorExtractionService.getColorsFromImage(
 | 
				
			||||||
      CloudImageWidget.provider(
 | 
					      CloudImageWidget.provider(
 | 
				
			||||||
        fileId: publisher.background!.id,
 | 
					        fileId: publisher.background!.id,
 | 
				
			||||||
        serverUrl: ref.watch(serverUrlProvider),
 | 
					        serverUrl: ref.watch(serverUrlProvider),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    final dominantColor = palette.dominantColor?.color;
 | 
					    if (colors.isEmpty) return null;
 | 
				
			||||||
    if (dominantColor == null) return null;
 | 
					    final dominantColor = colors.first;
 | 
				
			||||||
    return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
 | 
					    return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
 | 
				
			||||||
  } catch (_) {
 | 
					  } catch (_) {
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ import 'package:island/services/responsive.dart';
 | 
				
			|||||||
import 'package:island/widgets/account/account_pfc.dart';
 | 
					import 'package:island/widgets/account/account_pfc.dart';
 | 
				
			||||||
import 'package:island/widgets/account/status.dart';
 | 
					import 'package:island/widgets/account/status.dart';
 | 
				
			||||||
import 'package:island/widgets/post/post_list.dart';
 | 
					import 'package:island/widgets/post/post_list.dart';
 | 
				
			||||||
import 'package:palette_generator/palette_generator.dart';
 | 
					import 'package:island/services/color_extraction.dart';
 | 
				
			||||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
import 'package:gap/gap.dart';
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
import 'package:go_router/go_router.dart';
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
@@ -32,14 +32,14 @@ part 'realm_detail.g.dart';
 | 
				
			|||||||
Future<Color?> realmAppbarForegroundColor(Ref ref, String realmSlug) async {
 | 
					Future<Color?> realmAppbarForegroundColor(Ref ref, String realmSlug) async {
 | 
				
			||||||
  final realm = await ref.watch(realmProvider(realmSlug).future);
 | 
					  final realm = await ref.watch(realmProvider(realmSlug).future);
 | 
				
			||||||
  if (realm?.background == null) return null;
 | 
					  if (realm?.background == null) return null;
 | 
				
			||||||
  final palette = await PaletteGenerator.fromImageProvider(
 | 
					  final colors = await ColorExtractionService.getColorsFromImage(
 | 
				
			||||||
    CloudImageWidget.provider(
 | 
					    CloudImageWidget.provider(
 | 
				
			||||||
      fileId: realm!.background!.id,
 | 
					      fileId: realm!.background!.id,
 | 
				
			||||||
      serverUrl: ref.watch(serverUrlProvider),
 | 
					      serverUrl: ref.watch(serverUrlProvider),
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  final dominantColor = palette.dominantColor?.color;
 | 
					  if (colors.isEmpty) return null;
 | 
				
			||||||
  if (dominantColor == null) return null;
 | 
					  final dominantColor = colors.first;
 | 
				
			||||||
  return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
 | 
					  return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,11 +12,11 @@ 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/color_extraction.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:material_symbols_icons/symbols.dart';
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:palette_generator/palette_generator.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/pods/config.dart';
 | 
					import 'package:island/pods/config.dart';
 | 
				
			||||||
@@ -293,24 +293,26 @@ class SettingsScreen extends HookConsumerWidget {
 | 
				
			|||||||
              trailing: const Icon(Symbols.chevron_right),
 | 
					              trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
              onTap: () async {
 | 
					              onTap: () async {
 | 
				
			||||||
                showLoadingModal(context);
 | 
					                showLoadingModal(context);
 | 
				
			||||||
                final palette = await PaletteGenerator.fromImageProvider(
 | 
					                final colors = await ColorExtractionService.getColorsFromImage(
 | 
				
			||||||
                  FileImage(
 | 
					                  FileImage(
 | 
				
			||||||
                    File('${docBasepath.value}/$kAppBackgroundImagePath'),
 | 
					                    File('${docBasepath.value}/$kAppBackgroundImagePath'),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
                if (palette.darkVibrantColor == null ||
 | 
					                if (colors.isEmpty) {
 | 
				
			||||||
                    palette.lightVibrantColor == null) {
 | 
					 | 
				
			||||||
                  if (context.mounted) hideLoadingModal(context);
 | 
					                  if (context.mounted) hideLoadingModal(context);
 | 
				
			||||||
                  showErrorAlert(
 | 
					                  showErrorAlert(
 | 
				
			||||||
                    'Unable to calculate the domiant color of the background image.',
 | 
					                    'Unable to calculate the dominant color of the background image.',
 | 
				
			||||||
                  );
 | 
					                  );
 | 
				
			||||||
                  return;
 | 
					                  return;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                if (!context.mounted) return;
 | 
					                if (!context.mounted) return;
 | 
				
			||||||
 | 
					                final colorScheme = ColorScheme.fromSeed(
 | 
				
			||||||
 | 
					                  seedColor: colors.first,
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
                final color =
 | 
					                final color =
 | 
				
			||||||
                    MediaQuery.of(context).platformBrightness == Brightness.dark
 | 
					                    MediaQuery.of(context).platformBrightness == Brightness.dark
 | 
				
			||||||
                        ? palette.darkVibrantColor!.color
 | 
					                        ? colorScheme.primary
 | 
				
			||||||
                        : palette.lightVibrantColor!.color;
 | 
					                        : colorScheme.primary;
 | 
				
			||||||
                ref
 | 
					                ref
 | 
				
			||||||
                    .read(appSettingsNotifierProvider.notifier)
 | 
					                    .read(appSettingsNotifierProvider.notifier)
 | 
				
			||||||
                    .setAppColorScheme(color.value);
 | 
					                    .setAppColorScheme(color.value);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -48,11 +48,12 @@ class TrayService {
 | 
				
			|||||||
  void handleAction(MenuItem item) {
 | 
					  void handleAction(MenuItem item) {
 | 
				
			||||||
    switch (item.key) {
 | 
					    switch (item.key) {
 | 
				
			||||||
      case 'show_window':
 | 
					      case 'show_window':
 | 
				
			||||||
        if (appWindow.isVisible) {
 | 
					        () async {
 | 
				
			||||||
          appWindow.restore();
 | 
					        appWindow.show();
 | 
				
			||||||
        } else {
 | 
					        appWindow.restore();
 | 
				
			||||||
          appWindow.show();
 | 
					        await Future.delayed(const Duration(milliseconds: 32));
 | 
				
			||||||
        }
 | 
					        appWindow.show();
 | 
				
			||||||
 | 
					        }();
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      case 'exit_app':
 | 
					      case 'exit_app':
 | 
				
			||||||
        appWindow.close();
 | 
					        appWindow.close();
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										49
									
								
								lib/services/color_extraction.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								lib/services/color_extraction.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/widgets.dart';
 | 
				
			||||||
 | 
					import 'package:image/image.dart' as img;
 | 
				
			||||||
 | 
					import 'package:material_color_utilities/material_color_utilities.dart' as mcu;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ColorExtractionService {
 | 
				
			||||||
 | 
					  /// Extracts dominant colors from an image provider.
 | 
				
			||||||
 | 
					  /// Returns a list of colors suitable for UI theming.
 | 
				
			||||||
 | 
					  static Future<List<Color>> getColorsFromImage(ImageProvider provider) async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      if (provider is FileImage) {
 | 
				
			||||||
 | 
					        final bytes = await provider.file.readAsBytes();
 | 
				
			||||||
 | 
					        final image = img.decodeImage(bytes);
 | 
				
			||||||
 | 
					        if (image == null) return [];
 | 
				
			||||||
 | 
					        final Map<int, int> colorToCount = {};
 | 
				
			||||||
 | 
					        for (int y = 0; y < image.height; y++) {
 | 
				
			||||||
 | 
					          for (int x = 0; x < image.width; x++) {
 | 
				
			||||||
 | 
					            final pixel = image.getPixel(x, y) as int;
 | 
				
			||||||
 | 
					            final r = (pixel >> 24) & 0xff;
 | 
				
			||||||
 | 
					            final g = (pixel >> 16) & 0xff;
 | 
				
			||||||
 | 
					            final b = (pixel >> 8) & 0xff;
 | 
				
			||||||
 | 
					            final a = pixel & 0xff;
 | 
				
			||||||
 | 
					            if (a == 0) continue;
 | 
				
			||||||
 | 
					            final argb = (a << 24) | (r << 16) | (g << 8) | b;
 | 
				
			||||||
 | 
					            colorToCount[argb] = (colorToCount[argb] ?? 0) + 1;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        final List<int> filteredResults = mcu.Score.score(
 | 
				
			||||||
 | 
					          colorToCount,
 | 
				
			||||||
 | 
					          desired: 1,
 | 
				
			||||||
 | 
					          filter: true,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        final List<int> scoredResults = mcu.Score.score(
 | 
				
			||||||
 | 
					          colorToCount,
 | 
				
			||||||
 | 
					          desired: 4,
 | 
				
			||||||
 | 
					          filter: false,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return <dynamic>{
 | 
				
			||||||
 | 
					          ...filteredResults,
 | 
				
			||||||
 | 
					          ...scoredResults,
 | 
				
			||||||
 | 
					        }.toList().map((argb) => Color(argb)).toList();
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        return [];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      debugPrint('Error getting colors from image: $e');
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,15 +1,15 @@
 | 
				
			|||||||
import 'dart:async';
 | 
					import 'dart:async';
 | 
				
			||||||
import 'dart:convert';
 | 
					 | 
				
			||||||
import 'dart:io';
 | 
					import 'dart:io';
 | 
				
			||||||
import 'dart:ui';
 | 
					import 'dart:ui';
 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:croppy/croppy.dart';
 | 
					import 'package:croppy/croppy.dart';
 | 
				
			||||||
import 'package:cross_file/cross_file.dart';
 | 
					import 'package:cross_file/cross_file.dart';
 | 
				
			||||||
 | 
					import 'package:dio/dio.dart';
 | 
				
			||||||
import 'package:flutter/foundation.dart';
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
import 'package:flutter/widgets.dart';
 | 
					import 'package:flutter/widgets.dart';
 | 
				
			||||||
import 'package:island/models/file.dart';
 | 
					import 'package:island/models/file.dart';
 | 
				
			||||||
 | 
					import 'package:island/services/file_uploader.dart';
 | 
				
			||||||
import 'package:native_exif/native_exif.dart';
 | 
					import 'package:native_exif/native_exif.dart';
 | 
				
			||||||
import 'package:tus_client_dart/tus_client_dart.dart';
 | 
					import 'package:path_provider/path_provider.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Future<XFile?> cropImage(
 | 
					Future<XFile?> cropImage(
 | 
				
			||||||
  BuildContext context, {
 | 
					  BuildContext context, {
 | 
				
			||||||
@@ -44,6 +44,7 @@ Completer<SnCloudFile?> putMediaToCloud({
 | 
				
			|||||||
  required UniversalFile fileData,
 | 
					  required UniversalFile fileData,
 | 
				
			||||||
  required String atk,
 | 
					  required String atk,
 | 
				
			||||||
  required String baseUrl,
 | 
					  required String baseUrl,
 | 
				
			||||||
 | 
					  String? poolId,
 | 
				
			||||||
  String? filename,
 | 
					  String? filename,
 | 
				
			||||||
  String? mimetype,
 | 
					  String? mimetype,
 | 
				
			||||||
  Function(double progress, Duration estimate)? onProgress,
 | 
					  Function(double progress, Duration estimate)? onProgress,
 | 
				
			||||||
@@ -85,6 +86,7 @@ Completer<SnCloudFile?> putMediaToCloud({
 | 
				
			|||||||
              fileData,
 | 
					              fileData,
 | 
				
			||||||
              atk,
 | 
					              atk,
 | 
				
			||||||
              baseUrl,
 | 
					              baseUrl,
 | 
				
			||||||
 | 
					              poolId,
 | 
				
			||||||
              filename,
 | 
					              filename,
 | 
				
			||||||
              mimetype,
 | 
					              mimetype,
 | 
				
			||||||
              onProgress,
 | 
					              onProgress,
 | 
				
			||||||
@@ -98,6 +100,7 @@ Completer<SnCloudFile?> putMediaToCloud({
 | 
				
			|||||||
              fileData,
 | 
					              fileData,
 | 
				
			||||||
              atk,
 | 
					              atk,
 | 
				
			||||||
              baseUrl,
 | 
					              baseUrl,
 | 
				
			||||||
 | 
					              poolId,
 | 
				
			||||||
              filename,
 | 
					              filename,
 | 
				
			||||||
              mimetype,
 | 
					              mimetype,
 | 
				
			||||||
              onProgress,
 | 
					              onProgress,
 | 
				
			||||||
@@ -114,6 +117,7 @@ Completer<SnCloudFile?> putMediaToCloud({
 | 
				
			|||||||
    fileData,
 | 
					    fileData,
 | 
				
			||||||
    atk,
 | 
					    atk,
 | 
				
			||||||
    baseUrl,
 | 
					    baseUrl,
 | 
				
			||||||
 | 
					    poolId,
 | 
				
			||||||
    filename,
 | 
					    filename,
 | 
				
			||||||
    mimetype,
 | 
					    mimetype,
 | 
				
			||||||
    onProgress,
 | 
					    onProgress,
 | 
				
			||||||
@@ -127,6 +131,7 @@ Completer<SnCloudFile?> _processUpload(
 | 
				
			|||||||
  UniversalFile fileData,
 | 
					  UniversalFile fileData,
 | 
				
			||||||
  String atk,
 | 
					  String atk,
 | 
				
			||||||
  String baseUrl,
 | 
					  String baseUrl,
 | 
				
			||||||
 | 
					  String? poolId,
 | 
				
			||||||
  String? filename,
 | 
					  String? filename,
 | 
				
			||||||
  String? mimetype,
 | 
					  String? mimetype,
 | 
				
			||||||
  Function(double progress, Duration estimate)? onProgress,
 | 
					  Function(double progress, Duration estimate)? onProgress,
 | 
				
			||||||
@@ -168,26 +173,80 @@ Completer<SnCloudFile?> _processUpload(
 | 
				
			|||||||
    return completer;
 | 
					    return completer;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final Map<String, String> metadata = {
 | 
					  // Create Dio instance
 | 
				
			||||||
    'filename': actualFilename,
 | 
					  final dio = Dio(
 | 
				
			||||||
    'content-type': actualMimetype,
 | 
					    BaseOptions(
 | 
				
			||||||
  };
 | 
					      baseUrl: baseUrl,
 | 
				
			||||||
 | 
					      headers: {
 | 
				
			||||||
 | 
					        'Authorization': 'AtField $atk',
 | 
				
			||||||
 | 
					        'Accept': 'application/json',
 | 
				
			||||||
 | 
					        'Content-Type': 'application/json',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final client = TusClient(file);
 | 
					  final uploader = FileUploader(dio);
 | 
				
			||||||
  client
 | 
					
 | 
				
			||||||
      .upload(
 | 
					  // Get File object
 | 
				
			||||||
        uri: Uri.parse('$baseUrl/drive/tus'),
 | 
					  File fileObj;
 | 
				
			||||||
        headers: {'Authorization': 'AtField $atk'},
 | 
					  if (file.path.isNotEmpty) {
 | 
				
			||||||
        metadata: metadata,
 | 
					    fileObj = File(file.path);
 | 
				
			||||||
        onComplete: (lastResponse) {
 | 
					    // Call progress start
 | 
				
			||||||
          final resp = jsonDecode(lastResponse!.headers['x-fileinfo']!);
 | 
					    onProgress?.call(0.0, Duration.zero);
 | 
				
			||||||
          completer.complete(SnCloudFile.fromJson(resp));
 | 
					    uploader
 | 
				
			||||||
        },
 | 
					        .uploadFile(
 | 
				
			||||||
        onProgress: (double progress, Duration estimate) {
 | 
					          file: fileObj,
 | 
				
			||||||
          onProgress?.call(progress, estimate);
 | 
					          fileName: actualFilename,
 | 
				
			||||||
        },
 | 
					          contentType: actualMimetype,
 | 
				
			||||||
      )
 | 
					          poolId: poolId,
 | 
				
			||||||
      .catchError(completer.completeError);
 | 
					        )
 | 
				
			||||||
 | 
					        .then((result) {
 | 
				
			||||||
 | 
					          // Call progress end
 | 
				
			||||||
 | 
					          onProgress?.call(1.0, Duration.zero);
 | 
				
			||||||
 | 
					          completer.complete(result);
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .catchError((e) {
 | 
				
			||||||
 | 
					          completer.completeError(e);
 | 
				
			||||||
 | 
					          throw e;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    // Write to temp file
 | 
				
			||||||
 | 
					    getTemporaryDirectory()
 | 
				
			||||||
 | 
					        .then((tempDir) {
 | 
				
			||||||
 | 
					          final tempFile = File('${tempDir.path}/temp_upload_$actualFilename');
 | 
				
			||||||
 | 
					          tempFile
 | 
				
			||||||
 | 
					              .writeAsBytes(byteData!)
 | 
				
			||||||
 | 
					              .then((_) {
 | 
				
			||||||
 | 
					                fileObj = tempFile;
 | 
				
			||||||
 | 
					                // Call progress start
 | 
				
			||||||
 | 
					                onProgress?.call(0.0, Duration.zero);
 | 
				
			||||||
 | 
					                uploader
 | 
				
			||||||
 | 
					                    .uploadFile(
 | 
				
			||||||
 | 
					                      file: fileObj,
 | 
				
			||||||
 | 
					                      fileName: actualFilename,
 | 
				
			||||||
 | 
					                      contentType: actualMimetype,
 | 
				
			||||||
 | 
					                      poolId: poolId,
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .then((result) {
 | 
				
			||||||
 | 
					                      // Call progress end
 | 
				
			||||||
 | 
					                      onProgress?.call(1.0, Duration.zero);
 | 
				
			||||||
 | 
					                      completer.complete(result);
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    .catchError((e) {
 | 
				
			||||||
 | 
					                      completer.completeError(e);
 | 
				
			||||||
 | 
					                      throw e;
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					              })
 | 
				
			||||||
 | 
					              .catchError((e) {
 | 
				
			||||||
 | 
					                completer.completeError(e);
 | 
				
			||||||
 | 
					                throw e;
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .catchError((e) {
 | 
				
			||||||
 | 
					          completer.completeError(e);
 | 
				
			||||||
 | 
					          throw e;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return completer;
 | 
					  return completer;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										155
									
								
								lib/services/file_uploader.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								lib/services/file_uploader.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,155 @@
 | 
				
			|||||||
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					import 'dart:typed_data';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:crypto/crypto.dart';
 | 
				
			||||||
 | 
					import 'package:dio/dio.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:island/models/file.dart';
 | 
				
			||||||
 | 
					import 'package:island/pods/network.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FileUploader {
 | 
				
			||||||
 | 
					  final Dio _dio;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  FileUploader(this._dio);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Calculates the MD5 hash of a file.
 | 
				
			||||||
 | 
					  Future<String> _calculateFileHash(File file) async {
 | 
				
			||||||
 | 
					    final bytes = await file.readAsBytes();
 | 
				
			||||||
 | 
					    final digest = md5.convert(bytes);
 | 
				
			||||||
 | 
					    return digest.toString();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Creates an upload task for the given file.
 | 
				
			||||||
 | 
					  Future<Map<String, dynamic>> createUploadTask({
 | 
				
			||||||
 | 
					    required File file,
 | 
				
			||||||
 | 
					    required String fileName,
 | 
				
			||||||
 | 
					    required String contentType,
 | 
				
			||||||
 | 
					    String? poolId,
 | 
				
			||||||
 | 
					    String? bundleId,
 | 
				
			||||||
 | 
					    String? encryptPassword,
 | 
				
			||||||
 | 
					    String? expiredAt,
 | 
				
			||||||
 | 
					    int? chunkSize,
 | 
				
			||||||
 | 
					  }) async {
 | 
				
			||||||
 | 
					    final hash = await _calculateFileHash(file);
 | 
				
			||||||
 | 
					    final fileSize = await file.length();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final response = await _dio.post(
 | 
				
			||||||
 | 
					      '/drive/files/upload/create',
 | 
				
			||||||
 | 
					      data: {
 | 
				
			||||||
 | 
					        'hash': hash,
 | 
				
			||||||
 | 
					        'file_name': fileName,
 | 
				
			||||||
 | 
					        'file_size': fileSize,
 | 
				
			||||||
 | 
					        'content_type': contentType,
 | 
				
			||||||
 | 
					        'pool_id': poolId,
 | 
				
			||||||
 | 
					        'bundle_id': bundleId,
 | 
				
			||||||
 | 
					        'encrypt_password': encryptPassword,
 | 
				
			||||||
 | 
					        'expired_at': expiredAt,
 | 
				
			||||||
 | 
					        'chunk_size': chunkSize,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return response.data;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Uploads a single chunk of the file.
 | 
				
			||||||
 | 
					  Future<void> uploadChunk({
 | 
				
			||||||
 | 
					    required String taskId,
 | 
				
			||||||
 | 
					    required int chunkIndex,
 | 
				
			||||||
 | 
					    required Uint8List chunkData,
 | 
				
			||||||
 | 
					  }) async {
 | 
				
			||||||
 | 
					    final formData = FormData.fromMap({
 | 
				
			||||||
 | 
					      'chunk': MultipartFile.fromBytes(
 | 
				
			||||||
 | 
					        chunkData,
 | 
				
			||||||
 | 
					        filename: 'chunk_$chunkIndex',
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await _dio.post(
 | 
				
			||||||
 | 
					      '/drive/files/upload/chunk/$taskId/$chunkIndex',
 | 
				
			||||||
 | 
					      data: formData,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Completes the upload and returns the CloudFile object.
 | 
				
			||||||
 | 
					  Future<SnCloudFile> completeUpload(String taskId) async {
 | 
				
			||||||
 | 
					    final response = await _dio.post('/drive/files/upload/complete/$taskId');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return SnCloudFile.fromJson(response.data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Uploads a file in chunks using the multi-part API.
 | 
				
			||||||
 | 
					  Future<SnCloudFile> uploadFile({
 | 
				
			||||||
 | 
					    required File file,
 | 
				
			||||||
 | 
					    required String fileName,
 | 
				
			||||||
 | 
					    required String contentType,
 | 
				
			||||||
 | 
					    String? poolId,
 | 
				
			||||||
 | 
					    String? bundleId,
 | 
				
			||||||
 | 
					    String? encryptPassword,
 | 
				
			||||||
 | 
					    String? expiredAt,
 | 
				
			||||||
 | 
					    int? customChunkSize,
 | 
				
			||||||
 | 
					  }) async {
 | 
				
			||||||
 | 
					    // Step 1: Create upload task
 | 
				
			||||||
 | 
					    final createResponse = await createUploadTask(
 | 
				
			||||||
 | 
					      file: file,
 | 
				
			||||||
 | 
					      fileName: fileName,
 | 
				
			||||||
 | 
					      contentType: contentType,
 | 
				
			||||||
 | 
					      poolId: poolId,
 | 
				
			||||||
 | 
					      bundleId: bundleId,
 | 
				
			||||||
 | 
					      encryptPassword: encryptPassword,
 | 
				
			||||||
 | 
					      expiredAt: expiredAt,
 | 
				
			||||||
 | 
					      chunkSize: customChunkSize,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (createResponse['file_exists'] == true) {
 | 
				
			||||||
 | 
					      // File already exists, return the existing file
 | 
				
			||||||
 | 
					      return SnCloudFile.fromJson(createResponse['file']);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final taskId = createResponse['task_id'] as String;
 | 
				
			||||||
 | 
					    final chunkSize = createResponse['chunk_size'] as int;
 | 
				
			||||||
 | 
					    final chunksCount = createResponse['chunks_count'] as int;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Step 2: Upload chunks
 | 
				
			||||||
 | 
					    final stream = file.openRead();
 | 
				
			||||||
 | 
					    final chunks = <Uint8List>[];
 | 
				
			||||||
 | 
					    int bytesRead = 0;
 | 
				
			||||||
 | 
					    final buffer = BytesBuilder();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await for (final chunk in stream) {
 | 
				
			||||||
 | 
					      buffer.add(chunk);
 | 
				
			||||||
 | 
					      bytesRead += chunk.length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (bytesRead >= chunkSize) {
 | 
				
			||||||
 | 
					        chunks.add(buffer.takeBytes());
 | 
				
			||||||
 | 
					        bytesRead = 0;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Add remaining bytes as last chunk
 | 
				
			||||||
 | 
					    if (buffer.length > 0) {
 | 
				
			||||||
 | 
					      chunks.add(buffer.takeBytes());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Ensure we have the correct number of chunks
 | 
				
			||||||
 | 
					    if (chunks.length != chunksCount) {
 | 
				
			||||||
 | 
					      throw Exception(
 | 
				
			||||||
 | 
					        'Chunk count mismatch: expected $chunksCount, got ${chunks.length}',
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Upload each chunk
 | 
				
			||||||
 | 
					    for (int i = 0; i < chunks.length; i++) {
 | 
				
			||||||
 | 
					      await uploadChunk(taskId: taskId, chunkIndex: i, chunkData: chunks[i]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Step 3: Complete upload
 | 
				
			||||||
 | 
					    return await completeUpload(taskId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Riverpod provider for the FileUploader service
 | 
				
			||||||
 | 
					final fileUploaderProvider = Provider<FileUploader>((ref) {
 | 
				
			||||||
 | 
					  final dio = ref.watch(apiClientProvider);
 | 
				
			||||||
 | 
					  return FileUploader(dio);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@@ -1,230 +1,47 @@
 | 
				
			|||||||
import 'dart:async';
 | 
					import 'dart:async';
 | 
				
			||||||
import 'dart:developer';
 | 
					 | 
				
			||||||
import 'dart:io';
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:dio/dio.dart';
 | 
					import 'package:dio/dio.dart';
 | 
				
			||||||
import 'package:firebase_messaging/firebase_messaging.dart';
 | 
					 | 
				
			||||||
import 'package:flutter/foundation.dart';
 | 
					 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
 | 
					 | 
				
			||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
					import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
				
			||||||
import 'package:go_router/go_router.dart';
 | 
					 | 
				
			||||||
import 'package:island/main.dart';
 | 
					 | 
				
			||||||
import 'package:island/route.dart';
 | 
					 | 
				
			||||||
import 'package:island/models/account.dart';
 | 
					 | 
				
			||||||
import 'package:island/pods/websocket.dart';
 | 
					 | 
				
			||||||
import 'package:island/widgets/app_notification.dart';
 | 
					 | 
				
			||||||
import 'package:top_snackbar_flutter/top_snack_bar.dart';
 | 
					 | 
				
			||||||
import 'package:url_launcher/url_launcher_string.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
 | 
					// Conditional imports based on platform
 | 
				
			||||||
    FlutterLocalNotificationsPlugin();
 | 
					import 'notify.windows.dart' as windows_notify;
 | 
				
			||||||
 | 
					import 'notify.universal.dart' as universal_notify;
 | 
				
			||||||
AppLifecycleState _appLifecycleState = AppLifecycleState.resumed;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void _onAppLifecycleChanged(AppLifecycleState state) {
 | 
					 | 
				
			||||||
  _appLifecycleState = state;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Platform-specific delegation
 | 
				
			||||||
Future<void> initializeLocalNotifications() async {
 | 
					Future<void> initializeLocalNotifications() async {
 | 
				
			||||||
  const AndroidInitializationSettings initializationSettingsAndroid =
 | 
					  if (Platform.isWindows) {
 | 
				
			||||||
      AndroidInitializationSettings('@mipmap/ic_launcher');
 | 
					    return windows_notify.initializeLocalNotifications();
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
  const DarwinInitializationSettings initializationSettingsIOS =
 | 
					    return universal_notify.initializeLocalNotifications();
 | 
				
			||||||
      DarwinInitializationSettings();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const DarwinInitializationSettings initializationSettingsMacOS =
 | 
					 | 
				
			||||||
      DarwinInitializationSettings();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const LinuxInitializationSettings initializationSettingsLinux =
 | 
					 | 
				
			||||||
      LinuxInitializationSettings(defaultActionName: 'Open notification');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const WindowsInitializationSettings initializationSettingsWindows =
 | 
					 | 
				
			||||||
      WindowsInitializationSettings(
 | 
					 | 
				
			||||||
        appName: 'Island',
 | 
					 | 
				
			||||||
        appUserModelId: 'dev.solsynth.solian',
 | 
					 | 
				
			||||||
        guid: 'dev.solsynth.solian',
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const InitializationSettings initializationSettings = InitializationSettings(
 | 
					 | 
				
			||||||
    android: initializationSettingsAndroid,
 | 
					 | 
				
			||||||
    iOS: initializationSettingsIOS,
 | 
					 | 
				
			||||||
    macOS: initializationSettingsMacOS,
 | 
					 | 
				
			||||||
    linux: initializationSettingsLinux,
 | 
					 | 
				
			||||||
    windows: initializationSettingsWindows,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  await flutterLocalNotificationsPlugin.initialize(
 | 
					 | 
				
			||||||
    initializationSettings,
 | 
					 | 
				
			||||||
    onDidReceiveNotificationResponse: (NotificationResponse response) async {
 | 
					 | 
				
			||||||
      final payload = response.payload;
 | 
					 | 
				
			||||||
      if (payload != null) {
 | 
					 | 
				
			||||||
        if (payload.startsWith('/')) {
 | 
					 | 
				
			||||||
          // In-app routes
 | 
					 | 
				
			||||||
          rootNavigatorKey.currentContext?.push(payload);
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          // External URLs
 | 
					 | 
				
			||||||
          launchUrlString(payload);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  WidgetsBinding.instance.addObserver(
 | 
					 | 
				
			||||||
    LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged),
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class LifecycleEventHandler extends WidgetsBindingObserver {
 | 
					 | 
				
			||||||
  final void Function(AppLifecycleState) onAppLifecycleChanged;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  LifecycleEventHandler({required this.onAppLifecycleChanged});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  void didChangeAppLifecycleState(AppLifecycleState state) {
 | 
					 | 
				
			||||||
    onAppLifecycleChanged(state);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
StreamSubscription<WebSocketPacket> setupNotificationListener(
 | 
					StreamSubscription setupNotificationListener(
 | 
				
			||||||
  BuildContext context,
 | 
					  BuildContext context,
 | 
				
			||||||
  WidgetRef ref,
 | 
					  WidgetRef ref,
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  final ws = ref.watch(websocketProvider);
 | 
					  if (Platform.isWindows) {
 | 
				
			||||||
  return ws.dataStream.listen((pkt) async {
 | 
					    return windows_notify.setupNotificationListener(context, ref);
 | 
				
			||||||
    if (pkt.type == "notifications.new") {
 | 
					  } else {
 | 
				
			||||||
      final notification = SnNotification.fromJson(pkt.data!);
 | 
					    return universal_notify.setupNotificationListener(context, ref);
 | 
				
			||||||
      if (_appLifecycleState == AppLifecycleState.resumed) {
 | 
					  }
 | 
				
			||||||
        // App is focused, show in-app notification
 | 
					 | 
				
			||||||
        log(
 | 
					 | 
				
			||||||
          '[Notification] Showing in-app notification: ${notification.title}',
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        showTopSnackBar(
 | 
					 | 
				
			||||||
          globalOverlay.currentState!,
 | 
					 | 
				
			||||||
          Center(
 | 
					 | 
				
			||||||
            child: ConstrainedBox(
 | 
					 | 
				
			||||||
              constraints: const BoxConstraints(maxWidth: 480),
 | 
					 | 
				
			||||||
              child: NotificationCard(notification: notification),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          onTap: () {
 | 
					 | 
				
			||||||
            if (notification.meta['action_uri'] != null) {
 | 
					 | 
				
			||||||
              var uri = notification.meta['action_uri'] as String;
 | 
					 | 
				
			||||||
              if (uri.startsWith('/')) {
 | 
					 | 
				
			||||||
                // In-app routes
 | 
					 | 
				
			||||||
                rootNavigatorKey.currentContext?.push(
 | 
					 | 
				
			||||||
                  notification.meta['action_uri'],
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
              } else {
 | 
					 | 
				
			||||||
                // External URLs
 | 
					 | 
				
			||||||
                launchUrlString(uri);
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onDismissed: () {},
 | 
					 | 
				
			||||||
          dismissType: DismissType.onSwipe,
 | 
					 | 
				
			||||||
          displayDuration: const Duration(seconds: 5),
 | 
					 | 
				
			||||||
          snackBarPosition: SnackBarPosition.top,
 | 
					 | 
				
			||||||
          padding: EdgeInsets.only(
 | 
					 | 
				
			||||||
            left: 16,
 | 
					 | 
				
			||||||
            right: 16,
 | 
					 | 
				
			||||||
            top:
 | 
					 | 
				
			||||||
                (!kIsWeb &&
 | 
					 | 
				
			||||||
                        (Platform.isMacOS ||
 | 
					 | 
				
			||||||
                            Platform.isWindows ||
 | 
					 | 
				
			||||||
                            Platform.isLinux))
 | 
					 | 
				
			||||||
                    ? 28
 | 
					 | 
				
			||||||
                    // ignore: use_build_context_synchronously
 | 
					 | 
				
			||||||
                    : MediaQuery.of(context).padding.top + 16,
 | 
					 | 
				
			||||||
            bottom: 16,
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        // App is in background, show system notification (only on supported platforms)
 | 
					 | 
				
			||||||
        if (!kIsWeb && !Platform.isIOS) {
 | 
					 | 
				
			||||||
          log(
 | 
					 | 
				
			||||||
            '[Notification] Showing system notification: ${notification.title}',
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          const AndroidNotificationDetails androidNotificationDetails =
 | 
					 | 
				
			||||||
              AndroidNotificationDetails(
 | 
					 | 
				
			||||||
                'channel_id',
 | 
					 | 
				
			||||||
                'channel_name',
 | 
					 | 
				
			||||||
                channelDescription: 'channel_description',
 | 
					 | 
				
			||||||
                importance: Importance.max,
 | 
					 | 
				
			||||||
                priority: Priority.high,
 | 
					 | 
				
			||||||
                ticker: 'ticker',
 | 
					 | 
				
			||||||
              );
 | 
					 | 
				
			||||||
          const NotificationDetails notificationDetails = NotificationDetails(
 | 
					 | 
				
			||||||
            android: androidNotificationDetails,
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          await flutterLocalNotificationsPlugin.show(
 | 
					 | 
				
			||||||
            0,
 | 
					 | 
				
			||||||
            notification.title,
 | 
					 | 
				
			||||||
            notification.content,
 | 
					 | 
				
			||||||
            notificationDetails,
 | 
					 | 
				
			||||||
            payload: notification.meta['action_uri'] as String?,
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          log(
 | 
					 | 
				
			||||||
            '[Notification] Skipping system notification for unsupported platform: ${notification.title}',
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Future<void> subscribePushNotification(
 | 
					Future<void> subscribePushNotification(
 | 
				
			||||||
  Dio apiClient, {
 | 
					  Dio apiClient, {
 | 
				
			||||||
  bool detailedErrors = false,
 | 
					  bool detailedErrors = false,
 | 
				
			||||||
}) async {
 | 
					}) async {
 | 
				
			||||||
  if (!kIsWeb && Platform.isLinux) {
 | 
					  if (Platform.isWindows) {
 | 
				
			||||||
    return;
 | 
					    return windows_notify.subscribePushNotification(
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  await FirebaseMessaging.instance.requestPermission(
 | 
					 | 
				
			||||||
    alert: true,
 | 
					 | 
				
			||||||
    badge: true,
 | 
					 | 
				
			||||||
    sound: true,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  String? deviceToken;
 | 
					 | 
				
			||||||
  if (kIsWeb) {
 | 
					 | 
				
			||||||
    deviceToken = await FirebaseMessaging.instance.getToken(
 | 
					 | 
				
			||||||
      vapidKey:
 | 
					 | 
				
			||||||
          "BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU",
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  } else if (Platform.isAndroid) {
 | 
					 | 
				
			||||||
    deviceToken = await FirebaseMessaging.instance.getToken();
 | 
					 | 
				
			||||||
  } else if (Platform.isIOS) {
 | 
					 | 
				
			||||||
    deviceToken = await FirebaseMessaging.instance.getAPNSToken();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  FirebaseMessaging.instance.onTokenRefresh
 | 
					 | 
				
			||||||
      .listen((fcmToken) {
 | 
					 | 
				
			||||||
        _putTokenToRemote(apiClient, fcmToken, 1);
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .onError((err) {
 | 
					 | 
				
			||||||
        log("Failed to get firebase cloud messaging push token: $err");
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (deviceToken != null) {
 | 
					 | 
				
			||||||
    _putTokenToRemote(
 | 
					 | 
				
			||||||
      apiClient,
 | 
					      apiClient,
 | 
				
			||||||
      deviceToken,
 | 
					      detailedErrors: detailedErrors,
 | 
				
			||||||
      !kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1,
 | 
					    );
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return universal_notify.subscribePushNotification(
 | 
				
			||||||
 | 
					      apiClient,
 | 
				
			||||||
 | 
					      detailedErrors: detailedErrors,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  } else if (detailedErrors) {
 | 
					 | 
				
			||||||
    throw Exception("Failed to get device token for push notifications.");
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
Future<void> _putTokenToRemote(
 | 
					 | 
				
			||||||
  Dio apiClient,
 | 
					 | 
				
			||||||
  String token,
 | 
					 | 
				
			||||||
  int provider,
 | 
					 | 
				
			||||||
) async {
 | 
					 | 
				
			||||||
  await apiClient.put(
 | 
					 | 
				
			||||||
    "/pusher/notifications/subscription",
 | 
					 | 
				
			||||||
    data: {"provider": provider, "device_token": token},
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										232
									
								
								lib/services/notify.universal.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								lib/services/notify.universal.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,232 @@
 | 
				
			|||||||
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					import 'dart:developer';
 | 
				
			||||||
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:dio/dio.dart';
 | 
				
			||||||
 | 
					import 'package:firebase_messaging/firebase_messaging.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_local_notifications/flutter_local_notifications.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
 | 
					import 'package:island/main.dart';
 | 
				
			||||||
 | 
					import 'package:island/route.dart';
 | 
				
			||||||
 | 
					import 'package:island/models/account.dart';
 | 
				
			||||||
 | 
					import 'package:island/pods/websocket.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/app_notification.dart';
 | 
				
			||||||
 | 
					import 'package:top_snackbar_flutter/top_snack_bar.dart';
 | 
				
			||||||
 | 
					import 'package:url_launcher/url_launcher_string.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
 | 
				
			||||||
 | 
					    FlutterLocalNotificationsPlugin();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					AppLifecycleState _appLifecycleState = AppLifecycleState.resumed;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void _onAppLifecycleChanged(AppLifecycleState state) {
 | 
				
			||||||
 | 
					  _appLifecycleState = state;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Future<void> initializeLocalNotifications() async {
 | 
				
			||||||
 | 
					  const AndroidInitializationSettings initializationSettingsAndroid =
 | 
				
			||||||
 | 
					      AndroidInitializationSettings('@mipmap/ic_launcher');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const DarwinInitializationSettings initializationSettingsIOS =
 | 
				
			||||||
 | 
					      DarwinInitializationSettings();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const DarwinInitializationSettings initializationSettingsMacOS =
 | 
				
			||||||
 | 
					      DarwinInitializationSettings();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const LinuxInitializationSettings initializationSettingsLinux =
 | 
				
			||||||
 | 
					      LinuxInitializationSettings(defaultActionName: 'Open notification');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const WindowsInitializationSettings initializationSettingsWindows =
 | 
				
			||||||
 | 
					      WindowsInitializationSettings(
 | 
				
			||||||
 | 
					        appName: 'Island',
 | 
				
			||||||
 | 
					        appUserModelId: 'dev.solsynth.solian',
 | 
				
			||||||
 | 
					        guid: 'dev.solsynth.solian',
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const InitializationSettings initializationSettings = InitializationSettings(
 | 
				
			||||||
 | 
					    android: initializationSettingsAndroid,
 | 
				
			||||||
 | 
					    iOS: initializationSettingsIOS,
 | 
				
			||||||
 | 
					    macOS: initializationSettingsMacOS,
 | 
				
			||||||
 | 
					    linux: initializationSettingsLinux,
 | 
				
			||||||
 | 
					    windows: initializationSettingsWindows,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await flutterLocalNotificationsPlugin.initialize(
 | 
				
			||||||
 | 
					    initializationSettings,
 | 
				
			||||||
 | 
					    onDidReceiveNotificationResponse: (NotificationResponse response) async {
 | 
				
			||||||
 | 
					      final payload = response.payload;
 | 
				
			||||||
 | 
					      if (payload != null) {
 | 
				
			||||||
 | 
					        if (payload.startsWith('/')) {
 | 
				
			||||||
 | 
					          // In-app routes
 | 
				
			||||||
 | 
					          rootNavigatorKey.currentContext?.push(payload);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          // External URLs
 | 
				
			||||||
 | 
					          launchUrlString(payload);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  WidgetsBinding.instance.addObserver(
 | 
				
			||||||
 | 
					    LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LifecycleEventHandler extends WidgetsBindingObserver {
 | 
				
			||||||
 | 
					  final void Function(AppLifecycleState) onAppLifecycleChanged;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  LifecycleEventHandler({required this.onAppLifecycleChanged});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void didChangeAppLifecycleState(AppLifecycleState state) {
 | 
				
			||||||
 | 
					    onAppLifecycleChanged(state);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					StreamSubscription<WebSocketPacket> setupNotificationListener(
 | 
				
			||||||
 | 
					  BuildContext context,
 | 
				
			||||||
 | 
					  WidgetRef ref,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  final ws = ref.watch(websocketProvider);
 | 
				
			||||||
 | 
					  return ws.dataStream.listen((pkt) async {
 | 
				
			||||||
 | 
					    if (pkt.type == "notifications.new") {
 | 
				
			||||||
 | 
					      final notification = SnNotification.fromJson(pkt.data!);
 | 
				
			||||||
 | 
					      if (_appLifecycleState == AppLifecycleState.resumed) {
 | 
				
			||||||
 | 
					        // App is focused, show in-app notification
 | 
				
			||||||
 | 
					        log(
 | 
				
			||||||
 | 
					          '[Notification] Showing in-app notification: ${notification.title}',
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        showTopSnackBar(
 | 
				
			||||||
 | 
					          globalOverlay.currentState!,
 | 
				
			||||||
 | 
					          Center(
 | 
				
			||||||
 | 
					            child: ConstrainedBox(
 | 
				
			||||||
 | 
					              constraints: const BoxConstraints(maxWidth: 480),
 | 
				
			||||||
 | 
					              child: NotificationCard(notification: notification),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          onTap: () {
 | 
				
			||||||
 | 
					            if (notification.meta['action_uri'] != null) {
 | 
				
			||||||
 | 
					              var uri = notification.meta['action_uri'] as String;
 | 
				
			||||||
 | 
					              if (uri.startsWith('/')) {
 | 
				
			||||||
 | 
					                // In-app routes
 | 
				
			||||||
 | 
					                rootNavigatorKey.currentContext?.push(
 | 
				
			||||||
 | 
					                  notification.meta['action_uri'],
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                // External URLs
 | 
				
			||||||
 | 
					                launchUrlString(uri);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          onDismissed: () {},
 | 
				
			||||||
 | 
					          dismissType: DismissType.onSwipe,
 | 
				
			||||||
 | 
					          displayDuration: const Duration(seconds: 5),
 | 
				
			||||||
 | 
					          snackBarPosition: SnackBarPosition.top,
 | 
				
			||||||
 | 
					          padding: EdgeInsets.only(
 | 
				
			||||||
 | 
					            left: 16,
 | 
				
			||||||
 | 
					            right: 16,
 | 
				
			||||||
 | 
					            top:
 | 
				
			||||||
 | 
					                (!kIsWeb &&
 | 
				
			||||||
 | 
					                        (Platform.isMacOS ||
 | 
				
			||||||
 | 
					                            Platform.isWindows ||
 | 
				
			||||||
 | 
					                            Platform.isLinux))
 | 
				
			||||||
 | 
					                    ? 28
 | 
				
			||||||
 | 
					                    // ignore: use_build_context_synchronously
 | 
				
			||||||
 | 
					                    : MediaQuery.of(context).padding.top + 16,
 | 
				
			||||||
 | 
					            bottom: 16,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // App is in background, show system notification (only on supported platforms)
 | 
				
			||||||
 | 
					        if (!kIsWeb && !Platform.isIOS) {
 | 
				
			||||||
 | 
					          log(
 | 
				
			||||||
 | 
					            '[Notification] Showing system notification: ${notification.title}',
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // Use flutter_local_notifications for universal platforms
 | 
				
			||||||
 | 
					          const AndroidNotificationDetails androidNotificationDetails =
 | 
				
			||||||
 | 
					              AndroidNotificationDetails(
 | 
				
			||||||
 | 
					                'channel_id',
 | 
				
			||||||
 | 
					                'channel_name',
 | 
				
			||||||
 | 
					                channelDescription: 'channel_description',
 | 
				
			||||||
 | 
					                importance: Importance.max,
 | 
				
			||||||
 | 
					                priority: Priority.high,
 | 
				
			||||||
 | 
					                ticker: 'ticker',
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					          const NotificationDetails notificationDetails = NotificationDetails(
 | 
				
			||||||
 | 
					            android: androidNotificationDetails,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          await flutterLocalNotificationsPlugin.show(
 | 
				
			||||||
 | 
					            0,
 | 
				
			||||||
 | 
					            notification.title,
 | 
				
			||||||
 | 
					            notification.content,
 | 
				
			||||||
 | 
					            notificationDetails,
 | 
				
			||||||
 | 
					            payload: notification.meta['action_uri'] as String?,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          log(
 | 
				
			||||||
 | 
					            '[Notification] Skipping system notification for unsupported platform: ${notification.title}',
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Future<void> subscribePushNotification(
 | 
				
			||||||
 | 
					  Dio apiClient, {
 | 
				
			||||||
 | 
					  bool detailedErrors = false,
 | 
				
			||||||
 | 
					}) async {
 | 
				
			||||||
 | 
					  if (!kIsWeb && Platform.isLinux) {
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  await FirebaseMessaging.instance.requestPermission(
 | 
				
			||||||
 | 
					    alert: true,
 | 
				
			||||||
 | 
					    badge: true,
 | 
				
			||||||
 | 
					    sound: true,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String? deviceToken;
 | 
				
			||||||
 | 
					  if (kIsWeb) {
 | 
				
			||||||
 | 
					    deviceToken = await FirebaseMessaging.instance.getToken(
 | 
				
			||||||
 | 
					      vapidKey:
 | 
				
			||||||
 | 
					          "BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU",
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  } else if (Platform.isAndroid) {
 | 
				
			||||||
 | 
					    deviceToken = await FirebaseMessaging.instance.getToken();
 | 
				
			||||||
 | 
					  } else if (Platform.isIOS) {
 | 
				
			||||||
 | 
					    deviceToken = await FirebaseMessaging.instance.getAPNSToken();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  FirebaseMessaging.instance.onTokenRefresh
 | 
				
			||||||
 | 
					      .listen((fcmToken) {
 | 
				
			||||||
 | 
					        _putTokenToRemote(apiClient, fcmToken, 1);
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .onError((err) {
 | 
				
			||||||
 | 
					        log("Failed to get firebase cloud messaging push token: $err");
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (deviceToken != null) {
 | 
				
			||||||
 | 
					    _putTokenToRemote(
 | 
				
			||||||
 | 
					      apiClient,
 | 
				
			||||||
 | 
					      deviceToken,
 | 
				
			||||||
 | 
					      !kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  } else if (detailedErrors) {
 | 
				
			||||||
 | 
					    throw Exception("Failed to get device token for push notifications.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Future<void> _putTokenToRemote(
 | 
				
			||||||
 | 
					  Dio apiClient,
 | 
				
			||||||
 | 
					  String token,
 | 
				
			||||||
 | 
					  int provider,
 | 
				
			||||||
 | 
					) async {
 | 
				
			||||||
 | 
					  await apiClient.put(
 | 
				
			||||||
 | 
					    "/ring/notifications/subscription",
 | 
				
			||||||
 | 
					    data: {"provider": provider, "device_token": token},
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										176
									
								
								lib/services/notify.windows.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								lib/services/notify.windows.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,176 @@
 | 
				
			|||||||
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					import 'dart:developer';
 | 
				
			||||||
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:firebase_messaging/firebase_messaging.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
 | 
					import 'package:island/main.dart';
 | 
				
			||||||
 | 
					import 'package:island/route.dart';
 | 
				
			||||||
 | 
					import 'package:island/models/account.dart';
 | 
				
			||||||
 | 
					import 'package:island/pods/websocket.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/app_notification.dart';
 | 
				
			||||||
 | 
					import 'package:top_snackbar_flutter/top_snack_bar.dart';
 | 
				
			||||||
 | 
					import 'package:url_launcher/url_launcher_string.dart';
 | 
				
			||||||
 | 
					import 'package:windows_notification/windows_notification.dart'
 | 
				
			||||||
 | 
					    as windows_notification;
 | 
				
			||||||
 | 
					import 'package:windows_notification/notification_message.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:dio/dio.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Windows notification instance
 | 
				
			||||||
 | 
					windows_notification.WindowsNotification? windowsNotification;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					AppLifecycleState _appLifecycleState = AppLifecycleState.resumed;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void _onAppLifecycleChanged(AppLifecycleState state) {
 | 
				
			||||||
 | 
					  _appLifecycleState = state;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Future<void> initializeLocalNotifications() async {
 | 
				
			||||||
 | 
					  // Initialize Windows notification for Windows platform
 | 
				
			||||||
 | 
					  windowsNotification = windows_notification.WindowsNotification(
 | 
				
			||||||
 | 
					    applicationId: 'dev.solsynth.solian',
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  WidgetsBinding.instance.addObserver(
 | 
				
			||||||
 | 
					    LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LifecycleEventHandler extends WidgetsBindingObserver {
 | 
				
			||||||
 | 
					  final void Function(AppLifecycleState) onAppLifecycleChanged;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  LifecycleEventHandler({required this.onAppLifecycleChanged});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void didChangeAppLifecycleState(AppLifecycleState state) {
 | 
				
			||||||
 | 
					    onAppLifecycleChanged(state);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					StreamSubscription<WebSocketPacket> setupNotificationListener(
 | 
				
			||||||
 | 
					  BuildContext context,
 | 
				
			||||||
 | 
					  WidgetRef ref,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  final ws = ref.watch(websocketProvider);
 | 
				
			||||||
 | 
					  return ws.dataStream.listen((pkt) async {
 | 
				
			||||||
 | 
					    if (pkt.type == "notifications.new") {
 | 
				
			||||||
 | 
					      final notification = SnNotification.fromJson(pkt.data!);
 | 
				
			||||||
 | 
					      if (_appLifecycleState == AppLifecycleState.resumed) {
 | 
				
			||||||
 | 
					        // App is focused, show in-app notification
 | 
				
			||||||
 | 
					        log(
 | 
				
			||||||
 | 
					          '[Notification] Showing in-app notification: ${notification.title}',
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        showTopSnackBar(
 | 
				
			||||||
 | 
					          globalOverlay.currentState!,
 | 
				
			||||||
 | 
					          Center(
 | 
				
			||||||
 | 
					            child: ConstrainedBox(
 | 
				
			||||||
 | 
					              constraints: const BoxConstraints(maxWidth: 480),
 | 
				
			||||||
 | 
					              child: NotificationCard(notification: notification),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          onTap: () {
 | 
				
			||||||
 | 
					            if (notification.meta['action_uri'] != null) {
 | 
				
			||||||
 | 
					              var uri = notification.meta['action_uri'] as String;
 | 
				
			||||||
 | 
					              if (uri.startsWith('/')) {
 | 
				
			||||||
 | 
					                // In-app routes
 | 
				
			||||||
 | 
					                rootNavigatorKey.currentContext?.push(
 | 
				
			||||||
 | 
					                  notification.meta['action_uri'],
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                // External URLs
 | 
				
			||||||
 | 
					                launchUrlString(uri);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          onDismissed: () {},
 | 
				
			||||||
 | 
					          dismissType: DismissType.onSwipe,
 | 
				
			||||||
 | 
					          displayDuration: const Duration(seconds: 5),
 | 
				
			||||||
 | 
					          snackBarPosition: SnackBarPosition.top,
 | 
				
			||||||
 | 
					          padding: EdgeInsets.only(
 | 
				
			||||||
 | 
					            left: 16,
 | 
				
			||||||
 | 
					            right: 16,
 | 
				
			||||||
 | 
					            top: 28, // Windows specific padding
 | 
				
			||||||
 | 
					            bottom: 16,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // App is in background, show Windows system notification
 | 
				
			||||||
 | 
					        log(
 | 
				
			||||||
 | 
					          '[Notification] Showing Windows system notification: ${notification.title}',
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (windowsNotification != null) {
 | 
				
			||||||
 | 
					          // Use Windows notification for Windows platform
 | 
				
			||||||
 | 
					          final notificationMessage = NotificationMessage.fromPluginTemplate(
 | 
				
			||||||
 | 
					            DateTime.now().millisecondsSinceEpoch.toString(), // unique id
 | 
				
			||||||
 | 
					            notification.title,
 | 
				
			||||||
 | 
					            notification.content,
 | 
				
			||||||
 | 
					            launch: notification.meta['action_uri'] as String?,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          await windowsNotification!.showNotificationPluginTemplate(
 | 
				
			||||||
 | 
					            notificationMessage,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Future<void> subscribePushNotification(
 | 
				
			||||||
 | 
					  Dio apiClient, {
 | 
				
			||||||
 | 
					  bool detailedErrors = false,
 | 
				
			||||||
 | 
					}) async {
 | 
				
			||||||
 | 
					  if (!kIsWeb && Platform.isLinux) {
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  await FirebaseMessaging.instance.requestPermission(
 | 
				
			||||||
 | 
					    alert: true,
 | 
				
			||||||
 | 
					    badge: true,
 | 
				
			||||||
 | 
					    sound: true,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String? deviceToken;
 | 
				
			||||||
 | 
					  if (kIsWeb) {
 | 
				
			||||||
 | 
					    deviceToken = await FirebaseMessaging.instance.getToken(
 | 
				
			||||||
 | 
					      vapidKey:
 | 
				
			||||||
 | 
					          "BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU",
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  } else if (Platform.isAndroid) {
 | 
				
			||||||
 | 
					    deviceToken = await FirebaseMessaging.instance.getToken();
 | 
				
			||||||
 | 
					  } else if (Platform.isIOS) {
 | 
				
			||||||
 | 
					    deviceToken = await FirebaseMessaging.instance.getAPNSToken();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  FirebaseMessaging.instance.onTokenRefresh
 | 
				
			||||||
 | 
					      .listen((fcmToken) {
 | 
				
			||||||
 | 
					        _putTokenToRemote(apiClient, fcmToken, 1);
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .onError((err) {
 | 
				
			||||||
 | 
					        log("Failed to get firebase cloud messaging push token: $err");
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (deviceToken != null) {
 | 
				
			||||||
 | 
					    _putTokenToRemote(
 | 
				
			||||||
 | 
					      apiClient,
 | 
				
			||||||
 | 
					      deviceToken,
 | 
				
			||||||
 | 
					      !kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  } else if (detailedErrors) {
 | 
				
			||||||
 | 
					    throw Exception("Failed to get device token for push notifications.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Future<void> _putTokenToRemote(
 | 
				
			||||||
 | 
					  Dio apiClient,
 | 
				
			||||||
 | 
					  String token,
 | 
				
			||||||
 | 
					  int provider,
 | 
				
			||||||
 | 
					) async {
 | 
				
			||||||
 | 
					  await apiClient.put(
 | 
				
			||||||
 | 
					    "/ring/notifications/subscription",
 | 
				
			||||||
 | 
					    data: {"provider": provider, "device_token": token},
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -14,7 +14,7 @@ Future<void> initializeTzdb() async {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Future<String> getMachineTz() async {
 | 
					Future<String> getMachineTz() async {
 | 
				
			||||||
  return await FlutterTimezone.getLocalTimezone();
 | 
					  return (await FlutterTimezone.getLocalTimezone()).identifier;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
List<String> getAvailableTz() {
 | 
					List<String> getAvailableTz() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,7 +13,7 @@ Future<void> initializeTzdb() async {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Future<String> getMachineTz() async {
 | 
					Future<String> getMachineTz() async {
 | 
				
			||||||
  return await FlutterTimezone.getLocalTimezone();
 | 
					  return (await FlutterTimezone.getLocalTimezone()).identifier;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
List<String> getAvailableTz() {
 | 
					List<String> getAvailableTz() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1 +1,3 @@
 | 
				
			|||||||
export 'udid.native.dart' if (dart.library.html) 'udid.web.dart';
 | 
					export 'udid.native.dart'
 | 
				
			||||||
 | 
					    if (dart.library.html) 'udid.web.dart'
 | 
				
			||||||
 | 
					    if (dart.library.io) 'udid.native.dart';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,6 @@
 | 
				
			|||||||
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					import 'package:device_info_plus/device_info_plus.dart';
 | 
				
			||||||
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter_udid/flutter_udid.dart';
 | 
					import 'package:flutter_udid/flutter_udid.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
String? _cachedUdid;
 | 
					String? _cachedUdid;
 | 
				
			||||||
@@ -9,3 +12,18 @@ Future<String> getUdid() async {
 | 
				
			|||||||
  _cachedUdid = await FlutterUdid.consistentUdid;
 | 
					  _cachedUdid = await FlutterUdid.consistentUdid;
 | 
				
			||||||
  return _cachedUdid!;
 | 
					  return _cachedUdid!;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Future<String> getDeviceName() async {
 | 
				
			||||||
 | 
					  DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
 | 
				
			||||||
 | 
					  if (Platform.isAndroid) {
 | 
				
			||||||
 | 
					    AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
 | 
				
			||||||
 | 
					    return androidInfo.device;
 | 
				
			||||||
 | 
					  } else if (Platform.isIOS) {
 | 
				
			||||||
 | 
					    IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
 | 
				
			||||||
 | 
					    return iosInfo.name;
 | 
				
			||||||
 | 
					  } else if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) {
 | 
				
			||||||
 | 
					    return Platform.localHostname;
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return 'unknown'.tr();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,3 +9,18 @@ Future<String> getUdid() async {
 | 
				
			|||||||
  final hash = sha256.convert(bytes);
 | 
					  final hash = sha256.convert(bytes);
 | 
				
			||||||
  return hash.toString();
 | 
					  return hash.toString();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Future<String> getDeviceName() async {
 | 
				
			||||||
 | 
					  final userAgent = window.navigator.userAgent;
 | 
				
			||||||
 | 
					  if (userAgent.contains('Chrome') && !userAgent.contains('Edg')) {
 | 
				
			||||||
 | 
					    return 'Chrome';
 | 
				
			||||||
 | 
					  } else if (userAgent.contains('Firefox')) {
 | 
				
			||||||
 | 
					    return 'Firefox';
 | 
				
			||||||
 | 
					  } else if (userAgent.contains('Safari') && !userAgent.contains('Chrome')) {
 | 
				
			||||||
 | 
					    return 'Safari';
 | 
				
			||||||
 | 
					  } else if (userAgent.contains('Edg')) {
 | 
				
			||||||
 | 
					    return 'Edge';
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return 'Browser';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,10 +6,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			|||||||
import 'package:island/models/account.dart';
 | 
					import 'package:island/models/account.dart';
 | 
				
			||||||
import 'package:island/pods/network.dart';
 | 
					import 'package:island/pods/network.dart';
 | 
				
			||||||
import 'package:island/services/responsive.dart';
 | 
					import 'package:island/services/responsive.dart';
 | 
				
			||||||
 | 
					import 'package:island/services/time.dart';
 | 
				
			||||||
import 'package:island/services/udid.dart';
 | 
					import 'package:island/services/udid.dart';
 | 
				
			||||||
import 'package:island/widgets/alert.dart';
 | 
					import 'package:island/widgets/alert.dart';
 | 
				
			||||||
import 'package:island/widgets/content/sheet.dart';
 | 
					import 'package:island/widgets/content/sheet.dart';
 | 
				
			||||||
import 'package:island/widgets/response.dart';
 | 
					import 'package:island/widgets/response.dart';
 | 
				
			||||||
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
import 'package:island/widgets/extended_refresh_indicator.dart';
 | 
					import 'package:island/widgets/extended_refresh_indicator.dart';
 | 
				
			||||||
@@ -43,32 +45,11 @@ class _DeviceListTile extends StatelessWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return ListTile(
 | 
					    return ExpansionTile(
 | 
				
			||||||
      isThreeLine: true,
 | 
					      title: Row(
 | 
				
			||||||
      contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
 | 
					        spacing: 8,
 | 
				
			||||||
      leading: Icon(switch (device.platform) {
 | 
					 | 
				
			||||||
        0 => Icons.device_unknown, // Unidentified
 | 
					 | 
				
			||||||
        1 => Icons.web, // Web
 | 
					 | 
				
			||||||
        2 => Icons.phone_iphone, // iOS
 | 
					 | 
				
			||||||
        3 => Icons.phone_android, // Android
 | 
					 | 
				
			||||||
        4 => Icons.laptop_mac, // macOS
 | 
					 | 
				
			||||||
        5 => Icons.window, // Windows
 | 
					 | 
				
			||||||
        6 => Icons.computer, // Linux
 | 
					 | 
				
			||||||
        _ => Icons.device_unknown, // fallback
 | 
					 | 
				
			||||||
      }).padding(top: 4),
 | 
					 | 
				
			||||||
      subtitle: Column(
 | 
					 | 
				
			||||||
        crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
					 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          Text(
 | 
					          Flexible(child: Text(device.deviceLabel ?? device.deviceName)),
 | 
				
			||||||
            'lastActiveAt'.tr(
 | 
					 | 
				
			||||||
              args: [
 | 
					 | 
				
			||||||
                DateFormat().format(
 | 
					 | 
				
			||||||
                  device.challenges.first.createdAt.toLocal(),
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              ],
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          Text(device.challenges.first.ipAddress),
 | 
					 | 
				
			||||||
          if (device.isCurrent)
 | 
					          if (device.isCurrent)
 | 
				
			||||||
            Row(
 | 
					            Row(
 | 
				
			||||||
              children: [
 | 
					              children: [
 | 
				
			||||||
@@ -82,10 +63,29 @@ class _DeviceListTile extends StatelessWidget {
 | 
				
			|||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ).padding(top: 4),
 | 
					            ),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      title: Text(device.deviceLabel ?? device.deviceName),
 | 
					      subtitle: Column(
 | 
				
			||||||
 | 
					        crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          Text(
 | 
				
			||||||
 | 
					            'lastActiveAt'.tr(
 | 
				
			||||||
 | 
					              args: [device.challenges.first.createdAt.formatSystem()],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      leading: Icon(switch (device.platform) {
 | 
				
			||||||
 | 
					        0 => Icons.device_unknown, // Unidentified
 | 
				
			||||||
 | 
					        1 => Icons.web, // Web
 | 
				
			||||||
 | 
					        2 => Icons.phone_iphone, // iOS
 | 
				
			||||||
 | 
					        3 => Icons.phone_android, // Android
 | 
				
			||||||
 | 
					        4 => Icons.laptop_mac, // macOS
 | 
				
			||||||
 | 
					        5 => Icons.window, // Windows
 | 
				
			||||||
 | 
					        6 => Icons.computer, // Linux
 | 
				
			||||||
 | 
					        _ => Icons.device_unknown, // fallback
 | 
				
			||||||
 | 
					      }).padding(top: 4),
 | 
				
			||||||
      trailing:
 | 
					      trailing:
 | 
				
			||||||
          isWideScreen(context)
 | 
					          isWideScreen(context)
 | 
				
			||||||
              ? Row(
 | 
					              ? Row(
 | 
				
			||||||
@@ -105,6 +105,36 @@ class _DeviceListTile extends StatelessWidget {
 | 
				
			|||||||
                ],
 | 
					                ],
 | 
				
			||||||
              )
 | 
					              )
 | 
				
			||||||
              : null,
 | 
					              : null,
 | 
				
			||||||
 | 
					      expandedCrossAxisAlignment: CrossAxisAlignment.stretch,
 | 
				
			||||||
 | 
					      children: [
 | 
				
			||||||
 | 
					        Container(
 | 
				
			||||||
 | 
					          decoration: BoxDecoration(
 | 
				
			||||||
 | 
					            color: Theme.of(context).colorScheme.surfaceVariant,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
 | 
				
			||||||
 | 
					          child: Text('authDeviceChallenges'.tr()),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        for (final challenge in device.challenges)
 | 
				
			||||||
 | 
					          ListTile(
 | 
				
			||||||
 | 
					            minTileHeight: 48,
 | 
				
			||||||
 | 
					            title: Text(DateFormat().format(challenge.createdAt.toLocal())),
 | 
				
			||||||
 | 
					            subtitle: Column(
 | 
				
			||||||
 | 
					              crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
				
			||||||
 | 
					              children: [
 | 
				
			||||||
 | 
					                Text(challenge.ipAddress),
 | 
				
			||||||
 | 
					                if (challenge.location != null)
 | 
				
			||||||
 | 
					                  Row(
 | 
				
			||||||
 | 
					                    spacing: 4,
 | 
				
			||||||
 | 
					                    children:
 | 
				
			||||||
 | 
					                        [challenge.location?.city, challenge.location?.country]
 | 
				
			||||||
 | 
					                            .where((e) => e?.isNotEmpty ?? false)
 | 
				
			||||||
 | 
					                            .map((e) => Text(e!))
 | 
				
			||||||
 | 
					                            .toList(),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -176,72 +206,116 @@ class AccountSessionSheet extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return SheetScaffold(
 | 
					    return SheetScaffold(
 | 
				
			||||||
      titleText: 'authSessions'.tr(),
 | 
					      titleText: 'authSessions'.tr(),
 | 
				
			||||||
      child: authDevices.when(
 | 
					      child: Column(
 | 
				
			||||||
        data:
 | 
					        children: [
 | 
				
			||||||
            (data) => ExtendedRefreshIndicator(
 | 
					          if (!wideScreen)
 | 
				
			||||||
              onRefresh:
 | 
					            Container(
 | 
				
			||||||
                  () => Future.sync(() => ref.invalidate(authDevicesProvider)),
 | 
					              width: double.infinity,
 | 
				
			||||||
              child: ListView.builder(
 | 
					              padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
 | 
				
			||||||
                padding: EdgeInsets.zero,
 | 
					              color: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
				
			||||||
                itemCount: data.length,
 | 
					              child: Row(
 | 
				
			||||||
                itemBuilder: (context, index) {
 | 
					                mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
                  final device = data[index];
 | 
					                crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
                  if (wideScreen) {
 | 
					                spacing: 8,
 | 
				
			||||||
                    return _DeviceListTile(
 | 
					                children: [
 | 
				
			||||||
                      device: device,
 | 
					                  const Icon(Symbols.info, size: 16).padding(top: 2),
 | 
				
			||||||
                      updateDeviceLabel: updateDeviceLabel,
 | 
					                  Flexible(
 | 
				
			||||||
                      logoutDevice: logoutDevice,
 | 
					                    child: Text(
 | 
				
			||||||
                    );
 | 
					                      'authDeviceHint'.tr(),
 | 
				
			||||||
                  } else {
 | 
					                      style: TextStyle(
 | 
				
			||||||
                    return Dismissible(
 | 
					                        color: Theme.of(context).colorScheme.onSurfaceVariant,
 | 
				
			||||||
                      key: Key('device-${device.id}'),
 | 
					 | 
				
			||||||
                      direction:
 | 
					 | 
				
			||||||
                          device.isCurrent
 | 
					 | 
				
			||||||
                              ? DismissDirection.startToEnd
 | 
					 | 
				
			||||||
                              : DismissDirection.horizontal,
 | 
					 | 
				
			||||||
                      background: Container(
 | 
					 | 
				
			||||||
                        color: Colors.blue,
 | 
					 | 
				
			||||||
                        alignment: Alignment.centerLeft,
 | 
					 | 
				
			||||||
                        padding: EdgeInsets.symmetric(horizontal: 20),
 | 
					 | 
				
			||||||
                        child: Icon(Icons.edit, color: Colors.white),
 | 
					 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                      secondaryBackground: Container(
 | 
					                    ),
 | 
				
			||||||
                        color: Colors.red,
 | 
					                  ),
 | 
				
			||||||
                        alignment: Alignment.centerRight,
 | 
					                ],
 | 
				
			||||||
                        padding: EdgeInsets.symmetric(horizontal: 20),
 | 
					 | 
				
			||||||
                        child: Icon(Icons.logout, color: Colors.white),
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                      confirmDismiss: (direction) async {
 | 
					 | 
				
			||||||
                        if (direction == DismissDirection.startToEnd) {
 | 
					 | 
				
			||||||
                          updateDeviceLabel(device.deviceId);
 | 
					 | 
				
			||||||
                          return false;
 | 
					 | 
				
			||||||
                        } else {
 | 
					 | 
				
			||||||
                          final confirm = await showConfirmAlert(
 | 
					 | 
				
			||||||
                            'authDeviceLogoutHint'.tr(),
 | 
					 | 
				
			||||||
                            'authDeviceLogout'.tr(),
 | 
					 | 
				
			||||||
                          );
 | 
					 | 
				
			||||||
                          if (confirm && context.mounted) {
 | 
					 | 
				
			||||||
                            logoutDevice(device.deviceId);
 | 
					 | 
				
			||||||
                          }
 | 
					 | 
				
			||||||
                          return false; // Don't dismiss
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                      },
 | 
					 | 
				
			||||||
                      child: _DeviceListTile(
 | 
					 | 
				
			||||||
                        device: device,
 | 
					 | 
				
			||||||
                        updateDeviceLabel: updateDeviceLabel,
 | 
					 | 
				
			||||||
                        logoutDevice: logoutDevice,
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                  }
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
        error:
 | 
					          Expanded(
 | 
				
			||||||
            (err, _) => ResponseErrorWidget(
 | 
					            child: authDevices.when(
 | 
				
			||||||
              error: err,
 | 
					              data:
 | 
				
			||||||
              onRetry: () => ref.invalidate(authDevicesProvider),
 | 
					                  (data) => ExtendedRefreshIndicator(
 | 
				
			||||||
 | 
					                    onRefresh:
 | 
				
			||||||
 | 
					                        () => Future.sync(
 | 
				
			||||||
 | 
					                          () => ref.invalidate(authDevicesProvider),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                    child: ListView.builder(
 | 
				
			||||||
 | 
					                      padding: EdgeInsets.zero,
 | 
				
			||||||
 | 
					                      itemCount: data.length,
 | 
				
			||||||
 | 
					                      itemBuilder: (context, index) {
 | 
				
			||||||
 | 
					                        final device = data[index];
 | 
				
			||||||
 | 
					                        if (wideScreen) {
 | 
				
			||||||
 | 
					                          return _DeviceListTile(
 | 
				
			||||||
 | 
					                            device: device,
 | 
				
			||||||
 | 
					                            updateDeviceLabel: updateDeviceLabel,
 | 
				
			||||||
 | 
					                            logoutDevice: logoutDevice,
 | 
				
			||||||
 | 
					                          );
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                          return Dismissible(
 | 
				
			||||||
 | 
					                            key: Key('device-${device.id}'),
 | 
				
			||||||
 | 
					                            direction:
 | 
				
			||||||
 | 
					                                device.isCurrent
 | 
				
			||||||
 | 
					                                    ? DismissDirection.startToEnd
 | 
				
			||||||
 | 
					                                    : DismissDirection.horizontal,
 | 
				
			||||||
 | 
					                            background: Container(
 | 
				
			||||||
 | 
					                              color: Colors.blue,
 | 
				
			||||||
 | 
					                              alignment: Alignment.centerLeft,
 | 
				
			||||||
 | 
					                              padding: EdgeInsets.symmetric(horizontal: 20),
 | 
				
			||||||
 | 
					                              child: Icon(Icons.edit, color: Colors.white),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            secondaryBackground: Container(
 | 
				
			||||||
 | 
					                              color: Colors.red,
 | 
				
			||||||
 | 
					                              alignment: Alignment.centerRight,
 | 
				
			||||||
 | 
					                              padding: EdgeInsets.symmetric(horizontal: 20),
 | 
				
			||||||
 | 
					                              child: Icon(Icons.logout, color: Colors.white),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            confirmDismiss: (direction) async {
 | 
				
			||||||
 | 
					                              if (direction == DismissDirection.startToEnd) {
 | 
				
			||||||
 | 
					                                updateDeviceLabel(device.deviceId);
 | 
				
			||||||
 | 
					                                return false;
 | 
				
			||||||
 | 
					                              } else {
 | 
				
			||||||
 | 
					                                final confirm = await showConfirmAlert(
 | 
				
			||||||
 | 
					                                  'authDeviceLogoutHint'.tr(),
 | 
				
			||||||
 | 
					                                  'authDeviceLogout'.tr(),
 | 
				
			||||||
 | 
					                                );
 | 
				
			||||||
 | 
					                                if (confirm && context.mounted) {
 | 
				
			||||||
 | 
					                                  try {
 | 
				
			||||||
 | 
					                                    showLoadingModal(context);
 | 
				
			||||||
 | 
					                                    final apiClient = ref.watch(
 | 
				
			||||||
 | 
					                                      apiClientProvider,
 | 
				
			||||||
 | 
					                                    );
 | 
				
			||||||
 | 
					                                    await apiClient.delete(
 | 
				
			||||||
 | 
					                                      '/id/accounts/me/devices/${device.deviceId}',
 | 
				
			||||||
 | 
					                                    );
 | 
				
			||||||
 | 
					                                    ref.invalidate(authDevicesProvider);
 | 
				
			||||||
 | 
					                                  } catch (err) {
 | 
				
			||||||
 | 
					                                    showErrorAlert(err);
 | 
				
			||||||
 | 
					                                  } finally {
 | 
				
			||||||
 | 
					                                    if (context.mounted)
 | 
				
			||||||
 | 
					                                      hideLoadingModal(context);
 | 
				
			||||||
 | 
					                                  }
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                return confirm;
 | 
				
			||||||
 | 
					                              }
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                            child: _DeviceListTile(
 | 
				
			||||||
 | 
					                              device: device,
 | 
				
			||||||
 | 
					                              updateDeviceLabel: updateDeviceLabel,
 | 
				
			||||||
 | 
					                              logoutDevice: logoutDevice,
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          );
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					              error:
 | 
				
			||||||
 | 
					                  (err, _) => ResponseErrorWidget(
 | 
				
			||||||
 | 
					                    error: err,
 | 
				
			||||||
 | 
					                    onRetry: () => ref.invalidate(authDevicesProvider),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					              loading: () => ResponseLoadingWidget(),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
        loading: () => ResponseLoadingWidget(),
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@ import 'package:bitsdojo_window/bitsdojo_window.dart';
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.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:island/pods/activity_rpc.dart';
 | 
					import 'package:island/pods/activity/activity_rpc.dart';
 | 
				
			||||||
import 'package:island/pods/websocket.dart';
 | 
					import 'package:island/pods/websocket.dart';
 | 
				
			||||||
import 'package:island/screens/tray_manager.dart';
 | 
					import 'package:island/screens/tray_manager.dart';
 | 
				
			||||||
import 'package:island/services/notify.dart';
 | 
					import 'package:island/services/notify.dart';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -228,7 +228,7 @@ class CheckInWidget extends HookConsumerWidget {
 | 
				
			|||||||
          ),
 | 
					          ),
 | 
				
			||||||
          Column(
 | 
					          Column(
 | 
				
			||||||
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
					            mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
				
			||||||
            spacing: 3,
 | 
					            crossAxisAlignment: CrossAxisAlignment.end,
 | 
				
			||||||
            children: [
 | 
					            children: [
 | 
				
			||||||
              AnimatedSwitcher(
 | 
					              AnimatedSwitcher(
 | 
				
			||||||
                duration: const Duration(milliseconds: 300),
 | 
					                duration: const Duration(milliseconds: 300),
 | 
				
			||||||
@@ -244,7 +244,7 @@ class CheckInWidget extends HookConsumerWidget {
 | 
				
			|||||||
                  loading: () => Text('checkInNone').tr().fontSize(15).bold(),
 | 
					                  loading: () => Text('checkInNone').tr().fontSize(15).bold(),
 | 
				
			||||||
                  error: (err, stack) => Text('error').tr().fontSize(15).bold(),
 | 
					                  error: (err, stack) => Text('error').tr().fontSize(15).bold(),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ).padding(right: 4),
 | 
				
			||||||
              IconButton.outlined(
 | 
					              IconButton.outlined(
 | 
				
			||||||
                iconSize: 16,
 | 
					                iconSize: 16,
 | 
				
			||||||
                visualDensity: const VisualDensity(
 | 
					                visualDensity: const VisualDensity(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,11 @@ String _parseRemoteError(DioException err) {
 | 
				
			|||||||
  String? message;
 | 
					  String? message;
 | 
				
			||||||
  if (err.response?.data is String) {
 | 
					  if (err.response?.data is String) {
 | 
				
			||||||
    message = err.response?.data;
 | 
					    message = err.response?.data;
 | 
				
			||||||
 | 
					  } else if (err.response?.data?['message'] != null) {
 | 
				
			||||||
 | 
					    message = <String?>[
 | 
				
			||||||
 | 
					      err.response?.data?['message']?.toString(),
 | 
				
			||||||
 | 
					      err.response?.data?['detail']?.toString(),
 | 
				
			||||||
 | 
					    ].where((e) => e != null).cast<String>().map((e) => e.trim()).join('\n');
 | 
				
			||||||
  } else if (err.response?.data?['errors'] != null) {
 | 
					  } else if (err.response?.data?['errors'] != null) {
 | 
				
			||||||
    final errors = err.response?.data['errors'] as Map<String, dynamic>;
 | 
					    final errors = err.response?.data['errors'] as Map<String, dynamic>;
 | 
				
			||||||
    message = errors.values
 | 
					    message = errors.values
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,17 +8,26 @@ import 'package:easy_localization/easy_localization.dart';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
String _parseRemoteError(DioException err) {
 | 
					String _parseRemoteError(DioException err) {
 | 
				
			||||||
  log('${err.requestOptions.method} ${err.requestOptions.uri} ${err.message}');
 | 
					  log('${err.requestOptions.method} ${err.requestOptions.uri} ${err.message}');
 | 
				
			||||||
  if (err.response?.data is String) return err.response?.data;
 | 
					  String? message;
 | 
				
			||||||
  if (err.response?.data?['errors'] != null) {
 | 
					  if (err.response?.data is String) {
 | 
				
			||||||
 | 
					    message = err.response?.data;
 | 
				
			||||||
 | 
					  } else if (err.response?.data?['message'] != null) {
 | 
				
			||||||
 | 
					    message = <String?>[
 | 
				
			||||||
 | 
					      err.response?.data?['message']?.toString(),
 | 
				
			||||||
 | 
					      err.response?.data?['detail']?.toString(),
 | 
				
			||||||
 | 
					    ].where((e) => e != null).cast<String>().map((e) => e.trim()).join('\n');
 | 
				
			||||||
 | 
					  } else if (err.response?.data?['errors'] != null) {
 | 
				
			||||||
    final errors = err.response?.data['errors'] as Map<String, dynamic>;
 | 
					    final errors = err.response?.data['errors'] as Map<String, dynamic>;
 | 
				
			||||||
    return errors.values
 | 
					    message = errors.values
 | 
				
			||||||
        .map(
 | 
					        .map(
 | 
				
			||||||
          (ele) =>
 | 
					          (ele) =>
 | 
				
			||||||
              (ele as List<dynamic>).map((ele) => ele.toString()).join('\n'),
 | 
					              (ele as List<dynamic>).map((ele) => ele.toString()).join('\n'),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .join('\n');
 | 
					        .join('\n');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return err.message ?? err.toString();
 | 
					  if (message == null || message.isEmpty) message = err.response?.statusMessage;
 | 
				
			||||||
 | 
					  message ??= err.message;
 | 
				
			||||||
 | 
					  return message ?? err.toString();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void showErrorAlert(dynamic err) async {
 | 
					void showErrorAlert(dynamic err) async {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ part of 'post_award_history_sheet.dart';
 | 
				
			|||||||
// **************************************************************************
 | 
					// **************************************************************************
 | 
				
			||||||
 | 
					
 | 
				
			||||||
String _$postAwardListNotifierHash() =>
 | 
					String _$postAwardListNotifierHash() =>
 | 
				
			||||||
    r'492ae59a5dbbfb5c98f863f036023193b6e08668';
 | 
					    r'834d08f90ef352a2dfb0192455c75b1620e859c2';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Copied from Dart SDK
 | 
					/// Copied from Dart SDK
 | 
				
			||||||
class _SystemHash {
 | 
					class _SystemHash {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -111,9 +111,9 @@ PODS:
 | 
				
			|||||||
  - flutter_udid (0.0.1):
 | 
					  - flutter_udid (0.0.1):
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
    - SAMKeychain
 | 
					    - SAMKeychain
 | 
				
			||||||
  - flutter_webrtc (1.1.0):
 | 
					  - flutter_webrtc (1.2.0):
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
    - WebRTC-SDK (= 137.7151.03)
 | 
					    - WebRTC-SDK (= 137.7151.04)
 | 
				
			||||||
  - FlutterMacOS (1.0.0)
 | 
					  - FlutterMacOS (1.0.0)
 | 
				
			||||||
  - gal (1.0.0):
 | 
					  - gal (1.0.0):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
@@ -175,7 +175,7 @@ PODS:
 | 
				
			|||||||
  - livekit_client (2.5.0):
 | 
					  - livekit_client (2.5.0):
 | 
				
			||||||
    - flutter_webrtc
 | 
					    - flutter_webrtc
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
    - WebRTC-SDK (= 137.7151.03)
 | 
					    - WebRTC-SDK (= 137.7151.04)
 | 
				
			||||||
  - local_auth_darwin (0.0.1):
 | 
					  - local_auth_darwin (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
@@ -247,7 +247,7 @@ PODS:
 | 
				
			|||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - wakelock_plus (0.0.1):
 | 
					  - wakelock_plus (0.0.1):
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - WebRTC-SDK (137.7151.03)
 | 
					  - WebRTC-SDK (137.7151.04)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
DEPENDENCIES:
 | 
					DEPENDENCIES:
 | 
				
			||||||
  - bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
 | 
					  - bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
 | 
				
			||||||
@@ -419,17 +419,17 @@ SPEC CHECKSUMS:
 | 
				
			|||||||
  flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0
 | 
					  flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0
 | 
				
			||||||
  flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284
 | 
					  flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284
 | 
				
			||||||
  flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
 | 
					  flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
 | 
				
			||||||
  flutter_timezone: d59eea86178cbd7943cd2431cc2eaa9850f935d8
 | 
					  flutter_timezone: d272288c69082ad571630e0d17140b3d6b93dc0c
 | 
				
			||||||
  flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495
 | 
					  flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495
 | 
				
			||||||
  flutter_webrtc: 1ce7fe9a42f085286378355a575e682edd7f114d
 | 
					  flutter_webrtc: 718eae22a371cd94e5d56aa4f301443ebc5bb737
 | 
				
			||||||
  FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
 | 
					  FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
 | 
				
			||||||
  gal: baecd024ebfd13c441269ca7404792a7152fde89
 | 
					  gal: baecd024ebfd13c441269ca7404792a7152fde89
 | 
				
			||||||
  GoogleAppMeasurement: 09f341dfa8527d1612a09cbfe809a242c0b737af
 | 
					  GoogleAppMeasurement: 09f341dfa8527d1612a09cbfe809a242c0b737af
 | 
				
			||||||
  GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
 | 
					  GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
 | 
				
			||||||
  GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
 | 
					  GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
 | 
				
			||||||
  irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
 | 
					  irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
 | 
				
			||||||
  livekit_client: 5a5c0f1081978542bbf9a986c7ac9bffcdb73906
 | 
					  livekit_client: 95b4a47f51f98a8be3a181c3fa251be7823dddd4
 | 
				
			||||||
  local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
 | 
					  local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
 | 
				
			||||||
  media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
 | 
					  media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
 | 
				
			||||||
  media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
 | 
					  media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
 | 
				
			||||||
  nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
 | 
					  nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
 | 
				
			||||||
@@ -451,8 +451,8 @@ SPEC CHECKSUMS:
 | 
				
			|||||||
  tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166
 | 
					  tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166
 | 
				
			||||||
  url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
 | 
					  url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
 | 
				
			||||||
  volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd
 | 
					  volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd
 | 
				
			||||||
  wakelock_plus: 21ddc249ac4b8d018838dbdabd65c5976c308497
 | 
					  wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b
 | 
				
			||||||
  WebRTC-SDK: 69d4e56b0b4b27d788e87bab9b9a1326ed05b1e3
 | 
					  WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
PODFILE CHECKSUM: 346bfb2deb41d4a6ebd6f6799f92188bde2d246f
 | 
					PODFILE CHECKSUM: 346bfb2deb41d4a6ebd6f6799f92188bde2d246f
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,8 +6,6 @@
 | 
				
			|||||||
	<array>
 | 
						<array>
 | 
				
			||||||
		<string>Default</string>
 | 
							<string>Default</string>
 | 
				
			||||||
	</array>
 | 
						</array>
 | 
				
			||||||
	<key>com.apple.developer.device-information.user-assigned-device-name</key>
 | 
					 | 
				
			||||||
	<true/>
 | 
					 | 
				
			||||||
	<key>com.apple.security.app-sandbox</key>
 | 
						<key>com.apple.security.app-sandbox</key>
 | 
				
			||||||
	<true/>
 | 
						<true/>
 | 
				
			||||||
	<key>com.apple.security.cs.allow-jit</key>
 | 
						<key>com.apple.security.cs.allow-jit</key>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,8 +6,6 @@
 | 
				
			|||||||
	<array>
 | 
						<array>
 | 
				
			||||||
		<string>Default</string>
 | 
							<string>Default</string>
 | 
				
			||||||
	</array>
 | 
						</array>
 | 
				
			||||||
	<key>com.apple.developer.device-information.user-assigned-device-name</key>
 | 
					 | 
				
			||||||
	<true/>
 | 
					 | 
				
			||||||
	<key>com.apple.security.app-sandbox</key>
 | 
						<key>com.apple.security.app-sandbox</key>
 | 
				
			||||||
	<true/>
 | 
						<true/>
 | 
				
			||||||
	<key>com.apple.security.device.audio-input</key>
 | 
						<key>com.apple.security.device.audio-input</key>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										124
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										124
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -285,10 +285,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: code_builder
 | 
					      name: code_builder
 | 
				
			||||||
      sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
 | 
					      sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "4.10.1"
 | 
					    version: "4.11.0"
 | 
				
			||||||
  collection:
 | 
					  collection:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -333,10 +333,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: croppy
 | 
					      name: croppy
 | 
				
			||||||
      sha256: "2a69059d9ec007b79d6a494854094b2e3c0a4f7ed609cf55a4805c9de9ec171d"
 | 
					      sha256: ca70a77cd5a981172d69382a7b43629d15d6c868475b2bbb45efce32cfc58b86
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.3.6"
 | 
					    version: "1.4.0"
 | 
				
			||||||
  cross_file:
 | 
					  cross_file:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -401,6 +401,14 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.0.0+7.7.0"
 | 
					    version: "1.0.0+7.7.0"
 | 
				
			||||||
 | 
					  dart_ipc:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: dart_ipc
 | 
				
			||||||
 | 
					      sha256: "6cad558cda5304017c1f581df4c96fd4f8e4ee212aae7bfa4357716236faa9ba"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "1.0.1"
 | 
				
			||||||
  dart_style:
 | 
					  dart_style:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -413,10 +421,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: dart_webrtc
 | 
					      name: dart_webrtc
 | 
				
			||||||
      sha256: "3bfa069a8b14a53ba506f6dd529e9b88c878ba0cc238f311051a39bf1e53d075"
 | 
					      sha256: "51bcda4ba5d7dd9e65a309244ce3ac0b58025e6e1f6d7442cee4cd02134ef65f"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.5.3+hotfix.5"
 | 
					    version: "1.6.0"
 | 
				
			||||||
  dbus:
 | 
					  dbus:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -546,7 +554,7 @@ packages:
 | 
				
			|||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.3.3"
 | 
					    version: "1.3.3"
 | 
				
			||||||
  ffi:
 | 
					  ffi:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: ffi
 | 
					      name: ffi
 | 
				
			||||||
      sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
 | 
					      sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
 | 
				
			||||||
@@ -565,10 +573,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: file_picker
 | 
					      name: file_picker
 | 
				
			||||||
      sha256: e7e16c9d15c36330b94ca0e2ad8cb61f93cd5282d0158c09805aed13b5452f22
 | 
					      sha256: f2d9f173c2c14635cc0e9b14c143c49ef30b4934e8d1d274d6206fcb0086a06f
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "10.3.2"
 | 
					    version: "10.3.3"
 | 
				
			||||||
  file_saver:
 | 
					  file_saver:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -709,10 +717,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: fl_chart
 | 
					      name: fl_chart
 | 
				
			||||||
      sha256: d3f82f4a38e33ba23d05a08ff304d7d8b22d2a59a5503f20bd802966e915db89
 | 
					      sha256: "7ca9a40f4eb85949190e54087be8b4d6ac09dc4c54238d782a34cf1f7c011de9"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.1.0"
 | 
					    version: "1.1.1"
 | 
				
			||||||
  flutter:
 | 
					  flutter:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description: flutter
 | 
					    description: flutter
 | 
				
			||||||
@@ -906,10 +914,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: flutter_local_notifications
 | 
					      name: flutter_local_notifications
 | 
				
			||||||
      sha256: a9966c850de5e445331b854fa42df96a8020066d67f125a5964cbc6556643f68
 | 
					      sha256: "7ed76be64e8a7d01dfdf250b8434618e2a028c9dfa2a3c41dc9b531d4b3fc8a5"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "19.4.1"
 | 
					    version: "19.4.2"
 | 
				
			||||||
  flutter_local_notifications_linux:
 | 
					  flutter_local_notifications_linux:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -930,10 +938,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: flutter_local_notifications_windows
 | 
					      name: flutter_local_notifications_windows
 | 
				
			||||||
      sha256: ed46d7ae4ec9d19e4c8fa2badac5fe27ba87a3fe387343ce726f927af074ec98
 | 
					      sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.0.2"
 | 
					    version: "1.0.3"
 | 
				
			||||||
  flutter_localizations:
 | 
					  flutter_localizations:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description: flutter
 | 
					    description: flutter
 | 
				
			||||||
@@ -1076,10 +1084,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: flutter_timezone
 | 
					      name: flutter_timezone
 | 
				
			||||||
      sha256: "13b2109ad75651faced4831bf262e32559e44aa549426eab8a597610d385d934"
 | 
					      sha256: ccad42fbb5d01d51d3eb281cc4428fca556cc4063c52bd9fa40f80cd93b8e649
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "4.1.1"
 | 
					    version: "5.0.0"
 | 
				
			||||||
  flutter_typeahead:
 | 
					  flutter_typeahead:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1105,10 +1113,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: flutter_webrtc
 | 
					      name: flutter_webrtc
 | 
				
			||||||
      sha256: "945d0a38b90fbca8257eadb167d8fb9fa7075d9a1939fd2953c10054454d1de2"
 | 
					      sha256: "16ca9e30d428bae3dd32933e875c9f67c5843d1fa726c37cf1fc479eb9294549"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.1.0"
 | 
					    version: "1.2.0"
 | 
				
			||||||
  font_awesome_flutter:
 | 
					  font_awesome_flutter:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1177,10 +1185,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: go_router
 | 
					      name: go_router
 | 
				
			||||||
      sha256: eb059dfe59f08546e9787f895bd01652076f996bcbf485a8609ef990419ad227
 | 
					      sha256: b1488741c9ce37b72e026377c69a59c47378493156fc38efb5a54f6def3f92a3
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "16.2.1"
 | 
					    version: "16.2.2"
 | 
				
			||||||
  google_fonts:
 | 
					  google_fonts:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1262,7 +1270,7 @@ packages:
 | 
				
			|||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "4.1.2"
 | 
					    version: "4.1.2"
 | 
				
			||||||
  image:
 | 
					  image:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: image
 | 
					      name: image
 | 
				
			||||||
      sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
 | 
					      sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
 | 
				
			||||||
@@ -1281,10 +1289,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: image_picker_android
 | 
					      name: image_picker_android
 | 
				
			||||||
      sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e"
 | 
					      sha256: "8dfe08ea7fcf7467dbaf6889e72eebd5e0d6711caae201fdac780eb45232cd02"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.8.13+1"
 | 
					    version: "0.8.13+3"
 | 
				
			||||||
  image_picker_for_web:
 | 
					  image_picker_for_web:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1393,10 +1401,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: leak_tracker
 | 
					      name: leak_tracker
 | 
				
			||||||
      sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0"
 | 
					      sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "11.0.1"
 | 
					    version: "11.0.2"
 | 
				
			||||||
  leak_tracker_flutter_testing:
 | 
					  leak_tracker_flutter_testing:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1433,10 +1441,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: livekit_client
 | 
					      name: livekit_client
 | 
				
			||||||
      sha256: "011affc0fca22b2f9b0e8827219dad9948f84f2bf057980693de13039de904c7"
 | 
					      sha256: "4c1663c1e6ac20a743d9a46c7bc71f17e1949db99d245750c68661d554e30cd2"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.5.0+hotfix.3"
 | 
					    version: "2.5.1"
 | 
				
			||||||
  local_auth:
 | 
					  local_auth:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1449,18 +1457,18 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: local_auth_android
 | 
					      name: local_auth_android
 | 
				
			||||||
      sha256: "48924f4a8b3cc45994ad5993e2e232d3b00788a305c1bf1c7db32cef281ce9a3"
 | 
					      sha256: "1ee0e63fb8b5c6fa286796b5fb1570d256857c2f4a262127e728b36b80a570cf"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.0.52"
 | 
					    version: "1.0.53"
 | 
				
			||||||
  local_auth_darwin:
 | 
					  local_auth_darwin:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: local_auth_darwin
 | 
					      name: local_auth_darwin
 | 
				
			||||||
      sha256: "0e9706a8543a4a2eee60346294d6a633dd7c3ee60fae6b752570457c4ff32055"
 | 
					      sha256: "699873970067a40ef2f2c09b4c72eb1cfef64224ef041b3df9fdc5c4c1f91f49"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.6.0"
 | 
					    version: "1.6.1"
 | 
				
			||||||
  local_auth_platform_interface:
 | 
					  local_auth_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1537,10 +1545,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: material_symbols_icons
 | 
					      name: material_symbols_icons
 | 
				
			||||||
      sha256: "2cfd19bf1c3016b0de7298eb3d3444fcb6ef093d934deb870ceb946af89cfa58"
 | 
					      sha256: "9e2042673fda5dda0b77e262220b3c34cac5806a3833da85522e41bb27fbf6c0"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "4.2872.0"
 | 
					    version: "4.2873.0"
 | 
				
			||||||
  media_kit:
 | 
					  media_kit:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1697,10 +1705,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: package_info_plus
 | 
					      name: package_info_plus
 | 
				
			||||||
      sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
 | 
					      sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "8.3.1"
 | 
					    version: "9.0.0"
 | 
				
			||||||
  package_info_plus_platform_interface:
 | 
					  package_info_plus_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1709,14 +1717,6 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.2.1"
 | 
					    version: "3.2.1"
 | 
				
			||||||
  palette_generator:
 | 
					 | 
				
			||||||
    dependency: "direct main"
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: palette_generator
 | 
					 | 
				
			||||||
      sha256: "4420f7ccc3f0a4a906144e73f8b6267cd940b64f57a7262e95cb8cec3a8ae0ed"
 | 
					 | 
				
			||||||
      url: "https://pub.dev"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "0.3.3+7"
 | 
					 | 
				
			||||||
  pasteboard:
 | 
					  pasteboard:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1873,10 +1873,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: pool
 | 
					      name: pool
 | 
				
			||||||
      sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
 | 
					      sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.5.1"
 | 
					    version: "1.5.2"
 | 
				
			||||||
  posix:
 | 
					  posix:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1969,10 +1969,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: record_android
 | 
					      name: record_android
 | 
				
			||||||
      sha256: "8361a791c9a3fa5c065f0b8b5adb10f12531f8538c86b19474cf7b56ea80d426"
 | 
					      sha256: "854627cd78d8d66190377f98477eee06ca96ab7c9f2e662700daf33dbf7e6673"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.4.1"
 | 
					    version: "1.4.2"
 | 
				
			||||||
  record_ios:
 | 
					  record_ios:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -2137,10 +2137,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: share_plus
 | 
					      name: share_plus
 | 
				
			||||||
      sha256: d7dc0630a923883c6328ca31b89aa682bacbf2f8304162d29f7c6aaff03a27a1
 | 
					      sha256: "3424e9d5c22fd7f7590254ba09465febd6f8827c8b19a44350de4ac31d92d3a6"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "11.1.0"
 | 
					    version: "12.0.0"
 | 
				
			||||||
  share_plus_platform_interface:
 | 
					  share_plus_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -2576,10 +2576,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: url_launcher_android
 | 
					      name: url_launcher_android
 | 
				
			||||||
      sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7"
 | 
					      sha256: "199bc33e746088546a39cc5f36bac5a278c5e53b40cb3196f99e7345fdcfae6b"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "6.3.18"
 | 
					    version: "6.3.22"
 | 
				
			||||||
  url_launcher_ios:
 | 
					  url_launcher_ios:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -2704,18 +2704,18 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: wakelock_plus
 | 
					      name: wakelock_plus
 | 
				
			||||||
      sha256: a474e314c3e8fb5adef1f9ae2d247e57467ad557fa7483a2b895bc1b421c5678
 | 
					      sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.3.2"
 | 
					    version: "1.4.0"
 | 
				
			||||||
  wakelock_plus_platform_interface:
 | 
					  wakelock_plus_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: wakelock_plus_platform_interface
 | 
					      name: wakelock_plus_platform_interface
 | 
				
			||||||
      sha256: e10444072e50dbc4999d7316fd303f7ea53d31c824aa5eb05d7ccbdd98985207
 | 
					      sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.2.3"
 | 
					    version: "1.3.0"
 | 
				
			||||||
  watcher:
 | 
					  watcher:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -2765,7 +2765,7 @@ packages:
 | 
				
			|||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.3.0"
 | 
					    version: "1.3.0"
 | 
				
			||||||
  win32:
 | 
					  win32:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: win32
 | 
					      name: win32
 | 
				
			||||||
      sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
 | 
					      sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
 | 
				
			||||||
@@ -2780,6 +2780,14 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.1.0"
 | 
					    version: "2.1.0"
 | 
				
			||||||
 | 
					  windows_notification:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: windows_notification
 | 
				
			||||||
 | 
					      sha256: be3e650874615f315402c9b9f3656e29af156709c4b5cc272cb4ca0ab7ba94a8
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "1.3.0"
 | 
				
			||||||
  xdg_directories:
 | 
					  xdg_directories:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -2806,4 +2814,4 @@ packages:
 | 
				
			|||||||
    version: "3.1.3"
 | 
					    version: "3.1.3"
 | 
				
			||||||
sdks:
 | 
					sdks:
 | 
				
			||||||
  dart: ">=3.9.0 <4.0.0"
 | 
					  dart: ">=3.9.0 <4.0.0"
 | 
				
			||||||
  flutter: ">=3.32.0"
 | 
					  flutter: ">=3.35.0"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										33
									
								
								pubspec.yaml
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								pubspec.yaml
									
									
									
									
									
								
							@@ -39,7 +39,7 @@ dependencies:
 | 
				
			|||||||
  flutter_hooks: ^0.21.3+1
 | 
					  flutter_hooks: ^0.21.3+1
 | 
				
			||||||
  hooks_riverpod: ^2.6.1
 | 
					  hooks_riverpod: ^2.6.1
 | 
				
			||||||
  bitsdojo_window: ^0.1.6
 | 
					  bitsdojo_window: ^0.1.6
 | 
				
			||||||
  go_router: ^16.2.1
 | 
					  go_router: ^16.2.2
 | 
				
			||||||
  styled_widget: ^0.4.1
 | 
					  styled_widget: ^0.4.1
 | 
				
			||||||
  shared_preferences: ^2.5.3
 | 
					  shared_preferences: ^2.5.3
 | 
				
			||||||
  flutter_riverpod: ^2.6.1
 | 
					  flutter_riverpod: ^2.6.1
 | 
				
			||||||
@@ -67,29 +67,29 @@ dependencies:
 | 
				
			|||||||
  easy_localization: ^3.0.8
 | 
					  easy_localization: ^3.0.8
 | 
				
			||||||
  flutter_inappwebview: ^6.1.5
 | 
					  flutter_inappwebview: ^6.1.5
 | 
				
			||||||
  animations: ^2.0.11
 | 
					  animations: ^2.0.11
 | 
				
			||||||
  package_info_plus: ^8.3.1
 | 
					  package_info_plus: ^9.0.0
 | 
				
			||||||
  device_info_plus: ^11.5.0
 | 
					  device_info_plus: ^11.5.0
 | 
				
			||||||
  tus_client_dart:
 | 
					  tus_client_dart:
 | 
				
			||||||
    git: https://github.com/LittleSheep2Code/tus_client.git
 | 
					    git: https://github.com/LittleSheep2Code/tus_client.git
 | 
				
			||||||
  cross_file: ^0.3.4+2
 | 
					  cross_file: ^0.3.4+2
 | 
				
			||||||
  image_picker: ^1.2.0
 | 
					  image_picker: ^1.2.0
 | 
				
			||||||
  file_picker: ^10.3.2
 | 
					  file_picker: ^10.3.3
 | 
				
			||||||
  riverpod_annotation: ^2.6.1
 | 
					  riverpod_annotation: ^2.6.1
 | 
				
			||||||
  image_picker_platform_interface: ^2.11.0
 | 
					  image_picker_platform_interface: ^2.11.0
 | 
				
			||||||
  image_picker_android: ^0.8.13+1
 | 
					  image_picker_android: ^0.8.13+3
 | 
				
			||||||
  super_context_menu: ^0.9.1
 | 
					  super_context_menu: ^0.9.1
 | 
				
			||||||
  modal_bottom_sheet: ^3.0.0
 | 
					  modal_bottom_sheet: ^3.0.0
 | 
				
			||||||
  firebase_messaging: ^16.0.1
 | 
					  firebase_messaging: ^16.0.1
 | 
				
			||||||
  flutter_udid: ^4.0.0
 | 
					  flutter_udid: ^4.0.0
 | 
				
			||||||
  firebase_core: ^4.1.0
 | 
					  firebase_core: ^4.1.0
 | 
				
			||||||
  web_socket_channel: ^3.0.3
 | 
					  web_socket_channel: ^3.0.3
 | 
				
			||||||
  material_symbols_icons: ^4.2872.0
 | 
					  material_symbols_icons: ^4.2873.0
 | 
				
			||||||
  drift: ^2.28.1
 | 
					  drift: ^2.28.1
 | 
				
			||||||
  drift_flutter: ^0.2.6
 | 
					  drift_flutter: ^0.2.6
 | 
				
			||||||
  path: ^1.9.1
 | 
					  path: ^1.9.1
 | 
				
			||||||
  collection: ^1.19.1
 | 
					  collection: ^1.19.1
 | 
				
			||||||
  markdown_editor_plus: ^0.2.15
 | 
					  markdown_editor_plus: ^0.2.15
 | 
				
			||||||
  croppy: ^1.3.6
 | 
					  croppy: ^1.4.0
 | 
				
			||||||
  table_calendar: ^3.2.0
 | 
					  table_calendar: ^3.2.0
 | 
				
			||||||
  relative_time: ^5.0.0
 | 
					  relative_time: ^5.0.0
 | 
				
			||||||
  dropdown_button2: ^2.3.9
 | 
					  dropdown_button2: ^2.3.9
 | 
				
			||||||
@@ -103,24 +103,25 @@ dependencies:
 | 
				
			|||||||
  gal: ^2.3.2
 | 
					  gal: ^2.3.2
 | 
				
			||||||
  dismissible_page: ^1.0.2
 | 
					  dismissible_page: ^1.0.2
 | 
				
			||||||
  super_sliver_list: ^0.4.1
 | 
					  super_sliver_list: ^0.4.1
 | 
				
			||||||
  livekit_client: ^2.5.0+hotfix.3
 | 
					  livekit_client: ^2.5.1
 | 
				
			||||||
  pasteboard: ^0.4.0
 | 
					  pasteboard: ^0.4.0
 | 
				
			||||||
  flutter_colorpicker: ^1.1.0
 | 
					  flutter_colorpicker: ^1.1.0
 | 
				
			||||||
 | 
					  image: ^4.5.4
 | 
				
			||||||
  record: ^6.1.1
 | 
					  record: ^6.1.1
 | 
				
			||||||
  qr_flutter: ^4.1.0
 | 
					  qr_flutter: ^4.1.0
 | 
				
			||||||
  flutter_otp_text_field: ^1.5.1+1
 | 
					  flutter_otp_text_field: ^1.5.1+1
 | 
				
			||||||
  palette_generator: ^0.3.3+7
 | 
					
 | 
				
			||||||
  flutter_popup_card: ^0.0.6
 | 
					  flutter_popup_card: ^0.0.6
 | 
				
			||||||
  timezone: ^0.10.1
 | 
					  timezone: ^0.10.1
 | 
				
			||||||
  flutter_timezone: ^4.1.1
 | 
					  flutter_timezone: ^5.0.0
 | 
				
			||||||
  fl_chart: ^1.1.0
 | 
					  fl_chart: ^1.1.1
 | 
				
			||||||
  sign_in_with_apple: ^7.0.1
 | 
					  sign_in_with_apple: ^7.0.1
 | 
				
			||||||
  flutter_svg: ^2.2.1
 | 
					  flutter_svg: ^2.2.1
 | 
				
			||||||
  native_exif: ^0.6.2
 | 
					  native_exif: ^0.6.2
 | 
				
			||||||
  local_auth: ^2.3.0
 | 
					  local_auth: ^2.3.0
 | 
				
			||||||
  flutter_secure_storage: ^9.2.4
 | 
					  flutter_secure_storage: ^9.2.4
 | 
				
			||||||
  flutter_math_fork: ^0.7.4
 | 
					  flutter_math_fork: ^0.7.4
 | 
				
			||||||
  share_plus: ^11.1.0
 | 
					  share_plus: ^12.0.0
 | 
				
			||||||
  receive_sharing_intent: ^1.8.1
 | 
					  receive_sharing_intent: ^1.8.1
 | 
				
			||||||
  top_snackbar_flutter: ^3.3.0
 | 
					  top_snackbar_flutter: ^3.3.0
 | 
				
			||||||
  textfield_tags:
 | 
					  textfield_tags:
 | 
				
			||||||
@@ -141,12 +142,16 @@ dependencies:
 | 
				
			|||||||
  flutter_card_swiper: ^7.0.2
 | 
					  flutter_card_swiper: ^7.0.2
 | 
				
			||||||
  file_saver: ^0.3.1
 | 
					  file_saver: ^0.3.1
 | 
				
			||||||
  tray_manager: ^0.5.1
 | 
					  tray_manager: ^0.5.1
 | 
				
			||||||
  flutter_webrtc: ^1.1.0
 | 
					  flutter_webrtc: ^1.2.0
 | 
				
			||||||
  flutter_local_notifications: ^19.4.1
 | 
					  flutter_local_notifications: ^19.4.2
 | 
				
			||||||
  wakelock_plus: ^1.3.2
 | 
					  wakelock_plus: ^1.4.0
 | 
				
			||||||
  slide_countdown: ^2.0.2
 | 
					  slide_countdown: ^2.0.2
 | 
				
			||||||
  shelf: ^1.4.2
 | 
					  shelf: ^1.4.2
 | 
				
			||||||
  shelf_web_socket: ^3.0.0
 | 
					  shelf_web_socket: ^3.0.0
 | 
				
			||||||
 | 
					  windows_notification: ^1.3.0
 | 
				
			||||||
 | 
					  win32: ^5.14.0
 | 
				
			||||||
 | 
					  ffi: ^2.1.4
 | 
				
			||||||
 | 
					  dart_ipc: ^1.0.1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dev_dependencies:
 | 
					dev_dependencies:
 | 
				
			||||||
  flutter_test:
 | 
					  flutter_test:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
; ==================================================
 | 
					; ==================================================
 | 
				
			||||||
#define AppVersion "3.2.0"
 | 
					#define AppVersion "3.2.0"
 | 
				
			||||||
#define BuildNumber "124"
 | 
					#define BuildNumber "132"
 | 
				
			||||||
; ==================================================
 | 
					; ==================================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#define FullVersion AppVersion + "." + BuildNumber
 | 
					#define FullVersion AppVersion + "." + BuildNumber
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
 | 
					#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
 | 
				
			||||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
 | 
					#include <connectivity_plus/connectivity_plus_windows_plugin.h>
 | 
				
			||||||
 | 
					#include <dart_ipc/dart_ipc_plugin_c_api.h>
 | 
				
			||||||
#include <file_saver/file_saver_plugin.h>
 | 
					#include <file_saver/file_saver_plugin.h>
 | 
				
			||||||
#include <file_selector_windows/file_selector_windows.h>
 | 
					#include <file_selector_windows/file_selector_windows.h>
 | 
				
			||||||
#include <firebase_core/firebase_core_plugin_c_api.h>
 | 
					#include <firebase_core/firebase_core_plugin_c_api.h>
 | 
				
			||||||
@@ -31,12 +32,15 @@
 | 
				
			|||||||
#include <tray_manager/tray_manager_plugin.h>
 | 
					#include <tray_manager/tray_manager_plugin.h>
 | 
				
			||||||
#include <url_launcher_windows/url_launcher_windows.h>
 | 
					#include <url_launcher_windows/url_launcher_windows.h>
 | 
				
			||||||
#include <volume_controller/volume_controller_plugin_c_api.h>
 | 
					#include <volume_controller/volume_controller_plugin_c_api.h>
 | 
				
			||||||
 | 
					#include <windows_notification/windows_notification_plugin_c_api.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
 | 
					void RegisterPlugins(flutter::PluginRegistry* registry) {
 | 
				
			||||||
  BitsdojoWindowPluginRegisterWithRegistrar(
 | 
					  BitsdojoWindowPluginRegisterWithRegistrar(
 | 
				
			||||||
      registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
 | 
					      registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
 | 
				
			||||||
  ConnectivityPlusWindowsPluginRegisterWithRegistrar(
 | 
					  ConnectivityPlusWindowsPluginRegisterWithRegistrar(
 | 
				
			||||||
      registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
 | 
					      registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
 | 
				
			||||||
 | 
					  DartIpcPluginCApiRegisterWithRegistrar(
 | 
				
			||||||
 | 
					      registry->GetRegistrarForPlugin("DartIpcPluginCApi"));
 | 
				
			||||||
  FileSaverPluginRegisterWithRegistrar(
 | 
					  FileSaverPluginRegisterWithRegistrar(
 | 
				
			||||||
      registry->GetRegistrarForPlugin("FileSaverPlugin"));
 | 
					      registry->GetRegistrarForPlugin("FileSaverPlugin"));
 | 
				
			||||||
  FileSelectorWindowsRegisterWithRegistrar(
 | 
					  FileSelectorWindowsRegisterWithRegistrar(
 | 
				
			||||||
@@ -83,4 +87,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
 | 
				
			|||||||
      registry->GetRegistrarForPlugin("UrlLauncherWindows"));
 | 
					      registry->GetRegistrarForPlugin("UrlLauncherWindows"));
 | 
				
			||||||
  VolumeControllerPluginCApiRegisterWithRegistrar(
 | 
					  VolumeControllerPluginCApiRegisterWithRegistrar(
 | 
				
			||||||
      registry->GetRegistrarForPlugin("VolumeControllerPluginCApi"));
 | 
					      registry->GetRegistrarForPlugin("VolumeControllerPluginCApi"));
 | 
				
			||||||
 | 
					  WindowsNotificationPluginCApiRegisterWithRegistrar(
 | 
				
			||||||
 | 
					      registry->GetRegistrarForPlugin("WindowsNotificationPluginCApi"));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@
 | 
				
			|||||||
list(APPEND FLUTTER_PLUGIN_LIST
 | 
					list(APPEND FLUTTER_PLUGIN_LIST
 | 
				
			||||||
  bitsdojo_window_windows
 | 
					  bitsdojo_window_windows
 | 
				
			||||||
  connectivity_plus
 | 
					  connectivity_plus
 | 
				
			||||||
 | 
					  dart_ipc
 | 
				
			||||||
  file_saver
 | 
					  file_saver
 | 
				
			||||||
  file_selector_windows
 | 
					  file_selector_windows
 | 
				
			||||||
  firebase_core
 | 
					  firebase_core
 | 
				
			||||||
@@ -28,6 +29,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
 | 
				
			|||||||
  tray_manager
 | 
					  tray_manager
 | 
				
			||||||
  url_launcher_windows
 | 
					  url_launcher_windows
 | 
				
			||||||
  volume_controller
 | 
					  volume_controller
 | 
				
			||||||
 | 
					  windows_notification
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
 | 
					list(APPEND FLUTTER_FFI_PLUGIN_LIST
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user