Compare commits
	
		
			18 Commits
		
	
	
		
			8236d31ecc
			...
			3.2.0+125
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fbfe8cbdee | |||
| fbbab0a981 | |||
| ae2fb3b303 | |||
| 3d7a4666ed | |||
| 5d3e0fb800 | |||
| 85ff52a661 | |||
| da7fd64a43 | |||
| 3902633217 | |||
| f478ea8b84 | |||
| 0f481aff5b | |||
| 7a31663310 | |||
| 0239c53c04 | |||
| 16987c758e | |||
| 3a36915140 | |||
| 4bde708878 | |||
| 2f0cf560f8 | |||
| cf355a95fd | |||
| 2f43073172 | 
							
								
								
									
										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", | ||||
| @@ -793,5 +794,47 @@ | ||||
|   "joinedAt": "Joined at {}", | ||||
|   "searchAccounts": "Search accounts...", | ||||
|   "webFeeds": "Web Feeds", | ||||
|   "polls": "Polls" | ||||
| } | ||||
|   "polls": "Polls", | ||||
|   "sharePostSlogan": "Explore more on the Solar Network", | ||||
|   "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'; | ||||
|   | ||||
| @@ -2,12 +2,13 @@ import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| 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'; | ||||
| @@ -262,6 +263,10 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|     } | ||||
|  | ||||
|     final user = ref.watch(userInfoProvider); | ||||
|     final isCurrentUser = useMemoized( | ||||
|       () => user.value?.id == account.value?.id, | ||||
|       [user, account], | ||||
|     ); | ||||
|  | ||||
|     Widget accountBasicInfo(SnAccount data) => Padding( | ||||
|       padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), | ||||
| @@ -589,7 +594,7 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|                           child: CustomScrollView( | ||||
|                             slivers: [ | ||||
|                               SliverGap(24), | ||||
|                               if (user.value != null) | ||||
|                               if (user.value != null && !isCurrentUser) | ||||
|                                 SliverToBoxAdapter(child: accountAction(data)), | ||||
|                               SliverToBoxAdapter( | ||||
|                                 child: Card( | ||||
| @@ -686,7 +691,7 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|                             data, | ||||
|                           ).padding(horizontal: 4), | ||||
|                         ), | ||||
|                         if (user.value != null) | ||||
|                         if (user.value != null && !isCurrentUser) | ||||
|                           SliverToBoxAdapter( | ||||
|                             child: accountAction(data).padding(horizontal: 4), | ||||
|                           ), | ||||
|   | ||||
| @@ -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'; | ||||
|   | ||||
| @@ -31,6 +31,7 @@ class CloudFileList extends HookConsumerWidget { | ||||
|   final bool disableZoomIn; | ||||
|   final bool disableConstraint; | ||||
|   final EdgeInsets? padding; | ||||
|   final bool isColumn; | ||||
|   const CloudFileList({ | ||||
|     super.key, | ||||
|     required this.files, | ||||
| @@ -40,6 +41,7 @@ class CloudFileList extends HookConsumerWidget { | ||||
|     this.disableZoomIn = false, | ||||
|     this.disableConstraint = false, | ||||
|     this.padding, | ||||
|     this.isColumn = false, | ||||
|   }); | ||||
|  | ||||
|   double calculateAspectRatio() { | ||||
| @@ -63,6 +65,74 @@ class CloudFileList extends HookConsumerWidget { | ||||
|     ); | ||||
|  | ||||
|     if (files.isEmpty) return const SizedBox.shrink(); | ||||
|  | ||||
|     if (isColumn) { | ||||
|       final children = <Widget>[]; | ||||
|       const maxFiles = 2; | ||||
|       final filesToShow = files.take(maxFiles).toList(); | ||||
|  | ||||
|       for (var i = 0; i < filesToShow.length; i++) { | ||||
|         final file = filesToShow[i]; | ||||
|         final isImage = file.mimeType?.startsWith('image') ?? false; | ||||
|         final isAudio = file.mimeType?.startsWith('audio') ?? false; | ||||
|         final widgetItem = ClipRRect( | ||||
|           borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|           child: _CloudFileListEntry( | ||||
|             file: file, | ||||
|             heroTag: heroTags[i], | ||||
|             isImage: isImage, | ||||
|             disableZoomIn: disableZoomIn, | ||||
|             onTap: () { | ||||
|               if (!isImage) { | ||||
|                 return; | ||||
|               } | ||||
|               if (!disableZoomIn) { | ||||
|                 context.pushTransparentRoute( | ||||
|                   CloudFileZoomIn(item: file, heroTag: heroTags[i]), | ||||
|                   rootNavigator: true, | ||||
|                 ); | ||||
|               } | ||||
|             }, | ||||
|           ), | ||||
|         ); | ||||
|  | ||||
|         Widget item; | ||||
|         if (isAudio) { | ||||
|           item = SizedBox(height: 120, child: widgetItem); | ||||
|         } else { | ||||
|           item = AspectRatio( | ||||
|             aspectRatio: file.fileMeta?['ratio'] as double? ?? 1.0, | ||||
|             child: widgetItem, | ||||
|           ); | ||||
|         } | ||||
|         children.add(item); | ||||
|         if (i < filesToShow.length - 1) { | ||||
|           children.add(const Gap(8)); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (files.length > maxFiles) { | ||||
|         children.add(const Gap(8)); | ||||
|         children.add( | ||||
|           Text( | ||||
|             'filesListAdditional'.plural(files.length - filesToShow.length), | ||||
|             textAlign: TextAlign.center, | ||||
|             style: Theme.of(context).textTheme.bodyMedium?.copyWith( | ||||
|               color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), | ||||
|             ), | ||||
|           ), | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       return Padding( | ||||
|         padding: padding ?? EdgeInsets.zero, | ||||
|         child: Column( | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|           children: children, | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|     if (files.length == 1) { | ||||
|       final isImage = files.first.mimeType?.startsWith('image') ?? false; | ||||
|       final isAudio = files.first.mimeType?.startsWith('audio') ?? false; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -23,7 +23,6 @@ import 'package:island/widgets/safety/abuse_report_helper.dart'; | ||||
| import 'package:island/widgets/share/share_sheet.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:path_provider/path_provider.dart' show getTemporaryDirectory; | ||||
| import 'package:relative_time/relative_time.dart'; | ||||
| import 'package:screenshot/screenshot.dart'; | ||||
| import 'package:share_plus/share_plus.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| @@ -103,13 +102,13 @@ class PostActionableItem extends HookConsumerWidget { | ||||
|                 textDirection: TextDirection.ltr, | ||||
|                 child: SizedBox( | ||||
|                   width: 520, | ||||
|                   height: 640, | ||||
|                   child: PostItemScreenshot(item: item, isFullPost: isFullPost), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             context: context, | ||||
|             pixelRatio: MediaQuery.of(context).devicePixelRatio, | ||||
|             delay: const Duration(seconds: 1), | ||||
|           ) | ||||
|           .then((Uint8List? image) async { | ||||
|             if (image == null) return; | ||||
| @@ -212,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(), | ||||
| @@ -354,6 +354,7 @@ class PostItem extends HookConsumerWidget { | ||||
|     final translatedWidget = | ||||
|         (translatedText.value?.isNotEmpty ?? false) | ||||
|             ? Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|               children: [ | ||||
|                 Row( | ||||
|                   children: [ | ||||
| @@ -398,6 +399,7 @@ class PostItem extends HookConsumerWidget { | ||||
|             : null; | ||||
|  | ||||
|     final translationSection = Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|       children: [ | ||||
|         if (translatedWidget != null) translatedWidget, | ||||
|         if (translatableWidget != null) translatableWidget, | ||||
| @@ -468,7 +470,8 @@ class PostItem extends HookConsumerWidget { | ||||
|           translationSection: translationSection, | ||||
|           renderingPadding: renderingPadding, | ||||
|         ), | ||||
|         if (isShowReference) ReferencedPostWidget(item: item), | ||||
|         if (isShowReference) | ||||
|           ReferencedPostWidget(item: item, renderingPadding: renderingPadding), | ||||
|         if (item.repliesCount > 0 && isEmbedReply) | ||||
|           PostReplyPreview( | ||||
|             parent: item, | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
| import 'package:island/widgets/post/post_shared.dart'; | ||||
| import 'package:qr_flutter/qr_flutter.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| class PostItemScreenshot extends ConsumerWidget { | ||||
|   final SnPost item; | ||||
| @@ -31,42 +34,102 @@ class PostItemScreenshot extends ConsumerWidget { | ||||
|                 .map((e) => e.key) | ||||
|                 .last; | ||||
|  | ||||
|     return Column( | ||||
|       mainAxisSize: MainAxisSize.min, | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         PostHeader( | ||||
|           item: item, | ||||
|           isFullPost: isFullPost, | ||||
|           isInteractive: false, | ||||
|           renderingPadding: renderingPadding, | ||||
|           trailing: | ||||
|               mostReaction != null | ||||
|                   ? Row( | ||||
|     final isDark = MediaQuery.of(context).platformBrightness == Brightness.dark; | ||||
|  | ||||
|     return Material( | ||||
|       elevation: 0, | ||||
|       color: Theme.of(context).colorScheme.surface, | ||||
|       child: Column( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           Gap(renderingPadding.vertical), | ||||
|           PostHeader( | ||||
|             item: item, | ||||
|             isFullPost: isFullPost, | ||||
|             isInteractive: false, | ||||
|             renderingPadding: renderingPadding, | ||||
|             isRelativeTime: false, | ||||
|             trailing: | ||||
|                 mostReaction != null | ||||
|                     ? Row( | ||||
|                       children: [ | ||||
|                         Text( | ||||
|                           kReactionTemplates[mostReaction]?.icon ?? '', | ||||
|                           style: const TextStyle(fontSize: 20), | ||||
|                         ), | ||||
|                         const Gap(4), | ||||
|                         Text( | ||||
|                           'x${item.reactionsCount[mostReaction]}', | ||||
|                           style: const TextStyle(fontSize: 11), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ) | ||||
|                     : null, | ||||
|           ), | ||||
|           PostBody( | ||||
|             item: item, | ||||
|             renderingPadding: renderingPadding, | ||||
|             isFullPost: isFullPost, | ||||
|             isTextSelectable: false, | ||||
|             isInteractive: false, | ||||
|           ), | ||||
|           if (isShowReference) | ||||
|             ReferencedPostWidget( | ||||
|               item: item, | ||||
|               isInteractive: false, | ||||
|               renderingPadding: renderingPadding, | ||||
|             ), | ||||
|           Container( | ||||
|             color: Theme.of(context).colorScheme.surfaceContainerLow, | ||||
|             margin: const EdgeInsets.only(top: 8), | ||||
|             padding: EdgeInsets.symmetric( | ||||
|               horizontal: renderingPadding.horizontal, | ||||
|               vertical: 4, | ||||
|             ), | ||||
|             child: Row( | ||||
|               children: [ | ||||
|                 SizedBox( | ||||
|                   width: 44, | ||||
|                   height: 44, | ||||
|                   child: Image.asset( | ||||
|                     'assets/icons/icon${isDark ? '-dark' : ''}.png', | ||||
|                     width: 40, | ||||
|                     height: 40, | ||||
|                   ), | ||||
|                 ).padding(vertical: 8, right: 12), | ||||
|                 Expanded( | ||||
|                   child: Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                     children: [ | ||||
|                       Text( | ||||
|                         kReactionTemplates[mostReaction]?.icon ?? '', | ||||
|                         style: const TextStyle(fontSize: 20), | ||||
|                       ), | ||||
|                       const Gap(4), | ||||
|                       Text( | ||||
|                         'x${item.reactionsCount[mostReaction]}', | ||||
|                         style: const TextStyle(fontSize: 11), | ||||
|                       const Text( | ||||
|                         'Solar Network', | ||||
|                         style: TextStyle( | ||||
|                           fontSize: 14, | ||||
|                           fontWeight: FontWeight.bold, | ||||
|                         ), | ||||
|                       ), | ||||
|                       const Text( | ||||
|                         'sharePostSlogan', | ||||
|                         style: TextStyle(fontSize: 12), | ||||
|                       ).tr().opacity(0.9), | ||||
|                     ], | ||||
|                   ) | ||||
|                   : null, | ||||
|         ), | ||||
|         PostBody( | ||||
|           item: item, | ||||
|           renderingPadding: renderingPadding, | ||||
|           isFullPost: isFullPost, | ||||
|           isTextSelectable: false, | ||||
|           isInteractive: false, | ||||
|         ), | ||||
|         if (isShowReference) | ||||
|           ReferencedPostWidget(item: item, isInteractive: false), | ||||
|       ], | ||||
|                   ), | ||||
|                 ), | ||||
|                 QrImageView( | ||||
|                   data: 'https://solian.app/posts/${item.id}', | ||||
|                   version: QrVersions.auto, | ||||
|                   size: 60, | ||||
|                   errorCorrectionLevel: QrErrorCorrectLevel.M, | ||||
|                   backgroundColor: Colors.transparent, | ||||
|                   foregroundColor: Theme.of(context).colorScheme.onSurface, | ||||
|                   padding: const EdgeInsets.all(8), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -348,11 +348,13 @@ class PostTruncateHint extends StatelessWidget { | ||||
| class ReferencedPostWidget extends StatelessWidget { | ||||
|   final SnPost item; | ||||
|   final bool isInteractive; | ||||
|   final EdgeInsets renderingPadding; | ||||
|  | ||||
|   const ReferencedPostWidget({ | ||||
|     super.key, | ||||
|     required this.item, | ||||
|     this.isInteractive = true, | ||||
|     this.renderingPadding = EdgeInsets.zero, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
| @@ -363,8 +365,15 @@ class ReferencedPostWidget extends StatelessWidget { | ||||
|     final isReply = item.repliedPost != null; | ||||
|  | ||||
|     final content = Container( | ||||
|       padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), | ||||
|       margin: const EdgeInsets.only(top: 8), | ||||
|       padding: EdgeInsets.symmetric( | ||||
|         horizontal: renderingPadding.horizontal, | ||||
|         vertical: 8, | ||||
|       ), | ||||
|       margin: EdgeInsets.only( | ||||
|         top: 8, | ||||
|         left: renderingPadding.vertical, | ||||
|         right: renderingPadding.vertical, | ||||
|       ), | ||||
|       decoration: BoxDecoration( | ||||
|         color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5), | ||||
|         borderRadius: BorderRadius.circular(12), | ||||
| @@ -522,6 +531,7 @@ class PostHeader extends StatelessWidget { | ||||
|   final Widget? trailing; | ||||
|   final bool isInteractive; | ||||
|   final EdgeInsets renderingPadding; | ||||
|   final bool isRelativeTime; | ||||
|  | ||||
|   const PostHeader({ | ||||
|     super.key, | ||||
| @@ -530,6 +540,7 @@ class PostHeader extends StatelessWidget { | ||||
|     this.trailing, | ||||
|     this.isInteractive = true, | ||||
|     this.renderingPadding = EdgeInsets.zero, | ||||
|     this.isRelativeTime = true, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
| @@ -569,15 +580,21 @@ class PostHeader extends StatelessWidget { | ||||
|                 crossAxisAlignment: CrossAxisAlignment.end, | ||||
|                 children: [ | ||||
|                   Text( | ||||
|                     isFullPost | ||||
|                         ? (item.publishedAt ?? item.createdAt)!.formatSystem() | ||||
|                         : (item.publishedAt ?? item.createdAt)!.formatRelative( | ||||
|                     !isFullPost && isRelativeTime | ||||
|                         ? (item.publishedAt ?? item.createdAt)!.formatRelative( | ||||
|                           context, | ||||
|                         ), | ||||
|                         ) | ||||
|                         : (item.publishedAt ?? item.createdAt)!.formatSystem(), | ||||
|                   ).fontSize(10), | ||||
|                   if (item.editedAt != null) | ||||
|                     Text( | ||||
|                       'editedAt'.tr(args: [item.editedAt!.formatSystem()]), | ||||
|                       'editedAt'.tr( | ||||
|                         args: [ | ||||
|                           !isFullPost && isRelativeTime | ||||
|                               ? item.editedAt!.formatRelative(context) | ||||
|                               : item.editedAt!.formatSystem(), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ).fontSize(10), | ||||
|                   if (item.visibility != 0) | ||||
|                     Text( | ||||
| @@ -622,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, | ||||
| @@ -711,6 +733,7 @@ class PostBody extends ConsumerWidget { | ||||
|         if (item.attachments.isNotEmpty && item.type != 1) | ||||
|           CloudFileList( | ||||
|             files: item.attachments, | ||||
|             isColumn: !isInteractive, | ||||
|             padding: EdgeInsets.symmetric( | ||||
|               horizontal: renderingPadding.horizontal, | ||||
|               vertical: 4, | ||||
|   | ||||
							
								
								
									
										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