Compare commits
	
		
			13 Commits
		
	
	
		
			43dd13bac4
			...
			3.1.0+121
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 808e7dcffa | |||
| 9bed4fa6fb | |||
| e6255a340b | |||
| 78bf319fb7 | |||
| 36a966d582 | |||
| f72b268d36 | |||
| 44ef31034e | |||
| 229dc2186f | |||
| a2f9a1efb4 | |||
| 
						 | 
					823e3c5de6 | ||
| 
						 | 
					faac7bac35 | ||
| 1fac1bfe02 | |||
| 9394b1d9c8 | 
@@ -706,6 +706,7 @@
 | 
				
			|||||||
  "copyToClipboardTooltip": "Copy to clipboard",
 | 
					  "copyToClipboardTooltip": "Copy to clipboard",
 | 
				
			||||||
  "postForwardingTo": "Forwarding to",
 | 
					  "postForwardingTo": "Forwarding to",
 | 
				
			||||||
  "postReplyingTo": "Replying to",
 | 
					  "postReplyingTo": "Replying to",
 | 
				
			||||||
 | 
					  "postReplyPlaceholder": "Post your reply",
 | 
				
			||||||
  "postEditing": "You are editing an existing post",
 | 
					  "postEditing": "You are editing an existing post",
 | 
				
			||||||
  "postArticle": "Article",
 | 
					  "postArticle": "Article",
 | 
				
			||||||
  "aboutDeviceName": "Device Name",
 | 
					  "aboutDeviceName": "Device Name",
 | 
				
			||||||
