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