Compare commits
	
		
			16 Commits
		
	
	
		
			cf355a95fd
			...
			3.2.0+125
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fbfe8cbdee | |||
| fbbab0a981 | |||
| ae2fb3b303 | |||
| 3d7a4666ed | |||
| 5d3e0fb800 | |||
| 85ff52a661 | |||
| da7fd64a43 | |||
| 3902633217 | |||
| f478ea8b84 | |||
| 0f481aff5b | |||
| 7a31663310 | |||
| 0239c53c04 | |||
| 16987c758e | |||
| 3a36915140 | |||
| 4bde708878 | |||
| 2f0cf560f8 | 
							
								
								
									
										9
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							@@ -41,6 +41,15 @@ jobs:
 | 
			
		||||
        with:
 | 
			
		||||
          name: build-output-windows
 | 
			
		||||
          path: build/windows/x64/runner/Release
 | 
			
		||||
      - name: Compile Installer
 | 
			
		||||
        uses: Minionguyjpro/Inno-Setup-Action@v1.2.2
 | 
			
		||||
        with:
 | 
			
		||||
          path: setup.iss
 | 
			
		||||
      - name: Archive installer artifacts
 | 
			
		||||
        uses: actions/upload-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: build-output-windows-installer
 | 
			
		||||
          path: Installer/windows-x86_64-setup.exe
 | 
			
		||||
  build-linux:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -12,6 +12,9 @@
 | 
			
		||||
.swiftpm/
 | 
			
		||||
migrate_working_dir/
 | 
			
		||||
 | 
			
		||||
# Inno Setup
 | 
			
		||||
Installer/
 | 
			
		||||
 | 
			
		||||
# IntelliJ related
 | 
			
		||||
*.iml
 | 
			
		||||
*.ipr
 | 
			
		||||
 
 | 
			
		||||
@@ -761,6 +761,7 @@
 | 
			
		||||
  "pollsRecent": "Recent Polls",
 | 
			
		||||
  "pollCreateNew": "Create New",
 | 
			
		||||
  "pollCreateNewHint": "Create a new poll for your post. Pick a publisher and continue.",
 | 
			
		||||
  "pollQuestions": "Questions",
 | 
			
		||||
  "publisher": "Publisher",
 | 
			
		||||
  "publisherHint": "Enter the publisher name",
 | 
			
		||||
  "publisherCannotBeEmpty": "Publisher cannot be empty",
 | 
			
		||||