@@ -787,5 +788,6 @@
 | 
				
			|||||||
  "addLink": "Add link",
 | 
					  "addLink": "Add link",
 | 
				
			||||||
  "linkKey": "Link Name",
 | 
					  "linkKey": "Link Name",
 | 
				
			||||||
  "linkValue": "URL",
 | 
					  "linkValue": "URL",
 | 
				
			||||||
  "debugOptions": "Debug Options"
 | 
					  "debugOptions": "Debug Options",
 | 
				
			||||||
 | 
					  "joinedAt": "Joined at {}"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -73,6 +73,8 @@ PODS:
 | 
				
			|||||||
    - GoogleUtilities/UserDefaults (~> 8.1)
 | 
					    - GoogleUtilities/UserDefaults (~> 8.1)
 | 
				
			||||||
    - nanopb (~> 3.30910.0)
 | 
					    - nanopb (~> 3.30910.0)
 | 
				
			||||||
  - Flutter (1.0.0)
 | 
					  - Flutter (1.0.0)
 | 
				
			||||||
 | 
					  - flutter_app_update (0.0.1):
 | 
				
			||||||
 | 
					    - Flutter
 | 
				
			||||||
  - flutter_inappwebview_ios (0.0.1):
 | 
					  - flutter_inappwebview_ios (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - flutter_inappwebview_ios/Core (= 0.0.1)
 | 
					    - flutter_inappwebview_ios/Core (= 0.0.1)
 | 
				
			||||||
@@ -223,6 +225,7 @@ DEPENDENCIES:
 | 
				
			|||||||
  - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
 | 
					  - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
 | 
				
			||||||
  - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
 | 
					  - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
 | 
				
			||||||
  - Flutter (from `Flutter`)
 | 
					  - Flutter (from `Flutter`)
 | 
				
			||||||
 | 
					  - flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
 | 
				
			||||||
  - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
 | 
					  - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
 | 
				
			||||||
  - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
 | 
					  - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
 | 
				
			||||||
  - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
 | 
					  - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
 | 
				
			||||||
@@ -293,6 +296,8 @@ EXTERNAL SOURCES:
 | 
				
			|||||||
    :path: ".symlinks/plugins/firebase_messaging/ios"
 | 
					    :path: ".symlinks/plugins/firebase_messaging/ios"
 | 
				
			||||||
  Flutter:
 | 
					  Flutter:
 | 
				
			||||||
    :path: Flutter
 | 
					    :path: Flutter
 | 
				
			||||||
 | 
					  flutter_app_update:
 | 
				
			||||||
 | 
					    :path: ".symlinks/plugins/flutter_app_update/ios"
 | 
				
			||||||
  flutter_inappwebview_ios:
 | 
					  flutter_inappwebview_ios:
 | 
				
			||||||
    :path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
 | 
					    :path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
 | 
				
			||||||
  flutter_keyboard_visibility:
 | 
					  flutter_keyboard_visibility:
 | 
				
			||||||
@@ -372,6 +377,7 @@ SPEC CHECKSUMS:
 | 
				
			|||||||
  FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988
 | 
					  FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988
 | 
				
			||||||
  FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde
 | 
					  FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde
 | 
				
			||||||
  Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
 | 
					  Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
 | 
				
			||||||
 | 
					  flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
 | 
				
			||||||
  flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
 | 
					  flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
 | 
				
			||||||
  flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
 | 
					  flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
 | 
				
			||||||
  flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
 | 
					  flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,7 +30,6 @@ import 'package:image_picker_platform_interface/image_picker_platform_interface.
 | 
				
			|||||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
 | 
					import 'package:flutter_native_splash/flutter_native_splash.dart';
 | 
				
			||||||
import 'package:url_launcher/url_launcher_string.dart';
 | 
					import 'package:url_launcher/url_launcher_string.dart';
 | 
				
			||||||
import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect;
 | 
					import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect;
 | 
				
			||||||
import 'package:island/services/update_service.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pragma('vm:entry-point')
 | 
					@pragma('vm:entry-point')
 | 
				
			||||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
 | 
					Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
 | 
				
			||||||
@@ -144,15 +143,6 @@ void main() async {
 | 
				
			|||||||
      ),
 | 
					      ),
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Schedule update check shortly after startup, when a context is available.
 | 
					 | 
				
			||||||
  // Uses the global overlay key to obtain a BuildContext safely.
 | 
					 | 
				
			||||||
  WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
					 | 
				
			||||||
    final ctx = globalOverlay.currentContext;
 | 
					 | 
				
			||||||
    if (ctx != null) {
 | 
					 | 
				
			||||||
      UpdateService().checkForUpdates(ctx);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Router will be provided through Riverpod
 | 
					// Router will be provided through Riverpod
 | 
				
			||||||
@@ -181,6 +171,9 @@ class IslandApp extends HookConsumerWidget {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() {
 | 
					    useEffect(() {
 | 
				
			||||||
 | 
					      if (!kIsWeb && Platform.isLinux) {
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      const channel = MethodChannel('dev.solsynth.solian/notifications');
 | 
					      const channel = MethodChannel('dev.solsynth.solian/notifications');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      Future<void> handleInitialLink() async {
 | 
					      Future<void> handleInitialLink() async {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,6 +25,32 @@ sealed class SnAccount with _$SnAccount {
 | 
				
			|||||||
      _$SnAccountFromJson(json);
 | 
					      _$SnAccountFromJson(json);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@freezed
 | 
				
			||||||
 | 
					sealed class ProfileLink with _$ProfileLink {
 | 
				
			||||||
 | 
					  const factory ProfileLink({required String name, required String url}) =
 | 
				
			||||||
 | 
					      _ProfileLink;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory ProfileLink.fromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
 | 
					      _$ProfileLinkFromJson(json);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ProfileLinkConverter
 | 
				
			||||||
 | 
					    implements JsonConverter<List<ProfileLink>, dynamic> {
 | 
				
			||||||
 | 
					  const ProfileLinkConverter();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  List<ProfileLink> fromJson(dynamic json) {
 | 
				
			||||||
 | 
					    return json is List<dynamic>
 | 
				
			||||||
 | 
					        ? json.map((e) => ProfileLink.fromJson(e)).cast<ProfileLink>().toList()
 | 
				
			||||||
 | 
					        : <ProfileLink>[];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  List<dynamic> toJson(List<ProfileLink> object) {
 | 
				
			||||||
 | 
					    return object.map((e) => e.toJson()).toList();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@freezed
 | 
					@freezed
 | 
				
			||||||
sealed class SnAccountProfile with _$SnAccountProfile {
 | 
					sealed class SnAccountProfile with _$SnAccountProfile {
 | 
				
			||||||
  const factory SnAccountProfile({
 | 
					  const factory SnAccountProfile({
 | 
				
			||||||
@@ -38,7 +64,7 @@ sealed class SnAccountProfile with _$SnAccountProfile {
 | 
				
			|||||||
    @Default('') String location,
 | 
					    @Default('') String location,
 | 
				
			||||||
    @Default('') String timeZone,
 | 
					    @Default('') String timeZone,
 | 
				
			||||||
    DateTime? birthday,
 | 
					    DateTime? birthday,
 | 
				
			||||||
    @Default({}) Map<String, String> links,
 | 
					    @ProfileLinkConverter() @Default([]) List<ProfileLink> links,
 | 
				
			||||||
    DateTime? lastSeenAt,
 | 
					    DateTime? lastSeenAt,
 | 
				
			||||||
    SnAccountBadge? activeBadge,
 | 
					    SnAccountBadge? activeBadge,
 | 
				
			||||||
    required int experience,
 | 
					    required int experience,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -347,10 +347,270 @@ $SnWalletSubscriptionRefCopyWith<$Res>? get perkSubscription {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					mixin _$ProfileLink {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 String get name; String get url;
 | 
				
			||||||
 | 
					/// Create a copy of ProfileLink
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline')
 | 
				
			||||||
 | 
					$ProfileLinkCopyWith<ProfileLink> get copyWith => _$ProfileLinkCopyWithImpl<ProfileLink>(this as ProfileLink, _$identity);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Serializes this ProfileLink to a JSON map.
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					bool operator ==(Object other) {
 | 
				
			||||||
 | 
					  return identical(this, other) || (other.runtimeType == runtimeType&&other is ProfileLink&&(identical(other.name, name) || other.name == name)&&(identical(other.url, url) || other.url == url));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					int get hashCode => Object.hash(runtimeType,name,url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					String toString() {
 | 
				
			||||||
 | 
					  return 'ProfileLink(name: $name, url: $url)';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					abstract mixin class $ProfileLinkCopyWith<$Res>  {
 | 
				
			||||||
 | 
					  factory $ProfileLinkCopyWith(ProfileLink value, $Res Function(ProfileLink) _then) = _$ProfileLinkCopyWithImpl;
 | 
				
			||||||
 | 
					@useResult
 | 
				
			||||||
 | 
					$Res call({
 | 
				
			||||||
 | 
					 String name, String url
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					class _$ProfileLinkCopyWithImpl<$Res>
 | 
				
			||||||
 | 
					    implements $ProfileLinkCopyWith<$Res> {
 | 
				
			||||||
 | 
					  _$ProfileLinkCopyWithImpl(this._self, this._then);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final ProfileLink _self;
 | 
				
			||||||
 | 
					  final $Res Function(ProfileLink) _then;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create a copy of ProfileLink
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? url = null,}) {
 | 
				
			||||||
 | 
					  return _then(_self.copyWith(
 | 
				
			||||||
 | 
					name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,
 | 
				
			||||||
 | 
					  ));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Adds pattern-matching-related methods to [ProfileLink].
 | 
				
			||||||
 | 
					extension ProfileLinkPatterns on ProfileLink {
 | 
				
			||||||
 | 
					/// 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( _ProfileLink value)?  $default,{required TResult orElse(),}){
 | 
				
			||||||
 | 
					final _that = this;
 | 
				
			||||||
 | 
					switch (_that) {
 | 
				
			||||||
 | 
					case _ProfileLink() 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( _ProfileLink value)  $default,){
 | 
				
			||||||
 | 
					final _that = this;
 | 
				
			||||||
 | 
					switch (_that) {
 | 
				
			||||||
 | 
					case _ProfileLink():
 | 
				
			||||||
 | 
					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( _ProfileLink value)?  $default,){
 | 
				
			||||||
 | 
					final _that = this;
 | 
				
			||||||
 | 
					switch (_that) {
 | 
				
			||||||
 | 
					case _ProfileLink() 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 name,  String url)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
				
			||||||
 | 
					switch (_that) {
 | 
				
			||||||
 | 
					case _ProfileLink() when $default != null:
 | 
				
			||||||
 | 
					return $default(_that.name,_that.url);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 name,  String url)  $default,) {final _that = this;
 | 
				
			||||||
 | 
					switch (_that) {
 | 
				
			||||||
 | 
					case _ProfileLink():
 | 
				
			||||||
 | 
					return $default(_that.name,_that.url);}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// 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 name,  String url)?  $default,) {final _that = this;
 | 
				
			||||||
 | 
					switch (_that) {
 | 
				
			||||||
 | 
					case _ProfileLink() when $default != null:
 | 
				
			||||||
 | 
					return $default(_that.name,_that.url);case _:
 | 
				
			||||||
 | 
					  return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					@JsonSerializable()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _ProfileLink implements ProfileLink {
 | 
				
			||||||
 | 
					  const _ProfileLink({required this.name, required this.url});
 | 
				
			||||||
 | 
					  factory _ProfileLink.fromJson(Map<String, dynamic> json) => _$ProfileLinkFromJson(json);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override final  String name;
 | 
				
			||||||
 | 
					@override final  String url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create a copy of ProfileLink
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@override @JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline')
 | 
				
			||||||
 | 
					_$ProfileLinkCopyWith<_ProfileLink> get copyWith => __$ProfileLinkCopyWithImpl<_ProfileLink>(this, _$identity);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					Map<String, dynamic> toJson() {
 | 
				
			||||||
 | 
					  return _$ProfileLinkToJson(this, );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					bool operator ==(Object other) {
 | 
				
			||||||
 | 
					  return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProfileLink&&(identical(other.name, name) || other.name == name)&&(identical(other.url, url) || other.url == url));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					int get hashCode => Object.hash(runtimeType,name,url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					String toString() {
 | 
				
			||||||
 | 
					  return 'ProfileLink(name: $name, url: $url)';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					abstract mixin class _$ProfileLinkCopyWith<$Res> implements $ProfileLinkCopyWith<$Res> {
 | 
				
			||||||
 | 
					  factory _$ProfileLinkCopyWith(_ProfileLink value, $Res Function(_ProfileLink) _then) = __$ProfileLinkCopyWithImpl;
 | 
				
			||||||
 | 
					@override @useResult
 | 
				
			||||||
 | 
					$Res call({
 | 
				
			||||||
 | 
					 String name, String url
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					class __$ProfileLinkCopyWithImpl<$Res>
 | 
				
			||||||
 | 
					    implements _$ProfileLinkCopyWith<$Res> {
 | 
				
			||||||
 | 
					  __$ProfileLinkCopyWithImpl(this._self, this._then);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final _ProfileLink _self;
 | 
				
			||||||
 | 
					  final $Res Function(_ProfileLink) _then;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create a copy of ProfileLink
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? url = null,}) {
 | 
				
			||||||
 | 
					  return _then(_ProfileLink(
 | 
				
			||||||
 | 
					name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,
 | 
				
			||||||
 | 
					  ));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// @nodoc
 | 
					/// @nodoc
 | 
				
			||||||
mixin _$SnAccountProfile {
 | 
					mixin _$SnAccountProfile {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday; Map<String, String> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
 | 
					 String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday;@ProfileLinkConverter() List<ProfileLink> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
 | 
				
			||||||
/// Create a copy of SnAccountProfile
 | 
					/// Create a copy of SnAccountProfile
 | 
				
			||||||
/// with the given fields replaced by the non-null parameter values.
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
@@ -383,7 +643,7 @@ abstract mixin class $SnAccountProfileCopyWith<$Res>  {
 | 
				
			|||||||
  factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl;
 | 
					  factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl;
 | 
				
			||||||
@useResult
 | 
					@useResult
 | 
				
			||||||
$Res call({
 | 
					$Res call({
 | 
				
			||||||
 String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, Map<String, String> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
 | 
					 String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -413,7 +673,7 @@ as String,location: null == location ? _self.location : location // ignore: cast
 | 
				
			|||||||
as String,timeZone: null == timeZone ? _self.timeZone : timeZone // ignore: cast_nullable_to_non_nullable
 | 
					as String,timeZone: null == timeZone ? _self.timeZone : timeZone // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable
 | 
					as String,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as DateTime?,links: null == links ? _self.links : links // ignore: cast_nullable_to_non_nullable
 | 
					as DateTime?,links: null == links ? _self.links : links // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as Map<String, String>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable
 | 
					as List<ProfileLink>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable
 | 
					as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
 | 
					as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable
 | 
					as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
@@ -554,7 +814,7 @@ return $default(_that);case _:
 | 
				
			|||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String firstName,  String middleName,  String lastName,  String bio,  String gender,  String pronouns,  String location,  String timeZone,  DateTime? birthday,  Map<String, String> links,  DateTime? lastSeenAt,  SnAccountBadge? activeBadge,  int experience,  int level,  double levelingProgress,  SnCloudFile? picture,  SnCloudFile? background,  SnVerificationMark? verification,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
					@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String firstName,  String middleName,  String lastName,  String bio,  String gender,  String pronouns,  String location,  String timeZone,  DateTime? birthday, @ProfileLinkConverter()  List<ProfileLink> links,  DateTime? lastSeenAt,  SnAccountBadge? activeBadge,  int experience,  int level,  double levelingProgress,  SnCloudFile? picture,  SnCloudFile? background,  SnVerificationMark? verification,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
				
			||||||
switch (_that) {
 | 
					switch (_that) {
 | 
				
			||||||
case _SnAccountProfile() when $default != null:
 | 
					case _SnAccountProfile() when $default != null:
 | 
				
			||||||
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
 | 
					return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
 | 
				
			||||||
@@ -575,7 +835,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
 | 
				
			|||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String firstName,  String middleName,  String lastName,  String bio,  String gender,  String pronouns,  String location,  String timeZone,  DateTime? birthday,  Map<String, String> links,  DateTime? lastSeenAt,  SnAccountBadge? activeBadge,  int experience,  int level,  double levelingProgress,  SnCloudFile? picture,  SnCloudFile? background,  SnVerificationMark? verification,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this;
 | 
					@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String firstName,  String middleName,  String lastName,  String bio,  String gender,  String pronouns,  String location,  String timeZone,  DateTime? birthday, @ProfileLinkConverter()  List<ProfileLink> links,  DateTime? lastSeenAt,  SnAccountBadge? activeBadge,  int experience,  int level,  double levelingProgress,  SnCloudFile? picture,  SnCloudFile? background,  SnVerificationMark? verification,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this;
 | 
				
			||||||
switch (_that) {
 | 
					switch (_that) {
 | 
				
			||||||
case _SnAccountProfile():
 | 
					case _SnAccountProfile():
 | 
				
			||||||
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);}
 | 
					return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);}
 | 
				
			||||||
@@ -592,7 +852,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
 | 
				
			|||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String firstName,  String middleName,  String lastName,  String bio,  String gender,  String pronouns,  String location,  String timeZone,  DateTime? birthday,  Map<String, String> links,  DateTime? lastSeenAt,  SnAccountBadge? activeBadge,  int experience,  int level,  double levelingProgress,  SnCloudFile? picture,  SnCloudFile? background,  SnVerificationMark? verification,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this;
 | 
					@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String firstName,  String middleName,  String lastName,  String bio,  String gender,  String pronouns,  String location,  String timeZone,  DateTime? birthday, @ProfileLinkConverter()  List<ProfileLink> links,  DateTime? lastSeenAt,  SnAccountBadge? activeBadge,  int experience,  int level,  double levelingProgress,  SnCloudFile? picture,  SnCloudFile? background,  SnVerificationMark? verification,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this;
 | 
				
			||||||
switch (_that) {
 | 
					switch (_that) {
 | 
				
			||||||
case _SnAccountProfile() when $default != null:
 | 
					case _SnAccountProfile() when $default != null:
 | 
				
			||||||
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
 | 
					return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
 | 
				
			||||||
@@ -607,7 +867,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
 | 
				
			|||||||
@JsonSerializable()
 | 
					@JsonSerializable()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _SnAccountProfile implements SnAccountProfile {
 | 
					class _SnAccountProfile implements SnAccountProfile {
 | 
				
			||||||
  const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, final  Map<String, String> links = const {}, this.lastSeenAt, this.activeBadge, required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links;
 | 
					  const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, @ProfileLinkConverter() final  List<ProfileLink> links = const [], this.lastSeenAt, this.activeBadge, required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links;
 | 
				
			||||||
  factory _SnAccountProfile.fromJson(Map<String, dynamic> json) => _$SnAccountProfileFromJson(json);
 | 
					  factory _SnAccountProfile.fromJson(Map<String, dynamic> json) => _$SnAccountProfileFromJson(json);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@override final  String id;
 | 
					@override final  String id;
 | 
				
			||||||
@@ -620,11 +880,11 @@ class _SnAccountProfile implements SnAccountProfile {
 | 
				
			|||||||
@override@JsonKey() final  String location;
 | 
					@override@JsonKey() final  String location;
 | 
				
			||||||
@override@JsonKey() final  String timeZone;
 | 
					@override@JsonKey() final  String timeZone;
 | 
				
			||||||
@override final  DateTime? birthday;
 | 
					@override final  DateTime? birthday;
 | 
				
			||||||
 final  Map<String, String> _links;
 | 
					 final  List<ProfileLink> _links;
 | 
				
			||||||
@override@JsonKey() Map<String, String> get links {
 | 
					@override@JsonKey()@ProfileLinkConverter() List<ProfileLink> get links {
 | 
				
			||||||
  if (_links is EqualUnmodifiableMapView) return _links;
 | 
					  if (_links is EqualUnmodifiableListView) return _links;
 | 
				
			||||||
  // ignore: implicit_dynamic_type
 | 
					  // ignore: implicit_dynamic_type
 | 
				
			||||||
  return EqualUnmodifiableMapView(_links);
 | 
					  return EqualUnmodifiableListView(_links);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@override final  DateTime? lastSeenAt;
 | 
					@override final  DateTime? lastSeenAt;
 | 
				
			||||||
@@ -672,7 +932,7 @@ abstract mixin class _$SnAccountProfileCopyWith<$Res> implements $SnAccountProfi
 | 
				
			|||||||
  factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl;
 | 
					  factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl;
 | 
				
			||||||
@override @useResult
 | 
					@override @useResult
 | 
				
			||||||
$Res call({
 | 
					$Res call({
 | 
				
			||||||
 String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, Map<String, String> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
 | 
					 String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -702,7 +962,7 @@ as String,location: null == location ? _self.location : location // ignore: cast
 | 
				
			|||||||
as String,timeZone: null == timeZone ? _self.timeZone : timeZone // ignore: cast_nullable_to_non_nullable
 | 
					as String,timeZone: null == timeZone ? _self.timeZone : timeZone // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable
 | 
					as String,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as DateTime?,links: null == links ? _self._links : links // ignore: cast_nullable_to_non_nullable
 | 
					as DateTime?,links: null == links ? _self._links : links // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as Map<String, String>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable
 | 
					as List<ProfileLink>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable
 | 
					as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
 | 
					as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable
 | 
					as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -47,6 +47,12 @@ Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) =>
 | 
				
			|||||||
      'deleted_at': instance.deletedAt?.toIso8601String(),
 | 
					      'deleted_at': instance.deletedAt?.toIso8601String(),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					_ProfileLink _$ProfileLinkFromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
 | 
					    _ProfileLink(name: json['name'] as String, url: json['url'] as String);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Map<String, dynamic> _$ProfileLinkToJson(_ProfileLink instance) =>
 | 
				
			||||||
 | 
					    <String, dynamic>{'name': instance.name, 'url': instance.url};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
_SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
 | 
					_SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
    _SnAccountProfile(
 | 
					    _SnAccountProfile(
 | 
				
			||||||
      id: json['id'] as String,
 | 
					      id: json['id'] as String,
 | 
				
			||||||
@@ -63,10 +69,9 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
 | 
				
			|||||||
              ? null
 | 
					              ? null
 | 
				
			||||||
              : DateTime.parse(json['birthday'] as String),
 | 
					              : DateTime.parse(json['birthday'] as String),
 | 
				
			||||||
      links:
 | 
					      links:
 | 
				
			||||||
          (json['links'] as Map<String, dynamic>?)?.map(
 | 
					          json['links'] == null
 | 
				
			||||||
            (k, e) => MapEntry(k, e as String),
 | 
					              ? const []
 | 
				
			||||||
          ) ??
 | 
					              : const ProfileLinkConverter().fromJson(json['links']),
 | 
				
			||||||
          const {},
 | 
					 | 
				
			||||||
      lastSeenAt:
 | 
					      lastSeenAt:
 | 
				
			||||||
          json['last_seen_at'] == null
 | 
					          json['last_seen_at'] == null
 | 
				
			||||||
              ? null
 | 
					              ? null
 | 
				
			||||||
@@ -116,7 +121,7 @@ Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
 | 
				
			|||||||
      'location': instance.location,
 | 
					      'location': instance.location,
 | 
				
			||||||
      'time_zone': instance.timeZone,
 | 
					      'time_zone': instance.timeZone,
 | 
				
			||||||
      'birthday': instance.birthday?.toIso8601String(),
 | 
					      'birthday': instance.birthday?.toIso8601String(),
 | 
				
			||||||
      'links': instance.links,
 | 
					      'links': const ProfileLinkConverter().toJson(instance.links),
 | 
				
			||||||
      'last_seen_at': instance.lastSeenAt?.toIso8601String(),
 | 
					      'last_seen_at': instance.lastSeenAt?.toIso8601String(),
 | 
				
			||||||
      'active_badge': instance.activeBadge?.toJson(),
 | 
					      'active_badge': instance.activeBadge?.toJson(),
 | 
				
			||||||
      'experience': instance.experience,
 | 
					      'experience': instance.experience,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,12 +7,12 @@ import 'package:flutter/services.dart';
 | 
				
			|||||||
import 'package:gap/gap.dart';
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:island/services/udid.native.dart';
 | 
					import 'package:island/services/udid.native.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/alert.dart';
 | 
				
			||||||
import 'package:island/widgets/app_scaffold.dart';
 | 
					import 'package:island/widgets/app_scaffold.dart';
 | 
				
			||||||
import 'package:material_symbols_icons/symbols.dart';
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:package_info_plus/package_info_plus.dart';
 | 
					import 'package:package_info_plus/package_info_plus.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
import 'package:island/services/update_service.dart';
 | 
					import 'package:island/services/update_service.dart';
 | 
				
			||||||
import 'package:island/widgets/content/sheet.dart';
 | 
					 | 
				
			||||||
import 'package:url_launcher/url_launcher.dart';
 | 
					import 'package:url_launcher/url_launcher.dart';
 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:url_launcher/url_launcher_string.dart';
 | 
					import 'package:url_launcher/url_launcher_string.dart';
 | 
				
			||||||
@@ -205,33 +205,16 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
 | 
				
			|||||||
                                // Fetch latest release and show the unified sheet
 | 
					                                // Fetch latest release and show the unified sheet
 | 
				
			||||||
                                final svc = UpdateService();
 | 
					                                final svc = UpdateService();
 | 
				
			||||||
                                // Reuse service fetch + compare to decide content
 | 
					                                // Reuse service fetch + compare to decide content
 | 
				
			||||||
 | 
					                                showLoadingModal(context);
 | 
				
			||||||
                                final release = await svc.fetchLatestRelease();
 | 
					                                final release = await svc.fetchLatestRelease();
 | 
				
			||||||
 | 
					                                if (!context.mounted) return;
 | 
				
			||||||
 | 
					                                hideLoadingModal(context);
 | 
				
			||||||
                                if (release != null) {
 | 
					                                if (release != null) {
 | 
				
			||||||
                                  await svc.showUpdateSheet(context, release);
 | 
					                                  await svc.showUpdateSheet(context, release);
 | 
				
			||||||
                                } else {
 | 
					                                } else {
 | 
				
			||||||
                                  // Fallback: show a simple sheet indicating no info
 | 
					                                  showInfoAlert(
 | 
				
			||||||
                                  // Use your SheetScaffold for consistent styling
 | 
					                                    'Currently cannot get update from the GitHub.',
 | 
				
			||||||
                                  // Show a minimal message
 | 
					                                    'Unable to check for updates',
 | 
				
			||||||
                                  // ignore: use_build_context_synchronously
 | 
					 | 
				
			||||||
                                  showModalBottomSheet(
 | 
					 | 
				
			||||||
                                    context: context,
 | 
					 | 
				
			||||||
                                    isScrollControlled: true,
 | 
					 | 
				
			||||||
                                    useSafeArea: true,
 | 
					 | 
				
			||||||
                                    showDragHandle: true,
 | 
					 | 
				
			||||||
                                    backgroundColor:
 | 
					 | 
				
			||||||
                                        Theme.of(context).colorScheme.surface,
 | 
					 | 
				
			||||||
                                    builder:
 | 
					 | 
				
			||||||
                                        (_) => const SheetScaffold(
 | 
					 | 
				
			||||||
                                          titleText: 'Update',
 | 
					 | 
				
			||||||
                                          child: Center(
 | 
					 | 
				
			||||||
                                            child: Padding(
 | 
					 | 
				
			||||||
                                              padding: EdgeInsets.all(24),
 | 
					 | 
				
			||||||
                                              child: Text(
 | 
					 | 
				
			||||||
                                                'Unable to fetch release info at this time.',
 | 
					 | 
				
			||||||
                                              ),
 | 
					 | 
				
			||||||
                                            ),
 | 
					 | 
				
			||||||
                                          ),
 | 
					 | 
				
			||||||
                                        ),
 | 
					 | 
				
			||||||
                                  );
 | 
					                                  );
 | 
				
			||||||
                                }
 | 
					                                }
 | 
				
			||||||
                              },
 | 
					                              },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,7 @@ import 'package:gap/gap.dart';
 | 
				
			|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:image_picker/image_picker.dart';
 | 
					import 'package:image_picker/image_picker.dart';
 | 
				
			||||||
import 'package:island/models/file.dart';
 | 
					import 'package:island/models/file.dart';
 | 
				
			||||||
 | 
					import 'package:island/models/user.dart';
 | 
				
			||||||
import 'package:island/pods/config.dart';
 | 
					import 'package:island/pods/config.dart';
 | 
				
			||||||
import 'package:island/pods/network.dart';
 | 
					import 'package:island/pods/network.dart';
 | 
				
			||||||
import 'package:island/pods/userinfo.dart';
 | 
					import 'package:island/pods/userinfo.dart';
 | 
				
			||||||
@@ -95,11 +96,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
    final usernameController = useTextEditingController(text: user.value!.name);
 | 
					    final usernameController = useTextEditingController(text: user.value!.name);
 | 
				
			||||||
    final nicknameController = useTextEditingController(text: user.value!.nick);
 | 
					    final nicknameController = useTextEditingController(text: user.value!.nick);
 | 
				
			||||||
    final language = useState(user.value!.language);
 | 
					    final language = useState(user.value!.language);
 | 
				
			||||||
    final links = useState<List<Map<String, String>>>(
 | 
					    final links = useState<List<ProfileLink>>(user.value!.profile.links);
 | 
				
			||||||
      user.value!.profile.links.entries
 | 
					 | 
				
			||||||
          .map((e) => {'key': e.key, 'value': e.value})
 | 
					 | 
				
			||||||
          .toList(),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    void updateBasicInfo() async {
 | 
					    void updateBasicInfo() async {
 | 
				
			||||||
      if (!formKeyBasicInfo.currentState!.validate()) return;
 | 
					      if (!formKeyBasicInfo.currentState!.validate()) return;
 | 
				
			||||||
@@ -171,7 +168,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
            'location': locationController.text,
 | 
					            'location': locationController.text,
 | 
				
			||||||
            'time_zone': timeZoneController.text,
 | 
					            'time_zone': timeZoneController.text,
 | 
				
			||||||
            'birthday': birthday.value?.toUtc().toIso8601String(),
 | 
					            'birthday': birthday.value?.toUtc().toIso8601String(),
 | 
				
			||||||
            'links': {for (var e in links.value) e['key']!: e['value']!},
 | 
					            'links': links.value,
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        final userNotifier = ref.read(userInfoProvider.notifier);
 | 
					        final userNotifier = ref.read(userInfoProvider.notifier);
 | 
				
			||||||
@@ -575,13 +572,15 @@ class UpdateProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
                          children: [
 | 
					                          children: [
 | 
				
			||||||
                            Expanded(
 | 
					                            Expanded(
 | 
				
			||||||
                              child: TextFormField(
 | 
					                              child: TextFormField(
 | 
				
			||||||
                                initialValue: links.value[i]['key'],
 | 
					                                initialValue: links.value[i].name,
 | 
				
			||||||
                                decoration: InputDecoration(
 | 
					                                decoration: InputDecoration(
 | 
				
			||||||
                                  labelText: 'linkKey'.tr(),
 | 
					                                  labelText: 'linkKey'.tr(),
 | 
				
			||||||
                                  isDense: true,
 | 
					                                  isDense: true,
 | 
				
			||||||
                                ),
 | 
					                                ),
 | 
				
			||||||
                                onChanged: (value) {
 | 
					                                onChanged: (value) {
 | 
				
			||||||
                                  links.value[i]['key'] = value;
 | 
					                                  links.value[i] = links.value[i].copyWith(
 | 
				
			||||||
 | 
					                                    name: value,
 | 
				
			||||||
 | 
					                                  );
 | 
				
			||||||
                                },
 | 
					                                },
 | 
				
			||||||
                                onTapOutside:
 | 
					                                onTapOutside:
 | 
				
			||||||
                                    (_) =>
 | 
					                                    (_) =>
 | 
				
			||||||
@@ -592,13 +591,15 @@ class UpdateProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
                            const Gap(8),
 | 
					                            const Gap(8),
 | 
				
			||||||
                            Expanded(
 | 
					                            Expanded(
 | 
				
			||||||
                              child: TextFormField(
 | 
					                              child: TextFormField(
 | 
				
			||||||
                                initialValue: links.value[i]['value'],
 | 
					                                initialValue: links.value[i].url,
 | 
				
			||||||
                                decoration: InputDecoration(
 | 
					                                decoration: InputDecoration(
 | 
				
			||||||
                                  labelText: 'linkValue'.tr(),
 | 
					                                  labelText: 'linkValue'.tr(),
 | 
				
			||||||
                                  isDense: true,
 | 
					                                  isDense: true,
 | 
				
			||||||
                                ),
 | 
					                                ),
 | 
				
			||||||
                                onChanged: (value) {
 | 
					                                onChanged: (value) {
 | 
				
			||||||
                                  links.value[i]['value'] = value;
 | 
					                                  links.value[i] = links.value[i].copyWith(
 | 
				
			||||||
 | 
					                                    url: value,
 | 
				
			||||||
 | 
					                                  );
 | 
				
			||||||
                                },
 | 
					                                },
 | 
				
			||||||
                                onTapOutside:
 | 
					                                onTapOutside:
 | 
				
			||||||
                                    (_) =>
 | 
					                                    (_) =>
 | 
				
			||||||
@@ -620,7 +621,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
                        child: FilledButton.icon(
 | 
					                        child: FilledButton.icon(
 | 
				
			||||||
                          onPressed: () {
 | 
					                          onPressed: () {
 | 
				
			||||||
                            links.value = List.from(links.value)
 | 
					                            links.value = List.from(links.value)
 | 
				
			||||||
                              ..add({'key': '', 'value': ''});
 | 
					                              ..add(ProfileLink(name: '', url: ''));
 | 
				
			||||||
                          },
 | 
					                          },
 | 
				
			||||||
                          label: Text('addLink').tr(),
 | 
					                          label: Text('addLink').tr(),
 | 
				
			||||||
                          icon: const Icon(Symbols.add),
 | 
					                          icon: const Icon(Symbols.add),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
import 'package:dio/dio.dart';
 | 
					import 'package:dio/dio.dart';
 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:go_router/go_router.dart';
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
import 'package:gap/gap.dart';
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
@@ -196,6 +197,15 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    List<Widget> buildSubcolumn(SnAccount data) {
 | 
					    List<Widget> buildSubcolumn(SnAccount data) {
 | 
				
			||||||
      return [
 | 
					      return [
 | 
				
			||||||
 | 
					        Row(
 | 
				
			||||||
 | 
					          spacing: 6,
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            const Icon(Symbols.join, size: 17, fill: 1),
 | 
				
			||||||
 | 
					            Text(
 | 
				
			||||||
 | 
					              'joinedAt'.tr(args: [data.createdAt.formatCustom('yyyy-MM-dd')]),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
        if (data.profile.birthday != null)
 | 
					        if (data.profile.birthday != null)
 | 
				
			||||||
          Row(
 | 
					          Row(
 | 
				
			||||||
            spacing: 6,
 | 
					            spacing: 6,
 | 
				
			||||||
@@ -322,7 +332,7 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
              spacing: 2,
 | 
					              spacing: 2,
 | 
				
			||||||
              children: buildSubcolumn(data),
 | 
					              children: buildSubcolumn(data),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          if (data.profile.timeZone.isNotEmpty)
 | 
					          if (data.profile.timeZone.isNotEmpty && !kIsWeb)
 | 
				
			||||||
            Column(
 | 
					            Column(
 | 
				
			||||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
					              crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
              children: [
 | 
					              children: [
 | 
				
			||||||
@@ -357,17 +367,21 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
        crossAxisAlignment: CrossAxisAlignment.start,
 | 
					        crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          Text('links').tr().bold().padding(horizontal: 24, top: 12, bottom: 4),
 | 
					          Text('links').tr().bold().padding(horizontal: 24, top: 12, bottom: 4),
 | 
				
			||||||
          for (final link in data.profile.links.entries)
 | 
					          for (final link in data.profile.links)
 | 
				
			||||||
            ListTile(
 | 
					            ListTile(
 | 
				
			||||||
              title: Text(link.key.capitalizeEachWord()),
 | 
					              title: Text(link.name.capitalizeEachWord()),
 | 
				
			||||||
              subtitle: Text(link.value),
 | 
					              subtitle: Text(link.url),
 | 
				
			||||||
              contentPadding: EdgeInsets.symmetric(horizontal: 24),
 | 
					              contentPadding: EdgeInsets.symmetric(horizontal: 24),
 | 
				
			||||||
              trailing: const Icon(Symbols.chevron_right),
 | 
					              trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
              shape: RoundedRectangleBorder(
 | 
					              shape: RoundedRectangleBorder(
 | 
				
			||||||
                borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
					                borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              onTap: () {
 | 
					              onTap: () {
 | 
				
			||||||
                launchUrlString(link.value);
 | 
					                if (!link.url.startsWith('http') && !link.url.contains('://')) {
 | 
				
			||||||
 | 
					                  launchUrlString('https://${link.url}');
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                  launchUrlString(link.url);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
@@ -561,9 +575,10 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
                              SliverToBoxAdapter(
 | 
					                              SliverToBoxAdapter(
 | 
				
			||||||
                                child: accountProfileBio(data).padding(top: 4),
 | 
					                                child: accountProfileBio(data).padding(top: 4),
 | 
				
			||||||
                              ),
 | 
					                              ),
 | 
				
			||||||
                              SliverToBoxAdapter(
 | 
					                              if (data.profile.links.isNotEmpty)
 | 
				
			||||||
                                child: accountProfileLinks(data),
 | 
					                                SliverToBoxAdapter(
 | 
				
			||||||
                              ),
 | 
					                                  child: accountProfileLinks(data),
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
                              SliverToBoxAdapter(
 | 
					                              SliverToBoxAdapter(
 | 
				
			||||||
                                child: accountProfileDetail(data),
 | 
					                                child: accountProfileDetail(data),
 | 
				
			||||||
                              ),
 | 
					                              ),
 | 
				
			||||||
@@ -660,11 +675,12 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
                        SliverToBoxAdapter(
 | 
					                        SliverToBoxAdapter(
 | 
				
			||||||
                          child: accountProfileBio(data).padding(horizontal: 4),
 | 
					                          child: accountProfileBio(data).padding(horizontal: 4),
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                        SliverToBoxAdapter(
 | 
					                        if (data.profile.links.isNotEmpty)
 | 
				
			||||||
                          child: accountProfileLinks(
 | 
					                          SliverToBoxAdapter(
 | 
				
			||||||
                            data,
 | 
					                            child: accountProfileLinks(
 | 
				
			||||||
                          ).padding(horizontal: 4),
 | 
					                              data,
 | 
				
			||||||
                        ),
 | 
					                            ).padding(horizontal: 4),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
                        SliverToBoxAdapter(
 | 
					                        SliverToBoxAdapter(
 | 
				
			||||||
                          child: accountProfileDetail(
 | 
					                          child: accountProfileDetail(
 | 
				
			||||||
                            data,
 | 
					                            data,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
 | 
				
			|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:island/models/poll.dart';
 | 
					import 'package:island/models/poll.dart';
 | 
				
			||||||
import 'package:island/pods/network.dart';
 | 
					import 'package:island/pods/network.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/app_scaffold.dart';
 | 
				
			||||||
import 'package:island/widgets/poll/poll_feedback.dart';
 | 
					import 'package:island/widgets/poll/poll_feedback.dart';
 | 
				
			||||||
import 'package:material_symbols_icons/symbols.dart';
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
@@ -70,7 +71,7 @@ class CreatorPollListScreen extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(title: const Text('Polls')),
 | 
					      appBar: AppBar(title: const Text('Polls')),
 | 
				
			||||||
      floatingActionButton: FloatingActionButton(
 | 
					      floatingActionButton: FloatingActionButton(
 | 
				
			||||||
        onPressed: () => _createPoll(context),
 | 
					        onPressed: () => _createPoll(context),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -58,7 +58,7 @@ class StickerPackDetailScreen extends HookConsumerWidget {
 | 
				
			|||||||
      try {
 | 
					      try {
 | 
				
			||||||
        showLoadingModal(context);
 | 
					        showLoadingModal(context);
 | 
				
			||||||
        final apiClient = ref.watch(apiClientProvider);
 | 
					        final apiClient = ref.watch(apiClientProvider);
 | 
				
			||||||
        await apiClient.delete('/stickers/$id/content/${sticker.id}');
 | 
					        await apiClient.delete('/sphere/stickers/$id/content/${sticker.id}');
 | 
				
			||||||
        ref.invalidate(stickerPackContentProvider(id));
 | 
					        ref.invalidate(stickerPackContentProvider(id));
 | 
				
			||||||
      } catch (err) {
 | 
					      } catch (err) {
 | 
				
			||||||
        showErrorAlert(err);
 | 
					        showErrorAlert(err);
 | 
				
			||||||
@@ -297,7 +297,7 @@ class _StickerPackActionMenu extends HookConsumerWidget {
 | 
				
			|||||||
                ).then((confirm) {
 | 
					                ).then((confirm) {
 | 
				
			||||||
                  if (confirm) {
 | 
					                  if (confirm) {
 | 
				
			||||||
                    final client = ref.watch(apiClientProvider);
 | 
					                    final client = ref.watch(apiClientProvider);
 | 
				
			||||||
                    client.delete('/stickers/$packId');
 | 
					                    client.delete('/sphere/stickers/$packId');
 | 
				
			||||||
                    ref.invalidate(stickerPacksNotifierProvider);
 | 
					                    ref.invalidate(stickerPacksNotifierProvider);
 | 
				
			||||||
                    if (context.mounted) context.pop(true);
 | 
					                    if (context.mounted) context.pop(true);
 | 
				
			||||||
                  }
 | 
					                  }
 | 
				
			||||||
@@ -325,7 +325,7 @@ Future<SnSticker?> stickerPackSticker(
 | 
				
			|||||||
  if (query == null) return null;
 | 
					  if (query == null) return null;
 | 
				
			||||||
  final apiClient = ref.watch(apiClientProvider);
 | 
					  final apiClient = ref.watch(apiClientProvider);
 | 
				
			||||||
  final resp = await apiClient.get(
 | 
					  final resp = await apiClient.get(
 | 
				
			||||||
    '/stickers/${query.packId}/content/${query.id}',
 | 
					    '/sphere/stickers/${query.packId}/content/${query.id}',
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  if (resp.data == null) return null;
 | 
					  if (resp.data == null) return null;
 | 
				
			||||||
  return SnSticker.fromJson(resp.data);
 | 
					  return SnSticker.fromJson(resp.data);
 | 
				
			||||||
@@ -379,8 +379,8 @@ class EditStickersScreen extends HookConsumerWidget {
 | 
				
			|||||||
      try {
 | 
					      try {
 | 
				
			||||||
        final resp = await apiClient.request(
 | 
					        final resp = await apiClient.request(
 | 
				
			||||||
          id == null
 | 
					          id == null
 | 
				
			||||||
              ? '/stickers/$packId/content'
 | 
					              ? '/sphere/stickers/$packId/content'
 | 
				
			||||||
              : '/stickers/$packId/content/$id',
 | 
					              : '/sphere/stickers/$packId/content/$id',
 | 
				
			||||||
          data: {'slug': slugController.text, 'image_id': imageController.text},
 | 
					          data: {'slug': slugController.text, 'image_id': imageController.text},
 | 
				
			||||||
          options: Options(method: id == null ? 'POST' : 'PATCH'),
 | 
					          options: Options(method: id == null ? 'POST' : 'PATCH'),
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -151,7 +151,7 @@ class _StickerPackContentProviderElement
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
String _$stickerPackStickerHash() =>
 | 
					String _$stickerPackStickerHash() =>
 | 
				
			||||||
    r'36f524c047e632236d5597aaaa8678ed86599602';
 | 
					    r'5c553666b3a63530bdebae4b7cd52f303c5ab3a0';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// See also [stickerPackSticker].
 | 
					/// See also [stickerPackSticker].
 | 
				
			||||||
@ProviderFor(stickerPackSticker)
 | 
					@ProviderFor(stickerPackSticker)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -114,10 +114,11 @@ class WebFeedEditScreen extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return feedAsync.when(
 | 
					    return feedAsync.when(
 | 
				
			||||||
      loading:
 | 
					      loading:
 | 
				
			||||||
          () =>
 | 
					          () => const AppScaffold(
 | 
				
			||||||
              const Scaffold(body: Center(child: CircularProgressIndicator())),
 | 
					            body: Center(child: CircularProgressIndicator()),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
      error:
 | 
					      error:
 | 
				
			||||||
          (error, stack) => Scaffold(
 | 
					          (error, stack) => AppScaffold(
 | 
				
			||||||
            appBar: AppBar(title: const Text('Error')),
 | 
					            appBar: AppBar(title: const Text('Error')),
 | 
				
			||||||
            body: Center(child: Text('Error: $error')),
 | 
					            body: Center(child: Text('Error: $error')),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,7 @@ import 'package:gap/gap.dart';
 | 
				
			|||||||
import 'package:island/pods/network.dart';
 | 
					import 'package:island/pods/network.dart';
 | 
				
			||||||
import 'package:island/widgets/alert.dart';
 | 
					import 'package:island/widgets/alert.dart';
 | 
				
			||||||
import 'package:island/models/poll.dart';
 | 
					import 'package:island/models/poll.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/app_scaffold.dart';
 | 
				
			||||||
import 'package:uuid/uuid.dart';
 | 
					import 'package:uuid/uuid.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PollEditorState {
 | 
					class PollEditorState {
 | 
				
			||||||
@@ -413,7 +414,7 @@ class PollEditorScreen extends ConsumerWidget {
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        title: Text(model.id == null ? 'Create Poll' : 'Edit Poll'),
 | 
					        title: Text(model.id == null ? 'Create Poll' : 'Edit Poll'),
 | 
				
			||||||
        actions: [
 | 
					        actions: [
 | 
				
			||||||
@@ -428,175 +429,175 @@ class PollEditorScreen extends ConsumerWidget {
 | 
				
			|||||||
          const Gap(8),
 | 
					          const Gap(8),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      body: SafeArea(
 | 
					      body: Column(
 | 
				
			||||||
        child: Form(
 | 
					        children: [
 | 
				
			||||||
          key: ValueKey(model.id),
 | 
					          Expanded(
 | 
				
			||||||
          child: ListView(
 | 
					            child: Form(
 | 
				
			||||||
            padding: const EdgeInsets.all(16),
 | 
					              key: ValueKey(model.id),
 | 
				
			||||||
            children: [
 | 
					              child: ListView(
 | 
				
			||||||
              TextFormField(
 | 
					                padding: const EdgeInsets.all(16),
 | 
				
			||||||
                initialValue: model.title ?? '',
 | 
					 | 
				
			||||||
                decoration: const InputDecoration(
 | 
					 | 
				
			||||||
                  labelText: 'Title',
 | 
					 | 
				
			||||||
                  border: OutlineInputBorder(
 | 
					 | 
				
			||||||
                    borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                textInputAction: TextInputAction.next,
 | 
					 | 
				
			||||||
                maxLength: 256,
 | 
					 | 
				
			||||||
                onChanged: notifier.setTitle,
 | 
					 | 
				
			||||||
                onTapOutside:
 | 
					 | 
				
			||||||
                    (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
                validator: (v) {
 | 
					 | 
				
			||||||
                  if (v == null || v.trim().isEmpty) {
 | 
					 | 
				
			||||||
                    return 'Title is required';
 | 
					 | 
				
			||||||
                  }
 | 
					 | 
				
			||||||
                  return null;
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              const Gap(12),
 | 
					 | 
				
			||||||
              TextFormField(
 | 
					 | 
				
			||||||
                initialValue: model.description ?? '',
 | 
					 | 
				
			||||||
                decoration: const InputDecoration(
 | 
					 | 
				
			||||||
                  labelText: 'Description',
 | 
					 | 
				
			||||||
                  alignLabelWithHint: true,
 | 
					 | 
				
			||||||
                  border: OutlineInputBorder(
 | 
					 | 
				
			||||||
                    borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                maxLines: 3,
 | 
					 | 
				
			||||||
                maxLength: 4096,
 | 
					 | 
				
			||||||
                onChanged: notifier.setDescription,
 | 
					 | 
				
			||||||
                onTapOutside:
 | 
					 | 
				
			||||||
                    (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              const Gap(12),
 | 
					 | 
				
			||||||
              _EndDatePicker(
 | 
					 | 
				
			||||||
                value: model.endedAt,
 | 
					 | 
				
			||||||
                onChanged: notifier.setEndedAt,
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              const Gap(24),
 | 
					 | 
				
			||||||
              Row(
 | 
					 | 
				
			||||||
                children: [
 | 
					                children: [
 | 
				
			||||||
                  Text(
 | 
					                  TextFormField(
 | 
				
			||||||
                    'Questions',
 | 
					                    initialValue: model.title ?? '',
 | 
				
			||||||
                    style: Theme.of(context).textTheme.titleLarge,
 | 
					                    decoration: const InputDecoration(
 | 
				
			||||||
                  ),
 | 
					                      labelText: 'Title',
 | 
				
			||||||
                  const Spacer(),
 | 
					                      border: OutlineInputBorder(
 | 
				
			||||||
                  MenuAnchor(
 | 
					                        borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
				
			||||||
                    builder: (context, controller, child) {
 | 
					                      ),
 | 
				
			||||||
                      return FilledButton.icon(
 | 
					                    ),
 | 
				
			||||||
                        onPressed: () {
 | 
					                    textInputAction: TextInputAction.next,
 | 
				
			||||||
                          controller.isOpen
 | 
					                    maxLength: 256,
 | 
				
			||||||
                              ? controller.close()
 | 
					                    onChanged: notifier.setTitle,
 | 
				
			||||||
                              : controller.open();
 | 
					                    onTapOutside:
 | 
				
			||||||
                        },
 | 
					                        (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
                        icon: const Icon(Icons.add),
 | 
					                    validator: (v) {
 | 
				
			||||||
                        label: const Text('Add question'),
 | 
					                      if (v == null || v.trim().isEmpty) {
 | 
				
			||||||
                      );
 | 
					                        return 'Title is required';
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                      return null;
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                    menuChildren:
 | 
					 | 
				
			||||||
                        SnPollQuestionType.values
 | 
					 | 
				
			||||||
                            .map(
 | 
					 | 
				
			||||||
                              (t) => MenuItemButton(
 | 
					 | 
				
			||||||
                                leadingIcon: Icon(_iconForType(t)),
 | 
					 | 
				
			||||||
                                onPressed: () => notifier.addQuestion(t),
 | 
					 | 
				
			||||||
                                child: Text(_labelForType(t)),
 | 
					 | 
				
			||||||
                              ),
 | 
					 | 
				
			||||||
                            )
 | 
					 | 
				
			||||||
                            .toList(),
 | 
					 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
 | 
					                  const Gap(12),
 | 
				
			||||||
 | 
					                  TextFormField(
 | 
				
			||||||
 | 
					                    initialValue: model.description ?? '',
 | 
				
			||||||
 | 
					                    decoration: const InputDecoration(
 | 
				
			||||||
 | 
					                      labelText: 'Description',
 | 
				
			||||||
 | 
					                      alignLabelWithHint: true,
 | 
				
			||||||
 | 
					                      border: OutlineInputBorder(
 | 
				
			||||||
 | 
					                        borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    maxLines: 3,
 | 
				
			||||||
 | 
					                    maxLength: 4096,
 | 
				
			||||||
 | 
					                    onChanged: notifier.setDescription,
 | 
				
			||||||
 | 
					                    onTapOutside:
 | 
				
			||||||
 | 
					                        (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  const Gap(12),
 | 
				
			||||||
 | 
					                  _EndDatePicker(
 | 
				
			||||||
 | 
					                    value: model.endedAt,
 | 
				
			||||||
 | 
					                    onChanged: notifier.setEndedAt,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  const Gap(24),
 | 
				
			||||||
 | 
					                  Row(
 | 
				
			||||||
 | 
					                    children: [
 | 
				
			||||||
 | 
					                      Text(
 | 
				
			||||||
 | 
					                        'Questions',
 | 
				
			||||||
 | 
					                        style: Theme.of(context).textTheme.titleLarge,
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      const Spacer(),
 | 
				
			||||||
 | 
					                      MenuAnchor(
 | 
				
			||||||
 | 
					                        builder: (context, controller, child) {
 | 
				
			||||||
 | 
					                          return FilledButton.icon(
 | 
				
			||||||
 | 
					                            onPressed: () {
 | 
				
			||||||
 | 
					                              controller.isOpen
 | 
				
			||||||
 | 
					                                  ? controller.close()
 | 
				
			||||||
 | 
					                                  : controller.open();
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                            icon: const Icon(Icons.add),
 | 
				
			||||||
 | 
					                            label: const Text('Add question'),
 | 
				
			||||||
 | 
					                          );
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        menuChildren:
 | 
				
			||||||
 | 
					                            SnPollQuestionType.values
 | 
				
			||||||
 | 
					                                .map(
 | 
				
			||||||
 | 
					                                  (t) => MenuItemButton(
 | 
				
			||||||
 | 
					                                    leadingIcon: Icon(_iconForType(t)),
 | 
				
			||||||
 | 
					                                    onPressed: () => notifier.addQuestion(t),
 | 
				
			||||||
 | 
					                                    child: Text(_labelForType(t)),
 | 
				
			||||||
 | 
					                                  ),
 | 
				
			||||||
 | 
					                                )
 | 
				
			||||||
 | 
					                                .toList(),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  const Gap(8),
 | 
				
			||||||
 | 
					                  if (model.questions.isEmpty)
 | 
				
			||||||
 | 
					                    _EmptyState(
 | 
				
			||||||
 | 
					                      title: 'No questions yet',
 | 
				
			||||||
 | 
					                      subtitle:
 | 
				
			||||||
 | 
					                          'Use "Add question" to start building your poll.',
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                  else
 | 
				
			||||||
 | 
					                    ReorderableListView.builder(
 | 
				
			||||||
 | 
					                      shrinkWrap: true,
 | 
				
			||||||
 | 
					                      physics: const NeverScrollableScrollPhysics(),
 | 
				
			||||||
 | 
					                      itemCount: model.questions.length,
 | 
				
			||||||
 | 
					                      onReorder: (oldIndex, newIndex) {
 | 
				
			||||||
 | 
					                        // Convert to stepwise moves using provided functions
 | 
				
			||||||
 | 
					                        if (newIndex > oldIndex) newIndex -= 1;
 | 
				
			||||||
 | 
					                        final steps = newIndex - oldIndex;
 | 
				
			||||||
 | 
					                        if (steps == 0) return;
 | 
				
			||||||
 | 
					                        if (steps > 0) {
 | 
				
			||||||
 | 
					                          for (int i = 0; i < steps; i++) {
 | 
				
			||||||
 | 
					                            notifier.moveQuestionDown(oldIndex + i);
 | 
				
			||||||
 | 
					                          }
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                          for (int i = 0; i > steps; i--) {
 | 
				
			||||||
 | 
					                            notifier.moveQuestionUp(oldIndex + i);
 | 
				
			||||||
 | 
					                          }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                      buildDefaultDragHandles: false,
 | 
				
			||||||
 | 
					                      itemBuilder: (context, index) {
 | 
				
			||||||
 | 
					                        final q = model.questions[index];
 | 
				
			||||||
 | 
					                        return Card(
 | 
				
			||||||
 | 
					                          key: ValueKey('q_$index'),
 | 
				
			||||||
 | 
					                          margin: const EdgeInsets.symmetric(vertical: 8),
 | 
				
			||||||
 | 
					                          clipBehavior: Clip.antiAlias,
 | 
				
			||||||
 | 
					                          child: Column(
 | 
				
			||||||
 | 
					                            children: [
 | 
				
			||||||
 | 
					                              _QuestionHeader(
 | 
				
			||||||
 | 
					                                index: index,
 | 
				
			||||||
 | 
					                                question: q,
 | 
				
			||||||
 | 
					                                onMoveUp:
 | 
				
			||||||
 | 
					                                    index > 0
 | 
				
			||||||
 | 
					                                        ? () => notifier.moveQuestionUp(index)
 | 
				
			||||||
 | 
					                                        : null,
 | 
				
			||||||
 | 
					                                onMoveDown:
 | 
				
			||||||
 | 
					                                    index < model.questions.length - 1
 | 
				
			||||||
 | 
					                                        ? () => notifier.moveQuestionDown(index)
 | 
				
			||||||
 | 
					                                        : null,
 | 
				
			||||||
 | 
					                                onDelete: () => notifier.removeQuestion(index),
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                              const Divider(height: 1),
 | 
				
			||||||
 | 
					                              Padding(
 | 
				
			||||||
 | 
					                                padding: const EdgeInsets.all(16),
 | 
				
			||||||
 | 
					                                child: _QuestionEditor(
 | 
				
			||||||
 | 
					                                  index: index,
 | 
				
			||||||
 | 
					                                  question: q,
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                            ],
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  const Gap(96),
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              const Gap(8),
 | 
					            ),
 | 
				
			||||||
              if (model.questions.isEmpty)
 | 
					          ),
 | 
				
			||||||
                _EmptyState(
 | 
					          Row(
 | 
				
			||||||
                  title: 'No questions yet',
 | 
					            children: [
 | 
				
			||||||
                  subtitle: 'Use "Add question" to start building your poll.',
 | 
					              OutlinedButton.icon(
 | 
				
			||||||
                )
 | 
					                onPressed: () {
 | 
				
			||||||
              else
 | 
					                  Navigator.of(context).maybePop();
 | 
				
			||||||
                ReorderableListView.builder(
 | 
					                },
 | 
				
			||||||
                  shrinkWrap: true,
 | 
					                icon: const Icon(Icons.close),
 | 
				
			||||||
                  physics: const NeverScrollableScrollPhysics(),
 | 
					                label: const Text('Cancel'),
 | 
				
			||||||
                  itemCount: model.questions.length,
 | 
					              ),
 | 
				
			||||||
                  onReorder: (oldIndex, newIndex) {
 | 
					              const Spacer(),
 | 
				
			||||||
                    // Convert to stepwise moves using provided functions
 | 
					              FilledButton.icon(
 | 
				
			||||||
                    if (newIndex > oldIndex) newIndex -= 1;
 | 
					                onPressed: () {
 | 
				
			||||||
                    final steps = newIndex - oldIndex;
 | 
					                  _submitPoll(context, ref);
 | 
				
			||||||
                    if (steps == 0) return;
 | 
					                },
 | 
				
			||||||
                    if (steps > 0) {
 | 
					                icon: const Icon(Icons.cloud_upload_outlined),
 | 
				
			||||||
                      for (int i = 0; i < steps; i++) {
 | 
					                label: Text(model.id == null ? 'Create' : 'Update'),
 | 
				
			||||||
                        notifier.moveQuestionDown(oldIndex + i);
 | 
					              ),
 | 
				
			||||||
                      }
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                      for (int i = 0; i > steps; i--) {
 | 
					 | 
				
			||||||
                        notifier.moveQuestionUp(oldIndex + i);
 | 
					 | 
				
			||||||
                      }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                  buildDefaultDragHandles: false,
 | 
					 | 
				
			||||||
                  itemBuilder: (context, index) {
 | 
					 | 
				
			||||||
                    final q = model.questions[index];
 | 
					 | 
				
			||||||
                    return Card(
 | 
					 | 
				
			||||||
                      key: ValueKey('q_$index'),
 | 
					 | 
				
			||||||
                      margin: const EdgeInsets.symmetric(vertical: 8),
 | 
					 | 
				
			||||||
                      clipBehavior: Clip.antiAlias,
 | 
					 | 
				
			||||||
                      child: Column(
 | 
					 | 
				
			||||||
                        children: [
 | 
					 | 
				
			||||||
                          _QuestionHeader(
 | 
					 | 
				
			||||||
                            index: index,
 | 
					 | 
				
			||||||
                            question: q,
 | 
					 | 
				
			||||||
                            onMoveUp:
 | 
					 | 
				
			||||||
                                index > 0
 | 
					 | 
				
			||||||
                                    ? () => notifier.moveQuestionUp(index)
 | 
					 | 
				
			||||||
                                    : null,
 | 
					 | 
				
			||||||
                            onMoveDown:
 | 
					 | 
				
			||||||
                                index < model.questions.length - 1
 | 
					 | 
				
			||||||
                                    ? () => notifier.moveQuestionDown(index)
 | 
					 | 
				
			||||||
                                    : null,
 | 
					 | 
				
			||||||
                            onDelete: () => notifier.removeQuestion(index),
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                          const Divider(height: 1),
 | 
					 | 
				
			||||||
                          Padding(
 | 
					 | 
				
			||||||
                            padding: const EdgeInsets.all(16),
 | 
					 | 
				
			||||||
                            child: _QuestionEditor(index: index, question: q),
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                        ],
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              const Gap(96),
 | 
					 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        ),
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      bottomNavigationBar: Padding(
 | 
					 | 
				
			||||||
        padding: EdgeInsets.fromLTRB(
 | 
					 | 
				
			||||||
          16,
 | 
					 | 
				
			||||||
          8,
 | 
					 | 
				
			||||||
          16,
 | 
					 | 
				
			||||||
          16 + MediaQuery.of(context).padding.bottom,
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        child: Row(
 | 
					 | 
				
			||||||
          children: [
 | 
					 | 
				
			||||||
            OutlinedButton.icon(
 | 
					 | 
				
			||||||
              onPressed: () {
 | 
					 | 
				
			||||||
                Navigator.of(context).maybePop();
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
              icon: const Icon(Icons.close),
 | 
					 | 
				
			||||||
              label: const Text('Cancel'),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            const Spacer(),
 | 
					 | 
				
			||||||
            FilledButton.icon(
 | 
					 | 
				
			||||||
              onPressed: () {
 | 
					 | 
				
			||||||
                _submitPoll(context, ref);
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
              icon: const Icon(Icons.cloud_upload_outlined),
 | 
					 | 
				
			||||||
              label: Text(model.id == null ? 'Create' : 'Update'),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ],
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -92,6 +92,7 @@ class PostDetailScreen extends HookConsumerWidget {
 | 
				
			|||||||
                  right: 0,
 | 
					                  right: 0,
 | 
				
			||||||
                  child: Material(
 | 
					                  child: Material(
 | 
				
			||||||
                    elevation: 2,
 | 
					                    elevation: 2,
 | 
				
			||||||
 | 
					                    color: Theme.of(context).colorScheme.surfaceContainer,
 | 
				
			||||||
                    child: postState
 | 
					                    child: postState
 | 
				
			||||||
                        .when(
 | 
					                        .when(
 | 
				
			||||||
                          data:
 | 
					                          data:
 | 
				
			||||||
@@ -107,8 +108,8 @@ class PostDetailScreen extends HookConsumerWidget {
 | 
				
			|||||||
                          error: (_, _) => const SizedBox.shrink(),
 | 
					                          error: (_, _) => const SizedBox.shrink(),
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                        .padding(
 | 
					                        .padding(
 | 
				
			||||||
                          bottom: MediaQuery.of(context).padding.bottom + 16,
 | 
					                          bottom: MediaQuery.of(context).padding.bottom + 8,
 | 
				
			||||||
                          top: 16,
 | 
					                          top: 8,
 | 
				
			||||||
                          horizontal: 16,
 | 
					                          horizontal: 16,
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,19 +1,28 @@
 | 
				
			|||||||
import 'dart:async';
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					import 'dart:developer';
 | 
				
			||||||
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:dio/dio.dart';
 | 
					import 'package:dio/dio.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_app_update/azhon_app_update.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_app_update/update_model.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/content/markdown.dart';
 | 
				
			||||||
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:package_info_plus/package_info_plus.dart';
 | 
					import 'package:package_info_plus/package_info_plus.dart';
 | 
				
			||||||
 | 
					import 'package:collection/collection.dart'; // Added for firstWhereOrNull
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
import 'package:url_launcher/url_launcher.dart';
 | 
					import 'package:url_launcher/url_launcher.dart';
 | 
				
			||||||
import 'package:island/widgets/content/sheet.dart';
 | 
					import 'package:island/widgets/content/sheet.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Data model for a GitHub release we care about
 | 
					/// Data model for a GitHub release we care about
 | 
				
			||||||
class GithubReleaseInfo {
 | 
					class GithubReleaseInfo {
 | 
				
			||||||
  final String tagName; // e.g. 3.1.0+118
 | 
					  final String tagName;
 | 
				
			||||||
  final String name; // release title
 | 
					  final String name;
 | 
				
			||||||
  final String body; // changelog markdown
 | 
					  final String body;
 | 
				
			||||||
  final String htmlUrl; // release page
 | 
					  final String htmlUrl;
 | 
				
			||||||
  final DateTime createdAt;
 | 
					  final DateTime createdAt;
 | 
				
			||||||
 | 
					  final List<GithubReleaseAsset> assets;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const GithubReleaseInfo({
 | 
					  const GithubReleaseInfo({
 | 
				
			||||||
    required this.tagName,
 | 
					    required this.tagName,
 | 
				
			||||||
@@ -21,9 +30,28 @@ class GithubReleaseInfo {
 | 
				
			|||||||
    required this.body,
 | 
					    required this.body,
 | 
				
			||||||
    required this.htmlUrl,
 | 
					    required this.htmlUrl,
 | 
				
			||||||
    required this.createdAt,
 | 
					    required this.createdAt,
 | 
				
			||||||
 | 
					    this.assets = const [],
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Data model for a GitHub release asset
 | 
				
			||||||
 | 
					class GithubReleaseAsset {
 | 
				
			||||||
 | 
					  final String name;
 | 
				
			||||||
 | 
					  final String browserDownloadUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const GithubReleaseAsset({
 | 
				
			||||||
 | 
					    required this.name,
 | 
				
			||||||
 | 
					    required this.browserDownloadUrl,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory GithubReleaseAsset.fromJson(Map<String, dynamic> json) {
 | 
				
			||||||
 | 
					    return GithubReleaseAsset(
 | 
				
			||||||
 | 
					      name: json['name'] as String,
 | 
				
			||||||
 | 
					      browserDownloadUrl: json['browser_download_url'] as String,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Parses version and build number from "x.y.z+build"
 | 
					/// Parses version and build number from "x.y.z+build"
 | 
				
			||||||
class _ParsedVersion implements Comparable<_ParsedVersion> {
 | 
					class _ParsedVersion implements Comparable<_ParsedVersion> {
 | 
				
			||||||
  final int major;
 | 
					  final int major;
 | 
				
			||||||
@@ -62,7 +90,7 @@ class _ParsedVersion implements Comparable<_ParsedVersion> {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UpdateService {
 | 
					class UpdateService {
 | 
				
			||||||
  UpdateService({Dio? dio})
 | 
					  UpdateService({Dio? dio, this.useProxy = false})
 | 
				
			||||||
    : _dio =
 | 
					    : _dio =
 | 
				
			||||||
          dio ??
 | 
					          dio ??
 | 
				
			||||||
          Dio(
 | 
					          Dio(
 | 
				
			||||||
@@ -78,6 +106,9 @@ class UpdateService {
 | 
				
			|||||||
          );
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final Dio _dio;
 | 
					  final Dio _dio;
 | 
				
			||||||
 | 
					  final bool useProxy;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static const _proxyBaseUrl = 'https://ghfast.top/';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static const _releasesLatestApi =
 | 
					  static const _releasesLatestApi =
 | 
				
			||||||
      'https://api.github.com/repos/solsynth/solian/releases/latest';
 | 
					      'https://api.github.com/repos/solsynth/solian/releases/latest';
 | 
				
			||||||
@@ -85,31 +116,52 @@ class UpdateService {
 | 
				
			|||||||
  /// Checks GitHub for the latest release and compares against the current app version.
 | 
					  /// Checks GitHub for the latest release and compares against the current app version.
 | 
				
			||||||
  /// If update is available, shows a bottom sheet with changelog and an action to open release page.
 | 
					  /// If update is available, shows a bottom sheet with changelog and an action to open release page.
 | 
				
			||||||
  Future<void> checkForUpdates(BuildContext context) async {
 | 
					  Future<void> checkForUpdates(BuildContext context) async {
 | 
				
			||||||
 | 
					    log('[Update] Checking for updates...');
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      final release = await fetchLatestRelease();
 | 
					      final release = await fetchLatestRelease();
 | 
				
			||||||
      if (release == null) return;
 | 
					      if (release == null) {
 | 
				
			||||||
 | 
					        log('[Update] No latest release found or could not fetch.');
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      log('[Update] Fetched latest release: ${release.tagName}');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      final info = await PackageInfo.fromPlatform();
 | 
					      final info = await PackageInfo.fromPlatform();
 | 
				
			||||||
      final localVersionStr = '${info.version}+${info.buildNumber}';
 | 
					      final localVersionStr = '${info.version}+${info.buildNumber}';
 | 
				
			||||||
 | 
					      log('[Update] Local app version: $localVersionStr');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      final latest = _ParsedVersion.tryParse(release.tagName);
 | 
					      final latest = _ParsedVersion.tryParse(release.tagName);
 | 
				
			||||||
      final local = _ParsedVersion.tryParse(localVersionStr);
 | 
					      final local = _ParsedVersion.tryParse(localVersionStr);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (latest == null || local == null) {
 | 
					      if (latest == null || local == null) {
 | 
				
			||||||
 | 
					        log(
 | 
				
			||||||
 | 
					          '[Update] Failed to parse versions. Latest: ${release.tagName}, Local: $localVersionStr',
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
        // If parsing fails, do nothing silently
 | 
					        // If parsing fails, do nothing silently
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      log('[Update] Parsed versions. Latest: $latest, Local: $local');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      final needsUpdate = latest.compareTo(local) > 0;
 | 
					      final needsUpdate = latest.compareTo(local) > 0;
 | 
				
			||||||
      if (!needsUpdate) return;
 | 
					      if (!needsUpdate) {
 | 
				
			||||||
 | 
					        log('[Update] App is up to date. No update needed.');
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      log('[Update] Update available! Latest: $latest, Local: $local');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!context.mounted) return;
 | 
					      if (!context.mounted) {
 | 
				
			||||||
 | 
					        log('[Update] Context not mounted, cannot show update sheet.');
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Delay to ensure UI is ready (if called at startup)
 | 
					      // Delay to ensure UI is ready (if called at startup)
 | 
				
			||||||
      await Future.delayed(const Duration(milliseconds: 100));
 | 
					      await Future.delayed(const Duration(milliseconds: 100));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await showUpdateSheet(context, release);
 | 
					      if (context.mounted) {
 | 
				
			||||||
    } catch (_) {
 | 
					        await showUpdateSheet(context, release);
 | 
				
			||||||
 | 
					        log('[Update] Update sheet shown.');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      log('[Update] Error checking for updates: $e');
 | 
				
			||||||
      // Ignore errors (network, api, etc.)
 | 
					      // Ignore errors (network, api, etc.)
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -126,25 +178,68 @@ class UpdateService {
 | 
				
			|||||||
      context: context,
 | 
					      context: context,
 | 
				
			||||||
      isScrollControlled: true,
 | 
					      isScrollControlled: true,
 | 
				
			||||||
      useRootNavigator: true,
 | 
					      useRootNavigator: true,
 | 
				
			||||||
      builder:
 | 
					      builder: (ctx) {
 | 
				
			||||||
          (ctx) => _UpdateSheet(
 | 
					        String? androidUpdateUrl;
 | 
				
			||||||
            release: release,
 | 
					        if (Platform.isAndroid) {
 | 
				
			||||||
            onOpen: () async {
 | 
					          androidUpdateUrl = _getAndroidUpdateUrl(release.assets);
 | 
				
			||||||
              final uri = Uri.parse(release.htmlUrl);
 | 
					        }
 | 
				
			||||||
              if (await canLaunchUrl(uri)) {
 | 
					        return _UpdateSheet(
 | 
				
			||||||
                await launchUrl(uri, mode: LaunchMode.externalApplication);
 | 
					          release: release,
 | 
				
			||||||
              }
 | 
					          onOpen: () async {
 | 
				
			||||||
            },
 | 
					            final uri = Uri.parse(release.htmlUrl);
 | 
				
			||||||
          ),
 | 
					            if (await canLaunchUrl(uri)) {
 | 
				
			||||||
 | 
					              await launchUrl(uri, mode: LaunchMode.externalApplication);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          androidUpdateUrl: androidUpdateUrl,
 | 
				
			||||||
 | 
					          useProxy: useProxy, // Pass the useProxy flag
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String? _getAndroidUpdateUrl(List<GithubReleaseAsset> assets) {
 | 
				
			||||||
 | 
					    final arm64 = assets.firstWhereOrNull(
 | 
				
			||||||
 | 
					      (asset) => asset.name == 'app-arm64-v8a-release.apk',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    final armeabi = assets.firstWhereOrNull(
 | 
				
			||||||
 | 
					      (asset) => asset.name == 'app-armeabi-v7a-release.apk',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    final x86_64 = assets.firstWhereOrNull(
 | 
				
			||||||
 | 
					      (asset) => asset.name == 'app-x86_64-release.apk',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Prioritize arm64, then armeabi, then x86_64
 | 
				
			||||||
 | 
					    if (arm64 != null) {
 | 
				
			||||||
 | 
					      return arm64.browserDownloadUrl;
 | 
				
			||||||
 | 
					    } else if (armeabi != null) {
 | 
				
			||||||
 | 
					      return armeabi.browserDownloadUrl;
 | 
				
			||||||
 | 
					    } else if (x86_64 != null) {
 | 
				
			||||||
 | 
					      return x86_64.browserDownloadUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Fetch the latest release info from GitHub.
 | 
					  /// Fetch the latest release info from GitHub.
 | 
				
			||||||
  /// Public so other screens (e.g., About) can manually trigger update checks.
 | 
					  /// Public so other screens (e.g., About) can manually trigger update checks.
 | 
				
			||||||
  Future<GithubReleaseInfo?> fetchLatestRelease() async {
 | 
					  Future<GithubReleaseInfo?> fetchLatestRelease() async {
 | 
				
			||||||
    final resp = await _dio.get(_releasesLatestApi);
 | 
					    final apiEndpoint =
 | 
				
			||||||
    if (resp.statusCode != 200) return null;
 | 
					        useProxy
 | 
				
			||||||
 | 
					            ? '$_proxyBaseUrl${Uri.encodeComponent(_releasesLatestApi)}'
 | 
				
			||||||
 | 
					            : _releasesLatestApi;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log(
 | 
				
			||||||
 | 
					      '[Update] Fetching latest release from GitHub API: $apiEndpoint (Proxy: $useProxy)',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    final resp = await _dio.get(apiEndpoint);
 | 
				
			||||||
 | 
					    if (resp.statusCode != 200) {
 | 
				
			||||||
 | 
					      log(
 | 
				
			||||||
 | 
					        '[Update] Failed to fetch latest release. Status code: ${resp.statusCode}',
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    final data = resp.data as Map<String, dynamic>;
 | 
					    final data = resp.data as Map<String, dynamic>;
 | 
				
			||||||
 | 
					    log('[Update] Successfully fetched release data.');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final tagName = (data['tag_name'] ?? '').toString();
 | 
					    final tagName = (data['tag_name'] ?? '').toString();
 | 
				
			||||||
    final name = (data['name'] ?? tagName).toString();
 | 
					    final name = (data['name'] ?? tagName).toString();
 | 
				
			||||||
@@ -152,25 +247,70 @@ class UpdateService {
 | 
				
			|||||||
    final htmlUrl = (data['html_url'] ?? '').toString();
 | 
					    final htmlUrl = (data['html_url'] ?? '').toString();
 | 
				
			||||||
    final createdAtStr = (data['created_at'] ?? '').toString();
 | 
					    final createdAtStr = (data['created_at'] ?? '').toString();
 | 
				
			||||||
    final createdAt = DateTime.tryParse(createdAtStr) ?? DateTime.now();
 | 
					    final createdAt = DateTime.tryParse(createdAtStr) ?? DateTime.now();
 | 
				
			||||||
 | 
					    final assetsData =
 | 
				
			||||||
 | 
					        (data['assets'] as List<dynamic>?)
 | 
				
			||||||
 | 
					            ?.map((e) => GithubReleaseAsset.fromJson(e as Map<String, dynamic>))
 | 
				
			||||||
 | 
					            .toList() ??
 | 
				
			||||||
 | 
					        [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (tagName.isEmpty || htmlUrl.isEmpty) return null;
 | 
					    if (tagName.isEmpty || htmlUrl.isEmpty) {
 | 
				
			||||||
 | 
					      log(
 | 
				
			||||||
 | 
					        '[Update] Missing tag_name or html_url in release data. TagName: "$tagName", HtmlUrl: "$htmlUrl"',
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log('[Update] Returning GithubReleaseInfo for tag: $tagName');
 | 
				
			||||||
    return GithubReleaseInfo(
 | 
					    return GithubReleaseInfo(
 | 
				
			||||||
      tagName: tagName,
 | 
					      tagName: tagName,
 | 
				
			||||||
      name: name,
 | 
					      name: name,
 | 
				
			||||||
      body: body,
 | 
					      body: body,
 | 
				
			||||||
      htmlUrl: htmlUrl,
 | 
					      htmlUrl: htmlUrl,
 | 
				
			||||||
      createdAt: createdAt,
 | 
					      createdAt: createdAt,
 | 
				
			||||||
 | 
					      assets: assetsData,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _UpdateSheet extends StatelessWidget {
 | 
					class _UpdateSheet extends StatefulWidget {
 | 
				
			||||||
  const _UpdateSheet({required this.release, required this.onOpen});
 | 
					  const _UpdateSheet({
 | 
				
			||||||
 | 
					    required this.release,
 | 
				
			||||||
 | 
					    required this.onOpen,
 | 
				
			||||||
 | 
					    this.androidUpdateUrl,
 | 
				
			||||||
 | 
					    this.useProxy = false,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final String? androidUpdateUrl;
 | 
				
			||||||
 | 
					  final bool useProxy;
 | 
				
			||||||
  final GithubReleaseInfo release;
 | 
					  final GithubReleaseInfo release;
 | 
				
			||||||
  final VoidCallback onOpen;
 | 
					  final VoidCallback onOpen;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  State<_UpdateSheet> createState() => _UpdateSheetState();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _UpdateSheetState extends State<_UpdateSheet> {
 | 
				
			||||||
 | 
					  late bool _useProxy;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void initState() {
 | 
				
			||||||
 | 
					    super.initState();
 | 
				
			||||||
 | 
					    _useProxy = widget.useProxy;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _installUpdate(String url) async {
 | 
				
			||||||
 | 
					    final downloadUrl =
 | 
				
			||||||
 | 
					        _useProxy ? 'https://ghfast.top/${Uri.encodeComponent(url)}' : url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    UpdateModel model = UpdateModel(
 | 
				
			||||||
 | 
					      downloadUrl,
 | 
				
			||||||
 | 
					      "solian-update-${widget.release.tagName}.apk",
 | 
				
			||||||
 | 
					      "ic_launcher",
 | 
				
			||||||
 | 
					      'https://apps.apple.com/us/app/solian/id6499032345',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    AzhonAppUpdate.update(model);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final theme = Theme.of(context);
 | 
					    final theme = Theme.of(context);
 | 
				
			||||||
@@ -186,8 +326,11 @@ class _UpdateSheet extends StatelessWidget {
 | 
				
			|||||||
            Column(
 | 
					            Column(
 | 
				
			||||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
					              crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
              children: [
 | 
					              children: [
 | 
				
			||||||
                Text(release.name, style: theme.textTheme.titleMedium).bold(),
 | 
					                Text(
 | 
				
			||||||
                Text(release.tagName).fontSize(12),
 | 
					                  widget.release.name,
 | 
				
			||||||
 | 
					                  style: theme.textTheme.titleMedium,
 | 
				
			||||||
 | 
					                ).bold(),
 | 
				
			||||||
 | 
					                Text(widget.release.tagName).fontSize(12),
 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ).padding(vertical: 16, horizontal: 16),
 | 
					            ).padding(vertical: 16, horizontal: 16),
 | 
				
			||||||
            const Divider(height: 1),
 | 
					            const Divider(height: 1),
 | 
				
			||||||
@@ -197,21 +340,45 @@ class _UpdateSheet extends StatelessWidget {
 | 
				
			|||||||
                  horizontal: 16,
 | 
					                  horizontal: 16,
 | 
				
			||||||
                  vertical: 16,
 | 
					                  vertical: 16,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                child: SelectableText(
 | 
					                child: MarkdownTextContent(
 | 
				
			||||||
                  release.body.isEmpty
 | 
					                  content:
 | 
				
			||||||
                      ? 'No changelog provided.'
 | 
					                      widget.release.body.isEmpty
 | 
				
			||||||
                      : release.body,
 | 
					                          ? 'No changelog provided.'
 | 
				
			||||||
                  style: theme.textTheme.bodyMedium,
 | 
					                          : widget.release.body,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 | 
					            if (!kIsWeb && Platform.isAndroid)
 | 
				
			||||||
 | 
					              SwitchListTile(
 | 
				
			||||||
 | 
					                title: const Text('Use GitHub Proxy for Download'),
 | 
				
			||||||
 | 
					                value: _useProxy,
 | 
				
			||||||
 | 
					                onChanged: (value) {
 | 
				
			||||||
 | 
					                  setState(() {
 | 
				
			||||||
 | 
					                    _useProxy = value;
 | 
				
			||||||
 | 
					                  });
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              ).padding(horizontal: 8),
 | 
				
			||||||
            Column(
 | 
					            Column(
 | 
				
			||||||
              children: [
 | 
					              children: [
 | 
				
			||||||
                Row(
 | 
					                Row(
 | 
				
			||||||
 | 
					                  spacing: 8,
 | 
				
			||||||
                  children: [
 | 
					                  children: [
 | 
				
			||||||
 | 
					                    if (!kIsWeb &&
 | 
				
			||||||
 | 
					                        Platform.isAndroid &&
 | 
				
			||||||
 | 
					                        widget.androidUpdateUrl != null)
 | 
				
			||||||
 | 
					                      Expanded(
 | 
				
			||||||
 | 
					                        child: FilledButton.icon(
 | 
				
			||||||
 | 
					                          onPressed: () {
 | 
				
			||||||
 | 
					                            log(widget.androidUpdateUrl!);
 | 
				
			||||||
 | 
					                            _installUpdate(widget.androidUpdateUrl!);
 | 
				
			||||||
 | 
					                          },
 | 
				
			||||||
 | 
					                          icon: const Icon(Symbols.update),
 | 
				
			||||||
 | 
					                          label: const Text('Install update'),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
                    Expanded(
 | 
					                    Expanded(
 | 
				
			||||||
                      child: FilledButton.icon(
 | 
					                      child: FilledButton.icon(
 | 
				
			||||||
                        onPressed: onOpen,
 | 
					                        onPressed: widget.onOpen,
 | 
				
			||||||
                        icon: const Icon(Icons.open_in_new),
 | 
					                        icon: const Icon(Icons.open_in_new),
 | 
				
			||||||
                        label: const Text('Open release page'),
 | 
					                        label: const Text('Open release page'),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			|||||||
import 'package:island/pods/websocket.dart';
 | 
					import 'package:island/pods/websocket.dart';
 | 
				
			||||||
import 'package:island/services/notify.dart';
 | 
					import 'package:island/services/notify.dart';
 | 
				
			||||||
import 'package:island/services/sharing_intent.dart';
 | 
					import 'package:island/services/sharing_intent.dart';
 | 
				
			||||||
 | 
					import 'package:island/services/update_service.dart';
 | 
				
			||||||
import 'package:island/widgets/content/network_status_sheet.dart';
 | 
					import 'package:island/widgets/content/network_status_sheet.dart';
 | 
				
			||||||
import 'package:island/widgets/tour/tour.dart';
 | 
					import 'package:island/widgets/tour/tour.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -21,6 +22,7 @@ class AppWrapper extends HookConsumerWidget {
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
      final sharingService = SharingIntentService();
 | 
					      final sharingService = SharingIntentService();
 | 
				
			||||||
      sharingService.initialize(context);
 | 
					      sharingService.initialize(context);
 | 
				
			||||||
 | 
					      UpdateService().checkForUpdates(context);
 | 
				
			||||||
      return () {
 | 
					      return () {
 | 
				
			||||||
        sharingService.dispose();
 | 
					        sharingService.dispose();
 | 
				
			||||||
        ntySubs?.cancel();
 | 
					        ntySubs?.cancel();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
 | 
				
			|||||||
import 'package:flutter_highlight/themes/a11y-dark.dart';
 | 
					import 'package:flutter_highlight/themes/a11y-dark.dart';
 | 
				
			||||||
import 'package:flutter_highlight/themes/a11y-light.dart';
 | 
					import 'package:flutter_highlight/themes/a11y-light.dart';
 | 
				
			||||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
 | 
					import 'package:google_fonts/google_fonts.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:island/models/file.dart';
 | 
					import 'package:island/models/file.dart';
 | 
				
			||||||
import 'package:island/pods/config.dart';
 | 
					import 'package:island/pods/config.dart';
 | 
				
			||||||
@@ -71,7 +72,22 @@ class MarkdownTextContent extends HookConsumerWidget {
 | 
				
			|||||||
            textStyle: textStyle ?? Theme.of(context).textTheme.bodyMedium!,
 | 
					            textStyle: textStyle ?? Theme.of(context).textTheme.bodyMedium!,
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          HrConfig(height: 1, color: Theme.of(context).dividerColor),
 | 
					          HrConfig(height: 1, color: Theme.of(context).dividerColor),
 | 
				
			||||||
          PreConfig(theme: isDark ? a11yDarkTheme : a11yLightTheme),
 | 
					          PreConfig(
 | 
				
			||||||
 | 
					            theme: isDark ? a11yDarkTheme : a11yLightTheme,
 | 
				
			||||||
 | 
					            textStyle: GoogleFonts.robotoMono(fontSize: 14),
 | 
				
			||||||
 | 
					            styleNotMatched: GoogleFonts.robotoMono(fontSize: 14),
 | 
				
			||||||
 | 
					            decoration: BoxDecoration(
 | 
				
			||||||
 | 
					              color: Theme.of(context).colorScheme.surfaceContainerHighest,
 | 
				
			||||||
 | 
					              borderRadius: BorderRadius.all(Radius.circular(8.0)),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          TableConfig(
 | 
				
			||||||
 | 
					            wrapper:
 | 
				
			||||||
 | 
					                (child) => SingleChildScrollView(
 | 
				
			||||||
 | 
					                  scrollDirection: Axis.horizontal,
 | 
				
			||||||
 | 
					                  child: child,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
          LinkConfig(
 | 
					          LinkConfig(
 | 
				
			||||||
            style:
 | 
					            style:
 | 
				
			||||||
                linkStyle ??
 | 
					                linkStyle ??
 | 
				
			||||||
@@ -160,7 +176,7 @@ class MarkdownTextContent extends HookConsumerWidget {
 | 
				
			|||||||
                          uri: stickerUri,
 | 
					                          uri: stickerUri,
 | 
				
			||||||
                          width: size,
 | 
					                          width: size,
 | 
				
			||||||
                          height: size,
 | 
					                          height: size,
 | 
				
			||||||
                          fit: BoxFit.cover,
 | 
					                          fit: BoxFit.contain,
 | 
				
			||||||
                          noCacheOptimization: true,
 | 
					                          noCacheOptimization: true,
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -248,7 +248,7 @@ class _PaymentContentState extends ConsumerState<_PaymentContent> {
 | 
				
			|||||||
    try {
 | 
					    try {
 | 
				
			||||||
      final client = ref.read(apiClientProvider);
 | 
					      final client = ref.read(apiClientProvider);
 | 
				
			||||||
      final response = await client.post(
 | 
					      final response = await client.post(
 | 
				
			||||||
        '/orders/${widget.order.id}/pay',
 | 
					        '/id/orders/${widget.order.id}/pay',
 | 
				
			||||||
        data: {'pin_code': pin},
 | 
					        data: {'pin_code': pin},
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -273,7 +273,7 @@ class PostItem extends HookConsumerWidget {
 | 
				
			|||||||
            : item.reactionsCount.entries
 | 
					            : item.reactionsCount.entries
 | 
				
			||||||
                .sortedBy((e) => e.value)
 | 
					                .sortedBy((e) => e.value)
 | 
				
			||||||
                .map((e) => e.key)
 | 
					                .map((e) => e.key)
 | 
				
			||||||
                .first;
 | 
					                .last;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final postLanguage =
 | 
					    final postLanguage =
 | 
				
			||||||
        item.content != null
 | 
					        item.content != null
 | 
				
			||||||
@@ -480,7 +480,9 @@ class PostItem extends HookConsumerWidget {
 | 
				
			|||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          )
 | 
					          )
 | 
				
			||||||
        else if (item.content?.isNotEmpty ?? false)
 | 
					        else if ((item.content?.isNotEmpty ?? false) ||
 | 
				
			||||||
 | 
					            (item.title?.isNotEmpty ?? false) ||
 | 
				
			||||||
 | 
					            (item.description?.isNotEmpty ?? false))
 | 
				
			||||||
          Padding(
 | 
					          Padding(
 | 
				
			||||||
            padding: EdgeInsets.only(
 | 
					            padding: EdgeInsets.only(
 | 
				
			||||||
              left: renderingPadding.horizontal,
 | 
					              left: renderingPadding.horizontal,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,13 @@
 | 
				
			|||||||
import 'package:dio/dio.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:island/models/post.dart';
 | 
					import 'package:island/models/post.dart';
 | 
				
			||||||
import 'package:island/models/publisher.dart';
 | 
					import 'package:island/models/publisher.dart';
 | 
				
			||||||
import 'package:island/pods/network.dart';
 | 
					import 'package:island/pods/network.dart';
 | 
				
			||||||
import 'package:island/screens/creators/publishers.dart';
 | 
					import 'package:island/screens/creators/publishers.dart';
 | 
				
			||||||
 | 
					import 'package:island/screens/posts/compose.dart';
 | 
				
			||||||
import 'package:island/widgets/alert.dart';
 | 
					import 'package:island/widgets/alert.dart';
 | 
				
			||||||
import 'package:island/widgets/content/cloud_files.dart';
 | 
					import 'package:island/widgets/content/cloud_files.dart';
 | 
				
			||||||
import 'package:island/widgets/post/publishers_modal.dart';
 | 
					import 'package:island/widgets/post/publishers_modal.dart';
 | 
				
			||||||
@@ -14,8 +16,14 @@ import 'package:styled_widget/styled_widget.dart';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class PostQuickReply extends HookConsumerWidget {
 | 
					class PostQuickReply extends HookConsumerWidget {
 | 
				
			||||||
  final SnPost parent;
 | 
					  final SnPost parent;
 | 
				
			||||||
  final Function? onPosted;
 | 
					  final VoidCallback? onPosted;
 | 
				
			||||||
  const PostQuickReply({super.key, required this.parent, this.onPosted});
 | 
					  final VoidCallback? onLaunch;
 | 
				
			||||||
 | 
					  const PostQuickReply({
 | 
				
			||||||
 | 
					    super.key,
 | 
				
			||||||
 | 
					    required this.parent,
 | 
				
			||||||
 | 
					    this.onPosted,
 | 
				
			||||||
 | 
					    this.onLaunch,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
@@ -48,7 +56,7 @@ class PostQuickReply extends HookConsumerWidget {
 | 
				
			|||||||
            'content': contentController.text,
 | 
					            'content': contentController.text,
 | 
				
			||||||
            'replied_post_id': parent.id,
 | 
					            'replied_post_id': parent.id,
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          options: Options(headers: {'X-Pub': currentPublisher.value?.name}),
 | 
					          queryParameters: {'pub': currentPublisher.value?.name},
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        contentController.clear();
 | 
					        contentController.clear();
 | 
				
			||||||
        onPosted?.call();
 | 
					        onPosted?.call();
 | 
				
			||||||
@@ -83,9 +91,10 @@ class PostQuickReply extends HookConsumerWidget {
 | 
				
			|||||||
                child: TextField(
 | 
					                child: TextField(
 | 
				
			||||||
                  controller: contentController,
 | 
					                  controller: contentController,
 | 
				
			||||||
                  decoration: InputDecoration(
 | 
					                  decoration: InputDecoration(
 | 
				
			||||||
                    hintText: 'Post your reply',
 | 
					                    hintText: 'postReplyPlaceholder'.tr(),
 | 
				
			||||||
                    border: const OutlineInputBorder(),
 | 
					                    border: InputBorder.none,
 | 
				
			||||||
                    isDense: true,
 | 
					                    isDense: true,
 | 
				
			||||||
 | 
					                    isCollapsed: true,
 | 
				
			||||||
                    contentPadding: EdgeInsets.symmetric(
 | 
					                    contentPadding: EdgeInsets.symmetric(
 | 
				
			||||||
                      horizontal: 12,
 | 
					                      horizontal: 12,
 | 
				
			||||||
                      vertical: 8,
 | 
					                      vertical: 8,
 | 
				
			||||||
@@ -97,6 +106,26 @@ class PostQuickReply extends HookConsumerWidget {
 | 
				
			|||||||
                      (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
					                      (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
 | 
					              IconButton(
 | 
				
			||||||
 | 
					                onPressed: () {
 | 
				
			||||||
 | 
					                  onLaunch?.call();
 | 
				
			||||||
 | 
					                  GoRouter.of(context)
 | 
				
			||||||
 | 
					                      .pushNamed(
 | 
				
			||||||
 | 
					                        'postCompose',
 | 
				
			||||||
 | 
					                        extra: PostComposeInitialState(
 | 
				
			||||||
 | 
					                          content: contentController.text,
 | 
				
			||||||
 | 
					                          replyingTo: parent,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      )
 | 
				
			||||||
 | 
					                      .then((value) {
 | 
				
			||||||
 | 
					                        if (value != null) onPosted?.call();
 | 
				
			||||||
 | 
					                      });
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                icon: const Icon(Symbols.launch, size: 20),
 | 
				
			||||||
 | 
					                padding: EdgeInsets.zero,
 | 
				
			||||||
 | 
					                visualDensity: VisualDensity.compact,
 | 
				
			||||||
 | 
					                constraints: const BoxConstraints(),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
              IconButton(
 | 
					              IconButton(
 | 
				
			||||||
                padding: EdgeInsets.zero,
 | 
					                padding: EdgeInsets.zero,
 | 
				
			||||||
                visualDensity: VisualDensity.compact,
 | 
					                visualDensity: VisualDensity.compact,
 | 
				
			||||||
@@ -110,6 +139,7 @@ class PostQuickReply extends HookConsumerWidget {
 | 
				
			|||||||
                        : Icon(Symbols.send, size: 20),
 | 
					                        : Icon(Symbols.send, size: 20),
 | 
				
			||||||
                color: Theme.of(context).colorScheme.primary,
 | 
					                color: Theme.of(context).colorScheme.primary,
 | 
				
			||||||
                onPressed: submitting.value ? null : performAction,
 | 
					                onPressed: submitting.value ? null : performAction,
 | 
				
			||||||
 | 
					                constraints: const BoxConstraints(),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,14 +38,18 @@ class PostRepliesSheet extends HookConsumerWidget {
 | 
				
			|||||||
          if (user.value != null)
 | 
					          if (user.value != null)
 | 
				
			||||||
            Material(
 | 
					            Material(
 | 
				
			||||||
              elevation: 2,
 | 
					              elevation: 2,
 | 
				
			||||||
 | 
					              color: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
				
			||||||
              child: PostQuickReply(
 | 
					              child: PostQuickReply(
 | 
				
			||||||
                parent: post,
 | 
					                parent: post,
 | 
				
			||||||
                onPosted: () {
 | 
					                onPosted: () {
 | 
				
			||||||
                  ref.invalidate(postRepliesNotifierProvider(post.id));
 | 
					                  ref.invalidate(postRepliesNotifierProvider(post.id));
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
 | 
					                onLaunch: () {
 | 
				
			||||||
 | 
					                  Navigator.of(context).pop();
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
              ).padding(
 | 
					              ).padding(
 | 
				
			||||||
                bottom: MediaQuery.of(context).padding.bottom + 16,
 | 
					                bottom: MediaQuery.of(context).padding.bottom + 8,
 | 
				
			||||||
                top: 16,
 | 
					                top: 8,
 | 
				
			||||||
                horizontal: 16,
 | 
					                horizontal: 16,
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -662,6 +662,14 @@ packages:
 | 
				
			|||||||
    description: flutter
 | 
					    description: flutter
 | 
				
			||||||
    source: sdk
 | 
					    source: sdk
 | 
				
			||||||
    version: "0.0.0"
 | 
					    version: "0.0.0"
 | 
				
			||||||
 | 
					  flutter_app_update:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: flutter_app_update
 | 
				
			||||||
 | 
					      sha256: "09290240949c4651581cd6fc535e52d019e189e694d6019c56b5a56c2e69ba65"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "3.2.2"
 | 
				
			||||||
  flutter_blurhash:
 | 
					  flutter_blurhash:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
					# 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
 | 
					# 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.
 | 
					# of the product and file versions while build-number is used as the build suffix.
 | 
				
			||||||
version: 3.1.0+120
 | 
					version: 3.1.0+121
 | 
				
			||||||
 | 
					
 | 
				
			||||||
environment:
 | 
					environment:
 | 
				
			||||||
  sdk: ^3.7.2
 | 
					  sdk: ^3.7.2
 | 
				
			||||||
@@ -133,6 +133,7 @@ dependencies:
 | 
				
			|||||||
  flutter_typeahead: ^5.2.0
 | 
					  flutter_typeahead: ^5.2.0
 | 
				
			||||||
  flutter_langdetect: ^0.0.2
 | 
					  flutter_langdetect: ^0.0.2
 | 
				
			||||||
  waveform_flutter: ^1.2.0
 | 
					  waveform_flutter: ^1.2.0
 | 
				
			||||||
 | 
					  flutter_app_update: ^3.2.2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dev_dependencies:
 | 
					dev_dependencies:
 | 
				
			||||||
  flutter_test:
 | 
					  flutter_test:
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user