Compare commits
	
		
			4 Commits
		
	
	
		
			3.0.0+107
			...
			4deff5a920
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4deff5a920 | |||
| 0361f031db | |||
| e90b35f19f | |||
| f2829b2012 | 
| @@ -587,6 +587,11 @@ | ||||
|   "addAdditionalMessage": "Add additional message...", | ||||
|   "uploadingFiles": "Uploading files...", | ||||
|   "sharedSuccessfully": "Shared successfully!", | ||||
|   "shareSuccess": "Shared successfully!", | ||||
|   "shareToSpecificChatSuccess": "Shared to {} successfully!", | ||||
|   "wouldYouLikeToGoToChat": "Would you like to go to the chat?", | ||||
|   "no": "No", | ||||
|   "yes": "Yes", | ||||
|   "navigateToChat": "Navigate to Chat", | ||||
|   "wouldYouLikeToNavigateToChat": "Would you like to navigate to the chat?", | ||||
|   "abuseReport": "Report", | ||||
| @@ -610,5 +615,9 @@ | ||||
|   "abuseReportTypeOffensiveContent": "Offensive Content", | ||||
|   "abuseReportTypePrivacyViolation": "Privacy Violation", | ||||
|   "abuseReportTypeIllegalContent": "Illegal Content", | ||||
|   "abuseReportTypeOther": "Other" | ||||
|   "abuseReportTypeOther": "Other", | ||||
|   "tags": "Tags", | ||||
|   "tagsHint": "Enter tags, separated by commas", | ||||
|   "categories": "Categories", | ||||
|   "categoriesHint": "Enter categories, separated by commas" | ||||
| } | ||||
|   | ||||
| @@ -84,7 +84,7 @@ PODS: | ||||
|     - Flutter | ||||
|   - flutter_platform_alert (0.0.1): | ||||
|     - Flutter | ||||
|   - flutter_secure_storage (3.3.1): | ||||
|   - flutter_secure_storage (6.0.0): | ||||
|     - Flutter | ||||
|   - flutter_timezone (0.0.1): | ||||
|     - Flutter | ||||
| @@ -362,7 +362,7 @@ SPEC CHECKSUMS: | ||||
|   flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 | ||||
|   flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf | ||||
|   flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3 | ||||
|   flutter_secure_storage: 50035aef357c5a8bdd67fd6bc81370d46efc4d16 | ||||
|   flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 | ||||
|   flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544 | ||||
|   flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9 | ||||
|   flutter_webrtc: fd0d3bdef8766a0736dbbe2e5b7e85f1f3c52117 | ||||
|   | ||||
| @@ -18,7 +18,7 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; | ||||
| import 'package:island/pods/userinfo.dart'; | ||||
| import 'package:island/pods/websocket.dart'; | ||||
| import 'package:island/route.dart'; | ||||
| import 'package:island/screens/tabs.dart'; | ||||
|  | ||||
| import 'package:island/services/notify.dart'; | ||||
| import 'package:island/services/timezone.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| @@ -125,7 +125,7 @@ void main() async { | ||||
|   ); | ||||
| } | ||||
|  | ||||
| final appRouter = AppRouter(); | ||||
| // Router will be provided through Riverpod | ||||
|  | ||||
| final globalOverlay = GlobalKey<OverlayState>(); | ||||
|  | ||||
| @@ -141,7 +141,8 @@ class IslandApp extends HookConsumerWidget { | ||||
|         var uri = notification.data['action_uri'] as String; | ||||
|         if (uri.startsWith('/')) { | ||||
|           // In-app routes | ||||
|           appRouter.pushPath(notification.data['action_uri']); | ||||
|           final router = ref.read(routerProvider); | ||||
|           router.go(notification.data['action_uri']); | ||||
|         } else { | ||||
|           // External links | ||||
|           launchUrlString(uri); | ||||
| @@ -183,20 +184,13 @@ class IslandApp extends HookConsumerWidget { | ||||
|       return null; | ||||
|     }, []); | ||||
|  | ||||
|     final router = ref.watch(routerProvider); | ||||
|      | ||||
|     return MaterialApp.router( | ||||
|       theme: theme?.light, | ||||
|       darkTheme: theme?.dark, | ||||
|       themeMode: ThemeMode.system, | ||||
|       routerConfig: appRouter.config( | ||||
|         navigatorObservers: | ||||
|             () => [ | ||||
|               TabNavigationObserver( | ||||
|                 onChange: (route) { | ||||
|                   ref.read(currentRouteProvider.notifier).state = route; | ||||
|                 }, | ||||
|               ), | ||||
|             ], | ||||
|       ), | ||||
|       routerConfig: router, | ||||
|       supportedLocales: context.supportedLocales, | ||||
|       localizationsDelegates: [ | ||||
|         ...context.localizationDelegates, | ||||
| @@ -211,7 +205,6 @@ class IslandApp extends HookConsumerWidget { | ||||
|             OverlayEntry( | ||||
|               builder: | ||||
|                   (_) => WindowScaffold( | ||||
|                     router: appRouter, | ||||
|                     child: child ?? const SizedBox.shrink(), | ||||
|                   ), | ||||
|             ), | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| import 'package:island/models/file.dart'; | ||||
| import 'package:island/models/post_category.dart'; | ||||
| import 'package:island/models/post_tag.dart'; | ||||
| import 'package:island/models/user.dart'; | ||||
|  | ||||
| part 'post.freezed.dart'; | ||||
| @@ -33,8 +35,8 @@ sealed class SnPost with _$SnPost { | ||||
|     @Default(SnPublisher()) SnPublisher publisher, | ||||
|     @Default({}) Map<String, int> reactionsCount, | ||||
|     @Default([]) List<dynamic> reactions, | ||||
|     @Default([]) List<dynamic> tags, | ||||
|     @Default([]) List<dynamic> categories, | ||||
|     @Default([]) List<PostTag> tags, | ||||
|     @Default([]) List<PostCategory> categories, | ||||
|     @Default([]) List<dynamic> collections, | ||||
|     @Default(null) DateTime? createdAt, | ||||
|     @Default(null) DateTime? updatedAt, | ||||
|   | ||||
| @@ -16,7 +16,7 @@ T _$identity<T>(T value) => value; | ||||
| /// @nodoc | ||||
| mixin _$SnPost { | ||||
|  | ||||
|  String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime? get publishedAt; int get visibility; String? get content; int get type; Map<String, dynamic>? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; List<dynamic> get reactions; List<dynamic> get tags; List<dynamic> get categories; List<dynamic> get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated; | ||||
|  String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime? get publishedAt; int get visibility; String? get content; int get type; Map<String, dynamic>? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; List<dynamic> get reactions; List<PostTag> get tags; List<PostCategory> get categories; List<dynamic> get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated; | ||||
| /// Create a copy of SnPost | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @@ -49,7 +49,7 @@ abstract mixin class $SnPostCopyWith<$Res>  { | ||||
|   factory $SnPostCopyWith(SnPost value, $Res Function(SnPost) _then) = _$SnPostCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, List<dynamic> reactions, List<dynamic> tags, List<dynamic> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated | ||||
|  String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, List<dynamic> reactions, List<PostTag> tags, List<PostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -94,8 +94,8 @@ as List<SnCloudFile>,publisher: null == publisher ? _self.publisher : publisher | ||||
| as SnPublisher,reactionsCount: null == reactionsCount ? _self.reactionsCount : reactionsCount // ignore: cast_nullable_to_non_nullable | ||||
| as Map<String, int>,reactions: null == reactions ? _self.reactions : reactions // ignore: cast_nullable_to_non_nullable | ||||
| as List<dynamic>,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable | ||||
| as List<dynamic>,categories: null == categories ? _self.categories : categories // ignore: cast_nullable_to_non_nullable | ||||
| as List<dynamic>,collections: null == collections ? _self.collections : collections // ignore: cast_nullable_to_non_nullable | ||||
| as List<PostTag>,categories: null == categories ? _self.categories : categories // ignore: cast_nullable_to_non_nullable | ||||
| as List<PostCategory>,collections: null == collections ? _self.collections : collections // ignore: cast_nullable_to_non_nullable | ||||
| as List<dynamic>,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
| @@ -156,7 +156,7 @@ $SnPublisherCopyWith<$Res> get publisher { | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _SnPost implements SnPost { | ||||
|   const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.type = 0, final  Map<String, dynamic>? meta, this.viewsUnique = 0, this.viewsTotal = 0, this.upvotes = 0, this.downvotes = 0, this.repliesCount = 0, this.threadedPostId, this.threadedPost, this.repliedPostId, this.repliedPost, this.forwardedPostId, this.forwardedPost, final  List<SnCloudFile> attachments = const [], this.publisher = const SnPublisher(), final  Map<String, int> reactionsCount = const {}, final  List<dynamic> reactions = const [], final  List<dynamic> tags = const [], final  List<dynamic> categories = const [], final  List<dynamic> collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections; | ||||
|   const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.type = 0, final  Map<String, dynamic>? meta, this.viewsUnique = 0, this.viewsTotal = 0, this.upvotes = 0, this.downvotes = 0, this.repliesCount = 0, this.threadedPostId, this.threadedPost, this.repliedPostId, this.repliedPost, this.forwardedPostId, this.forwardedPost, final  List<SnCloudFile> attachments = const [], this.publisher = const SnPublisher(), final  Map<String, int> reactionsCount = const {}, final  List<dynamic> reactions = const [], final  List<PostTag> tags = const [], final  List<PostCategory> categories = const [], final  List<dynamic> collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections; | ||||
|   factory _SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @@ -210,15 +210,15 @@ class _SnPost implements SnPost { | ||||
|   return EqualUnmodifiableListView(_reactions); | ||||
| } | ||||
|  | ||||
|  final  List<dynamic> _tags; | ||||
| @override@JsonKey() List<dynamic> get tags { | ||||
|  final  List<PostTag> _tags; | ||||
| @override@JsonKey() List<PostTag> get tags { | ||||
|   if (_tags is EqualUnmodifiableListView) return _tags; | ||||
|   // ignore: implicit_dynamic_type | ||||
|   return EqualUnmodifiableListView(_tags); | ||||
| } | ||||
|  | ||||
|  final  List<dynamic> _categories; | ||||
| @override@JsonKey() List<dynamic> get categories { | ||||
|  final  List<PostCategory> _categories; | ||||
| @override@JsonKey() List<PostCategory> get categories { | ||||
|   if (_categories is EqualUnmodifiableListView) return _categories; | ||||
|   // ignore: implicit_dynamic_type | ||||
|   return EqualUnmodifiableListView(_categories); | ||||
| @@ -269,7 +269,7 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> { | ||||
|   factory _$SnPostCopyWith(_SnPost value, $Res Function(_SnPost) _then) = __$SnPostCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, List<dynamic> reactions, List<dynamic> tags, List<dynamic> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated | ||||
|  String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, List<dynamic> reactions, List<PostTag> tags, List<PostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -314,8 +314,8 @@ as List<SnCloudFile>,publisher: null == publisher ? _self.publisher : publisher | ||||
| as SnPublisher,reactionsCount: null == reactionsCount ? _self._reactionsCount : reactionsCount // ignore: cast_nullable_to_non_nullable | ||||
| as Map<String, int>,reactions: null == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable | ||||
| as List<dynamic>,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable | ||||
| as List<dynamic>,categories: null == categories ? _self._categories : categories // ignore: cast_nullable_to_non_nullable | ||||
| as List<dynamic>,collections: null == collections ? _self._collections : collections // ignore: cast_nullable_to_non_nullable | ||||
| as List<PostTag>,categories: null == categories ? _self._categories : categories // ignore: cast_nullable_to_non_nullable | ||||
| as List<PostCategory>,collections: null == collections ? _self._collections : collections // ignore: cast_nullable_to_non_nullable | ||||
| as List<dynamic>,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
|   | ||||
| @@ -58,8 +58,16 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost( | ||||
|       ) ?? | ||||
|       const {}, | ||||
|   reactions: json['reactions'] as List<dynamic>? ?? const [], | ||||
|   tags: json['tags'] as List<dynamic>? ?? const [], | ||||
|   categories: json['categories'] as List<dynamic>? ?? const [], | ||||
|   tags: | ||||
|       (json['tags'] as List<dynamic>?) | ||||
|           ?.map((e) => PostTag.fromJson(e as Map<String, dynamic>)) | ||||
|           .toList() ?? | ||||
|       const [], | ||||
|   categories: | ||||
|       (json['categories'] as List<dynamic>?) | ||||
|           ?.map((e) => PostCategory.fromJson(e as Map<String, dynamic>)) | ||||
|           .toList() ?? | ||||
|       const [], | ||||
|   collections: json['collections'] as List<dynamic>? ?? const [], | ||||
|   createdAt: | ||||
|       json['created_at'] == null | ||||
| @@ -102,8 +110,8 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{ | ||||
|   'publisher': instance.publisher.toJson(), | ||||
|   'reactions_count': instance.reactionsCount, | ||||
|   'reactions': instance.reactions, | ||||
|   'tags': instance.tags, | ||||
|   'categories': instance.categories, | ||||
|   'tags': instance.tags.map((e) => e.toJson()).toList(), | ||||
|   'categories': instance.categories.map((e) => e.toJson()).toList(), | ||||
|   'collections': instance.collections, | ||||
|   'created_at': instance.createdAt?.toIso8601String(), | ||||
|   'updated_at': instance.updatedAt?.toIso8601String(), | ||||
|   | ||||
							
								
								
									
										19
									
								
								lib/models/post_category.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								lib/models/post_category.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
|  | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
|  | ||||
| part 'post_category.freezed.dart'; | ||||
| part 'post_category.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| sealed class PostCategory with _$PostCategory { | ||||
|   const factory PostCategory({ | ||||
|     required String id, | ||||
|     required String slug, | ||||
|     String? name, | ||||
|     @Default([]) List<SnPost> posts, | ||||
|   }) = _PostCategory; | ||||
|  | ||||
|   factory PostCategory.fromJson(Map<String, dynamic> json) => | ||||
|       _$PostCategoryFromJson(json); | ||||
| } | ||||
							
								
								
									
										163
									
								
								lib/models/post_category.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								lib/models/post_category.freezed.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | ||||
| // dart format width=80 | ||||
| // coverage:ignore-file | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| // ignore_for_file: type=lint | ||||
| // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark | ||||
|  | ||||
| part of 'post_category.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // FreezedGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| // dart format off | ||||
| T _$identity<T>(T value) => value; | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$PostCategory { | ||||
|  | ||||
|  String get id; String get slug; String? get name; List<SnPost> get posts; | ||||
| /// Create a copy of PostCategory | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $PostCategoryCopyWith<PostCategory> get copyWith => _$PostCategoryCopyWithImpl<PostCategory>(this as PostCategory, _$identity); | ||||
|  | ||||
|   /// Serializes this PostCategory to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is PostCategory&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other.posts, posts)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(posts)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'PostCategory(id: $id, slug: $slug, name: $name, posts: $posts)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $PostCategoryCopyWith<$Res>  { | ||||
|   factory $PostCategoryCopyWith(PostCategory value, $Res Function(PostCategory) _then) = _$PostCategoryCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String slug, String? name, List<SnPost> posts | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$PostCategoryCopyWithImpl<$Res> | ||||
|     implements $PostCategoryCopyWith<$Res> { | ||||
|   _$PostCategoryCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final PostCategory _self; | ||||
|   final $Res Function(PostCategory) _then; | ||||
|  | ||||
| /// Create a copy of PostCategory | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,}) { | ||||
|   return _then(_self.copyWith( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable | ||||
| as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| as String?,posts: null == posts ? _self.posts : posts // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnPost>, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _PostCategory implements PostCategory { | ||||
|   const _PostCategory({required this.id, required this.slug, this.name, final  List<SnPost> posts = const []}): _posts = posts; | ||||
|   factory _PostCategory.fromJson(Map<String, dynamic> json) => _$PostCategoryFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @override final  String slug; | ||||
| @override final  String? name; | ||||
|  final  List<SnPost> _posts; | ||||
| @override@JsonKey() List<SnPost> get posts { | ||||
|   if (_posts is EqualUnmodifiableListView) return _posts; | ||||
|   // ignore: implicit_dynamic_type | ||||
|   return EqualUnmodifiableListView(_posts); | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Create a copy of PostCategory | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| _$PostCategoryCopyWith<_PostCategory> get copyWith => __$PostCategoryCopyWithImpl<_PostCategory>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$PostCategoryToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _PostCategory&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other._posts, _posts)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(_posts)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'PostCategory(id: $id, slug: $slug, name: $name, posts: $posts)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$PostCategoryCopyWith<$Res> implements $PostCategoryCopyWith<$Res> { | ||||
|   factory _$PostCategoryCopyWith(_PostCategory value, $Res Function(_PostCategory) _then) = __$PostCategoryCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String slug, String? name, List<SnPost> posts | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class __$PostCategoryCopyWithImpl<$Res> | ||||
|     implements _$PostCategoryCopyWith<$Res> { | ||||
|   __$PostCategoryCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _PostCategory _self; | ||||
|   final $Res Function(_PostCategory) _then; | ||||
|  | ||||
| /// Create a copy of PostCategory | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,}) { | ||||
|   return _then(_PostCategory( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable | ||||
| as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| as String?,posts: null == posts ? _self._posts : posts // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnPost>, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| // dart format on | ||||
							
								
								
									
										27
									
								
								lib/models/post_category.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								lib/models/post_category.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'post_category.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _PostCategory _$PostCategoryFromJson(Map<String, dynamic> json) => | ||||
|     _PostCategory( | ||||
|       id: json['id'] as String, | ||||
|       slug: json['slug'] as String, | ||||
|       name: json['name'] as String?, | ||||
|       posts: | ||||
|           (json['posts'] as List<dynamic>?) | ||||
|               ?.map((e) => SnPost.fromJson(e as Map<String, dynamic>)) | ||||
|               .toList() ?? | ||||
|           const [], | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$PostCategoryToJson(_PostCategory instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'slug': instance.slug, | ||||
|       'name': instance.name, | ||||
|       'posts': instance.posts.map((e) => e.toJson()).toList(), | ||||
|     }; | ||||
							
								
								
									
										19
									
								
								lib/models/post_tag.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								lib/models/post_tag.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
|  | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
|  | ||||
| part 'post_tag.freezed.dart'; | ||||
| part 'post_tag.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| sealed class PostTag with _$PostTag { | ||||
|   const factory PostTag({ | ||||
|     required String id, | ||||
|     required String slug, | ||||
|     String? name, | ||||
|     @Default([]) List<SnPost> posts, | ||||
|   }) = _PostTag; | ||||
|  | ||||
|   factory PostTag.fromJson(Map<String, dynamic> json) => | ||||
|       _$PostTagFromJson(json); | ||||
| } | ||||
							
								
								
									
										163
									
								
								lib/models/post_tag.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								lib/models/post_tag.freezed.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | ||||
| // dart format width=80 | ||||
| // coverage:ignore-file | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| // ignore_for_file: type=lint | ||||
| // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark | ||||
|  | ||||
| part of 'post_tag.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // FreezedGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| // dart format off | ||||
| T _$identity<T>(T value) => value; | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$PostTag { | ||||
|  | ||||
|  String get id; String get slug; String? get name; List<SnPost> get posts; | ||||
| /// Create a copy of PostTag | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $PostTagCopyWith<PostTag> get copyWith => _$PostTagCopyWithImpl<PostTag>(this as PostTag, _$identity); | ||||
|  | ||||
|   /// Serializes this PostTag to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is PostTag&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other.posts, posts)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(posts)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'PostTag(id: $id, slug: $slug, name: $name, posts: $posts)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $PostTagCopyWith<$Res>  { | ||||
|   factory $PostTagCopyWith(PostTag value, $Res Function(PostTag) _then) = _$PostTagCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String slug, String? name, List<SnPost> posts | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$PostTagCopyWithImpl<$Res> | ||||
|     implements $PostTagCopyWith<$Res> { | ||||
|   _$PostTagCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final PostTag _self; | ||||
|   final $Res Function(PostTag) _then; | ||||
|  | ||||
| /// Create a copy of PostTag | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,}) { | ||||
|   return _then(_self.copyWith( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable | ||||
| as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| as String?,posts: null == posts ? _self.posts : posts // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnPost>, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _PostTag implements PostTag { | ||||
|   const _PostTag({required this.id, required this.slug, this.name, final  List<SnPost> posts = const []}): _posts = posts; | ||||
|   factory _PostTag.fromJson(Map<String, dynamic> json) => _$PostTagFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @override final  String slug; | ||||
| @override final  String? name; | ||||
|  final  List<SnPost> _posts; | ||||
| @override@JsonKey() List<SnPost> get posts { | ||||
|   if (_posts is EqualUnmodifiableListView) return _posts; | ||||
|   // ignore: implicit_dynamic_type | ||||
|   return EqualUnmodifiableListView(_posts); | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Create a copy of PostTag | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| _$PostTagCopyWith<_PostTag> get copyWith => __$PostTagCopyWithImpl<_PostTag>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$PostTagToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _PostTag&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other._posts, _posts)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEquality().hash(_posts)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'PostTag(id: $id, slug: $slug, name: $name, posts: $posts)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$PostTagCopyWith<$Res> implements $PostTagCopyWith<$Res> { | ||||
|   factory _$PostTagCopyWith(_PostTag value, $Res Function(_PostTag) _then) = __$PostTagCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String slug, String? name, List<SnPost> posts | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class __$PostTagCopyWithImpl<$Res> | ||||
|     implements _$PostTagCopyWith<$Res> { | ||||
|   __$PostTagCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _PostTag _self; | ||||
|   final $Res Function(_PostTag) _then; | ||||
|  | ||||
| /// Create a copy of PostTag | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,}) { | ||||
|   return _then(_PostTag( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable | ||||
| as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| as String?,posts: null == posts ? _self._posts : posts // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnPost>, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| // dart format on | ||||
							
								
								
									
										25
									
								
								lib/models/post_tag.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								lib/models/post_tag.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'post_tag.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _PostTag _$PostTagFromJson(Map<String, dynamic> json) => _PostTag( | ||||
|   id: json['id'] as String, | ||||
|   slug: json['slug'] as String, | ||||
|   name: json['name'] as String?, | ||||
|   posts: | ||||
|       (json['posts'] as List<dynamic>?) | ||||
|           ?.map((e) => SnPost.fromJson(e as Map<String, dynamic>)) | ||||
|           .toList() ?? | ||||
|       const [], | ||||
| ); | ||||
|  | ||||
| Map<String, dynamic> _$PostTagToJson(_PostTag instance) => <String, dynamic>{ | ||||
|   'id': instance.id, | ||||
|   'slug': instance.slug, | ||||
|   'name': instance.name, | ||||
|   'posts': instance.posts.map((e) => e.toJson()).toList(), | ||||
| }; | ||||
							
								
								
									
										412
									
								
								lib/route.dart
									
									
									
									
									
								
							
							
						
						
									
										412
									
								
								lib/route.dart
									
									
									
									
									
								
							| @@ -1,98 +1,322 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/widgets/app_wrapper.dart'; | ||||
| import 'package:island/screens/tabs.dart'; | ||||
|  | ||||
| @AutoRouterConfig(replaceInRouteName: 'Screen|Page,Route') | ||||
| class AppRouter extends RootStackRouter { | ||||
|   @override | ||||
|   RouteType get defaultRouteType => RouteType.adaptive(); | ||||
| import 'package:island/screens/explore.dart'; | ||||
| import 'package:island/screens/account.dart'; | ||||
| import 'package:island/screens/notification.dart'; | ||||
| import 'package:island/screens/wallet.dart'; | ||||
| import 'package:island/screens/account/relationship.dart'; | ||||
| import 'package:island/screens/account/profile.dart'; | ||||
| import 'package:island/screens/account/me/update.dart'; | ||||
| import 'package:island/screens/account/leveling.dart'; | ||||
| import 'package:island/screens/account/me/settings.dart'; | ||||
| import 'package:island/screens/chat/chat.dart'; | ||||
| import 'package:island/screens/chat/room.dart'; | ||||
| import 'package:island/screens/chat/room_detail.dart'; | ||||
| import 'package:island/screens/chat/call.dart'; | ||||
| import 'package:island/screens/creators/hub.dart'; | ||||
| import 'package:island/screens/creators/posts/list.dart'; | ||||
| import 'package:island/screens/creators/stickers/stickers.dart'; | ||||
| import 'package:island/screens/creators/stickers/pack_detail.dart'; | ||||
| import 'package:island/screens/creators/publishers.dart'; | ||||
| import 'package:island/screens/posts/compose.dart'; | ||||
| import 'package:island/screens/posts/detail.dart'; | ||||
| import 'package:island/screens/posts/pub_profile.dart'; | ||||
| import 'package:island/screens/auth/login.dart'; | ||||
| import 'package:island/screens/auth/create_account.dart'; | ||||
| import 'package:island/screens/settings.dart'; | ||||
| import 'package:island/screens/realm/realms.dart'; | ||||
| import 'package:island/screens/realm/detail.dart'; | ||||
| import 'package:island/screens/account/event_calendar.dart'; | ||||
|  | ||||
|   @override | ||||
|   List<AutoRoute> get routes => [ | ||||
|     AutoRoute(path: '/', page: AppWrapper.page, children: _appRoutes), | ||||
|   ]; | ||||
| // Shell route keys for nested navigation | ||||
| final rootNavigatorKey = GlobalKey<NavigatorState>(); | ||||
| final _shellNavigatorKey = GlobalKey<NavigatorState>(); | ||||
| final _tabsShellKey = GlobalKey<NavigatorState>(); | ||||
|  | ||||
|   List<AutoRoute> get _appRoutes => [ | ||||
|     // Standalone routes without bottom navigation | ||||
|     AutoRoute(page: PostComposeRoute.page, path: 'posts/compose'), | ||||
|     AutoRoute(page: PostEditRoute.page, path: 'posts/:id/edit'), | ||||
|     AutoRoute(page: CallRoute.page, path: 'chat/:id/call'), | ||||
|     AutoRoute(page: EventCalanderRoute.page, path: 'account/:name/calendar'), | ||||
|      | ||||
|     // Main tabs with bottom navigation and shell routes for desktop layout | ||||
|     AutoRoute( | ||||
|       page: TabsRoute.page, | ||||
|       path: '', | ||||
|       children: [ | ||||
|         AutoRoute( | ||||
|           page: ExploreShellRoute.page, | ||||
|           path: '', | ||||
|           children: [ | ||||
|             AutoRoute(page: ExploreRoute.page, path: ''), | ||||
|             AutoRoute(page: PostDetailRoute.page, path: 'posts/:id'), | ||||
|             AutoRoute( | ||||
|               page: PublisherProfileRoute.page, | ||||
|               path: 'publishers/:name', | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|         AutoRoute( | ||||
|           page: AccountShellRoute.page, | ||||
|           path: 'account', | ||||
|           children: [ | ||||
|             AutoRoute(page: AccountRoute.page, path: ''), | ||||
|             AutoRoute(page: NotificationRoute.page, path: 'notifications'), | ||||
|             AutoRoute(page: WalletRoute.page, path: 'wallet'), | ||||
|             AutoRoute(page: RelationshipRoute.page, path: 'relationships'), | ||||
|             AutoRoute(page: AccountProfileRoute.page, path: ':name'), | ||||
|             AutoRoute(page: UpdateProfileRoute.page, path: 'me/update'), | ||||
|             AutoRoute(page: LevelingRoute.page, path: 'me/leveling'), | ||||
|             AutoRoute(page: AccountSettingsRoute.page, path: 'settings'), | ||||
|           ], | ||||
|         ), | ||||
|         AutoRoute(page: RealmListRoute.page, path: 'realms'), | ||||
|         AutoRoute( | ||||
|           page: ChatShellRoute.page, | ||||
|           path: 'chat', | ||||
|           children: [ | ||||
|             AutoRoute(page: ChatListRoute.page, path: ''), | ||||
|             AutoRoute(page: ChatRoomRoute.page, path: ':id'), | ||||
|             AutoRoute(page: NewChatRoute.page, path: 'new'), | ||||
|             AutoRoute(page: EditChatRoute.page, path: ':id/edit'), | ||||
|             AutoRoute(page: ChatDetailRoute.page, path: ':id/detail'), | ||||
|           ], | ||||
|         ), | ||||
|       ], | ||||
|     ), | ||||
|     AutoRoute( | ||||
|       page: CreatorHubShellRoute.page, | ||||
|       path: 'creators', | ||||
|       children: [ | ||||
|         AutoRoute(page: CreatorHubRoute.page, path: ''), | ||||
|         AutoRoute(page: CreatorPostListRoute.page, path: ':name/posts'), | ||||
|         AutoRoute(page: StickersRoute.page, path: ':name/stickers'), | ||||
|         AutoRoute(page: NewStickerPacksRoute.page, path: ':name/stickers/new'), | ||||
|         AutoRoute( | ||||
|           page: EditStickerPacksRoute.page, | ||||
|           path: ':name/stickers/:packId/edit', | ||||
|         ), | ||||
|         AutoRoute( | ||||
|           page: StickerPackDetailRoute.page, | ||||
|           path: ':name/stickers/:packId', | ||||
|         ), | ||||
|         AutoRoute(page: NewStickersRoute.page, path: ':name/stickers/new'), | ||||
|         AutoRoute( | ||||
|           page: EditStickersRoute.page, | ||||
|           path: ':name/stickers/:id/edit', | ||||
|         ), | ||||
|         AutoRoute(page: NewPublisherRoute.page, path: 'new'), | ||||
|         AutoRoute(page: EditPublisherRoute.page, path: ':name/edit'), | ||||
|       ], | ||||
|     ), | ||||
|     AutoRoute(page: LoginRoute.page, path: 'auth/login'), | ||||
|     AutoRoute(page: CreateAccountRoute.page, path: 'auth/create-account'), | ||||
|     AutoRoute(page: SettingsRoute.page, path: 'settings'), | ||||
|     AutoRoute(page: NewRealmRoute.page, path: 'realms/new'), | ||||
|     AutoRoute(page: RealmDetailRoute.page, path: 'realms/:slug'), | ||||
|     AutoRoute(page: EditRealmRoute.page, path: 'realms/:slug/edit'), | ||||
|   ]; | ||||
| // Provider for the router | ||||
| final routerProvider = Provider<GoRouter>((ref) { | ||||
|   return GoRouter( | ||||
|     navigatorKey: rootNavigatorKey, | ||||
|     initialLocation: '/', | ||||
|     routes: [ | ||||
|       ShellRoute( | ||||
|         navigatorKey: _shellNavigatorKey, | ||||
|         builder: (context, state, child) { | ||||
|           return AppWrapper(child: child); | ||||
|         }, | ||||
|         routes: [ | ||||
|           // Standalone routes without bottom navigation | ||||
|           GoRoute( | ||||
|             path: '/posts/compose', | ||||
|             builder: (context, state) => const PostComposeScreen(), | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/posts/:id/edit', | ||||
|             builder: (context, state) { | ||||
|               final id = state.pathParameters['id']!; | ||||
|               return PostEditScreen(id: id); | ||||
|             }, | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/chat/:id/call', | ||||
|             builder: (context, state) { | ||||
|               final id = state.pathParameters['id']!; | ||||
|               return CallScreen(roomId: id); | ||||
|             }, | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/account/:name/calendar', | ||||
|             builder: (context, state) { | ||||
|               final name = state.pathParameters['name']!; | ||||
|               return EventCalanderScreen(name: name); | ||||
|             }, | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/creators', | ||||
|             builder: (context, state) => const CreatorHubScreen(), | ||||
|             routes: [ | ||||
|               GoRoute( | ||||
|                 path: ':name/posts', | ||||
|                 builder: (context, state) { | ||||
|                   final name = state.pathParameters['name']!; | ||||
|                   return CreatorPostListScreen(pubName: name); | ||||
|                 }, | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: ':name/stickers', | ||||
|                 builder: (context, state) { | ||||
|                   final name = state.pathParameters['name']!; | ||||
|                   return StickersScreen(pubName: name); | ||||
|                 }, | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: ':name/stickers/new', | ||||
|                 builder: (context, state) { | ||||
|                   final name = state.pathParameters['name']!; | ||||
|                   return NewStickerPacksScreen(pubName: name); | ||||
|                 }, | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: ':name/stickers/:packId/edit', | ||||
|                 builder: (context, state) { | ||||
|                   final name = state.pathParameters['name']!; | ||||
|                   final packId = state.pathParameters['packId']!; | ||||
|                   return EditStickerPacksScreen( | ||||
|                     pubName: name, | ||||
|                     packId: packId, | ||||
|                   ); | ||||
|                 }, | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: ':name/stickers/:packId', | ||||
|                 builder: (context, state) { | ||||
|                   final name = state.pathParameters['name']!; | ||||
|                   final packId = state.pathParameters['packId']!; | ||||
|                   return StickerPackDetailScreen(pubName: name, id: packId); | ||||
|                 }, | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: ':name/stickers/:packId/new', | ||||
|                 builder: (context, state) { | ||||
|                   final packId = state.pathParameters['packId']!; | ||||
|                   return NewStickersScreen(packId: packId); | ||||
|                 }, | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: ':name/stickers/:packId/:id/edit', | ||||
|                 builder: (context, state) { | ||||
|                   final packId = state.pathParameters['packId']!; | ||||
|                   final id = state.pathParameters['id']!; | ||||
|                   return EditStickersScreen(id: id, packId: packId); | ||||
|                 }, | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: 'new', | ||||
|                 builder: (context, state) => const NewPublisherScreen(), | ||||
|               ), | ||||
|               GoRoute( | ||||
|                 path: ':name/edit', | ||||
|                 builder: (context, state) { | ||||
|                   final name = state.pathParameters['name']!; | ||||
|                   return EditPublisherScreen(name: name); | ||||
|                 }, | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|  | ||||
|           // Auth routes | ||||
|           GoRoute( | ||||
|             path: '/auth/login', | ||||
|             builder: (context, state) => const LoginScreen(), | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/auth/create-account', | ||||
|             builder: (context, state) => const CreateAccountScreen(), | ||||
|           ), | ||||
|  | ||||
|           // Other routes | ||||
|           GoRoute( | ||||
|             path: '/settings', | ||||
|             builder: (context, state) => const SettingsScreen(), | ||||
|           ), | ||||
|  | ||||
|           // Main tabs with TabsScreen shell | ||||
|           ShellRoute( | ||||
|             navigatorKey: _tabsShellKey, | ||||
|             builder: (context, state, child) { | ||||
|               return TabsScreen(child: child); | ||||
|             }, | ||||
|             routes: [ | ||||
|               // Explore tab | ||||
|               GoRoute( | ||||
|                 path: '/', | ||||
|                 builder: (context, state) => const ExploreScreen(), | ||||
|                 routes: [ | ||||
|                   GoRoute( | ||||
|                     path: 'posts/:id', | ||||
|                     builder: (context, state) { | ||||
|                       final id = state.pathParameters['id']!; | ||||
|                       return PostDetailScreen(id: id); | ||||
|                     }, | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: 'publishers/:name', | ||||
|                     builder: (context, state) { | ||||
|                       final name = state.pathParameters['name']!; | ||||
|                       return PublisherProfileScreen(name: name); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|  | ||||
|               // Chat tab | ||||
|               GoRoute( | ||||
|                 path: '/chat', | ||||
|                 builder: (context, state) => const ChatListScreen(), | ||||
|                 routes: [ | ||||
|                   GoRoute( | ||||
|                     path: ':id', | ||||
|                     builder: (context, state) { | ||||
|                       final id = state.pathParameters['id']!; | ||||
|                       return ChatRoomScreen(id: id); | ||||
|                     }, | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: 'new', | ||||
|                     builder: (context, state) => const NewChatScreen(), | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: ':id/edit', | ||||
|                     builder: (context, state) { | ||||
|                       final id = state.pathParameters['id']!; | ||||
|                       return EditChatScreen(id: id); | ||||
|                     }, | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: ':id/detail', | ||||
|                     builder: (context, state) { | ||||
|                       final id = state.pathParameters['id']!; | ||||
|                       return ChatDetailScreen(id: id); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|  | ||||
|               // Realms tab | ||||
|                GoRoute( | ||||
|                  path: '/realms', | ||||
|                  builder: (context, state) => const RealmListScreen(), | ||||
|                 routes: [ | ||||
|                   GoRoute( | ||||
|                     path: 'new', | ||||
|                     builder: (context, state) => const NewRealmScreen(), | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: ':slug', | ||||
|                     builder: (context, state) { | ||||
|                       final slug = state.pathParameters['slug']!; | ||||
|                       return RealmDetailScreen(slug: slug); | ||||
|                     }, | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: ':slug/edit', | ||||
|                     builder: (context, state) { | ||||
|                       final slug = state.pathParameters['slug']!; | ||||
|                       return EditRealmScreen(slug: slug); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|  | ||||
|               // Account tab | ||||
|               GoRoute( | ||||
|                 path: '/account', | ||||
|                 builder: (context, state) => const AccountScreen(), | ||||
|                 routes: [ | ||||
|                   GoRoute( | ||||
|                     path: 'notifications', | ||||
|                     builder: (context, state) => const NotificationScreen(), | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: 'wallet', | ||||
|                     builder: (context, state) => const WalletScreen(), | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: 'relationships', | ||||
|                     builder: (context, state) => const RelationshipScreen(), | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: ':name', | ||||
|                     builder: (context, state) { | ||||
|                       final name = state.pathParameters['name']!; | ||||
|                       return AccountProfileScreen(name: name); | ||||
|                     }, | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: 'me/update', | ||||
|                     builder: (context, state) => const UpdateProfileScreen(), | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: 'me/leveling', | ||||
|                     builder: (context, state) => const LevelingScreen(), | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     path: 'settings', | ||||
|                     builder: (context, state) => const AccountSettingsScreen(), | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ], | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| // Navigation helper functions | ||||
| class AppRouter { | ||||
|   static GoRouter of(BuildContext context) { | ||||
|     return GoRouter.of(context); | ||||
|   } | ||||
|  | ||||
|   static void go(BuildContext context, String path) { | ||||
|     context.go(path); | ||||
|   } | ||||
|  | ||||
|   static void push(BuildContext context, String path) { | ||||
|     context.push(path); | ||||
|   } | ||||
|  | ||||
|   static void pop(BuildContext context) { | ||||
|     context.pop(); | ||||
|   } | ||||
|  | ||||
|   static bool canPop(BuildContext context) { | ||||
|     return context.canPop(); | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										1773
									
								
								lib/route.gr.dart
									
									
									
									
									
								
							
							
						
						
									
										1773
									
								
								lib/route.gr.dart
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,14 +1,13 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/pods/message.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/pods/userinfo.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/screens/notification.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/widgets/account/account_name.dart'; | ||||
| @@ -19,9 +18,9 @@ import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| @RoutePage() | ||||
| class AccountShellScreen extends HookConsumerWidget { | ||||
|   const AccountShellScreen({super.key}); | ||||
|   final Widget child; | ||||
|   const AccountShellScreen({super.key, required this.child}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -34,17 +33,16 @@ class AccountShellScreen extends HookConsumerWidget { | ||||
|           children: [ | ||||
|             Flexible(flex: 2, child: AccountScreen(isAside: true)), | ||||
|             VerticalDivider(width: 1), | ||||
|             Flexible(flex: 3, child: AutoRouter()), | ||||
|             Flexible(flex: 3, child: child), | ||||
|           ], | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return AppBackground(isRoot: true, child: AutoRouter()); | ||||
|     return AppBackground(isRoot: true, child: child); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class AccountScreen extends HookConsumerWidget { | ||||
|   final bool isAside; | ||||
|   const AccountScreen({super.key, this.isAside = false}); | ||||
| @@ -100,9 +98,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|                           radius: 24, | ||||
|                         ), | ||||
|                         onTap: () { | ||||
|                           context.router.push( | ||||
|                             AccountProfileRoute(name: user.value!.name), | ||||
|                           ); | ||||
|                           context.push('/account/${user.value!.name}'); | ||||
|                         }, | ||||
|                       ), | ||||
|                       Expanded( | ||||
| @@ -147,7 +143,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|                 progress: user.value!.profile.levelingProgress, | ||||
|               ), | ||||
|               onTap: () { | ||||
|                 context.router.push(LevelingRoute()); | ||||
|                 context.push('/account/leveling'); | ||||
|               }, | ||||
|             ).padding(horizontal: 12), | ||||
|             Row( | ||||
| @@ -165,7 +161,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|                         ], | ||||
|                       ).padding(horizontal: 16, vertical: 12), | ||||
|                       onTap: () { | ||||
|                         context.router.push(CreatorHubShellRoute()); | ||||
|                         context.push('/creators'); | ||||
|                       }, | ||||
|                     ), | ||||
|                   ).height(140), | ||||
| @@ -204,7 +200,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|                 ], | ||||
|               ), | ||||
|               onTap: () { | ||||
|                 context.router.push(NotificationRoute()); | ||||
|                 context.push('/notification'); | ||||
|               }, | ||||
|             ), | ||||
|             ListTile( | ||||
| @@ -214,7 +210,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|               title: Text('wallet').tr(), | ||||
|               onTap: () { | ||||
|                 context.router.push(WalletRoute()); | ||||
|                 context.push('/wallet'); | ||||
|               }, | ||||
|             ), | ||||
|             ListTile( | ||||
| @@ -224,7 +220,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|               title: Text('relationships').tr(), | ||||
|               onTap: () { | ||||
|                 context.router.push(RelationshipRoute()); | ||||
|                 context.push('/account/relationship'); | ||||
|               }, | ||||
|             ), | ||||
|             const Divider(height: 1).padding(vertical: 8), | ||||
| @@ -235,7 +231,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|               title: Text('appSettings').tr(), | ||||
|               onTap: () { | ||||
|                 context.router.push(SettingsRoute()); | ||||
|                 context.push('/settings'); | ||||
|               }, | ||||
|             ), | ||||
|             ListTile( | ||||
| @@ -245,7 +241,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|               title: Text('updateYourProfile').tr(), | ||||
|               onTap: () { | ||||
|                 context.router.push(UpdateProfileRoute()); | ||||
|                 context.push('/account/me/update'); | ||||
|               }, | ||||
|             ), | ||||
|             ListTile( | ||||
| @@ -255,7 +251,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|               title: Text('accountSettings').tr(), | ||||
|               onTap: () { | ||||
|                 context.router.push(AccountSettingsRoute()); | ||||
|                 context.push('/account/me/settings'); | ||||
|               }, | ||||
|             ), | ||||
|             if (kDebugMode) const Divider(height: 1).padding(vertical: 8), | ||||
| @@ -320,7 +316,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget { | ||||
|                   child: Card( | ||||
|                     child: InkWell( | ||||
|                       onTap: () { | ||||
|                         context.router.push(CreateAccountRoute()); | ||||
|                         context.push('/auth/create'); | ||||
|                       }, | ||||
|                       child: Padding( | ||||
|                         padding: const EdgeInsets.all(16), | ||||
| @@ -342,7 +338,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget { | ||||
|                   child: Card( | ||||
|                     child: InkWell( | ||||
|                       onTap: () { | ||||
|                         context.router.push(LoginRoute()); | ||||
|                         context.push('/auth/login'); | ||||
|                       }, | ||||
|                       child: Padding( | ||||
|                         padding: const EdgeInsets.all(16), | ||||
| @@ -361,7 +357,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget { | ||||
|                 const Gap(8), | ||||
|                 TextButton( | ||||
|                   onPressed: () { | ||||
|                     context.router.push(SettingsRoute()); | ||||
|                     context.push('/settings'); | ||||
|                   }, | ||||
|                   child: Text('appSettings').tr(), | ||||
|                 ).center(), | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -12,10 +11,9 @@ import 'package:island/widgets/account/event_calendar.dart'; | ||||
| import 'package:island/widgets/account/fortune_graph.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| @RoutePage() | ||||
| class EventCalanderScreen extends HookConsumerWidget { | ||||
|   final String name; | ||||
|   const EventCalanderScreen({super.key, @PathParam("name") required this.name}); | ||||
|   const EventCalanderScreen({super.key, required this.name}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| @@ -31,7 +30,6 @@ Future<SnWalletSubscription?> accountStellarSubscription(Ref ref) async { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class LevelingScreen extends HookConsumerWidget { | ||||
|   const LevelingScreen({super.key}); | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:auto_route/annotations.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| @@ -51,7 +50,6 @@ Future<List<SnAccountConnection>> accountConnections(Ref ref) async { | ||||
|       .toList(); | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class AccountSettingsScreen extends HookConsumerWidget { | ||||
|   const AccountSettingsScreen({super.key}); | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:croppy/croppy.dart' hide cropImage; | ||||
| import 'package:dropdown_button2/dropdown_button2.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| @@ -20,7 +19,6 @@ import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| const kServerSupportedLanguages = {'en-US': 'en-us', 'zh-CN': 'zh-hans'}; | ||||
|  | ||||
| @RoutePage() | ||||
| class UpdateProfileScreen extends HookConsumerWidget { | ||||
|   const UpdateProfileScreen({super.key}); | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/chat.dart'; | ||||
| @@ -96,13 +96,9 @@ Future<SnRelationship?> accountRelationship(Ref ref, String uname) async { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class AccountProfileScreen extends HookConsumerWidget { | ||||
|   final String name; | ||||
|   const AccountProfileScreen({ | ||||
|     super.key, | ||||
|     @PathParam("name") required this.name, | ||||
|   }); | ||||
|   const AccountProfileScreen({super.key, required this.name}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -142,7 +138,7 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|     Future<void> directMessageAction() async { | ||||
|       if (!account.hasValue) return; | ||||
|       if (accountChat.value != null) { | ||||
|         context.router.pushPath('/chat/${accountChat.value!.id}'); | ||||
|         context.push('/chat/${accountChat.value!.id}'); | ||||
|         return; | ||||
|       } | ||||
|       showLoadingModal(context); | ||||
| @@ -153,7 +149,7 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|           data: {'related_user_id': account.value!.id}, | ||||
|         ); | ||||
|         final chat = SnChatRoom.fromJson(resp.data); | ||||
|         if (context.mounted) context.router.pushPath('/chat/${chat.id}'); | ||||
|         if (context.mounted) context.push('/chat/${chat.id}'); | ||||
|         ref.invalidate(accountDirectChatProvider(name)); | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| @@ -204,7 +203,6 @@ class RelationshipListTile extends StatelessWidget { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class RelationshipScreen extends HookConsumerWidget { | ||||
|   const RelationshipScreen({super.key}); | ||||
|  | ||||
|   | ||||
| @@ -1,12 +1,11 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:email_validator/email_validator.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/screens/account/me/update.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| @@ -16,7 +15,6 @@ import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| import 'captcha.dart'; | ||||
|  | ||||
| @RoutePage() | ||||
| class CreateAccountScreen extends HookConsumerWidget { | ||||
|   const CreateAccountScreen({super.key}); | ||||
|  | ||||
| @@ -307,7 +305,7 @@ class _PostCreateModal extends HookConsumerWidget { | ||||
|             TextButton( | ||||
|               onPressed: () { | ||||
|                 Navigator.pop(context); | ||||
|                 context.router.replace(LoginRoute()); | ||||
|                 context.pushReplacement('/auth/login'); | ||||
|               }, | ||||
|               child: Text('login'.tr()), | ||||
|             ), | ||||
|   | ||||
| @@ -3,7 +3,6 @@ import 'dart:io'; | ||||
| import 'dart:math' as math; | ||||
|  | ||||
| import 'package:animations/animations.dart'; | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:device_info_plus/device_info_plus.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| @@ -43,7 +42,6 @@ final Map<int, (String, String, IconData)> kFactorTypes = { | ||||
|   4: ('authFactorPin', 'authFactorPinDescription', Symbols.nest_secure_alarm), | ||||
| }; | ||||
|  | ||||
| @RoutePage() | ||||
| class LoginScreen extends HookConsumerWidget { | ||||
|   const LoginScreen({super.key}); | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import 'package:auto_route/annotations.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -14,10 +13,9 @@ import 'package:livekit_client/livekit_client.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| @RoutePage() | ||||
| class CallScreen extends HookConsumerWidget { | ||||
|   final String roomId; | ||||
|   const CallScreen({super.key, @PathParam('id') required this.roomId}); | ||||
|   const CallScreen({super.key, required this.roomId}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:croppy/croppy.dart' hide cropImage; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| @@ -15,7 +15,6 @@ import 'package:island/pods/call.dart'; | ||||
| import 'package:island/pods/chat_summary.dart'; | ||||
| import 'package:island/pods/config.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/screens/realm/realms.dart'; | ||||
| import 'package:island/services/file.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| @@ -173,9 +172,9 @@ Future<List<SnChatRoom>> chatroomsJoined(Ref ref) async { | ||||
|       .toList(); | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class ChatShellScreen extends HookConsumerWidget { | ||||
|   const ChatShellScreen({super.key}); | ||||
|   final Widget child; | ||||
|   const ChatShellScreen({super.key, required this.child}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -188,17 +187,16 @@ class ChatShellScreen extends HookConsumerWidget { | ||||
|           children: [ | ||||
|             Flexible(flex: 2, child: ChatListScreen(isAside: true)), | ||||
|             VerticalDivider(width: 1), | ||||
|             Flexible(flex: 4, child: AutoRouter()), | ||||
|             Flexible(flex: 4, child: child), | ||||
|           ], | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return AppBackground(isRoot: true, child: AutoRouter()); | ||||
|     return AppBackground(isRoot: true, child: child); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class ChatListScreen extends HookConsumerWidget { | ||||
|   final bool isAside; | ||||
|   const ChatListScreen({super.key, this.isAside = false}); | ||||
| @@ -319,7 +317,7 @@ class ChatListScreen extends HookConsumerWidget { | ||||
|                       leading: const Icon(Symbols.add), | ||||
|                       onTap: () { | ||||
|                         Navigator.pop(context); | ||||
|                         context.pushRoute(NewChatRoute()).then((value) { | ||||
|                         context.push('/chat/new').then((value) { | ||||
|                           if (value != null) { | ||||
|                             ref.invalidate(chatroomsJoinedProvider); | ||||
|                           } | ||||
| @@ -400,16 +398,7 @@ class ChatListScreen extends HookConsumerWidget { | ||||
|                               room: item, | ||||
|                               isDirect: item.type == 1, | ||||
|                               onTap: () { | ||||
|                                 if (context.router.topRoute.name == | ||||
|                                     ChatRoomRoute.name) { | ||||
|                                   context.router.replace( | ||||
|                                     ChatRoomRoute(id: item.id), | ||||
|                                   ); | ||||
|                                 } else { | ||||
|                                   context.router.push( | ||||
|                                     ChatRoomRoute(id: item.id), | ||||
|                                   ); | ||||
|                                 } | ||||
|                                 context.push('/chat/${item.id}'); | ||||
|                               }, | ||||
|                             ); | ||||
|                           }, | ||||
| @@ -456,7 +445,6 @@ Future<SnChatMember?> chatroomIdentity(Ref ref, String? identifier) async { | ||||
|   return SnChatMember.fromJson(resp.data); | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class NewChatScreen extends StatelessWidget { | ||||
|   const NewChatScreen({super.key}); | ||||
|  | ||||
| @@ -466,10 +454,9 @@ class NewChatScreen extends StatelessWidget { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class EditChatScreen extends HookConsumerWidget { | ||||
|   final String? id; | ||||
|   const EditChatScreen({super.key, @PathParam("id") this.id}); | ||||
|   const EditChatScreen({super.key, this.id}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -579,7 +566,7 @@ class EditChatScreen extends HookConsumerWidget { | ||||
|           options: Options(method: id == null ? 'POST' : 'PATCH'), | ||||
|         ); | ||||
|         if (context.mounted) { | ||||
|           context.maybePop(SnChatRoom.fromJson(resp.data)); | ||||
|           context.pop(SnChatRoom.fromJson(resp.data)); | ||||
|         } | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:convert'; | ||||
| import 'dart:io'; | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| @@ -18,7 +18,6 @@ import 'package:island/pods/config.dart'; | ||||
| import 'package:island/pods/database.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/pods/websocket.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| @@ -288,10 +287,9 @@ class MessagesNotifier extends _$MessagesNotifier { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class ChatRoomScreen extends HookConsumerWidget { | ||||
|   final String id; | ||||
|   const ChatRoomScreen({super.key, @PathParam("id") required this.id}); | ||||
|   const ChatRoomScreen({super.key, required this.id}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -605,7 +603,7 @@ class ChatRoomScreen extends HookConsumerWidget { | ||||
|           IconButton( | ||||
|             icon: const Icon(Icons.more_vert), | ||||
|             onPressed: () { | ||||
|               context.router.push(ChatDetailRoute(id: id)); | ||||
|               context.push('/chat/id/detail'); | ||||
|             }, | ||||
|           ), | ||||
|           const Gap(8), | ||||
|   | ||||
| @@ -1,14 +1,13 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/chat.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/screens/chat/chat.dart'; | ||||
| import 'package:island/widgets/account/account_picker.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| @@ -23,10 +22,9 @@ import 'package:styled_widget/styled_widget.dart'; | ||||
| part 'room_detail.freezed.dart'; | ||||
| part 'room_detail.g.dart'; | ||||
|  | ||||
| @RoutePage() | ||||
| class ChatDetailScreen extends HookConsumerWidget { | ||||
|   final String id; | ||||
|   const ChatDetailScreen({super.key, @PathParam("id") required this.id}); | ||||
|   const ChatDetailScreen({super.key, required this.id}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -391,7 +389,7 @@ class _ChatRoomActionMenu extends HookConsumerWidget { | ||||
|             if ((chatIdentity.value?.role ?? 0) >= 50) | ||||
|               PopupMenuItem( | ||||
|                 onTap: () { | ||||
|                   context.router.replace(EditChatRoute(id: id)); | ||||
|                   context.pushReplacement('/chat/$id/edit'); | ||||
|                 }, | ||||
|                 child: Row( | ||||
|                   children: [ | ||||
| @@ -426,9 +424,7 @@ class _ChatRoomActionMenu extends HookConsumerWidget { | ||||
|                       client.delete('/chat/$id'); | ||||
|                       ref.invalidate(chatroomsJoinedProvider); | ||||
|                       if (context.mounted) { | ||||
|                         context.router.popUntil( | ||||
|                           (route) => route is ChatRoomRoute, | ||||
|                         ); | ||||
|                         context.pop(); | ||||
|                       } | ||||
|                     } | ||||
|                   }); | ||||
| @@ -461,9 +457,7 @@ class _ChatRoomActionMenu extends HookConsumerWidget { | ||||
|                       client.delete('/chat/$id/members/me'); | ||||
|                       ref.invalidate(chatroomsJoinedProvider); | ||||
|                       if (context.mounted) { | ||||
|                         context.router.popUntil( | ||||
|                           (route) => route is ChatRoomRoute, | ||||
|                         ); | ||||
|                         context.pop(); | ||||
|                       } | ||||
|                     } | ||||
|                   }); | ||||
|   | ||||
| @@ -1,13 +1,12 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:dropdown_button2/dropdown_button2.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/screens/creators/publishers.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| @@ -27,9 +26,9 @@ Future<SnPublisherStats?> publisherStats(Ref ref, String? uname) async { | ||||
|   return SnPublisherStats.fromJson(resp.data); | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class CreatorHubShellScreen extends StatelessWidget { | ||||
|   const CreatorHubShellScreen({super.key}); | ||||
|   final Widget child; | ||||
|   const CreatorHubShellScreen({super.key, required this.child}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
| @@ -39,15 +38,14 @@ class CreatorHubShellScreen extends StatelessWidget { | ||||
|         children: [ | ||||
|           SizedBox(width: 360, child: const CreatorHubScreen(isAside: true)), | ||||
|           const VerticalDivider(width: 1), | ||||
|           Expanded(child: AutoRouter()), | ||||
|           Expanded(child: child), | ||||
|         ], | ||||
|       ); | ||||
|     } | ||||
|     return AutoRouter(); | ||||
|     return child; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class CreatorHubScreen extends HookConsumerWidget { | ||||
|   final bool isAside; | ||||
|   const CreatorHubScreen({super.key, this.isAside = false}); | ||||
| @@ -65,8 +63,8 @@ class CreatorHubScreen extends HookConsumerWidget { | ||||
|     ); | ||||
|  | ||||
|     void updatePublisher() { | ||||
|       context.router | ||||
|           .push(EditPublisherRoute(name: currentPublisher.value!.name)) | ||||
|       context | ||||
|           .push('/creators/${currentPublisher.value!.name}/edit') | ||||
|           .then((value) async { | ||||
|             if (value == null) return; | ||||
|             final data = await ref.refresh(publishersManagedProvider.future); | ||||
| @@ -223,7 +221,7 @@ class CreatorHubScreen extends HookConsumerWidget { | ||||
|                             subtitle: Text('createPublisherHint').tr(), | ||||
|                             trailing: const Icon(Symbols.chevron_right), | ||||
|                             onTap: () { | ||||
|                               context.router.push(NewPublisherRoute()).then(( | ||||
|                               context.push('/creators/publishers/new').then(( | ||||
|                                 value, | ||||
|                               ) { | ||||
|                                 if (value != null) { | ||||
| @@ -249,10 +247,8 @@ class CreatorHubScreen extends HookConsumerWidget { | ||||
|                               horizontal: 24, | ||||
|                             ), | ||||
|                             onTap: () { | ||||
|                               context.router.push( | ||||
|                                 StickersRoute( | ||||
|                                   pubName: currentPublisher.value!.name, | ||||
|                                 ), | ||||
|                               context.push( | ||||
|                                 '/creators/${currentPublisher.value!.name}/stickers', | ||||
|                               ); | ||||
|                             }, | ||||
|                           ), | ||||
| @@ -265,10 +261,8 @@ class CreatorHubScreen extends HookConsumerWidget { | ||||
|                               horizontal: 24, | ||||
|                             ), | ||||
|                             onTap: () { | ||||
|                               context.router.push( | ||||
|                                 CreatorPostListRoute( | ||||
|                                   pubName: currentPublisher.value!.name, | ||||
|                                 ), | ||||
|                               context.push( | ||||
|                                 '/creators/${currentPublisher.value!.name}/posts', | ||||
|                               ); | ||||
|                             }, | ||||
|                           ), | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| @@ -8,13 +8,9 @@ import 'package:island/widgets/content/sheet.dart'; | ||||
| import 'package:island/widgets/post/post_list.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
|  | ||||
| @RoutePage() | ||||
| class CreatorPostListScreen extends HookConsumerWidget { | ||||
|   final String pubName; | ||||
|   const CreatorPostListScreen({ | ||||
|     super.key, | ||||
|     @PathParam('name') required this.pubName, | ||||
|   }); | ||||
|   const CreatorPostListScreen({super.key, required this.pubName}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -34,7 +30,7 @@ class CreatorPostListScreen extends HookConsumerWidget { | ||||
|                     subtitle: Text('Create a regular post'), | ||||
|                     onTap: () async { | ||||
|                       Navigator.pop(context); | ||||
|                       final result = await context.router.pushPath( | ||||
|                       final result = await context.push( | ||||
|                         '/posts/compose?type=0', | ||||
|                       ); | ||||
|                       if (result == true) { | ||||
| @@ -48,7 +44,7 @@ class CreatorPostListScreen extends HookConsumerWidget { | ||||
|                     subtitle: Text('Create a detailed article'), | ||||
|                     onTap: () async { | ||||
|                       Navigator.pop(context); | ||||
|                       final result = await context.router.pushPath( | ||||
|                       final result = await context.push( | ||||
|                         '/posts/compose?type=1', | ||||
|                       ); | ||||
|                       if (result == true) { | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:croppy/croppy.dart' hide cropImage; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:image_picker/image_picker.dart'; | ||||
| import 'package:island/models/file.dart'; | ||||
| @@ -44,7 +44,6 @@ Future<SnPublisher?> publisher(Ref ref, String? identifier) async { | ||||
|   return SnPublisher.fromJson(resp.data); | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class NewPublisherScreen extends StatelessWidget { | ||||
|   const NewPublisherScreen({super.key}); | ||||
|  | ||||
| @@ -54,10 +53,9 @@ class NewPublisherScreen extends StatelessWidget { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class EditPublisherScreen extends HookConsumerWidget { | ||||
|   final String? name; | ||||
|   const EditPublisherScreen({super.key, @PathParam('id') this.name}); | ||||
|   const EditPublisherScreen({super.key, this.name}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -177,7 +175,7 @@ class EditPublisherScreen extends HookConsumerWidget { | ||||
|           options: Options(method: name == null ? 'POST' : 'PATCH'), | ||||
|         ); | ||||
|         if (context.mounted) { | ||||
|           context.maybePop(SnPublisher.fromJson(resp.data)); | ||||
|           context.pop(SnPublisher.fromJson(resp.data)); | ||||
|         } | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| @@ -10,7 +10,6 @@ import 'package:google_fonts/google_fonts.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/sticker.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/screens/creators/stickers/stickers.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| @@ -34,14 +33,13 @@ Future<List<SnSticker>> stickerPackContent(Ref ref, String packId) async { | ||||
|       .toList(); | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class StickerPackDetailScreen extends HookConsumerWidget { | ||||
|   final String id; | ||||
|   final String pubName; | ||||
|   const StickerPackDetailScreen({ | ||||
|     super.key, | ||||
|     @PathParam('name') required this.pubName, | ||||
|     @PathParam('packId') required this.id, | ||||
|     required this.pubName, | ||||
|     required this.id, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
| @@ -76,7 +74,7 @@ class StickerPackDetailScreen extends HookConsumerWidget { | ||||
|           IconButton( | ||||
|             icon: const Icon(Symbols.add_circle), | ||||
|             onPressed: () { | ||||
|               AutoRouter.of(context).push(NewStickersRoute(packId: id)).then(( | ||||
|               context.push('/creators/stickers/$id/new').then(( | ||||
|                 value, | ||||
|               ) { | ||||
|                 if (value != null) { | ||||
| @@ -175,12 +173,9 @@ class StickerPackDetailScreen extends HookConsumerWidget { | ||||
|                                         title: 'edit'.tr(), | ||||
|                                         image: MenuImage.icon(Symbols.edit), | ||||
|                                         callback: () { | ||||
|                                           context.router | ||||
|                                           context | ||||
|                                               .push( | ||||
|                                                 EditStickersRoute( | ||||
|                                                   packId: id, | ||||
|                                                   id: sticker.id, | ||||
|                                                 ), | ||||
|                                                 '/creators/stickers/$id/edit/${sticker.id}', | ||||
|                                               ) | ||||
|                                               .then((value) { | ||||
|                                                 if (value != null) { | ||||
| @@ -264,8 +259,8 @@ class _StickerPackActionMenu extends HookConsumerWidget { | ||||
|           (context) => [ | ||||
|             PopupMenuItem( | ||||
|               onTap: () { | ||||
|                 context.router.push( | ||||
|                   EditStickerPacksRoute(pubName: pubName, packId: packId), | ||||
|                 context.push( | ||||
|                   '/creators/$pubName/stickers/$packId/edit', | ||||
|                 ); | ||||
|               }, | ||||
|               child: Row( | ||||
| @@ -299,7 +294,7 @@ class _StickerPackActionMenu extends HookConsumerWidget { | ||||
|                     final client = ref.watch(apiClientProvider); | ||||
|                     client.delete('/stickers/$packId'); | ||||
|                     ref.invalidate(stickerPacksNotifierProvider); | ||||
|                     if (context.mounted) context.router.maybePop(true); | ||||
|                     if (context.mounted) context.pop(true); | ||||
|                   } | ||||
|                 }); | ||||
|               }, | ||||
| @@ -331,13 +326,9 @@ Future<SnSticker?> stickerPackSticker( | ||||
|   return SnSticker.fromJson(resp.data); | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class NewStickersScreen extends StatelessWidget { | ||||
|   final String packId; | ||||
|   const NewStickersScreen({ | ||||
|     super.key, | ||||
|     @PathParam('packId') required this.packId, | ||||
|   }); | ||||
|   const NewStickersScreen({super.key, required this.packId}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
| @@ -345,15 +336,10 @@ class NewStickersScreen extends StatelessWidget { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class EditStickersScreen extends HookConsumerWidget { | ||||
|   final String packId; | ||||
|   final String? id; | ||||
|   const EditStickersScreen({ | ||||
|     super.key, | ||||
|     @PathParam("packId") required this.packId, | ||||
|     @PathParam("id") required this.id, | ||||
|   }); | ||||
|   const EditStickersScreen({super.key, required this.packId, required this.id}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|   | ||||
| @@ -1,13 +1,12 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/sticker.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| @@ -17,10 +16,9 @@ import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||
|  | ||||
| part 'stickers.g.dart'; | ||||
|  | ||||
| @RoutePage() | ||||
| class StickersScreen extends HookConsumerWidget { | ||||
|   final String pubName; | ||||
|   const StickersScreen({super.key, @PathParam("name") required this.pubName}); | ||||
|   const StickersScreen({super.key, required this.pubName}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -30,7 +28,7 @@ class StickersScreen extends HookConsumerWidget { | ||||
|         actions: [ | ||||
|           IconButton( | ||||
|             onPressed: () { | ||||
|               context.router.push(NewStickerPacksRoute(pubName: pubName)).then(( | ||||
|               context.push('/creators/stickers/new?pubName=pubName').then(( | ||||
|                 value, | ||||
|               ) { | ||||
|                 if (value != null) { | ||||
| @@ -73,8 +71,8 @@ class SliverStickerPacksList extends HookConsumerWidget { | ||||
|                 subtitle: Text(sticker.description), | ||||
|                 trailing: const Icon(Symbols.chevron_right), | ||||
|                 onTap: () { | ||||
|                   context.router.push( | ||||
|                     StickerPackDetailRoute(pubName: pubName, id: sticker.id), | ||||
|                   context.push( | ||||
|                     '/creators/$pubName/stickers/${sticker.id}', | ||||
|                   ); | ||||
|                 }, | ||||
|               ); | ||||
| @@ -137,13 +135,9 @@ Future<SnStickerPack?> stickerPack(Ref ref, String? packId) async { | ||||
|   return SnStickerPack.fromJson(resp.data); | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class NewStickerPacksScreen extends HookConsumerWidget { | ||||
|   final String pubName; | ||||
|   const NewStickerPacksScreen({ | ||||
|     super.key, | ||||
|     @PathParam("name") required this.pubName, | ||||
|   }); | ||||
|   const NewStickerPacksScreen({super.key, required this.pubName}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -151,15 +145,10 @@ class NewStickerPacksScreen extends HookConsumerWidget { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class EditStickerPacksScreen extends HookConsumerWidget { | ||||
|   final String pubName; | ||||
|   final String? packId; | ||||
|   const EditStickerPacksScreen({ | ||||
|     super.key, | ||||
|     @PathParam("name") required this.pubName, | ||||
|     @PathParam("packId") this.packId, | ||||
|   }); | ||||
|   const EditStickerPacksScreen({super.key, required this.pubName, this.packId}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -200,7 +189,7 @@ class EditStickerPacksScreen extends HookConsumerWidget { | ||||
|           ), | ||||
|         ); | ||||
|         if (!context.mounted) return; | ||||
|         context.router.maybePop(SnStickerPack.fromJson(resp.data)); | ||||
|         context.pop(SnStickerPack.fromJson(resp.data)); | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
|       } finally { | ||||
|   | ||||
| @@ -1,12 +1,11 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/activity.dart'; | ||||
| import 'package:island/pods/userinfo.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
| @@ -22,13 +21,13 @@ import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| part 'explore.g.dart'; | ||||
|  | ||||
| @RoutePage() | ||||
| class ExploreShellScreen extends ConsumerWidget { | ||||
|   const ExploreShellScreen({super.key}); | ||||
| class ExploreShellScreen extends HookConsumerWidget { | ||||
|   final Widget child; | ||||
|   const ExploreShellScreen({super.key, required this.child}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final isWide = isWideScreen(context); | ||||
|     final isWide = MediaQuery.of(context).size.width > 640; | ||||
|  | ||||
|     if (isWide) { | ||||
|       return AppBackground( | ||||
| @@ -37,17 +36,16 @@ class ExploreShellScreen extends ConsumerWidget { | ||||
|           children: [ | ||||
|             Flexible(flex: 2, child: ExploreScreen(isAside: true)), | ||||
|             VerticalDivider(width: 1), | ||||
|             Flexible(flex: 3, child: AutoRouter()), | ||||
|             Flexible(flex: 3, child: child), | ||||
|           ], | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return AppBackground(isRoot: true, child: AutoRouter()); | ||||
|     return AppBackground(isRoot: true, child: child); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class ExploreScreen extends HookConsumerWidget { | ||||
|   final bool isAside; | ||||
|   const ExploreScreen({super.key, this.isAside = false}); | ||||
| @@ -126,7 +124,7 @@ class ExploreScreen extends HookConsumerWidget { | ||||
|         floatingActionButton: FloatingActionButton( | ||||
|           heroTag: Key("explore-page-fab"), | ||||
|           onPressed: () { | ||||
|             context.router.push(PostComposeRoute()).then((value) { | ||||
|             context.push('/posts/compose').then((value) { | ||||
|               if (value != null) { | ||||
|                 activitiesNotifier.forceRefresh(); | ||||
|               } | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:math' as math; | ||||
|  | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/user.dart'; | ||||
| @@ -107,7 +107,6 @@ class NotificationListNotifier extends _$NotificationListNotifier | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class NotificationScreen extends HookConsumerWidget { | ||||
|   const NotificationScreen({super.key}); | ||||
|  | ||||
| @@ -198,7 +197,7 @@ class NotificationScreen extends HookConsumerWidget { | ||||
|                         return; | ||||
|                       } | ||||
|                       if (uri.scheme == 'solian') { | ||||
|                         context.router.pushPath( | ||||
|                         context.push( | ||||
|                           ['', uri.host, ...uri.pathSegments].join('/'), | ||||
|                         ); | ||||
|                         return; | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -40,10 +39,9 @@ sealed class PostComposeInitialState with _$PostComposeInitialState { | ||||
|       _$PostComposeInitialStateFromJson(json); | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class PostEditScreen extends HookConsumerWidget { | ||||
|   final String id; | ||||
|   const PostEditScreen({super.key, @PathParam('id') required this.id}); | ||||
|   const PostEditScreen({super.key, required this.id}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -66,7 +64,6 @@ class PostEditScreen extends HookConsumerWidget { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class PostComposeScreen extends HookConsumerWidget { | ||||
|   final SnPost? originalPost; | ||||
|   final SnPost? repliedPost; | ||||
| @@ -78,7 +75,7 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|     this.originalPost, | ||||
|     this.repliedPost, | ||||
|     this.forwardedPost, | ||||
|     @QueryParam('type') this.type, | ||||
|     this.type, | ||||
|     this.initialState, | ||||
|   }); | ||||
|  | ||||
| @@ -106,15 +103,32 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|         originalPost: originalPost, | ||||
|         forwardedPost: effectiveForwardedPost, | ||||
|         repliedPost: effectiveRepliedPost, | ||||
|         postType: 0, // Regular post type | ||||
|       ), | ||||
|       [originalPost, effectiveForwardedPost, effectiveRepliedPost], | ||||
|     ); | ||||
|  | ||||
|     // Add a listener to the entire state to trigger rebuilds | ||||
|     final stateNotifier = useMemoized( | ||||
|       () => Listenable.merge([ | ||||
|         state.titleController, | ||||
|         state.descriptionController, | ||||
|         state.contentController, | ||||
|         state.visibility, | ||||
|         state.attachments, | ||||
|         state.attachmentProgress, | ||||
|         state.currentPublisher, | ||||
|         state.submitting, | ||||
|       ]), | ||||
|       [state], | ||||
|     ); | ||||
|     useListenable(stateNotifier); | ||||
|  | ||||
|     // Start auto-save when component mounts | ||||
|     useEffect(() { | ||||
|       if (originalPost == null) { | ||||
|         // Only auto-save for new posts, not edits | ||||
|         state.startAutoSave(ref, postType: 0); | ||||
|         state.startAutoSave(ref); | ||||
|       } | ||||
|       return () => state.stopAutoSave(); | ||||
|     }, [state]); | ||||
| @@ -153,13 +167,18 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|         final drafts = ref.read(composeStorageNotifierProvider); | ||||
|         if (drafts.isNotEmpty) { | ||||
|           final mostRecentDraft = drafts.values.reduce( | ||||
|             (a, b) => (a.updatedAt ?? DateTime(0)).isAfter(b.updatedAt ?? DateTime(0)) ? a : b, | ||||
|             (a, b) => | ||||
|                 (a.updatedAt ?? DateTime(0)).isAfter(b.updatedAt ?? DateTime(0)) | ||||
|                     ? a | ||||
|                     : b, | ||||
|           ); | ||||
|  | ||||
|           // Only load if the draft has meaningful content | ||||
|           if (mostRecentDraft.content?.isNotEmpty == true || mostRecentDraft.title?.isNotEmpty == true) { | ||||
|           if (mostRecentDraft.content?.isNotEmpty == true || | ||||
|               mostRecentDraft.title?.isNotEmpty == true) { | ||||
|             state.titleController.text = mostRecentDraft.title ?? ''; | ||||
|             state.descriptionController.text = mostRecentDraft.description ?? ''; | ||||
|             state.descriptionController.text = | ||||
|                 mostRecentDraft.description ?? ''; | ||||
|             state.contentController.text = mostRecentDraft.content ?? ''; | ||||
|             state.visibility.value = mostRecentDraft.visibility; | ||||
|           } | ||||
| @@ -187,6 +206,8 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|               titleController: state.titleController, | ||||
|               descriptionController: state.descriptionController, | ||||
|               visibility: state.visibility, | ||||
|               tagsController: state.tagsController, | ||||
|               categoriesController: state.categoriesController, | ||||
|               onVisibilityChanged: () { | ||||
|                 // Trigger rebuild if needed | ||||
|               }, | ||||
| @@ -206,22 +227,18 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|         ), | ||||
|         itemCount: state.attachments.value.length, | ||||
|         itemBuilder: (context, idx) { | ||||
|           return ValueListenableBuilder<Map<int, double>>( | ||||
|             valueListenable: state.attachmentProgress, | ||||
|             builder: (context, progressMap, _) { | ||||
|               return AttachmentPreview( | ||||
|                 item: state.attachments.value[idx], | ||||
|                 progress: progressMap[idx], | ||||
|                 onRequestUpload: | ||||
|                     () => ComposeLogic.uploadAttachment(ref, state, idx), | ||||
|                 onDelete: () => ComposeLogic.deleteAttachment(ref, state, idx), | ||||
|                 onMove: (delta) { | ||||
|                   state.attachments.value = ComposeLogic.moveAttachment( | ||||
|                     state.attachments.value, | ||||
|                     idx, | ||||
|                     delta, | ||||
|                   ); | ||||
|                 }, | ||||
|           final progressMap = state.attachmentProgress.value; | ||||
|           return AttachmentPreview( | ||||
|             item: state.attachments.value[idx], | ||||
|             progress: progressMap[idx], | ||||
|             onRequestUpload: | ||||
|                 () => ComposeLogic.uploadAttachment(ref, state, idx), | ||||
|             onDelete: () => ComposeLogic.deleteAttachment(ref, state, idx), | ||||
|             onMove: (delta) { | ||||
|               state.attachments.value = ComposeLogic.moveAttachment( | ||||
|                 state.attachments.value, | ||||
|                 idx, | ||||
|                 delta, | ||||
|               ); | ||||
|             }, | ||||
|           ); | ||||
| @@ -235,26 +252,24 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|           for (var idx = 0; idx < state.attachments.value.length; idx++) | ||||
|             Container( | ||||
|               margin: const EdgeInsets.only(bottom: 8), | ||||
|               child: ValueListenableBuilder<Map<int, double>>( | ||||
|                 valueListenable: state.attachmentProgress, | ||||
|                 builder: (context, progressMap, _) { | ||||
|                   return AttachmentPreview( | ||||
|                     item: state.attachments.value[idx], | ||||
|                     progress: progressMap[idx], | ||||
|                     onRequestUpload: | ||||
|                         () => ComposeLogic.uploadAttachment(ref, state, idx), | ||||
|                     onDelete: | ||||
|                         () => ComposeLogic.deleteAttachment(ref, state, idx), | ||||
|                     onMove: (delta) { | ||||
|                       state.attachments.value = ComposeLogic.moveAttachment( | ||||
|                         state.attachments.value, | ||||
|                         idx, | ||||
|                         delta, | ||||
|                       ); | ||||
|                     }, | ||||
|                   ); | ||||
|                 }, | ||||
|               ), | ||||
|               child: () { | ||||
|                 final progressMap = state.attachmentProgress.value; | ||||
|                 return AttachmentPreview( | ||||
|                   item: state.attachments.value[idx], | ||||
|                   progress: progressMap[idx], | ||||
|                   onRequestUpload: | ||||
|                       () => ComposeLogic.uploadAttachment(ref, state, idx), | ||||
|                   onDelete: | ||||
|                       () => ComposeLogic.deleteAttachment(ref, state, idx), | ||||
|                   onMove: (delta) { | ||||
|                     state.attachments.value = ComposeLogic.moveAttachment( | ||||
|                       state.attachments.value, | ||||
|                       idx, | ||||
|                       delta, | ||||
|                     ); | ||||
|                   }, | ||||
|                 ); | ||||
|               }(), | ||||
|             ), | ||||
|         ], | ||||
|       ); | ||||
| @@ -290,7 +305,8 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|                               state.titleController.text = draft.title ?? ''; | ||||
|                               state.descriptionController.text = | ||||
|                                   draft.description ?? ''; | ||||
|                               state.contentController.text = draft.content ?? ''; | ||||
|                               state.contentController.text = | ||||
|                                   draft.content ?? ''; | ||||
|                               state.visibility.value = draft.visibility; | ||||
|                             } | ||||
|                           }, | ||||
| @@ -309,39 +325,31 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|               onPressed: showSettingsSheet, | ||||
|               tooltip: 'postSettings'.tr(), | ||||
|             ), | ||||
|             ValueListenableBuilder<bool>( | ||||
|               valueListenable: state.submitting, | ||||
|               builder: (context, submitting, _) { | ||||
|                 return IconButton( | ||||
|                   onPressed: | ||||
|                       submitting | ||||
|                           ? null | ||||
|                           : () => ComposeLogic.performAction( | ||||
|                             ref, | ||||
|                             state, | ||||
|                             context, | ||||
|                             originalPost: originalPost, | ||||
|                             repliedPost: repliedPost, | ||||
|                             forwardedPost: forwardedPost, | ||||
|                             postType: 0, // Regular post type | ||||
|                           ), | ||||
|                   icon: | ||||
|                       submitting | ||||
|                           ? SizedBox( | ||||
|                             width: 28, | ||||
|                             height: 28, | ||||
|                             child: const CircularProgressIndicator( | ||||
|                               color: Colors.white, | ||||
|                               strokeWidth: 2.5, | ||||
|                             ), | ||||
|                           ).center() | ||||
|                           : Icon( | ||||
|                             originalPost != null | ||||
|                                 ? Symbols.edit | ||||
|                                 : Symbols.upload, | ||||
|                           ), | ||||
|                 ); | ||||
|               }, | ||||
|             IconButton( | ||||
|               onPressed: | ||||
|                   state.submitting.value | ||||
|                       ? null | ||||
|                       : () => ComposeLogic.performAction( | ||||
|                         ref, | ||||
|                         state, | ||||
|                         context, | ||||
|                         originalPost: originalPost, | ||||
|                         repliedPost: repliedPost, | ||||
|                         forwardedPost: forwardedPost, | ||||
|                       ), | ||||
|               icon: | ||||
|                   state.submitting.value | ||||
|                       ? SizedBox( | ||||
|                         width: 28, | ||||
|                         height: 28, | ||||
|                         child: const CircularProgressIndicator( | ||||
|                           color: Colors.white, | ||||
|                           strokeWidth: 2.5, | ||||
|                         ), | ||||
|                       ).center() | ||||
|                       : Icon( | ||||
|                         originalPost != null ? Symbols.edit : Symbols.upload, | ||||
|                       ), | ||||
|             ), | ||||
|             const Gap(8), | ||||
|           ], | ||||
| @@ -402,7 +410,6 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|                                     originalPost: originalPost, | ||||
|                                     repliedPost: repliedPost, | ||||
|                                     forwardedPost: forwardedPost, | ||||
|                                     postType: 0, // Regular post type | ||||
|                                   ), | ||||
|                               child: TextField( | ||||
|                                 controller: state.contentController, | ||||
| @@ -423,22 +430,17 @@ class PostComposeScreen extends HookConsumerWidget { | ||||
|                             const Gap(8), | ||||
|  | ||||
|                             // Attachments preview | ||||
|                             ValueListenableBuilder<List<UniversalFile>>( | ||||
|                               valueListenable: state.attachments, | ||||
|                               builder: (context, attachments, _) { | ||||
|                                 if (attachments.isEmpty) { | ||||
|                                   return const SizedBox.shrink(); | ||||
|                                 } | ||||
|                                 return LayoutBuilder( | ||||
|                                   builder: (context, constraints) { | ||||
|                                     final isWide = isWideScreen(context); | ||||
|                                     return isWide | ||||
|                                         ? buildWideAttachmentGrid() | ||||
|                                         : buildNarrowAttachmentList(); | ||||
|                                   }, | ||||
|                                 ); | ||||
|                               }, | ||||
|                             ), | ||||
|                             if (state.attachments.value.isNotEmpty) | ||||
|                               LayoutBuilder( | ||||
|                                 builder: (context, constraints) { | ||||
|                                   final isWide = isWideScreen(context); | ||||
|                                   return isWide | ||||
|                                       ? buildWideAttachmentGrid() | ||||
|                                       : buildNarrowAttachmentList(); | ||||
|                                 }, | ||||
|                               ) | ||||
|                             else | ||||
|                               const SizedBox.shrink(), | ||||
|                           ], | ||||
|                         ), | ||||
|                       ), | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import 'dart:async'; | ||||
|  | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| @@ -26,10 +25,9 @@ import 'package:island/widgets/post/draft_manager.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| @RoutePage() | ||||
| class ArticleEditScreen extends HookConsumerWidget { | ||||
|   final String id; | ||||
|   const ArticleEditScreen({super.key, @PathParam('id') required this.id}); | ||||
|   const ArticleEditScreen({super.key, required this.id}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -50,7 +48,6 @@ class ArticleEditScreen extends HookConsumerWidget { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class ArticleComposeScreen extends HookConsumerWidget { | ||||
|   final SnPost? originalPost; | ||||
|  | ||||
| @@ -63,7 +60,10 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|  | ||||
|     final publishers = ref.watch(publishersManagedProvider); | ||||
|     final state = useMemoized( | ||||
|       () => ComposeLogic.createState(originalPost: originalPost), | ||||
|       () => ComposeLogic.createState( | ||||
|         originalPost: originalPost, | ||||
|         postType: 1, // Article type | ||||
|       ), | ||||
|       [originalPost], | ||||
|     ); | ||||
|  | ||||
| @@ -73,7 +73,7 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|       if (originalPost == null) { | ||||
|         // Only auto-save for new articles, not edits | ||||
|         autoSaveTimer = Timer.periodic(const Duration(seconds: 3), (_) { | ||||
|           ComposeLogic.saveDraftWithoutUpload(ref, state, postType: 1); | ||||
|           ComposeLogic.saveDraftWithoutUpload(ref, state); | ||||
|         }); | ||||
|       } | ||||
|       return () { | ||||
| @@ -81,7 +81,7 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|         state.stopAutoSave(); | ||||
|         // Save final draft before disposing | ||||
|         if (originalPost == null) { | ||||
|           ComposeLogic.saveDraftWithoutUpload(ref, state, postType: 1); | ||||
|           ComposeLogic.saveDraftWithoutUpload(ref, state); | ||||
|         } | ||||
|         ComposeLogic.dispose(state); | ||||
|         autoSaveTimer?.cancel(); | ||||
| @@ -143,6 +143,8 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|               titleController: state.titleController, | ||||
|               descriptionController: state.descriptionController, | ||||
|               visibility: state.visibility, | ||||
|               tagsController: state.tagsController, | ||||
|               categoriesController: state.categoriesController, | ||||
|               onVisibilityChanged: () { | ||||
|                 // Trigger rebuild if needed | ||||
|               }, | ||||
| @@ -363,7 +365,7 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|     return PopScope( | ||||
|       onPopInvoked: (_) { | ||||
|         if (originalPost == null) { | ||||
|           ComposeLogic.saveDraftWithoutUpload(ref, state, postType: 1); | ||||
|           ComposeLogic.saveDraftWithoutUpload(ref, state); | ||||
|         } | ||||
|       }, | ||||
|       child: AppScaffold( | ||||
| @@ -411,7 +413,7 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|               ), | ||||
|             IconButton( | ||||
|               icon: const Icon(Symbols.save), | ||||
|               onPressed: () => ComposeLogic.saveDraft(ref, state, postType: 1), | ||||
|               onPressed: () => ComposeLogic.saveDraft(ref, state), | ||||
|               tooltip: 'saveDraft'.tr(), | ||||
|             ), | ||||
|             IconButton( | ||||
| @@ -438,7 +440,6 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|                             state, | ||||
|                             context, | ||||
|                             originalPost: originalPost, | ||||
|                             postType: 1, // Article type | ||||
|                           ), | ||||
|                   icon: | ||||
|                       submitting | ||||
| @@ -531,18 +532,17 @@ class ArticleComposeScreen extends HookConsumerWidget { | ||||
|     if (isPaste && isModifierPressed) { | ||||
|       ComposeLogic.handlePaste(state); | ||||
|     } else if (isSave && isModifierPressed) { | ||||
|       ComposeLogic.saveDraft(ref, state, postType: 1); | ||||
|       ComposeLogic.saveDraft(ref, state); | ||||
|       ComposeLogic.saveDraft(ref, state); | ||||
|     } else if (isSubmit && isModifierPressed && !state.submitting.value) { | ||||
|       ComposeLogic.performAction( | ||||
|         ref, | ||||
|         state, | ||||
|         context, | ||||
|         originalPost: originalPost, | ||||
|         postType: 1, // Article type | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Helper method to save article draft | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| @@ -22,10 +21,9 @@ Future<SnPost?> post(Ref ref, String id) async { | ||||
|   return SnPost.fromJson(resp.data); | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class PostDetailScreen extends HookConsumerWidget { | ||||
|   final String id; | ||||
|   const PostDetailScreen({super.key, @PathParam('id') required this.id}); | ||||
|   const PostDetailScreen({super.key, required this.id}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| @@ -67,12 +67,11 @@ Future<Color?> publisherAppbarForcegroundColor(Ref ref, String pubName) async { | ||||
|   return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class PublisherProfileScreen extends HookConsumerWidget { | ||||
|   final String name; | ||||
|   const PublisherProfileScreen({ | ||||
|     super.key, | ||||
|     @PathParam("name") required this.name, | ||||
|     required this.name, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
| @@ -186,7 +185,7 @@ class PublisherProfileScreen extends HookConsumerWidget { | ||||
|                         ), | ||||
|                         onTap: () { | ||||
|                           Navigator.pop(context, true); | ||||
|                           context.router.pushPath('/account/${data.name}'); | ||||
|                           context.push('/account/${data.name}'); | ||||
|                         }, | ||||
|                       ), | ||||
|                       Expanded( | ||||
|   | ||||
| @@ -1,13 +1,12 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/realm.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/screens/realm/realms.dart'; | ||||
| import 'package:island/widgets/account/account_picker.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| @@ -27,10 +26,10 @@ Future<SnRealmMember?> realmIdentity(Ref ref, String realmSlug) async { | ||||
|   return SnRealmMember.fromJson(response.data); | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class RealmDetailScreen extends HookConsumerWidget { | ||||
|   final String slug; | ||||
|   const RealmDetailScreen({super.key, @PathParam("slug") required this.slug}); | ||||
|  | ||||
|   const RealmDetailScreen({super.key, required this.slug}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -129,7 +128,7 @@ class _RealmActionMenu extends HookConsumerWidget { | ||||
|             if (isModerator) | ||||
|               PopupMenuItem( | ||||
|                 onTap: () { | ||||
|                   context.router.replace(EditRealmRoute(slug: realmSlug)); | ||||
|                   context.pushReplacement('/realms/$realmSlug/edit'); | ||||
|                 }, | ||||
|                 child: Row( | ||||
|                   children: [ | ||||
| @@ -167,7 +166,7 @@ class _RealmActionMenu extends HookConsumerWidget { | ||||
|                                   client.delete('/realms/$realmSlug'); | ||||
|                                   ref.invalidate(realmsJoinedProvider); | ||||
|                                   if (context.mounted) { | ||||
|                                     context.router.maybePop(true); | ||||
|                                     context.pop(true); | ||||
|                                   } | ||||
|                                 } | ||||
|                               }); | ||||
| @@ -201,7 +200,7 @@ class _RealmActionMenu extends HookConsumerWidget { | ||||
|                                   ); | ||||
|                                   ref.invalidate(realmsJoinedProvider); | ||||
|                                   if (context.mounted) { | ||||
|                                     context.router.maybePop(true); | ||||
|                                     context.pop(true); | ||||
|                                   } | ||||
|                                 } | ||||
|                               }); | ||||
| @@ -239,7 +238,7 @@ class _RealmActionMenu extends HookConsumerWidget { | ||||
|                           client.delete('/realms/$realmSlug/members/me'); | ||||
|                           ref.invalidate(realmsJoinedProvider); | ||||
|                           if (context.mounted) { | ||||
|                             context.router.maybePop(true); | ||||
|                             context.pop(true); | ||||
|                           } | ||||
|                         } | ||||
|                       }); | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:croppy/croppy.dart' show CropAspectRatio; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| @@ -11,7 +11,6 @@ import 'package:island/models/file.dart'; | ||||
| import 'package:island/models/realm.dart'; | ||||
| import 'package:island/pods/config.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/services/file.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| @@ -33,7 +32,6 @@ Future<List<SnRealm>> realmsJoined(Ref ref) async { | ||||
|   return resp.data.map((e) => SnRealm.fromJson(e)).cast<SnRealm>().toList(); | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class RealmListScreen extends HookConsumerWidget { | ||||
|   const RealmListScreen({super.key}); | ||||
|  | ||||
| @@ -79,7 +77,7 @@ class RealmListScreen extends HookConsumerWidget { | ||||
|         heroTag: Key("realms-page-fab"), | ||||
|         child: const Icon(Symbols.add), | ||||
|         onPressed: () { | ||||
|           context.router.push(NewRealmRoute()).then((value) { | ||||
|           context.push('/realms/new').then((value) { | ||||
|             if (value != null) { | ||||
|               ref.invalidate(realmsJoinedProvider); | ||||
|             } | ||||
| @@ -106,9 +104,7 @@ class RealmListScreen extends HookConsumerWidget { | ||||
|                           title: Text(value[item].name), | ||||
|                           subtitle: Text(value[item].description), | ||||
|                           onTap: () { | ||||
|                             context.router.push( | ||||
|                               RealmDetailRoute(slug: value[item].slug), | ||||
|                             ); | ||||
|                             context.push('/realms/${value[item].slug}'); | ||||
|                           }, | ||||
|                           contentPadding: EdgeInsets.only( | ||||
|                             left: 16, | ||||
| @@ -143,7 +139,6 @@ Future<SnRealm?> realm(Ref ref, String? identifier) async { | ||||
|   return SnRealm.fromJson(resp.data); | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class NewRealmScreen extends StatelessWidget { | ||||
|   const NewRealmScreen({super.key}); | ||||
|  | ||||
| @@ -153,10 +148,9 @@ class NewRealmScreen extends StatelessWidget { | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class EditRealmScreen extends HookConsumerWidget { | ||||
|   final String? slug; | ||||
|   const EditRealmScreen({super.key, @PathParam('slug') this.slug}); | ||||
|   const EditRealmScreen({super.key, this.slug}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -262,7 +256,7 @@ class EditRealmScreen extends HookConsumerWidget { | ||||
|           options: Options(method: slug == null ? 'POST' : 'PATCH'), | ||||
|         ); | ||||
|         if (context.mounted) { | ||||
|           context.maybePop(SnRealm.fromJson(resp.data)); | ||||
|           context.pop(SnRealm.fromJson(resp.data)); | ||||
|         } | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:dropdown_button2/dropdown_button2.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_colorpicker/flutter_colorpicker.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| @@ -21,7 +21,6 @@ import 'package:path_provider/path_provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:island/pods/config.dart'; | ||||
|  | ||||
| @RoutePage() | ||||
| class SettingsScreen extends HookConsumerWidget { | ||||
|   const SettingsScreen({super.key}); | ||||
|  | ||||
| @@ -590,7 +589,7 @@ class SettingsScreen extends HookConsumerWidget { | ||||
|           if (isDesktop && | ||||
|               event is KeyDownEvent && | ||||
|               event.logicalKey == LogicalKeyboardKey.escape) { | ||||
|             context.router.pop(); | ||||
|             context.pop(); | ||||
|             return KeyEventResult.handled; | ||||
|           } | ||||
|           return KeyEventResult.ignored; | ||||
|   | ||||
| @@ -1,10 +1,9 @@ | ||||
| import 'dart:developer'; | ||||
| import 'dart:ui'; | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:island/screens/notification.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/widgets/navigation/conditional_bottom_nav.dart'; | ||||
| @@ -12,42 +11,22 @@ import 'package:material_symbols_icons/symbols.dart'; | ||||
|  | ||||
| final currentRouteProvider = StateProvider<String?>((ref) => null); | ||||
|  | ||||
| class TabNavigationObserver extends AutoRouterObserver { | ||||
|   Function(String?) onChange; | ||||
|   TabNavigationObserver({required this.onChange}); | ||||
|  | ||||
|   @override | ||||
|   void didPush(Route route, Route? previousRoute) { | ||||
|     log('pushed ${previousRoute?.settings.name} -> ${route.settings.name}'); | ||||
|     if (route is DialogRoute) return; | ||||
|     final name = route.settings.name; | ||||
|     if (name == null) return; | ||||
|     if (name.contains('Shell')) return; | ||||
|     Future(() { | ||||
|       onChange(name); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void didPop(Route route, Route? previousRoute) { | ||||
|     log('popped ${route.settings.name} -> ${previousRoute?.settings.name}'); | ||||
|     if (previousRoute is DialogRoute) return; | ||||
|     final name = previousRoute?.settings.name; | ||||
|     if (name == null) return; | ||||
|     if (name.contains('Shell')) return; | ||||
|     Future(() { | ||||
|       onChange(name); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class TabsScreen extends HookConsumerWidget { | ||||
|   const TabsScreen({super.key}); | ||||
|   final Widget? child; | ||||
|   const TabsScreen({super.key, this.child}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final useHorizontalLayout = isWideScreen(context); | ||||
|     // final useHorizontalLayout = isWideScreen(context); | ||||
|     final currentLocation = GoRouterState.of(context).uri.toString(); | ||||
|      | ||||
|     // Update the current route provider whenever the location changes | ||||
|     useEffect(() { | ||||
|       WidgetsBinding.instance.addPostFrameCallback((_) { | ||||
|         ref.read(currentRouteProvider.notifier).state = currentLocation; | ||||
|       }); | ||||
|       return null; | ||||
|     }, [currentLocation]); | ||||
|  | ||||
|     final notificationUnreadCount = ref.watch( | ||||
|       notificationUnreadCountNotifierProvider, | ||||
| @@ -73,85 +52,89 @@ class TabsScreen extends HookConsumerWidget { | ||||
|       ), | ||||
|     ]; | ||||
|  | ||||
|     final routes = <PageRouteInfo>[ | ||||
|       ExploreShellRoute(), | ||||
|       ChatShellRoute(), | ||||
|       RealmListRoute(), | ||||
|       AccountShellRoute(), | ||||
|     final routes = [ | ||||
|       '/', | ||||
|       '/chat', | ||||
|       '/realms', | ||||
|       '/account', | ||||
|     ]; | ||||
|  | ||||
|     return AutoTabsRouter.tabBar( | ||||
|       routes: routes, | ||||
|       scrollDirection: useHorizontalLayout ? Axis.vertical : Axis.horizontal, | ||||
|       physics: const NeverScrollableScrollPhysics(), | ||||
|       builder: (context, child, _) { | ||||
|         final tabsRouter = AutoTabsRouter.of(context); | ||||
|     int getCurrentIndex() { | ||||
|       if (currentLocation.startsWith('/chat')) return 1; | ||||
|       if (currentLocation.startsWith('/realms')) return 2; | ||||
|       if (currentLocation.startsWith('/account')) return 3; | ||||
|       return 0; // Default to explore | ||||
|     } | ||||
|  | ||||
|         if (isWideScreen(context)) { | ||||
|           return Row( | ||||
|             children: [ | ||||
|               NavigationRail( | ||||
|                 destinations: | ||||
|                     destinations | ||||
|                         .map( | ||||
|                           (e) => NavigationRailDestination( | ||||
|                             icon: e.icon, | ||||
|                             label: Text(e.label), | ||||
|                           ), | ||||
|                         ) | ||||
|                         .toList(), | ||||
|                 selectedIndex: tabsRouter.activeIndex, | ||||
|                 onDestinationSelected: tabsRouter.setActiveIndex, | ||||
|               ), | ||||
|               const VerticalDivider(width: 1), | ||||
|               Expanded(child: child), | ||||
|             ], | ||||
|           ); | ||||
|         } | ||||
|     void onDestinationSelected(int index) { | ||||
|       context.go(routes[index]); | ||||
|     } | ||||
|  | ||||
|         return Stack( | ||||
|           children: [ | ||||
|             Positioned.fill(child: child), | ||||
|             Positioned( | ||||
|               left: 0, | ||||
|               right: 0, | ||||
|               bottom: 0, | ||||
|               child: ConditionalBottomNav( | ||||
|                 child: ClipRRect( | ||||
|                   child: BackdropFilter( | ||||
|                     filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), | ||||
|                     child: Container( | ||||
|                       decoration: BoxDecoration( | ||||
|                         color: Theme.of( | ||||
|                           context, | ||||
|                         ).colorScheme.surface.withOpacity(0.8), | ||||
|     final currentIndex = getCurrentIndex(); | ||||
|  | ||||
|     if (isWideScreen(context)) { | ||||
|       return Row( | ||||
|         children: [ | ||||
|           NavigationRail( | ||||
|             destinations: | ||||
|                 destinations | ||||
|                     .map( | ||||
|                       (e) => NavigationRailDestination( | ||||
|                         icon: e.icon, | ||||
|                         label: Text(e.label), | ||||
|                       ), | ||||
|                       child: MediaQuery.removePadding( | ||||
|                         context: context, | ||||
|                         removeTop: true, | ||||
|                         child: NavigationBar( | ||||
|                           backgroundColor: Colors.transparent, | ||||
|                           shadowColor: Colors.transparent, | ||||
|                           overlayColor: const WidgetStatePropertyAll( | ||||
|                             Colors.transparent, | ||||
|                           ), | ||||
|                           surfaceTintColor: Colors.transparent, | ||||
|                           height: 56, | ||||
|                           labelBehavior: | ||||
|                               NavigationDestinationLabelBehavior.alwaysHide, | ||||
|                           selectedIndex: tabsRouter.activeIndex, | ||||
|                           onDestinationSelected: tabsRouter.setActiveIndex, | ||||
|                           destinations: destinations, | ||||
|                         ), | ||||
|                     ) | ||||
|                     .toList(), | ||||
|             selectedIndex: currentIndex, | ||||
|             onDestinationSelected: onDestinationSelected, | ||||
|           ), | ||||
|           const VerticalDivider(width: 1), | ||||
|           Expanded(child: child ?? const SizedBox.shrink()), | ||||
|         ], | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return Stack( | ||||
|       children: [ | ||||
|         Positioned.fill(child: child ?? const SizedBox.shrink()), | ||||
|         Positioned( | ||||
|           left: 0, | ||||
|           right: 0, | ||||
|           bottom: 0, | ||||
|           child: ConditionalBottomNav( | ||||
|             child: ClipRRect( | ||||
|               child: BackdropFilter( | ||||
|                 filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), | ||||
|                 child: Container( | ||||
|                   decoration: BoxDecoration( | ||||
|                     color: Theme.of( | ||||
|                       context, | ||||
|                     ).colorScheme.surface.withOpacity(0.8), | ||||
|                   ), | ||||
|                   child: MediaQuery.removePadding( | ||||
|                     context: context, | ||||
|                     removeTop: true, | ||||
|                     child: NavigationBar( | ||||
|                       backgroundColor: Colors.transparent, | ||||
|                       shadowColor: Colors.transparent, | ||||
|                       overlayColor: const WidgetStatePropertyAll( | ||||
|                         Colors.transparent, | ||||
|                       ), | ||||
|                       surfaceTintColor: Colors.transparent, | ||||
|                       height: 56, | ||||
|                       labelBehavior: | ||||
|                           NavigationDestinationLabelBehavior.alwaysHide, | ||||
|                       selectedIndex: currentIndex, | ||||
|                       onDestinationSelected: onDestinationSelected, | ||||
|                       destinations: destinations, | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ); | ||||
|       }, | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import 'package:auto_route/annotations.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| @@ -72,7 +71,6 @@ class TransactionListNotifier extends _$TransactionListNotifier | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| class WalletScreen extends HookConsumerWidget { | ||||
|   const WalletScreen({super.key}); | ||||
|  | ||||
|   | ||||
| @@ -185,7 +185,6 @@ Completer<SnCloudFile?> _processUpload( | ||||
|         onProgress: (double progress, Duration estimate) { | ||||
|           onProgress?.call(progress, estimate); | ||||
|         }, | ||||
|         measureUploadSpeed: true, | ||||
|       ) | ||||
|       .catchError(completer.completeError); | ||||
|  | ||||
|   | ||||
| @@ -7,7 +7,9 @@ import 'package:firebase_messaging/firebase_messaging.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:island/main.dart'; | ||||
| import 'package:island/route.dart'; | ||||
| import 'package:island/models/user.dart'; | ||||
| import 'package:island/pods/websocket.dart'; | ||||
| import 'package:island/widgets/app_notification.dart'; | ||||
| @@ -30,7 +32,7 @@ StreamSubscription<WebSocketPacket> setupNotificationListener( | ||||
|             var uri = notification.meta['action_uri'] as String; | ||||
|             if (uri.startsWith('/')) { | ||||
|               // In-app routes | ||||
|               appRouter.pushPath(notification.meta['action_uri']); | ||||
|               rootNavigatorKey.currentContext?.push(notification.meta['action_uri']); | ||||
|             } else { | ||||
|               // External URLs | ||||
|               launchUrlString(uri); | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import 'dart:math' as math; | ||||
|  | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter_popup_card/flutter_popup_card.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| @@ -105,7 +105,7 @@ class AccountProfileCard extends HookConsumerWidget { | ||||
|                       FilledButton.tonalIcon( | ||||
|                         onPressed: () { | ||||
|                           Navigator.pop(context); | ||||
|                           context.router.pushPath('/account/${data.name}'); | ||||
|                           context.push('/account/${data.name}'); | ||||
|                         }, | ||||
|                         icon: const Icon(Symbols.launch), | ||||
|                         label: Text('accountProfileView').tr(), | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:fl_chart/fl_chart.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/activity.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| @@ -66,7 +66,7 @@ class FortuneGraphWidget extends HookConsumerWidget { | ||||
|                 padding: EdgeInsets.zero, | ||||
|                 constraints: const BoxConstraints(), | ||||
|                 onPressed: () { | ||||
|                   context.router.pushNamed( | ||||
|                   context.pushNamed( | ||||
|                     '/account/$eventCalanderUser/calendar', | ||||
|                   ); | ||||
|                 }, | ||||
|   | ||||
| @@ -1,16 +1,14 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:bitsdojo_window/bitsdojo_window.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/pods/config.dart'; | ||||
| import 'package:island/pods/userinfo.dart'; | ||||
| import 'package:island/pods/websocket.dart'; | ||||
| import 'package:island/route.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:material_symbols_icons/material_symbols_icons.dart'; | ||||
| import 'package:path_provider/path_provider.dart'; | ||||
| @@ -18,8 +16,7 @@ import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| class WindowScaffold extends HookConsumerWidget { | ||||
|   final Widget child; | ||||
|   final AppRouter router; | ||||
|   const WindowScaffold({super.key, required this.child, required this.router}); | ||||
|   const WindowScaffold({super.key, required this.child}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -238,7 +235,7 @@ class PageBackButton extends StatelessWidget { | ||||
|     return IconButton( | ||||
|       onPressed: () { | ||||
|         onWillPop?.call(); | ||||
|         context.router.maybePop(); | ||||
|         context.pop(); | ||||
|       }, | ||||
|       icon: Icon( | ||||
|         color: color, | ||||
|   | ||||
| @@ -1,15 +1,13 @@ | ||||
| import 'dart:async'; | ||||
|  | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/services/notify.dart'; | ||||
| import 'package:island/services/sharing_intent.dart'; | ||||
|  | ||||
| @RoutePage() | ||||
| class AppWrapper extends HookConsumerWidget { | ||||
|   const AppWrapper({super.key}); | ||||
|   final Widget child; | ||||
|   const AppWrapper({super.key, required this.child}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -26,6 +24,6 @@ class AppWrapper extends HookConsumerWidget { | ||||
|       }; | ||||
|     }, const []); | ||||
|  | ||||
|     return AutoRouter(); | ||||
|     return child; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,11 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/chat.dart'; | ||||
| import 'package:island/pods/call.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
|  | ||||
| @@ -45,7 +44,7 @@ class AudioCallButton extends HookConsumerWidget { | ||||
|       try { | ||||
|         await apiClient.post('/chat/realtime/$roomId'); | ||||
|         if (context.mounted) { | ||||
|           context.router.push(CallRoute(roomId: roomId)); | ||||
|           context.push('/chat/call/roomId'); | ||||
|         } | ||||
|       } catch (e) { | ||||
|         showErrorAlert(e); | ||||
| @@ -97,7 +96,7 @@ class AudioCallButton extends HookConsumerWidget { | ||||
|         tooltip: 'Join Ongoing Call', | ||||
|         onPressed: () { | ||||
|           if (context.mounted) { | ||||
|             context.router.push(CallRoute(roomId: roomId)); | ||||
|             context.push('/chat/call/roomId'); | ||||
|           } | ||||
|         }, | ||||
|       ); | ||||
|   | ||||
| @@ -1,10 +1,9 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/pods/call.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/chat/call_participant_tile.dart'; | ||||
| import 'package:island/widgets/content/sheet.dart'; | ||||
| @@ -361,7 +360,7 @@ class CallOverlayBar extends HookConsumerWidget { | ||||
|         ).padding(all: 16), | ||||
|       ), | ||||
|       onTap: () { | ||||
|         context.router.push(CallRoute(roomId: callNotifier.roomId!)); | ||||
|         context.push('/chat/call/callNotifier.roomId!'); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -1,15 +1,14 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/activity.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/pods/userinfo.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/screens/auth/captcha.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/content/cloud_files.dart'; | ||||
| @@ -137,7 +136,7 @@ class CheckInWidget extends HookConsumerWidget { | ||||
|               if (todayResult.valueOrNull == null) { | ||||
|                 checkIn(); | ||||
|               } else { | ||||
|                 context.router.push(EventCalanderRoute(name: 'me')); | ||||
|                 context.push('/account/me/calendar'); | ||||
|               } | ||||
|             }, | ||||
|             icon: AnimatedSwitcher( | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_highlight/themes/a11y-dark.dart'; | ||||
| import 'package:flutter_highlight/themes/a11y-light.dart'; | ||||
| @@ -74,7 +74,7 @@ class MarkdownTextContent extends HookConsumerWidget { | ||||
|               final url = Uri.tryParse(href); | ||||
|               if (url != null) { | ||||
|                 if (url.scheme == 'solian') { | ||||
|                   context.router.pushPath( | ||||
|                   context.push( | ||||
|                     ['', url.host, ...url.pathSegments].join('/'), | ||||
|                   ); | ||||
|                   return; | ||||
|   | ||||
| @@ -1,26 +1,26 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/screens/tabs.dart'; | ||||
|  | ||||
| class ConditionalBottomNav extends HookConsumerWidget { | ||||
|   final Widget child; | ||||
|  | ||||
|   const ConditionalBottomNav({super.key, required this.child}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final currentRouteName = ref.watch(currentRouteProvider); | ||||
|     final currentLocation = GoRouterState.of(context).uri.toString(); | ||||
|  | ||||
|     const mainTabRoutes = { | ||||
|       ExploreRoute.name, | ||||
|       ChatListRoute.name, | ||||
|       RealmListRoute.name, | ||||
|       AccountRoute.name, | ||||
|     }; | ||||
|     // Force rebuild when route changes | ||||
|     useEffect(() { | ||||
|       // This effect will run whenever currentLocation changes | ||||
|       return null; | ||||
|     }, [currentLocation]); | ||||
|  | ||||
|     debugPrint(currentRouteName); | ||||
|     final shouldShowBottomNav = mainTabRoutes.contains(currentRouteName); | ||||
|     // Use the same route logic as TabsScreen for consistency | ||||
|     const mainTabRoutes = ['/', '/chat', '/realms', '/account']; | ||||
|  | ||||
|     final shouldShowBottomNav = mainTabRoutes.contains(currentLocation); | ||||
|  | ||||
|     return shouldShowBottomNav ? child : const SizedBox.shrink(); | ||||
|   } | ||||
|   | ||||
| @@ -4,12 +4,107 @@ import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:island/widgets/content/sheet.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:textfield_tags/textfield_tags.dart'; | ||||
|  | ||||
| /// A reusable widget for tag input fields with chip display | ||||
| class ChipTagInputField extends StatelessWidget { | ||||
|   final InputFieldValues inputFieldValues; | ||||
|   final String labelText; | ||||
|   final String hintText; | ||||
|  | ||||
|   const ChipTagInputField({ | ||||
|     super.key, | ||||
|     required this.inputFieldValues, | ||||
|     required this.labelText, | ||||
|     required this.hintText, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return TextField( | ||||
|       controller: inputFieldValues.textEditingController, | ||||
|       focusNode: inputFieldValues.focusNode, | ||||
|       decoration: InputDecoration( | ||||
|         label: Text(labelText).tr(), | ||||
|         border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), | ||||
|         contentPadding: const EdgeInsets.all(16), | ||||
|         hintText: inputFieldValues.tags.isNotEmpty ? '' : hintText.tr(), | ||||
|         errorText: inputFieldValues.error, | ||||
|         prefixIconConstraints: BoxConstraints( | ||||
|           maxWidth: MediaQuery.of(context).size.width * 0.8, | ||||
|         ), | ||||
|         prefixIcon: | ||||
|             inputFieldValues.tags.isNotEmpty | ||||
|                 ? SingleChildScrollView( | ||||
|                   controller: inputFieldValues.tagScrollController, | ||||
|                   scrollDirection: Axis.vertical, | ||||
|                   child: Padding( | ||||
|                     padding: const EdgeInsets.only(top: 8, bottom: 8, left: 8), | ||||
|                     child: Wrap( | ||||
|                       runSpacing: 4.0, | ||||
|                       spacing: 4.0, | ||||
|                       children: | ||||
|                           inputFieldValues.tags.map<Widget>((dynamic tag) { | ||||
|                             return Container( | ||||
|                               decoration: BoxDecoration( | ||||
|                                 borderRadius: BorderRadius.all( | ||||
|                                   Radius.circular(20.0), | ||||
|                                 ), | ||||
|                                 color: Theme.of(context).colorScheme.primary, | ||||
|                               ), | ||||
|                               margin: const EdgeInsets.only(left: 5), | ||||
|                               padding: const EdgeInsets.symmetric( | ||||
|                                 horizontal: 10.0, | ||||
|                                 vertical: 5.0, | ||||
|                               ), | ||||
|                               child: Row( | ||||
|                                 mainAxisAlignment: MainAxisAlignment.start, | ||||
|                                 mainAxisSize: MainAxisSize.min, | ||||
|                                 children: [ | ||||
|                                   InkWell( | ||||
|                                     child: Text( | ||||
|                                       '#$tag', | ||||
|                                       style: TextStyle( | ||||
|                                         color: | ||||
|                                             Theme.of( | ||||
|                                               context, | ||||
|                                             ).colorScheme.onPrimary, | ||||
|                                       ), | ||||
|                                     ), | ||||
|                                   ), | ||||
|                                   const Gap(4), | ||||
|                                   InkWell( | ||||
|                                     child: const Icon( | ||||
|                                       Icons.cancel, | ||||
|                                       size: 14.0, | ||||
|                                       color: Color.fromARGB(255, 233, 233, 233), | ||||
|                                     ), | ||||
|                                     onTap: () { | ||||
|                                       inputFieldValues.onTagRemoved(tag); | ||||
|                                     }, | ||||
|                                   ), | ||||
|                                 ], | ||||
|                               ), | ||||
|                             ); | ||||
|                           }).toList(), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ) | ||||
|                 : null, | ||||
|       ), | ||||
|       onChanged: inputFieldValues.onTagChanged, | ||||
|       onSubmitted: inputFieldValues.onTagSubmitted, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class ComposeSettingsSheet extends HookWidget { | ||||
|   final TextEditingController titleController; | ||||
|   final TextEditingController descriptionController; | ||||
|   final ValueNotifier<int> visibility; | ||||
|   final VoidCallback? onVisibilityChanged; | ||||
|   final StringTagController tagsController; | ||||
|   final StringTagController categoriesController; | ||||
|  | ||||
|   const ComposeSettingsSheet({ | ||||
|     super.key, | ||||
| @@ -17,6 +112,8 @@ class ComposeSettingsSheet extends HookWidget { | ||||
|     required this.descriptionController, | ||||
|     required this.visibility, | ||||
|     this.onVisibilityChanged, | ||||
|     required this.tagsController, | ||||
|     required this.categoriesController, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
| @@ -117,6 +214,7 @@ class ComposeSettingsSheet extends HookWidget { | ||||
|         padding: const EdgeInsets.all(16), | ||||
|         child: Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           spacing: 16, | ||||
|           children: [ | ||||
|             // Title field | ||||
|             TextField( | ||||
| @@ -133,7 +231,6 @@ class ComposeSettingsSheet extends HookWidget { | ||||
|               onTapOutside: | ||||
|                   (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|             ), | ||||
|             const Gap(16), | ||||
|  | ||||
|             // Description field | ||||
|             TextField( | ||||
| @@ -151,7 +248,45 @@ class ComposeSettingsSheet extends HookWidget { | ||||
|               onTapOutside: | ||||
|                   (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|             ), | ||||
|             const Gap(16), | ||||
|  | ||||
|             // Tags field | ||||
|             TextFieldTags( | ||||
|               textfieldTagsController: tagsController, | ||||
|               textSeparators: const [' ', ','], | ||||
|               letterCase: LetterCase.normal, | ||||
|               validator: (String tag) { | ||||
|                 if (tag.isEmpty) { | ||||
|                   return 'No, cannot be empty'; | ||||
|                 } | ||||
|                 return null; | ||||
|               }, | ||||
|               inputFieldBuilder: (context, inputFieldValues) { | ||||
|                 return ChipTagInputField( | ||||
|                   inputFieldValues: inputFieldValues, | ||||
|                   labelText: 'tags', | ||||
|                   hintText: 'tagsHint', | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|  | ||||
|             // Categories field | ||||
|             TextFieldTags( | ||||
|               textfieldTagsController: categoriesController, | ||||
|               textSeparators: const [' ', ','], | ||||
|               letterCase: LetterCase.small, | ||||
|               validator: (String tag) { | ||||
|                 if (tag.isEmpty) return 'No, cannot be empty'; | ||||
|                 if (tag.contains(' ')) return 'Tags should be URL-safe'; | ||||
|                 return null; | ||||
|               }, | ||||
|               inputFieldBuilder: (context, inputFieldValues) { | ||||
|                 return ChipTagInputField( | ||||
|                   inputFieldValues: inputFieldValues, | ||||
|                   labelText: 'categories', | ||||
|                   hintText: 'categoriesHint', | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|  | ||||
|             // Visibility setting | ||||
|             Container( | ||||
|   | ||||
| @@ -16,34 +16,42 @@ import 'package:pasteboard/pasteboard.dart'; | ||||
| import 'dart:async'; | ||||
| import 'dart:developer'; | ||||
|  | ||||
| import 'package:textfield_tags/textfield_tags.dart'; | ||||
|  | ||||
| class ComposeState { | ||||
|   final ValueNotifier<List<UniversalFile>> attachments; | ||||
|   final TextEditingController titleController; | ||||
|   final TextEditingController descriptionController; | ||||
|   final TextEditingController contentController; | ||||
|   final ValueNotifier<int> visibility; | ||||
|   final ValueNotifier<bool> submitting; | ||||
|   final ValueNotifier<List<UniversalFile>> attachments; | ||||
|   final ValueNotifier<Map<int, double>> attachmentProgress; | ||||
|   final ValueNotifier<SnPublisher?> currentPublisher; | ||||
|   final ValueNotifier<bool> submitting; | ||||
|   StringTagController tagsController; | ||||
|   StringTagController categoriesController; | ||||
|   final String draftId; | ||||
|   int postType; | ||||
|   Timer? _autoSaveTimer; | ||||
|  | ||||
|   ComposeState({ | ||||
|     required this.attachments, | ||||
|     required this.titleController, | ||||
|     required this.descriptionController, | ||||
|     required this.contentController, | ||||
|     required this.visibility, | ||||
|     required this.submitting, | ||||
|     required this.attachments, | ||||
|     required this.attachmentProgress, | ||||
|     required this.currentPublisher, | ||||
|     required this.submitting, | ||||
|     required this.tagsController, | ||||
|     required this.categoriesController, | ||||
|     required this.draftId, | ||||
|     this.postType = 0, | ||||
|   }); | ||||
|  | ||||
|   void startAutoSave(WidgetRef ref, {int postType = 0}) { | ||||
|   void startAutoSave(WidgetRef ref) { | ||||
|     _autoSaveTimer?.cancel(); | ||||
|     _autoSaveTimer = Timer.periodic(const Duration(seconds: 3), (_) { | ||||
|       ComposeLogic.saveDraftWithoutUpload(ref, this, postType: postType); | ||||
|       ComposeLogic.saveDraftWithoutUpload(ref, this); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
| @@ -59,9 +67,15 @@ class ComposeLogic { | ||||
|     SnPost? forwardedPost, | ||||
|     SnPost? repliedPost, | ||||
|     String? draftId, | ||||
|     int postType = 0, | ||||
|   }) { | ||||
|     final id = draftId ?? DateTime.now().millisecondsSinceEpoch.toString(); | ||||
|  | ||||
|     final tagsController = StringTagController(); | ||||
|     final categoriesController = StringTagController(); | ||||
|     originalPost?.tags.forEach((x) => tagsController.addTag(x.slug)); | ||||
|     originalPost?.categories.forEach( | ||||
|       (x) => categoriesController.addTag(x.slug), | ||||
|     ); | ||||
|     return ComposeState( | ||||
|       attachments: ValueNotifier<List<UniversalFile>>( | ||||
|         originalPost?.attachments | ||||
| @@ -86,17 +100,32 @@ class ComposeLogic { | ||||
|       contentController: TextEditingController( | ||||
|         text: | ||||
|             originalPost?.content ?? | ||||
|             (forwardedPost != null ? '> ${forwardedPost.content}\n\n' : null), | ||||
|             (forwardedPost != null | ||||
|                 ? '''> ${forwardedPost.content} | ||||
|  | ||||
| ''' | ||||
|                 : null), | ||||
|       ), | ||||
|       visibility: ValueNotifier<int>(originalPost?.visibility ?? 0), | ||||
|       submitting: ValueNotifier<bool>(false), | ||||
|       attachmentProgress: ValueNotifier<Map<int, double>>({}), | ||||
|       currentPublisher: ValueNotifier<SnPublisher?>(null), | ||||
|       tagsController: tagsController, | ||||
|       categoriesController: categoriesController, | ||||
|       draftId: id, | ||||
|       postType: postType, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   static ComposeState createStateFromDraft(SnPost draft) { | ||||
|   static ComposeState createStateFromDraft(SnPost draft, {int postType = 0}) { | ||||
|     final tagsController = StringTagController(); | ||||
|     final categoriesController = StringTagController(); | ||||
|     for (var x in draft.tags) { | ||||
|       tagsController.addTag(x.slug); | ||||
|     } | ||||
|     for (var x in draft.categories) { | ||||
|       categoriesController.addTag(x.slug); | ||||
|     } | ||||
|     return ComposeState( | ||||
|       attachments: ValueNotifier<List<UniversalFile>>( | ||||
|         draft.attachments.map((e) => UniversalFile.fromAttachment(e)).toList(), | ||||
| @@ -108,15 +137,14 @@ class ComposeLogic { | ||||
|       submitting: ValueNotifier<bool>(false), | ||||
|       attachmentProgress: ValueNotifier<Map<int, double>>({}), | ||||
|       currentPublisher: ValueNotifier<SnPublisher?>(null), | ||||
|       tagsController: tagsController, | ||||
|       categoriesController: categoriesController, | ||||
|       draftId: draft.id, | ||||
|       postType: postType, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   static Future<void> saveDraft( | ||||
|     WidgetRef ref, | ||||
|     ComposeState state, { | ||||
|     int postType = 0, | ||||
|   }) async { | ||||
|   static Future<void> saveDraft(WidgetRef ref, ComposeState state) async { | ||||
|     final hasContent = | ||||
|         state.titleController.text.trim().isNotEmpty || | ||||
|         state.descriptionController.text.trim().isNotEmpty || | ||||
| @@ -148,7 +176,7 @@ class ComposeLogic { | ||||
|                   baseUrl: baseUrl, | ||||
|                   filename: | ||||
|                       attachment.data.name ?? | ||||
|                       (postType == 1 ? 'Article media' : 'Post media'), | ||||
|                       (state.postType == 1 ? 'Article media' : 'Post media'), | ||||
|                   mimetype: | ||||
|                       attachment.data.mimeType ?? | ||||
|                       ComposeLogic.getMimeTypeFromFileType(attachment.type), | ||||
| @@ -175,7 +203,7 @@ class ComposeLogic { | ||||
|         publishedAt: DateTime.now(), | ||||
|         visibility: state.visibility.value, | ||||
|         content: state.contentController.text, | ||||
|         type: postType, | ||||
|         type: state.postType, | ||||
|         meta: null, | ||||
|         viewsUnique: 0, | ||||
|         viewsTotal: 0, | ||||
| @@ -225,9 +253,8 @@ class ComposeLogic { | ||||
|  | ||||
|   static Future<void> saveDraftWithoutUpload( | ||||
|     WidgetRef ref, | ||||
|     ComposeState state, { | ||||
|     int postType = 0, | ||||
|   }) async { | ||||
|     ComposeState state, | ||||
|   ) async { | ||||
|     final hasContent = | ||||
|         state.titleController.text.trim().isNotEmpty || | ||||
|         state.descriptionController.text.trim().isNotEmpty || | ||||
| @@ -252,7 +279,7 @@ class ComposeLogic { | ||||
|         publishedAt: DateTime.now(), | ||||
|         visibility: state.visibility.value, | ||||
|         content: state.contentController.text, | ||||
|         type: postType, | ||||
|         type: state.postType, | ||||
|         meta: null, | ||||
|         viewsUnique: 0, | ||||
|         viewsTotal: 0, | ||||
| @@ -306,54 +333,7 @@ class ComposeLogic { | ||||
|     BuildContext context, | ||||
|   ) async { | ||||
|     try { | ||||
|       final draft = SnPost( | ||||
|         id: state.draftId, | ||||
|         title: state.titleController.text, | ||||
|         description: state.descriptionController.text, | ||||
|         language: null, | ||||
|         editedAt: null, | ||||
|         publishedAt: DateTime.now(), | ||||
|         visibility: state.visibility.value, | ||||
|         content: state.contentController.text, | ||||
|         type: 0, | ||||
|         meta: null, | ||||
|         viewsUnique: 0, | ||||
|         viewsTotal: 0, | ||||
|         upvotes: 0, | ||||
|         downvotes: 0, | ||||
|         repliesCount: 0, | ||||
|         threadedPostId: null, | ||||
|         threadedPost: null, | ||||
|         repliedPostId: null, | ||||
|         repliedPost: null, | ||||
|         forwardedPostId: null, | ||||
|         forwardedPost: null, | ||||
|         attachments: [], // TODO: Handle attachments | ||||
|         publisher: SnPublisher( | ||||
|           id: '', | ||||
|           type: 0, | ||||
|           name: '', | ||||
|           nick: '', | ||||
|           picture: null, | ||||
|           background: null, | ||||
|           account: null, | ||||
|           accountId: null, | ||||
|           createdAt: DateTime.now(), | ||||
|           updatedAt: DateTime.now(), | ||||
|           deletedAt: null, | ||||
|           realmId: null, | ||||
|           verification: null, | ||||
|         ), | ||||
|         reactions: [], | ||||
|         tags: [], | ||||
|         categories: [], | ||||
|         collections: [], | ||||
|         createdAt: DateTime.now(), | ||||
|         updatedAt: DateTime.now(), | ||||
|         deletedAt: null, | ||||
|       ); | ||||
|  | ||||
|       await ref.read(composeStorageNotifierProvider.notifier).saveDraft(draft); | ||||
|       await saveDraft(ref, state); | ||||
|  | ||||
|       if (context.mounted) { | ||||
|         showSnackBar('draftSaved'.tr()); | ||||
| @@ -508,7 +488,6 @@ class ComposeLogic { | ||||
|     SnPost? originalPost, | ||||
|     SnPost? repliedPost, | ||||
|     SnPost? forwardedPost, | ||||
|     int? postType, // 0 for regular post, 1 for article | ||||
|   }) async { | ||||
|     if (state.submitting.value) return; | ||||
|  | ||||
| @@ -554,9 +533,11 @@ class ComposeLogic { | ||||
|                 .where((e) => e.isOnCloud) | ||||
|                 .map((e) => e.data.id) | ||||
|                 .toList(), | ||||
|         if (postType != null) 'type': postType, | ||||
|         'type': state.postType, | ||||
|         if (repliedPost != null) 'replied_post_id': repliedPost.id, | ||||
|         if (forwardedPost != null) 'forwarded_post_id': forwardedPost.id, | ||||
|         'tags': state.tagsController.getTags, | ||||
|         'categories': state.categoriesController.getTags, | ||||
|       }; | ||||
|  | ||||
|       // Send request | ||||
| @@ -570,7 +551,7 @@ class ComposeLogic { | ||||
|       ); | ||||
|  | ||||
|       // Delete draft after successful submission | ||||
|       if (postType == 1) { | ||||
|       if (state.postType == 1) { | ||||
|         // Delete article draft | ||||
|         await ref | ||||
|             .read(composeStorageNotifierProvider.notifier) | ||||
| @@ -613,7 +594,6 @@ class ComposeLogic { | ||||
|     SnPost? originalPost, | ||||
|     SnPost? repliedPost, | ||||
|     SnPost? forwardedPost, | ||||
|     int? postType, | ||||
|   }) { | ||||
|     if (event is! RawKeyDownEvent) return; | ||||
|  | ||||
| @@ -634,7 +614,6 @@ class ComposeLogic { | ||||
|         originalPost: originalPost, | ||||
|         repliedPost: repliedPost, | ||||
|         forwardedPost: forwardedPost, | ||||
|         postType: postType, | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| @@ -649,5 +628,7 @@ class ComposeLogic { | ||||
|     state.submitting.dispose(); | ||||
|     state.attachmentProgress.dispose(); | ||||
|     state.currentPublisher.dispose(); | ||||
|     state.tagsController.dispose(); | ||||
|     state.categoriesController.dispose(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/services/compose_storage_db.dart'; | ||||
| import 'package:island/widgets/content/sheet.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
|  | ||||
| class DraftManagerSheet extends HookConsumerWidget { | ||||
| @@ -43,9 +44,9 @@ class DraftManagerSheet extends HookConsumerWidget { | ||||
|       ], | ||||
|     ); | ||||
|  | ||||
|     return Scaffold( | ||||
|       appBar: AppBar(title: Text('drafts'.tr())), | ||||
|       body: | ||||
|     return SheetScaffold( | ||||
|       titleText: 'drafts'.tr(), | ||||
|       child: | ||||
|           isLoading.value | ||||
|               ? const Center(child: CircularProgressIndicator()) | ||||
|               : Column( | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| @@ -11,7 +11,6 @@ import 'package:island/models/post.dart'; | ||||
| import 'package:island/pods/config.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/pods/userinfo.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/services/time.dart'; | ||||
| import 'package:island/widgets/account/account_name.dart'; | ||||
| @@ -72,7 +71,7 @@ class PostItem extends HookConsumerWidget { | ||||
|                 title: 'edit'.tr(), | ||||
|                 image: MenuImage.icon(Symbols.edit), | ||||
|                 callback: () { | ||||
|                   context.router.push(PostEditRoute(id: item.id)).then((value) { | ||||
|                   context.push('/posts/item.id/edit').then((value) { | ||||
|                     if (value != null) { | ||||
|                       onRefresh?.call(); | ||||
|                     } | ||||
| @@ -117,14 +116,14 @@ class PostItem extends HookConsumerWidget { | ||||
|               title: 'reply'.tr(), | ||||
|               image: MenuImage.icon(Symbols.reply), | ||||
|               callback: () { | ||||
|                 context.router.push(PostComposeRoute(repliedPost: item)); | ||||
|                 context.push('/posts/compose', extra: {'repliedPost': item}); | ||||
|               }, | ||||
|             ), | ||||
|             MenuAction( | ||||
|               title: 'forward'.tr(), | ||||
|               image: MenuImage.icon(Symbols.forward), | ||||
|               callback: () { | ||||
|                 context.router.push(PostComposeRoute(forwardedPost: item)); | ||||
|                 context.push('/posts/compose', extra: {'forwardedPost': item}); | ||||
|               }, | ||||
|             ), | ||||
|             MenuSeparator(), | ||||
| @@ -168,9 +167,7 @@ class PostItem extends HookConsumerWidget { | ||||
|                   GestureDetector( | ||||
|                     child: ProfilePictureWidget(file: item.publisher.picture), | ||||
|                     onTap: () { | ||||
|                       context.router.push( | ||||
|                         PublisherProfileRoute(name: item.publisher.name), | ||||
|                       ); | ||||
|                       context.push('/publishers/${item.publisher.name}'); | ||||
|                     }, | ||||
|                   ), | ||||
|                   Expanded( | ||||
| @@ -245,6 +242,47 @@ class PostItem extends HookConsumerWidget { | ||||
|                                       ? EdgeInsets.only(bottom: 8) | ||||
|                                       : null, | ||||
|                             ), | ||||
|                           // Render tags and categories if they exist | ||||
|                           if (item.tags.isNotEmpty || item.categories.isNotEmpty) | ||||
|                             Column( | ||||
|                               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                               children: [ | ||||
|                                 if (item.tags.isNotEmpty) | ||||
|                                   Wrap( | ||||
|                                     children: [ | ||||
|                                       for (final tag in item.tags) | ||||
|                                         InkWell( | ||||
|                                           child: Row( | ||||
|                                             spacing: 4, | ||||
|                                             children: [ | ||||
|                                               const Icon(Symbols.label, size: 13), | ||||
|                                               Text(tag.name ?? '#${tag.slug}') | ||||
|                                                   .fontSize(13) | ||||
|                                             ], | ||||
|                                           ), | ||||
|                                           onTap: () {}, | ||||
|                                         ), | ||||
|                                     ], | ||||
|                                   ), | ||||
|                                 if (item.categories.isNotEmpty) | ||||
|                                   Wrap( | ||||
|                                     children: [ | ||||
|                                       for (final category in item.categories) | ||||
|                                         InkWell( | ||||
|                                           child: Row( | ||||
|                                             spacing: 4, | ||||
|                                             children: [ | ||||
|                                               const Icon(Symbols.category, size: 13), | ||||
|                                               Text(category.name ?? '#${category.slug}') | ||||
|                                                   .fontSize(13) | ||||
|                                             ], | ||||
|                                           ), | ||||
|                                           onTap: () {}, | ||||
|                                         ), | ||||
|                                     ], | ||||
|                                   ), | ||||
|                               ], | ||||
|                             ), | ||||
|                           // Show truncation hint if post is truncated | ||||
|                           if (item.isTruncated && !isFullPost) | ||||
|                             _PostTruncateHint().padding( | ||||
| @@ -286,7 +324,7 @@ class PostItem extends HookConsumerWidget { | ||||
|                       ), | ||||
|                       onTap: () { | ||||
|                         if (isOpenable) { | ||||
|                           context.router.push(PostDetailRoute(id: item.id)); | ||||
|                           context.push('/posts/item.id'); | ||||
|                         } | ||||
|                       }, | ||||
|                     ), | ||||
| @@ -487,9 +525,7 @@ Widget _buildReferencePost(BuildContext context, SnPost item) { | ||||
|         ), | ||||
|       ], | ||||
|     ), | ||||
|   ).gestures( | ||||
|     onTap: () => context.router.push(PostDetailRoute(id: referencePost.id)), | ||||
|   ); | ||||
|   ).gestures(onTap: () => context.push('/posts/referencePost.id')); | ||||
| } | ||||
|  | ||||
| class PostReactionList extends HookConsumerWidget { | ||||
|   | ||||
| @@ -1,11 +1,10 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/services/time.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/content/cloud_file_collection.dart'; | ||||
| @@ -46,7 +45,7 @@ class PostItemCreator extends HookConsumerWidget { | ||||
|               title: 'edit'.tr(), | ||||
|               image: MenuImage.icon(Symbols.edit), | ||||
|               callback: () { | ||||
|                 context.router.push(PostEditRoute(id: item.id)).then((value) { | ||||
|                 context.push('/posts/item.id/edit').then((value) { | ||||
|                   if (value != null) { | ||||
|                     onRefresh?.call(); | ||||
|                   } | ||||
| @@ -81,7 +80,7 @@ class PostItemCreator extends HookConsumerWidget { | ||||
|               image: MenuImage.icon(Symbols.link), | ||||
|               callback: () { | ||||
|                 // Copy post link to clipboard | ||||
|                 context.router.push(PostDetailRoute(id: item.id)); | ||||
|                 context.push('/posts/item.id'); | ||||
|               }, | ||||
|             ), | ||||
|           ], | ||||
| @@ -95,7 +94,7 @@ class PostItemCreator extends HookConsumerWidget { | ||||
|           borderRadius: BorderRadius.circular(12), | ||||
|           onTap: () { | ||||
|             if (isOpenable) { | ||||
|               context.router.pushPath('/posts/${item.id}'); | ||||
|               context.push('/posts/${item.id}'); | ||||
|             } | ||||
|           }, | ||||
|           child: Padding( | ||||
|   | ||||
| @@ -1,11 +1,10 @@ | ||||
| import 'dart:math' as math; | ||||
|  | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/screens/creators/publishers.dart'; | ||||
| import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| @@ -44,8 +43,7 @@ class PublisherModal extends HookConsumerWidget { | ||||
|                                     const Gap(12), | ||||
|                                     ElevatedButton( | ||||
|                                       onPressed: () { | ||||
|                                         context.router | ||||
|                                             .push(NewPublisherRoute()) | ||||
|                                         context.push('/creators/publishers/new') | ||||
|                                             .then((value) { | ||||
|                                               if (value != null) { | ||||
|                                                 ref.invalidate( | ||||
|   | ||||
| @@ -1,12 +1,11 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/content/sheet.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:island/route.gr.dart'; | ||||
| import 'package:island/screens/posts/compose.dart'; | ||||
| import 'package:island/models/file.dart'; | ||||
| import 'package:island/pods/link_preview.dart'; | ||||
| @@ -179,7 +178,7 @@ class _ShareSheetState extends ConsumerState<ShareSheet> { | ||||
|  | ||||
|       // Navigate to compose screen | ||||
|       if (mounted) { | ||||
|         context.router.push(PostComposeRoute(initialState: initialState)); | ||||
|         context.push('/posts/compose', extra: initialState); | ||||
|         Navigator.of(context).pop(); // Close the share sheet | ||||
|       } | ||||
|     } catch (e) { | ||||
| @@ -325,7 +324,7 @@ class _ShareSheetState extends ConsumerState<ShareSheet> { | ||||
|  | ||||
|         // Navigate to chat if requested | ||||
|         if (shouldNavigate == true && mounted) { | ||||
|           context.router.pushPath('/chat/$chatRoom'); | ||||
|           context.push('/chat/${chatRoom.id}'); | ||||
|         } | ||||
|       } | ||||
|     } catch (e) { | ||||
| @@ -405,132 +404,153 @@ class _ShareSheetState extends ConsumerState<ShareSheet> { | ||||
|       heightFactor: 0.75, | ||||
|       child: Column( | ||||
|         children: [ | ||||
|           // Content preview | ||||
|           Container( | ||||
|             margin: const EdgeInsets.all(16), | ||||
|             padding: const EdgeInsets.all(16), | ||||
|             decoration: BoxDecoration( | ||||
|               color: Theme.of(context).colorScheme.surfaceContainerHighest, | ||||
|               borderRadius: BorderRadius.circular(12), | ||||
|             ), | ||||
|             child: Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Text( | ||||
|                   'contentToShare'.tr(), | ||||
|                   style: Theme.of(context).textTheme.labelMedium?.copyWith( | ||||
|                     color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|                   ), | ||||
|                 ), | ||||
|                 const SizedBox(height: 8), | ||||
|                 _ContentPreview(content: widget.content), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|  | ||||
|           // Share options | ||||
|           // Share options with keyboard avoidance | ||||
|           Expanded( | ||||
|             child: SingleChildScrollView( | ||||
|               child: Column( | ||||
|                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                 children: [ | ||||
|                   // Quick actions row (horizontally scrollable) | ||||
|                   Padding( | ||||
|                     padding: const EdgeInsets.symmetric(horizontal: 16), | ||||
|                     child: Column( | ||||
|                       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                       children: [ | ||||
|                         Text( | ||||
|                           'quickActions'.tr(), | ||||
|                           style: Theme.of( | ||||
|                             context, | ||||
|                           ).textTheme.titleSmall?.copyWith( | ||||
|                             color: | ||||
|                                 Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|             child: AnimatedPadding( | ||||
|               duration: const Duration(milliseconds: 300), | ||||
|               padding: EdgeInsets.only( | ||||
|                 bottom: MediaQuery.of(context).viewInsets.bottom, | ||||
|               ), | ||||
|               child: SingleChildScrollView( | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                   children: [ | ||||
|                     // Content preview | ||||
|                     Container( | ||||
|                       margin: const EdgeInsets.all(16), | ||||
|                       padding: const EdgeInsets.all(16), | ||||
|                       decoration: BoxDecoration( | ||||
|                         color: | ||||
|                             Theme.of( | ||||
|                               context, | ||||
|                             ).colorScheme.surfaceContainerHighest, | ||||
|                         borderRadius: BorderRadius.circular(12), | ||||
|                       ), | ||||
|                       child: Column( | ||||
|                         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                         children: [ | ||||
|                           Text( | ||||
|                             'contentToShare'.tr(), | ||||
|                             style: Theme.of( | ||||
|                               context, | ||||
|                             ).textTheme.labelMedium?.copyWith( | ||||
|                               color: | ||||
|                                   Theme.of( | ||||
|                                     context, | ||||
|                                   ).colorScheme.onSurfaceVariant, | ||||
|                             ), | ||||
|                           ), | ||||
|                         ), | ||||
|                         const SizedBox(height: 12), | ||||
|                         SizedBox( | ||||
|                           height: 80, | ||||
|                           child: ListView( | ||||
|                             scrollDirection: Axis.horizontal, | ||||
|                             children: [ | ||||
|                               _CompactShareOption( | ||||
|                                 icon: Symbols.post_add, | ||||
|                                 title: 'post'.tr(), | ||||
|                                 onTap: _isLoading ? null : _shareToPost, | ||||
|                               ), | ||||
|                               const SizedBox(width: 12), | ||||
|                               _CompactShareOption( | ||||
|                                 icon: Symbols.content_copy, | ||||
|                                 title: 'copy'.tr(), | ||||
|                                 onTap: _isLoading ? null : _copyToClipboard, | ||||
|                               ), | ||||
|                               if (widget.toSystem) ...<Widget>[ | ||||
|                           const SizedBox(height: 8), | ||||
|                           _ContentPreview(content: widget.content), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ), | ||||
|                     // Quick actions row (horizontally scrollable) | ||||
|                     Padding( | ||||
|                       padding: const EdgeInsets.symmetric(horizontal: 16), | ||||
|                       child: Column( | ||||
|                         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                         children: [ | ||||
|                           Text( | ||||
|                             'quickActions'.tr(), | ||||
|                             style: Theme.of( | ||||
|                               context, | ||||
|                             ).textTheme.titleSmall?.copyWith( | ||||
|                               color: | ||||
|                                   Theme.of( | ||||
|                                     context, | ||||
|                                   ).colorScheme.onSurfaceVariant, | ||||
|                             ), | ||||
|                           ), | ||||
|                           const SizedBox(height: 12), | ||||
|                           SizedBox( | ||||
|                             height: 80, | ||||
|                             child: ListView( | ||||
|                               scrollDirection: Axis.horizontal, | ||||
|                               children: [ | ||||
|                                 _CompactShareOption( | ||||
|                                   icon: Symbols.post_add, | ||||
|                                   title: 'post'.tr(), | ||||
|                                   onTap: _isLoading ? null : _shareToPost, | ||||
|                                 ), | ||||
|                                 const SizedBox(width: 12), | ||||
|                                 _CompactShareOption( | ||||
|                                   icon: Symbols.share, | ||||
|                                   title: 'share'.tr(), | ||||
|                                   onTap: _isLoading ? null : _shareToSystem, | ||||
|                                   icon: Symbols.content_copy, | ||||
|                                   title: 'copy'.tr(), | ||||
|                                   onTap: _isLoading ? null : _copyToClipboard, | ||||
|                                 ), | ||||
|                                 if (widget.toSystem) ...<Widget>[ | ||||
|                                   const SizedBox(width: 12), | ||||
|                                   _CompactShareOption( | ||||
|                                     icon: Symbols.share, | ||||
|                                     title: 'share'.tr(), | ||||
|                                     onTap: _isLoading ? null : _shareToSystem, | ||||
|                                   ), | ||||
|                                 ], | ||||
|                               ], | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                   ), | ||||
|  | ||||
|                   const SizedBox(height: 24), | ||||
|  | ||||
|                   // Chat section | ||||
|                   Padding( | ||||
|                     padding: const EdgeInsets.symmetric(horizontal: 16), | ||||
|                     child: Column( | ||||
|                       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                       children: [ | ||||
|                         Text( | ||||
|                           'sendToChat'.tr(), | ||||
|                           style: Theme.of( | ||||
|                             context, | ||||
|                           ).textTheme.titleSmall?.copyWith( | ||||
|                             color: | ||||
|                                 Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|                           ), | ||||
|                         ), | ||||
|                         const SizedBox(height: 12), | ||||
|  | ||||
|                         // Additional message input | ||||
|                         Container( | ||||
|                           margin: const EdgeInsets.only(bottom: 16), | ||||
|                           child: TextField( | ||||
|                             controller: _messageController, | ||||
|                             decoration: InputDecoration( | ||||
|                               hintText: 'addAdditionalMessage'.tr(), | ||||
|                               border: OutlineInputBorder( | ||||
|                                 borderRadius: BorderRadius.circular(12), | ||||
|                               ), | ||||
|                               contentPadding: const EdgeInsets.symmetric( | ||||
|                                 horizontal: 16, | ||||
|                                 vertical: 12, | ||||
|                               ), | ||||
|                             ), | ||||
|                             maxLines: 3, | ||||
|                             minLines: 1, | ||||
|                             enabled: !_isLoading, | ||||
|                           ), | ||||
|                         ), | ||||
|  | ||||
|                         _ChatRoomsList( | ||||
|                           onChatSelected: | ||||
|                               _isLoading ? null : _shareToSpecificChat, | ||||
|                         ), | ||||
|                       ], | ||||
|                         ], | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|  | ||||
|                   const SizedBox(height: 16), | ||||
|                 ], | ||||
|                     const SizedBox(height: 24), | ||||
|  | ||||
|                     // Chat section | ||||
|                     Padding( | ||||
|                       padding: const EdgeInsets.symmetric(horizontal: 16), | ||||
|                       child: Column( | ||||
|                         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                         children: [ | ||||
|                           Text( | ||||
|                             'sendToChat'.tr(), | ||||
|                             style: Theme.of( | ||||
|                               context, | ||||
|                             ).textTheme.titleSmall?.copyWith( | ||||
|                               color: | ||||
|                                   Theme.of( | ||||
|                                     context, | ||||
|                                   ).colorScheme.onSurfaceVariant, | ||||
|                             ), | ||||
|                           ), | ||||
|                           const SizedBox(height: 12), | ||||
|  | ||||
|                           // Additional message input | ||||
|                           Container( | ||||
|                             margin: const EdgeInsets.only(bottom: 16), | ||||
|                             child: TextField( | ||||
|                               controller: _messageController, | ||||
|                               decoration: InputDecoration( | ||||
|                                 hintText: 'addAdditionalMessage'.tr(), | ||||
|                                 border: OutlineInputBorder( | ||||
|                                   borderRadius: BorderRadius.circular(12), | ||||
|                                 ), | ||||
|                                 contentPadding: const EdgeInsets.symmetric( | ||||
|                                   horizontal: 16, | ||||
|                                   vertical: 12, | ||||
|                                 ), | ||||
|                               ), | ||||
|                               onTapOutside: | ||||
|                                   (_) => | ||||
|                                       FocusManager.instance.primaryFocus | ||||
|                                           ?.unfocus(), | ||||
|                               maxLines: 3, | ||||
|                               minLines: 1, | ||||
|                               enabled: !_isLoading, | ||||
|                             ), | ||||
|                           ), | ||||
|  | ||||
|                           _ChatRoomsList( | ||||
|                             onChatSelected: | ||||
|                                 _isLoading ? null : _shareToSpecificChat, | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ), | ||||
|  | ||||
|                     const SizedBox(height: 16), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
| @@ -830,9 +850,7 @@ class _TextPreview extends StatelessWidget { | ||||
|   Widget build(BuildContext context) { | ||||
|     return Container( | ||||
|       constraints: const BoxConstraints(maxHeight: kPreviewMaxHeight), | ||||
|       child: SingleChildScrollView( | ||||
|         child: Text(text, style: Theme.of(context).textTheme.bodyMedium), | ||||
|       ), | ||||
|       child: Text(text, style: Theme.of(context).textTheme.bodyMedium), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1001,13 +1019,11 @@ class _LinkPreview extends ConsumerWidget { | ||||
|           ), | ||||
|           const SizedBox(height: 8), | ||||
|           Expanded( | ||||
|             child: SingleChildScrollView( | ||||
|               child: SelectableText( | ||||
|                 link, | ||||
|                 style: Theme.of(context).textTheme.bodyMedium?.copyWith( | ||||
|                   color: Theme.of(context).colorScheme.primary, | ||||
|                   decoration: TextDecoration.underline, | ||||
|                 ), | ||||
|             child: SelectableText( | ||||
|               link, | ||||
|               style: Theme.of(context).textTheme.bodyMedium?.copyWith( | ||||
|                 color: Theme.of(context).colorScheme.primary, | ||||
|                 decoration: TextDecoration.underline, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
| @@ -1236,6 +1252,7 @@ void showShareSheet({ | ||||
|   showModalBottomSheet( | ||||
|     context: context, | ||||
|     isScrollControlled: true, | ||||
|     useRootNavigator: true, | ||||
|     builder: | ||||
|         (context) => ShareSheet( | ||||
|           content: content, | ||||
|   | ||||
							
								
								
									
										37
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										37
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -74,7 +74,7 @@ packages: | ||||
|     source: hosted | ||||
|     version: "2.13.0" | ||||
|   auto_route: | ||||
|     dependency: "direct main" | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: auto_route | ||||
|       sha256: b8c036fa613a98a759cf0fdcba26e62f4985dcbff01a5e760ab411e8554bbaf0 | ||||
| @@ -1037,6 +1037,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.1.3" | ||||
|   go_router: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: go_router | ||||
|       sha256: ac294be30ba841830cfa146e5a3b22bb09f8dc5a0fdd9ca9332b04b0bde99ebf | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "15.2.4" | ||||
|   google_fonts: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -2098,14 +2106,6 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.10.1" | ||||
|   speed_test_dart: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: speed_test_dart | ||||
|       sha256: "4131faa68d5c9259766626450a10e552bc11ff6e651bb6377cc56476443e1cfa" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.0.5+0" | ||||
|   sprintf: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -2250,14 +2250,6 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.4.1" | ||||
|   sync: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: sync | ||||
|       sha256: f2ebb89eac969abb02b498562a35c4da63d6843396c4fe81948cd06a76845fce | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.3.0" | ||||
|   synchronized: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -2306,6 +2298,15 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.6.8" | ||||
|   textfield_tags: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       path: "." | ||||
|       ref: "fixes/allow-controller-re-registration" | ||||
|       resolved-ref: "7574e79649e34df1c3cc0c49b2f0cc2b92de6a7b" | ||||
|       url: "https://github.com/lionelmennig/textfield_tags.git" | ||||
|     source: git | ||||
|     version: "3.0.1" | ||||
|   timezone: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -2343,7 +2344,7 @@ packages: | ||||
|     description: | ||||
|       path: "." | ||||
|       ref: HEAD | ||||
|       resolved-ref: "55e0eecfb7a7af67be4a7b6e8e73d128d4460436" | ||||
|       resolved-ref: e33aa4f363104d083e681103b102037e212e32ab | ||||
|       url: "https://github.com/LittleSheep2Code/tus_client.git" | ||||
|     source: git | ||||
|     version: "2.5.0" | ||||
|   | ||||
| @@ -37,7 +37,7 @@ dependencies: | ||||
|   flutter_hooks: ^0.21.2 | ||||
|   hooks_riverpod: ^2.6.1 | ||||
|   bitsdojo_window: ^0.1.6 | ||||
|   auto_route: ^10.0.1 | ||||
|   go_router: ^15.2.4 | ||||
|   styled_widget: ^0.4.1 | ||||
|   shared_preferences: ^2.5.3 | ||||
|   flutter_riverpod: ^2.6.1 | ||||
| @@ -122,6 +122,10 @@ dependencies: | ||||
|   share_plus: ^11.0.0 | ||||
|   receive_sharing_intent: ^1.8.1 | ||||
|   top_snackbar_flutter: ^3.3.0 | ||||
|   textfield_tags: | ||||
|    git: | ||||
|       url: https://github.com/lionelmennig/textfield_tags.git | ||||
|       ref: fixes/allow-controller-re-registration | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user