Compare commits
	
		
			19 Commits
		
	
	
		
			6558854a7a
			...
			3.1.0+122
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8e0c0c6054 | |||
| f3d1183076 | |||
| a9f7f0cce0 | |||
| f2943f8411 | |||
| 808e7dcffa | |||
| 9bed4fa6fb | |||
| e6255a340b | |||
| 78bf319fb7 | |||
| 36a966d582 | |||
| f72b268d36 | |||
| 44ef31034e | |||
| 229dc2186f | |||
| a2f9a1efb4 | |||
| 
						 | 
					823e3c5de6 | ||
| 
						 | 
					faac7bac35 | ||
| 1fac1bfe02 | |||
| 9394b1d9c8 | |||
| 43dd13bac4 | |||
| 65bc372103 | 
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/drawable/ic_notification.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								android/app/src/main/res/drawable/ic_notification.png
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 70 KiB  | 
@@ -706,6 +706,7 @@
 | 
			
		||||
  "copyToClipboardTooltip": "Copy to clipboard",
 | 
			
		||||
  "postForwardingTo": "Forwarding to",
 | 
			
		||||
  "postReplyingTo": "Replying to",
 | 
			
		||||
  "postReplyPlaceholder": "Post your reply",
 | 
			
		||||
  "postEditing": "You are editing an existing post",
 | 
			
		||||
  "postArticle": "Article",
 | 
			
		||||
  "aboutDeviceName": "Device Name",
 | 
			
		||||
@@ -787,5 +788,6 @@
 | 
			
		||||
  "addLink": "Add link",
 | 
			
		||||
  "linkKey": "Link Name",
 | 
			
		||||
  "linkValue": "URL",
 | 
			
		||||
  "debugOptions": "Debug Options"
 | 
			
		||||
  "debugOptions": "Debug Options",
 | 
			
		||||
  "joinedAt": "Joined at {}"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -73,6 +73,8 @@ PODS:
 | 
			
		||||
    - GoogleUtilities/UserDefaults (~> 8.1)
 | 
			
		||||
    - nanopb (~> 3.30910.0)
 | 
			
		||||
  - Flutter (1.0.0)
 | 
			
		||||
  - flutter_app_update (0.0.1):
 | 
			
		||||
    - Flutter
 | 
			
		||||
  - flutter_inappwebview_ios (0.0.1):
 | 
			
		||||
    - Flutter
 | 
			
		||||
    - flutter_inappwebview_ios/Core (= 0.0.1)
 | 
			
		||||
@@ -178,25 +180,25 @@ PODS:
 | 
			
		||||
  - sqflite_darwin (0.0.4):
 | 
			
		||||
    - Flutter
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
  - sqlite3 (3.50.3):
 | 
			
		||||
    - sqlite3/common (= 3.50.3)
 | 
			
		||||
  - sqlite3/common (3.50.3)
 | 
			
		||||
  - sqlite3/dbstatvtab (3.50.3):
 | 
			
		||||
  - sqlite3 (3.50.4):
 | 
			
		||||
    - sqlite3/common (= 3.50.4)
 | 
			
		||||
  - sqlite3/common (3.50.4)
 | 
			
		||||
  - sqlite3/dbstatvtab (3.50.4):
 | 
			
		||||
    - sqlite3/common
 | 
			
		||||
  - sqlite3/fts5 (3.50.3):
 | 
			
		||||
  - sqlite3/fts5 (3.50.4):
 | 
			
		||||
    - sqlite3/common
 | 
			
		||||
  - sqlite3/math (3.50.3):
 | 
			
		||||
  - sqlite3/math (3.50.4):
 | 
			
		||||
    - sqlite3/common
 | 
			
		||||
  - sqlite3/perf-threadsafe (3.50.3):
 | 
			
		||||
  - sqlite3/perf-threadsafe (3.50.4):
 | 
			
		||||
    - sqlite3/common
 | 
			
		||||
  - sqlite3/rtree (3.50.3):
 | 
			
		||||
  - sqlite3/rtree (3.50.4):
 | 
			
		||||
    - sqlite3/common
 | 
			
		||||
  - sqlite3/session (3.50.3):
 | 
			
		||||
  - sqlite3/session (3.50.4):
 | 
			
		||||
    - sqlite3/common
 | 
			
		||||
  - sqlite3_flutter_libs (0.0.1):
 | 
			
		||||
    - Flutter
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
    - sqlite3 (~> 3.50.3)
 | 
			
		||||
    - sqlite3 (~> 3.50.4)
 | 
			
		||||
    - sqlite3/dbstatvtab
 | 
			
		||||
    - sqlite3/fts5
 | 
			
		||||
    - sqlite3/math
 | 
			
		||||
@@ -223,6 +225,7 @@ DEPENDENCIES:
 | 
			
		||||
  - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
 | 
			
		||||
  - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
 | 
			
		||||
  - Flutter (from `Flutter`)
 | 
			
		||||
  - flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
 | 
			
		||||
  - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
 | 
			
		||||
  - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
 | 
			
		||||
  - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
 | 
			
		||||
@@ -293,6 +296,8 @@ EXTERNAL SOURCES:
 | 
			
		||||
    :path: ".symlinks/plugins/firebase_messaging/ios"
 | 
			
		||||
  Flutter:
 | 
			
		||||
    :path: Flutter
 | 
			
		||||
  flutter_app_update:
 | 
			
		||||
    :path: ".symlinks/plugins/flutter_app_update/ios"
 | 
			
		||||
  flutter_inappwebview_ios:
 | 
			
		||||
    :path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
 | 
			
		||||
  flutter_keyboard_visibility:
 | 
			
		||||
@@ -372,6 +377,7 @@ SPEC CHECKSUMS:
 | 
			
		||||
  FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988
 | 
			
		||||
  FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde
 | 
			
		||||
  Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
 | 
			
		||||
  flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
 | 
			
		||||
  flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
 | 
			
		||||
  flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
 | 
			
		||||
  flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
 | 
			
		||||
