Compare commits
	
		
			6 Commits
		
	
	
		
			52111c4b95
			...
			3.0.0+106
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 91c5a2e1b6 | |||
| cb991d1574 | |||
| 75097ab6fc | |||
| 8d855867c1 | |||
| 89fd80bcb8 | |||
| ab4f4faafe | 
| @@ -100,6 +100,11 @@ | |||||||
|   "permissionModerator": "Moderator", |   "permissionModerator": "Moderator", | ||||||
|   "permissionMember": "Member", |   "permissionMember": "Member", | ||||||
|   "reply": "Reply", |   "reply": "Reply", | ||||||
|  |   "repliesCount": { | ||||||
|  |     "zero": "No reply", | ||||||
|  |     "one": "{} reply", | ||||||
|  |     "other": "{} replies" | ||||||
|  |   }, | ||||||
|   "forward": "Forward", |   "forward": "Forward", | ||||||
|   "repliedTo": "Replied to", |   "repliedTo": "Replied to", | ||||||
|   "forwarded": "Forwarded", |   "forwarded": "Forwarded", | ||||||
| @@ -337,6 +342,9 @@ | |||||||
|   "unauthorized": "Unauthorized", |   "unauthorized": "Unauthorized", | ||||||
|   "unauthorizedHint": "You're not signed in or session expired, please sign in again.", |   "unauthorizedHint": "You're not signed in or session expired, please sign in again.", | ||||||
|   "publisherBelongsTo": "Belongs to {}", |   "publisherBelongsTo": "Belongs to {}", | ||||||
|  |   "postContent": "Content", | ||||||
|  |   "postSettings": "Settings", | ||||||
|  |   "postPublisherUnselected": "Publisher Unspecified", | ||||||
|   "postVisibility": "Visibility", |   "postVisibility": "Visibility", | ||||||
|   "postVisibilityPublic": "Public", |   "postVisibilityPublic": "Public", | ||||||
|   "postVisibilityFriends": "Friends Only", |   "postVisibilityFriends": "Friends Only", | ||||||
| @@ -449,5 +457,7 @@ | |||||||
|   "checkInResultT4": "Best", |   "checkInResultT4": "Best", | ||||||
|   "accountProfileView": "View Profile", |   "accountProfileView": "View Profile", | ||||||
|   "unspecified": "Unspecified", |   "unspecified": "Unspecified", | ||||||
|   "added": "Added" |   "added": "Added", | ||||||
|  |   "preview": "Preview", | ||||||
|  |   "togglePreview": "Toggle Preview" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -140,6 +140,8 @@ PODS: | |||||||
|     - nanopb/encode (= 3.30910.0) |     - nanopb/encode (= 3.30910.0) | ||||||
|   - nanopb/decode (3.30910.0) |   - nanopb/decode (3.30910.0) | ||||||
|   - nanopb/encode (3.30910.0) |   - nanopb/encode (3.30910.0) | ||||||
|  |   - native_exif (0.0.1): | ||||||
|  |     - Flutter | ||||||
|   - OrderedSet (6.0.3) |   - OrderedSet (6.0.3) | ||||||
|   - package_info_plus (0.4.5): |   - package_info_plus (0.4.5): | ||||||
|     - Flutter |     - Flutter | ||||||
| @@ -218,6 +220,7 @@ DEPENDENCIES: | |||||||
|   - livekit_client (from `.symlinks/plugins/livekit_client/ios`) |   - livekit_client (from `.symlinks/plugins/livekit_client/ios`) | ||||||
|   - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) |   - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) | ||||||
|   - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) |   - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) | ||||||
|  |   - native_exif (from `.symlinks/plugins/native_exif/ios`) | ||||||
|   - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) |   - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) | ||||||
|   - pasteboard (from `.symlinks/plugins/pasteboard/ios`) |   - pasteboard (from `.symlinks/plugins/pasteboard/ios`) | ||||||
|   - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) |   - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) | ||||||
| @@ -292,6 +295,8 @@ EXTERNAL SOURCES: | |||||||
|     :path: ".symlinks/plugins/media_kit_libs_ios_video/ios" |     :path: ".symlinks/plugins/media_kit_libs_ios_video/ios" | ||||||
|   media_kit_video: |   media_kit_video: | ||||||
|     :path: ".symlinks/plugins/media_kit_video/ios" |     :path: ".symlinks/plugins/media_kit_video/ios" | ||||||
|  |   native_exif: | ||||||
|  |     :path: ".symlinks/plugins/native_exif/ios" | ||||||
|   package_info_plus: |   package_info_plus: | ||||||
|     :path: ".symlinks/plugins/package_info_plus/ios" |     :path: ".symlinks/plugins/package_info_plus/ios" | ||||||
|   pasteboard: |   pasteboard: | ||||||
| @@ -349,6 +354,7 @@ SPEC CHECKSUMS: | |||||||
|   media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 |   media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 | ||||||
|   media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 |   media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 | ||||||
|   nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 |   nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 | ||||||
|  |   native_exif: 0eb73d3d5b3ca892719228df8d2d1b13d1ae396c | ||||||
|   OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 |   OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 | ||||||
|   package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 |   package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 | ||||||
|   pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c |   pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c | ||||||
|   | |||||||
| @@ -22,6 +22,7 @@ sealed class SnPost with _$SnPost { | |||||||
|     required int viewsTotal, |     required int viewsTotal, | ||||||
|     required int upvotes, |     required int upvotes, | ||||||
|     required int downvotes, |     required int downvotes, | ||||||
|  |     required int repliesCount, | ||||||
|     required String? threadedPostId, |     required String? threadedPostId, | ||||||
|     required SnPost? threadedPost, |     required SnPost? threadedPost, | ||||||
|     required String? repliedPostId, |     required String? repliedPostId, | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ T _$identity<T>(T value) => value; | |||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$SnPost { | 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; 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; |  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; | ||||||
| /// Create a copy of SnPost | /// Create a copy of SnPost | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @@ -29,16 +29,16 @@ $SnPostCopyWith<SnPost> get copyWith => _$SnPostCopyWithImpl<SnPost>(this as SnP | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other.reactionsCount, reactionsCount)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.collections, collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); |   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other.reactionsCount, reactionsCount)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.collections, collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); | ||||||
| } | } | ||||||
|  |  | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(meta),viewsUnique,viewsTotal,upvotes,downvotes,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,const DeepCollectionEquality().hash(attachments),publisher,const DeepCollectionEquality().hash(reactionsCount),const DeepCollectionEquality().hash(reactions),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(collections),createdAt,updatedAt,deletedAt]); | int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,const DeepCollectionEquality().hash(attachments),publisher,const DeepCollectionEquality().hash(reactionsCount),const DeepCollectionEquality().hash(reactions),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(collections),createdAt,updatedAt,deletedAt]); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; |   return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -49,7 +49,7 @@ abstract mixin class $SnPostCopyWith<$Res>  { | |||||||
|   factory $SnPostCopyWith(SnPost value, $Res Function(SnPost) _then) = _$SnPostCopyWithImpl; |   factory $SnPostCopyWith(SnPost value, $Res Function(SnPost) _then) = _$SnPostCopyWithImpl; | ||||||
| @useResult | @useResult | ||||||
| $Res call({ | $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, 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 |  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 | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -66,7 +66,7 @@ class _$SnPostCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnPost | /// Create a copy of SnPost | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = null,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = null,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||||
|   return _then(_self.copyWith( |   return _then(_self.copyWith( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
| as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -82,6 +82,7 @@ as Map<String, dynamic>?,viewsUnique: null == viewsUnique ? _self.viewsUnique : | |||||||
| as int,viewsTotal: null == viewsTotal ? _self.viewsTotal : viewsTotal // ignore: cast_nullable_to_non_nullable | as int,viewsTotal: null == viewsTotal ? _self.viewsTotal : viewsTotal // ignore: cast_nullable_to_non_nullable | ||||||
| as int,upvotes: null == upvotes ? _self.upvotes : upvotes // ignore: cast_nullable_to_non_nullable | as int,upvotes: null == upvotes ? _self.upvotes : upvotes // ignore: cast_nullable_to_non_nullable | ||||||
| as int,downvotes: null == downvotes ? _self.downvotes : downvotes // ignore: cast_nullable_to_non_nullable | as int,downvotes: null == downvotes ? _self.downvotes : downvotes // ignore: cast_nullable_to_non_nullable | ||||||
|  | as int,repliesCount: null == repliesCount ? _self.repliesCount : repliesCount // ignore: cast_nullable_to_non_nullable | ||||||
| as int,threadedPostId: freezed == threadedPostId ? _self.threadedPostId : threadedPostId // ignore: cast_nullable_to_non_nullable | as int,threadedPostId: freezed == threadedPostId ? _self.threadedPostId : threadedPostId // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,threadedPost: freezed == threadedPost ? _self.threadedPost : threadedPost // ignore: cast_nullable_to_non_nullable | as String?,threadedPost: freezed == threadedPost ? _self.threadedPost : threadedPost // ignore: cast_nullable_to_non_nullable | ||||||
| as SnPost?,repliedPostId: freezed == repliedPostId ? _self.repliedPostId : repliedPostId // ignore: cast_nullable_to_non_nullable | as SnPost?,repliedPostId: freezed == repliedPostId ? _self.repliedPostId : repliedPostId // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -154,7 +155,7 @@ $SnPublisherCopyWith<$Res> get publisher { | |||||||
| @JsonSerializable() | @JsonSerializable() | ||||||
|  |  | ||||||
| class _SnPost implements SnPost { | class _SnPost implements SnPost { | ||||||
|   const _SnPost({required this.id, required this.title, required this.description, required this.language, required this.editedAt, required this.publishedAt, required this.visibility, required this.content, required this.type, required final  Map<String, dynamic>? meta, required this.viewsUnique, required this.viewsTotal, required this.upvotes, required this.downvotes, required this.threadedPostId, required this.threadedPost, required this.repliedPostId, required this.repliedPost, required this.forwardedPostId, required this.forwardedPost, required final  List<SnCloudFile> attachments, required this.publisher, final  Map<String, int> reactionsCount = const {}, required final  List<dynamic> reactions, required final  List<dynamic> tags, required final  List<dynamic> categories, required final  List<dynamic> collections, required this.createdAt, required this.updatedAt, required this.deletedAt}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections; |   const _SnPost({required this.id, required this.title, required this.description, required this.language, required this.editedAt, required this.publishedAt, required this.visibility, required this.content, required this.type, required final  Map<String, dynamic>? meta, required this.viewsUnique, required this.viewsTotal, required this.upvotes, required this.downvotes, required this.repliesCount, required this.threadedPostId, required this.threadedPost, required this.repliedPostId, required this.repliedPost, required this.forwardedPostId, required this.forwardedPost, required final  List<SnCloudFile> attachments, required this.publisher, final  Map<String, int> reactionsCount = const {}, required final  List<dynamic> reactions, required final  List<dynamic> tags, required final  List<dynamic> categories, required final  List<dynamic> collections, required this.createdAt, required this.updatedAt, required this.deletedAt}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections; | ||||||
|   factory _SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json); |   factory _SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json); | ||||||
|  |  | ||||||
| @override final  String id; | @override final  String id; | ||||||
| @@ -179,6 +180,7 @@ class _SnPost implements SnPost { | |||||||
| @override final  int viewsTotal; | @override final  int viewsTotal; | ||||||
| @override final  int upvotes; | @override final  int upvotes; | ||||||
| @override final  int downvotes; | @override final  int downvotes; | ||||||
|  | @override final  int repliesCount; | ||||||
| @override final  String? threadedPostId; | @override final  String? threadedPostId; | ||||||
| @override final  SnPost? threadedPost; | @override final  SnPost? threadedPost; | ||||||
| @override final  String? repliedPostId; | @override final  String? repliedPostId; | ||||||
| @@ -245,16 +247,16 @@ Map<String, dynamic> toJson() { | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other._reactionsCount, _reactionsCount)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._collections, _collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); |   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other._reactionsCount, _reactionsCount)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._collections, _collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); | ||||||
| } | } | ||||||
|  |  | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(_meta),viewsUnique,viewsTotal,upvotes,downvotes,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,const DeepCollectionEquality().hash(_attachments),publisher,const DeepCollectionEquality().hash(_reactionsCount),const DeepCollectionEquality().hash(_reactions),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_collections),createdAt,updatedAt,deletedAt]); | int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(_meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,const DeepCollectionEquality().hash(_attachments),publisher,const DeepCollectionEquality().hash(_reactionsCount),const DeepCollectionEquality().hash(_reactions),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_collections),createdAt,updatedAt,deletedAt]); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; |   return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -265,7 +267,7 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> { | |||||||
|   factory _$SnPostCopyWith(_SnPost value, $Res Function(_SnPost) _then) = __$SnPostCopyWithImpl; |   factory _$SnPostCopyWith(_SnPost value, $Res Function(_SnPost) _then) = __$SnPostCopyWithImpl; | ||||||
| @override @useResult | @override @useResult | ||||||
| $Res call({ | $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, 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 |  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 | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -282,7 +284,7 @@ class __$SnPostCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnPost | /// Create a copy of SnPost | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = null,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = null,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||||
|   return _then(_SnPost( |   return _then(_SnPost( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
| as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -298,6 +300,7 @@ as Map<String, dynamic>?,viewsUnique: null == viewsUnique ? _self.viewsUnique : | |||||||
| as int,viewsTotal: null == viewsTotal ? _self.viewsTotal : viewsTotal // ignore: cast_nullable_to_non_nullable | as int,viewsTotal: null == viewsTotal ? _self.viewsTotal : viewsTotal // ignore: cast_nullable_to_non_nullable | ||||||
| as int,upvotes: null == upvotes ? _self.upvotes : upvotes // ignore: cast_nullable_to_non_nullable | as int,upvotes: null == upvotes ? _self.upvotes : upvotes // ignore: cast_nullable_to_non_nullable | ||||||
| as int,downvotes: null == downvotes ? _self.downvotes : downvotes // ignore: cast_nullable_to_non_nullable | as int,downvotes: null == downvotes ? _self.downvotes : downvotes // ignore: cast_nullable_to_non_nullable | ||||||
|  | as int,repliesCount: null == repliesCount ? _self.repliesCount : repliesCount // ignore: cast_nullable_to_non_nullable | ||||||
| as int,threadedPostId: freezed == threadedPostId ? _self.threadedPostId : threadedPostId // ignore: cast_nullable_to_non_nullable | as int,threadedPostId: freezed == threadedPostId ? _self.threadedPostId : threadedPostId // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,threadedPost: freezed == threadedPost ? _self.threadedPost : threadedPost // ignore: cast_nullable_to_non_nullable | as String?,threadedPost: freezed == threadedPost ? _self.threadedPost : threadedPost // ignore: cast_nullable_to_non_nullable | ||||||
| as SnPost?,repliedPostId: freezed == repliedPostId ? _self.repliedPostId : repliedPostId // ignore: cast_nullable_to_non_nullable | as SnPost?,repliedPostId: freezed == repliedPostId ? _self.repliedPostId : repliedPostId // ignore: cast_nullable_to_non_nullable | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost( | |||||||
|   viewsTotal: (json['views_total'] as num).toInt(), |   viewsTotal: (json['views_total'] as num).toInt(), | ||||||
|   upvotes: (json['upvotes'] as num).toInt(), |   upvotes: (json['upvotes'] as num).toInt(), | ||||||
|   downvotes: (json['downvotes'] as num).toInt(), |   downvotes: (json['downvotes'] as num).toInt(), | ||||||
|  |   repliesCount: (json['replies_count'] as num).toInt(), | ||||||
|   threadedPostId: json['threaded_post_id'] as String?, |   threadedPostId: json['threaded_post_id'] as String?, | ||||||
|   threadedPost: |   threadedPost: | ||||||
|       json['threaded_post'] == null |       json['threaded_post'] == null | ||||||
| @@ -76,6 +77,7 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{ | |||||||
|   'views_total': instance.viewsTotal, |   'views_total': instance.viewsTotal, | ||||||
|   'upvotes': instance.upvotes, |   'upvotes': instance.upvotes, | ||||||
|   'downvotes': instance.downvotes, |   'downvotes': instance.downvotes, | ||||||
|  |   'replies_count': instance.repliesCount, | ||||||
|   'threaded_post_id': instance.threadedPostId, |   'threaded_post_id': instance.threadedPostId, | ||||||
|   'threaded_post': instance.threadedPost?.toJson(), |   'threaded_post': instance.threadedPost?.toJson(), | ||||||
|   'replied_post_id': instance.repliedPostId, |   'replied_post_id': instance.repliedPostId, | ||||||
|   | |||||||
| @@ -16,27 +16,42 @@ import 'config.dart'; | |||||||
| final imagePickerProvider = Provider((ref) => ImagePicker()); | final imagePickerProvider = Provider((ref) => ImagePicker()); | ||||||
|  |  | ||||||
| final userAgentProvider = FutureProvider<String>((ref) async { | final userAgentProvider = FutureProvider<String>((ref) async { | ||||||
|  |   // Helper function to sanitize strings for HTTP headers | ||||||
|  |   String sanitizeForHeader(String input) { | ||||||
|  |     // Remove or replace characters that are not allowed in HTTP headers | ||||||
|  |     // Keep only ASCII printable characters (32-126) and replace others with underscore | ||||||
|  |     return input.runes.map((rune) { | ||||||
|  |       if (rune >= 32 && rune <= 126) { | ||||||
|  |         return String.fromCharCode(rune); | ||||||
|  |       } else { | ||||||
|  |         return '_'; | ||||||
|  |       } | ||||||
|  |     }).join(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   final String platformInfo; |   final String platformInfo; | ||||||
|   if (kIsWeb) { |   if (kIsWeb) { | ||||||
|     final deviceInfo = await DeviceInfoPlugin().webBrowserInfo; |     final deviceInfo = await DeviceInfoPlugin().webBrowserInfo; | ||||||
|     platformInfo = 'Web; ${deviceInfo.vendor}'; |     platformInfo = 'Web; ${sanitizeForHeader(deviceInfo.vendor ?? 'Unknown')}'; | ||||||
|   } else if (Platform.isAndroid) { |   } else if (Platform.isAndroid) { | ||||||
|     final deviceInfo = await DeviceInfoPlugin().androidInfo; |     final deviceInfo = await DeviceInfoPlugin().androidInfo; | ||||||
|     platformInfo = |     platformInfo = | ||||||
|         'Android; ${deviceInfo.brand} ${deviceInfo.model}; ${deviceInfo.id}'; |         'Android; ${sanitizeForHeader(deviceInfo.brand)} ${sanitizeForHeader(deviceInfo.model)}; ${sanitizeForHeader(deviceInfo.id)}'; | ||||||
|   } else if (Platform.isIOS) { |   } else if (Platform.isIOS) { | ||||||
|     final deviceInfo = await DeviceInfoPlugin().iosInfo; |     final deviceInfo = await DeviceInfoPlugin().iosInfo; | ||||||
|     platformInfo = 'iOS; ${deviceInfo.model}; ${deviceInfo.name}'; |     platformInfo = | ||||||
|  |         'iOS; ${sanitizeForHeader(deviceInfo.model)}; ${sanitizeForHeader(deviceInfo.name)}'; | ||||||
|   } else if (Platform.isMacOS) { |   } else if (Platform.isMacOS) { | ||||||
|     final deviceInfo = await DeviceInfoPlugin().macOsInfo; |     final deviceInfo = await DeviceInfoPlugin().macOsInfo; | ||||||
|     platformInfo = 'MacOS; ${deviceInfo.model}; ${deviceInfo.hostName}'; |     platformInfo = | ||||||
|  |         'MacOS; ${sanitizeForHeader(deviceInfo.model)}; ${sanitizeForHeader(deviceInfo.hostName)}'; | ||||||
|   } else if (Platform.isWindows) { |   } else if (Platform.isWindows) { | ||||||
|     final deviceInfo = await DeviceInfoPlugin().windowsInfo; |     final deviceInfo = await DeviceInfoPlugin().windowsInfo; | ||||||
|     platformInfo = |     platformInfo = | ||||||
|         'Windows NT; ${deviceInfo.productName}; ${deviceInfo.computerName}'; |         'Windows NT; ${sanitizeForHeader(deviceInfo.productName)}; ${sanitizeForHeader(deviceInfo.computerName)}'; | ||||||
|   } else if (Platform.isLinux) { |   } else if (Platform.isLinux) { | ||||||
|     final deviceInfo = await DeviceInfoPlugin().linuxInfo; |     final deviceInfo = await DeviceInfoPlugin().linuxInfo; | ||||||
|     platformInfo = 'Linux; ${deviceInfo.prettyName}'; |     platformInfo = 'Linux; ${sanitizeForHeader(deviceInfo.prettyName)}'; | ||||||
|   } else { |   } else { | ||||||
|     platformInfo = 'Unknown'; |     platformInfo = 'Unknown'; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -8,14 +8,14 @@ class AppRouter extends RootStackRouter { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   List<AutoRoute> get routes => [ |   List<AutoRoute> get routes => [ | ||||||
|  |     AutoRoute(page: PostComposeRoute.page, path: '/posts/compose'), | ||||||
|  |     AutoRoute(page: PostEditRoute.page, path: '/posts/:id/edit'), | ||||||
|     AutoRoute( |     AutoRoute( | ||||||
|       page: ExploreShellRoute.page, |       page: ExploreShellRoute.page, | ||||||
|       path: '/', |       path: '/', | ||||||
|       children: [ |       children: [ | ||||||
|         AutoRoute(page: ExploreRoute.page, path: ''), |         AutoRoute(page: ExploreRoute.page, path: ''), | ||||||
|         AutoRoute(page: PostComposeRoute.page, path: 'posts/compose'), |  | ||||||
|         AutoRoute(page: PostDetailRoute.page, path: 'posts/:id'), |         AutoRoute(page: PostDetailRoute.page, path: 'posts/:id'), | ||||||
|         AutoRoute(page: PostEditRoute.page, path: 'posts/:id/edit'), |  | ||||||
|         AutoRoute(page: PublisherProfileRoute.page, path: 'publishers/:name'), |         AutoRoute(page: PublisherProfileRoute.page, path: 'publishers/:name'), | ||||||
|       ], |       ], | ||||||
|     ), |     ), | ||||||
|   | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,9 +1,12 @@ | |||||||
| import 'package:auto_route/auto_route.dart'; | import 'package:auto_route/auto_route.dart'; | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/widgets/app_scaffold.dart'; | import 'package:island/widgets/app_scaffold.dart'; | ||||||
|  | import 'package:island/widgets/content/sheet.dart'; | ||||||
| import 'package:island/widgets/post/post_list.dart'; | import 'package:island/widgets/post/post_list.dart'; | ||||||
|  | import 'package:material_symbols_icons/symbols.dart'; | ||||||
|  |  | ||||||
| @RoutePage() | @RoutePage() | ||||||
| class CreatorPostListScreen extends HookConsumerWidget { | class CreatorPostListScreen extends HookConsumerWidget { | ||||||
| @@ -15,13 +18,62 @@ class CreatorPostListScreen extends HookConsumerWidget { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     final refreshKey = useState(0); | ||||||
|  |  | ||||||
|  |     void showCreatePostSheet() { | ||||||
|  |       showModalBottomSheet( | ||||||
|  |         context: context, | ||||||
|  |         builder: | ||||||
|  |             (context) => SheetScaffold( | ||||||
|  |               titleText: 'create'.tr(), | ||||||
|  |               child: Column( | ||||||
|  |                 children: [ | ||||||
|  |                   ListTile( | ||||||
|  |                     leading: const Icon(Symbols.edit), | ||||||
|  |                     title: Text('postContent'.tr()), | ||||||
|  |                     subtitle: Text('Create a regular post'), | ||||||
|  |                     onTap: () async { | ||||||
|  |                       Navigator.pop(context); | ||||||
|  |                       final result = await context.router.pushPath( | ||||||
|  |                         '/posts/compose?type=0', | ||||||
|  |                       ); | ||||||
|  |                       if (result == true) { | ||||||
|  |                         refreshKey.value++; | ||||||
|  |                       } | ||||||
|  |                     }, | ||||||
|  |                   ), | ||||||
|  |                   ListTile( | ||||||
|  |                     leading: const Icon(Symbols.article), | ||||||
|  |                     title: Text('Article'), | ||||||
|  |                     subtitle: Text('Create a detailed article'), | ||||||
|  |                     onTap: () async { | ||||||
|  |                       Navigator.pop(context); | ||||||
|  |                       final result = await context.router.pushPath( | ||||||
|  |                         '/posts/compose?type=1', | ||||||
|  |                       ); | ||||||
|  |                       if (result == true) { | ||||||
|  |                         refreshKey.value++; | ||||||
|  |                       } | ||||||
|  |                     }, | ||||||
|  |                   ), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return AppScaffold( |     return AppScaffold( | ||||||
|       appBar: AppBar(title: Text('posts').tr()), |       appBar: AppBar(title: Text('posts').tr()), | ||||||
|       body: CustomScrollView( |       body: CustomScrollView( | ||||||
|  |         key: ValueKey(refreshKey.value), | ||||||
|         slivers: [ |         slivers: [ | ||||||
|           SliverPostList(pubName: pubName, itemType: PostItemType.creator), |           SliverPostList(pubName: pubName, itemType: PostItemType.creator), | ||||||
|         ], |         ], | ||||||
|       ), |       ), | ||||||
|  |       floatingActionButton: FloatingActionButton( | ||||||
|  |         onPressed: showCreatePostSheet, | ||||||
|  |         child: const Icon(Symbols.add), | ||||||
|  |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -47,8 +47,9 @@ class NotificationUnreadCountNotifier | |||||||
|   void _subscribeToWebSocket() { |   void _subscribeToWebSocket() { | ||||||
|     final webSocketService = ref.read(websocketProvider); |     final webSocketService = ref.read(websocketProvider); | ||||||
|     _subscription = webSocketService.dataStream.listen((packet) { |     _subscription = webSocketService.dataStream.listen((packet) { | ||||||
|       if (packet.type == 'notifications.new') { |       if (packet.type == 'notifications.new' && packet.data != null) { | ||||||
|         _incrementCounter(); |         final notification = SnNotification.fromJson(packet.data!); | ||||||
|  |         if (notification.topic != 'messages.new') _incrementCounter(); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,28 +1,22 @@ | |||||||
| import 'package:auto_route/auto_route.dart'; | import 'package:auto_route/auto_route.dart'; | ||||||
| import 'package:collection/collection.dart'; |  | ||||||
| import 'package:dio/dio.dart'; |  | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter/services.dart'; |  | ||||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:image_picker/image_picker.dart'; |  | ||||||
| import 'package:island/models/file.dart'; |  | ||||||
| import 'package:island/models/post.dart'; | import 'package:island/models/post.dart'; | ||||||
| import 'package:island/pods/config.dart'; |  | ||||||
| import 'package:island/pods/network.dart'; |  | ||||||
| import 'package:island/screens/creators/publishers.dart'; | import 'package:island/screens/creators/publishers.dart'; | ||||||
| import 'package:island/screens/posts/detail.dart'; | import 'package:island/screens/posts/compose_article.dart'; | ||||||
| import 'package:island/services/file.dart'; |  | ||||||
| import 'package:island/services/responsive.dart'; | import 'package:island/services/responsive.dart'; | ||||||
| import 'package:island/widgets/alert.dart'; |  | ||||||
| import 'package:island/widgets/app_scaffold.dart'; | import 'package:island/widgets/app_scaffold.dart'; | ||||||
| import 'package:island/widgets/content/cloud_files.dart'; |  | ||||||
| import 'package:island/widgets/content/attachment_preview.dart'; | import 'package:island/widgets/content/attachment_preview.dart'; | ||||||
|  | import 'package:island/widgets/content/cloud_files.dart'; | ||||||
|  | import 'package:island/widgets/post/compose_shared.dart'; | ||||||
|  | import 'package:island/widgets/post/post_item.dart'; | ||||||
| import 'package:island/widgets/post/publishers_modal.dart'; | import 'package:island/widgets/post/publishers_modal.dart'; | ||||||
|  | import 'package:island/screens/posts/detail.dart'; | ||||||
|  | import 'package:island/widgets/post/compose_settings_sheet.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:pasteboard/pasteboard.dart'; |  | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  |  | ||||||
| @RoutePage() | @RoutePage() | ||||||
| @@ -54,282 +48,150 @@ class PostComposeScreen extends HookConsumerWidget { | |||||||
|   final SnPost? originalPost; |   final SnPost? originalPost; | ||||||
|   final SnPost? repliedPost; |   final SnPost? repliedPost; | ||||||
|   final SnPost? forwardedPost; |   final SnPost? forwardedPost; | ||||||
|  |   final int? type; | ||||||
|   const PostComposeScreen({ |   const PostComposeScreen({ | ||||||
|     super.key, |     super.key, | ||||||
|     this.originalPost, |     this.originalPost, | ||||||
|     this.repliedPost, |     this.repliedPost, | ||||||
|     this.forwardedPost, |     this.forwardedPost, | ||||||
|  |     @QueryParam('type') this.type, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     // Determine the compose type: auto-detect from edited post or use query parameter | ||||||
|  |     final composeType = originalPost?.type ?? type ?? 0; | ||||||
|  |  | ||||||
|  |     // If type is 1 (article), return ArticleComposeScreen | ||||||
|  |     if (composeType == 1) { | ||||||
|  |       return ArticleComposeScreen(originalPost: originalPost); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Otherwise, continue with regular post compose | ||||||
|  |     final theme = Theme.of(context); | ||||||
|  |     final colorScheme = theme.colorScheme; | ||||||
|  |  | ||||||
|     final publishers = ref.watch(publishersManagedProvider); |     final publishers = ref.watch(publishersManagedProvider); | ||||||
|  |     final state = useMemoized( | ||||||
|  |       () => ComposeLogic.createState( | ||||||
|  |         originalPost: originalPost, | ||||||
|  |         forwardedPost: forwardedPost, | ||||||
|  |       ), | ||||||
|  |       [originalPost, forwardedPost], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     final currentPublisher = useState<SnPublisher?>(null); |     // Initialize publisher once when data is available | ||||||
|  |  | ||||||
|     useEffect(() { |     useEffect(() { | ||||||
|       if (publishers.value?.isNotEmpty ?? false) { |       if (publishers.value?.isNotEmpty ?? false) { | ||||||
|         currentPublisher.value = publishers.value!.first; |         state.currentPublisher.value = publishers.value!.first; | ||||||
|       } |       } | ||||||
|       return null; |       return null; | ||||||
|     }, [publishers]); |     }, [publishers]); | ||||||
|  |  | ||||||
|     // Contains the XFile, ByteData, or SnCloudFile |     // Dispose state when widget is disposed | ||||||
|     final attachments = useState<List<UniversalFile>>( |     useEffect(() { | ||||||
|       originalPost?.attachments |       return () => ComposeLogic.dispose(state); | ||||||
|               .map( |     }, []); | ||||||
|                 (e) => UniversalFile( |  | ||||||
|                   data: e, |  | ||||||
|                   type: switch (e.mimeType?.split('/').firstOrNull) { |  | ||||||
|                     'image' => UniversalFileType.image, |  | ||||||
|                     'video' => UniversalFileType.video, |  | ||||||
|                     'audio' => UniversalFileType.audio, |  | ||||||
|                     _ => UniversalFileType.file, |  | ||||||
|                   }, |  | ||||||
|                 ), |  | ||||||
|               ) |  | ||||||
|               .toList() ?? |  | ||||||
|           [], |  | ||||||
|     ); |  | ||||||
|     final titleController = useTextEditingController(text: originalPost?.title); |  | ||||||
|     final descriptionController = useTextEditingController( |  | ||||||
|       text: originalPost?.description, |  | ||||||
|     ); |  | ||||||
|     final contentController = useTextEditingController( |  | ||||||
|       text: |  | ||||||
|           originalPost?.content ?? |  | ||||||
|           (forwardedPost != null ? '> ${forwardedPost!.content}\n\n' : null), |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     // Add visibility state with default value from original post or 0 (public) |     // Helper methods | ||||||
|     final visibility = useState<int>(originalPost?.visibility ?? 0); |  | ||||||
|  |  | ||||||
|     final submitting = useState(false); |     void showSettingsSheet() { | ||||||
|  |       showModalBottomSheet( | ||||||
|     Future<void> pickPhotoMedia() async { |  | ||||||
|       final result = await ref |  | ||||||
|           .watch(imagePickerProvider) |  | ||||||
|           .pickMultiImage(requestFullMetadata: true); |  | ||||||
|       if (result.isEmpty) return; |  | ||||||
|       attachments.value = [ |  | ||||||
|         ...attachments.value, |  | ||||||
|         ...result.map( |  | ||||||
|           (e) => UniversalFile(data: e, type: UniversalFileType.image), |  | ||||||
|         ), |  | ||||||
|       ]; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     Future<void> pickVideoMedia() async { |  | ||||||
|       final result = await ref |  | ||||||
|           .watch(imagePickerProvider) |  | ||||||
|           .pickVideo(source: ImageSource.gallery); |  | ||||||
|       if (result == null) return; |  | ||||||
|       attachments.value = [ |  | ||||||
|         ...attachments.value, |  | ||||||
|         UniversalFile(data: result, type: UniversalFileType.video), |  | ||||||
|       ]; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     final attachmentProgress = useState<Map<int, double>>({}); |  | ||||||
|  |  | ||||||
|     Future<void> uploadAttachment(int index) async { |  | ||||||
|       final attachment = attachments.value[index]; |  | ||||||
|       if (attachment is SnCloudFile) return; |  | ||||||
|       final baseUrl = ref.watch(serverUrlProvider); |  | ||||||
|       final token = await getToken(ref.watch(tokenProvider)); |  | ||||||
|       if (token == null) throw ArgumentError('Token is null'); |  | ||||||
|       try { |  | ||||||
|         attachmentProgress.value = {...attachmentProgress.value, index: 0}; |  | ||||||
|         final cloudFile = |  | ||||||
|             await putMediaToCloud( |  | ||||||
|               fileData: attachment, |  | ||||||
|               atk: token, |  | ||||||
|               baseUrl: baseUrl, |  | ||||||
|               filename: attachment.data.name ?? 'Post media', |  | ||||||
|               mimetype: |  | ||||||
|                   attachment.data.mimeType ?? |  | ||||||
|                   switch (attachment.type) { |  | ||||||
|                     UniversalFileType.image => 'image/unknown', |  | ||||||
|                     UniversalFileType.video => 'video/unknown', |  | ||||||
|                     UniversalFileType.audio => 'audio/unknown', |  | ||||||
|                     UniversalFileType.file => 'application/octet-stream', |  | ||||||
|                   }, |  | ||||||
|               onProgress: (progress, estimate) { |  | ||||||
|                 attachmentProgress.value = { |  | ||||||
|                   ...attachmentProgress.value, |  | ||||||
|                   index: progress, |  | ||||||
|                 }; |  | ||||||
|               }, |  | ||||||
|             ).future; |  | ||||||
|         if (cloudFile == null) { |  | ||||||
|           throw ArgumentError('Failed to upload the file...'); |  | ||||||
|         } |  | ||||||
|         final clone = List.of(attachments.value); |  | ||||||
|         clone[index] = UniversalFile(data: cloudFile, type: attachment.type); |  | ||||||
|         attachments.value = clone; |  | ||||||
|       } catch (err) { |  | ||||||
|         showErrorAlert(err); |  | ||||||
|       } finally { |  | ||||||
|         attachmentProgress.value = attachmentProgress.value..remove(index); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     Future<void> deleteAttachment(int index) async { |  | ||||||
|       final attachment = attachments.value[index]; |  | ||||||
|       if (attachment.isOnCloud) { |  | ||||||
|         final client = ref.watch(apiClientProvider); |  | ||||||
|         await client.delete('/files/${attachment.data.id}'); |  | ||||||
|       } |  | ||||||
|       final clone = List.of(attachments.value); |  | ||||||
|       clone.removeAt(index); |  | ||||||
|       attachments.value = clone; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     Future<void> performAction() async { |  | ||||||
|       try { |  | ||||||
|         submitting.value = true; |  | ||||||
|  |  | ||||||
|         await Future.wait( |  | ||||||
|           attachments.value |  | ||||||
|               .where((e) => e.isOnDevice) |  | ||||||
|               .mapIndexed((idx, e) => uploadAttachment(idx)), |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         final client = ref.watch(apiClientProvider); |  | ||||||
|         await client.request( |  | ||||||
|           originalPost == null ? '/posts' : '/posts/${originalPost!.id}', |  | ||||||
|           data: { |  | ||||||
|             'title': titleController.text, |  | ||||||
|             'description': descriptionController.text, |  | ||||||
|             'content': contentController.text, |  | ||||||
|             'visibility': |  | ||||||
|                 visibility.value, // Add visibility field to API request |  | ||||||
|             'attachments': |  | ||||||
|                 attachments.value |  | ||||||
|                     .where((e) => e.isOnCloud) |  | ||||||
|                     .map((e) => e.data.id) |  | ||||||
|                     .toList(), |  | ||||||
|             if (repliedPost != null) 'replied_post_id': repliedPost!.id, |  | ||||||
|             if (forwardedPost != null) 'forwarded_post_id': forwardedPost!.id, |  | ||||||
|           }, |  | ||||||
|           options: Options( |  | ||||||
|             headers: {'X-Pub': currentPublisher.value?.name}, |  | ||||||
|             method: originalPost == null ? 'POST' : 'PATCH', |  | ||||||
|           ), |  | ||||||
|         ); |  | ||||||
|         if (context.mounted) { |  | ||||||
|           context.maybePop(true); |  | ||||||
|         } |  | ||||||
|       } catch (err) { |  | ||||||
|         showErrorAlert(err); |  | ||||||
|       } finally { |  | ||||||
|         submitting.value = false; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     Future<void> handlePaste() async { |  | ||||||
|       final clipboard = await Pasteboard.image; |  | ||||||
|       if (clipboard == null) return; |  | ||||||
|  |  | ||||||
|       attachments.value = [ |  | ||||||
|         ...attachments.value, |  | ||||||
|         UniversalFile( |  | ||||||
|           data: XFile.fromData(clipboard, mimeType: "image/jpeg"), |  | ||||||
|           type: UniversalFileType.image, |  | ||||||
|         ), |  | ||||||
|       ]; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     void handleKeyPress(RawKeyEvent event) { |  | ||||||
|       if (event is! RawKeyDownEvent) return; |  | ||||||
|  |  | ||||||
|       final isPaste = event.logicalKey == LogicalKeyboardKey.keyV; |  | ||||||
|       final isModifierPressed = event.isMetaPressed || event.isControlPressed; |  | ||||||
|  |  | ||||||
|       if (isPaste && isModifierPressed) { |  | ||||||
|         handlePaste(); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     void showVisibilityModal() { |  | ||||||
|       showDialog( |  | ||||||
|         context: context, |         context: context, | ||||||
|  |         isScrollControlled: true, | ||||||
|         builder: |         builder: | ||||||
|             (context) => AlertDialog( |             (context) => ComposeSettingsSheet( | ||||||
|               title: Text('postVisibility'.tr()), |               titleController: state.titleController, | ||||||
|               content: Column( |               descriptionController: state.descriptionController, | ||||||
|                 mainAxisSize: MainAxisSize.min, |               visibility: state.visibility, | ||||||
|                 children: [ |               onVisibilityChanged: () { | ||||||
|                   ListTile( |                 // Trigger rebuild if needed | ||||||
|                     leading: Icon(Symbols.public), |               }, | ||||||
|                     title: Text('postVisibilityPublic'.tr()), |  | ||||||
|                     onTap: () { |  | ||||||
|                       visibility.value = 0; |  | ||||||
|                       Navigator.pop(context); |  | ||||||
|                     }, |  | ||||||
|                     selected: visibility.value == 0, |  | ||||||
|                   ), |  | ||||||
|                   ListTile( |  | ||||||
|                     leading: Icon(Symbols.group), |  | ||||||
|                     title: Text('postVisibilityFriends'.tr()), |  | ||||||
|                     onTap: () { |  | ||||||
|                       visibility.value = 1; |  | ||||||
|                       Navigator.pop(context); |  | ||||||
|                     }, |  | ||||||
|                     selected: visibility.value == 1, |  | ||||||
|                   ), |  | ||||||
|                   ListTile( |  | ||||||
|                     leading: Icon(Symbols.link_off), |  | ||||||
|                     title: Text('postVisibilityUnlisted'.tr()), |  | ||||||
|                     onTap: () { |  | ||||||
|                       visibility.value = 2; |  | ||||||
|                       Navigator.pop(context); |  | ||||||
|                     }, |  | ||||||
|                     selected: visibility.value == 2, |  | ||||||
|                   ), |  | ||||||
|                   ListTile( |  | ||||||
|                     leading: Icon(Symbols.lock), |  | ||||||
|                     title: Text('postVisibilityPrivate'.tr()), |  | ||||||
|                     onTap: () { |  | ||||||
|                       visibility.value = 3; |  | ||||||
|                       Navigator.pop(context); |  | ||||||
|                     }, |  | ||||||
|                     selected: visibility.value == 3, |  | ||||||
|                   ), |  | ||||||
|                 ], |  | ||||||
|               ), |  | ||||||
|             ), |             ), | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Helper method to get the appropriate icon for each visibility status |     void showKeyboardShortcutsDialog() { | ||||||
|     IconData getVisibilityIcon(int visibilityValue) { |       showDialog( | ||||||
|       switch (visibilityValue) { |         context: context, | ||||||
|         case 1: // Friends |         builder: | ||||||
|           return Symbols.group; |             (context) => AlertDialog( | ||||||
|         case 2: // Unlisted |               title: Text('keyboard_shortcuts'.tr()), | ||||||
|           return Symbols.link_off; |               content: Column( | ||||||
|         case 3: // Private |                 mainAxisSize: MainAxisSize.min, | ||||||
|           return Symbols.lock; |                 crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|         default: // Public (0) or unknown |                 children: [ | ||||||
|           return Symbols.public; |                   Text('Ctrl/Cmd + Enter: ${'submit'.tr()}'), | ||||||
|       } |                   Text('Ctrl/Cmd + V: ${'paste'.tr()}'), | ||||||
|  |                   Text('Ctrl/Cmd + I: ${'add_image'.tr()}'), | ||||||
|  |                   Text('Ctrl/Cmd + Shift + V: ${'add_video'.tr()}'), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |               actions: [ | ||||||
|  |                 TextButton( | ||||||
|  |                   onPressed: () => Navigator.of(context).pop(), | ||||||
|  |                   child: Text('close'.tr()), | ||||||
|  |                 ), | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Helper method to get the translation key for each visibility status |     Widget buildWideAttachmentGrid() { | ||||||
|     String getVisibilityText(int visibilityValue) { |       return GridView.builder( | ||||||
|       switch (visibilityValue) { |         shrinkWrap: true, | ||||||
|         case 1: // Friends |         physics: const NeverScrollableScrollPhysics(), | ||||||
|           return 'postVisibilityFriends'; |         gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( | ||||||
|         case 2: // Unlisted |           crossAxisCount: 3, | ||||||
|           return 'postVisibilityUnlisted'; |           crossAxisSpacing: 8, | ||||||
|         case 3: // Private |           mainAxisSpacing: 8, | ||||||
|           return 'postVisibilityPrivate'; |         ), | ||||||
|         default: // Public (0) or unknown |         itemCount: state.attachments.value.length, | ||||||
|           return 'postVisibilityPublic'; |         itemBuilder: (context, idx) { | ||||||
|       } |           return AttachmentPreview( | ||||||
|  |             item: state.attachments.value[idx], | ||||||
|  |             progress: state.attachmentProgress.value[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, | ||||||
|  |               ); | ||||||
|  |             }, | ||||||
|  |           ); | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     Widget buildNarrowAttachmentList() { | ||||||
|  |       return Column( | ||||||
|  |         children: [ | ||||||
|  |           for (var idx = 0; idx < state.attachments.value.length; idx++) | ||||||
|  |             Container( | ||||||
|  |               margin: const EdgeInsets.only(bottom: 8), | ||||||
|  |               child: AttachmentPreview( | ||||||
|  |                 item: state.attachments.value[idx], | ||||||
|  |                 progress: state.attachmentProgress.value[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, | ||||||
|  |                   ); | ||||||
|  |                 }, | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |         ], | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Build UI | ||||||
|     return AppScaffold( |     return AppScaffold( | ||||||
|       appBar: AppBar( |       appBar: AppBar( | ||||||
|         leading: const PageBackButton(), |         leading: const PageBackButton(), | ||||||
| @@ -338,53 +200,50 @@ class PostComposeScreen extends HookConsumerWidget { | |||||||
|                 ? Text(originalPost != null ? 'editPost'.tr() : 'newPost'.tr()) |                 ? Text(originalPost != null ? 'editPost'.tr() : 'newPost'.tr()) | ||||||
|                 : null, |                 : null, | ||||||
|         actions: [ |         actions: [ | ||||||
|  |           IconButton( | ||||||
|  |             icon: const Icon(Symbols.settings), | ||||||
|  |             onPressed: showSettingsSheet, | ||||||
|  |             tooltip: 'postSettings'.tr(), | ||||||
|  |           ), | ||||||
|           if (isWideScreen(context)) |           if (isWideScreen(context)) | ||||||
|             Tooltip( |             Tooltip( | ||||||
|               message: 'keyboard_shortcuts'.tr(), |               message: 'keyboard_shortcuts'.tr(), | ||||||
|               child: IconButton( |               child: IconButton( | ||||||
|                 icon: const Icon(Symbols.keyboard), |                 icon: const Icon(Symbols.keyboard), | ||||||
|                 onPressed: () { |                 onPressed: showKeyboardShortcutsDialog, | ||||||
|                   showDialog( |  | ||||||
|                     context: context, |  | ||||||
|                     builder: |  | ||||||
|                         (context) => AlertDialog( |  | ||||||
|                           title: Text('keyboard_shortcuts'.tr()), |  | ||||||
|                           content: Column( |  | ||||||
|                             mainAxisSize: MainAxisSize.min, |  | ||||||
|                             crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|                             children: [ |  | ||||||
|                               Text('Ctrl/Cmd + Enter: ${'submit'.tr()}'), |  | ||||||
|                               Text('Ctrl/Cmd + V: ${'paste'.tr()}'), |  | ||||||
|                               Text('Ctrl/Cmd + I: ${'add_image'.tr()}'), |  | ||||||
|                               Text('Ctrl/Cmd + Shift + V: ${'add_video'.tr()}'), |  | ||||||
|                             ], |  | ||||||
|                           ), |  | ||||||
|                           actions: [ |  | ||||||
|                             TextButton( |  | ||||||
|                               onPressed: () => Navigator.of(context).pop(), |  | ||||||
|                               child: Text('close'.tr()), |  | ||||||
|                             ), |  | ||||||
|                           ], |  | ||||||
|                         ), |  | ||||||
|                   ); |  | ||||||
|                 }, |  | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|           IconButton( |           ValueListenableBuilder<bool>( | ||||||
|             onPressed: submitting.value ? null : performAction, |             valueListenable: state.submitting, | ||||||
|             icon: |             builder: (context, submitting, _) { | ||||||
|                 submitting.value |               return IconButton( | ||||||
|                     ? SizedBox( |                 onPressed: | ||||||
|                       width: 28, |                     submitting | ||||||
|                       height: 28, |                         ? null | ||||||
|                       child: const CircularProgressIndicator( |                         : () => ComposeLogic.performAction( | ||||||
|                         color: Colors.white, |                           ref, | ||||||
|                         strokeWidth: 2.5, |                           state, | ||||||
|                       ), |                           context, | ||||||
|                     ).center() |                           originalPost: originalPost, | ||||||
|                     : originalPost != null |                           repliedPost: repliedPost, | ||||||
|                     ? const Icon(Symbols.edit) |                           forwardedPost: forwardedPost, | ||||||
|                     : const Icon(Symbols.upload), |                           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, | ||||||
|  |                         ), | ||||||
|  |               ); | ||||||
|  |             }, | ||||||
|           ), |           ), | ||||||
|           const Gap(8), |           const Gap(8), | ||||||
|         ], |         ], | ||||||
| @@ -392,59 +251,22 @@ class PostComposeScreen extends HookConsumerWidget { | |||||||
|       body: Column( |       body: Column( | ||||||
|         crossAxisAlignment: CrossAxisAlignment.start, |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|         children: [ |         children: [ | ||||||
|           if (repliedPost != null) |           // Reply/Forward info section | ||||||
|             Container( |           _buildInfoBanner(context), | ||||||
|               padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), |  | ||||||
|               color: Theme.of( |           // Main content area | ||||||
|                 context, |  | ||||||
|               ).colorScheme.surfaceVariant.withOpacity(0.5), |  | ||||||
|               child: Row( |  | ||||||
|                 children: [ |  | ||||||
|                   const Icon(Symbols.reply, size: 16), |  | ||||||
|                   const Gap(8), |  | ||||||
|                   Expanded( |  | ||||||
|                     child: Text( |  | ||||||
|                       '${'reply'.tr()}: ${repliedPost!.publisher.nick}', |  | ||||||
|                       style: Theme.of(context).textTheme.bodySmall, |  | ||||||
|                       maxLines: 1, |  | ||||||
|                       overflow: TextOverflow.ellipsis, |  | ||||||
|                     ), |  | ||||||
|                   ), |  | ||||||
|                 ], |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|           if (forwardedPost != null) |  | ||||||
|             Container( |  | ||||||
|               padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), |  | ||||||
|               color: Theme.of( |  | ||||||
|                 context, |  | ||||||
|               ).colorScheme.surfaceVariant.withOpacity(0.5), |  | ||||||
|               child: Row( |  | ||||||
|                 children: [ |  | ||||||
|                   const Icon(Symbols.forward, size: 16), |  | ||||||
|                   const Gap(8), |  | ||||||
|                   Expanded( |  | ||||||
|                     child: Text( |  | ||||||
|                       '${'forward'.tr()}: ${forwardedPost!.publisher.nick}', |  | ||||||
|                       style: Theme.of(context).textTheme.bodySmall, |  | ||||||
|                       maxLines: 1, |  | ||||||
|                       overflow: TextOverflow.ellipsis, |  | ||||||
|                     ), |  | ||||||
|                   ), |  | ||||||
|                 ], |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|           Expanded( |           Expanded( | ||||||
|             child: Row( |             child: Row( | ||||||
|               spacing: 12, |               spacing: 12, | ||||||
|               crossAxisAlignment: CrossAxisAlignment.start, |               crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|               children: [ |               children: [ | ||||||
|  |                 // Publisher profile picture | ||||||
|                 GestureDetector( |                 GestureDetector( | ||||||
|                   child: ProfilePictureWidget( |                   child: ProfilePictureWidget( | ||||||
|                     fileId: currentPublisher.value?.picture?.id, |                     fileId: state.currentPublisher.value?.picture?.id, | ||||||
|                     radius: 20, |                     radius: 20, | ||||||
|                     fallbackIcon: |                     fallbackIcon: | ||||||
|                         currentPublisher.value == null |                         state.currentPublisher.value == null | ||||||
|                             ? Symbols.question_mark |                             ? Symbols.question_mark | ||||||
|                             : null, |                             : null, | ||||||
|                   ), |                   ), | ||||||
| @@ -452,93 +274,43 @@ class PostComposeScreen extends HookConsumerWidget { | |||||||
|                     showModalBottomSheet( |                     showModalBottomSheet( | ||||||
|                       isScrollControlled: true, |                       isScrollControlled: true, | ||||||
|                       context: context, |                       context: context, | ||||||
|                       builder: (context) => PublisherModal(), |                       builder: (context) => const PublisherModal(), | ||||||
|                     ).then((value) { |                     ).then((value) { | ||||||
|                       if (value is SnPublisher) currentPublisher.value = value; |                       if (value != null) { | ||||||
|  |                         state.currentPublisher.value = value; | ||||||
|  |                       } | ||||||
|                     }); |                     }); | ||||||
|                   }, |                   }, | ||||||
|                 ).padding(top: 16), |                 ).padding(top: 16), | ||||||
|  |  | ||||||
|  |                 // Post content form | ||||||
|                 Expanded( |                 Expanded( | ||||||
|                   child: SingleChildScrollView( |                   child: SingleChildScrollView( | ||||||
|                     padding: EdgeInsets.symmetric(vertical: 16), |                     padding: const EdgeInsets.symmetric(vertical: 12), | ||||||
|                     child: Column( |                     child: Column( | ||||||
|                       crossAxisAlignment: CrossAxisAlignment.start, |                       crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|                       children: [ |                       children: [ | ||||||
|                         Row( |                         // Content field with borderless design | ||||||
|                           children: [ |  | ||||||
|                             OutlinedButton( |  | ||||||
|                               onPressed: () { |  | ||||||
|                                 showVisibilityModal(); |  | ||||||
|                               }, |  | ||||||
|                               style: OutlinedButton.styleFrom( |  | ||||||
|                                 shape: RoundedRectangleBorder( |  | ||||||
|                                   borderRadius: BorderRadius.circular(20), |  | ||||||
|                                 ), |  | ||||||
|                                 side: BorderSide( |  | ||||||
|                                   color: Theme.of( |  | ||||||
|                                     context, |  | ||||||
|                                   ).colorScheme.primary.withOpacity(0.5), |  | ||||||
|                                 ), |  | ||||||
|                                 padding: EdgeInsets.symmetric(horizontal: 16), |  | ||||||
|                                 visualDensity: const VisualDensity( |  | ||||||
|                                   vertical: -2, |  | ||||||
|                                   horizontal: -4, |  | ||||||
|                                 ), |  | ||||||
|                               ), |  | ||||||
|                               child: Row( |  | ||||||
|                                 mainAxisSize: MainAxisSize.min, |  | ||||||
|                                 children: [ |  | ||||||
|                                   Icon( |  | ||||||
|                                     getVisibilityIcon(visibility.value), |  | ||||||
|                                     size: 16, |  | ||||||
|                                     color: |  | ||||||
|                                         Theme.of(context).colorScheme.primary, |  | ||||||
|                                   ), |  | ||||||
|                                   const SizedBox(width: 6), |  | ||||||
|                                   Text( |  | ||||||
|                                     getVisibilityText(visibility.value).tr(), |  | ||||||
|                                     style: TextStyle( |  | ||||||
|                                       fontSize: 14, |  | ||||||
|                                       color: |  | ||||||
|                                           Theme.of(context).colorScheme.primary, |  | ||||||
|                                     ), |  | ||||||
|                                   ), |  | ||||||
|                                 ], |  | ||||||
|                               ), |  | ||||||
|                             ), |  | ||||||
|                           ], |  | ||||||
|                         ).padding(bottom: 6), |  | ||||||
|                         TextField( |  | ||||||
|                           controller: titleController, |  | ||||||
|                           decoration: InputDecoration.collapsed( |  | ||||||
|                             hintText: 'postTitle'.tr(), |  | ||||||
|                           ), |  | ||||||
|                           style: TextStyle(fontSize: 16), |  | ||||||
|                           onTapOutside: |  | ||||||
|                               (_) => |  | ||||||
|                                   FocusManager.instance.primaryFocus?.unfocus(), |  | ||||||
|                         ), |  | ||||||
|                         TextField( |  | ||||||
|                           controller: descriptionController, |  | ||||||
|                           decoration: InputDecoration.collapsed( |  | ||||||
|                             hintText: 'postDescription'.tr(), |  | ||||||
|                           ), |  | ||||||
|                           style: TextStyle(fontSize: 16), |  | ||||||
|                           onTapOutside: |  | ||||||
|                               (_) => |  | ||||||
|                                   FocusManager.instance.primaryFocus?.unfocus(), |  | ||||||
|                         ), |  | ||||||
|                         const Gap(8), |  | ||||||
|                         RawKeyboardListener( |                         RawKeyboardListener( | ||||||
|                           focusNode: FocusNode(), |                           focusNode: FocusNode(), | ||||||
|                           onKey: handleKeyPress, |                           onKey: | ||||||
|  |                               (event) => ComposeLogic.handleKeyPress( | ||||||
|  |                                 event, | ||||||
|  |                                 state, | ||||||
|  |                                 ref, | ||||||
|  |                                 context, | ||||||
|  |                                 originalPost: originalPost, | ||||||
|  |                                 repliedPost: repliedPost, | ||||||
|  |                                 forwardedPost: forwardedPost, | ||||||
|  |                                 postType: 0, // Regular post type | ||||||
|  |                               ), | ||||||
|                           child: TextField( |                           child: TextField( | ||||||
|                             controller: contentController, |                             controller: state.contentController, | ||||||
|                             style: TextStyle(fontSize: 14), |                             style: theme.textTheme.bodyMedium, | ||||||
|                             decoration: InputDecoration( |                             decoration: InputDecoration( | ||||||
|                               border: InputBorder.none, |                               border: InputBorder.none, | ||||||
|                               hintText: 'postPlaceholder'.tr(), |                               hintText: 'postContent'.tr(), | ||||||
|                               isDense: true, |                               contentPadding: const EdgeInsets.all(8), | ||||||
|                             ), |                             ), | ||||||
|                             maxLines: null, |                             maxLines: null, | ||||||
|                             onTapOutside: |                             onTapOutside: | ||||||
| @@ -547,81 +319,16 @@ class PostComposeScreen extends HookConsumerWidget { | |||||||
|                                         ?.unfocus(), |                                         ?.unfocus(), | ||||||
|                           ), |                           ), | ||||||
|                         ), |                         ), | ||||||
|  |  | ||||||
|                         const Gap(8), |                         const Gap(8), | ||||||
|  |  | ||||||
|  |                         // Attachments preview | ||||||
|                         LayoutBuilder( |                         LayoutBuilder( | ||||||
|                           builder: (context, constraints) { |                           builder: (context, constraints) { | ||||||
|                             final isWide = isWideScreen(context); |                             final isWide = isWideScreen(context); | ||||||
|                             return isWide |                             return isWide | ||||||
|                                 ? Wrap( |                                 ? buildWideAttachmentGrid() | ||||||
|                                   spacing: 8, |                                 : buildNarrowAttachmentList(); | ||||||
|                                   runSpacing: 8, |  | ||||||
|                                   children: [ |  | ||||||
|                                     for ( |  | ||||||
|                                       var idx = 0; |  | ||||||
|                                       idx < attachments.value.length; |  | ||||||
|                                       idx++ |  | ||||||
|                                     ) |  | ||||||
|                                       SizedBox( |  | ||||||
|                                         width: constraints.maxWidth / 2 - 4, |  | ||||||
|                                         child: AttachmentPreview( |  | ||||||
|                                           item: attachments.value[idx], |  | ||||||
|                                           progress: |  | ||||||
|                                               attachmentProgress.value[idx], |  | ||||||
|                                           onRequestUpload: |  | ||||||
|                                               () => uploadAttachment(idx), |  | ||||||
|                                           onDelete: () => deleteAttachment(idx), |  | ||||||
|                                           onMove: (delta) { |  | ||||||
|                                             if (idx + delta < 0 || |  | ||||||
|                                                 idx + delta >= |  | ||||||
|                                                     attachments.value.length) { |  | ||||||
|                                               return; |  | ||||||
|                                             } |  | ||||||
|                                             final clone = List.of( |  | ||||||
|                                               attachments.value, |  | ||||||
|                                             ); |  | ||||||
|                                             clone.insert( |  | ||||||
|                                               idx + delta, |  | ||||||
|                                               clone.removeAt(idx), |  | ||||||
|                                             ); |  | ||||||
|                                             attachments.value = clone; |  | ||||||
|                                           }, |  | ||||||
|                                         ), |  | ||||||
|                                       ), |  | ||||||
|                                   ], |  | ||||||
|                                 ) |  | ||||||
|                                 : Column( |  | ||||||
|                                   crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|                                   spacing: 8, |  | ||||||
|                                   children: [ |  | ||||||
|                                     for ( |  | ||||||
|                                       var idx = 0; |  | ||||||
|                                       idx < attachments.value.length; |  | ||||||
|                                       idx++ |  | ||||||
|                                     ) |  | ||||||
|                                       AttachmentPreview( |  | ||||||
|                                         item: attachments.value[idx], |  | ||||||
|                                         progress: attachmentProgress.value[idx], |  | ||||||
|                                         onRequestUpload: |  | ||||||
|                                             () => uploadAttachment(idx), |  | ||||||
|                                         onDelete: () => deleteAttachment(idx), |  | ||||||
|                                         onMove: (delta) { |  | ||||||
|                                           if (idx + delta < 0 || |  | ||||||
|                                               idx + delta >= |  | ||||||
|                                                   attachments.value.length) { |  | ||||||
|                                             return; |  | ||||||
|                                           } |  | ||||||
|                                           final clone = List.of( |  | ||||||
|                                             attachments.value, |  | ||||||
|                                           ); |  | ||||||
|                                           clone.insert( |  | ||||||
|                                             idx + delta, |  | ||||||
|                                             clone.removeAt(idx), |  | ||||||
|                                           ); |  | ||||||
|                                           attachments.value = clone; |  | ||||||
|                                         }, |  | ||||||
|                                       ), |  | ||||||
|                                   ], |  | ||||||
|                                 ); |  | ||||||
|                           }, |                           }, | ||||||
|                         ), |                         ), | ||||||
|                       ], |                       ], | ||||||
| @@ -631,19 +338,21 @@ class PostComposeScreen extends HookConsumerWidget { | |||||||
|               ], |               ], | ||||||
|             ).padding(horizontal: 16), |             ).padding(horizontal: 16), | ||||||
|           ), |           ), | ||||||
|  |  | ||||||
|  |           // Bottom toolbar | ||||||
|           Material( |           Material( | ||||||
|             elevation: 4, |             elevation: 4, | ||||||
|             child: Row( |             child: Row( | ||||||
|               children: [ |               children: [ | ||||||
|                 IconButton( |                 IconButton( | ||||||
|                   onPressed: pickPhotoMedia, |                   onPressed: () => ComposeLogic.pickPhotoMedia(ref, state), | ||||||
|                   icon: const Icon(Symbols.add_a_photo), |                   icon: const Icon(Symbols.add_a_photo), | ||||||
|                   color: Theme.of(context).colorScheme.primary, |                   color: colorScheme.primary, | ||||||
|                 ), |                 ), | ||||||
|                 IconButton( |                 IconButton( | ||||||
|                   onPressed: pickVideoMedia, |                   onPressed: () => ComposeLogic.pickVideoMedia(ref, state), | ||||||
|                   icon: const Icon(Symbols.videocam), |                   icon: const Icon(Symbols.videocam), | ||||||
|                   color: Theme.of(context).colorScheme.primary, |                   color: colorScheme.primary, | ||||||
|                 ), |                 ), | ||||||
|               ], |               ], | ||||||
|             ).padding( |             ).padding( | ||||||
| @@ -656,4 +365,37 @@ class PostComposeScreen extends HookConsumerWidget { | |||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   Widget _buildInfoBanner(BuildContext context) { | ||||||
|  |     if (originalPost != null) { | ||||||
|  |       return Container( | ||||||
|  |         width: double.infinity, | ||||||
|  |         color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||||
|  |         child: Column( | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |           children: [ | ||||||
|  |             Row( | ||||||
|  |               children: [ | ||||||
|  |                 Icon( | ||||||
|  |                   repliedPost != null ? Symbols.reply : Symbols.forward, | ||||||
|  |                   size: 16, | ||||||
|  |                 ), | ||||||
|  |                 const Gap(4), | ||||||
|  |                 Text( | ||||||
|  |                   repliedPost != null | ||||||
|  |                       ? 'postReplyingTo'.tr() | ||||||
|  |                       : 'postForwardingTo'.tr(), | ||||||
|  |                   style: Theme.of(context).textTheme.labelMedium, | ||||||
|  |                 ), | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |             const Gap(8), | ||||||
|  |             PostItem(item: originalPost!, isOpenable: false), | ||||||
|  |           ], | ||||||
|  |         ).padding(all: 16), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return const SizedBox.shrink(); | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										401
									
								
								lib/screens/posts/compose_article.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										401
									
								
								lib/screens/posts/compose_article.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,401 @@ | |||||||
|  | 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:gap/gap.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  |  | ||||||
|  | import 'package:island/models/post.dart'; | ||||||
|  | import 'package:island/screens/creators/publishers.dart'; | ||||||
|  | import 'package:island/services/responsive.dart'; | ||||||
|  |  | ||||||
|  | import 'package:island/widgets/app_scaffold.dart'; | ||||||
|  | import 'package:island/screens/posts/detail.dart'; | ||||||
|  | import 'package:island/widgets/content/attachment_preview.dart'; | ||||||
|  | import 'package:island/widgets/post/compose_shared.dart'; | ||||||
|  | import 'package:island/widgets/post/publishers_modal.dart'; | ||||||
|  | import 'package:island/widgets/content/cloud_files.dart'; | ||||||
|  | import 'package:island/widgets/post/compose_settings_sheet.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}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     final post = ref.watch(postProvider(id)); | ||||||
|  |     return post.when( | ||||||
|  |       data: (post) => ArticleComposeScreen(originalPost: post), | ||||||
|  |       loading: | ||||||
|  |           () => AppScaffold( | ||||||
|  |             appBar: AppBar(leading: const PageBackButton()), | ||||||
|  |             body: const Center(child: CircularProgressIndicator()), | ||||||
|  |           ), | ||||||
|  |       error: | ||||||
|  |           (e, _) => AppScaffold( | ||||||
|  |             appBar: AppBar(leading: const PageBackButton()), | ||||||
|  |             body: Text('Error: $e', textAlign: TextAlign.center), | ||||||
|  |           ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @RoutePage() | ||||||
|  | class ArticleComposeScreen extends HookConsumerWidget { | ||||||
|  |   final SnPost? originalPost; | ||||||
|  |  | ||||||
|  |   const ArticleComposeScreen({super.key, this.originalPost}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     final theme = Theme.of(context); | ||||||
|  |     final colorScheme = theme.colorScheme; | ||||||
|  |  | ||||||
|  |     final publishers = ref.watch(publishersManagedProvider); | ||||||
|  |     final state = useMemoized( | ||||||
|  |       () => ComposeLogic.createState(originalPost: originalPost), | ||||||
|  |       [originalPost], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     final showPreview = useState(false); | ||||||
|  |  | ||||||
|  |     // Initialize publisher once when data is available | ||||||
|  |     useEffect(() { | ||||||
|  |       if (publishers.value?.isNotEmpty ?? false) { | ||||||
|  |         state.currentPublisher.value = publishers.value!.first; | ||||||
|  |       } | ||||||
|  |       return null; | ||||||
|  |     }, [publishers]); | ||||||
|  |  | ||||||
|  |     // Dispose state when widget is disposed | ||||||
|  |     useEffect(() { | ||||||
|  |       return () => ComposeLogic.dispose(state); | ||||||
|  |     }, []); | ||||||
|  |  | ||||||
|  |     // Helper methods | ||||||
|  |     void showSettingsSheet() { | ||||||
|  |       showModalBottomSheet( | ||||||
|  |         context: context, | ||||||
|  |         isScrollControlled: true, | ||||||
|  |         builder: | ||||||
|  |             (context) => ComposeSettingsSheet( | ||||||
|  |               titleController: state.titleController, | ||||||
|  |               descriptionController: state.descriptionController, | ||||||
|  |               visibility: state.visibility, | ||||||
|  |               onVisibilityChanged: () { | ||||||
|  |                 // Trigger rebuild if needed | ||||||
|  |               }, | ||||||
|  |             ), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     void showKeyboardShortcutsDialog() { | ||||||
|  |       showDialog( | ||||||
|  |         context: context, | ||||||
|  |         builder: | ||||||
|  |             (context) => AlertDialog( | ||||||
|  |               title: Text('keyboard_shortcuts'.tr()), | ||||||
|  |               content: Column( | ||||||
|  |                 mainAxisSize: MainAxisSize.min, | ||||||
|  |                 crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                 children: [ | ||||||
|  |                   Text('Ctrl/Cmd + Enter: ${'submit'.tr()}'), | ||||||
|  |                   Text('Ctrl/Cmd + V: ${'paste'.tr()}'), | ||||||
|  |                   Text('Ctrl/Cmd + I: ${'add_image'.tr()}'), | ||||||
|  |                   Text('Ctrl/Cmd + Shift + V: ${'add_video'.tr()}'), | ||||||
|  |                   Text('Ctrl/Cmd + P: ${'toggle_preview'.tr()}'), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |               actions: [ | ||||||
|  |                 TextButton( | ||||||
|  |                   onPressed: () => Navigator.of(context).pop(), | ||||||
|  |                   child: Text('close'.tr()), | ||||||
|  |                 ), | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Widget buildPreviewPane() { | ||||||
|  |       return Container( | ||||||
|  |         decoration: BoxDecoration( | ||||||
|  |           border: Border.all(color: colorScheme.outline.withOpacity(0.3)), | ||||||
|  |           borderRadius: BorderRadius.circular(8), | ||||||
|  |         ), | ||||||
|  |         child: Column( | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |           children: [ | ||||||
|  |             Container( | ||||||
|  |               padding: const EdgeInsets.all(16), | ||||||
|  |               decoration: BoxDecoration( | ||||||
|  |                 color: colorScheme.surfaceVariant.withOpacity(0.3), | ||||||
|  |                 borderRadius: const BorderRadius.only( | ||||||
|  |                   topLeft: Radius.circular(8), | ||||||
|  |                   topRight: Radius.circular(8), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |               child: Row( | ||||||
|  |                 children: [ | ||||||
|  |                   Icon(Symbols.preview, size: 20), | ||||||
|  |                   const Gap(8), | ||||||
|  |                   Text('preview'.tr(), style: theme.textTheme.titleMedium), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |             Expanded( | ||||||
|  |               child: SingleChildScrollView( | ||||||
|  |                 padding: const EdgeInsets.all(16), | ||||||
|  |                 child: Column( | ||||||
|  |                   crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                   children: [ | ||||||
|  |                     if (state.titleController.text.isNotEmpty) ...[ | ||||||
|  |                       Text( | ||||||
|  |                         state.titleController.text, | ||||||
|  |                         style: theme.textTheme.headlineSmall?.copyWith( | ||||||
|  |                           fontWeight: FontWeight.bold, | ||||||
|  |                         ), | ||||||
|  |                       ), | ||||||
|  |                       const Gap(16), | ||||||
|  |                     ], | ||||||
|  |                     if (state.descriptionController.text.isNotEmpty) ...[ | ||||||
|  |                       Text( | ||||||
|  |                         state.descriptionController.text, | ||||||
|  |                         style: theme.textTheme.bodyLarge?.copyWith( | ||||||
|  |                           color: colorScheme.onSurface.withOpacity(0.7), | ||||||
|  |                         ), | ||||||
|  |                       ), | ||||||
|  |                       const Gap(16), | ||||||
|  |                     ], | ||||||
|  |                     if (state.contentController.text.isNotEmpty) | ||||||
|  |                       Text( | ||||||
|  |                         state.contentController.text, | ||||||
|  |                         style: theme.textTheme.bodyMedium, | ||||||
|  |                       ), | ||||||
|  |                   ], | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Widget buildEditorPane() { | ||||||
|  |       return Column( | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |         children: [ | ||||||
|  |           // Publisher row | ||||||
|  |           Card( | ||||||
|  |             elevation: 1, | ||||||
|  |             child: Padding( | ||||||
|  |               padding: const EdgeInsets.all(12), | ||||||
|  |               child: Row( | ||||||
|  |                 children: [ | ||||||
|  |                   GestureDetector( | ||||||
|  |                     child: ProfilePictureWidget( | ||||||
|  |                       fileId: state.currentPublisher.value?.picture?.id, | ||||||
|  |                       radius: 20, | ||||||
|  |                       fallbackIcon: | ||||||
|  |                           state.currentPublisher.value == null | ||||||
|  |                               ? Symbols.question_mark | ||||||
|  |                               : null, | ||||||
|  |                     ), | ||||||
|  |                     onTap: () { | ||||||
|  |                       showModalBottomSheet( | ||||||
|  |                         isScrollControlled: true, | ||||||
|  |                         context: context, | ||||||
|  |                         builder: (context) => const PublisherModal(), | ||||||
|  |                       ).then((value) { | ||||||
|  |                         if (value != null) { | ||||||
|  |                           state.currentPublisher.value = value; | ||||||
|  |                         } | ||||||
|  |                       }); | ||||||
|  |                     }, | ||||||
|  |                   ), | ||||||
|  |                   const Gap(12), | ||||||
|  |                   Text( | ||||||
|  |                     state.currentPublisher.value?.name ?? | ||||||
|  |                         'postPublisherUnselected'.tr(), | ||||||
|  |                     style: theme.textTheme.bodyMedium, | ||||||
|  |                   ), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |  | ||||||
|  |           // Content field with keyboard listener | ||||||
|  |           Expanded( | ||||||
|  |             child: RawKeyboardListener( | ||||||
|  |               focusNode: FocusNode(), | ||||||
|  |               onKey: | ||||||
|  |                   (event) => ComposeLogic.handleKeyPress( | ||||||
|  |                     event, | ||||||
|  |                     state, | ||||||
|  |                     ref, | ||||||
|  |                     context, | ||||||
|  |                     originalPost: originalPost, | ||||||
|  |                     postType: 1, // Article type | ||||||
|  |                   ), | ||||||
|  |               child: TextField( | ||||||
|  |                 controller: state.contentController, | ||||||
|  |                 style: theme.textTheme.bodyMedium, | ||||||
|  |                 decoration: InputDecoration( | ||||||
|  |                   border: InputBorder.none, | ||||||
|  |                   hintText: 'postContent'.tr(), | ||||||
|  |                   contentPadding: const EdgeInsets.all(8), | ||||||
|  |                 ), | ||||||
|  |                 maxLines: null, | ||||||
|  |                 expands: true, | ||||||
|  |                 textAlignVertical: TextAlignVertical.top, | ||||||
|  |                 onTapOutside: | ||||||
|  |                     (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |  | ||||||
|  |           // Attachments preview | ||||||
|  |           if (state.attachments.value.isNotEmpty) ...[ | ||||||
|  |             const Gap(16), | ||||||
|  |             Wrap( | ||||||
|  |               spacing: 8, | ||||||
|  |               runSpacing: 8, | ||||||
|  |               children: [ | ||||||
|  |                 for (var idx = 0; idx < state.attachments.value.length; idx++) | ||||||
|  |                   SizedBox( | ||||||
|  |                     width: 120, | ||||||
|  |                     height: 120, | ||||||
|  |                     child: AttachmentPreview( | ||||||
|  |                       item: state.attachments.value[idx], | ||||||
|  |                       progress: state.attachmentProgress.value[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, | ||||||
|  |                         ); | ||||||
|  |                       }, | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ], | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return AppScaffold( | ||||||
|  |       appBar: AppBar( | ||||||
|  |         leading: const PageBackButton(), | ||||||
|  |         actions: [ | ||||||
|  |           IconButton( | ||||||
|  |             icon: const Icon(Symbols.settings), | ||||||
|  |             onPressed: showSettingsSheet, | ||||||
|  |             tooltip: 'postSettings'.tr(), | ||||||
|  |           ), | ||||||
|  |           Tooltip( | ||||||
|  |             message: 'togglePreview'.tr(), | ||||||
|  |             child: IconButton( | ||||||
|  |               icon: Icon(showPreview.value ? Symbols.edit : Symbols.preview), | ||||||
|  |               onPressed: () => showPreview.value = !showPreview.value, | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |           if (isWideScreen(context)) | ||||||
|  |             Tooltip( | ||||||
|  |               message: 'keyboard_shortcuts'.tr(), | ||||||
|  |               child: IconButton( | ||||||
|  |                 icon: const Icon(Symbols.keyboard), | ||||||
|  |                 onPressed: showKeyboardShortcutsDialog, | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ValueListenableBuilder<bool>( | ||||||
|  |             valueListenable: state.submitting, | ||||||
|  |             builder: (context, submitting, _) { | ||||||
|  |               return IconButton( | ||||||
|  |                 onPressed: | ||||||
|  |                     submitting | ||||||
|  |                         ? null | ||||||
|  |                         : () => ComposeLogic.performAction( | ||||||
|  |                           ref, | ||||||
|  |                           state, | ||||||
|  |                           context, | ||||||
|  |                           originalPost: originalPost, | ||||||
|  |                           postType: 1, // Article 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, | ||||||
|  |                         ), | ||||||
|  |               ); | ||||||
|  |             }, | ||||||
|  |           ), | ||||||
|  |           const Gap(8), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |       body: Column( | ||||||
|  |         children: [ | ||||||
|  |           Expanded( | ||||||
|  |             child: Padding( | ||||||
|  |               padding: const EdgeInsets.all(16), | ||||||
|  |               child: | ||||||
|  |                   isWideScreen(context) | ||||||
|  |                       ? Row( | ||||||
|  |                         spacing: 16, | ||||||
|  |                         children: [ | ||||||
|  |                           Expanded( | ||||||
|  |                             flex: showPreview.value ? 1 : 2, | ||||||
|  |                             child: buildEditorPane(), | ||||||
|  |                           ), | ||||||
|  |                           if (showPreview.value) | ||||||
|  |                             Expanded(child: buildPreviewPane()), | ||||||
|  |                         ], | ||||||
|  |                       ) | ||||||
|  |                       : showPreview.value | ||||||
|  |                       ? buildPreviewPane() | ||||||
|  |                       : buildEditorPane(), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |  | ||||||
|  |           // Bottom toolbar | ||||||
|  |           Material( | ||||||
|  |             elevation: 4, | ||||||
|  |             child: Row( | ||||||
|  |               children: [ | ||||||
|  |                 IconButton( | ||||||
|  |                   onPressed: () => ComposeLogic.pickPhotoMedia(ref, state), | ||||||
|  |                   icon: const Icon(Symbols.add_a_photo), | ||||||
|  |                   color: colorScheme.primary, | ||||||
|  |                 ), | ||||||
|  |                 IconButton( | ||||||
|  |                   onPressed: () => ComposeLogic.pickVideoMedia(ref, state), | ||||||
|  |                   icon: const Icon(Symbols.videocam), | ||||||
|  |                   color: colorScheme.primary, | ||||||
|  |                 ), | ||||||
|  |               ], | ||||||
|  |             ).padding( | ||||||
|  |               bottom: MediaQuery.of(context).padding.bottom + 16, | ||||||
|  |               horizontal: 16, | ||||||
|  |               top: 8, | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,236 +0,0 @@ | |||||||
| // ignore_for_file: implementation_imports, invalid_use_of_internal_member |  | ||||||
|  |  | ||||||
| import 'package:flutter/material.dart'; |  | ||||||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; |  | ||||||
| import 'package:riverpod_paging_utils/src/paging_data.dart'; |  | ||||||
| import 'package:riverpod_paging_utils/src/paging_helper_view_theme.dart'; |  | ||||||
| import 'package:riverpod_paging_utils/src/paging_notifier_mixin.dart'; |  | ||||||
| import 'package:visibility_detector/visibility_detector.dart'; |  | ||||||
|  |  | ||||||
| /// A generic widget for pagination. |  | ||||||
| /// |  | ||||||
| /// Main features: |  | ||||||
| /// 1. Displays the widget created by [contentBuilder] when data is available. |  | ||||||
| /// 2. Shows a CircularProgressIndicator while loading the first page. |  | ||||||
| /// 3. Displays an error widget when there is an error on the first page. |  | ||||||
| /// 4. Shows error messages using a SnackBar. |  | ||||||
| /// 5. Loads the next page when the last item is displayed. |  | ||||||
| /// 6. Supports pull-to-refresh functionality. |  | ||||||
| /// |  | ||||||
| /// You can customize the appearance of the loading view, error view, and endItemView using [PagingHelperViewTheme]. |  | ||||||
| final class PagingHelperSliverView<D extends PagingData<I>, I> |  | ||||||
|     extends ConsumerWidget { |  | ||||||
|   const PagingHelperSliverView({ |  | ||||||
|     required this.provider, |  | ||||||
|     required this.futureRefreshable, |  | ||||||
|     required this.notifierRefreshable, |  | ||||||
|     required this.contentBuilder, |  | ||||||
|     this.showSecondPageError = true, |  | ||||||
|     super.key, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   final ProviderListenable<AsyncValue<D>> provider; |  | ||||||
|   final Refreshable<Future<D>> futureRefreshable; |  | ||||||
|   final Refreshable<PagingNotifierMixin<D, I>> notifierRefreshable; |  | ||||||
|  |  | ||||||
|   /// Specifies a function that returns a widget to display when data is available. |  | ||||||
|   /// endItemView is a widget to detect when the last displayed item is visible. |  | ||||||
|   /// If endItemView is non-null, it is displayed at the end of the list. |  | ||||||
|   final Widget Function(D data, int widgetCount, Widget endItemView) |  | ||||||
|   contentBuilder; |  | ||||||
|  |  | ||||||
|   final bool showSecondPageError; |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |  | ||||||
|     final theme = Theme.of(context).extension<PagingHelperViewTheme>(); |  | ||||||
|  |  | ||||||
|     final loadingBuilder = |  | ||||||
|         theme?.loadingViewBuilder ?? |  | ||||||
|         (context) => SliverFillRemaining( |  | ||||||
|           child: const Center(child: CircularProgressIndicator()), |  | ||||||
|         ); |  | ||||||
|     final errorBuilder = |  | ||||||
|         theme?.errorViewBuilder ?? |  | ||||||
|         (context, e, st, onPressed) => SliverFillRemaining( |  | ||||||
|           child: Center( |  | ||||||
|             child: Column( |  | ||||||
|               mainAxisSize: MainAxisSize.min, |  | ||||||
|               children: [ |  | ||||||
|                 IconButton( |  | ||||||
|                   onPressed: onPressed, |  | ||||||
|                   icon: const Icon(Icons.refresh), |  | ||||||
|                 ), |  | ||||||
|                 Text(e.toString()), |  | ||||||
|               ], |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|     return ref |  | ||||||
|         .watch(provider) |  | ||||||
|         .whenIgnorableError( |  | ||||||
|           data: ( |  | ||||||
|             data, { |  | ||||||
|             required hasError, |  | ||||||
|             required isLoading, |  | ||||||
|             required error, |  | ||||||
|           }) { |  | ||||||
|             final content = contentBuilder( |  | ||||||
|               data, |  | ||||||
|               // Add 1 to the length to include the endItemView |  | ||||||
|               data.items.length + 1, |  | ||||||
|               switch ((data.hasMore, hasError, isLoading)) { |  | ||||||
|                 // Display a widget to detect when the last element is reached |  | ||||||
|                 // if there are more pages and no errors |  | ||||||
|                 (true, false, _) => _EndVDLoadingItemView( |  | ||||||
|                   onScrollEnd: |  | ||||||
|                       () async => ref.read(notifierRefreshable).loadNext(), |  | ||||||
|                 ), |  | ||||||
|                 (true, true, false) when showSecondPageError => |  | ||||||
|                   _EndErrorItemView( |  | ||||||
|                     error: error, |  | ||||||
|                     onRetryButtonPressed: |  | ||||||
|                         () async => ref.read(notifierRefreshable).loadNext(), |  | ||||||
|                   ), |  | ||||||
|                 (true, true, true) => const _EndLoadingItemView(), |  | ||||||
|                 _ => const SizedBox.shrink(), |  | ||||||
|               }, |  | ||||||
|             ); |  | ||||||
|  |  | ||||||
|             return content; |  | ||||||
|           }, |  | ||||||
|           // Loading state for the first page |  | ||||||
|           loading: () => loadingBuilder(context), |  | ||||||
|           // Error state for the first page |  | ||||||
|           error: |  | ||||||
|               (e, st) => errorBuilder( |  | ||||||
|                 context, |  | ||||||
|                 e, |  | ||||||
|                 st, |  | ||||||
|                 () => ref.read(notifierRefreshable).forceRefresh(), |  | ||||||
|               ), |  | ||||||
|           // Prioritize data for errors on the second page and beyond |  | ||||||
|           skipErrorOnHasValue: true, |  | ||||||
|         ); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| final class _EndLoadingItemView extends StatelessWidget { |  | ||||||
|   const _EndLoadingItemView(); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     final theme = Theme.of(context).extension<PagingHelperViewTheme>(); |  | ||||||
|     final childBuilder = |  | ||||||
|         theme?.endLoadingViewBuilder ?? |  | ||||||
|         (context) => const Center( |  | ||||||
|           child: Padding( |  | ||||||
|             padding: EdgeInsets.all(16), |  | ||||||
|             child: CircularProgressIndicator(), |  | ||||||
|           ), |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|     return childBuilder(context); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| final class _EndVDLoadingItemView extends StatelessWidget { |  | ||||||
|   const _EndVDLoadingItemView({required this.onScrollEnd}); |  | ||||||
|   final VoidCallback onScrollEnd; |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     return VisibilityDetector( |  | ||||||
|       key: key ?? const Key('EndItem'), |  | ||||||
|       onVisibilityChanged: (info) { |  | ||||||
|         if (info.visibleFraction > 0.1) { |  | ||||||
|           onScrollEnd(); |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|       child: const _EndLoadingItemView(), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| final class _EndErrorItemView extends StatelessWidget { |  | ||||||
|   const _EndErrorItemView({ |  | ||||||
|     required this.error, |  | ||||||
|     required this.onRetryButtonPressed, |  | ||||||
|   }); |  | ||||||
|   final Object? error; |  | ||||||
|   final VoidCallback onRetryButtonPressed; |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     final theme = Theme.of(context).extension<PagingHelperViewTheme>(); |  | ||||||
|     final childBuilder = |  | ||||||
|         theme?.endErrorViewBuilder ?? |  | ||||||
|         (context, e, onPressed) => Center( |  | ||||||
|           child: Padding( |  | ||||||
|             padding: const EdgeInsets.all(16), |  | ||||||
|             child: Column( |  | ||||||
|               children: [ |  | ||||||
|                 IconButton( |  | ||||||
|                   onPressed: onPressed, |  | ||||||
|                   icon: const Icon(Icons.refresh), |  | ||||||
|                 ), |  | ||||||
|                 Text(error.toString()), |  | ||||||
|               ], |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|     return childBuilder(context, error, onRetryButtonPressed); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| extension _AsyncValueX<T> on AsyncValue<T> { |  | ||||||
|   /// Extends the [when] method to handle async data states more effectively, |  | ||||||
|   /// especially when maintaining data integrity despite errors. |  | ||||||
|   /// |  | ||||||
|   /// Use `skipErrorOnHasValue` to retain and display existing data |  | ||||||
|   /// even if subsequent fetch attempts result in errors, |  | ||||||
|   /// ideal for maintaining a seamless user experience. |  | ||||||
|   R whenIgnorableError<R>({ |  | ||||||
|     required R Function( |  | ||||||
|       T data, { |  | ||||||
|       required bool hasError, |  | ||||||
|       required bool isLoading, |  | ||||||
|       required Object? error, |  | ||||||
|     }) |  | ||||||
|     data, |  | ||||||
|     required R Function(Object error, StackTrace stackTrace) error, |  | ||||||
|     required R Function() loading, |  | ||||||
|     bool skipLoadingOnReload = false, |  | ||||||
|     bool skipLoadingOnRefresh = true, |  | ||||||
|     bool skipError = false, |  | ||||||
|     bool skipErrorOnHasValue = false, |  | ||||||
|   }) { |  | ||||||
|     if (skipErrorOnHasValue) { |  | ||||||
|       if (hasValue && hasError) { |  | ||||||
|         return data( |  | ||||||
|           requireValue, |  | ||||||
|           hasError: true, |  | ||||||
|           isLoading: isLoading, |  | ||||||
|           error: this.error, |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return when( |  | ||||||
|       skipLoadingOnReload: skipLoadingOnReload, |  | ||||||
|       skipLoadingOnRefresh: skipLoadingOnRefresh, |  | ||||||
|       skipError: skipError, |  | ||||||
|       data: |  | ||||||
|           (d) => data( |  | ||||||
|             d, |  | ||||||
|             hasError: hasError, |  | ||||||
|             isLoading: isLoading, |  | ||||||
|             error: this.error, |  | ||||||
|           ), |  | ||||||
|       error: error, |  | ||||||
|       loading: loading, |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										178
									
								
								lib/widgets/post/compose_settings_sheet.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								lib/widgets/post/compose_settings_sheet.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,178 @@ | |||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
|  | import 'package:island/widgets/content/sheet.dart'; | ||||||
|  | import 'package:material_symbols_icons/symbols.dart'; | ||||||
|  |  | ||||||
|  | class ComposeSettingsSheet extends HookWidget { | ||||||
|  |   final TextEditingController titleController; | ||||||
|  |   final TextEditingController descriptionController; | ||||||
|  |   final ValueNotifier<int> visibility; | ||||||
|  |   final VoidCallback? onVisibilityChanged; | ||||||
|  |  | ||||||
|  |   const ComposeSettingsSheet({ | ||||||
|  |     super.key, | ||||||
|  |     required this.titleController, | ||||||
|  |     required this.descriptionController, | ||||||
|  |     required this.visibility, | ||||||
|  |     this.onVisibilityChanged, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     final theme = Theme.of(context); | ||||||
|  |     final colorScheme = theme.colorScheme; | ||||||
|  |  | ||||||
|  |     IconData getVisibilityIcon(int visibilityValue) { | ||||||
|  |       switch (visibilityValue) { | ||||||
|  |         case 1: | ||||||
|  |           return Symbols.group; | ||||||
|  |         case 2: | ||||||
|  |           return Symbols.link_off; | ||||||
|  |         case 3: | ||||||
|  |           return Symbols.lock; | ||||||
|  |         default: | ||||||
|  |           return Symbols.public; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     String getVisibilityText(int visibilityValue) { | ||||||
|  |       switch (visibilityValue) { | ||||||
|  |         case 1: | ||||||
|  |           return 'postVisibilityFriends'; | ||||||
|  |         case 2: | ||||||
|  |           return 'postVisibilityUnlisted'; | ||||||
|  |         case 3: | ||||||
|  |           return 'postVisibilityPrivate'; | ||||||
|  |         default: | ||||||
|  |           return 'postVisibilityPublic'; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Widget buildVisibilityOption( | ||||||
|  |       BuildContext context, | ||||||
|  |       int value, | ||||||
|  |       IconData icon, | ||||||
|  |       String textKey, | ||||||
|  |     ) { | ||||||
|  |       return ListTile( | ||||||
|  |         leading: Icon(icon), | ||||||
|  |         title: Text(textKey.tr()), | ||||||
|  |         onTap: () { | ||||||
|  |           visibility.value = value; | ||||||
|  |           onVisibilityChanged?.call(); | ||||||
|  |           Navigator.pop(context); | ||||||
|  |         }, | ||||||
|  |         selected: visibility.value == value, | ||||||
|  |         contentPadding: const EdgeInsets.symmetric(horizontal: 20), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     void showVisibilitySheet() { | ||||||
|  |       showModalBottomSheet( | ||||||
|  |         context: context, | ||||||
|  |         builder: (context) => SheetScaffold( | ||||||
|  |           titleText: 'postVisibility'.tr(), | ||||||
|  |           child: Column( | ||||||
|  |             mainAxisSize: MainAxisSize.min, | ||||||
|  |             children: [ | ||||||
|  |               buildVisibilityOption( | ||||||
|  |                 context, | ||||||
|  |                 0, | ||||||
|  |                 Symbols.public, | ||||||
|  |                 'postVisibilityPublic', | ||||||
|  |               ), | ||||||
|  |               buildVisibilityOption( | ||||||
|  |                 context, | ||||||
|  |                 1, | ||||||
|  |                 Symbols.group, | ||||||
|  |                 'postVisibilityFriends', | ||||||
|  |               ), | ||||||
|  |               buildVisibilityOption( | ||||||
|  |                 context, | ||||||
|  |                 2, | ||||||
|  |                 Symbols.link_off, | ||||||
|  |                 'postVisibilityUnlisted', | ||||||
|  |               ), | ||||||
|  |               buildVisibilityOption( | ||||||
|  |                 context, | ||||||
|  |                 3, | ||||||
|  |                 Symbols.lock, | ||||||
|  |                 'postVisibilityPrivate', | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return SheetScaffold( | ||||||
|  |       titleText: 'postSettings'.tr(), | ||||||
|  |       child: SingleChildScrollView( | ||||||
|  |         padding: const EdgeInsets.all(16), | ||||||
|  |         child: Column( | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |           children: [ | ||||||
|  |             // Title field | ||||||
|  |             TextField( | ||||||
|  |               controller: titleController, | ||||||
|  |               decoration: InputDecoration( | ||||||
|  |                 labelText: 'postTitle'.tr(), | ||||||
|  |                 hintText: 'postTitle'.tr(), | ||||||
|  |                 border: OutlineInputBorder( | ||||||
|  |                   borderRadius: BorderRadius.circular(12), | ||||||
|  |                 ), | ||||||
|  |                 contentPadding: const EdgeInsets.all(16), | ||||||
|  |               ), | ||||||
|  |               style: theme.textTheme.titleLarge, | ||||||
|  |               onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|  |             ), | ||||||
|  |             const SizedBox(height: 16), | ||||||
|  |  | ||||||
|  |             // Description field | ||||||
|  |             TextField( | ||||||
|  |               controller: descriptionController, | ||||||
|  |               decoration: InputDecoration( | ||||||
|  |                 labelText: 'postDescription'.tr(), | ||||||
|  |                 hintText: 'postDescription'.tr(), | ||||||
|  |                 border: OutlineInputBorder( | ||||||
|  |                   borderRadius: BorderRadius.circular(12), | ||||||
|  |                 ), | ||||||
|  |                 contentPadding: const EdgeInsets.all(16), | ||||||
|  |               ), | ||||||
|  |               style: theme.textTheme.bodyLarge, | ||||||
|  |               maxLines: 3, | ||||||
|  |               onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|  |             ), | ||||||
|  |             const SizedBox(height: 24), | ||||||
|  |  | ||||||
|  |             // Visibility setting | ||||||
|  |             Container( | ||||||
|  |               decoration: BoxDecoration( | ||||||
|  |                 border: Border.all( | ||||||
|  |                   color: colorScheme.outline, | ||||||
|  |                   width: 1, | ||||||
|  |                 ), | ||||||
|  |                 borderRadius: BorderRadius.circular(12), | ||||||
|  |               ), | ||||||
|  |               child: ListTile( | ||||||
|  |                 leading: Icon(getVisibilityIcon(visibility.value)), | ||||||
|  |                 title: Text('postVisibility'.tr()), | ||||||
|  |                 subtitle: Text(getVisibilityText(visibility.value).tr()), | ||||||
|  |                 trailing: const Icon(Symbols.chevron_right), | ||||||
|  |                 onTap: showVisibilitySheet, | ||||||
|  |                 shape: RoundedRectangleBorder( | ||||||
|  |                   borderRadius: BorderRadius.circular(12), | ||||||
|  |                 ), | ||||||
|  |                 contentPadding: const EdgeInsets.symmetric( | ||||||
|  |                   horizontal: 16, | ||||||
|  |                   vertical: 8, | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										308
									
								
								lib/widgets/post/compose_shared.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										308
									
								
								lib/widgets/post/compose_shared.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,308 @@ | |||||||
|  | import 'package:collection/collection.dart'; | ||||||
|  | import 'package:dio/dio.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter/services.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:image_picker/image_picker.dart'; | ||||||
|  | import 'package:island/models/file.dart'; | ||||||
|  | import 'package:island/models/post.dart'; | ||||||
|  | import 'package:island/pods/config.dart'; | ||||||
|  | import 'package:island/pods/network.dart'; | ||||||
|  | import 'package:island/services/file.dart'; | ||||||
|  | import 'package:island/widgets/alert.dart'; | ||||||
|  | import 'package:pasteboard/pasteboard.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<Map<int, double>> attachmentProgress; | ||||||
|  |   final ValueNotifier<SnPublisher?> currentPublisher; | ||||||
|  |  | ||||||
|  |   ComposeState({ | ||||||
|  |     required this.attachments, | ||||||
|  |     required this.titleController, | ||||||
|  |     required this.descriptionController, | ||||||
|  |     required this.contentController, | ||||||
|  |     required this.visibility, | ||||||
|  |     required this.submitting, | ||||||
|  |     required this.attachmentProgress, | ||||||
|  |     required this.currentPublisher, | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class ComposeLogic { | ||||||
|  |   static ComposeState createState({ | ||||||
|  |     SnPost? originalPost, | ||||||
|  |     SnPost? forwardedPost, | ||||||
|  |   }) { | ||||||
|  |     return ComposeState( | ||||||
|  |       attachments: ValueNotifier<List<UniversalFile>>( | ||||||
|  |         originalPost?.attachments | ||||||
|  |                 .map( | ||||||
|  |                   (e) => UniversalFile( | ||||||
|  |                     data: e, | ||||||
|  |                     type: switch (e.mimeType?.split('/').firstOrNull) { | ||||||
|  |                       'image' => UniversalFileType.image, | ||||||
|  |                       'video' => UniversalFileType.video, | ||||||
|  |                       'audio' => UniversalFileType.audio, | ||||||
|  |                       _ => UniversalFileType.file, | ||||||
|  |                     }, | ||||||
|  |                   ), | ||||||
|  |                 ) | ||||||
|  |                 .toList() ?? | ||||||
|  |             [], | ||||||
|  |       ), | ||||||
|  |       titleController: TextEditingController(text: originalPost?.title), | ||||||
|  |       descriptionController: TextEditingController( | ||||||
|  |         text: originalPost?.description, | ||||||
|  |       ), | ||||||
|  |       contentController: TextEditingController( | ||||||
|  |         text: | ||||||
|  |             originalPost?.content ?? | ||||||
|  |             (forwardedPost != null ? '> ${forwardedPost.content}\n\n' : null), | ||||||
|  |       ), | ||||||
|  |       visibility: ValueNotifier<int>(originalPost?.visibility ?? 0), | ||||||
|  |       submitting: ValueNotifier<bool>(false), | ||||||
|  |       attachmentProgress: ValueNotifier<Map<int, double>>({}), | ||||||
|  |       currentPublisher: ValueNotifier<SnPublisher?>(null), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static String getMimeTypeFromFileType(UniversalFileType type) { | ||||||
|  |     return switch (type) { | ||||||
|  |       UniversalFileType.image => 'image/unknown', | ||||||
|  |       UniversalFileType.video => 'video/unknown', | ||||||
|  |       UniversalFileType.audio => 'audio/unknown', | ||||||
|  |       UniversalFileType.file => 'application/octet-stream', | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static Future<void> pickPhotoMedia(WidgetRef ref, ComposeState state) async { | ||||||
|  |     final result = await ref | ||||||
|  |         .watch(imagePickerProvider) | ||||||
|  |         .pickMultiImage(requestFullMetadata: true); | ||||||
|  |     if (result.isEmpty) return; | ||||||
|  |     state.attachments.value = [ | ||||||
|  |       ...state.attachments.value, | ||||||
|  |       ...result.map( | ||||||
|  |         (e) => UniversalFile(data: e, type: UniversalFileType.image), | ||||||
|  |       ), | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static Future<void> pickVideoMedia(WidgetRef ref, ComposeState state) async { | ||||||
|  |     final result = await ref | ||||||
|  |         .watch(imagePickerProvider) | ||||||
|  |         .pickVideo(source: ImageSource.gallery); | ||||||
|  |     if (result == null) return; | ||||||
|  |     state.attachments.value = [ | ||||||
|  |       ...state.attachments.value, | ||||||
|  |       UniversalFile(data: result, type: UniversalFileType.video), | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static Future<void> uploadAttachment( | ||||||
|  |     WidgetRef ref, | ||||||
|  |     ComposeState state, | ||||||
|  |     int index, | ||||||
|  |   ) async { | ||||||
|  |     final attachment = state.attachments.value[index]; | ||||||
|  |     if (attachment.isOnCloud) return; | ||||||
|  |  | ||||||
|  |     final baseUrl = ref.watch(serverUrlProvider); | ||||||
|  |     final token = await getToken(ref.watch(tokenProvider)); | ||||||
|  |     if (token == null) throw ArgumentError('Token is null'); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       // Update progress state | ||||||
|  |       state.attachmentProgress.value = { | ||||||
|  |         ...state.attachmentProgress.value, | ||||||
|  |         index: 0, | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       // Upload file to cloud | ||||||
|  |       final cloudFile = | ||||||
|  |           await putMediaToCloud( | ||||||
|  |             fileData: attachment, | ||||||
|  |             atk: token, | ||||||
|  |             baseUrl: baseUrl, | ||||||
|  |             filename: attachment.data.name ?? 'Post media', | ||||||
|  |             mimetype: | ||||||
|  |                 attachment.data.mimeType ?? | ||||||
|  |                 getMimeTypeFromFileType(attachment.type), | ||||||
|  |             onProgress: (progress, _) { | ||||||
|  |               state.attachmentProgress.value = { | ||||||
|  |                 ...state.attachmentProgress.value, | ||||||
|  |                 index: progress, | ||||||
|  |               }; | ||||||
|  |             }, | ||||||
|  |           ).future; | ||||||
|  |  | ||||||
|  |       if (cloudFile == null) { | ||||||
|  |         throw ArgumentError('Failed to upload the file...'); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Update attachments list with cloud file | ||||||
|  |       final clone = List.of(state.attachments.value); | ||||||
|  |       clone[index] = UniversalFile(data: cloudFile, type: attachment.type); | ||||||
|  |       state.attachments.value = clone; | ||||||
|  |     } catch (err) { | ||||||
|  |       showErrorAlert(err); | ||||||
|  |     } finally { | ||||||
|  |       // Clean up progress state | ||||||
|  |       state.attachmentProgress.value = {...state.attachmentProgress.value} | ||||||
|  |         ..remove(index); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static List<UniversalFile> moveAttachment( | ||||||
|  |     List<UniversalFile> attachments, | ||||||
|  |     int idx, | ||||||
|  |     int delta, | ||||||
|  |   ) { | ||||||
|  |     if (idx + delta < 0 || idx + delta >= attachments.length) { | ||||||
|  |       return attachments; | ||||||
|  |     } | ||||||
|  |     final clone = List.of(attachments); | ||||||
|  |     clone.insert(idx + delta, clone.removeAt(idx)); | ||||||
|  |     return clone; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static Future<void> deleteAttachment( | ||||||
|  |     WidgetRef ref, | ||||||
|  |     ComposeState state, | ||||||
|  |     int index, | ||||||
|  |   ) async { | ||||||
|  |     final attachment = state.attachments.value[index]; | ||||||
|  |     if (attachment.isOnCloud) { | ||||||
|  |       final client = ref.watch(apiClientProvider); | ||||||
|  |       await client.delete('/files/${attachment.data.id}'); | ||||||
|  |     } | ||||||
|  |     final clone = List.of(state.attachments.value); | ||||||
|  |     clone.removeAt(index); | ||||||
|  |     state.attachments.value = clone; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static Future<void> performAction( | ||||||
|  |     WidgetRef ref, | ||||||
|  |     ComposeState state, | ||||||
|  |     BuildContext context, { | ||||||
|  |     SnPost? originalPost, | ||||||
|  |     SnPost? repliedPost, | ||||||
|  |     SnPost? forwardedPost, | ||||||
|  |     int? postType, // 0 for regular post, 1 for article | ||||||
|  |   }) async { | ||||||
|  |     if (state.submitting.value) return; | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       state.submitting.value = true; | ||||||
|  |  | ||||||
|  |       // Upload any local attachments first | ||||||
|  |       await Future.wait( | ||||||
|  |         state.attachments.value | ||||||
|  |             .asMap() | ||||||
|  |             .entries | ||||||
|  |             .where((entry) => entry.value.isOnDevice) | ||||||
|  |             .map((entry) => uploadAttachment(ref, state, entry.key)), | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       // Prepare API request | ||||||
|  |       final client = ref.watch(apiClientProvider); | ||||||
|  |       final isNewPost = originalPost == null; | ||||||
|  |       final endpoint = isNewPost ? '/posts' : '/posts/${originalPost.id}'; | ||||||
|  |  | ||||||
|  |       // Create request payload | ||||||
|  |       final payload = { | ||||||
|  |         'title': state.titleController.text, | ||||||
|  |         'description': state.descriptionController.text, | ||||||
|  |         'content': state.contentController.text, | ||||||
|  |         'visibility': state.visibility.value, | ||||||
|  |         'attachments': | ||||||
|  |             state.attachments.value | ||||||
|  |                 .where((e) => e.isOnCloud) | ||||||
|  |                 .map((e) => e.data.id) | ||||||
|  |                 .toList(), | ||||||
|  |         if (postType != null) 'type': postType, | ||||||
|  |         if (repliedPost != null) 'replied_post_id': repliedPost.id, | ||||||
|  |         if (forwardedPost != null) 'forwarded_post_id': forwardedPost.id, | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       // Send request | ||||||
|  |       await client.request( | ||||||
|  |         endpoint, | ||||||
|  |         data: payload, | ||||||
|  |         options: Options( | ||||||
|  |           headers: {'X-Pub': state.currentPublisher.value?.name}, | ||||||
|  |           method: isNewPost ? 'POST' : 'PATCH', | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       if (context.mounted) { | ||||||
|  |         Navigator.of(context).maybePop(true); | ||||||
|  |       } | ||||||
|  |     } catch (err) { | ||||||
|  |       showErrorAlert(err); | ||||||
|  |     } finally { | ||||||
|  |       state.submitting.value = false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static Future<void> handlePaste(ComposeState state) async { | ||||||
|  |     final clipboard = await Pasteboard.image; | ||||||
|  |     if (clipboard == null) return; | ||||||
|  |  | ||||||
|  |     state.attachments.value = [ | ||||||
|  |       ...state.attachments.value, | ||||||
|  |       UniversalFile( | ||||||
|  |         data: XFile.fromData(clipboard, mimeType: "image/jpeg"), | ||||||
|  |         type: UniversalFileType.image, | ||||||
|  |       ), | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static void handleKeyPress( | ||||||
|  |     RawKeyEvent event, | ||||||
|  |     ComposeState state, | ||||||
|  |     WidgetRef ref, | ||||||
|  |     BuildContext context, { | ||||||
|  |     SnPost? originalPost, | ||||||
|  |     SnPost? repliedPost, | ||||||
|  |     SnPost? forwardedPost, | ||||||
|  |     int? postType, | ||||||
|  |   }) { | ||||||
|  |     if (event is! RawKeyDownEvent) return; | ||||||
|  |  | ||||||
|  |     final isPaste = event.logicalKey == LogicalKeyboardKey.keyV; | ||||||
|  |     final isModifierPressed = event.isMetaPressed || event.isControlPressed; | ||||||
|  |     final isSubmit = event.logicalKey == LogicalKeyboardKey.enter; | ||||||
|  |  | ||||||
|  |     if (isPaste && isModifierPressed) { | ||||||
|  |       handlePaste(state); | ||||||
|  |     } else if (isSubmit && isModifierPressed && !state.submitting.value) { | ||||||
|  |       performAction( | ||||||
|  |         ref, | ||||||
|  |         state, | ||||||
|  |         context, | ||||||
|  |         originalPost: originalPost, | ||||||
|  |         repliedPost: repliedPost, | ||||||
|  |         forwardedPost: forwardedPost, | ||||||
|  |         postType: postType, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static void dispose(ComposeState state) { | ||||||
|  |     state.titleController.dispose(); | ||||||
|  |     state.descriptionController.dispose(); | ||||||
|  |     state.contentController.dispose(); | ||||||
|  |     state.attachments.dispose(); | ||||||
|  |     state.visibility.dispose(); | ||||||
|  |     state.submitting.dispose(); | ||||||
|  |     state.attachmentProgress.dispose(); | ||||||
|  |     state.currentPublisher.dispose(); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -19,6 +19,7 @@ import 'package:island/widgets/app_scaffold.dart'; | |||||||
| import 'package:island/widgets/content/cloud_file_collection.dart'; | import 'package:island/widgets/content/cloud_file_collection.dart'; | ||||||
| import 'package:island/widgets/content/cloud_files.dart'; | import 'package:island/widgets/content/cloud_files.dart'; | ||||||
| import 'package:island/widgets/content/markdown.dart'; | import 'package:island/widgets/content/markdown.dart'; | ||||||
|  | import 'package:island/widgets/post/post_replies_sheet.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:super_context_menu/super_context_menu.dart'; | import 'package:super_context_menu/super_context_menu.dart'; | ||||||
| @@ -235,18 +236,52 @@ class PostItem extends HookConsumerWidget { | |||||||
|                   ), |                   ), | ||||||
|                 ], |                 ], | ||||||
|               ), |               ), | ||||||
|               PostReactionList( |               Row( | ||||||
|                 parentId: item.id, |                 children: [ | ||||||
|                 reactions: item.reactionsCount, |                   // Replies count button | ||||||
|                 padding: EdgeInsets.only(left: 48), |                   Padding( | ||||||
|                 onReact: (symbol, attitude, delta) { |                     padding: const EdgeInsets.only(left: 48, right: 12), | ||||||
|                   final reactionsCount = Map<String, int>.from( |                     child: ActionChip( | ||||||
|                     item.reactionsCount, |                       avatar: Icon(Symbols.reply, size: 16), | ||||||
|                   ); |                       label: Text( | ||||||
|                   reactionsCount[symbol] = |                         (item.repliesCount > 0) | ||||||
|                       (reactionsCount[symbol] ?? 0) + delta; |                             ? 'repliesCount'.plural(item.repliesCount) | ||||||
|                   onUpdate?.call(item.copyWith(reactionsCount: reactionsCount)); |                             : 'reply'.tr(), | ||||||
|                 }, |                       ), | ||||||
|  |                       visualDensity: const VisualDensity( | ||||||
|  |                         horizontal: VisualDensity.minimumDensity, | ||||||
|  |                         vertical: VisualDensity.minimumDensity, | ||||||
|  |                       ), | ||||||
|  |                       onPressed: () { | ||||||
|  |                          if (isOpenable) { | ||||||
|  |                            showModalBottomSheet( | ||||||
|  |                              context: context, | ||||||
|  |                              isScrollControlled: true, | ||||||
|  |                              builder: (context) => PostRepliesSheet(post: item), | ||||||
|  |                            ); | ||||||
|  |                          } | ||||||
|  |                        }, | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                   // Reactions list | ||||||
|  |                   Expanded( | ||||||
|  |                     child: PostReactionList( | ||||||
|  |                       parentId: item.id, | ||||||
|  |                       reactions: item.reactionsCount, | ||||||
|  |                       padding: EdgeInsets.zero, | ||||||
|  |                       onReact: (symbol, attitude, delta) { | ||||||
|  |                         final reactionsCount = Map<String, int>.from( | ||||||
|  |                           item.reactionsCount, | ||||||
|  |                         ); | ||||||
|  |                         reactionsCount[symbol] = | ||||||
|  |                             (reactionsCount[symbol] ?? 0) + delta; | ||||||
|  |                         onUpdate?.call( | ||||||
|  |                           item.copyWith(reactionsCount: reactionsCount), | ||||||
|  |                         ); | ||||||
|  |                       }, | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 ], | ||||||
|               ), |               ), | ||||||
|             ], |             ], | ||||||
|           ), |           ), | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/post.dart'; | import 'package:island/models/post.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/widgets/content/paging_helper_ext.dart'; |  | ||||||
| import 'package:island/widgets/post/post_item.dart'; | import 'package:island/widgets/post/post_item.dart'; | ||||||
| import 'package:island/widgets/post/post_item_creator.dart'; | import 'package:island/widgets/post/post_item_creator.dart'; | ||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
| @@ -51,9 +50,9 @@ class PostListNotifier extends _$PostListNotifier | |||||||
| enum PostItemType { | enum PostItemType { | ||||||
|   /// Regular post item with user information |   /// Regular post item with user information | ||||||
|   regular, |   regular, | ||||||
|    |  | ||||||
|   /// Creator view with analytics and metadata |   /// Creator view with analytics and metadata | ||||||
|   creator |   creator, | ||||||
| } | } | ||||||
|  |  | ||||||
| class SliverPostList extends HookConsumerWidget { | class SliverPostList extends HookConsumerWidget { | ||||||
| @@ -64,9 +63,9 @@ class SliverPostList extends HookConsumerWidget { | |||||||
|   final bool isOpenable; |   final bool isOpenable; | ||||||
|   final Function? onRefresh; |   final Function? onRefresh; | ||||||
|   final Function(SnPost)? onUpdate; |   final Function(SnPost)? onUpdate; | ||||||
|    |  | ||||||
|   const SliverPostList({ |   const SliverPostList({ | ||||||
|     super.key,  |     super.key, | ||||||
|     this.pubName, |     this.pubName, | ||||||
|     this.itemType = PostItemType.regular, |     this.itemType = PostItemType.regular, | ||||||
|     this.backgroundColor, |     this.backgroundColor, | ||||||
| @@ -89,20 +88,17 @@ class SliverPostList extends HookConsumerWidget { | |||||||
|               if (index == widgetCount - 1) { |               if (index == widgetCount - 1) { | ||||||
|                 return endItemView; |                 return endItemView; | ||||||
|               } |               } | ||||||
|                |  | ||||||
|               final post = data.items[index]; |               final post = data.items[index]; | ||||||
|                |  | ||||||
|               return Column( |               return Column( | ||||||
|                 children: [ |                 children: [_buildPostItem(post), const Divider(height: 1)], | ||||||
|                   _buildPostItem(post), |  | ||||||
|                   const Divider(height: 1), |  | ||||||
|                 ], |  | ||||||
|               ); |               ); | ||||||
|             }, |             }, | ||||||
|           ), |           ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|    |  | ||||||
|   Widget _buildPostItem(SnPost post) { |   Widget _buildPostItem(SnPost post) { | ||||||
|     switch (itemType) { |     switch (itemType) { | ||||||
|       case PostItemType.creator: |       case PostItemType.creator: | ||||||
| @@ -115,7 +111,6 @@ class SliverPostList extends HookConsumerWidget { | |||||||
|           onUpdate: onUpdate, |           onUpdate: onUpdate, | ||||||
|         ); |         ); | ||||||
|       case PostItemType.regular: |       case PostItemType.regular: | ||||||
|       default: |  | ||||||
|         return PostItem(item: post); |         return PostItem(item: post); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -3,7 +3,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; | |||||||
| import 'package:island/models/post.dart'; | import 'package:island/models/post.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/services/responsive.dart'; | import 'package:island/services/responsive.dart'; | ||||||
| import 'package:island/widgets/content/paging_helper_ext.dart'; |  | ||||||
| import 'package:island/widgets/post/post_item.dart'; | import 'package:island/widgets/post/post_item.dart'; | ||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
| import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||||
| @@ -57,7 +56,8 @@ class PostRepliesNotifier extends _$PostRepliesNotifier | |||||||
|  |  | ||||||
| class PostRepliesList extends HookConsumerWidget { | class PostRepliesList extends HookConsumerWidget { | ||||||
|   final String postId; |   final String postId; | ||||||
|   const PostRepliesList({super.key, required this.postId}); |   final Color? backgroundColor; | ||||||
|  |   const PostRepliesList({super.key, required this.postId, this.backgroundColor}); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
| @@ -93,7 +93,7 @@ class PostRepliesList extends HookConsumerWidget { | |||||||
|               children: [ |               children: [ | ||||||
|                 PostItem( |                 PostItem( | ||||||
|                   item: data.items[index], |                   item: data.items[index], | ||||||
|                   backgroundColor: isWide ? Colors.transparent : null, |                   backgroundColor: backgroundColor ?? (isWide ? Colors.transparent : null), | ||||||
|                   showReferencePost: false, |                   showReferencePost: false, | ||||||
|                 ), |                 ), | ||||||
|                 const Divider(height: 1), |                 const Divider(height: 1), | ||||||
|   | |||||||
							
								
								
									
										48
									
								
								lib/widgets/post/post_replies_sheet.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								lib/widgets/post/post_replies_sheet.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | |||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:island/models/post.dart'; | ||||||
|  | import 'package:island/widgets/content/sheet.dart'; | ||||||
|  | import 'package:island/widgets/post/post_replies.dart'; | ||||||
|  | import 'package:island/widgets/post/post_quick_reply.dart'; | ||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  |  | ||||||
|  | class PostRepliesSheet extends HookConsumerWidget { | ||||||
|  |   final SnPost post; | ||||||
|  |  | ||||||
|  |   const PostRepliesSheet({super.key, required this.post}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     return SheetScaffold( | ||||||
|  |       titleText: 'repliesCount'.plural(post.repliesCount), | ||||||
|  |       child: Column( | ||||||
|  |         children: [ | ||||||
|  |           // Replies list | ||||||
|  |           Expanded( | ||||||
|  |             child: CustomScrollView( | ||||||
|  |               slivers: [PostRepliesList( | ||||||
|  |                 postId: post.id.toString(), | ||||||
|  |                 backgroundColor: Colors.transparent, | ||||||
|  |               )], | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |           // Quick reply section | ||||||
|  |           Material( | ||||||
|  |             elevation: 2, | ||||||
|  |             child: PostQuickReply( | ||||||
|  |               parent: post, | ||||||
|  |               onPosted: () { | ||||||
|  |                 ref.invalidate(postRepliesNotifierProvider(post.id)); | ||||||
|  |               }, | ||||||
|  |             ).padding( | ||||||
|  |               bottom: MediaQuery.of(context).padding.bottom + 16, | ||||||
|  |               top: 16, | ||||||
|  |               horizontal: 16, | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,14 +1,4 @@ | |||||||
| PODS: | PODS: | ||||||
|   - AppAuth (1.7.6): |  | ||||||
|     - AppAuth/Core (= 1.7.6) |  | ||||||
|     - AppAuth/ExternalUserAgent (= 1.7.6) |  | ||||||
|   - AppAuth/Core (1.7.6) |  | ||||||
|   - AppAuth/ExternalUserAgent (1.7.6): |  | ||||||
|     - AppAuth/Core |  | ||||||
|   - AppCheckCore (11.2.0): |  | ||||||
|     - GoogleUtilities/Environment (~> 8.0) |  | ||||||
|     - GoogleUtilities/UserDefaults (~> 8.0) |  | ||||||
|     - PromisesObjC (~> 2.4) |  | ||||||
|   - bitsdojo_window_macos (0.0.1): |   - bitsdojo_window_macos (0.0.1): | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - connectivity_plus (0.0.1): |   - connectivity_plus (0.0.1): | ||||||
| @@ -71,20 +61,9 @@ PODS: | |||||||
|   - gal (1.0.0): |   - gal (1.0.0): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - google_sign_in_ios (0.0.1): |  | ||||||
|     - AppAuth (>= 1.7.4) |  | ||||||
|     - Flutter |  | ||||||
|     - FlutterMacOS |  | ||||||
|     - GoogleSignIn (~> 8.0) |  | ||||||
|     - GTMSessionFetcher (>= 3.4.0) |  | ||||||
|   - GoogleDataTransport (10.1.0): |   - GoogleDataTransport (10.1.0): | ||||||
|     - nanopb (~> 3.30910.0) |     - nanopb (~> 3.30910.0) | ||||||
|     - PromisesObjC (~> 2.4) |     - PromisesObjC (~> 2.4) | ||||||
|   - GoogleSignIn (8.0.0): |  | ||||||
|     - AppAuth (< 2.0, >= 1.7.3) |  | ||||||
|     - AppCheckCore (~> 11.0) |  | ||||||
|     - GTMAppAuth (< 5.0, >= 4.1.1) |  | ||||||
|     - GTMSessionFetcher/Core (~> 3.3) |  | ||||||
|   - GoogleUtilities/AppDelegateSwizzler (8.1.0): |   - GoogleUtilities/AppDelegateSwizzler (8.1.0): | ||||||
|     - GoogleUtilities/Environment |     - GoogleUtilities/Environment | ||||||
|     - GoogleUtilities/Logger |     - GoogleUtilities/Logger | ||||||
| @@ -109,14 +88,6 @@ PODS: | |||||||
|   - GoogleUtilities/UserDefaults (8.1.0): |   - GoogleUtilities/UserDefaults (8.1.0): | ||||||
|     - GoogleUtilities/Logger |     - GoogleUtilities/Logger | ||||||
|     - GoogleUtilities/Privacy |     - GoogleUtilities/Privacy | ||||||
|   - GTMAppAuth (4.1.1): |  | ||||||
|     - AppAuth/Core (~> 1.7) |  | ||||||
|     - GTMSessionFetcher/Core (< 4.0, >= 3.3) |  | ||||||
|   - GTMSessionFetcher (3.5.0): |  | ||||||
|     - GTMSessionFetcher/Full (= 3.5.0) |  | ||||||
|   - GTMSessionFetcher/Core (3.5.0) |  | ||||||
|   - GTMSessionFetcher/Full (3.5.0): |  | ||||||
|     - GTMSessionFetcher/Core |  | ||||||
|   - irondash_engine_context (0.0.1): |   - irondash_engine_context (0.0.1): | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - livekit_client (2.4.8): |   - livekit_client (2.4.8): | ||||||
| @@ -200,7 +171,6 @@ DEPENDENCIES: | |||||||
|   - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`) |   - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`) | ||||||
|   - FlutterMacOS (from `Flutter/ephemeral`) |   - FlutterMacOS (from `Flutter/ephemeral`) | ||||||
|   - gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`) |   - gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`) | ||||||
|   - google_sign_in_ios (from `Flutter/ephemeral/.symlinks/plugins/google_sign_in_ios/darwin`) |  | ||||||
|   - irondash_engine_context (from `Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos`) |   - irondash_engine_context (from `Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos`) | ||||||
|   - livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`) |   - livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`) | ||||||
|   - media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`) |   - media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`) | ||||||
| @@ -220,18 +190,13 @@ DEPENDENCIES: | |||||||
|  |  | ||||||
| SPEC REPOS: | SPEC REPOS: | ||||||
|   trunk: |   trunk: | ||||||
|     - AppAuth |  | ||||||
|     - AppCheckCore |  | ||||||
|     - Firebase |     - Firebase | ||||||
|     - FirebaseCore |     - FirebaseCore | ||||||
|     - FirebaseCoreInternal |     - FirebaseCoreInternal | ||||||
|     - FirebaseInstallations |     - FirebaseInstallations | ||||||
|     - FirebaseMessaging |     - FirebaseMessaging | ||||||
|     - GoogleDataTransport |     - GoogleDataTransport | ||||||
|     - GoogleSignIn |  | ||||||
|     - GoogleUtilities |     - GoogleUtilities | ||||||
|     - GTMAppAuth |  | ||||||
|     - GTMSessionFetcher |  | ||||||
|     - nanopb |     - nanopb | ||||||
|     - OrderedSet |     - OrderedSet | ||||||
|     - PromisesObjC |     - PromisesObjC | ||||||
| @@ -270,8 +235,6 @@ EXTERNAL SOURCES: | |||||||
|     :path: Flutter/ephemeral |     :path: Flutter/ephemeral | ||||||
|   gal: |   gal: | ||||||
|     :path: Flutter/ephemeral/.symlinks/plugins/gal/darwin |     :path: Flutter/ephemeral/.symlinks/plugins/gal/darwin | ||||||
|   google_sign_in_ios: |  | ||||||
|     :path: Flutter/ephemeral/.symlinks/plugins/google_sign_in_ios/darwin |  | ||||||
|   irondash_engine_context: |   irondash_engine_context: | ||||||
|     :path: Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos |     :path: Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos | ||||||
|   livekit_client: |   livekit_client: | ||||||
| @@ -306,8 +269,6 @@ EXTERNAL SOURCES: | |||||||
|     :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos |     :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos | ||||||
|  |  | ||||||
| SPEC CHECKSUMS: | SPEC CHECKSUMS: | ||||||
|   AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 |  | ||||||
|   AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f |  | ||||||
|   bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 |   bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 | ||||||
|   connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e |   connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e | ||||||
|   croppy: d9bfc8c02f3cd1851f669a421df298a474b78f43 |   croppy: d9bfc8c02f3cd1851f669a421df298a474b78f43 | ||||||
| @@ -328,12 +289,8 @@ SPEC CHECKSUMS: | |||||||
|   flutter_webrtc: a7eeb54859e672228c28f4b48b1fb61561976ea3 |   flutter_webrtc: a7eeb54859e672228c28f4b48b1fb61561976ea3 | ||||||
|   FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 |   FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 | ||||||
|   gal: baecd024ebfd13c441269ca7404792a7152fde89 |   gal: baecd024ebfd13c441269ca7404792a7152fde89 | ||||||
|   google_sign_in_ios: b48bb9af78576358a168361173155596c845f0b9 |  | ||||||
|   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 |   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 | ||||||
|   GoogleSignIn: ce8c89bb9b37fb624b92e7514cc67335d1e277e4 |  | ||||||
|   GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 |   GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 | ||||||
|   GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de |  | ||||||
|   GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 |  | ||||||
|   irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba |   irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba | ||||||
|   livekit_client: 6a35243df3da61750c98e266e02dedcf5d25c888 |   livekit_client: 6a35243df3da61750c98e266e02dedcf5d25c888 | ||||||
|   media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65 |   media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65 | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ | |||||||
|             <BuildableReference |             <BuildableReference | ||||||
|                BuildableIdentifier = "primary" |                BuildableIdentifier = "primary" | ||||||
|                BlueprintIdentifier = "33CC10EC2044A3C60003C045" |                BlueprintIdentifier = "33CC10EC2044A3C60003C045" | ||||||
|                BuildableName = "island.app" |                BuildableName = "Solian.app" | ||||||
|                BlueprintName = "Runner" |                BlueprintName = "Runner" | ||||||
|                ReferencedContainer = "container:Runner.xcodeproj"> |                ReferencedContainer = "container:Runner.xcodeproj"> | ||||||
|             </BuildableReference> |             </BuildableReference> | ||||||
| @@ -31,7 +31,7 @@ | |||||||
|          <BuildableReference |          <BuildableReference | ||||||
|             BuildableIdentifier = "primary" |             BuildableIdentifier = "primary" | ||||||
|             BlueprintIdentifier = "33CC10EC2044A3C60003C045" |             BlueprintIdentifier = "33CC10EC2044A3C60003C045" | ||||||
|             BuildableName = "island.app" |             BuildableName = "Solian.app" | ||||||
|             BlueprintName = "Runner" |             BlueprintName = "Runner" | ||||||
|             ReferencedContainer = "container:Runner.xcodeproj"> |             ReferencedContainer = "container:Runner.xcodeproj"> | ||||||
|          </BuildableReference> |          </BuildableReference> | ||||||
| @@ -66,7 +66,7 @@ | |||||||
|          <BuildableReference |          <BuildableReference | ||||||
|             BuildableIdentifier = "primary" |             BuildableIdentifier = "primary" | ||||||
|             BlueprintIdentifier = "33CC10EC2044A3C60003C045" |             BlueprintIdentifier = "33CC10EC2044A3C60003C045" | ||||||
|             BuildableName = "island.app" |             BuildableName = "Solian.app" | ||||||
|             BlueprintName = "Runner" |             BlueprintName = "Runner" | ||||||
|             ReferencedContainer = "container:Runner.xcodeproj"> |             ReferencedContainer = "container:Runner.xcodeproj"> | ||||||
|          </BuildableReference> |          </BuildableReference> | ||||||
| @@ -83,7 +83,7 @@ | |||||||
|          <BuildableReference |          <BuildableReference | ||||||
|             BuildableIdentifier = "primary" |             BuildableIdentifier = "primary" | ||||||
|             BlueprintIdentifier = "33CC10EC2044A3C60003C045" |             BlueprintIdentifier = "33CC10EC2044A3C60003C045" | ||||||
|             BuildableName = "island.app" |             BuildableName = "Solian.app" | ||||||
|             BlueprintName = "Runner" |             BlueprintName = "Runner" | ||||||
|             ReferencedContainer = "container:Runner.xcodeproj"> |             ReferencedContainer = "container:Runner.xcodeproj"> | ||||||
|          </BuildableReference> |          </BuildableReference> | ||||||
|   | |||||||
							
								
								
									
										38
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -149,10 +149,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: build |       name: build | ||||||
|       sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 |       sha256: "7cf79af8eb6023bee797a77b067fb6e63ac5650f3789546e023958098feb776e" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.4.2" |     version: "2.5.2" | ||||||
|   build_config: |   build_config: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -173,26 +173,26 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: build_resolvers |       name: build_resolvers | ||||||
|       sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 |       sha256: "7a507e6026abe52074836d51a945bfad456daa7493eb7a6cac565e490e7d5b54" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.4.4" |     version: "2.5.2" | ||||||
|   build_runner: |   build_runner: | ||||||
|     dependency: "direct dev" |     dependency: "direct dev" | ||||||
|     description: |     description: | ||||||
|       name: build_runner |       name: build_runner | ||||||
|       sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" |       sha256: "1ce1e5063b564f26c27bda54c82a3d38339df69ec58f90e0017f447de77e4839" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.4.15" |     version: "2.5.2" | ||||||
|   build_runner_core: |   build_runner_core: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: build_runner_core |       name: build_runner_core | ||||||
|       sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" |       sha256: "564230f3fd9363df7870058fef11ec5502ee620aec3b1ee8106b943be5c63a76" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "8.0.0" |     version: "9.1.0" | ||||||
|   built_collection: |   built_collection: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -453,18 +453,18 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: device_info_plus |       name: device_info_plus | ||||||
|       sha256: "0c6396126421b590089447154c5f98a5de423b70cfb15b1578fd018843ee6f53" |       sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "11.4.0" |     version: "11.5.0" | ||||||
|   device_info_plus_platform_interface: |   device_info_plus_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: device_info_plus_platform_interface |       name: device_info_plus_platform_interface | ||||||
|       sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2" |       sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "7.0.2" |     version: "7.0.3" | ||||||
|   dio: |   dio: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -895,10 +895,10 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: flutter_svg |       name: flutter_svg | ||||||
|       sha256: d44bf546b13025ec7353091516f6881f1d4c633993cb109c3916c3a0159dadf1 |       sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.0" |     version: "2.2.0" | ||||||
|   flutter_test: |   flutter_test: | ||||||
|     dependency: "direct dev" |     dependency: "direct dev" | ||||||
|     description: flutter |     description: flutter | ||||||
| @@ -1217,10 +1217,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: lean_builder |       name: lean_builder | ||||||
|       sha256: ac129cd2173aa4e53e1327bcee2233d738d68ee446f3c797135633deafe6ca8a |       sha256: dca2165cfe681c69ae903a0880cab90ee93d730777605a0f44c9dd08cec7e1b9 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.1.0-alpha.12" |     version: "0.1.0-alpha.13" | ||||||
|   lint: |   lint: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -1769,10 +1769,10 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: riverpod_paging_utils |       name: riverpod_paging_utils | ||||||
|       sha256: "18f59960807835b1d3cb993e825442d7b09928d0f55ad50bda65c002b5893bdc" |       sha256: a3eb7cc87d53d90dac9bf0b0d695ecdc049aae5dd6debd7d2d62ab3682cf5841 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.8.0" |     version: "0.8.1" | ||||||
|   rxdart: |   rxdart: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -2509,4 +2509,4 @@ packages: | |||||||
|     version: "3.1.3" |     version: "3.1.3" | ||||||
| sdks: | sdks: | ||||||
|   dart: ">=3.8.0 <4.0.0" |   dart: ">=3.8.0 <4.0.0" | ||||||
|   flutter: ">=3.27.4" |   flutter: ">=3.29.0" | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev | |||||||
| # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | ||||||
| # In Windows, build-name is used as the major, minor, and patch parts | # In Windows, build-name is used as the major, minor, and patch parts | ||||||
| # of the product and file versions while build-number is used as the build suffix. | # of the product and file versions while build-number is used as the build suffix. | ||||||
| version: 3.0.0+104 | version: 3.0.0+106 | ||||||
|  |  | ||||||
| environment: | environment: | ||||||
|   sdk: ^3.7.2 |   sdk: ^3.7.2 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user