@@ -798,5 +799,42 @@
 | 
			
		||||
  "filesListAdditional": {
 | 
			
		||||
    "one": "+{} file remaining",
 | 
			
		||||
    "other": "+{} files remaining"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
  },
 | 
			
		||||
  "pollAnswerSubmitted": "Poll answer has been submitted.",
 | 
			
		||||
  "modifyAnswers": "Modify Answers",
 | 
			
		||||
  "back": "Back",
 | 
			
		||||
  "submit": "Submit",
 | 
			
		||||
  "pollOptionDefaultLabel": "Option 1",
 | 
			
		||||
  "pollUpdated": "Poll updated.",
 | 
			
		||||
  "pollCreated": "Poll created.",
 | 
			
		||||
  "pollCreate": "Create Poll",
 | 
			
		||||
  "pollEdit": "Edit Poll",
 | 
			
		||||
  "pollPreviewJsonDebug": "Debug Preview",
 | 
			
		||||
  "pollTitleRequired": "Title is required",
 | 
			
		||||
  "pollEndDateOptional": "End date & time (optional)",
 | 
			
		||||
  "notSet": "Not set",
 | 
			
		||||
  "pick": "Pick",
 | 
			
		||||
  "clear": "Clear",
 | 
			
		||||
  "questions": "Questions",
 | 
			
		||||
  "pollAddQuestion": "Add question",
 | 
			
		||||
  "pollQuestionTypeSingleChoice": "Single choice",
 | 
			
		||||
  "pollQuestionTypeMultipleChoice": "Multiple choice",
 | 
			
		||||
  "pollQuestionTypeFreeText": "Free text",
 | 
			
		||||
  "pollQuestionTypeYesNo": "Yes / No",
 | 
			
		||||
  "pollQuestionTypeRating": "Rating",
 | 
			
		||||
  "pollNoQuestionsYet": "No questions yet",
 | 
			
		||||
  "pollNoQuestionsHint": "Use \"Add question\" to start building your poll.",
 | 
			
		||||
  "pollDebugPreview": "Debug Preview",
 | 
			
		||||
  "pollUntitledQuestion": "Untitled question",
 | 
			
		||||
  "moveUp": "Move up",
 | 
			
		||||
  "moveDown": "Move down",
 | 
			
		||||
  "required": "Required",
 | 
			
		||||
  "pollQuestionTitle": "Question title",
 | 
			
		||||
  "pollQuestionTitleRequired": "Question title is required",
 | 
			
		||||
  "pollQuestionDescriptionOptional": "Question description (optional)",
 | 
			
		||||
  "options": "Options",
 | 
			
		||||
  "pollAddOption": "Add option",
 | 
			
		||||
  "pollOptionLabel": "Option label",
 | 
			
		||||
  "pollLongTextAnswerPreview": "Long text answer (preview)",
 | 
			
		||||
  "pollShortTextAnswerPreview": "Short text answer (preview)"
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/icons/icon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/icons/icon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 108 KiB  | 
@@ -62,12 +62,17 @@ void main() async {
 | 
			
		||||
      FirebaseMessaging.onBackgroundMessage(
 | 
			
		||||
        _firebaseMessagingBackgroundHandler,
 | 
			
		||||
      );
 | 
			
		||||
      FlutterError.onError =
 | 
			
		||||
          FirebaseCrashlytics.instance.recordFlutterFatalError;
 | 
			
		||||
      PlatformDispatcher.instance.onError = (error, stack) {
 | 
			
		||||
        FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
 | 
			
		||||
        return true;
 | 
			
		||||
      };
 | 
			
		||||
      // Although previous if case checked this. Still check is web or not
 | 
			
		||||
      // Otherwise the web platform will broke due to there is no Platform api on the web
 | 
			
		||||
      // Skip crashlytics setup on debug mode to prevent unexpected report to firebase
 | 
			
		||||
      if ((kIsWeb || !Platform.isWindows) && !kDebugMode) {
 | 
			
		||||
        FlutterError.onError =
 | 
			
		||||
            FirebaseCrashlytics.instance.recordFlutterFatalError;
 | 
			
		||||
        PlatformDispatcher.instance.onError = (error, stack) {
 | 
			
		||||
          FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
 | 
			
		||||
          return true;
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    log("[SplashScreen] Firebase is ready!");
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
import 'package:freezed_annotation/freezed_annotation.dart';
 | 
			
		||||
import 'package:island/models/auth.dart';
 | 
			
		||||
import 'package:island/models/file.dart';
 | 
			
		||||
import 'package:island/models/wallet.dart';
 | 
			
		||||
 | 
			
		||||
part 'user.freezed.dart';
 | 
			
		||||
part 'user.g.dart';
 | 
			
		||||
part 'account.freezed.dart';
 | 
			
		||||
part 'account.g.dart';
 | 
			
		||||
 | 
			
		||||
@freezed
 | 
			
		||||
sealed class SnAccount with _$SnAccount {
 | 
			
		||||
@@ -174,3 +175,36 @@ sealed class SnVerificationMark with _$SnVerificationMark {
 | 
			
		||||
  factory SnVerificationMark.fromJson(Map<String, dynamic> json) =>
 | 
			
		||||
      _$SnVerificationMarkFromJson(json);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@freezed
 | 
			
		||||
sealed class SnAuthDevice with _$SnAuthDevice {
 | 
			
		||||
  const factory SnAuthDevice({
 | 
			
		||||
    required String id,
 | 
			
		||||
    required String deviceId,
 | 
			
		||||
    required String deviceName,
 | 
			
		||||
    required String? deviceLabel,
 | 
			
		||||
    required String accountId,
 | 
			
		||||
    required int platform,
 | 
			
		||||
    @Default(false) bool isCurrent,
 | 
			
		||||
  }) = _SnAuthDevice;
 | 
			
		||||
 | 
			
		||||
  factory SnAuthDevice.fromJson(Map<String, dynamic> json) =>
 | 
			
		||||
      _$SnAuthDeviceFromJson(json);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@freezed
 | 
			
		||||
sealed class SnAuthDeviceWithChallenge with _$SnAuthDeviceWithChallenge {
 | 
			
		||||
  const factory SnAuthDeviceWithChallenge({
 | 
			
		||||
    required String id,
 | 
			
		||||
    required String deviceId,
 | 
			
		||||
    required String deviceName,
 | 
			
		||||
    required String? deviceLabel,
 | 
			
		||||
    required String accountId,
 | 
			
		||||
    required int platform,
 | 
			
		||||
    required List<SnAuthChallenge> challenges,
 | 
			
		||||
    @Default(false) bool isCurrent,
 | 
			
		||||
  }) = _SnAuthDeviceWithChallengee;
 | 
			
		||||
 | 
			
		||||
  factory SnAuthDeviceWithChallenge.fromJson(Map<String, dynamic> json) =>
 | 
			
		||||
      _$SnAuthDeviceWithChallengeFromJson(json);
 | 
			
		||||
}
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
// ignore_for_file: type=lint
 | 
			
		||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
 | 
			
		||||
 | 
			
		||||
part of 'user.dart';
 | 
			
		||||
part of 'account.dart';
 | 
			
		||||
 | 
			
		||||
// **************************************************************************
 | 
			
		||||
// FreezedGenerator
 | 
			
		||||
@@ -2452,6 +2452,572 @@ as String?,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
mixin _$SnAuthDevice {
 | 
			
		||||
 | 
			
		||||
 String get id; String get deviceId; String get deviceName; String? get deviceLabel; String get accountId; int get platform; bool get isCurrent;
 | 
			
		||||
/// Create a copy of SnAuthDevice
 | 
			
		||||
/// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
@pragma('vm:prefer-inline')
 | 
			
		||||
$SnAuthDeviceCopyWith<SnAuthDevice> get copyWith => _$SnAuthDeviceCopyWithImpl<SnAuthDevice>(this as SnAuthDevice, _$identity);
 | 
			
		||||
 | 
			
		||||
  /// Serializes this SnAuthDevice to a JSON map.
 | 
			
		||||
  Map<String, dynamic> toJson();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@override
 | 
			
		||||
bool operator ==(Object other) {
 | 
			
		||||
  return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAuthDevice&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)&&(identical(other.deviceLabel, deviceLabel) || other.deviceLabel == deviceLabel)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.platform, platform) || other.platform == platform)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
@override
 | 
			
		||||
int get hashCode => Object.hash(runtimeType,id,deviceId,deviceName,deviceLabel,accountId,platform,isCurrent);
 | 
			
		||||
 | 
			
		||||
@override
 | 
			
		||||
String toString() {
 | 
			
		||||
  return 'SnAuthDevice(id: $id, deviceId: $deviceId, deviceName: $deviceName, deviceLabel: $deviceLabel, accountId: $accountId, platform: $platform, isCurrent: $isCurrent)';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
abstract mixin class $SnAuthDeviceCopyWith<$Res>  {
 | 
			
		||||
  factory $SnAuthDeviceCopyWith(SnAuthDevice value, $Res Function(SnAuthDevice) _then) = _$SnAuthDeviceCopyWithImpl;
 | 
			
		||||
@useResult
 | 
			
		||||
$Res call({
 | 
			
		||||
 String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, bool isCurrent
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
/// @nodoc
 | 
			
		||||
class _$SnAuthDeviceCopyWithImpl<$Res>
 | 
			
		||||
    implements $SnAuthDeviceCopyWith<$Res> {
 | 
			
		||||
  _$SnAuthDeviceCopyWithImpl(this._self, this._then);
 | 
			
		||||
 | 
			
		||||
  final SnAuthDevice _self;
 | 
			
		||||
  final $Res Function(SnAuthDevice) _then;
 | 
			
		||||
 | 
			
		||||
/// Create a copy of SnAuthDevice
 | 
			
		||||
/// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? deviceId = null,Object? deviceName = null,Object? deviceLabel = freezed,Object? accountId = null,Object? platform = null,Object? isCurrent = null,}) {
 | 
			
		||||
  return _then(_self.copyWith(
 | 
			
		||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,deviceName: null == deviceName ? _self.deviceName : deviceName // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,deviceLabel: freezed == deviceLabel ? _self.deviceLabel : deviceLabel // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as int,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as bool,
 | 
			
		||||
  ));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/// Adds pattern-matching-related methods to [SnAuthDevice].
 | 
			
		||||
extension SnAuthDevicePatterns on SnAuthDevice {
 | 
			
		||||
/// A variant of `map` that fallback to returning `orElse`.
 | 
			
		||||
///
 | 
			
		||||
/// It is equivalent to doing:
 | 
			
		||||
/// ```dart
 | 
			
		||||
/// switch (sealedClass) {
 | 
			
		||||
///   case final Subclass value:
 | 
			
		||||
///     return ...;
 | 
			
		||||
///   case _:
 | 
			
		||||
///     return orElse();
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
 | 
			
		||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnAuthDevice value)?  $default,{required TResult orElse(),}){
 | 
			
		||||
final _that = this;
 | 
			
		||||
switch (_that) {
 | 
			
		||||
case _SnAuthDevice() when $default != null:
 | 
			
		||||
return $default(_that);case _:
 | 
			
		||||
  return orElse();
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
/// A `switch`-like method, using callbacks.
 | 
			
		||||
///
 | 
			
		||||
/// Callbacks receives the raw object, upcasted.
 | 
			
		||||
/// It is equivalent to doing:
 | 
			
		||||
/// ```dart
 | 
			
		||||
/// switch (sealedClass) {
 | 
			
		||||
///   case final Subclass value:
 | 
			
		||||
///     return ...;
 | 
			
		||||
///   case final Subclass2 value:
 | 
			
		||||
///     return ...;
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
 | 
			
		||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnAuthDevice value)  $default,){
 | 
			
		||||
final _that = this;
 | 
			
		||||
switch (_that) {
 | 
			
		||||
case _SnAuthDevice():
 | 
			
		||||
return $default(_that);}
 | 
			
		||||
}
 | 
			
		||||
/// A variant of `map` that fallback to returning `null`.
 | 
			
		||||
///
 | 
			
		||||
/// It is equivalent to doing:
 | 
			
		||||
/// ```dart
 | 
			
		||||
/// switch (sealedClass) {
 | 
			
		||||
///   case final Subclass value:
 | 
			
		||||
///     return ...;
 | 
			
		||||
///   case _:
 | 
			
		||||
///     return null;
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
 | 
			
		||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnAuthDevice value)?  $default,){
 | 
			
		||||
final _that = this;
 | 
			
		||||
switch (_that) {
 | 
			
		||||
case _SnAuthDevice() when $default != null:
 | 
			
		||||
return $default(_that);case _:
 | 
			
		||||
  return null;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
/// A variant of `when` that fallback to an `orElse` callback.
 | 
			
		||||
///
 | 
			
		||||
/// It is equivalent to doing:
 | 
			
		||||
/// ```dart
 | 
			
		||||
/// switch (sealedClass) {
 | 
			
		||||
///   case Subclass(:final field):
 | 
			
		||||
///     return ...;
 | 
			
		||||
///   case _:
 | 
			
		||||
///     return orElse();
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
 | 
			
		||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String deviceId,  String deviceName,  String? deviceLabel,  String accountId,  int platform,  bool isCurrent)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
			
		||||
switch (_that) {
 | 
			
		||||
case _SnAuthDevice() when $default != null:
 | 
			
		||||
return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.isCurrent);case _:
 | 
			
		||||
  return orElse();
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
/// A `switch`-like method, using callbacks.
 | 
			
		||||
///
 | 
			
		||||
/// As opposed to `map`, this offers destructuring.
 | 
			
		||||
/// It is equivalent to doing:
 | 
			
		||||
/// ```dart
 | 
			
		||||
/// switch (sealedClass) {
 | 
			
		||||
///   case Subclass(:final field):
 | 
			
		||||
///     return ...;
 | 
			
		||||
///   case Subclass2(:final field2):
 | 
			
		||||
///     return ...;
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
 | 
			
		||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String deviceId,  String deviceName,  String? deviceLabel,  String accountId,  int platform,  bool isCurrent)  $default,) {final _that = this;
 | 
			
		||||
switch (_that) {
 | 
			
		||||
case _SnAuthDevice():
 | 
			
		||||
return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.isCurrent);}
 | 
			
		||||
}
 | 
			
		||||
/// A variant of `when` that fallback to returning `null`
 | 
			
		||||
///
 | 
			
		||||
/// It is equivalent to doing:
 | 
			
		||||
/// ```dart
 | 
			
		||||
/// switch (sealedClass) {
 | 
			
		||||
///   case Subclass(:final field):
 | 
			
		||||
///     return ...;
 | 
			
		||||
///   case _:
 | 
			
		||||
///     return null;
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
 | 
			
		||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String deviceId,  String deviceName,  String? deviceLabel,  String accountId,  int platform,  bool isCurrent)?  $default,) {final _that = this;
 | 
			
		||||
switch (_that) {
 | 
			
		||||
case _SnAuthDevice() when $default != null:
 | 
			
		||||
return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.isCurrent);case _:
 | 
			
		||||
  return null;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
@JsonSerializable()
 | 
			
		||||
 | 
			
		||||
class _SnAuthDevice implements SnAuthDevice {
 | 
			
		||||
  const _SnAuthDevice({required this.id, required this.deviceId, required this.deviceName, required this.deviceLabel, required this.accountId, required this.platform, this.isCurrent = false});
 | 
			
		||||
  factory _SnAuthDevice.fromJson(Map<String, dynamic> json) => _$SnAuthDeviceFromJson(json);
 | 
			
		||||
 | 
			
		||||
@override final  String id;
 | 
			
		||||
@override final  String deviceId;
 | 
			
		||||
@override final  String deviceName;
 | 
			
		||||
@override final  String? deviceLabel;
 | 
			
		||||
@override final  String accountId;
 | 
			
		||||
@override final  int platform;
 | 
			
		||||
@override@JsonKey() final  bool isCurrent;
 | 
			
		||||
 | 
			
		||||
/// Create a copy of SnAuthDevice
 | 
			
		||||
/// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
@pragma('vm:prefer-inline')
 | 
			
		||||
_$SnAuthDeviceCopyWith<_SnAuthDevice> get copyWith => __$SnAuthDeviceCopyWithImpl<_SnAuthDevice>(this, _$identity);
 | 
			
		||||
 | 
			
		||||
@override
 | 
			
		||||
Map<String, dynamic> toJson() {
 | 
			
		||||
  return _$SnAuthDeviceToJson(this, );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@override
 | 
			
		||||
bool operator ==(Object other) {
 | 
			
		||||
  return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAuthDevice&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)&&(identical(other.deviceLabel, deviceLabel) || other.deviceLabel == deviceLabel)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.platform, platform) || other.platform == platform)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
@override
 | 
			
		||||
int get hashCode => Object.hash(runtimeType,id,deviceId,deviceName,deviceLabel,accountId,platform,isCurrent);
 | 
			
		||||
 | 
			
		||||
@override
 | 
			
		||||
String toString() {
 | 
			
		||||
  return 'SnAuthDevice(id: $id, deviceId: $deviceId, deviceName: $deviceName, deviceLabel: $deviceLabel, accountId: $accountId, platform: $platform, isCurrent: $isCurrent)';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
abstract mixin class _$SnAuthDeviceCopyWith<$Res> implements $SnAuthDeviceCopyWith<$Res> {
 | 
			
		||||
  factory _$SnAuthDeviceCopyWith(_SnAuthDevice value, $Res Function(_SnAuthDevice) _then) = __$SnAuthDeviceCopyWithImpl;
 | 
			
		||||
@override @useResult
 | 
			
		||||
$Res call({
 | 
			
		||||
 String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, bool isCurrent
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
/// @nodoc
 | 
			
		||||
class __$SnAuthDeviceCopyWithImpl<$Res>
 | 
			
		||||
    implements _$SnAuthDeviceCopyWith<$Res> {
 | 
			
		||||
  __$SnAuthDeviceCopyWithImpl(this._self, this._then);
 | 
			
		||||
 | 
			
		||||
  final _SnAuthDevice _self;
 | 
			
		||||
  final $Res Function(_SnAuthDevice) _then;
 | 
			
		||||
 | 
			
		||||
/// Create a copy of SnAuthDevice
 | 
			
		||||
/// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? deviceId = null,Object? deviceName = null,Object? deviceLabel = freezed,Object? accountId = null,Object? platform = null,Object? isCurrent = null,}) {
 | 
			
		||||
  return _then(_SnAuthDevice(
 | 
			
		||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,deviceName: null == deviceName ? _self.deviceName : deviceName // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,deviceLabel: freezed == deviceLabel ? _self.deviceLabel : deviceLabel // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as int,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as bool,
 | 
			
		||||
  ));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SnAuthDeviceWithChallenge _$SnAuthDeviceWithChallengeFromJson(
 | 
			
		||||
  Map<String, dynamic> json
 | 
			
		||||
) {
 | 
			
		||||
    return _SnAuthDeviceWithChallengee.fromJson(
 | 
			
		||||
      json
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
mixin _$SnAuthDeviceWithChallenge {
 | 
			
		||||
 | 
			
		||||
 String get id; String get deviceId; String get deviceName; String? get deviceLabel; String get accountId; int get platform; List<SnAuthChallenge> get challenges; bool get isCurrent;
 | 
			
		||||
/// Create a copy of SnAuthDeviceWithChallenge
 | 
			
		||||
/// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
@pragma('vm:prefer-inline')
 | 
			
		||||
$SnAuthDeviceWithChallengeCopyWith<SnAuthDeviceWithChallenge> get copyWith => _$SnAuthDeviceWithChallengeCopyWithImpl<SnAuthDeviceWithChallenge>(this as SnAuthDeviceWithChallenge, _$identity);
 | 
			
		||||
 | 
			
		||||
  /// Serializes this SnAuthDeviceWithChallenge to a JSON map.
 | 
			
		||||
  Map<String, dynamic> toJson();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@override
 | 
			
		||||
bool operator ==(Object other) {
 | 
			
		||||
  return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAuthDeviceWithChallenge&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)&&(identical(other.deviceLabel, deviceLabel) || other.deviceLabel == deviceLabel)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.platform, platform) || other.platform == platform)&&const DeepCollectionEquality().equals(other.challenges, challenges)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
@override
 | 
			
		||||
int get hashCode => Object.hash(runtimeType,id,deviceId,deviceName,deviceLabel,accountId,platform,const DeepCollectionEquality().hash(challenges),isCurrent);
 | 
			
		||||
 | 
			
		||||
@override
 | 
			
		||||
String toString() {
 | 
			
		||||
  return 'SnAuthDeviceWithChallenge(id: $id, deviceId: $deviceId, deviceName: $deviceName, deviceLabel: $deviceLabel, accountId: $accountId, platform: $platform, challenges: $challenges, isCurrent: $isCurrent)';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
abstract mixin class $SnAuthDeviceWithChallengeCopyWith<$Res>  {
 | 
			
		||||
  factory $SnAuthDeviceWithChallengeCopyWith(SnAuthDeviceWithChallenge value, $Res Function(SnAuthDeviceWithChallenge) _then) = _$SnAuthDeviceWithChallengeCopyWithImpl;
 | 
			
		||||
@useResult
 | 
			
		||||
$Res call({
 | 
			
		||||
 String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, List<SnAuthChallenge> challenges, bool isCurrent
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
/// @nodoc
 | 
			
		||||
class _$SnAuthDeviceWithChallengeCopyWithImpl<$Res>
 | 
			
		||||
    implements $SnAuthDeviceWithChallengeCopyWith<$Res> {
 | 
			
		||||
  _$SnAuthDeviceWithChallengeCopyWithImpl(this._self, this._then);
 | 
			
		||||
 | 
			
		||||
  final SnAuthDeviceWithChallenge _self;
 | 
			
		||||
  final $Res Function(SnAuthDeviceWithChallenge) _then;
 | 
			
		||||
 | 
			
		||||
/// Create a copy of SnAuthDeviceWithChallenge
 | 
			
		||||
/// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? deviceId = null,Object? deviceName = null,Object? deviceLabel = freezed,Object? accountId = null,Object? platform = null,Object? challenges = null,Object? isCurrent = null,}) {
 | 
			
		||||
  return _then(_self.copyWith(
 | 
			
		||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,deviceName: null == deviceName ? _self.deviceName : deviceName // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,deviceLabel: freezed == deviceLabel ? _self.deviceLabel : deviceLabel // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as int,challenges: null == challenges ? _self.challenges : challenges // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as List<SnAuthChallenge>,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as bool,
 | 
			
		||||
  ));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/// Adds pattern-matching-related methods to [SnAuthDeviceWithChallenge].
 | 
			
		||||
extension SnAuthDeviceWithChallengePatterns on SnAuthDeviceWithChallenge {
 | 
			
		||||
/// A variant of `map` that fallback to returning `orElse`.
 | 
			
		||||
///
 | 
			
		||||
/// It is equivalent to doing:
 | 
			
		||||
/// ```dart
 | 
			
		||||
/// switch (sealedClass) {
 | 
			
		||||
///   case final Subclass value:
 | 
			
		||||
///     return ...;
 | 
			
		||||
///   case _:
 | 
			
		||||
///     return orElse();
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
 | 
			
		||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnAuthDeviceWithChallengee value)?  $default,{required TResult orElse(),}){
 | 
			
		||||
final _that = this;
 | 
			
		||||
switch (_that) {
 | 
			
		||||
case _SnAuthDeviceWithChallengee() when $default != null:
 | 
			
		||||
return $default(_that);case _:
 | 
			
		||||
  return orElse();
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
/// A `switch`-like method, using callbacks.
 | 
			
		||||
///
 | 
			
		||||
/// Callbacks receives the raw object, upcasted.
 | 
			
		||||
/// It is equivalent to doing:
 | 
			
		||||
/// ```dart
 | 
			
		||||
/// switch (sealedClass) {
 | 
			
		||||
///   case final Subclass value:
 | 
			
		||||
///     return ...;
 | 
			
		||||
///   case final Subclass2 value:
 | 
			
		||||
///     return ...;
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
 | 
			
		||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnAuthDeviceWithChallengee value)  $default,){
 | 
			
		||||
final _that = this;
 | 
			
		||||
switch (_that) {
 | 
			
		||||
case _SnAuthDeviceWithChallengee():
 | 
			
		||||
return $default(_that);}
 | 
			
		||||
}
 | 
			
		||||
/// A variant of `map` that fallback to returning `null`.
 | 
			
		||||
///
 | 
			
		||||
/// It is equivalent to doing:
 | 
			
		||||
/// ```dart
 | 
			
		||||
/// switch (sealedClass) {
 | 
			
		||||
///   case final Subclass value:
 | 
			
		||||
///     return ...;
 | 
			
		||||
///   case _:
 | 
			
		||||
///     return null;
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
 | 
			
		||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnAuthDeviceWithChallengee value)?  $default,){
 | 
			
		||||
final _that = this;
 | 
			
		||||
switch (_that) {
 | 
			
		||||
case _SnAuthDeviceWithChallengee() when $default != null:
 | 
			
		||||
return $default(_that);case _:
 | 
			
		||||
  return null;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
/// A variant of `when` that fallback to an `orElse` callback.
 | 
			
		||||
///
 | 
			
		||||
/// It is equivalent to doing:
 | 
			
		||||
/// ```dart
 | 
			
		||||
/// switch (sealedClass) {
 | 
			
		||||
///   case Subclass(:final field):
 | 
			
		||||
///     return ...;
 | 
			
		||||
///   case _:
 | 
			
		||||
///     return orElse();
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
 | 
			
		||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String deviceId,  String deviceName,  String? deviceLabel,  String accountId,  int platform,  List<SnAuthChallenge> challenges,  bool isCurrent)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
			
		||||
switch (_that) {
 | 
			
		||||
case _SnAuthDeviceWithChallengee() when $default != null:
 | 
			
		||||
return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.challenges,_that.isCurrent);case _:
 | 
			
		||||
  return orElse();
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
/// A `switch`-like method, using callbacks.
 | 
			
		||||
///
 | 
			
		||||
/// As opposed to `map`, this offers destructuring.
 | 
			
		||||
/// It is equivalent to doing:
 | 
			
		||||
/// ```dart
 | 
			
		||||
/// switch (sealedClass) {
 | 
			
		||||
///   case Subclass(:final field):
 | 
			
		||||
///     return ...;
 | 
			
		||||
///   case Subclass2(:final field2):
 | 
			
		||||
///     return ...;
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
 | 
			
		||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String deviceId,  String deviceName,  String? deviceLabel,  String accountId,  int platform,  List<SnAuthChallenge> challenges,  bool isCurrent)  $default,) {final _that = this;
 | 
			
		||||
switch (_that) {
 | 
			
		||||
case _SnAuthDeviceWithChallengee():
 | 
			
		||||
return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.challenges,_that.isCurrent);}
 | 
			
		||||
}
 | 
			
		||||
/// A variant of `when` that fallback to returning `null`
 | 
			
		||||
///
 | 
			
		||||
/// It is equivalent to doing:
 | 
			
		||||
/// ```dart
 | 
			
		||||
/// switch (sealedClass) {
 | 
			
		||||
///   case Subclass(:final field):
 | 
			
		||||
///     return ...;
 | 
			
		||||
///   case _:
 | 
			
		||||
///     return null;
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
 | 
			
		||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String deviceId,  String deviceName,  String? deviceLabel,  String accountId,  int platform,  List<SnAuthChallenge> challenges,  bool isCurrent)?  $default,) {final _that = this;
 | 
			
		||||
switch (_that) {
 | 
			
		||||
case _SnAuthDeviceWithChallengee() when $default != null:
 | 
			
		||||
return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.challenges,_that.isCurrent);case _:
 | 
			
		||||
  return null;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
@JsonSerializable()
 | 
			
		||||
 | 
			
		||||
class _SnAuthDeviceWithChallengee implements SnAuthDeviceWithChallenge {
 | 
			
		||||
  const _SnAuthDeviceWithChallengee({required this.id, required this.deviceId, required this.deviceName, required this.deviceLabel, required this.accountId, required this.platform, required final  List<SnAuthChallenge> challenges, this.isCurrent = false}): _challenges = challenges;
 | 
			
		||||
  factory _SnAuthDeviceWithChallengee.fromJson(Map<String, dynamic> json) => _$SnAuthDeviceWithChallengeeFromJson(json);
 | 
			
		||||
 | 
			
		||||
@override final  String id;
 | 
			
		||||
@override final  String deviceId;
 | 
			
		||||
@override final  String deviceName;
 | 
			
		||||
@override final  String? deviceLabel;
 | 
			
		||||
@override final  String accountId;
 | 
			
		||||
@override final  int platform;
 | 
			
		||||
 final  List<SnAuthChallenge> _challenges;
 | 
			
		||||
@override List<SnAuthChallenge> get challenges {
 | 
			
		||||
  if (_challenges is EqualUnmodifiableListView) return _challenges;
 | 
			
		||||
  // ignore: implicit_dynamic_type
 | 
			
		||||
  return EqualUnmodifiableListView(_challenges);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@override@JsonKey() final  bool isCurrent;
 | 
			
		||||
 | 
			
		||||
/// Create a copy of SnAuthDeviceWithChallenge
 | 
			
		||||
/// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
@pragma('vm:prefer-inline')
 | 
			
		||||
_$SnAuthDeviceWithChallengeeCopyWith<_SnAuthDeviceWithChallengee> get copyWith => __$SnAuthDeviceWithChallengeeCopyWithImpl<_SnAuthDeviceWithChallengee>(this, _$identity);
 | 
			
		||||
 | 
			
		||||
@override
 | 
			
		||||
Map<String, dynamic> toJson() {
 | 
			
		||||
  return _$SnAuthDeviceWithChallengeeToJson(this, );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@override
 | 
			
		||||
bool operator ==(Object other) {
 | 
			
		||||
  return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAuthDeviceWithChallengee&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)&&(identical(other.deviceLabel, deviceLabel) || other.deviceLabel == deviceLabel)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.platform, platform) || other.platform == platform)&&const DeepCollectionEquality().equals(other._challenges, _challenges)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
@override
 | 
			
		||||
int get hashCode => Object.hash(runtimeType,id,deviceId,deviceName,deviceLabel,accountId,platform,const DeepCollectionEquality().hash(_challenges),isCurrent);
 | 
			
		||||
 | 
			
		||||
@override
 | 
			
		||||
String toString() {
 | 
			
		||||
  return 'SnAuthDeviceWithChallenge(id: $id, deviceId: $deviceId, deviceName: $deviceName, deviceLabel: $deviceLabel, accountId: $accountId, platform: $platform, challenges: $challenges, isCurrent: $isCurrent)';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
abstract mixin class _$SnAuthDeviceWithChallengeeCopyWith<$Res> implements $SnAuthDeviceWithChallengeCopyWith<$Res> {
 | 
			
		||||
  factory _$SnAuthDeviceWithChallengeeCopyWith(_SnAuthDeviceWithChallengee value, $Res Function(_SnAuthDeviceWithChallengee) _then) = __$SnAuthDeviceWithChallengeeCopyWithImpl;
 | 
			
		||||
@override @useResult
 | 
			
		||||
$Res call({
 | 
			
		||||
 String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, List<SnAuthChallenge> challenges, bool isCurrent
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
/// @nodoc
 | 
			
		||||
class __$SnAuthDeviceWithChallengeeCopyWithImpl<$Res>
 | 
			
		||||
    implements _$SnAuthDeviceWithChallengeeCopyWith<$Res> {
 | 
			
		||||
  __$SnAuthDeviceWithChallengeeCopyWithImpl(this._self, this._then);
 | 
			
		||||
 | 
			
		||||
  final _SnAuthDeviceWithChallengee _self;
 | 
			
		||||
  final $Res Function(_SnAuthDeviceWithChallengee) _then;
 | 
			
		||||
 | 
			
		||||
/// Create a copy of SnAuthDeviceWithChallenge
 | 
			
		||||
/// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? deviceId = null,Object? deviceName = null,Object? deviceLabel = freezed,Object? accountId = null,Object? platform = null,Object? challenges = null,Object? isCurrent = null,}) {
 | 
			
		||||
  return _then(_SnAuthDeviceWithChallengee(
 | 
			
		||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,deviceName: null == deviceName ? _self.deviceName : deviceName // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,deviceLabel: freezed == deviceLabel ? _self.deviceLabel : deviceLabel // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as int,challenges: null == challenges ? _self._challenges : challenges // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as List<SnAuthChallenge>,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as bool,
 | 
			
		||||
  ));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// dart format on
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
// GENERATED CODE - DO NOT MODIFY BY HAND
 | 
			
		||||
 | 
			
		||||
part of 'user.dart';
 | 
			
		||||
part of 'account.dart';
 | 
			
		||||
 | 
			
		||||
// **************************************************************************
 | 
			
		||||
// JsonSerializableGenerator
 | 
			
		||||
@@ -297,3 +297,54 @@ Map<String, dynamic> _$SnVerificationMarkToJson(_SnVerificationMark instance) =>
 | 
			
		||||
      'description': instance.description,
 | 
			
		||||
      'verified_by': instance.verifiedBy,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
_SnAuthDevice _$SnAuthDeviceFromJson(Map<String, dynamic> json) =>
 | 
			
		||||
    _SnAuthDevice(
 | 
			
		||||
      id: json['id'] as String,
 | 
			
		||||
      deviceId: json['device_id'] as String,
 | 
			
		||||
      deviceName: json['device_name'] as String,
 | 
			
		||||
      deviceLabel: json['device_label'] as String?,
 | 
			
		||||
      accountId: json['account_id'] as String,
 | 
			
		||||
      platform: (json['platform'] as num).toInt(),
 | 
			
		||||
      isCurrent: json['is_current'] as bool? ?? false,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
Map<String, dynamic> _$SnAuthDeviceToJson(_SnAuthDevice instance) =>
 | 
			
		||||
    <String, dynamic>{
 | 
			
		||||
      'id': instance.id,
 | 
			
		||||
      'device_id': instance.deviceId,
 | 
			
		||||
      'device_name': instance.deviceName,
 | 
			
		||||
      'device_label': instance.deviceLabel,
 | 
			
		||||
      'account_id': instance.accountId,
 | 
			
		||||
      'platform': instance.platform,
 | 
			
		||||
      'is_current': instance.isCurrent,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
_SnAuthDeviceWithChallengee _$SnAuthDeviceWithChallengeeFromJson(
 | 
			
		||||
  Map<String, dynamic> json,
 | 
			
		||||
) => _SnAuthDeviceWithChallengee(
 | 
			
		||||
  id: json['id'] as String,
 | 
			
		||||
  deviceId: json['device_id'] as String,
 | 
			
		||||
  deviceName: json['device_name'] as String,
 | 
			
		||||
  deviceLabel: json['device_label'] as String?,
 | 
			
		||||
  accountId: json['account_id'] as String,
 | 
			
		||||
  platform: (json['platform'] as num).toInt(),
 | 
			
		||||
  challenges:
 | 
			
		||||
      (json['challenges'] as List<dynamic>)
 | 
			
		||||
          .map((e) => SnAuthChallenge.fromJson(e as Map<String, dynamic>))
 | 
			
		||||
          .toList(),
 | 
			
		||||
  isCurrent: json['is_current'] as bool? ?? false,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
Map<String, dynamic> _$SnAuthDeviceWithChallengeeToJson(
 | 
			
		||||
  _SnAuthDeviceWithChallengee instance,
 | 
			
		||||
) => <String, dynamic>{
 | 
			
		||||
  'id': instance.id,
 | 
			
		||||
  'device_id': instance.deviceId,
 | 
			
		||||
  'device_name': instance.deviceName,
 | 
			
		||||
  'device_label': instance.deviceLabel,
 | 
			
		||||
  'account_id': instance.accountId,
 | 
			
		||||
  'platform': instance.platform,
 | 
			
		||||
  'challenges': instance.challenges.map((e) => e.toJson()).toList(),
 | 
			
		||||
  'is_current': instance.isCurrent,
 | 
			
		||||
};
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import 'package:freezed_annotation/freezed_annotation.dart';
 | 
			
		||||
import 'package:island/models/user.dart';
 | 
			
		||||
import 'package:island/models/account.dart';
 | 
			
		||||
 | 
			
		||||
part 'activity.freezed.dart';
 | 
			
		||||
part 'activity.g.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -19,14 +19,12 @@ sealed class SnAuthChallenge with _$SnAuthChallenge {
 | 
			
		||||
    required int stepRemain,
 | 
			
		||||
    required int stepTotal,
 | 
			
		||||
    required int failedAttempts,
 | 
			
		||||
    required int platform,
 | 
			
		||||
    required int type,
 | 
			
		||||
    required List<String> blacklistFactors,
 | 
			
		||||
    required List<dynamic> audiences,
 | 
			
		||||
    required List<dynamic> scopes,
 | 
			
		||||
    required String ipAddress,
 | 
			
		||||
    required String userAgent,
 | 
			
		||||
    required String deviceId,
 | 
			
		||||
    required String? nonce,
 | 
			
		||||
    required String? location,
 | 
			
		||||
    required String accountId,
 | 
			
		||||
@@ -76,22 +74,6 @@ sealed class SnAuthFactor with _$SnAuthFactor {
 | 
			
		||||
      _$SnAuthFactorFromJson(json);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@freezed
 | 
			
		||||
sealed class SnAuthDevice with _$SnAuthDevice {
 | 
			
		||||
  const factory SnAuthDevice({
 | 
			
		||||
    required dynamic label,
 | 
			
		||||
    required String userAgent,
 | 
			
		||||
    required String deviceId,
 | 
			
		||||
    required int platform,
 | 
			
		||||
    required List<SnAuthSession> sessions,
 | 
			
		||||
    // Not from backend, used for UI
 | 
			
		||||
    @Default(false) bool isCurrent,
 | 
			
		||||
  }) = _SnAuthDevice;
 | 
			
		||||
 | 
			
		||||
  factory SnAuthDevice.fromJson(Map<String, dynamic> json) =>
 | 
			
		||||
      _$SnAuthDeviceFromJson(json);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@freezed
 | 
			
		||||
sealed class SnAccountConnection with _$SnAccountConnection {
 | 
			
		||||
  const factory SnAccountConnection({
 | 
			
		||||
 
 | 
			
		||||
@@ -272,7 +272,7 @@ as String,
 | 
			
		||||
/// @nodoc
 | 
			
		||||
mixin _$SnAuthChallenge {
 | 
			
		||||
 | 
			
		||||
 String get id; DateTime get expiredAt; int get stepRemain; int get stepTotal; int get failedAttempts; int get platform; int get type; List<String> get blacklistFactors; List<dynamic> get audiences; List<dynamic> get scopes; String get ipAddress; String get userAgent; String get deviceId; String? get nonce; String? 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; String? get location; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
 | 
			
		||||
/// Create a copy of SnAuthChallenge
 | 
			
		||||
/// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
@@ -285,16 +285,16 @@ $SnAuthChallengeCopyWith<SnAuthChallenge> get copyWith => _$SnAuthChallengeCopyW
 | 
			
		||||
 | 
			
		||||
@override
 | 
			
		||||
bool operator ==(Object other) {
 | 
			
		||||
  return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAuthChallenge&&(identical(other.id, id) || other.id == id)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.stepRemain, stepRemain) || other.stepRemain == stepRemain)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&(identical(other.failedAttempts, failedAttempts) || other.failedAttempts == failedAttempts)&&(identical(other.platform, platform) || other.platform == platform)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.blacklistFactors, blacklistFactors)&&const DeepCollectionEquality().equals(other.audiences, audiences)&&const DeepCollectionEquality().equals(other.scopes, scopes)&&(identical(other.ipAddress, ipAddress) || other.ipAddress == ipAddress)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&(identical(other.location, location) || other.location == location)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
 | 
			
		||||
  return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAuthChallenge&&(identical(other.id, id) || other.id == id)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.stepRemain, stepRemain) || other.stepRemain == stepRemain)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&(identical(other.failedAttempts, failedAttempts) || other.failedAttempts == failedAttempts)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.blacklistFactors, blacklistFactors)&&const DeepCollectionEquality().equals(other.audiences, audiences)&&const DeepCollectionEquality().equals(other.scopes, scopes)&&(identical(other.ipAddress, ipAddress) || other.ipAddress == ipAddress)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&(identical(other.location, location) || other.location == location)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
@override
 | 
			
		||||
int get hashCode => Object.hashAll([runtimeType,id,expiredAt,stepRemain,stepTotal,failedAttempts,platform,type,const DeepCollectionEquality().hash(blacklistFactors),const DeepCollectionEquality().hash(audiences),const DeepCollectionEquality().hash(scopes),ipAddress,userAgent,deviceId,nonce,location,accountId,createdAt,updatedAt,deletedAt]);
 | 
			
		||||
int get hashCode => Object.hash(runtimeType,id,expiredAt,stepRemain,stepTotal,failedAttempts,type,const DeepCollectionEquality().hash(blacklistFactors),const DeepCollectionEquality().hash(audiences),const DeepCollectionEquality().hash(scopes),ipAddress,userAgent,nonce,location,accountId,createdAt,updatedAt,deletedAt);
 | 
			
		||||
 | 
			
		||||
@override
 | 
			
		||||
String toString() {
 | 
			
		||||
  return 'SnAuthChallenge(id: $id, expiredAt: $expiredAt, stepRemain: $stepRemain, stepTotal: $stepTotal, failedAttempts: $failedAttempts, platform: $platform, type: $type, blacklistFactors: $blacklistFactors, audiences: $audiences, scopes: $scopes, ipAddress: $ipAddress, userAgent: $userAgent, deviceId: $deviceId, nonce: $nonce, location: $location, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
 | 
			
		||||
  return 'SnAuthChallenge(id: $id, expiredAt: $expiredAt, stepRemain: $stepRemain, stepTotal: $stepTotal, failedAttempts: $failedAttempts, type: $type, blacklistFactors: $blacklistFactors, audiences: $audiences, scopes: $scopes, ipAddress: $ipAddress, userAgent: $userAgent, nonce: $nonce, location: $location, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -305,7 +305,7 @@ abstract mixin class $SnAuthChallengeCopyWith<$Res>  {
 | 
			
		||||
  factory $SnAuthChallengeCopyWith(SnAuthChallenge value, $Res Function(SnAuthChallenge) _then) = _$SnAuthChallengeCopyWithImpl;
 | 
			
		||||
@useResult
 | 
			
		||||
$Res call({
 | 
			
		||||
 String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int platform, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String deviceId, String? nonce, String? 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, String? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -322,21 +322,19 @@ class _$SnAuthChallengeCopyWithImpl<$Res>
 | 
			
		||||
 | 
			
		||||
/// Create a copy of SnAuthChallenge
 | 
			
		||||
/// 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? platform = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? deviceId = 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 = 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,}) {
 | 
			
		||||
  return _then(_self.copyWith(
 | 
			
		||||
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 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,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as int,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as int,blacklistFactors: null == blacklistFactors ? _self.blacklistFactors : blacklistFactors // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as List<String>,audiences: null == audiences ? _self.audiences : audiences // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as List<dynamic>,scopes: null == scopes ? _self.scopes : scopes // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as List<dynamic>,ipAddress: null == ipAddress ? _self.ipAddress : ipAddress // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String?,location: freezed == location ? _self.location : location // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
@@ -425,10 +423,10 @@ return $default(_that);case _:
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
 | 
			
		||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  DateTime expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int platform,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String deviceId,  String? nonce,  String? 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,  String? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
			
		||||
switch (_that) {
 | 
			
		||||
case _SnAuthChallenge() when $default != null:
 | 
			
		||||
return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.platform,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.deviceId,_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 _:
 | 
			
		||||
  return orElse();
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -446,10 +444,10 @@ 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 platform,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String deviceId,  String? nonce,  String? 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,  String? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this;
 | 
			
		||||
switch (_that) {
 | 
			
		||||
case _SnAuthChallenge():
 | 
			
		||||
return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.platform,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.deviceId,_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);}
 | 
			
		||||
}
 | 
			
		||||
/// A variant of `when` that fallback to returning `null`
 | 
			
		||||
///
 | 
			
		||||
@@ -463,10 +461,10 @@ 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 platform,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String deviceId,  String? nonce,  String? 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,  String? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this;
 | 
			
		||||
switch (_that) {
 | 
			
		||||
case _SnAuthChallenge() when $default != null:
 | 
			
		||||
return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.platform,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.deviceId,_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 _:
 | 
			
		||||
  return null;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -478,7 +476,7 @@ return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.
 | 
			
		||||
@JsonSerializable()
 | 
			
		||||
 | 
			
		||||
class _SnAuthChallenge implements SnAuthChallenge {
 | 
			
		||||
  const _SnAuthChallenge({required this.id, required this.expiredAt, required this.stepRemain, required this.stepTotal, required this.failedAttempts, required this.platform, required this.type, required final  List<String> blacklistFactors, required final  List<dynamic> audiences, required final  List<dynamic> scopes, required this.ipAddress, required this.userAgent, required this.deviceId, required this.nonce, required this.location, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}): _blacklistFactors = blacklistFactors,_audiences = audiences,_scopes = scopes;
 | 
			
		||||
  const _SnAuthChallenge({required this.id, required this.expiredAt, required this.stepRemain, required this.stepTotal, required this.failedAttempts, required this.type, required final  List<String> blacklistFactors, required final  List<dynamic> audiences, required final  List<dynamic> scopes, required this.ipAddress, required this.userAgent, required this.nonce, required this.location, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}): _blacklistFactors = blacklistFactors,_audiences = audiences,_scopes = scopes;
 | 
			
		||||
  factory _SnAuthChallenge.fromJson(Map<String, dynamic> json) => _$SnAuthChallengeFromJson(json);
 | 
			
		||||
 | 
			
		||||
@override final  String id;
 | 
			
		||||
@@ -486,7 +484,6 @@ class _SnAuthChallenge implements SnAuthChallenge {
 | 
			
		||||
@override final  int stepRemain;
 | 
			
		||||
@override final  int stepTotal;
 | 
			
		||||
@override final  int failedAttempts;
 | 
			
		||||
@override final  int platform;
 | 
			
		||||
@override final  int type;
 | 
			
		||||
 final  List<String> _blacklistFactors;
 | 
			
		||||
@override List<String> get blacklistFactors {
 | 
			
		||||
@@ -511,7 +508,6 @@ class _SnAuthChallenge implements SnAuthChallenge {
 | 
			
		||||
 | 
			
		||||
@override final  String ipAddress;
 | 
			
		||||
@override final  String userAgent;
 | 
			
		||||
@override final  String deviceId;
 | 
			
		||||
@override final  String? nonce;
 | 
			
		||||
@override final  String? location;
 | 
			
		||||
@override final  String accountId;
 | 
			
		||||
@@ -532,16 +528,16 @@ Map<String, dynamic> toJson() {
 | 
			
		||||
 | 
			
		||||
@override
 | 
			
		||||
bool operator ==(Object other) {
 | 
			
		||||
  return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAuthChallenge&&(identical(other.id, id) || other.id == id)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.stepRemain, stepRemain) || other.stepRemain == stepRemain)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&(identical(other.failedAttempts, failedAttempts) || other.failedAttempts == failedAttempts)&&(identical(other.platform, platform) || other.platform == platform)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._blacklistFactors, _blacklistFactors)&&const DeepCollectionEquality().equals(other._audiences, _audiences)&&const DeepCollectionEquality().equals(other._scopes, _scopes)&&(identical(other.ipAddress, ipAddress) || other.ipAddress == ipAddress)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&(identical(other.location, location) || other.location == location)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
 | 
			
		||||
  return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAuthChallenge&&(identical(other.id, id) || other.id == id)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.stepRemain, stepRemain) || other.stepRemain == stepRemain)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&(identical(other.failedAttempts, failedAttempts) || other.failedAttempts == failedAttempts)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._blacklistFactors, _blacklistFactors)&&const DeepCollectionEquality().equals(other._audiences, _audiences)&&const DeepCollectionEquality().equals(other._scopes, _scopes)&&(identical(other.ipAddress, ipAddress) || other.ipAddress == ipAddress)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&(identical(other.location, location) || other.location == location)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
@override
 | 
			
		||||
int get hashCode => Object.hashAll([runtimeType,id,expiredAt,stepRemain,stepTotal,failedAttempts,platform,type,const DeepCollectionEquality().hash(_blacklistFactors),const DeepCollectionEquality().hash(_audiences),const DeepCollectionEquality().hash(_scopes),ipAddress,userAgent,deviceId,nonce,location,accountId,createdAt,updatedAt,deletedAt]);
 | 
			
		||||
int get hashCode => Object.hash(runtimeType,id,expiredAt,stepRemain,stepTotal,failedAttempts,type,const DeepCollectionEquality().hash(_blacklistFactors),const DeepCollectionEquality().hash(_audiences),const DeepCollectionEquality().hash(_scopes),ipAddress,userAgent,nonce,location,accountId,createdAt,updatedAt,deletedAt);
 | 
			
		||||
 | 
			
		||||
@override
 | 
			
		||||
String toString() {
 | 
			
		||||
  return 'SnAuthChallenge(id: $id, expiredAt: $expiredAt, stepRemain: $stepRemain, stepTotal: $stepTotal, failedAttempts: $failedAttempts, platform: $platform, type: $type, blacklistFactors: $blacklistFactors, audiences: $audiences, scopes: $scopes, ipAddress: $ipAddress, userAgent: $userAgent, deviceId: $deviceId, nonce: $nonce, location: $location, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
 | 
			
		||||
  return 'SnAuthChallenge(id: $id, expiredAt: $expiredAt, stepRemain: $stepRemain, stepTotal: $stepTotal, failedAttempts: $failedAttempts, type: $type, blacklistFactors: $blacklistFactors, audiences: $audiences, scopes: $scopes, ipAddress: $ipAddress, userAgent: $userAgent, nonce: $nonce, location: $location, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -552,7 +548,7 @@ abstract mixin class _$SnAuthChallengeCopyWith<$Res> implements $SnAuthChallenge
 | 
			
		||||
  factory _$SnAuthChallengeCopyWith(_SnAuthChallenge value, $Res Function(_SnAuthChallenge) _then) = __$SnAuthChallengeCopyWithImpl;
 | 
			
		||||
@override @useResult
 | 
			
		||||
$Res call({
 | 
			
		||||
 String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int platform, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String deviceId, String? nonce, String? 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, String? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -569,21 +565,19 @@ class __$SnAuthChallengeCopyWithImpl<$Res>
 | 
			
		||||
 | 
			
		||||
/// Create a copy of SnAuthChallenge
 | 
			
		||||
/// 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? platform = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? deviceId = 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 = 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,}) {
 | 
			
		||||
  return _then(_SnAuthChallenge(
 | 
			
		||||
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 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,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as int,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as int,blacklistFactors: null == blacklistFactors ? _self._blacklistFactors : blacklistFactors // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as List<String>,audiences: null == audiences ? _self._audiences : audiences // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as List<dynamic>,scopes: null == scopes ? _self._scopes : scopes // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as List<dynamic>,ipAddress: null == ipAddress ? _self.ipAddress : ipAddress // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String?,location: freezed == location ? _self.location : location // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
@@ -1189,286 +1183,6 @@ as Map<String, dynamic>?,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
mixin _$SnAuthDevice {
 | 
			
		||||
 | 
			
		||||
 dynamic get label; String get userAgent; String get deviceId; int get platform; List<SnAuthSession> get sessions;// Not from backend, used for UI
 | 
			
		||||
 bool get isCurrent;
 | 
			
		||||
/// Create a copy of SnAuthDevice
 | 
			
		||||
/// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
@pragma('vm:prefer-inline')
 | 
			
		||||
$SnAuthDeviceCopyWith<SnAuthDevice> get copyWith => _$SnAuthDeviceCopyWithImpl<SnAuthDevice>(this as SnAuthDevice, _$identity);
 | 
			
		||||
 | 
			
		||||
  /// Serializes this SnAuthDevice to a JSON map.
 | 
			
		||||
  Map<String, dynamic> toJson();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@override
 | 
			
		||||
bool operator ==(Object other) {
 | 
			
		||||
  return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAuthDevice&&const DeepCollectionEquality().equals(other.label, label)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.platform, platform) || other.platform == platform)&&const DeepCollectionEquality().equals(other.sessions, sessions)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
@override
 | 
			
		||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(label),userAgent,deviceId,platform,const DeepCollectionEquality().hash(sessions),isCurrent);
 | 
			
		||||
 | 
			
		||||
@override
 | 
			
		||||
String toString() {
 | 
			
		||||
  return 'SnAuthDevice(label: $label, userAgent: $userAgent, deviceId: $deviceId, platform: $platform, sessions: $sessions, isCurrent: $isCurrent)';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
abstract mixin class $SnAuthDeviceCopyWith<$Res>  {
 | 
			
		||||
  factory $SnAuthDeviceCopyWith(SnAuthDevice value, $Res Function(SnAuthDevice) _then) = _$SnAuthDeviceCopyWithImpl;
 | 
			
		||||
@useResult
 | 
			
		||||
$Res call({
 | 
			
		||||
 dynamic label, String userAgent, String deviceId, int platform, List<SnAuthSession> sessions, bool isCurrent
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
/// @nodoc
 | 
			
		||||
class _$SnAuthDeviceCopyWithImpl<$Res>
 | 
			
		||||
    implements $SnAuthDeviceCopyWith<$Res> {
 | 
			
		||||
  _$SnAuthDeviceCopyWithImpl(this._self, this._then);
 | 
			
		||||
 | 
			
		||||
  final SnAuthDevice _self;
 | 
			
		||||
  final $Res Function(SnAuthDevice) _then;
 | 
			
		||||
 | 
			
		||||
/// Create a copy of SnAuthDevice
 | 
			
		||||
/// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
@pragma('vm:prefer-inline') @override $Res call({Object? label = freezed,Object? userAgent = null,Object? deviceId = null,Object? platform = null,Object? sessions = null,Object? isCurrent = null,}) {
 | 
			
		||||
  return _then(_self.copyWith(
 | 
			
		||||
label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as dynamic,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as int,sessions: null == sessions ? _self.sessions : sessions // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as List<SnAuthSession>,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as bool,
 | 
			
		||||
  ));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/// Adds pattern-matching-related methods to [SnAuthDevice].
 | 
			
		||||
extension SnAuthDevicePatterns on SnAuthDevice {
 | 
			
		||||
/// A variant of `map` that fallback to returning `orElse`.
 | 
			
		||||
///
 | 
			
		||||
/// It is equivalent to doing:
 | 
			
		||||
/// ```dart
 | 
			
		||||
/// switch (sealedClass) {
 | 
			
		||||
///   case final Subclass value:
 | 
			
		||||
///     return ...;
 | 
			
		||||
///   case _:
 | 
			
		||||
///     return orElse();
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
 | 
			
		||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnAuthDevice value)?  $default,{required TResult orElse(),}){
 | 
			
		||||
final _that = this;
 | 
			
		||||
switch (_that) {
 | 
			
		||||
case _SnAuthDevice() when $default != null:
 | 
			
		||||
return $default(_that);case _:
 | 
			
		||||
  return orElse();
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
/// A `switch`-like method, using callbacks.
 | 
			
		||||
///
 | 
			
		||||
/// Callbacks receives the raw object, upcasted.
 | 
			
		||||
/// It is equivalent to doing:
 | 
			
		||||
/// ```dart
 | 
			
		||||
/// switch (sealedClass) {
 | 
			
		||||
///   case final Subclass value:
 | 
			
		||||
///     return ...;
 | 
			
		||||
///   case final Subclass2 value:
 | 
			
		||||
///     return ...;
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
 | 
			
		||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnAuthDevice value)  $default,){
 | 
			
		||||
final _that = this;
 | 
			
		||||
switch (_that) {
 | 
			
		||||
case _SnAuthDevice():
 | 
			
		||||
return $default(_that);}
 | 
			
		||||
}
 | 
			
		||||
/// A variant of `map` that fallback to returning `null`.
 | 
			
		||||
///
 | 
			
		||||
/// It is equivalent to doing:
 | 
			
		||||
/// ```dart
 | 
			
		||||
/// switch (sealedClass) {
 | 
			
		||||
///   case final Subclass value:
 | 
			
		||||
///     return ...;
 | 
			
		||||
///   case _:
 | 
			
		||||
///     return null;
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
 | 
			
		||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnAuthDevice value)?  $default,){
 | 
			
		||||
final _that = this;
 | 
			
		||||
switch (_that) {
 | 
			
		||||
case _SnAuthDevice() when $default != null:
 | 
			
		||||
return $default(_that);case _:
 | 
			
		||||
  return null;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
/// A variant of `when` that fallback to an `orElse` callback.
 | 
			
		||||
///
 | 
			
		||||
/// It is equivalent to doing:
 | 
			
		||||
/// ```dart
 | 
			
		||||
/// switch (sealedClass) {
 | 
			
		||||
///   case Subclass(:final field):
 | 
			
		||||
///     return ...;
 | 
			
		||||
///   case _:
 | 
			
		||||
///     return orElse();
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
 | 
			
		||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( dynamic label,  String userAgent,  String deviceId,  int platform,  List<SnAuthSession> sessions,  bool isCurrent)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
			
		||||
switch (_that) {
 | 
			
		||||
case _SnAuthDevice() when $default != null:
 | 
			
		||||
return $default(_that.label,_that.userAgent,_that.deviceId,_that.platform,_that.sessions,_that.isCurrent);case _:
 | 
			
		||||
  return orElse();
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
/// A `switch`-like method, using callbacks.
 | 
			
		||||
///
 | 
			
		||||
/// As opposed to `map`, this offers destructuring.
 | 
			
		||||
/// It is equivalent to doing:
 | 
			
		||||
/// ```dart
 | 
			
		||||
/// switch (sealedClass) {
 | 
			
		||||
///   case Subclass(:final field):
 | 
			
		||||
///     return ...;
 | 
			
		||||
///   case Subclass2(:final field2):
 | 
			
		||||
///     return ...;
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
 | 
			
		||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( dynamic label,  String userAgent,  String deviceId,  int platform,  List<SnAuthSession> sessions,  bool isCurrent)  $default,) {final _that = this;
 | 
			
		||||
switch (_that) {
 | 
			
		||||
case _SnAuthDevice():
 | 
			
		||||
return $default(_that.label,_that.userAgent,_that.deviceId,_that.platform,_that.sessions,_that.isCurrent);}
 | 
			
		||||
}
 | 
			
		||||
/// A variant of `when` that fallback to returning `null`
 | 
			
		||||
///
 | 
			
		||||
/// It is equivalent to doing:
 | 
			
		||||
/// ```dart
 | 
			
		||||
/// switch (sealedClass) {
 | 
			
		||||
///   case Subclass(:final field):
 | 
			
		||||
///     return ...;
 | 
			
		||||
///   case _:
 | 
			
		||||
///     return null;
 | 
			
		||||
/// }
 | 
			
		||||
/// ```
 | 
			
		||||
 | 
			
		||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( dynamic label,  String userAgent,  String deviceId,  int platform,  List<SnAuthSession> sessions,  bool isCurrent)?  $default,) {final _that = this;
 | 
			
		||||
switch (_that) {
 | 
			
		||||
case _SnAuthDevice() when $default != null:
 | 
			
		||||
return $default(_that.label,_that.userAgent,_that.deviceId,_that.platform,_that.sessions,_that.isCurrent);case _:
 | 
			
		||||
  return null;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
@JsonSerializable()
 | 
			
		||||
 | 
			
		||||
class _SnAuthDevice implements SnAuthDevice {
 | 
			
		||||
  const _SnAuthDevice({required this.label, required this.userAgent, required this.deviceId, required this.platform, required final  List<SnAuthSession> sessions, this.isCurrent = false}): _sessions = sessions;
 | 
			
		||||
  factory _SnAuthDevice.fromJson(Map<String, dynamic> json) => _$SnAuthDeviceFromJson(json);
 | 
			
		||||
 | 
			
		||||
@override final  dynamic label;
 | 
			
		||||
@override final  String userAgent;
 | 
			
		||||
@override final  String deviceId;
 | 
			
		||||
@override final  int platform;
 | 
			
		||||
 final  List<SnAuthSession> _sessions;
 | 
			
		||||
@override List<SnAuthSession> get sessions {
 | 
			
		||||
  if (_sessions is EqualUnmodifiableListView) return _sessions;
 | 
			
		||||
  // ignore: implicit_dynamic_type
 | 
			
		||||
  return EqualUnmodifiableListView(_sessions);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Not from backend, used for UI
 | 
			
		||||
@override@JsonKey() final  bool isCurrent;
 | 
			
		||||
 | 
			
		||||
/// Create a copy of SnAuthDevice
 | 
			
		||||
/// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
@pragma('vm:prefer-inline')
 | 
			
		||||
_$SnAuthDeviceCopyWith<_SnAuthDevice> get copyWith => __$SnAuthDeviceCopyWithImpl<_SnAuthDevice>(this, _$identity);
 | 
			
		||||
 | 
			
		||||
@override
 | 
			
		||||
Map<String, dynamic> toJson() {
 | 
			
		||||
  return _$SnAuthDeviceToJson(this, );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@override
 | 
			
		||||
bool operator ==(Object other) {
 | 
			
		||||
  return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAuthDevice&&const DeepCollectionEquality().equals(other.label, label)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.platform, platform) || other.platform == platform)&&const DeepCollectionEquality().equals(other._sessions, _sessions)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
@override
 | 
			
		||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(label),userAgent,deviceId,platform,const DeepCollectionEquality().hash(_sessions),isCurrent);
 | 
			
		||||
 | 
			
		||||
@override
 | 
			
		||||
String toString() {
 | 
			
		||||
  return 'SnAuthDevice(label: $label, userAgent: $userAgent, deviceId: $deviceId, platform: $platform, sessions: $sessions, isCurrent: $isCurrent)';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
abstract mixin class _$SnAuthDeviceCopyWith<$Res> implements $SnAuthDeviceCopyWith<$Res> {
 | 
			
		||||
  factory _$SnAuthDeviceCopyWith(_SnAuthDevice value, $Res Function(_SnAuthDevice) _then) = __$SnAuthDeviceCopyWithImpl;
 | 
			
		||||
@override @useResult
 | 
			
		||||
$Res call({
 | 
			
		||||
 dynamic label, String userAgent, String deviceId, int platform, List<SnAuthSession> sessions, bool isCurrent
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
/// @nodoc
 | 
			
		||||
class __$SnAuthDeviceCopyWithImpl<$Res>
 | 
			
		||||
    implements _$SnAuthDeviceCopyWith<$Res> {
 | 
			
		||||
  __$SnAuthDeviceCopyWithImpl(this._self, this._then);
 | 
			
		||||
 | 
			
		||||
  final _SnAuthDevice _self;
 | 
			
		||||
  final $Res Function(_SnAuthDevice) _then;
 | 
			
		||||
 | 
			
		||||
/// Create a copy of SnAuthDevice
 | 
			
		||||
/// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
@override @pragma('vm:prefer-inline') $Res call({Object? label = freezed,Object? userAgent = null,Object? deviceId = null,Object? platform = null,Object? sessions = null,Object? isCurrent = null,}) {
 | 
			
		||||
  return _then(_SnAuthDevice(
 | 
			
		||||
label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as dynamic,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as int,sessions: null == sessions ? _self._sessions : sessions // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as List<SnAuthSession>,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
as bool,
 | 
			
		||||
  ));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
mixin _$SnAccountConnection {
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,6 @@ _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) =>
 | 
			
		||||
      stepRemain: (json['step_remain'] as num).toInt(),
 | 
			
		||||
      stepTotal: (json['step_total'] as num).toInt(),
 | 
			
		||||
      failedAttempts: (json['failed_attempts'] as num).toInt(),
 | 
			
		||||
      platform: (json['platform'] as num).toInt(),
 | 
			
		||||
      type: (json['type'] as num).toInt(),
 | 
			
		||||
      blacklistFactors:
 | 
			
		||||
          (json['blacklist_factors'] as List<dynamic>)
 | 
			
		||||
@@ -30,7 +29,6 @@ _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) =>
 | 
			
		||||
      scopes: json['scopes'] as List<dynamic>,
 | 
			
		||||
      ipAddress: json['ip_address'] as String,
 | 
			
		||||
      userAgent: json['user_agent'] as String,
 | 
			
		||||
      deviceId: json['device_id'] as String,
 | 
			
		||||
      nonce: json['nonce'] as String?,
 | 
			
		||||
      location: json['location'] as String?,
 | 
			
		||||
      accountId: json['account_id'] as String,
 | 
			
		||||
@@ -49,14 +47,12 @@ Map<String, dynamic> _$SnAuthChallengeToJson(_SnAuthChallenge instance) =>
 | 
			
		||||
      'step_remain': instance.stepRemain,
 | 
			
		||||
      'step_total': instance.stepTotal,
 | 
			
		||||
      'failed_attempts': instance.failedAttempts,
 | 
			
		||||
      'platform': instance.platform,
 | 
			
		||||
      'type': instance.type,
 | 
			
		||||
      'blacklist_factors': instance.blacklistFactors,
 | 
			
		||||
      'audiences': instance.audiences,
 | 
			
		||||
      'scopes': instance.scopes,
 | 
			
		||||
      'ip_address': instance.ipAddress,
 | 
			
		||||
      'user_agent': instance.userAgent,
 | 
			
		||||
      'device_id': instance.deviceId,
 | 
			
		||||
      'nonce': instance.nonce,
 | 
			
		||||
      'location': instance.location,
 | 
			
		||||
      'account_id': instance.accountId,
 | 
			
		||||
@@ -133,29 +129,6 @@ Map<String, dynamic> _$SnAuthFactorToJson(_SnAuthFactor instance) =>
 | 
			
		||||
      'created_response': instance.createdResponse,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
_SnAuthDevice _$SnAuthDeviceFromJson(Map<String, dynamic> json) =>
 | 
			
		||||
    _SnAuthDevice(
 | 
			
		||||
      label: json['label'],
 | 
			
		||||
      userAgent: json['user_agent'] as String,
 | 
			
		||||
      deviceId: json['device_id'] as String,
 | 
			
		||||
      platform: (json['platform'] as num).toInt(),
 | 
			
		||||
      sessions:
 | 
			
		||||
          (json['sessions'] as List<dynamic>)
 | 
			
		||||
              .map((e) => SnAuthSession.fromJson(e as Map<String, dynamic>))
 | 
			
		||||
              .toList(),
 | 
			
		||||
      isCurrent: json['is_current'] as bool? ?? false,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
Map<String, dynamic> _$SnAuthDeviceToJson(_SnAuthDevice instance) =>
 | 
			
		||||
    <String, dynamic>{
 | 
			
		||||
      'label': instance.label,
 | 
			
		||||
      'user_agent': instance.userAgent,
 | 
			
		||||
      'device_id': instance.deviceId,
 | 
			
		||||
      'platform': instance.platform,
 | 
			
		||||
      'sessions': instance.sessions.map((e) => e.toJson()).toList(),
 | 
			
		||||
      'is_current': instance.isCurrent,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
_SnAccountConnection _$SnAccountConnectionFromJson(Map<String, dynamic> json) =>
 | 
			
		||||
    _SnAccountConnection(
 | 
			
		||||
      id: json['id'] as String,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import 'package:freezed_annotation/freezed_annotation.dart';
 | 
			
		||||
import 'package:island/models/file.dart';
 | 
			
		||||
import 'package:island/models/realm.dart';
 | 
			
		||||
import 'package:island/models/user.dart';
 | 
			
		||||
import 'package:island/models/account.dart';
 | 
			
		||||
 | 
			
		||||
part 'chat.freezed.dart';
 | 
			
		||||
part 'chat.g.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import 'package:freezed_annotation/freezed_annotation.dart';
 | 
			
		||||
import 'package:island/models/file.dart';
 | 
			
		||||
import 'package:island/models/user.dart';
 | 
			
		||||
import 'package:island/models/account.dart';
 | 
			
		||||
 | 
			
		||||
part 'custom_app.freezed.dart';
 | 
			
		||||
part 'custom_app.g.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ part 'poll.g.dart';
 | 
			
		||||
sealed class SnPollWithStats with _$SnPollWithStats {
 | 
			
		||||
  const factory SnPollWithStats({
 | 
			
		||||
    required Map<String, dynamic>? userAnswer,
 | 
			
		||||
    required Map<String, dynamic> stats,
 | 
			
		||||
    @Default({}) Map<String, dynamic> stats,
 | 
			
		||||
    required String id,
 | 
			
		||||
    required List<SnPollQuestion> questions,
 | 
			
		||||
    String? title,
 | 
			
		||||
 
 | 
			
		||||
@@ -213,7 +213,7 @@ return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.titl
 | 
			
		||||
@JsonSerializable()
 | 
			
		||||
 | 
			
		||||
class _SnPollWithStats implements SnPollWithStats {
 | 
			
		||||
  const _SnPollWithStats({required final  Map<String, dynamic>? userAnswer, required final  Map<String, dynamic> stats, required this.id, required final  List<SnPollQuestion> questions, this.title, this.description, this.endedAt, required this.publisherId, required this.createdAt, required this.updatedAt, this.deletedAt}): _userAnswer = userAnswer,_stats = stats,_questions = questions;
 | 
			
		||||
  const _SnPollWithStats({required final  Map<String, dynamic>? userAnswer, final  Map<String, dynamic> stats = const {}, required this.id, required final  List<SnPollQuestion> questions, this.title, this.description, this.endedAt, required this.publisherId, required this.createdAt, required this.updatedAt, this.deletedAt}): _userAnswer = userAnswer,_stats = stats,_questions = questions;
 | 
			
		||||
  factory _SnPollWithStats.fromJson(Map<String, dynamic> json) => _$SnPollWithStatsFromJson(json);
 | 
			
		||||
 | 
			
		||||
 final  Map<String, dynamic>? _userAnswer;
 | 
			
		||||
@@ -226,7 +226,7 @@ class _SnPollWithStats implements SnPollWithStats {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 final  Map<String, dynamic> _stats;
 | 
			
		||||
@override Map<String, dynamic> get stats {
 | 
			
		||||
@override@JsonKey() Map<String, dynamic> get stats {
 | 
			
		||||
  if (_stats is EqualUnmodifiableMapView) return _stats;
 | 
			
		||||
  // ignore: implicit_dynamic_type
 | 
			
		||||
  return EqualUnmodifiableMapView(_stats);
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ part of 'poll.dart';
 | 
			
		||||
_SnPollWithStats _$SnPollWithStatsFromJson(Map<String, dynamic> json) =>
 | 
			
		||||
    _SnPollWithStats(
 | 
			
		||||
      userAnswer: json['user_answer'] as Map<String, dynamic>?,
 | 
			
		||||
      stats: json['stats'] as Map<String, dynamic>,
 | 
			
		||||
      stats: json['stats'] as Map<String, dynamic>? ?? const {},
 | 
			
		||||
      id: json['id'] as String,
 | 
			
		||||
      questions:
 | 
			
		||||
          (json['questions'] as List<dynamic>)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import 'package:freezed_annotation/freezed_annotation.dart';
 | 
			
		||||
import 'package:island/models/file.dart';
 | 
			
		||||
import 'package:island/models/user.dart';
 | 
			
		||||
import 'package:island/models/account.dart';
 | 
			
		||||
 | 
			
		||||
part 'publisher.freezed.dart';
 | 
			
		||||
part 'publisher.g.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import 'package:freezed_annotation/freezed_annotation.dart';
 | 
			
		||||
import 'package:island/models/file.dart';
 | 
			
		||||
import 'package:island/models/user.dart';
 | 
			
		||||
import 'package:island/models/account.dart';
 | 
			
		||||
 | 
			
		||||
part 'realm.freezed.dart';
 | 
			
		||||
part 'realm.g.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import 'package:freezed_annotation/freezed_annotation.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:island/models/user.dart';
 | 
			
		||||
import 'package:island/models/account.dart';
 | 
			
		||||
 | 
			
		||||
part 'relationship.freezed.dart';
 | 
			
		||||
part 'relationship.g.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import 'package:freezed_annotation/freezed_annotation.dart';
 | 
			
		||||
import 'package:island/models/user.dart';
 | 
			
		||||
import 'package:island/models/account.dart';
 | 
			
		||||
 | 
			
		||||
part 'wallet.freezed.dart';
 | 
			
		||||
part 'wallet.g.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import 'dart:developer';
 | 
			
		||||
import 'package:firebase_analytics/firebase_analytics.dart';
 | 
			
		||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:island/models/user.dart';
 | 
			
		||||
import 'package:island/models/account.dart';
 | 
			
		||||
import 'package:island/pods/config.dart';
 | 
			
		||||
import 'package:island/pods/network.dart';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import 'package:firebase_analytics/firebase_analytics.dart';
 | 
			
		||||
import 'package:firebase_analytics/observer.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,14 @@ import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:island/pods/network.dart';
 | 
			
		||||
import 'package:island/pods/userinfo.dart';
 | 
			
		||||
import 'package:island/screens/notification.dart';
 | 
			
		||||
import 'package:island/services/responsive.dart';
 | 
			
		||||
import 'package:island/widgets/account/account_name.dart';
 | 
			
		||||
import 'package:island/widgets/account/status.dart';
 | 
			
		||||
import 'package:island/widgets/account/leveling_progress.dart';
 | 
			
		||||
import 'package:island/widgets/alert.dart';
 | 
			
		||||
import 'package:island/widgets/app_scaffold.dart';
 | 
			
		||||
import 'package:island/widgets/content/cloud_files.dart';
 | 
			
		||||
import 'package:island/widgets/debug_sheet.dart';
 | 
			
		||||
@@ -303,7 +305,12 @@ class AccountScreen extends HookConsumerWidget {
 | 
			
		||||
              trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
              contentPadding: EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
              title: Text('logout').tr(),
 | 
			
		||||
              onTap: () {
 | 
			
		||||
              onTap: () async {
 | 
			
		||||
                final apiClient = ref.watch(apiClientProvider);
 | 
			
		||||
                showLoadingModal(context);
 | 
			
		||||
                await apiClient.delete('/id/accounts/me/sessions/current');
 | 
			
		||||
                if (!context.mounted) return;
 | 
			
		||||
                hideLoadingModal(context);
 | 
			
		||||
                final userNotifier = ref.read(userInfoProvider.notifier);
 | 
			
		||||
                userNotifier.logOut();
 | 
			
		||||
              },
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:island/models/auth.dart';
 | 
			
		||||
import 'package:island/models/user.dart';
 | 
			
		||||
import 'package:island/models/account.dart';
 | 
			
		||||
import 'package:island/pods/network.dart';
 | 
			
		||||
import 'package:island/pods/userinfo.dart';
 | 
			
		||||
import 'package:island/screens/account/me/settings_auth_factors.dart';
 | 
			
		||||
@@ -15,7 +15,7 @@ import 'package:island/screens/account/me/settings_contacts.dart';
 | 
			
		||||
import 'package:island/screens/auth/captcha.dart';
 | 
			
		||||
import 'package:island/screens/auth/login.dart';
 | 
			
		||||
import 'package:island/services/responsive.dart';
 | 
			
		||||
import 'package:island/widgets/account/account_session_sheet.dart';
 | 
			
		||||
import 'package:island/widgets/account/account_devices.dart';
 | 
			
		||||
import 'package:island/widgets/alert.dart';
 | 
			
		||||
import 'package:island/widgets/app_scaffold.dart';
 | 
			
		||||
import 'package:island/widgets/response.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -166,7 +166,7 @@ class AccountConnectionNewSheet extends HookConsumerWidget {
 | 
			
		||||
              webAuthenticationOptions: WebAuthenticationOptions(
 | 
			
		||||
                clientId: 'dev.solsynth.solarpass',
 | 
			
		||||
                redirectUri: Uri.parse(
 | 
			
		||||
                  'https://nt.solian.app/auth/callback/apple',
 | 
			
		||||
                  'https://id.solian.app/auth/callback/apple',
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            );
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:island/models/user.dart';
 | 
			
		||||
import 'package:island/models/account.dart';
 | 
			
		||||
import 'package:island/pods/network.dart';
 | 
			
		||||
import 'package:island/widgets/alert.dart';
 | 
			
		||||
import 'package:island/widgets/content/sheet.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:image_picker/image_picker.dart';
 | 
			
		||||
import 'package:island/models/file.dart';
 | 
			
		||||
import 'package:island/models/user.dart';
 | 
			
		||||
import 'package:island/models/account.dart';
 | 
			
		||||
import 'package:island/pods/config.dart';
 | 
			
		||||
import 'package:island/pods/network.dart';
 | 
			
		||||
import 'package:island/pods/userinfo.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:island/models/chat.dart';
 | 
			
		||||
import 'package:island/models/relationship.dart';
 | 
			
		||||
import 'package:island/models/user.dart';
 | 
			
		||||
import 'package:island/models/account.dart';
 | 
			
		||||
import 'package:island/pods/config.dart';
 | 
			
		||||
import 'package:island/pods/event_calendar.dart';
 | 
			
		||||
import 'package:island/pods/network.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,22 @@ final Map<int, (String, String, IconData)> kFactorTypes = {
 | 
			
		||||
  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 {
 | 
			
		||||
  const LoginScreen({super.key});
 | 
			
		||||
 | 
			
		||||
@@ -198,28 +214,6 @@ class _LoginCheckScreen extends HookConsumerWidget {
 | 
			
		||||
        wsNotifier.connect();
 | 
			
		||||
        if (context.mounted) Navigator.pop(context, true);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // Update the sessions' device name is available
 | 
			
		||||
      if (!kIsWeb) {
 | 
			
		||||
        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;
 | 
			
		||||
        }
 | 
			
		||||
        if (name != null) {
 | 
			
		||||
          final client = ref.watch(apiClientProvider);
 | 
			
		||||
          await client.patch(
 | 
			
		||||
            '/id/accounts/me/sessions/current/label',
 | 
			
		||||
            data: jsonEncode(name),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    useEffect(() {
 | 
			
		||||
@@ -578,6 +572,7 @@ class _LoginLookupScreen extends HookConsumerWidget {
 | 
			
		||||
          data: {
 | 
			
		||||
            'account': uname,
 | 
			
		||||
            'device_id': await getUdid(),
 | 
			
		||||
            'device_name': await getDeviceName(),
 | 
			
		||||
            'platform':
 | 
			
		||||
                kIsWeb
 | 
			
		||||
                    ? 1
 | 
			
		||||
@@ -628,6 +623,7 @@ class _LoginLookupScreen extends HookConsumerWidget {
 | 
			
		||||
            'identity_token': credential.identityToken!,
 | 
			
		||||
            'authorization_code': credential.authorizationCode,
 | 
			
		||||
            'device_id': await getUdid(),
 | 
			
		||||
            'device_name': await getDeviceName(),
 | 
			
		||||
          },
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,17 +14,19 @@ part 'poll_list.g.dart';
 | 
			
		||||
 | 
			
		||||
@riverpod
 | 
			
		||||
class PollListNotifier extends _$PollListNotifier
 | 
			
		||||
    with CursorPagingNotifierMixin<SnPoll> {
 | 
			
		||||
    with CursorPagingNotifierMixin<SnPollWithStats> {
 | 
			
		||||
  static const int _pageSize = 20;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<CursorPagingData<SnPoll>> build(String? pubName) {
 | 
			
		||||
  Future<CursorPagingData<SnPollWithStats>> build(String? pubName) {
 | 
			
		||||
    // immediately load first page
 | 
			
		||||
    return fetch(cursor: null);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<CursorPagingData<SnPoll>> fetch({required String? cursor}) async {
 | 
			
		||||
  Future<CursorPagingData<SnPollWithStats>> fetch({
 | 
			
		||||
    required String? cursor,
 | 
			
		||||
  }) async {
 | 
			
		||||
    final client = ref.read(apiClientProvider);
 | 
			
		||||
    final offset = cursor == null ? 0 : int.parse(cursor);
 | 
			
		||||
 | 
			
		||||
@@ -42,7 +44,7 @@ class PollListNotifier extends _$PollListNotifier
 | 
			
		||||
    );
 | 
			
		||||
    final total = int.parse(response.headers.value('X-Total') ?? '0');
 | 
			
		||||
    final List<dynamic> data = response.data;
 | 
			
		||||
    final items = data.map((json) => SnPoll.fromJson(json)).toList();
 | 
			
		||||
    final items = data.map((json) => SnPollWithStats.fromJson(json)).toList();
 | 
			
		||||
 | 
			
		||||
    final hasMore = offset + items.length < total;
 | 
			
		||||
    final nextCursor = hasMore ? (offset + items.length).toString() : null;
 | 
			
		||||
@@ -55,6 +57,13 @@ class PollListNotifier extends _$PollListNotifier
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@riverpod
 | 
			
		||||
Future<SnPollWithStats> pollWithStats(Ref ref, String id) async {
 | 
			
		||||
  final apiClient = ref.watch(apiClientProvider);
 | 
			
		||||
  final resp = await apiClient.get('/sphere/polls/$id');
 | 
			
		||||
  return SnPollWithStats.fromJson(resp.data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class CreatorPollListScreen extends HookConsumerWidget {
 | 
			
		||||
  const CreatorPollListScreen({super.key, required this.pubName});
 | 
			
		||||
 | 
			
		||||
@@ -64,7 +73,7 @@ class CreatorPollListScreen extends HookConsumerWidget {
 | 
			
		||||
    final result = await GoRouter.of(
 | 
			
		||||
      context,
 | 
			
		||||
    ).pushNamed('creatorPollNew', pathParameters: {'name': pubName});
 | 
			
		||||
    if (result is SnPoll && context.mounted) {
 | 
			
		||||
    if (result is SnPollWithStats && context.mounted) {
 | 
			
		||||
      Navigator.of(context).maybePop(result);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -92,8 +101,11 @@ class CreatorPollListScreen extends HookConsumerWidget {
 | 
			
		||||
                      if (index == widgetCount - 1) {
 | 
			
		||||
                        return endItemView;
 | 
			
		||||
                      }
 | 
			
		||||
                      final poll = data.items[index];
 | 
			
		||||
                      return _CreatorPollItem(poll: poll, pubName: pubName);
 | 
			
		||||
                      final pollWithStats = data.items[index];
 | 
			
		||||
                      return _CreatorPollItem(
 | 
			
		||||
                        pollWithStats: pollWithStats,
 | 
			
		||||
                        pubName: pubName,
 | 
			
		||||
                      );
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
            ),
 | 
			
		||||
@@ -106,14 +118,14 @@ class CreatorPollListScreen extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
class _CreatorPollItem extends StatelessWidget {
 | 
			
		||||
  final String pubName;
 | 
			
		||||
  const _CreatorPollItem({required this.poll, required this.pubName});
 | 
			
		||||
  const _CreatorPollItem({required this.pollWithStats, required this.pubName});
 | 
			
		||||
 | 
			
		||||
  final SnPoll poll;
 | 
			
		||||
  final SnPollWithStats pollWithStats;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final theme = Theme.of(context);
 | 
			
		||||
    final ended = poll.endedAt;
 | 
			
		||||
    final ended = pollWithStats.endedAt;
 | 
			
		||||
    final endedText =
 | 
			
		||||
        ended == null
 | 
			
		||||
            ? 'No end'
 | 
			
		||||
@@ -123,15 +135,16 @@ class _CreatorPollItem extends StatelessWidget {
 | 
			
		||||
      margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
 | 
			
		||||
      clipBehavior: Clip.antiAlias,
 | 
			
		||||
      child: ListTile(
 | 
			
		||||
        title: Text(poll.title ?? 'Untitled poll'),
 | 
			
		||||
        title: Text(pollWithStats.title ?? 'Untitled poll'),
 | 
			
		||||
        subtitle: Column(
 | 
			
		||||
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
          children: [
 | 
			
		||||
            if (poll.description != null && poll.description!.isNotEmpty)
 | 
			
		||||
            if (pollWithStats.description != null &&
 | 
			
		||||
                pollWithStats.description!.isNotEmpty)
 | 
			
		||||
              Padding(
 | 
			
		||||
                padding: const EdgeInsets.only(top: 4),
 | 
			
		||||
                child: Text(
 | 
			
		||||
                  poll.description!,
 | 
			
		||||
                  pollWithStats.description!,
 | 
			
		||||
                  maxLines: 2,
 | 
			
		||||
                  overflow: TextOverflow.ellipsis,
 | 
			
		||||
                ),
 | 
			
		||||
@@ -139,7 +152,7 @@ class _CreatorPollItem extends StatelessWidget {
 | 
			
		||||
            Padding(
 | 
			
		||||
              padding: const EdgeInsets.only(top: 4),
 | 
			
		||||
              child: Text(
 | 
			
		||||
                'Questions: ${poll.questions.length} · Ends: $endedText',
 | 
			
		||||
                'Questions: ${pollWithStats.questions.length} · Ends: $endedText',
 | 
			
		||||
                style: theme.textTheme.bodySmall,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
@@ -159,7 +172,7 @@ class _CreatorPollItem extends StatelessWidget {
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    GoRouter.of(context).pushNamed(
 | 
			
		||||
                      'creatorPollEdit',
 | 
			
		||||
                      pathParameters: {'name': pubName, 'id': poll.id},
 | 
			
		||||
                      pathParameters: {'name': pubName, 'id': pollWithStats.id},
 | 
			
		||||
                    );
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
@@ -170,8 +183,7 @@ class _CreatorPollItem extends StatelessWidget {
 | 
			
		||||
            context: context,
 | 
			
		||||
            useRootNavigator: true,
 | 
			
		||||
            isScrollControlled: true,
 | 
			
		||||
            builder:
 | 
			
		||||
                (context) => PollFeedbackSheet(pollId: poll.id, poll: poll),
 | 
			
		||||
            builder: (context) => PollFeedbackSheet(pollId: pollWithStats.id),
 | 
			
		||||
          );
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ part of 'poll_list.dart';
 | 
			
		||||
// RiverpodGenerator
 | 
			
		||||
// **************************************************************************
 | 
			
		||||
 | 
			
		||||
String _$pollListNotifierHash() => r'd3da24ff6bbb8f35b06d57fc41625dc0312508e4';
 | 
			
		||||
String _$pollWithStatsHash() => r'6bb910046ce1e09368f9922dbec52fdc2cc86740';
 | 
			
		||||
 | 
			
		||||
/// Copied from Dart SDK
 | 
			
		||||
class _SystemHash {
 | 
			
		||||
@@ -29,11 +29,133 @@ class _SystemHash {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// See also [pollWithStats].
 | 
			
		||||
@ProviderFor(pollWithStats)
 | 
			
		||||
const pollWithStatsProvider = PollWithStatsFamily();
 | 
			
		||||
 | 
			
		||||
/// See also [pollWithStats].
 | 
			
		||||
class PollWithStatsFamily extends Family<AsyncValue<SnPollWithStats>> {
 | 
			
		||||
  /// See also [pollWithStats].
 | 
			
		||||
  const PollWithStatsFamily();
 | 
			
		||||
 | 
			
		||||
  /// See also [pollWithStats].
 | 
			
		||||
  PollWithStatsProvider call(String id) {
 | 
			
		||||
    return PollWithStatsProvider(id);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  PollWithStatsProvider getProviderOverride(
 | 
			
		||||
    covariant PollWithStatsProvider provider,
 | 
			
		||||
  ) {
 | 
			
		||||
    return call(provider.id);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static const Iterable<ProviderOrFamily>? _dependencies = null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Iterable<ProviderOrFamily>? get dependencies => _dependencies;
 | 
			
		||||
 | 
			
		||||
  static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
 | 
			
		||||
      _allTransitiveDependencies;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String? get name => r'pollWithStatsProvider';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// See also [pollWithStats].
 | 
			
		||||
class PollWithStatsProvider extends AutoDisposeFutureProvider<SnPollWithStats> {
 | 
			
		||||
  /// See also [pollWithStats].
 | 
			
		||||
  PollWithStatsProvider(String id)
 | 
			
		||||
    : this._internal(
 | 
			
		||||
        (ref) => pollWithStats(ref as PollWithStatsRef, id),
 | 
			
		||||
        from: pollWithStatsProvider,
 | 
			
		||||
        name: r'pollWithStatsProvider',
 | 
			
		||||
        debugGetCreateSourceHash:
 | 
			
		||||
            const bool.fromEnvironment('dart.vm.product')
 | 
			
		||||
                ? null
 | 
			
		||||
                : _$pollWithStatsHash,
 | 
			
		||||
        dependencies: PollWithStatsFamily._dependencies,
 | 
			
		||||
        allTransitiveDependencies:
 | 
			
		||||
            PollWithStatsFamily._allTransitiveDependencies,
 | 
			
		||||
        id: id,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
  PollWithStatsProvider._internal(
 | 
			
		||||
    super._createNotifier, {
 | 
			
		||||
    required super.name,
 | 
			
		||||
    required super.dependencies,
 | 
			
		||||
    required super.allTransitiveDependencies,
 | 
			
		||||
    required super.debugGetCreateSourceHash,
 | 
			
		||||
    required super.from,
 | 
			
		||||
    required this.id,
 | 
			
		||||
  }) : super.internal();
 | 
			
		||||
 | 
			
		||||
  final String id;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Override overrideWith(
 | 
			
		||||
    FutureOr<SnPollWithStats> Function(PollWithStatsRef provider) create,
 | 
			
		||||
  ) {
 | 
			
		||||
    return ProviderOverride(
 | 
			
		||||
      origin: this,
 | 
			
		||||
      override: PollWithStatsProvider._internal(
 | 
			
		||||
        (ref) => create(ref as PollWithStatsRef),
 | 
			
		||||
        from: from,
 | 
			
		||||
        name: null,
 | 
			
		||||
        dependencies: null,
 | 
			
		||||
        allTransitiveDependencies: null,
 | 
			
		||||
        debugGetCreateSourceHash: null,
 | 
			
		||||
        id: id,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  AutoDisposeFutureProviderElement<SnPollWithStats> createElement() {
 | 
			
		||||
    return _PollWithStatsProviderElement(this);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  bool operator ==(Object other) {
 | 
			
		||||
    return other is PollWithStatsProvider && other.id == id;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  int get hashCode {
 | 
			
		||||
    var hash = _SystemHash.combine(0, runtimeType.hashCode);
 | 
			
		||||
    hash = _SystemHash.combine(hash, id.hashCode);
 | 
			
		||||
 | 
			
		||||
    return _SystemHash.finish(hash);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
 | 
			
		||||
// ignore: unused_element
 | 
			
		||||
mixin PollWithStatsRef on AutoDisposeFutureProviderRef<SnPollWithStats> {
 | 
			
		||||
  /// The parameter `id` of this provider.
 | 
			
		||||
  String get id;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _PollWithStatsProviderElement
 | 
			
		||||
    extends AutoDisposeFutureProviderElement<SnPollWithStats>
 | 
			
		||||
    with PollWithStatsRef {
 | 
			
		||||
  _PollWithStatsProviderElement(super.provider);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String get id => (origin as PollWithStatsProvider).id;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
String _$pollListNotifierHash() => r'd5b822e737788be8982f5cb3b501d460441930c1';
 | 
			
		||||
 | 
			
		||||
abstract class _$PollListNotifier
 | 
			
		||||
    extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPoll>> {
 | 
			
		||||
    extends
 | 
			
		||||
        BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPollWithStats>> {
 | 
			
		||||
  late final String? pubName;
 | 
			
		||||
 | 
			
		||||
  FutureOr<CursorPagingData<SnPoll>> build(String? pubName);
 | 
			
		||||
  FutureOr<CursorPagingData<SnPollWithStats>> build(String? pubName);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// See also [PollListNotifier].
 | 
			
		||||
@@ -42,7 +164,7 @@ const pollListNotifierProvider = PollListNotifierFamily();
 | 
			
		||||
 | 
			
		||||
/// See also [PollListNotifier].
 | 
			
		||||
class PollListNotifierFamily
 | 
			
		||||
    extends Family<AsyncValue<CursorPagingData<SnPoll>>> {
 | 
			
		||||
    extends Family<AsyncValue<CursorPagingData<SnPollWithStats>>> {
 | 
			
		||||
  /// See also [PollListNotifier].
 | 
			
		||||
  const PollListNotifierFamily();
 | 
			
		||||
 | 
			
		||||
@@ -78,7 +200,7 @@ class PollListNotifierProvider
 | 
			
		||||
    extends
 | 
			
		||||
        AutoDisposeAsyncNotifierProviderImpl<
 | 
			
		||||
          PollListNotifier,
 | 
			
		||||
          CursorPagingData<SnPoll>
 | 
			
		||||
          CursorPagingData<SnPollWithStats>
 | 
			
		||||
        > {
 | 
			
		||||
  /// See also [PollListNotifier].
 | 
			
		||||
  PollListNotifierProvider(String? pubName)
 | 
			
		||||
@@ -109,7 +231,7 @@ class PollListNotifierProvider
 | 
			
		||||
  final String? pubName;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  FutureOr<CursorPagingData<SnPoll>> runNotifierBuild(
 | 
			
		||||
  FutureOr<CursorPagingData<SnPollWithStats>> runNotifierBuild(
 | 
			
		||||
    covariant PollListNotifier notifier,
 | 
			
		||||
  ) {
 | 
			
		||||
    return notifier.build(pubName);
 | 
			
		||||
@@ -134,7 +256,7 @@ class PollListNotifierProvider
 | 
			
		||||
  @override
 | 
			
		||||
  AutoDisposeAsyncNotifierProviderElement<
 | 
			
		||||
    PollListNotifier,
 | 
			
		||||
    CursorPagingData<SnPoll>
 | 
			
		||||
    CursorPagingData<SnPollWithStats>
 | 
			
		||||
  >
 | 
			
		||||
  createElement() {
 | 
			
		||||
    return _PollListNotifierProviderElement(this);
 | 
			
		||||
@@ -157,7 +279,7 @@ class PollListNotifierProvider
 | 
			
		||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
 | 
			
		||||
// ignore: unused_element
 | 
			
		||||
mixin PollListNotifierRef
 | 
			
		||||
    on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPoll>> {
 | 
			
		||||
    on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPollWithStats>> {
 | 
			
		||||
  /// The parameter `pubName` of this provider.
 | 
			
		||||
  String? get pubName;
 | 
			
		||||
}
 | 
			
		||||
@@ -166,7 +288,7 @@ class _PollListNotifierProviderElement
 | 
			
		||||
    extends
 | 
			
		||||
        AutoDisposeAsyncNotifierProviderElement<
 | 
			
		||||
          PollListNotifier,
 | 
			
		||||
          CursorPagingData<SnPoll>
 | 
			
		||||
          CursorPagingData<SnPollWithStats>
 | 
			
		||||
        >
 | 
			
		||||
    with PollListNotifierRef {
 | 
			
		||||
  _PollListNotifierProviderElement(super.provider);
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:island/models/user.dart';
 | 
			
		||||
import 'package:island/models/account.dart';
 | 
			
		||||
import 'package:island/pods/network.dart';
 | 
			
		||||
import 'package:island/pods/websocket.dart';
 | 
			
		||||
import 'package:island/route.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ import 'package:island/widgets/alert.dart';
 | 
			
		||||
import 'package:island/models/poll.dart';
 | 
			
		||||
import 'package:island/widgets/app_scaffold.dart';
 | 
			
		||||
import 'package:uuid/uuid.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
 | 
			
		||||
class PollEditorState {
 | 
			
		||||
  String? id; // for editing
 | 
			
		||||
@@ -110,7 +111,7 @@ class PollEditor extends Notifier<PollEditorState> {
 | 
			
		||||
              ? [
 | 
			
		||||
                SnPollOption(
 | 
			
		||||
                  id: const Uuid().v4(),
 | 
			
		||||
                  label: 'Option 1',
 | 
			
		||||
                  label: 'pollOptionDefaultLabel'.tr(),
 | 
			
		||||
                  order: 0,
 | 
			
		||||
                ),
 | 
			
		||||
              ]
 | 
			
		||||
@@ -191,7 +192,7 @@ class PollEditor extends Notifier<PollEditorState> {
 | 
			
		||||
                : [
 | 
			
		||||
                  SnPollOption(
 | 
			
		||||
                    id: const Uuid().v4(),
 | 
			
		||||
                    label: 'Option 1',
 | 
			
		||||
                    label: 'pollOptionDefaultLabel'.tr(),
 | 
			
		||||
                    order: 0,
 | 
			
		||||
                  ),
 | 
			
		||||
                ])
 | 
			
		||||
@@ -389,7 +390,7 @@ class PollEditorScreen extends ConsumerWidget {
 | 
			
		||||
                data: body,
 | 
			
		||||
              ));
 | 
			
		||||
 | 
			
		||||
      showSnackBar(isUpdate ? 'Poll updated.' : 'Poll created.');
 | 
			
		||||
      showSnackBar(isUpdate ? 'pollUpdated'.tr() : 'pollCreated'.tr());
 | 
			
		||||
 | 
			
		||||
      if (!context.mounted) return;
 | 
			
		||||
      Navigator.of(context).maybePop(res.data);
 | 
			
		||||
@@ -416,11 +417,11 @@ class PollEditorScreen extends ConsumerWidget {
 | 
			
		||||
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: Text(model.id == null ? 'Create Poll' : 'Edit Poll'),
 | 
			
		||||
        title: Text(model.id == null ? 'pollCreate'.tr() : 'pollEdit'.tr()),
 | 
			
		||||
        actions: [
 | 
			
		||||
          if (kDebugMode)
 | 
			
		||||
            IconButton(
 | 
			
		||||
              tooltip: 'Preview JSON (debug)',
 | 
			
		||||
              tooltip: 'pollPreviewJsonDebug'.tr(),
 | 
			
		||||
              onPressed: () {
 | 
			
		||||
                _showDebugPreview(context, model);
 | 
			
		||||
              },
 | 
			
		||||
@@ -439,8 +440,8 @@ class PollEditorScreen extends ConsumerWidget {
 | 
			
		||||
                children: [
 | 
			
		||||
                  TextFormField(
 | 
			
		||||
                    initialValue: model.title ?? '',
 | 
			
		||||
                    decoration: const InputDecoration(
 | 
			
		||||
                      labelText: 'Title',
 | 
			
		||||
                    decoration: InputDecoration(
 | 
			
		||||
                      labelText: 'title'.tr(),
 | 
			
		||||
                      border: OutlineInputBorder(
 | 
			
		||||
                        borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
			
		||||
                      ),
 | 
			
		||||
@@ -452,7 +453,7 @@ class PollEditorScreen extends ConsumerWidget {
 | 
			
		||||
                        (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                    validator: (v) {
 | 
			
		||||
                      if (v == null || v.trim().isEmpty) {
 | 
			
		||||
                        return 'Title is required';
 | 
			
		||||
                        return 'pollTitleRequired'.tr();
 | 
			
		||||
                      }
 | 
			
		||||
                      return null;
 | 
			
		||||
                    },
 | 
			
		||||
@@ -460,8 +461,8 @@ class PollEditorScreen extends ConsumerWidget {
 | 
			
		||||
                  const Gap(12),
 | 
			
		||||
                  TextFormField(
 | 
			
		||||
                    initialValue: model.description ?? '',
 | 
			
		||||
                    decoration: const InputDecoration(
 | 
			
		||||
                      labelText: 'Description',
 | 
			
		||||
                    decoration: InputDecoration(
 | 
			
		||||
                      labelText: 'description'.tr(),
 | 
			
		||||
                      alignLabelWithHint: true,
 | 
			
		||||
                      border: OutlineInputBorder(
 | 
			
		||||
                        borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
			
		||||
@@ -482,7 +483,7 @@ class PollEditorScreen extends ConsumerWidget {
 | 
			
		||||
                  Row(
 | 
			
		||||
                    children: [
 | 
			
		||||
                      Text(
 | 
			
		||||
                        'Questions',
 | 
			
		||||
                        'questions'.tr(),
 | 
			
		||||
                        style: Theme.of(context).textTheme.titleLarge,
 | 
			
		||||
                      ),
 | 
			
		||||
                      const Spacer(),
 | 
			
		||||
@@ -495,7 +496,7 @@ class PollEditorScreen extends ConsumerWidget {
 | 
			
		||||
                                  : controller.open();
 | 
			
		||||
                            },
 | 
			
		||||
                            icon: const Icon(Icons.add),
 | 
			
		||||
                            label: const Text('Add question'),
 | 
			
		||||
                            label: Text('pollAddQuestion'.tr()),
 | 
			
		||||
                          );
 | 
			
		||||
                        },
 | 
			
		||||
                        menuChildren:
 | 
			
		||||
@@ -514,9 +515,9 @@ class PollEditorScreen extends ConsumerWidget {
 | 
			
		||||
                  const Gap(8),
 | 
			
		||||
                  if (model.questions.isEmpty)
 | 
			
		||||
                    _EmptyState(
 | 
			
		||||
                      title: 'No questions yet',
 | 
			
		||||
                      title: 'pollNoQuestionsYet'.tr(),
 | 
			
		||||
                      subtitle:
 | 
			
		||||
                          'Use "Add question" to start building your poll.',
 | 
			
		||||
                          'pollNoQuestionsHint'.tr(),
 | 
			
		||||
                    )
 | 
			
		||||
                  else
 | 
			
		||||
                    ReorderableListView.builder(
 | 
			
		||||
@@ -585,7 +586,7 @@ class PollEditorScreen extends ConsumerWidget {
 | 
			
		||||
                  Navigator.of(context).maybePop();
 | 
			
		||||
                },
 | 
			
		||||
                icon: const Icon(Icons.close),
 | 
			
		||||
                label: const Text('Cancel'),
 | 
			
		||||
                label: Text('cancel'.tr()),
 | 
			
		||||
              ),
 | 
			
		||||
              const Spacer(),
 | 
			
		||||
              FilledButton.icon(
 | 
			
		||||
@@ -593,7 +594,7 @@ class PollEditorScreen extends ConsumerWidget {
 | 
			
		||||
                  _submitPoll(context, ref);
 | 
			
		||||
                },
 | 
			
		||||
                icon: const Icon(Icons.cloud_upload_outlined),
 | 
			
		||||
                label: Text(model.id == null ? 'Create' : 'Update'),
 | 
			
		||||
                label: Text(model.id == null ? 'create'.tr() : 'update'.tr()),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
@@ -637,14 +638,14 @@ class PollEditorScreen extends ConsumerWidget {
 | 
			
		||||
      context: context,
 | 
			
		||||
      builder:
 | 
			
		||||
          (_) => AlertDialog(
 | 
			
		||||
            title: const Text('Debug Preview'),
 | 
			
		||||
            title: Text('pollDebugPreview'.tr()),
 | 
			
		||||
            content: SingleChildScrollView(
 | 
			
		||||
              child: SelectableText(buf.toString()),
 | 
			
		||||
            ),
 | 
			
		||||
            actions: [
 | 
			
		||||
              TextButton(
 | 
			
		||||
                onPressed: () => Navigator.of(context).pop(),
 | 
			
		||||
                child: const Text('Close'),
 | 
			
		||||
                child: Text('close'.tr()),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
@@ -673,15 +674,15 @@ IconData _iconForType(SnPollQuestionType t) {
 | 
			
		||||
String _labelForType(SnPollQuestionType t) {
 | 
			
		||||
  switch (t) {
 | 
			
		||||
    case SnPollQuestionType.singleChoice:
 | 
			
		||||
      return 'Single choice';
 | 
			
		||||
      return 'pollQuestionTypeSingleChoice'.tr();
 | 
			
		||||
    case SnPollQuestionType.multipleChoice:
 | 
			
		||||
      return 'Multiple choice';
 | 
			
		||||
      return 'pollQuestionTypeMultipleChoice'.tr();
 | 
			
		||||
    case SnPollQuestionType.freeText:
 | 
			
		||||
      return 'Free text';
 | 
			
		||||
      return 'pollQuestionTypeFreeText'.tr();
 | 
			
		||||
    case SnPollQuestionType.yesNo:
 | 
			
		||||
      return 'Yes / No';
 | 
			
		||||
      return 'pollQuestionTypeYesNo'.tr();
 | 
			
		||||
    case SnPollQuestionType.rating:
 | 
			
		||||
      return 'Rating';
 | 
			
		||||
      return 'pollQuestionTypeRating'.tr();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -698,8 +699,8 @@ class _EndDatePicker extends StatelessWidget {
 | 
			
		||||
      children: [
 | 
			
		||||
        Expanded(
 | 
			
		||||
          child: InputDecorator(
 | 
			
		||||
            decoration: const InputDecoration(
 | 
			
		||||
              labelText: 'End date & time (optional)',
 | 
			
		||||
            decoration: InputDecoration(
 | 
			
		||||
              labelText: 'pollEndDateOptional'.tr(),
 | 
			
		||||
              border: OutlineInputBorder(
 | 
			
		||||
                borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
			
		||||
              ),
 | 
			
		||||
@@ -711,7 +712,7 @@ class _EndDatePicker extends StatelessWidget {
 | 
			
		||||
                Icon(Icons.event, color: Theme.of(context).colorScheme.primary),
 | 
			
		||||
                Text(
 | 
			
		||||
                  value == null
 | 
			
		||||
                      ? 'Not set'
 | 
			
		||||
                      ? 'notSet'.tr()
 | 
			
		||||
                      : MaterialLocalizations.of(
 | 
			
		||||
                        context,
 | 
			
		||||
                      ).formatFullDate(value!),
 | 
			
		||||
@@ -759,12 +760,12 @@ class _EndDatePicker extends StatelessWidget {
 | 
			
		||||
                    );
 | 
			
		||||
                    onChanged(dt);
 | 
			
		||||
                  },
 | 
			
		||||
                  child: const Text('Pick'),
 | 
			
		||||
                  child: Text('pick'.tr()),
 | 
			
		||||
                ),
 | 
			
		||||
                if (value != null)
 | 
			
		||||
                  TextButton(
 | 
			
		||||
                    onPressed: () => onChanged(null),
 | 
			
		||||
                    child: const Text('Clear'),
 | 
			
		||||
                    child: Text('clear'.tr()),
 | 
			
		||||
                  ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
@@ -799,7 +800,7 @@ class _QuestionHeader extends StatelessWidget {
 | 
			
		||||
        child: const Icon(Icons.drag_handle),
 | 
			
		||||
      ),
 | 
			
		||||
      title: Text(
 | 
			
		||||
        question.title.isEmpty ? 'Untitled question' : question.title,
 | 
			
		||||
        question.title.isEmpty ? 'pollUntitledQuestion'.tr() : question.title,
 | 
			
		||||
        maxLines: 1,
 | 
			
		||||
        overflow: TextOverflow.ellipsis,
 | 
			
		||||
      ),
 | 
			
		||||
@@ -808,17 +809,17 @@ class _QuestionHeader extends StatelessWidget {
 | 
			
		||||
        spacing: 4,
 | 
			
		||||
        children: [
 | 
			
		||||
          IconButton(
 | 
			
		||||
            tooltip: 'Move up',
 | 
			
		||||
            tooltip: 'moveUp'.tr(),
 | 
			
		||||
            onPressed: onMoveUp,
 | 
			
		||||
            icon: const Icon(Icons.arrow_upward),
 | 
			
		||||
          ),
 | 
			
		||||
          IconButton(
 | 
			
		||||
            tooltip: 'Move down',
 | 
			
		||||
            tooltip: 'moveDown'.tr(),
 | 
			
		||||
            onPressed: onMoveDown,
 | 
			
		||||
            icon: const Icon(Icons.arrow_downward),
 | 
			
		||||
          ),
 | 
			
		||||
          IconButton(
 | 
			
		||||
            tooltip: 'Delete',
 | 
			
		||||
            tooltip: 'delete'.tr(),
 | 
			
		||||
            onPressed: onDelete,
 | 
			
		||||
            icon: const Icon(Icons.delete_outline),
 | 
			
		||||
            color: Theme.of(context).colorScheme.error,
 | 
			
		||||
@@ -853,7 +854,7 @@ class _QuestionEditor extends ConsumerWidget {
 | 
			
		||||
              onChanged: (t) => notifier.setQuestionType(index, t),
 | 
			
		||||
            ),
 | 
			
		||||
            FilterChip(
 | 
			
		||||
              label: const Text('Required'),
 | 
			
		||||
              label: Text('required'.tr()),
 | 
			
		||||
              selected: question.isRequired,
 | 
			
		||||
              onSelected: (v) => notifier.setQuestionRequired(index, v),
 | 
			
		||||
              avatar: Icon(
 | 
			
		||||
@@ -867,8 +868,8 @@ class _QuestionEditor extends ConsumerWidget {
 | 
			
		||||
        const Gap(12),
 | 
			
		||||
        TextFormField(
 | 
			
		||||
          initialValue: question.title,
 | 
			
		||||
          decoration: const InputDecoration(
 | 
			
		||||
            labelText: 'Question title',
 | 
			
		||||
          decoration: InputDecoration(
 | 
			
		||||
            labelText: 'pollQuestionTitle'.tr(),
 | 
			
		||||
            border: OutlineInputBorder(
 | 
			
		||||
              borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
			
		||||
            ),
 | 
			
		||||
@@ -879,7 +880,7 @@ class _QuestionEditor extends ConsumerWidget {
 | 
			
		||||
          onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
          validator: (v) {
 | 
			
		||||
            if (v == null || v.trim().isEmpty) {
 | 
			
		||||
              return 'Question title is required';
 | 
			
		||||
              return 'pollQuestionTitleRequired'.tr();
 | 
			
		||||
            }
 | 
			
		||||
            return null;
 | 
			
		||||
          },
 | 
			
		||||
@@ -887,8 +888,8 @@ class _QuestionEditor extends ConsumerWidget {
 | 
			
		||||
        const Gap(12),
 | 
			
		||||
        TextFormField(
 | 
			
		||||
          initialValue: question.description ?? '',
 | 
			
		||||
          decoration: const InputDecoration(
 | 
			
		||||
            labelText: 'Question description (optional)',
 | 
			
		||||
          decoration: InputDecoration(
 | 
			
		||||
            labelText: 'pollQuestionDescriptionOptional'.tr(),
 | 
			
		||||
            border: OutlineInputBorder(
 | 
			
		||||
              borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
			
		||||
            ),
 | 
			
		||||
@@ -902,7 +903,7 @@ class _QuestionEditor extends ConsumerWidget {
 | 
			
		||||
        ),
 | 
			
		||||
        if (question.options != null) ...[
 | 
			
		||||
          const Gap(16),
 | 
			
		||||
          Text('Options', style: Theme.of(context).textTheme.titleMedium),
 | 
			
		||||
          Text('options'.tr(), style: Theme.of(context).textTheme.titleMedium),
 | 
			
		||||
          const Gap(8),
 | 
			
		||||
          _OptionsEditor(index: index, options: question.options!),
 | 
			
		||||
          const Gap(4),
 | 
			
		||||
@@ -911,7 +912,7 @@ class _QuestionEditor extends ConsumerWidget {
 | 
			
		||||
            child: OutlinedButton.icon(
 | 
			
		||||
              onPressed: () => notifier.addOption(index),
 | 
			
		||||
              icon: const Icon(Icons.add),
 | 
			
		||||
              label: const Text('Add option'),
 | 
			
		||||
              label: Text('pollAddOption'.tr()),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
@@ -937,8 +938,8 @@ class _QuestionTypePicker extends StatelessWidget {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return DropdownButtonFormField<SnPollQuestionType>(
 | 
			
		||||
      value: value,
 | 
			
		||||
      decoration: const InputDecoration(
 | 
			
		||||
        labelText: 'Type',
 | 
			
		||||
      decoration: InputDecoration(
 | 
			
		||||
        labelText: 'Type'.tr(),
 | 
			
		||||
        border: OutlineInputBorder(
 | 
			
		||||
          borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
			
		||||
        ),
 | 
			
		||||
@@ -987,8 +988,8 @@ class _OptionsEditor extends ConsumerWidget {
 | 
			
		||||
                  child: TextFormField(
 | 
			
		||||
                    key: ValueKey(options[i].id),
 | 
			
		||||
                    initialValue: options[i].label,
 | 
			
		||||
                    decoration: const InputDecoration(
 | 
			
		||||
                      labelText: 'Option label',
 | 
			
		||||
                    decoration: InputDecoration(
 | 
			
		||||
                      labelText: 'pollOptionLabel'.tr(),
 | 
			
		||||
                      border: OutlineInputBorder(
 | 
			
		||||
                        borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
			
		||||
                      ),
 | 
			
		||||
@@ -1003,7 +1004,7 @@ class _OptionsEditor extends ConsumerWidget {
 | 
			
		||||
                SizedBox(
 | 
			
		||||
                  width: 40,
 | 
			
		||||
                  child: IconButton(
 | 
			
		||||
                    tooltip: 'Move up',
 | 
			
		||||
                    tooltip: 'moveUp'.tr(),
 | 
			
		||||
                    onPressed:
 | 
			
		||||
                        i > 0 ? () => notifier.moveOptionUp(index, i) : null,
 | 
			
		||||
                    icon: const Icon(Icons.arrow_upward),
 | 
			
		||||
@@ -1012,7 +1013,7 @@ class _OptionsEditor extends ConsumerWidget {
 | 
			
		||||
                SizedBox(
 | 
			
		||||
                  width: 40,
 | 
			
		||||
                  child: IconButton(
 | 
			
		||||
                    tooltip: 'Move down',
 | 
			
		||||
                    tooltip: 'moveDown'.tr(),
 | 
			
		||||
                    onPressed:
 | 
			
		||||
                        i < options.length - 1
 | 
			
		||||
                            ? () => notifier.moveOptionDown(index, i)
 | 
			
		||||
@@ -1023,7 +1024,7 @@ class _OptionsEditor extends ConsumerWidget {
 | 
			
		||||
                SizedBox(
 | 
			
		||||
                  width: 40,
 | 
			
		||||
                  child: IconButton(
 | 
			
		||||
                    tooltip: 'Delete',
 | 
			
		||||
                    tooltip: 'delete'.tr(),
 | 
			
		||||
                    onPressed: () => notifier.removeOption(index, i),
 | 
			
		||||
                    icon: const Icon(Icons.close),
 | 
			
		||||
                  ),
 | 
			
		||||
@@ -1048,7 +1049,7 @@ class _TextAnswerPreview extends StatelessWidget {
 | 
			
		||||
      maxLines: long ? 4 : 1,
 | 
			
		||||
      decoration: InputDecoration(
 | 
			
		||||
        labelText:
 | 
			
		||||
            long ? 'Long text answer (preview)' : 'Short text answer (preview)',
 | 
			
		||||
            long ? 'pollLongTextAnswerPreview'.tr() : 'pollShortTextAnswerPreview'.tr(),
 | 
			
		||||
        border: const OutlineInputBorder(
 | 
			
		||||
          borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
			
		||||
        ),
 | 
			
		||||
@@ -1082,9 +1083,9 @@ class _EmptyState extends StatelessWidget {
 | 
			
		||||
            child: Column(
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
              children: [
 | 
			
		||||
                Text(title, style: Theme.of(context).textTheme.titleMedium),
 | 
			
		||||
                Text('pollNoQuestionsYet'.tr(), style: Theme.of(context).textTheme.titleMedium),
 | 
			
		||||
                const Gap(4),
 | 
			
		||||
                Text(subtitle, style: Theme.of(context).textTheme.bodyMedium),
 | 
			
		||||
                Text('pollNoQuestionsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:island/models/post.dart';
 | 
			
		||||
import 'package:island/models/publisher.dart';
 | 
			
		||||
import 'package:island/models/user.dart';
 | 
			
		||||
import 'package:island/models/account.dart';
 | 
			
		||||
import 'package:island/pods/config.dart';
 | 
			
		||||
import 'package:island/pods/network.dart';
 | 
			
		||||
import 'package:island/services/color.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ 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/user.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';
 | 
			
		||||
@@ -67,7 +67,7 @@ Future<void> subscribePushNotification(
 | 
			
		||||
  Dio apiClient, {
 | 
			
		||||
  bool detailedErrors = false,
 | 
			
		||||
}) async {
 | 
			
		||||
  if (Platform.isLinux){
 | 
			
		||||
  if (Platform.isLinux) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  await FirebaseMessaging.instance.requestPermission(
 | 
			
		||||
 
 | 
			
		||||
@@ -3,33 +3,34 @@ import 'dart:convert';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:island/models/auth.dart';
 | 
			
		||||
import 'package:island/models/account.dart';
 | 
			
		||||
import 'package:island/pods/network.dart';
 | 
			
		||||
import 'package:island/services/responsive.dart';
 | 
			
		||||
import 'package:island/services/udid.dart';
 | 
			
		||||
import 'package:island/widgets/alert.dart';
 | 
			
		||||
import 'package:island/widgets/content/sheet.dart';
 | 
			
		||||
import 'package:island/widgets/response.dart';
 | 
			
		||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
 | 
			
		||||
part 'account_session_sheet.g.dart';
 | 
			
		||||
part 'account_devices.g.dart';
 | 
			
		||||
 | 
			
		||||
@riverpod
 | 
			
		||||
Future<List<SnAuthDevice>> authDevices(Ref ref) async {
 | 
			
		||||
Future<List<SnAuthDeviceWithChallenge>> authDevices(Ref ref) async {
 | 
			
		||||
  final resp = await ref
 | 
			
		||||
      .watch(apiClientProvider)
 | 
			
		||||
      .get('/id/accounts/me/devices');
 | 
			
		||||
  final sessionId = resp.headers.value('x-auth-session');
 | 
			
		||||
  final currentId = await getUdid();
 | 
			
		||||
  final data =
 | 
			
		||||
      resp.data.map<SnAuthDevice>((e) {
 | 
			
		||||
        final ele = SnAuthDevice.fromJson(e);
 | 
			
		||||
        return ele.copyWith(isCurrent: ele.sessions.first.id == sessionId);
 | 
			
		||||
      resp.data.map<SnAuthDeviceWithChallenge>((e) {
 | 
			
		||||
        final ele = SnAuthDeviceWithChallenge.fromJson(e);
 | 
			
		||||
        return ele.copyWith(isCurrent: ele.deviceId == currentId);
 | 
			
		||||
      }).toList();
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _DeviceListTile extends StatelessWidget {
 | 
			
		||||
  final SnAuthDevice device;
 | 
			
		||||
  final SnAuthDeviceWithChallenge device;
 | 
			
		||||
  final Function(String) updateDeviceLabel;
 | 
			
		||||
  final Function(String) logoutDevice;
 | 
			
		||||
 | 
			
		||||
@@ -57,17 +58,16 @@ class _DeviceListTile extends StatelessWidget {
 | 
			
		||||
      subtitle: Column(
 | 
			
		||||
        crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
			
		||||
        children: [
 | 
			
		||||
          Text('authSessionsCount'.plural(device.sessions.length)),
 | 
			
		||||
          Text(
 | 
			
		||||
            'lastActiveAt'.tr(
 | 
			
		||||
              args: [
 | 
			
		||||
                DateFormat().format(
 | 
			
		||||
                  device.sessions.first.lastGrantedAt.toLocal(),
 | 
			
		||||
                  device.challenges.first.createdAt.toLocal(),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          Text(device.sessions.first.challenge.ipAddress),
 | 
			
		||||
          Text(device.challenges.first.ipAddress),
 | 
			
		||||
          if (device.isCurrent)
 | 
			
		||||
            Row(
 | 
			
		||||
              children: [
 | 
			
		||||
@@ -84,7 +84,7 @@ class _DeviceListTile extends StatelessWidget {
 | 
			
		||||
            ).padding(top: 4),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      title: Text(device.label ?? device.sessions.first.challenge.userAgent),
 | 
			
		||||
      title: Text(device.deviceLabel ?? device.deviceName),
 | 
			
		||||
      trailing:
 | 
			
		||||
          isWideScreen(context)
 | 
			
		||||
              ? Row(
 | 
			
		||||
@@ -93,14 +93,13 @@ class _DeviceListTile extends StatelessWidget {
 | 
			
		||||
                  IconButton(
 | 
			
		||||
                    icon: Icon(Icons.edit),
 | 
			
		||||
                    tooltip: 'authDeviceEditLabel'.tr(),
 | 
			
		||||
                    onPressed:
 | 
			
		||||
                        () => updateDeviceLabel(device.sessions.first.id),
 | 
			
		||||
                    onPressed: () => updateDeviceLabel(device.deviceId),
 | 
			
		||||
                  ),
 | 
			
		||||
                  if (!device.isCurrent)
 | 
			
		||||
                    IconButton(
 | 
			
		||||
                      icon: Icon(Icons.logout),
 | 
			
		||||
                      tooltip: 'authDeviceLogout'.tr(),
 | 
			
		||||
                      onPressed: () => logoutDevice(device.sessions.first.id),
 | 
			
		||||
                      onPressed: () => logoutDevice(device.deviceId),
 | 
			
		||||
                    ),
 | 
			
		||||
                ],
 | 
			
		||||
              )
 | 
			
		||||
@@ -124,7 +123,7 @@ class AccountSessionSheet extends HookConsumerWidget {
 | 
			
		||||
      if (!confirm || !context.mounted) return;
 | 
			
		||||
      try {
 | 
			
		||||
        final apiClient = ref.watch(apiClientProvider);
 | 
			
		||||
        await apiClient.delete('/id/accounts/me/sessions/$sessionId');
 | 
			
		||||
        await apiClient.delete('/id/accounts/me/devices/$sessionId');
 | 
			
		||||
        ref.invalidate(authDevicesProvider);
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        showErrorAlert(err);
 | 
			
		||||
@@ -163,7 +162,7 @@ class AccountSessionSheet extends HookConsumerWidget {
 | 
			
		||||
      try {
 | 
			
		||||
        final apiClient = ref.watch(apiClientProvider);
 | 
			
		||||
        await apiClient.patch(
 | 
			
		||||
          '/accounts/me/sessions/$sessionId/label',
 | 
			
		||||
          '/accounts/me/devices/$sessionId/label',
 | 
			
		||||
          data: jsonEncode(label),
 | 
			
		||||
        );
 | 
			
		||||
        ref.invalidate(authDevicesProvider);
 | 
			
		||||
@@ -194,7 +193,7 @@ class AccountSessionSheet extends HookConsumerWidget {
 | 
			
		||||
                    );
 | 
			
		||||
                  } else {
 | 
			
		||||
                    return Dismissible(
 | 
			
		||||
                      key: Key('device-${device.sessions.first.id}'),
 | 
			
		||||
                      key: Key('device-${device.id}'),
 | 
			
		||||
                      direction:
 | 
			
		||||
                          device.isCurrent
 | 
			
		||||
                              ? DismissDirection.startToEnd
 | 
			
		||||
@@ -213,7 +212,7 @@ class AccountSessionSheet extends HookConsumerWidget {
 | 
			
		||||
                      ),
 | 
			
		||||
                      confirmDismiss: (direction) async {
 | 
			
		||||
                        if (direction == DismissDirection.startToEnd) {
 | 
			
		||||
                          updateDeviceLabel(device.sessions.first.id);
 | 
			
		||||
                          updateDeviceLabel(device.deviceId);
 | 
			
		||||
                          return false;
 | 
			
		||||
                        } else {
 | 
			
		||||
                          final confirm = await showConfirmAlert(
 | 
			
		||||
@@ -221,7 +220,7 @@ class AccountSessionSheet extends HookConsumerWidget {
 | 
			
		||||
                            'authDeviceLogout'.tr(),
 | 
			
		||||
                          );
 | 
			
		||||
                          if (confirm && context.mounted) {
 | 
			
		||||
                            logoutDevice(device.sessions.first.id);
 | 
			
		||||
                            logoutDevice(device.deviceId);
 | 
			
		||||
                          }
 | 
			
		||||
                          return false; // Don't dismiss
 | 
			
		||||
                        }
 | 
			
		||||
@@ -1,17 +1,17 @@
 | 
			
		||||
// GENERATED CODE - DO NOT MODIFY BY HAND
 | 
			
		||||
 | 
			
		||||
part of 'account_session_sheet.dart';
 | 
			
		||||
part of 'account_devices.dart';
 | 
			
		||||
 | 
			
		||||
// **************************************************************************
 | 
			
		||||
// RiverpodGenerator
 | 
			
		||||
// **************************************************************************
 | 
			
		||||
 | 
			
		||||
String _$authDevicesHash() => r'8bc41a1ffc37df8e757c977b4ddae11db8faaeb5';
 | 
			
		||||
String _$authDevicesHash() => r'feb19238f759921e51c888f8b443a3d7761e68da';
 | 
			
		||||
 | 
			
		||||
/// See also [authDevices].
 | 
			
		||||
@ProviderFor(authDevices)
 | 
			
		||||
final authDevicesProvider =
 | 
			
		||||
    AutoDisposeFutureProvider<List<SnAuthDevice>>.internal(
 | 
			
		||||
    AutoDisposeFutureProvider<List<SnAuthDeviceWithChallenge>>.internal(
 | 
			
		||||
      authDevices,
 | 
			
		||||
      name: r'authDevicesProvider',
 | 
			
		||||
      debugGetCreateSourceHash:
 | 
			
		||||
@@ -24,6 +24,7 @@ final authDevicesProvider =
 | 
			
		||||
 | 
			
		||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
 | 
			
		||||
// ignore: unused_element
 | 
			
		||||
typedef AuthDevicesRef = AutoDisposeFutureProviderRef<List<SnAuthDevice>>;
 | 
			
		||||
typedef AuthDevicesRef =
 | 
			
		||||
    AutoDisposeFutureProviderRef<List<SnAuthDeviceWithChallenge>>;
 | 
			
		||||
// ignore_for_file: type=lint
 | 
			
		||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:island/models/user.dart';
 | 
			
		||||
import 'package:island/models/account.dart';
 | 
			
		||||
import 'package:island/models/wallet.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:island/models/user.dart';
 | 
			
		||||
import 'package:island/models/account.dart';
 | 
			
		||||
import 'package:island/pods/network.dart';
 | 
			
		||||
import 'package:island/widgets/content/cloud_files.dart';
 | 
			
		||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:island/models/user.dart';
 | 
			
		||||
import 'package:island/models/account.dart';
 | 
			
		||||
import 'package:island/models/badge.dart';
 | 
			
		||||
 | 
			
		||||
class BadgeList extends StatelessWidget {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:island/models/user.dart';
 | 
			
		||||
import 'package:island/models/account.dart';
 | 
			
		||||
import 'package:island/pods/network.dart';
 | 
			
		||||
import 'package:island/screens/account/profile.dart';
 | 
			
		||||
import 'package:island/services/time.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:island/models/user.dart';
 | 
			
		||||
import 'package:island/models/account.dart';
 | 
			
		||||
import 'package:island/pods/network.dart';
 | 
			
		||||
import 'package:island/pods/userinfo.dart';
 | 
			
		||||
import 'package:island/widgets/account/status.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -69,7 +69,7 @@ void showLoadingModal(BuildContext context) {
 | 
			
		||||
                child: Column(
 | 
			
		||||
                  mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    CircularProgressIndicator(year2023: true),
 | 
			
		||||
                    CircularProgressIndicator(year2023: false),
 | 
			
		||||
                    const Gap(24),
 | 
			
		||||
                    Text('loading'.tr()),
 | 
			
		||||
                  ],
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:island/models/user.dart';
 | 
			
		||||
import 'package:island/models/account.dart';
 | 
			
		||||
import 'package:island/widgets/content/cloud_files.dart';
 | 
			
		||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,14 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:island/models/poll.dart';
 | 
			
		||||
import 'package:island/pods/network.dart';
 | 
			
		||||
import 'package:island/screens/creators/poll/poll_list.dart';
 | 
			
		||||
import 'package:island/services/time.dart';
 | 
			
		||||
import 'package:island/widgets/content/sheet.dart';
 | 
			
		||||
import 'package:island/widgets/poll/poll_stats_widget.dart';
 | 
			
		||||
import 'package:island/widgets/response.dart';
 | 
			
		||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
			
		||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
@@ -52,59 +57,60 @@ class PollFeedbackNotifier extends _$PollFeedbackNotifier
 | 
			
		||||
class PollFeedbackSheet extends HookConsumerWidget {
 | 
			
		||||
  final String pollId;
 | 
			
		||||
  final String? title;
 | 
			
		||||
  final SnPoll poll;
 | 
			
		||||
  final Map<String, dynamic>? stats; // stats object similar to PollSubmit
 | 
			
		||||
  const PollFeedbackSheet({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.pollId,
 | 
			
		||||
    required this.poll,
 | 
			
		||||
    this.title,
 | 
			
		||||
    this.stats,
 | 
			
		||||
  });
 | 
			
		||||
  const PollFeedbackSheet({super.key, required this.pollId, this.title});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final poll = ref.watch(pollWithStatsProvider(pollId));
 | 
			
		||||
 | 
			
		||||
    return SheetScaffold(
 | 
			
		||||
      titleText: title ?? 'Poll feedback',
 | 
			
		||||
      child: Column(
 | 
			
		||||
        crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
			
		||||
        children: [
 | 
			
		||||
          _PollHeader(poll: poll, stats: stats),
 | 
			
		||||
          const Divider(height: 1),
 | 
			
		||||
          Expanded(
 | 
			
		||||
            child: PagingHelperView(
 | 
			
		||||
              provider: pollFeedbackNotifierProvider(pollId),
 | 
			
		||||
              futureRefreshable: pollFeedbackNotifierProvider(pollId).future,
 | 
			
		||||
              notifierRefreshable:
 | 
			
		||||
                  pollFeedbackNotifierProvider(pollId).notifier,
 | 
			
		||||
              contentBuilder:
 | 
			
		||||
                  (data, widgetCount, endItemView) => ListView.separated(
 | 
			
		||||
                    padding: const EdgeInsets.symmetric(vertical: 4),
 | 
			
		||||
                    itemCount: widgetCount,
 | 
			
		||||
                    itemBuilder: (context, index) {
 | 
			
		||||
                      if (index == widgetCount - 1) {
 | 
			
		||||
                        // Provided by PagingHelperView to indicate end/loading
 | 
			
		||||
                        return endItemView;
 | 
			
		||||
                      }
 | 
			
		||||
                      final answer = data.items[index];
 | 
			
		||||
                      return _PollAnswerTile(answer: answer, poll: poll);
 | 
			
		||||
                    },
 | 
			
		||||
                    separatorBuilder:
 | 
			
		||||
                        (context, index) =>
 | 
			
		||||
                            const Divider(height: 1).padding(vertical: 4),
 | 
			
		||||
                  ),
 | 
			
		||||
      child: poll.when(
 | 
			
		||||
        data:
 | 
			
		||||
            (data) => CustomScrollView(
 | 
			
		||||
              slivers: [
 | 
			
		||||
                SliverToBoxAdapter(child: _PollHeader(poll: data)),
 | 
			
		||||
                SliverToBoxAdapter(child: const Divider(height: 1)),
 | 
			
		||||
                SliverGap(4),
 | 
			
		||||
                PagingHelperSliverView(
 | 
			
		||||
                  provider: pollFeedbackNotifierProvider(pollId),
 | 
			
		||||
                  futureRefreshable:
 | 
			
		||||
                      pollFeedbackNotifierProvider(pollId).future,
 | 
			
		||||
                  notifierRefreshable:
 | 
			
		||||
                      pollFeedbackNotifierProvider(pollId).notifier,
 | 
			
		||||
                  contentBuilder:
 | 
			
		||||
                      (val, widgetCount, endItemView) => SliverList.separated(
 | 
			
		||||
                        itemCount: widgetCount,
 | 
			
		||||
                        itemBuilder: (context, index) {
 | 
			
		||||
                          if (index == widgetCount - 1) {
 | 
			
		||||
                            // Provided by PagingHelperView to indicate end/loading
 | 
			
		||||
                            return endItemView;
 | 
			
		||||
                          }
 | 
			
		||||
                          final answer = val.items[index];
 | 
			
		||||
                          return _PollAnswerTile(answer: answer, poll: data);
 | 
			
		||||
                        },
 | 
			
		||||
                        separatorBuilder:
 | 
			
		||||
                            (context, index) =>
 | 
			
		||||
                                const Divider(height: 1).padding(vertical: 4),
 | 
			
		||||
                      ),
 | 
			
		||||
                ),
 | 
			
		||||
                SliverGap(4 + MediaQuery.of(context).padding.bottom),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
        error:
 | 
			
		||||
            (err, _) => ResponseErrorWidget(
 | 
			
		||||
              error: err,
 | 
			
		||||
              onRetry: () => ref.invalidate(pollWithStatsProvider(pollId)),
 | 
			
		||||
            ),
 | 
			
		||||
        loading: () => ResponseLoadingWidget(),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _PollHeader extends StatelessWidget {
 | 
			
		||||
  const _PollHeader({required this.poll, this.stats});
 | 
			
		||||
  final SnPoll poll;
 | 
			
		||||
  final Map<String, dynamic>? stats;
 | 
			
		||||
  const _PollHeader({required this.poll});
 | 
			
		||||
  final SnPollWithStats poll;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
@@ -112,18 +118,32 @@ class _PollHeader extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
    return Column(
 | 
			
		||||
      crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
      spacing: 12,
 | 
			
		||||
      children: [
 | 
			
		||||
        if (poll.title != null)
 | 
			
		||||
          Text(poll.title!, style: theme.textTheme.titleLarge),
 | 
			
		||||
        if (poll.description != null)
 | 
			
		||||
          Padding(
 | 
			
		||||
            padding: const EdgeInsets.only(top: 2),
 | 
			
		||||
            child: Text(
 | 
			
		||||
              poll.description!,
 | 
			
		||||
              style: theme.textTheme.bodyMedium?.copyWith(
 | 
			
		||||
                color: theme.textTheme.bodyMedium?.color?.withOpacity(0.7),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
        if (poll.title != null || (poll.description?.isNotEmpty ?? false))
 | 
			
		||||
          Column(
 | 
			
		||||
            crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
            children: [
 | 
			
		||||
              if (poll.title != null)
 | 
			
		||||
                Text(poll.title!, style: theme.textTheme.titleLarge),
 | 
			
		||||
              if (poll.description?.isNotEmpty ?? false)
 | 
			
		||||
                Text(
 | 
			
		||||
                  poll.description!,
 | 
			
		||||
                  style: theme.textTheme.bodyMedium?.copyWith(
 | 
			
		||||
                    color: theme.textTheme.bodyMedium?.color?.withOpacity(0.7),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        Text('pollQuestions').tr().fontSize(17).bold(),
 | 
			
		||||
        for (final q in poll.questions)
 | 
			
		||||
          Column(
 | 
			
		||||
            crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
            children: [
 | 
			
		||||
              if (q.title.isNotEmpty) Text(q.title).bold(),
 | 
			
		||||
              if (q.description?.isNotEmpty ?? false) Text(q.description!),
 | 
			
		||||
              PollStatsWidget(question: q, stats: poll.stats),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
      ],
 | 
			
		||||
    ).padding(horizontal: 20, vertical: 16);
 | 
			
		||||
@@ -132,7 +152,7 @@ class _PollHeader extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
class _PollAnswerTile extends StatelessWidget {
 | 
			
		||||
  final SnPollAnswer answer;
 | 
			
		||||
  final SnPoll poll;
 | 
			
		||||
  final SnPollWithStats poll;
 | 
			
		||||
  const _PollAnswerTile({required this.answer, required this.poll});
 | 
			
		||||
 | 
			
		||||
  String _formatPerQuestionAnswer(
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										233
									
								
								lib/widgets/poll/poll_stats_widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								lib/widgets/poll/poll_stats_widget.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,233 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:island/models/poll.dart';
 | 
			
		||||
 | 
			
		||||
class PollStatsWidget extends StatelessWidget {
 | 
			
		||||
  const PollStatsWidget({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.question,
 | 
			
		||||
    required this.stats,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final SnPollQuestion question;
 | 
			
		||||
  final Map<String, dynamic>? stats;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    if (stats == null) return const SizedBox.shrink();
 | 
			
		||||
    final raw = stats![question.id];
 | 
			
		||||
    if (raw == null) return const SizedBox.shrink();
 | 
			
		||||
 | 
			
		||||
    Widget? body;
 | 
			
		||||
 | 
			
		||||
    switch (question.type) {
 | 
			
		||||
      case SnPollQuestionType.rating:
 | 
			
		||||
        // rating: avg score (double or int)
 | 
			
		||||
        final avg = (raw['rating'] as num?)?.toDouble();
 | 
			
		||||
        if (avg == null) break;
 | 
			
		||||
        final theme = Theme.of(context);
 | 
			
		||||
        body = Row(
 | 
			
		||||
          mainAxisAlignment: MainAxisAlignment.start,
 | 
			
		||||
          children: [
 | 
			
		||||
            Icon(Icons.star, color: Colors.amber.shade600, size: 18),
 | 
			
		||||
            const SizedBox(width: 6),
 | 
			
		||||
            Text(
 | 
			
		||||
              avg.toStringAsFixed(1),
 | 
			
		||||
              style: theme.textTheme.labelMedium?.copyWith(
 | 
			
		||||
                color: theme.colorScheme.onSurfaceVariant,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        );
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      case SnPollQuestionType.yesNo:
 | 
			
		||||
        // yes/no: map {true: count, false: count}
 | 
			
		||||
        if (raw is Map) {
 | 
			
		||||
          final int yes =
 | 
			
		||||
              (raw[true] is int)
 | 
			
		||||
                  ? raw[true] as int
 | 
			
		||||
                  : int.tryParse('${raw[true]}') ?? 0;
 | 
			
		||||
          final int no =
 | 
			
		||||
              (raw[false] is int)
 | 
			
		||||
                  ? raw[false] as int
 | 
			
		||||
                  : int.tryParse('${raw[false]}') ?? 0;
 | 
			
		||||
          final total = (yes + no).clamp(0, 1 << 31);
 | 
			
		||||
          final yesPct = total == 0 ? 0.0 : yes / total;
 | 
			
		||||
          final noPct = total == 0 ? 0.0 : no / total;
 | 
			
		||||
          final theme = Theme.of(context);
 | 
			
		||||
          body = Column(
 | 
			
		||||
            crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
            children: [
 | 
			
		||||
              _BarStatRow(
 | 
			
		||||
                label: 'Yes',
 | 
			
		||||
                count: yes,
 | 
			
		||||
                fraction: yesPct,
 | 
			
		||||
                color: Colors.green.shade600,
 | 
			
		||||
              ),
 | 
			
		||||
              const SizedBox(height: 6),
 | 
			
		||||
              _BarStatRow(
 | 
			
		||||
                label: 'No',
 | 
			
		||||
                count: no,
 | 
			
		||||
                fraction: noPct,
 | 
			
		||||
                color: Colors.red.shade600,
 | 
			
		||||
              ),
 | 
			
		||||
              const SizedBox(height: 4),
 | 
			
		||||
              Text(
 | 
			
		||||
                'Total: $total',
 | 
			
		||||
                style: theme.textTheme.labelSmall?.copyWith(
 | 
			
		||||
                  color: theme.colorScheme.onSurfaceVariant,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      case SnPollQuestionType.singleChoice:
 | 
			
		||||
      case SnPollQuestionType.multipleChoice:
 | 
			
		||||
        // map optionId -> count
 | 
			
		||||
        if (raw is Map) {
 | 
			
		||||
          final options = [...?question.options]
 | 
			
		||||
            ..sort((a, b) => a.order.compareTo(b.order));
 | 
			
		||||
          final List<_OptionCount> items = [];
 | 
			
		||||
          int total = 0;
 | 
			
		||||
          for (final opt in options) {
 | 
			
		||||
            final dynamic v = raw[opt.id];
 | 
			
		||||
            final int count = v is int ? v : int.tryParse('$v') ?? 0;
 | 
			
		||||
            total += count;
 | 
			
		||||
            items.add(_OptionCount(id: opt.id, label: opt.label, count: count));
 | 
			
		||||
          }
 | 
			
		||||
          if (items.isNotEmpty) {
 | 
			
		||||
            items.sort(
 | 
			
		||||
              (a, b) => b.count.compareTo(a.count),
 | 
			
		||||
            ); // show highest first
 | 
			
		||||
          }
 | 
			
		||||
          body = Column(
 | 
			
		||||
            crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
            children: [
 | 
			
		||||
              for (final it in items)
 | 
			
		||||
                Padding(
 | 
			
		||||
                  padding: const EdgeInsets.only(bottom: 6),
 | 
			
		||||
                  child: _BarStatRow(
 | 
			
		||||
                    label: it.label,
 | 
			
		||||
                    count: it.count,
 | 
			
		||||
                    fraction: total == 0 ? 0 : it.count / total,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              if (items.isNotEmpty)
 | 
			
		||||
                Text(
 | 
			
		||||
                  'Total: $total',
 | 
			
		||||
                  style: Theme.of(context).textTheme.labelSmall?.copyWith(
 | 
			
		||||
                    color: Theme.of(context).colorScheme.onSurfaceVariant,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      case SnPollQuestionType.freeText:
 | 
			
		||||
        // No stats
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (body == null) return Text('No stats available');
 | 
			
		||||
 | 
			
		||||
    return Padding(
 | 
			
		||||
      padding: const EdgeInsets.only(top: 8),
 | 
			
		||||
      child: DecoratedBox(
 | 
			
		||||
        decoration: BoxDecoration(
 | 
			
		||||
          color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.35),
 | 
			
		||||
          borderRadius: BorderRadius.circular(8),
 | 
			
		||||
        ),
 | 
			
		||||
        child: Padding(
 | 
			
		||||
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
 | 
			
		||||
          child: Column(
 | 
			
		||||
            crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
            children: [
 | 
			
		||||
              Text(
 | 
			
		||||
                'Stats',
 | 
			
		||||
                style: Theme.of(context).textTheme.labelLarge?.copyWith(
 | 
			
		||||
                  color: Theme.of(context).colorScheme.onSurfaceVariant,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              const SizedBox(height: 8),
 | 
			
		||||
              body,
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _OptionCount {
 | 
			
		||||
  final String id;
 | 
			
		||||
  final String label;
 | 
			
		||||
  final int count;
 | 
			
		||||
  const _OptionCount({
 | 
			
		||||
    required this.id,
 | 
			
		||||
    required this.label,
 | 
			
		||||
    required this.count,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _BarStatRow extends StatelessWidget {
 | 
			
		||||
  const _BarStatRow({
 | 
			
		||||
    required this.label,
 | 
			
		||||
    required this.count,
 | 
			
		||||
    required this.fraction,
 | 
			
		||||
    this.color,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final String label;
 | 
			
		||||
  final int count;
 | 
			
		||||
  final double fraction;
 | 
			
		||||
  final Color? color;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final barColor = color ?? Theme.of(context).colorScheme.primary;
 | 
			
		||||
    final bgColor = Theme.of(
 | 
			
		||||
      context,
 | 
			
		||||
    ).colorScheme.surfaceVariant.withOpacity(0.6);
 | 
			
		||||
    final fg =
 | 
			
		||||
        (fraction.isNaN || fraction.isInfinite)
 | 
			
		||||
            ? 0.0
 | 
			
		||||
            : fraction.clamp(0.0, 1.0);
 | 
			
		||||
 | 
			
		||||
    return Column(
 | 
			
		||||
      crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
      children: [
 | 
			
		||||
        Text('$label · $count', style: Theme.of(context).textTheme.labelMedium),
 | 
			
		||||
        const SizedBox(height: 4),
 | 
			
		||||
        LayoutBuilder(
 | 
			
		||||
          builder: (context, constraints) {
 | 
			
		||||
            final width = constraints.maxWidth;
 | 
			
		||||
            final filled = width * fg;
 | 
			
		||||
            return Stack(
 | 
			
		||||
              children: [
 | 
			
		||||
                Container(
 | 
			
		||||
                  height: 8,
 | 
			
		||||
                  width: width,
 | 
			
		||||
                  decoration: BoxDecoration(
 | 
			
		||||
                    color: bgColor,
 | 
			
		||||
                    borderRadius: BorderRadius.circular(999),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                Container(
 | 
			
		||||
                  height: 8,
 | 
			
		||||
                  width: filled,
 | 
			
		||||
                  decoration: BoxDecoration(
 | 
			
		||||
                    color: barColor,
 | 
			
		||||
                    borderRadius: BorderRadius.circular(999),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +1,11 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
			
		||||
import 'package:island/models/poll.dart';
 | 
			
		||||
import 'package:island/pods/network.dart';
 | 
			
		||||
import 'package:island/widgets/alert.dart';
 | 
			
		||||
import 'package:island/widgets/poll/poll_stats_widget.dart';
 | 
			
		||||
 | 
			
		||||
class PollSubmit extends ConsumerStatefulWidget {
 | 
			
		||||
  const PollSubmit({
 | 
			
		||||
@@ -42,6 +44,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
 | 
			
		||||
  late final List<SnPollQuestion> _questions;
 | 
			
		||||
  int _index = 0;
 | 
			
		||||
  bool _submitting = false;
 | 
			
		||||
  bool _isModifying = false; // New state to track if user is modifying answers
 | 
			
		||||
 | 
			
		||||
  /// Collected answers, keyed by questionId
 | 
			
		||||
  late Map<String, dynamic> _answers;
 | 
			
		||||
@@ -64,6 +67,11 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
 | 
			
		||||
    _answers = Map<String, dynamic>.from(widget.initialAnswers ?? {});
 | 
			
		||||
    if (!widget.isReadonly) {
 | 
			
		||||
      _loadCurrentIntoLocalState();
 | 
			
		||||
      // If initial answers are provided, set _isModifying to false initially
 | 
			
		||||
      // so the "Modify" button is shown.
 | 
			
		||||
      if (widget.initialAnswers != null && widget.initialAnswers!.isNotEmpty) {
 | 
			
		||||
        _isModifying = false;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -81,6 +89,8 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
 | 
			
		||||
        );
 | 
			
		||||
      if (!widget.isReadonly) {
 | 
			
		||||
        _loadCurrentIntoLocalState();
 | 
			
		||||
        // If poll ID changes, reset modification state
 | 
			
		||||
        _isModifying = false;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -203,7 +213,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
 | 
			
		||||
      // Only call onSubmit after server accepts
 | 
			
		||||
      widget.onSubmit(Map<String, dynamic>.unmodifiable(_answers));
 | 
			
		||||
 | 
			
		||||
      showSnackBar('Poll answer has been submitted.');
 | 
			
		||||
      showSnackBar('pollAnswerSubmitted'.tr());
 | 
			
		||||
      HapticFeedback.heavyImpact();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      showErrorAlert(e);
 | 
			
		||||
@@ -266,16 +276,17 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
 | 
			
		||||
                    child: Text(
 | 
			
		||||
                      widget.poll.description!,
 | 
			
		||||
                      style: Theme.of(context).textTheme.bodyMedium?.copyWith(
 | 
			
		||||
                            color: Theme.of(
 | 
			
		||||
                              context,
 | 
			
		||||
                            ).textTheme.bodyMedium?.color?.withOpacity(0.7),
 | 
			
		||||
                          ),
 | 
			
		||||
                        color: Theme.of(
 | 
			
		||||
                          context,
 | 
			
		||||
                        ).textTheme.bodyMedium?.color?.withOpacity(0.7),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        if (widget.showProgress)
 | 
			
		||||
        if (widget.showProgress &&
 | 
			
		||||
            _isModifying) // Only show progress when modifying
 | 
			
		||||
          Text(
 | 
			
		||||
            '${_index + 1} / ${_questions.length}',
 | 
			
		||||
            style: Theme.of(context).textTheme.labelMedium,
 | 
			
		||||
@@ -294,8 +305,8 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
 | 
			
		||||
                child: Text(
 | 
			
		||||
                  '*',
 | 
			
		||||
                  style: Theme.of(context).textTheme.titleMedium?.copyWith(
 | 
			
		||||
                        color: Theme.of(context).colorScheme.error,
 | 
			
		||||
                      ),
 | 
			
		||||
                    color: Theme.of(context).colorScheme.error,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
          ],
 | 
			
		||||
@@ -306,10 +317,10 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
 | 
			
		||||
            child: Text(
 | 
			
		||||
              q.description!,
 | 
			
		||||
              style: Theme.of(context).textTheme.bodySmall?.copyWith(
 | 
			
		||||
                    color: Theme.of(
 | 
			
		||||
                      context,
 | 
			
		||||
                    ).textTheme.bodySmall?.color?.withOpacity(0.7),
 | 
			
		||||
                  ),
 | 
			
		||||
                color: Theme.of(
 | 
			
		||||
                  context,
 | 
			
		||||
                ).textTheme.bodySmall?.color?.withOpacity(0.7),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
      ],
 | 
			
		||||
@@ -317,152 +328,13 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Widget _buildStats(BuildContext context, SnPollQuestion q) {
 | 
			
		||||
    if (widget.stats == null) return const SizedBox.shrink();
 | 
			
		||||
    final raw = widget.stats![q.id];
 | 
			
		||||
    if (raw == null) return const SizedBox.shrink();
 | 
			
		||||
 | 
			
		||||
    Widget? body;
 | 
			
		||||
 | 
			
		||||
    switch (q.type) {
 | 
			
		||||
      case SnPollQuestionType.rating:
 | 
			
		||||
        // rating: avg score (double or int)
 | 
			
		||||
        final avg = (raw['rating'] as num?)?.toDouble();
 | 
			
		||||
        if (avg == null) break;
 | 
			
		||||
        final theme = Theme.of(context);
 | 
			
		||||
        body = Row(
 | 
			
		||||
          mainAxisAlignment: MainAxisAlignment.start,
 | 
			
		||||
          children: [
 | 
			
		||||
            Icon(Icons.star, color: Colors.amber.shade600, size: 18),
 | 
			
		||||
            const SizedBox(width: 6),
 | 
			
		||||
            Text(
 | 
			
		||||
              avg.toStringAsFixed(1),
 | 
			
		||||
              style: theme.textTheme.labelMedium?.copyWith(
 | 
			
		||||
                color: theme.colorScheme.onSurfaceVariant,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        );
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      case SnPollQuestionType.yesNo:
 | 
			
		||||
        // yes/no: map {true: count, false: count}
 | 
			
		||||
        if (raw is Map) {
 | 
			
		||||
          final int yes = (raw[true] is int)
 | 
			
		||||
              ? raw[true] as int
 | 
			
		||||
              : int.tryParse('${raw[true]}') ?? 0;
 | 
			
		||||
          final int no = (raw[false] is int)
 | 
			
		||||
              ? raw[false] as int
 | 
			
		||||
              : int.tryParse('${raw[false]}') ?? 0;
 | 
			
		||||
          final total = (yes + no).clamp(0, 1 << 31);
 | 
			
		||||
          final yesPct = total == 0 ? 0.0 : yes / total;
 | 
			
		||||
          final noPct = total == 0 ? 0.0 : no / total;
 | 
			
		||||
          final theme = Theme.of(context);
 | 
			
		||||
          body = Column(
 | 
			
		||||
            crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
            children: [
 | 
			
		||||
              _BarStatRow(
 | 
			
		||||
                label: 'Yes',
 | 
			
		||||
                count: yes,
 | 
			
		||||
                fraction: yesPct,
 | 
			
		||||
                color: Colors.green.shade600,
 | 
			
		||||
              ),
 | 
			
		||||
              const SizedBox(height: 6),
 | 
			
		||||
              _BarStatRow(
 | 
			
		||||
                label: 'No',
 | 
			
		||||
                count: no,
 | 
			
		||||
                fraction: noPct,
 | 
			
		||||
                color: Colors.red.shade600,
 | 
			
		||||
              ),
 | 
			
		||||
              const SizedBox(height: 4),
 | 
			
		||||
              Text(
 | 
			
		||||
                'Total: $total',
 | 
			
		||||
                style: theme.textTheme.labelSmall?.copyWith(
 | 
			
		||||
                  color: theme.colorScheme.onSurfaceVariant,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      case SnPollQuestionType.singleChoice:
 | 
			
		||||
      case SnPollQuestionType.multipleChoice:
 | 
			
		||||
        // map optionId -> count
 | 
			
		||||
        if (raw is Map) {
 | 
			
		||||
          final options = [...?q.options]
 | 
			
		||||
            ..sort((a, b) => a.order.compareTo(b.order));
 | 
			
		||||
          final List<_OptionCount> items = [];
 | 
			
		||||
          int total = 0;
 | 
			
		||||
          for (final opt in options) {
 | 
			
		||||
            final dynamic v = raw[opt.id];
 | 
			
		||||
            final int count = v is int ? v : int.tryParse('$v') ?? 0;
 | 
			
		||||
            total += count;
 | 
			
		||||
            items.add(_OptionCount(id: opt.id, label: opt.label, count: count));
 | 
			
		||||
          }
 | 
			
		||||
          if (items.isNotEmpty) {
 | 
			
		||||
            items.sort(
 | 
			
		||||
              (a, b) => b.count.compareTo(a.count),
 | 
			
		||||
            ); // show highest first
 | 
			
		||||
          }
 | 
			
		||||
          body = Column(
 | 
			
		||||
            crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
            children: [
 | 
			
		||||
              for (final it in items)
 | 
			
		||||
                Padding(
 | 
			
		||||
                  padding: const EdgeInsets.only(bottom: 6),
 | 
			
		||||
                  child: _BarStatRow(
 | 
			
		||||
                    label: it.label,
 | 
			
		||||
                    count: it.count,
 | 
			
		||||
                    fraction: total == 0 ? 0 : it.count / total,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              if (items.isNotEmpty)
 | 
			
		||||
                Text(
 | 
			
		||||
                  'Total: $total',
 | 
			
		||||
                  style: Theme.of(context).textTheme.labelSmall?.copyWith(
 | 
			
		||||
                        color: Theme.of(context).colorScheme.onSurfaceVariant,
 | 
			
		||||
                      ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      case SnPollQuestionType.freeText:
 | 
			
		||||
        // No stats
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (body == null) return const SizedBox.shrink();
 | 
			
		||||
 | 
			
		||||
    return Padding(
 | 
			
		||||
      padding: const EdgeInsets.only(top: 8),
 | 
			
		||||
      child: DecoratedBox(
 | 
			
		||||
        decoration: BoxDecoration(
 | 
			
		||||
          color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.35),
 | 
			
		||||
          borderRadius: BorderRadius.circular(8),
 | 
			
		||||
        ),
 | 
			
		||||
        child: Padding(
 | 
			
		||||
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
 | 
			
		||||
          child: Column(
 | 
			
		||||
            crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
            children: [
 | 
			
		||||
              Text(
 | 
			
		||||
                'Stats',
 | 
			
		||||
                style: Theme.of(context).textTheme.labelLarge?.copyWith(
 | 
			
		||||
                      color: Theme.of(context).colorScheme.onSurfaceVariant,
 | 
			
		||||
                    ),
 | 
			
		||||
              ),
 | 
			
		||||
              const SizedBox(height: 8),
 | 
			
		||||
              body,
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
    return PollStatsWidget(question: q, stats: widget.stats);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Widget _buildBody(BuildContext context) {
 | 
			
		||||
    if (widget.initialAnswers != null && !widget.isReadonly && !_isModifying) {
 | 
			
		||||
      return const SizedBox.shrink(); // Collapse input fields if already submitted and not modifying
 | 
			
		||||
    }
 | 
			
		||||
    final q = _current;
 | 
			
		||||
    switch (q.type) {
 | 
			
		||||
      case SnPollQuestionType.singleChoice:
 | 
			
		||||
@@ -522,9 +394,9 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
 | 
			
		||||
      children: [
 | 
			
		||||
        Expanded(
 | 
			
		||||
          child: SegmentedButton<bool>(
 | 
			
		||||
            segments: const [
 | 
			
		||||
              ButtonSegment(value: true, label: Text('Yes')),
 | 
			
		||||
              ButtonSegment(value: false, label: Text('No')),
 | 
			
		||||
            segments: [
 | 
			
		||||
              ButtonSegment(value: true, label: Text('yes'.tr())),
 | 
			
		||||
              ButtonSegment(value: false, label: Text('no'.tr())),
 | 
			
		||||
            ],
 | 
			
		||||
            selected: _yesNoSelected == null ? {} : {_yesNoSelected!},
 | 
			
		||||
            onSelectionChanged: (sel) {
 | 
			
		||||
@@ -573,29 +445,135 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
 | 
			
		||||
    final isLast = _index == _questions.length - 1;
 | 
			
		||||
    final canProceed = _isCurrentAnswered() && !_submitting;
 | 
			
		||||
 | 
			
		||||
    if (widget.initialAnswers != null && !_isModifying && !widget.isReadonly) {
 | 
			
		||||
      // If poll is submitted and not in modification mode, show "Modify" button
 | 
			
		||||
      return FilledButton.icon(
 | 
			
		||||
        icon: const Icon(Icons.edit),
 | 
			
		||||
        label: Text('modifyAnswers'.tr()),
 | 
			
		||||
        onPressed: () {
 | 
			
		||||
          setState(() {
 | 
			
		||||
            _isModifying = true;
 | 
			
		||||
            _index = 0; // Reset to first question for modification
 | 
			
		||||
            _loadCurrentIntoLocalState();
 | 
			
		||||
          });
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Row(
 | 
			
		||||
      children: [
 | 
			
		||||
        OutlinedButton.icon(
 | 
			
		||||
          icon: const Icon(Icons.arrow_back),
 | 
			
		||||
          label: Text(_index == 0 ? 'Cancel' : 'Back'),
 | 
			
		||||
          onPressed: _submitting ? null : _back,
 | 
			
		||||
          label: Text(_index == 0 ? 'cancel'.tr() : 'back'.tr()),
 | 
			
		||||
          onPressed:
 | 
			
		||||
              _submitting
 | 
			
		||||
                  ? null
 | 
			
		||||
                  : () {
 | 
			
		||||
                    if (_index == 0 && _isModifying) {
 | 
			
		||||
                      // If at first question and in modification mode, go back to submitted view
 | 
			
		||||
                      setState(() {
 | 
			
		||||
                        _isModifying = false;
 | 
			
		||||
                      });
 | 
			
		||||
                    } else {
 | 
			
		||||
                      _back();
 | 
			
		||||
                    }
 | 
			
		||||
                  },
 | 
			
		||||
        ),
 | 
			
		||||
        const Spacer(),
 | 
			
		||||
        FilledButton.icon(
 | 
			
		||||
          icon: _submitting
 | 
			
		||||
              ? const SizedBox(
 | 
			
		||||
                  width: 16,
 | 
			
		||||
                  height: 16,
 | 
			
		||||
                  child: CircularProgressIndicator(strokeWidth: 2),
 | 
			
		||||
                )
 | 
			
		||||
              : Icon(isLast ? Icons.check : Icons.arrow_forward),
 | 
			
		||||
          label: Text(isLast ? 'Submit' : 'Next'),
 | 
			
		||||
          icon:
 | 
			
		||||
              _submitting
 | 
			
		||||
                  ? const SizedBox(
 | 
			
		||||
                    width: 16,
 | 
			
		||||
                    height: 16,
 | 
			
		||||
                    child: CircularProgressIndicator(strokeWidth: 2),
 | 
			
		||||
                  )
 | 
			
		||||
                  : Icon(isLast ? Icons.check : Icons.arrow_forward),
 | 
			
		||||
          label: Text(isLast ? 'submit'.tr() : 'next'.tr()),
 | 
			
		||||
          onPressed: canProceed ? _next : null,
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Widget _buildSubmittedView(BuildContext context) {
 | 
			
		||||
    return Column(
 | 
			
		||||
      crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
      children: [
 | 
			
		||||
        if (widget.poll.title != null || widget.poll.description != null)
 | 
			
		||||
          Padding(
 | 
			
		||||
            padding: const EdgeInsets.only(bottom: 12),
 | 
			
		||||
            child: Column(
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
              children: [
 | 
			
		||||
                if (widget.poll.title?.isNotEmpty ?? false)
 | 
			
		||||
                  Text(
 | 
			
		||||
                    widget.poll.title!,
 | 
			
		||||
                    style: Theme.of(context).textTheme.titleLarge,
 | 
			
		||||
                  ),
 | 
			
		||||
                if (widget.poll.description?.isNotEmpty ?? false)
 | 
			
		||||
                  Padding(
 | 
			
		||||
                    padding: const EdgeInsets.only(top: 4),
 | 
			
		||||
                    child: Text(
 | 
			
		||||
                      widget.poll.description!,
 | 
			
		||||
                      style: Theme.of(context).textTheme.bodyMedium?.copyWith(
 | 
			
		||||
                        color: Theme.of(
 | 
			
		||||
                          context,
 | 
			
		||||
                        ).textTheme.bodyMedium?.color?.withOpacity(0.7),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        for (final q in _questions)
 | 
			
		||||
          Padding(
 | 
			
		||||
            padding: const EdgeInsets.only(bottom: 16.0),
 | 
			
		||||
            child: Column(
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
              children: [
 | 
			
		||||
                Row(
 | 
			
		||||
                  children: [
 | 
			
		||||
                    Expanded(
 | 
			
		||||
                      child: Text(
 | 
			
		||||
                        q.title,
 | 
			
		||||
                        style: Theme.of(context).textTheme.titleMedium,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    if (q.isRequired)
 | 
			
		||||
                      Padding(
 | 
			
		||||
                        padding: const EdgeInsets.only(left: 8),
 | 
			
		||||
                        child: Text(
 | 
			
		||||
                          '*',
 | 
			
		||||
                          style: Theme.of(
 | 
			
		||||
                            context,
 | 
			
		||||
                          ).textTheme.titleMedium?.copyWith(
 | 
			
		||||
                            color: Theme.of(context).colorScheme.error,
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
                if (q.description != null)
 | 
			
		||||
                  Padding(
 | 
			
		||||
                    padding: const EdgeInsets.only(top: 4),
 | 
			
		||||
                    child: Text(
 | 
			
		||||
                      q.description!,
 | 
			
		||||
                      style: Theme.of(context).textTheme.bodySmall?.copyWith(
 | 
			
		||||
                        color: Theme.of(
 | 
			
		||||
                          context,
 | 
			
		||||
                        ).textTheme.bodySmall?.color?.withOpacity(0.7),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                _buildStats(context, q),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Widget _buildReadonlyView(BuildContext context) {
 | 
			
		||||
    return Column(
 | 
			
		||||
      crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
@@ -617,10 +595,10 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
 | 
			
		||||
                    child: Text(
 | 
			
		||||
                      widget.poll.description!,
 | 
			
		||||
                      style: Theme.of(context).textTheme.bodyMedium?.copyWith(
 | 
			
		||||
                            color: Theme.of(
 | 
			
		||||
                              context,
 | 
			
		||||
                            ).textTheme.bodyMedium?.color?.withOpacity(0.7),
 | 
			
		||||
                          ),
 | 
			
		||||
                        color: Theme.of(
 | 
			
		||||
                          context,
 | 
			
		||||
                        ).textTheme.bodyMedium?.color?.withOpacity(0.7),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
              ],
 | 
			
		||||
@@ -645,9 +623,11 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
 | 
			
		||||
                        padding: const EdgeInsets.only(left: 8),
 | 
			
		||||
                        child: Text(
 | 
			
		||||
                          '*',
 | 
			
		||||
                          style: Theme.of(context).textTheme.titleMedium?.copyWith(
 | 
			
		||||
                                color: Theme.of(context).colorScheme.error,
 | 
			
		||||
                              ),
 | 
			
		||||
                          style: Theme.of(
 | 
			
		||||
                            context,
 | 
			
		||||
                          ).textTheme.titleMedium?.copyWith(
 | 
			
		||||
                            color: Theme.of(context).colorScheme.error,
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                  ],
 | 
			
		||||
@@ -658,10 +638,10 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
 | 
			
		||||
                    child: Text(
 | 
			
		||||
                      q.description!,
 | 
			
		||||
                      style: Theme.of(context).textTheme.bodySmall?.copyWith(
 | 
			
		||||
                            color: Theme.of(
 | 
			
		||||
                              context,
 | 
			
		||||
                            ).textTheme.bodySmall?.color?.withOpacity(0.7),
 | 
			
		||||
                          ),
 | 
			
		||||
                        color: Theme.of(
 | 
			
		||||
                          context,
 | 
			
		||||
                        ).textTheme.bodySmall?.color?.withOpacity(0.7),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                _buildStats(context, q),
 | 
			
		||||
@@ -678,6 +658,15 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
 | 
			
		||||
      return const SizedBox.shrink();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // If poll is already submitted and not in readonly mode, and not in modification mode, show submitted view
 | 
			
		||||
    if (widget.initialAnswers != null && !widget.isReadonly && !_isModifying) {
 | 
			
		||||
      return Column(
 | 
			
		||||
        crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
			
		||||
        children: [_buildSubmittedView(context), _buildNavBar(context)],
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // If poll is in readonly mode, show readonly view
 | 
			
		||||
    if (widget.isReadonly) {
 | 
			
		||||
      return _buildReadonlyView(context);
 | 
			
		||||
    }
 | 
			
		||||
@@ -701,76 +690,6 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _OptionCount {
 | 
			
		||||
  final String id;
 | 
			
		||||
  final String label;
 | 
			
		||||
  final int count;
 | 
			
		||||
  const _OptionCount({
 | 
			
		||||
    required this.id,
 | 
			
		||||
    required this.label,
 | 
			
		||||
    required this.count,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _BarStatRow extends StatelessWidget {
 | 
			
		||||
  const _BarStatRow({
 | 
			
		||||
    required this.label,
 | 
			
		||||
    required this.count,
 | 
			
		||||
    required this.fraction,
 | 
			
		||||
    this.color,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final String label;
 | 
			
		||||
  final int count;
 | 
			
		||||
  final double fraction;
 | 
			
		||||
  final Color? color;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final barColor = color ?? Theme.of(context).colorScheme.primary;
 | 
			
		||||
    final bgColor = Theme.of(
 | 
			
		||||
      context,
 | 
			
		||||
    ).colorScheme.surfaceVariant.withOpacity(0.6);
 | 
			
		||||
    final fg = (fraction.isNaN || fraction.isInfinite)
 | 
			
		||||
        ? 0.0
 | 
			
		||||
        : fraction.clamp(0.0, 1.0);
 | 
			
		||||
 | 
			
		||||
    return Column(
 | 
			
		||||
      crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
      children: [
 | 
			
		||||
        Text('$label · $count', style: Theme.of(context).textTheme.labelMedium),
 | 
			
		||||
        const SizedBox(height: 4),
 | 
			
		||||
        LayoutBuilder(
 | 
			
		||||
          builder: (context, constraints) {
 | 
			
		||||
            final width = constraints.maxWidth;
 | 
			
		||||
            final filled = width * fg;
 | 
			
		||||
            return Stack(
 | 
			
		||||
              children: [
 | 
			
		||||
                Container(
 | 
			
		||||
                  height: 8,
 | 
			
		||||
                  width: width,
 | 
			
		||||
                  decoration: BoxDecoration(
 | 
			
		||||
                    color: bgColor,
 | 
			
		||||
                    borderRadius: BorderRadius.circular(999),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                Container(
 | 
			
		||||
                  height: 8,
 | 
			
		||||
                  width: filled,
 | 
			
		||||
                  decoration: BoxDecoration(
 | 
			
		||||
                    color: barColor,
 | 
			
		||||
                    borderRadius: BorderRadius.circular(999),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Simple fade/slide transition between questions.
 | 
			
		||||
class _AnimatedStep extends StatelessWidget {
 | 
			
		||||
  const _AnimatedStep({super.key, required this.child});
 | 
			
		||||
@@ -794,4 +713,4 @@ class _AnimatedStep extends StatelessWidget {
 | 
			
		||||
      child: child,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -186,10 +186,9 @@ class ComposePollSheet extends HookConsumerWidget {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Widget? _buildPollSubtitle(SnPoll poll) {
 | 
			
		||||
  Widget? _buildPollSubtitle(SnPollWithStats poll) {
 | 
			
		||||
    try {
 | 
			
		||||
      final SnPoll dyn = poll;
 | 
			
		||||
      final List<SnPollQuestion> options = dyn.questions;
 | 
			
		||||
      final List<SnPollQuestion> options = poll.questions;
 | 
			
		||||
      if (options.isEmpty) return null;
 | 
			
		||||
      final preview = options.take(3).map((e) => e.title).join(' · ');
 | 
			
		||||
      if (preview.trim().isEmpty) return null;
 | 
			
		||||
 
 | 
			
		||||
@@ -211,13 +211,14 @@ class PostActionableItem extends HookConsumerWidget {
 | 
			
		||||
                );
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
            MenuAction(
 | 
			
		||||
              title: 'sharePostPhoto'.tr(),
 | 
			
		||||
              image: MenuImage.icon(Symbols.share_reviews),
 | 
			
		||||
              callback: () {
 | 
			
		||||
                shareAsScreenshot();
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
            if (!kIsWeb)
 | 
			
		||||
              MenuAction(
 | 
			
		||||
                title: 'sharePostPhoto'.tr(),
 | 
			
		||||
                image: MenuImage.icon(Symbols.share_reviews),
 | 
			
		||||
                callback: () {
 | 
			
		||||
                  shareAsScreenshot();
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
            MenuSeparator(),
 | 
			
		||||
            MenuAction(
 | 
			
		||||
              title: 'abuseReport'.tr(),
 | 
			
		||||
@@ -353,6 +354,7 @@ class PostItem extends HookConsumerWidget {
 | 
			
		||||
    final translatedWidget =
 | 
			
		||||
        (translatedText.value?.isNotEmpty ?? false)
 | 
			
		||||
            ? Column(
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
			
		||||
              children: [
 | 
			
		||||
                Row(
 | 
			
		||||
                  children: [
 | 
			
		||||
@@ -397,6 +399,7 @@ class PostItem extends HookConsumerWidget {
 | 
			
		||||
            : null;
 | 
			
		||||
 | 
			
		||||
    final translationSection = Column(
 | 
			
		||||
      crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
			
		||||
      children: [
 | 
			
		||||
        if (translatedWidget != null) translatedWidget,
 | 
			
		||||
        if (translatableWidget != null) translatableWidget,
 | 
			
		||||
 
 | 
			
		||||
@@ -639,13 +639,18 @@ class PostBody extends ConsumerWidget {
 | 
			
		||||
        if (!isFullPost && item.type == 1)
 | 
			
		||||
          Container(
 | 
			
		||||
            decoration: BoxDecoration(
 | 
			
		||||
              color: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
			
		||||
              border: Border.all(
 | 
			
		||||
                color: Theme.of(context).dividerColor.withOpacity(0.5),
 | 
			
		||||
              ),
 | 
			
		||||
              borderRadius: const BorderRadius.all(Radius.circular(16)),
 | 
			
		||||
              borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
            ),
 | 
			
		||||
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
 | 
			
		||||
            margin: const EdgeInsets.only(top: 4),
 | 
			
		||||
            margin: EdgeInsets.only(
 | 
			
		||||
              top: 4,
 | 
			
		||||
              left: renderingPadding.horizontal,
 | 
			
		||||
              right: renderingPadding.vertical,
 | 
			
		||||
            ),
 | 
			
		||||
            child: Column(
 | 
			
		||||
              mainAxisSize: MainAxisSize.min,
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										66
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										66
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -301,10 +301,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: connectivity_plus
 | 
			
		||||
      sha256: "051849e2bd7c7b3bc5844ea0d096609ddc3a859890ec3a9ac4a65a2620cc1f99"
 | 
			
		||||
      sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "6.1.4"
 | 
			
		||||
    version: "6.1.5"
 | 
			
		||||
  connectivity_plus_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -313,6 +313,14 @@ packages:
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.0.1"
 | 
			
		||||
  console:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: console
 | 
			
		||||
      sha256: e04e7824384c5b39389acdd6dc7d33f3efe6b232f6f16d7626f194f6a01ad69a
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "4.1.0"
 | 
			
		||||
  convert:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -405,10 +413,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: dart_webrtc
 | 
			
		||||
      sha256: a2ae542cdadc21359022adedc26138fa3487cc3b3547c24ff4f556681869e28c
 | 
			
		||||
      sha256: "3bfa069a8b14a53ba506f6dd529e9b88c878ba0cc238f311051a39bf1e53d075"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.5.3+hotfix.4"
 | 
			
		||||
    version: "1.5.3+hotfix.5"
 | 
			
		||||
  dbus:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -557,10 +565,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: file_picker
 | 
			
		||||
      sha256: "970d33d79e1da667b6da222575fd7f2e30e323ca76251504477e6d51405b2d9a"
 | 
			
		||||
      sha256: ef7d2a085c1b1d69d17b6842d0734aad90156de08df6bd3c12496d0bd6ddf8e2
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "10.2.4"
 | 
			
		||||
    version: "10.3.1"
 | 
			
		||||
  file_selector_linux:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -746,10 +754,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_hooks
 | 
			
		||||
      sha256: b772e710d16d7a20c0740c4f855095026b31c7eb5ba3ab67d2bd52021cd9461d
 | 
			
		||||
      sha256: c3df76c62bb3a9f9bee75c57cdab40abab6123b734c1cd7e9b26a5dbd436eceb
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.21.2"
 | 
			
		||||
    version: "0.21.3"
 | 
			
		||||
  flutter_inappwebview:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1109,6 +1117,14 @@ packages:
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.0.1"
 | 
			
		||||
  get_it:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: get_it
 | 
			
		||||
      sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "8.2.0"
 | 
			
		||||
  glob:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1581,6 +1597,14 @@ packages:
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.0.0"
 | 
			
		||||
  msix:
 | 
			
		||||
    dependency: "direct dev"
 | 
			
		||||
    description:
 | 
			
		||||
      name: msix
 | 
			
		||||
      sha256: f88033fcb9e0dd8de5b18897cbebbd28ea30596810f4a7c86b12b0c03ace87e5
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.16.12"
 | 
			
		||||
  native_exif:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1625,18 +1649,18 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: package_info_plus
 | 
			
		||||
      sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
 | 
			
		||||
      sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "8.3.0"
 | 
			
		||||
    version: "8.3.1"
 | 
			
		||||
  package_info_plus_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: package_info_plus_platform_interface
 | 
			
		||||
      sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
 | 
			
		||||
      sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.2.0"
 | 
			
		||||
    version: "3.2.1"
 | 
			
		||||
  palette_generator:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1809,10 +1833,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: protobuf
 | 
			
		||||
      sha256: "6153efcc92a06910918f3db8231fd2cf828ac81e50ebd87adc8f8a8cb3caff0e"
 | 
			
		||||
      sha256: de9c9eb2c33f8e933a42932fe1dc504800ca45ebc3d673e6ed7f39754ee4053e
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "4.1.1"
 | 
			
		||||
    version: "4.2.0"
 | 
			
		||||
  provider:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -2049,18 +2073,18 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: share_plus
 | 
			
		||||
      sha256: b2961506569e28948d75ec346c28775bb111986bb69dc6a20754a457e3d97fa0
 | 
			
		||||
      sha256: d7dc0630a923883c6328ca31b89aa682bacbf2f8304162d29f7c6aaff03a27a1
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "11.0.0"
 | 
			
		||||
    version: "11.1.0"
 | 
			
		||||
  share_plus_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: share_plus_platform_interface
 | 
			
		||||
      sha256: "1032d392bc5d2095a77447a805aa3f804d2ae6a4d5eef5e6ebb3bd94c1bc19ef"
 | 
			
		||||
      sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "6.0.0"
 | 
			
		||||
    version: "6.1.0"
 | 
			
		||||
  shared_preferences:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -2182,10 +2206,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: source_helper
 | 
			
		||||
      sha256: "4f81479fe5194a622cdd1713fe1ecb683a6e6c85cd8cec8e2e35ee5ab3fdf2a1"
 | 
			
		||||
      sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.3.6"
 | 
			
		||||
    version: "1.3.7"
 | 
			
		||||
  source_span:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -2694,4 +2718,4 @@ packages:
 | 
			
		||||
    version: "3.1.3"
 | 
			
		||||
sdks:
 | 
			
		||||
  dart: ">=3.8.0 <4.0.0"
 | 
			
		||||
  flutter: ">=3.29.0"
 | 
			
		||||
  flutter: ">=3.32.0"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								pubspec.yaml
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								pubspec.yaml
									
									
									
									
									
								
							@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
 | 
			
		||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
			
		||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
			
		||||
# of the product and file versions while build-number is used as the build suffix.
 | 
			
		||||
version: 3.2.0+124
 | 
			
		||||
version: 3.2.0+125
 | 
			
		||||
 | 
			
		||||
environment:
 | 
			
		||||
  sdk: ^3.7.2
 | 
			
		||||
@@ -36,7 +36,7 @@ dependencies:
 | 
			
		||||
  # The following adds the Cupertino Icons font to your application.
 | 
			
		||||
  # Use with the CupertinoIcons class for iOS style icons.
 | 
			
		||||
  cupertino_icons: ^1.0.8
 | 
			
		||||
  flutter_hooks: ^0.21.2
 | 
			
		||||
  flutter_hooks: ^0.21.3
 | 
			
		||||
  hooks_riverpod: ^2.6.1
 | 
			
		||||
  bitsdojo_window: ^0.1.6
 | 
			
		||||
  go_router: ^16.1.0
 | 
			
		||||
@@ -67,13 +67,13 @@ dependencies:
 | 
			
		||||
  easy_localization: ^3.0.8
 | 
			
		||||
  flutter_inappwebview: ^6.1.5
 | 
			
		||||
  animations: ^2.0.11
 | 
			
		||||
  package_info_plus: ^8.3.0
 | 
			
		||||
  package_info_plus: ^8.3.1
 | 
			
		||||
  device_info_plus: ^11.5.0
 | 
			
		||||
  tus_client_dart:
 | 
			
		||||
    git: https://github.com/LittleSheep2Code/tus_client.git
 | 
			
		||||
  cross_file: ^0.3.4+2
 | 
			
		||||
  image_picker: ^1.1.2
 | 
			
		||||
  file_picker: ^10.2.4
 | 
			
		||||
  file_picker: ^10.3.1
 | 
			
		||||
  riverpod_annotation: ^2.6.1
 | 
			
		||||
  image_picker_platform_interface: ^2.10.1
 | 
			
		||||
  image_picker_android: ^0.8.12+25
 | 
			
		||||
@@ -121,7 +121,7 @@ dependencies:
 | 
			
		||||
  local_auth: ^2.3.0
 | 
			
		||||
  flutter_secure_storage: ^9.2.4
 | 
			
		||||
  flutter_math_fork: ^0.7.4
 | 
			
		||||
  share_plus: ^11.0.0
 | 
			
		||||
  share_plus: ^11.1.0
 | 
			
		||||
  receive_sharing_intent: ^1.8.1
 | 
			
		||||
  top_snackbar_flutter: ^3.3.0
 | 
			
		||||
  textfield_tags:
 | 
			
		||||
@@ -157,6 +157,7 @@ dev_dependencies:
 | 
			
		||||
  riverpod_lint: ^2.6.5
 | 
			
		||||
  drift_dev: ^2.28.0
 | 
			
		||||
  flutter_launcher_icons: ^0.14.4
 | 
			
		||||
  msix: ^3.16.12
 | 
			
		||||
 | 
			
		||||
# For information on the generic Dart part of this file, see the
 | 
			
		||||
# following page: https://dart.dev/tools/pub/pubspec
 | 
			
		||||
@@ -226,3 +227,11 @@ flutter_native_splash:
 | 
			
		||||
  image_dark: "assets/icons/icon-dark.png"
 | 
			
		||||
  color: "#ffffff"
 | 
			
		||||
  color_dark: "#121212"
 | 
			
		||||
 | 
			
		||||
msix_config:
 | 
			
		||||
  display_name: Solian
 | 
			
		||||
  publisher_display_name: Solsynth LLC
 | 
			
		||||
  identity_name: dev.solian.app
 | 
			
		||||
  msix_version: 3.2.0.0
 | 
			
		||||
  logo_path: .\assets\icons\icon.png
 | 
			
		||||
  capabilities: internetClientServer, location, microphone, webcam
 | 
			
		||||
							
								
								
									
										52
									
								
								setup.iss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								setup.iss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
; ==================================================
 | 
			
		||||
#define AppVersion "3.2.0"
 | 
			
		||||
#define BuildNumber "124"
 | 
			
		||||
; ==================================================
 | 
			
		||||
 | 
			
		||||
#define FullVersion AppVersion + "." + BuildNumber
 | 
			
		||||
 | 
			
		||||
[Setup]
 | 
			
		||||
AppName=Solian
 | 
			
		||||
AppVersion={#AppVersion}
 | 
			
		||||
AppPublisher=Solsynth
 | 
			
		||||
AppPublisherURL=https://solsynth.dev
 | 
			
		||||
AppSupportURL=https://kb.solsynth.dev/zh/solar-network
 | 
			
		||||
AppUpdatesURL=https://github.com/Solsynth/Solian/releases
 | 
			
		||||
AppCopyright=Copyright © 2025 Solsynth
 | 
			
		||||
VersionInfoVersion={#FullVersion}
 | 
			
		||||
UninstallDisplayName=Solian
 | 
			
		||||
UninstallDisplayIcon={app}\Solian.exe
 | 
			
		||||
 | 
			
		||||
DefaultDirName={commonpf}\Solian
 | 
			
		||||
UsePreviousAppDir=no
 | 
			
		||||
 | 
			
		||||
OutputDir=.\Installer
 | 
			
		||||
OutputBaseFilename=windows-x86_64-setup
 | 
			
		||||
SetupIconFile=.\assets\icons\icon.ico  
 | 
			
		||||
 | 
			
		||||
Compression=lzma2/ultra64
 | 
			
		||||
SolidCompression=yes
 | 
			
		||||
LZMAUseSeparateProcess=yes
 | 
			
		||||
LZMANumBlockThreads=4
 | 
			
		||||
 | 
			
		||||
ArchitecturesAllowed=x64compatible
 | 
			
		||||
PrivilegesRequired=admin
 | 
			
		||||
 | 
			
		||||
[Files]
 | 
			
		||||
Source: ".\build\windows\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
 | 
			
		||||
 | 
			
		||||
[Icons]
 | 
			
		||||
Name: "{group}\Solian"; Filename: "{app}\Solian.exe";IconFilename: "{app}\Solian.exe"
 | 
			
		||||
Name: "{group}\{cm:UninstallProgram,Solian}"; Filename: "{uninstallexe}"
 | 
			
		||||
Name: "{autodesktop}\Solian"; Filename: "{app}\Solian.exe"; Tasks: desktopicon
 | 
			
		||||
 | 
			
		||||
[Tasks]
 | 
			
		||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
 | 
			
		||||
 | 
			
		||||
[Run]
 | 
			
		||||
Filename: "{app}\Solian.exe"; Description: "Launch Solian"; Flags: nowait postinstall skipifsilent
 | 
			
		||||
 | 
			
		||||
[UninstallDelete]
 | 
			
		||||
Type: filesandordirs; Name: "{userappdata}\dev.solsynth\Solian"
 | 
			
		||||
Type: files; Name: "{group}\Solian.lnk" ;
 | 
			
		||||
Type: files; Name: "{autodesktop}\Solian.lnk" ;
 | 
			
		||||
		Reference in New Issue
	
	Block a user