@@ -406,8 +412,8 @@ SPEC CHECKSUMS:
 | 
			
		||||
  shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
 | 
			
		||||
  sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
 | 
			
		||||
  sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
 | 
			
		||||
  sqlite3: 83105acd294c9137c026e2da1931c30b4588ab81
 | 
			
		||||
  sqlite3_flutter_libs: 616267f2fca40e9c6af8c5d82324e05667040b6e
 | 
			
		||||
  sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
 | 
			
		||||
  sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
 | 
			
		||||
  super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
 | 
			
		||||
  SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
 | 
			
		||||
  url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@ class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate {
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        let serverUrl = UserDefaults.standard.getServerUrl()
 | 
			
		||||
        let url = "\(serverUrl)/chat/\(metadata["room_id"] ?? "")/messages"
 | 
			
		||||
        let url = "\(serverUrl)/sphere/chat/\(metadata["room_id"] ?? "")/messages"
 | 
			
		||||
        
 | 
			
		||||
        let parameters: [String: Any?] = [
 | 
			
		||||
            "content": textResponse.userText,
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
func getAttachmentUrl(for identifier: String) -> String {
 | 
			
		||||
    let serverBaseUrl = "https://nt.solian.app"
 | 
			
		||||
    let serverBaseUrl = "https://api.solian.app"
 | 
			
		||||
    
 | 
			
		||||
    return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/files/\(identifier)"
 | 
			
		||||
    return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/drive/files/\(identifier)"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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:url_launcher/url_launcher_string.dart';
 | 
			
		||||
import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect;
 | 
			
		||||
import 'package:island/services/update_service.dart';
 | 
			
		||||
 | 
			
		||||
@pragma('vm:entry-point')
 | 
			
		||||
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
 | 
			
		||||
@@ -181,6 +171,9 @@ class IslandApp extends HookConsumerWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    useEffect(() {
 | 
			
		||||
      if (!kIsWeb && Platform.isLinux) {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
      const channel = MethodChannel('dev.solsynth.solian/notifications');
 | 
			
		||||
 | 
			
		||||
      Future<void> handleInitialLink() async {
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,32 @@ sealed class SnAccount with _$SnAccount {
 | 
			
		||||
      _$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
 | 
			
		||||
sealed class SnAccountProfile with _$SnAccountProfile {
 | 
			
		||||
  const factory SnAccountProfile({
 | 
			
		||||
@@ -38,7 +64,7 @@ sealed class SnAccountProfile with _$SnAccountProfile {
 | 
			
		||||
    @Default('') String location,
 | 
			
		||||
    @Default('') String timeZone,
 | 
			
		||||
    DateTime? birthday,
 | 
			
		||||
    @Default({}) Map<String, String> links,
 | 
			
		||||
    @ProfileLinkConverter() @Default([]) List<ProfileLink> links,
 | 
			
		||||
    DateTime? lastSeenAt,
 | 
			
		||||
    SnAccountBadge? activeBadge,
 | 
			
		||||
    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
 | 
			
		||||
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
 | 
			
		||||
/// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
@@ -383,7 +643,7 @@ abstract mixin class $SnAccountProfileCopyWith<$Res>  {
 | 
			
		||||
  factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl;
 | 
			
		||||
@useResult
 | 
			
		||||
$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,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 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 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
 | 
			
		||||
@@ -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) {
 | 
			
		||||
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 _:
 | 
			
		||||
@@ -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) {
 | 
			
		||||
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);}
 | 
			
		||||
@@ -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) {
 | 
			
		||||
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 _:
 | 
			
		||||
@@ -607,7 +867,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
 | 
			
		||||
@JsonSerializable()
 | 
			
		||||
 | 
			
		||||
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);
 | 
			
		||||
 | 
			
		||||
@override final  String id;
 | 
			
		||||
@@ -620,11 +880,11 @@ class _SnAccountProfile implements SnAccountProfile {
 | 
			
		||||
@override@JsonKey() final  String location;
 | 
			
		||||
@override@JsonKey() final  String timeZone;
 | 
			
		||||
@override final  DateTime? birthday;
 | 
			
		||||
 final  Map<String, String> _links;
 | 
			
		||||
@override@JsonKey() Map<String, String> get links {
 | 
			
		||||
  if (_links is EqualUnmodifiableMapView) return _links;
 | 
			
		||||
 final  List<ProfileLink> _links;
 | 
			
		||||
@override@JsonKey()@ProfileLinkConverter() List<ProfileLink> get links {
 | 
			
		||||
  if (_links is EqualUnmodifiableListView) return _links;
 | 
			
		||||
  // ignore: implicit_dynamic_type
 | 
			
		||||
  return EqualUnmodifiableMapView(_links);
 | 
			
		||||
  return EqualUnmodifiableListView(_links);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@override final  DateTime? lastSeenAt;
 | 
			
		||||
@@ -672,7 +932,7 @@ abstract mixin class _$SnAccountProfileCopyWith<$Res> implements $SnAccountProfi
 | 
			
		||||
  factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl;
 | 
			
		||||
@override @useResult
 | 
			
		||||
$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,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 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 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
 | 
			
		||||
 
 | 
			
		||||
@@ -47,6 +47,12 @@ Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) =>
 | 
			
		||||
      '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(
 | 
			
		||||
      id: json['id'] as String,
 | 
			
		||||
@@ -63,10 +69,9 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
 | 
			
		||||
              ? null
 | 
			
		||||
              : DateTime.parse(json['birthday'] as String),
 | 
			
		||||
      links:
 | 
			
		||||
          (json['links'] as Map<String, dynamic>?)?.map(
 | 
			
		||||
            (k, e) => MapEntry(k, e as String),
 | 
			
		||||
          ) ??
 | 
			
		||||
          const {},
 | 
			
		||||
          json['links'] == null
 | 
			
		||||
              ? const []
 | 
			
		||||
              : const ProfileLinkConverter().fromJson(json['links']),
 | 
			
		||||
      lastSeenAt:
 | 
			
		||||
          json['last_seen_at'] == null
 | 
			
		||||
              ? null
 | 
			
		||||
@@ -116,7 +121,7 @@ Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
 | 
			
		||||
      'location': instance.location,
 | 
			
		||||
      'time_zone': instance.timeZone,
 | 
			
		||||
      'birthday': instance.birthday?.toIso8601String(),
 | 
			
		||||
      'links': instance.links,
 | 
			
		||||
      'links': const ProfileLinkConverter().toJson(instance.links),
 | 
			
		||||
      'last_seen_at': instance.lastSeenAt?.toIso8601String(),
 | 
			
		||||
      'active_badge': instance.activeBadge?.toJson(),
 | 
			
		||||
      'experience': instance.experience,
 | 
			
		||||
 
 | 
			
		||||
@@ -7,12 +7,12 @@ import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:island/services/udid.native.dart';
 | 
			
		||||
import 'package:island/widgets/alert.dart';
 | 
			
		||||
import 'package:island/widgets/app_scaffold.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:package_info_plus/package_info_plus.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:island/services/update_service.dart';
 | 
			
		||||
import 'package:island/widgets/content/sheet.dart';
 | 
			
		||||
import 'package:url_launcher/url_launcher.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.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
 | 
			
		||||
                                final svc = UpdateService();
 | 
			
		||||
                                // Reuse service fetch + compare to decide content
 | 
			
		||||
                                showLoadingModal(context);
 | 
			
		||||
                                final release = await svc.fetchLatestRelease();
 | 
			
		||||
                                if (!context.mounted) return;
 | 
			
		||||
                                hideLoadingModal(context);
 | 
			
		||||
                                if (release != null) {
 | 
			
		||||
                                  await svc.showUpdateSheet(context, release);
 | 
			
		||||
                                } else {
 | 
			
		||||
                                  // Fallback: show a simple sheet indicating no info
 | 
			
		||||
                                  // Use your SheetScaffold for consistent styling
 | 
			
		||||
                                  // Show a minimal message
 | 
			
		||||
                                  // 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.',
 | 
			
		||||
                                              ),
 | 
			
		||||
                                            ),
 | 
			
		||||
                                          ),
 | 
			
		||||
                                        ),
 | 
			
		||||
                                  showInfoAlert(
 | 
			
		||||
                                    'Currently cannot get update from the GitHub.',
 | 
			
		||||
                                    'Unable to check for updates',
 | 
			
		||||
                                  );
 | 
			
		||||
                                }
 | 
			
		||||
                              },
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:image_picker/image_picker.dart';
 | 
			
		||||
import 'package:island/models/file.dart';
 | 
			
		||||
import 'package:island/models/user.dart';
 | 
			
		||||
import 'package:island/pods/config.dart';
 | 
			
		||||
import 'package:island/pods/network.dart';
 | 
			
		||||
import 'package:island/pods/userinfo.dart';
 | 
			
		||||
@@ -95,11 +96,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
 | 
			
		||||
    final usernameController = useTextEditingController(text: user.value!.name);
 | 
			
		||||
    final nicknameController = useTextEditingController(text: user.value!.nick);
 | 
			
		||||
    final language = useState(user.value!.language);
 | 
			
		||||
    final links = useState<List<Map<String, String>>>(
 | 
			
		||||
      user.value!.profile.links.entries
 | 
			
		||||
          .map((e) => {'key': e.key, 'value': e.value})
 | 
			
		||||
          .toList(),
 | 
			
		||||
    );
 | 
			
		||||
    final links = useState<List<ProfileLink>>(user.value!.profile.links);
 | 
			
		||||
 | 
			
		||||
    void updateBasicInfo() async {
 | 
			
		||||
      if (!formKeyBasicInfo.currentState!.validate()) return;
 | 
			
		||||
@@ -171,7 +168,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
 | 
			
		||||
            'location': locationController.text,
 | 
			
		||||
            'time_zone': timeZoneController.text,
 | 
			
		||||
            '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);
 | 
			
		||||
@@ -575,13 +572,15 @@ class UpdateProfileScreen extends HookConsumerWidget {
 | 
			
		||||
                          children: [
 | 
			
		||||
                            Expanded(
 | 
			
		||||
                              child: TextFormField(
 | 
			
		||||
                                initialValue: links.value[i]['key'],
 | 
			
		||||
                                initialValue: links.value[i].name,
 | 
			
		||||
                                decoration: InputDecoration(
 | 
			
		||||
                                  labelText: 'linkKey'.tr(),
 | 
			
		||||
                                  isDense: true,
 | 
			
		||||
                                ),
 | 
			
		||||
                                onChanged: (value) {
 | 
			
		||||
                                  links.value[i]['key'] = value;
 | 
			
		||||
                                  links.value[i] = links.value[i].copyWith(
 | 
			
		||||
                                    name: value,
 | 
			
		||||
                                  );
 | 
			
		||||
                                },
 | 
			
		||||
                                onTapOutside:
 | 
			
		||||
                                    (_) =>
 | 
			
		||||
@@ -592,13 +591,15 @@ class UpdateProfileScreen extends HookConsumerWidget {
 | 
			
		||||
                            const Gap(8),
 | 
			
		||||
                            Expanded(
 | 
			
		||||
                              child: TextFormField(
 | 
			
		||||
                                initialValue: links.value[i]['value'],
 | 
			
		||||
                                initialValue: links.value[i].url,
 | 
			
		||||
                                decoration: InputDecoration(
 | 
			
		||||
                                  labelText: 'linkValue'.tr(),
 | 
			
		||||
                                  isDense: true,
 | 
			
		||||
                                ),
 | 
			
		||||
                                onChanged: (value) {
 | 
			
		||||
                                  links.value[i]['value'] = value;
 | 
			
		||||
                                  links.value[i] = links.value[i].copyWith(
 | 
			
		||||
                                    url: value,
 | 
			
		||||
                                  );
 | 
			
		||||
                                },
 | 
			
		||||
                                onTapOutside:
 | 
			
		||||
                                    (_) =>
 | 
			
		||||
@@ -620,7 +621,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
 | 
			
		||||
                        child: FilledButton.icon(
 | 
			
		||||
                          onPressed: () {
 | 
			
		||||
                            links.value = List.from(links.value)
 | 
			
		||||
                              ..add({'key': '', 'value': ''});
 | 
			
		||||
                              ..add(ProfileLink(name: '', url: ''));
 | 
			
		||||
                          },
 | 
			
		||||
                          label: Text('addLink').tr(),
 | 
			
		||||
                          icon: const Icon(Symbols.add),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
@@ -196,6 +197,15 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
    List<Widget> buildSubcolumn(SnAccount data) {
 | 
			
		||||
      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)
 | 
			
		||||
          Row(
 | 
			
		||||
            spacing: 6,
 | 
			
		||||
@@ -322,7 +332,7 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
			
		||||
              spacing: 2,
 | 
			
		||||
              children: buildSubcolumn(data),
 | 
			
		||||
            ),
 | 
			
		||||
          if (data.profile.timeZone.isNotEmpty)
 | 
			
		||||
          if (data.profile.timeZone.isNotEmpty && !kIsWeb)
 | 
			
		||||
            Column(
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
              children: [
 | 
			
		||||
@@ -357,17 +367,21 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
			
		||||
        crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
        children: [
 | 
			
		||||
          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(
 | 
			
		||||
              title: Text(link.key.capitalizeEachWord()),
 | 
			
		||||
              subtitle: Text(link.value),
 | 
			
		||||
              title: Text(link.name.capitalizeEachWord()),
 | 
			
		||||
              subtitle: Text(link.url),
 | 
			
		||||
              contentPadding: EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
              trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
              shape: RoundedRectangleBorder(
 | 
			
		||||
                borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
              ),
 | 
			
		||||
              onTap: () {
 | 
			
		||||
                launchUrlString(link.value);
 | 
			
		||||
                if (!link.url.startsWith('http') && !link.url.contains('://')) {
 | 
			
		||||
                  launchUrlString('https://${link.url}');
 | 
			
		||||
                } else {
 | 
			
		||||
                  launchUrlString(link.url);
 | 
			
		||||
                }
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
        ],
 | 
			
		||||
@@ -561,6 +575,7 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
			
		||||
                              SliverToBoxAdapter(
 | 
			
		||||
                                child: accountProfileBio(data).padding(top: 4),
 | 
			
		||||
                              ),
 | 
			
		||||
                              if (data.profile.links.isNotEmpty)
 | 
			
		||||
                                SliverToBoxAdapter(
 | 
			
		||||
                                  child: accountProfileLinks(data),
 | 
			
		||||
                                ),
 | 
			
		||||
@@ -660,6 +675,7 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
			
		||||
                        SliverToBoxAdapter(
 | 
			
		||||
                          child: accountProfileBio(data).padding(horizontal: 4),
 | 
			
		||||
                        ),
 | 
			
		||||
                        if (data.profile.links.isNotEmpty)
 | 
			
		||||
                          SliverToBoxAdapter(
 | 
			
		||||
                            child: accountProfileLinks(
 | 
			
		||||
                              data,
 | 
			
		||||
 
 | 
			
		||||
@@ -339,7 +339,7 @@ class ChatRoomScreen extends HookConsumerWidget {
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            await apiClient.post(
 | 
			
		||||
                              '/chat/${chatRoom.value!.id}/members/me',
 | 
			
		||||
                              '/sphere/chat/${chatRoom.value!.id}/members/me',
 | 
			
		||||
                            );
 | 
			
		||||
                            ref.invalidate(chatroomIdentityProvider(id));
 | 
			
		||||
                          } catch (err) {
 | 
			
		||||
@@ -929,7 +929,7 @@ class ChatRoomScreen extends HookConsumerWidget {
 | 
			
		||||
                            if (attachment.isOnCloud) {
 | 
			
		||||
                              final client = ref.watch(apiClientProvider);
 | 
			
		||||
                              await client.delete(
 | 
			
		||||
                                '/files/${attachment.data.id}',
 | 
			
		||||
                                '/drive/files/${attachment.data.id}',
 | 
			
		||||
                              );
 | 
			
		||||
                            }
 | 
			
		||||
                            final clone = List.of(attachments.value);
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:island/models/poll.dart';
 | 
			
		||||
import 'package:island/pods/network.dart';
 | 
			
		||||
import 'package:island/widgets/app_scaffold.dart';
 | 
			
		||||
import 'package:island/widgets/poll/poll_feedback.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
			
		||||
@@ -70,7 +71,7 @@ class CreatorPollListScreen extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(title: const Text('Polls')),
 | 
			
		||||
      floatingActionButton: FloatingActionButton(
 | 
			
		||||
        onPressed: () => _createPoll(context),
 | 
			
		||||
 
 | 
			
		||||
@@ -58,7 +58,7 @@ class StickerPackDetailScreen extends HookConsumerWidget {
 | 
			
		||||
      try {
 | 
			
		||||
        showLoadingModal(context);
 | 
			
		||||
        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));
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        showErrorAlert(err);
 | 
			
		||||
@@ -297,7 +297,7 @@ class _StickerPackActionMenu extends HookConsumerWidget {
 | 
			
		||||
                ).then((confirm) {
 | 
			
		||||
                  if (confirm) {
 | 
			
		||||
                    final client = ref.watch(apiClientProvider);
 | 
			
		||||
                    client.delete('/stickers/$packId');
 | 
			
		||||
                    client.delete('/sphere/stickers/$packId');
 | 
			
		||||
                    ref.invalidate(stickerPacksNotifierProvider);
 | 
			
		||||
                    if (context.mounted) context.pop(true);
 | 
			
		||||
                  }
 | 
			
		||||
@@ -325,7 +325,7 @@ Future<SnSticker?> stickerPackSticker(
 | 
			
		||||
  if (query == null) return null;
 | 
			
		||||
  final apiClient = ref.watch(apiClientProvider);
 | 
			
		||||
  final resp = await apiClient.get(
 | 
			
		||||
    '/stickers/${query.packId}/content/${query.id}',
 | 
			
		||||
    '/sphere/stickers/${query.packId}/content/${query.id}',
 | 
			
		||||
  );
 | 
			
		||||
  if (resp.data == null) return null;
 | 
			
		||||
  return SnSticker.fromJson(resp.data);
 | 
			
		||||
@@ -379,8 +379,8 @@ class EditStickersScreen extends HookConsumerWidget {
 | 
			
		||||
      try {
 | 
			
		||||
        final resp = await apiClient.request(
 | 
			
		||||
          id == null
 | 
			
		||||
              ? '/stickers/$packId/content'
 | 
			
		||||
              : '/stickers/$packId/content/$id',
 | 
			
		||||
              ? '/sphere/stickers/$packId/content'
 | 
			
		||||
              : '/sphere/stickers/$packId/content/$id',
 | 
			
		||||
          data: {'slug': slugController.text, 'image_id': imageController.text},
 | 
			
		||||
          options: Options(method: id == null ? 'POST' : 'PATCH'),
 | 
			
		||||
        );
 | 
			
		||||
 
 | 
			
		||||
@@ -151,7 +151,7 @@ class _StickerPackContentProviderElement
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
String _$stickerPackStickerHash() =>
 | 
			
		||||
    r'36f524c047e632236d5597aaaa8678ed86599602';
 | 
			
		||||
    r'5c553666b3a63530bdebae4b7cd52f303c5ab3a0';
 | 
			
		||||
 | 
			
		||||
/// See also [stickerPackSticker].
 | 
			
		||||
@ProviderFor(stickerPackSticker)
 | 
			
		||||
 
 | 
			
		||||
@@ -114,10 +114,11 @@ class WebFeedEditScreen extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
    return feedAsync.when(
 | 
			
		||||
      loading:
 | 
			
		||||
          () =>
 | 
			
		||||
              const Scaffold(body: Center(child: CircularProgressIndicator())),
 | 
			
		||||
          () => const AppScaffold(
 | 
			
		||||
            body: Center(child: CircularProgressIndicator()),
 | 
			
		||||
          ),
 | 
			
		||||
      error:
 | 
			
		||||
          (error, stack) => Scaffold(
 | 
			
		||||
          (error, stack) => AppScaffold(
 | 
			
		||||
            appBar: AppBar(title: const Text('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/widgets/alert.dart';
 | 
			
		||||
import 'package:island/models/poll.dart';
 | 
			
		||||
import 'package:island/widgets/app_scaffold.dart';
 | 
			
		||||
import 'package:uuid/uuid.dart';
 | 
			
		||||
 | 
			
		||||
class PollEditorState {
 | 
			
		||||
@@ -413,7 +414,7 @@ class PollEditorScreen extends ConsumerWidget {
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: Text(model.id == null ? 'Create Poll' : 'Edit Poll'),
 | 
			
		||||
        actions: [
 | 
			
		||||
@@ -428,7 +429,9 @@ class PollEditorScreen extends ConsumerWidget {
 | 
			
		||||
          const Gap(8),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      body: SafeArea(
 | 
			
		||||
      body: Column(
 | 
			
		||||
        children: [
 | 
			
		||||
          Expanded(
 | 
			
		||||
            child: Form(
 | 
			
		||||
              key: ValueKey(model.id),
 | 
			
		||||
              child: ListView(
 | 
			
		||||
@@ -512,7 +515,8 @@ class PollEditorScreen extends ConsumerWidget {
 | 
			
		||||
                  if (model.questions.isEmpty)
 | 
			
		||||
                    _EmptyState(
 | 
			
		||||
                      title: 'No questions yet',
 | 
			
		||||
                  subtitle: 'Use "Add question" to start building your poll.',
 | 
			
		||||
                      subtitle:
 | 
			
		||||
                          'Use "Add question" to start building your poll.',
 | 
			
		||||
                    )
 | 
			
		||||
                  else
 | 
			
		||||
                    ReorderableListView.builder(
 | 
			
		||||
@@ -559,7 +563,10 @@ class PollEditorScreen extends ConsumerWidget {
 | 
			
		||||
                              const Divider(height: 1),
 | 
			
		||||
                              Padding(
 | 
			
		||||
                                padding: const EdgeInsets.all(16),
 | 
			
		||||
                            child: _QuestionEditor(index: index, question: q),
 | 
			
		||||
                                child: _QuestionEditor(
 | 
			
		||||
                                  index: index,
 | 
			
		||||
                                  question: q,
 | 
			
		||||
                                ),
 | 
			
		||||
                              ),
 | 
			
		||||
                            ],
 | 
			
		||||
                          ),
 | 
			
		||||
@@ -571,14 +578,7 @@ class PollEditorScreen extends ConsumerWidget {
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
      bottomNavigationBar: Padding(
 | 
			
		||||
        padding: EdgeInsets.fromLTRB(
 | 
			
		||||
          16,
 | 
			
		||||
          8,
 | 
			
		||||
          16,
 | 
			
		||||
          16 + MediaQuery.of(context).padding.bottom,
 | 
			
		||||
        ),
 | 
			
		||||
        child: Row(
 | 
			
		||||
          Row(
 | 
			
		||||
            children: [
 | 
			
		||||
              OutlinedButton.icon(
 | 
			
		||||
                onPressed: () {
 | 
			
		||||
@@ -597,6 +597,7 @@ class PollEditorScreen extends ConsumerWidget {
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -92,6 +92,7 @@ class PostDetailScreen extends HookConsumerWidget {
 | 
			
		||||
                  right: 0,
 | 
			
		||||
                  child: Material(
 | 
			
		||||
                    elevation: 2,
 | 
			
		||||
                    color: Theme.of(context).colorScheme.surfaceContainer,
 | 
			
		||||
                    child: postState
 | 
			
		||||
                        .when(
 | 
			
		||||
                          data:
 | 
			
		||||
@@ -107,8 +108,8 @@ class PostDetailScreen extends HookConsumerWidget {
 | 
			
		||||
                          error: (_, _) => const SizedBox.shrink(),
 | 
			
		||||
                        )
 | 
			
		||||
                        .padding(
 | 
			
		||||
                          bottom: MediaQuery.of(context).padding.bottom + 16,
 | 
			
		||||
                          top: 16,
 | 
			
		||||
                          bottom: MediaQuery.of(context).padding.bottom + 8,
 | 
			
		||||
                          top: 8,
 | 
			
		||||
                          horizontal: 16,
 | 
			
		||||
                        ),
 | 
			
		||||
                  ),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,28 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
import 'dart:developer';
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:flutter/foundation.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:collection/collection.dart'; // Added for firstWhereOrNull
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:url_launcher/url_launcher.dart';
 | 
			
		||||
import 'package:island/widgets/content/sheet.dart';
 | 
			
		||||
 | 
			
		||||
/// Data model for a GitHub release we care about
 | 
			
		||||
class GithubReleaseInfo {
 | 
			
		||||
  final String tagName; // e.g. 3.1.0+118
 | 
			
		||||
  final String name; // release title
 | 
			
		||||
  final String body; // changelog markdown
 | 
			
		||||
  final String htmlUrl; // release page
 | 
			
		||||
  final String tagName;
 | 
			
		||||
  final String name;
 | 
			
		||||
  final String body;
 | 
			
		||||
  final String htmlUrl;
 | 
			
		||||
  final DateTime createdAt;
 | 
			
		||||
  final List<GithubReleaseAsset> assets;
 | 
			
		||||
 | 
			
		||||
  const GithubReleaseInfo({
 | 
			
		||||
    required this.tagName,
 | 
			
		||||
@@ -21,9 +30,28 @@ class GithubReleaseInfo {
 | 
			
		||||
    required this.body,
 | 
			
		||||
    required this.htmlUrl,
 | 
			
		||||
    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"
 | 
			
		||||
class _ParsedVersion implements Comparable<_ParsedVersion> {
 | 
			
		||||
  final int major;
 | 
			
		||||
@@ -62,7 +90,7 @@ class _ParsedVersion implements Comparable<_ParsedVersion> {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class UpdateService {
 | 
			
		||||
  UpdateService({Dio? dio})
 | 
			
		||||
  UpdateService({Dio? dio, this.useProxy = false})
 | 
			
		||||
    : _dio =
 | 
			
		||||
          dio ??
 | 
			
		||||
          Dio(
 | 
			
		||||
@@ -78,6 +106,9 @@ class UpdateService {
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
  final Dio _dio;
 | 
			
		||||
  final bool useProxy;
 | 
			
		||||
 | 
			
		||||
  static const _proxyBaseUrl = 'https://ghfast.top/';
 | 
			
		||||
 | 
			
		||||
  static const _releasesLatestApi =
 | 
			
		||||
      '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.
 | 
			
		||||
  /// If update is available, shows a bottom sheet with changelog and an action to open release page.
 | 
			
		||||
  Future<void> checkForUpdates(BuildContext context) async {
 | 
			
		||||
    log('[Update] Checking for updates...');
 | 
			
		||||
    try {
 | 
			
		||||
      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 localVersionStr = '${info.version}+${info.buildNumber}';
 | 
			
		||||
      log('[Update] Local app version: $localVersionStr');
 | 
			
		||||
 | 
			
		||||
      final latest = _ParsedVersion.tryParse(release.tagName);
 | 
			
		||||
      final local = _ParsedVersion.tryParse(localVersionStr);
 | 
			
		||||
 | 
			
		||||
      if (latest == null || local == null) {
 | 
			
		||||
        log(
 | 
			
		||||
          '[Update] Failed to parse versions. Latest: ${release.tagName}, Local: $localVersionStr',
 | 
			
		||||
        );
 | 
			
		||||
        // If parsing fails, do nothing silently
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      log('[Update] Parsed versions. Latest: $latest, Local: $local');
 | 
			
		||||
 | 
			
		||||
      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)
 | 
			
		||||
      await Future.delayed(const Duration(milliseconds: 100));
 | 
			
		||||
 | 
			
		||||
      if (context.mounted) {
 | 
			
		||||
        await showUpdateSheet(context, release);
 | 
			
		||||
    } catch (_) {
 | 
			
		||||
        log('[Update] Update sheet shown.');
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      log('[Update] Error checking for updates: $e');
 | 
			
		||||
      // Ignore errors (network, api, etc.)
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@@ -126,8 +178,12 @@ class UpdateService {
 | 
			
		||||
      context: context,
 | 
			
		||||
      isScrollControlled: true,
 | 
			
		||||
      useRootNavigator: true,
 | 
			
		||||
      builder:
 | 
			
		||||
          (ctx) => _UpdateSheet(
 | 
			
		||||
      builder: (ctx) {
 | 
			
		||||
        String? androidUpdateUrl;
 | 
			
		||||
        if (Platform.isAndroid) {
 | 
			
		||||
          androidUpdateUrl = _getAndroidUpdateUrl(release.assets);
 | 
			
		||||
        }
 | 
			
		||||
        return _UpdateSheet(
 | 
			
		||||
          release: release,
 | 
			
		||||
          onOpen: () async {
 | 
			
		||||
            final uri = Uri.parse(release.htmlUrl);
 | 
			
		||||
@@ -135,16 +191,55 @@ class UpdateService {
 | 
			
		||||
              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.
 | 
			
		||||
  /// Public so other screens (e.g., About) can manually trigger update checks.
 | 
			
		||||
  Future<GithubReleaseInfo?> fetchLatestRelease() async {
 | 
			
		||||
    final resp = await _dio.get(_releasesLatestApi);
 | 
			
		||||
    if (resp.statusCode != 200) return null;
 | 
			
		||||
    final apiEndpoint =
 | 
			
		||||
        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>;
 | 
			
		||||
    log('[Update] Successfully fetched release data.');
 | 
			
		||||
 | 
			
		||||
    final tagName = (data['tag_name'] ?? '').toString();
 | 
			
		||||
    final name = (data['name'] ?? tagName).toString();
 | 
			
		||||
@@ -152,25 +247,70 @@ class UpdateService {
 | 
			
		||||
    final htmlUrl = (data['html_url'] ?? '').toString();
 | 
			
		||||
    final createdAtStr = (data['created_at'] ?? '').toString();
 | 
			
		||||
    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(
 | 
			
		||||
      tagName: tagName,
 | 
			
		||||
      name: name,
 | 
			
		||||
      body: body,
 | 
			
		||||
      htmlUrl: htmlUrl,
 | 
			
		||||
      createdAt: createdAt,
 | 
			
		||||
      assets: assetsData,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _UpdateSheet extends StatelessWidget {
 | 
			
		||||
  const _UpdateSheet({required this.release, required this.onOpen});
 | 
			
		||||
class _UpdateSheet extends StatefulWidget {
 | 
			
		||||
  const _UpdateSheet({
 | 
			
		||||
    required this.release,
 | 
			
		||||
    required this.onOpen,
 | 
			
		||||
    this.androidUpdateUrl,
 | 
			
		||||
    this.useProxy = false,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  final String? androidUpdateUrl;
 | 
			
		||||
  final bool useProxy;
 | 
			
		||||
  final GithubReleaseInfo release;
 | 
			
		||||
  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",
 | 
			
		||||
      "launcher_icon",
 | 
			
		||||
      'https://apps.apple.com/us/app/solian/id6499032345',
 | 
			
		||||
    );
 | 
			
		||||
    AzhonAppUpdate.update(model);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final theme = Theme.of(context);
 | 
			
		||||
@@ -186,8 +326,11 @@ class _UpdateSheet extends StatelessWidget {
 | 
			
		||||
            Column(
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
              children: [
 | 
			
		||||
                Text(release.name, style: theme.textTheme.titleMedium).bold(),
 | 
			
		||||
                Text(release.tagName).fontSize(12),
 | 
			
		||||
                Text(
 | 
			
		||||
                  widget.release.name,
 | 
			
		||||
                  style: theme.textTheme.titleMedium,
 | 
			
		||||
                ).bold(),
 | 
			
		||||
                Text(widget.release.tagName).fontSize(12),
 | 
			
		||||
              ],
 | 
			
		||||
            ).padding(vertical: 16, horizontal: 16),
 | 
			
		||||
            const Divider(height: 1),
 | 
			
		||||
@@ -197,21 +340,45 @@ class _UpdateSheet extends StatelessWidget {
 | 
			
		||||
                  horizontal: 16,
 | 
			
		||||
                  vertical: 16,
 | 
			
		||||
                ),
 | 
			
		||||
                child: SelectableText(
 | 
			
		||||
                  release.body.isEmpty
 | 
			
		||||
                child: MarkdownTextContent(
 | 
			
		||||
                  content:
 | 
			
		||||
                      widget.release.body.isEmpty
 | 
			
		||||
                          ? 'No changelog provided.'
 | 
			
		||||
                      : release.body,
 | 
			
		||||
                  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(
 | 
			
		||||
              children: [
 | 
			
		||||
                Row(
 | 
			
		||||
                  spacing: 8,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    if (!kIsWeb &&
 | 
			
		||||
                        Platform.isAndroid &&
 | 
			
		||||
                        widget.androidUpdateUrl != null)
 | 
			
		||||
                      Expanded(
 | 
			
		||||
                        child: FilledButton.icon(
 | 
			
		||||
                        onPressed: onOpen,
 | 
			
		||||
                          onPressed: () {
 | 
			
		||||
                            log(widget.androidUpdateUrl!);
 | 
			
		||||
                            _installUpdate(widget.androidUpdateUrl!);
 | 
			
		||||
                          },
 | 
			
		||||
                          icon: const Icon(Symbols.update),
 | 
			
		||||
                          label: const Text('Install update'),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    Expanded(
 | 
			
		||||
                      child: FilledButton.icon(
 | 
			
		||||
                        onPressed: widget.onOpen,
 | 
			
		||||
                        icon: const Icon(Icons.open_in_new),
 | 
			
		||||
                        label: const Text('Open release page'),
 | 
			
		||||
                      ),
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@ class AccountStatusCreationSheet extends HookConsumerWidget {
 | 
			
		||||
            'attitude': attitude.value,
 | 
			
		||||
            'is_invisible': isInvisible.value,
 | 
			
		||||
            'is_not_disturb': isNotDisturb.value,
 | 
			
		||||
            'cleared_at': clearedAt.value?.toIso8601String(),
 | 
			
		||||
            'cleared_at': clearedAt.value?.toUtc().toIso8601String(),
 | 
			
		||||
            if (labelController.text.isNotEmpty) 'label': labelController.text,
 | 
			
		||||
          },
 | 
			
		||||
          options: Options(method: initialStatus == null ? 'POST' : 'PATCH'),
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:island/pods/websocket.dart';
 | 
			
		||||
import 'package:island/services/notify.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/tour/tour.dart';
 | 
			
		||||
 | 
			
		||||
@@ -21,6 +22,7 @@ class AppWrapper extends HookConsumerWidget {
 | 
			
		||||
      });
 | 
			
		||||
      final sharingService = SharingIntentService();
 | 
			
		||||
      sharingService.initialize(context);
 | 
			
		||||
      UpdateService().checkForUpdates(context);
 | 
			
		||||
      return () {
 | 
			
		||||
        sharingService.dispose();
 | 
			
		||||
        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-light.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:google_fonts/google_fonts.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:island/models/file.dart';
 | 
			
		||||
import 'package:island/pods/config.dart';
 | 
			
		||||
@@ -71,7 +72,22 @@ class MarkdownTextContent extends HookConsumerWidget {
 | 
			
		||||
            textStyle: textStyle ?? Theme.of(context).textTheme.bodyMedium!,
 | 
			
		||||
          ),
 | 
			
		||||
          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(
 | 
			
		||||
            style:
 | 
			
		||||
                linkStyle ??
 | 
			
		||||
@@ -160,7 +176,7 @@ class MarkdownTextContent extends HookConsumerWidget {
 | 
			
		||||
                          uri: stickerUri,
 | 
			
		||||
                          width: size,
 | 
			
		||||
                          height: size,
 | 
			
		||||
                          fit: BoxFit.cover,
 | 
			
		||||
                          fit: BoxFit.contain,
 | 
			
		||||
                          noCacheOptimization: true,
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
 
 | 
			
		||||
@@ -248,7 +248,7 @@ class _PaymentContentState extends ConsumerState<_PaymentContent> {
 | 
			
		||||
    try {
 | 
			
		||||
      final client = ref.read(apiClientProvider);
 | 
			
		||||
      final response = await client.post(
 | 
			
		||||
        '/orders/${widget.order.id}/pay',
 | 
			
		||||
        '/id/orders/${widget.order.id}/pay',
 | 
			
		||||
        data: {'pin_code': pin},
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -273,7 +273,7 @@ class PostItem extends HookConsumerWidget {
 | 
			
		||||
            : item.reactionsCount.entries
 | 
			
		||||
                .sortedBy((e) => e.value)
 | 
			
		||||
                .map((e) => e.key)
 | 
			
		||||
                .first;
 | 
			
		||||
                .last;
 | 
			
		||||
 | 
			
		||||
    final postLanguage =
 | 
			
		||||
        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: EdgeInsets.only(
 | 
			
		||||
              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_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:island/models/post.dart';
 | 
			
		||||
import 'package:island/models/publisher.dart';
 | 
			
		||||
import 'package:island/pods/network.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/content/cloud_files.dart';
 | 
			
		||||
import 'package:island/widgets/post/publishers_modal.dart';
 | 
			
		||||
@@ -14,8 +16,14 @@ import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
 | 
			
		||||
class PostQuickReply extends HookConsumerWidget {
 | 
			
		||||
  final SnPost parent;
 | 
			
		||||
  final Function? onPosted;
 | 
			
		||||
  const PostQuickReply({super.key, required this.parent, this.onPosted});
 | 
			
		||||
  final VoidCallback? onPosted;
 | 
			
		||||
  final VoidCallback? onLaunch;
 | 
			
		||||
  const PostQuickReply({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.parent,
 | 
			
		||||
    this.onPosted,
 | 
			
		||||
    this.onLaunch,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
@@ -48,7 +56,7 @@ class PostQuickReply extends HookConsumerWidget {
 | 
			
		||||
            'content': contentController.text,
 | 
			
		||||
            'replied_post_id': parent.id,
 | 
			
		||||
          },
 | 
			
		||||
          options: Options(headers: {'X-Pub': currentPublisher.value?.name}),
 | 
			
		||||
          queryParameters: {'pub': currentPublisher.value?.name},
 | 
			
		||||
        );
 | 
			
		||||
        contentController.clear();
 | 
			
		||||
        onPosted?.call();
 | 
			
		||||
@@ -83,9 +91,10 @@ class PostQuickReply extends HookConsumerWidget {
 | 
			
		||||
                child: TextField(
 | 
			
		||||
                  controller: contentController,
 | 
			
		||||
                  decoration: InputDecoration(
 | 
			
		||||
                    hintText: 'Post your reply',
 | 
			
		||||
                    border: const OutlineInputBorder(),
 | 
			
		||||
                    hintText: 'postReplyPlaceholder'.tr(),
 | 
			
		||||
                    border: InputBorder.none,
 | 
			
		||||
                    isDense: true,
 | 
			
		||||
                    isCollapsed: true,
 | 
			
		||||
                    contentPadding: EdgeInsets.symmetric(
 | 
			
		||||
                      horizontal: 12,
 | 
			
		||||
                      vertical: 8,
 | 
			
		||||
@@ -97,6 +106,26 @@ class PostQuickReply extends HookConsumerWidget {
 | 
			
		||||
                      (_) => 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(
 | 
			
		||||
                padding: EdgeInsets.zero,
 | 
			
		||||
                visualDensity: VisualDensity.compact,
 | 
			
		||||
@@ -110,6 +139,7 @@ class PostQuickReply extends HookConsumerWidget {
 | 
			
		||||
                        : Icon(Symbols.send, size: 20),
 | 
			
		||||
                color: Theme.of(context).colorScheme.primary,
 | 
			
		||||
                onPressed: submitting.value ? null : performAction,
 | 
			
		||||
                constraints: const BoxConstraints(),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
 
 | 
			
		||||
@@ -38,14 +38,18 @@ class PostRepliesSheet extends HookConsumerWidget {
 | 
			
		||||
          if (user.value != null)
 | 
			
		||||
            Material(
 | 
			
		||||
              elevation: 2,
 | 
			
		||||
              color: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
			
		||||
              child: PostQuickReply(
 | 
			
		||||
                parent: post,
 | 
			
		||||
                onPosted: () {
 | 
			
		||||
                  ref.invalidate(postRepliesNotifierProvider(post.id));
 | 
			
		||||
                },
 | 
			
		||||
                onLaunch: () {
 | 
			
		||||
                  Navigator.of(context).pop();
 | 
			
		||||
                },
 | 
			
		||||
              ).padding(
 | 
			
		||||
                bottom: MediaQuery.of(context).padding.bottom + 16,
 | 
			
		||||
                top: 16,
 | 
			
		||||
                bottom: MediaQuery.of(context).padding.bottom + 8,
 | 
			
		||||
                top: 8,
 | 
			
		||||
                horizontal: 16,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
 
 | 
			
		||||
@@ -130,25 +130,25 @@ PODS:
 | 
			
		||||
  - sqflite_darwin (0.0.4):
 | 
			
		||||
    - Flutter
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
  - sqlite3 (3.50.3):
 | 
			
		||||
    - sqlite3/common (= 3.50.3)
 | 
			
		||||
  - sqlite3/common (3.50.3)
 | 
			
		||||
  - sqlite3/dbstatvtab (3.50.3):
 | 
			
		||||
  - sqlite3 (3.50.4):
 | 
			
		||||
    - sqlite3/common (= 3.50.4)
 | 
			
		||||
  - sqlite3/common (3.50.4)
 | 
			
		||||
  - sqlite3/dbstatvtab (3.50.4):
 | 
			
		||||
    - sqlite3/common
 | 
			
		||||
  - sqlite3/fts5 (3.50.3):
 | 
			
		||||
  - sqlite3/fts5 (3.50.4):
 | 
			
		||||
    - sqlite3/common
 | 
			
		||||
  - sqlite3/math (3.50.3):
 | 
			
		||||
  - sqlite3/math (3.50.4):
 | 
			
		||||
    - sqlite3/common
 | 
			
		||||
  - sqlite3/perf-threadsafe (3.50.3):
 | 
			
		||||
  - sqlite3/perf-threadsafe (3.50.4):
 | 
			
		||||
    - sqlite3/common
 | 
			
		||||
  - sqlite3/rtree (3.50.3):
 | 
			
		||||
  - sqlite3/rtree (3.50.4):
 | 
			
		||||
    - sqlite3/common
 | 
			
		||||
  - sqlite3/session (3.50.3):
 | 
			
		||||
  - sqlite3/session (3.50.4):
 | 
			
		||||
    - sqlite3/common
 | 
			
		||||
  - sqlite3_flutter_libs (0.0.1):
 | 
			
		||||
    - Flutter
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
    - sqlite3 (~> 3.50.3)
 | 
			
		||||
    - sqlite3 (~> 3.50.4)
 | 
			
		||||
    - sqlite3/dbstatvtab
 | 
			
		||||
    - sqlite3/fts5
 | 
			
		||||
    - sqlite3/math
 | 
			
		||||
@@ -328,8 +328,8 @@ SPEC CHECKSUMS:
 | 
			
		||||
  shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
 | 
			
		||||
  sign_in_with_apple: 6673c03c9e3643f6c8d33601943fbfa9ae99f94e
 | 
			
		||||
  sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
 | 
			
		||||
  sqlite3: 83105acd294c9137c026e2da1931c30b4588ab81
 | 
			
		||||
  sqlite3_flutter_libs: 616267f2fca40e9c6af8c5d82324e05667040b6e
 | 
			
		||||
  sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
 | 
			
		||||
  sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
 | 
			
		||||
  super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189
 | 
			
		||||
  url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
 | 
			
		||||
  volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd
 | 
			
		||||
 
 | 
			
		||||
@@ -662,6 +662,14 @@ packages:
 | 
			
		||||
    description: flutter
 | 
			
		||||
    source: sdk
 | 
			
		||||
    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:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    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
 | 
			
		||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
			
		||||
# of the product and file versions while build-number is used as the build suffix.
 | 
			
		||||
version: 3.1.0+120
 | 
			
		||||
version: 3.1.0+122
 | 
			
		||||
 | 
			
		||||
environment:
 | 
			
		||||
  sdk: ^3.7.2
 | 
			
		||||
@@ -133,6 +133,7 @@ dependencies:
 | 
			
		||||
  flutter_typeahead: ^5.2.0
 | 
			
		||||
  flutter_langdetect: ^0.0.2
 | 
			
		||||
  waveform_flutter: ^1.2.0
 | 
			
		||||
  flutter_app_update: ^3.2.2
 | 
			
		||||
 | 
			
		||||
dev_dependencies:
 | 
			
		||||
  flutter_test:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user