From 89fd80bcb80fc2eca9650cc5526de24b89d02409 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 19 Jun 2025 23:38:38 +0800 Subject: [PATCH] :sparkles: Article editor --- assets/i18n/en-US.json | 7 +- ios/Podfile.lock | 6 + lib/route.dart | 4 +- lib/route.gr.dart | 715 +++++++++++-------- lib/screens/creators/posts/list.dart | 52 ++ lib/screens/posts/compose.dart | 672 +++++------------ lib/screens/posts/compose_article.dart | 401 +++++++++++ lib/widgets/post/compose_settings_sheet.dart | 178 +++++ lib/widgets/post/compose_shared.dart | 308 ++++++++ 9 files changed, 1557 insertions(+), 786 deletions(-) create mode 100644 lib/screens/posts/compose_article.dart create mode 100644 lib/widgets/post/compose_settings_sheet.dart create mode 100644 lib/widgets/post/compose_shared.dart diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index bb2171e..071e367 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -337,6 +337,9 @@ "unauthorized": "Unauthorized", "unauthorizedHint": "You're not signed in or session expired, please sign in again.", "publisherBelongsTo": "Belongs to {}", + "postContent": "Content", + "postSettings": "Settings", + "postPublisherUnselected": "Publisher Unspecified", "postVisibility": "Visibility", "postVisibilityPublic": "Public", "postVisibilityFriends": "Friends Only", @@ -449,5 +452,7 @@ "checkInResultT4": "Best", "accountProfileView": "View Profile", "unspecified": "Unspecified", - "added": "Added" + "added": "Added", + "preview": "Preview", + "togglePreview": "Toggle Preview" } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index acaa896..0bf9654 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -140,6 +140,8 @@ PODS: - nanopb/encode (= 3.30910.0) - nanopb/decode (3.30910.0) - nanopb/encode (3.30910.0) + - native_exif (0.0.1): + - Flutter - OrderedSet (6.0.3) - package_info_plus (0.4.5): - Flutter @@ -218,6 +220,7 @@ DEPENDENCIES: - livekit_client (from `.symlinks/plugins/livekit_client/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`) + - native_exif (from `.symlinks/plugins/native_exif/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - pasteboard (from `.symlinks/plugins/pasteboard/ios`) - 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" media_kit_video: :path: ".symlinks/plugins/media_kit_video/ios" + native_exif: + :path: ".symlinks/plugins/native_exif/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" pasteboard: @@ -349,6 +354,7 @@ SPEC CHECKSUMS: media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + native_exif: 0eb73d3d5b3ca892719228df8d2d1b13d1ae396c OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c diff --git a/lib/route.dart b/lib/route.dart index 472c03a..f8a840b 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -8,14 +8,14 @@ class AppRouter extends RootStackRouter { @override List get routes => [ + AutoRoute(page: PostComposeRoute.page, path: '/posts/compose'), + AutoRoute(page: PostEditRoute.page, path: '/posts/:id/edit'), AutoRoute( page: ExploreShellRoute.page, path: '/', children: [ AutoRoute(page: ExploreRoute.page, path: ''), - AutoRoute(page: PostComposeRoute.page, path: 'posts/compose'), AutoRoute(page: PostDetailRoute.page, path: 'posts/:id'), - AutoRoute(page: PostEditRoute.page, path: 'posts/:id/edit'), AutoRoute(page: PublisherProfileRoute.page, path: 'publishers/:name'), ], ), diff --git a/lib/route.gr.dart b/lib/route.gr.dart index e3c67c5..fb1931c 100644 --- a/lib/route.gr.dart +++ b/lib/route.gr.dart @@ -9,46 +9,47 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i28; -import 'package:flutter/foundation.dart' as _i30; -import 'package:flutter/material.dart' as _i29; -import 'package:island/models/post.dart' as _i31; -import 'package:island/route.dart' as _i32; +import 'package:auto_route/auto_route.dart' as _i29; +import 'package:flutter/foundation.dart' as _i31; +import 'package:flutter/material.dart' as _i30; +import 'package:island/models/post.dart' as _i32; +import 'package:island/route.dart' as _i33; import 'package:island/screens/account.dart' as _i2; -import 'package:island/screens/account/event_calendar.dart' as _i15; +import 'package:island/screens/account/event_calendar.dart' as _i16; import 'package:island/screens/account/me/settings.dart' as _i3; -import 'package:island/screens/account/me/update.dart' as _i26; +import 'package:island/screens/account/me/update.dart' as _i27; import 'package:island/screens/account/profile.dart' as _i1; -import 'package:island/screens/account/relationship.dart' as _i23; -import 'package:island/screens/auth/create_account.dart' as _i8; -import 'package:island/screens/auth/login.dart' as _i17; -import 'package:island/screens/auth/tabs.dart' as _i25; -import 'package:island/screens/chat/call.dart' as _i4; -import 'package:island/screens/chat/chat.dart' as _i6; -import 'package:island/screens/chat/room.dart' as _i7; -import 'package:island/screens/chat/room_detail.dart' as _i5; -import 'package:island/screens/creators/hub.dart' as _i9; -import 'package:island/screens/creators/posts/list.dart' as _i10; -import 'package:island/screens/creators/publishers.dart' as _i11; -import 'package:island/screens/creators/stickers/pack_detail.dart' as _i14; -import 'package:island/screens/creators/stickers/stickers.dart' as _i13; -import 'package:island/screens/explore.dart' as _i16; -import 'package:island/screens/notification.dart' as _i18; -import 'package:island/screens/posts/compose.dart' as _i19; -import 'package:island/screens/posts/detail.dart' as _i20; -import 'package:island/screens/posts/pub_profile.dart' as _i21; -import 'package:island/screens/realm/detail.dart' as _i22; -import 'package:island/screens/realm/realms.dart' as _i12; -import 'package:island/screens/settings.dart' as _i24; -import 'package:island/screens/wallet.dart' as _i27; +import 'package:island/screens/account/relationship.dart' as _i24; +import 'package:island/screens/auth/create_account.dart' as _i9; +import 'package:island/screens/auth/login.dart' as _i18; +import 'package:island/screens/auth/tabs.dart' as _i26; +import 'package:island/screens/chat/call.dart' as _i5; +import 'package:island/screens/chat/chat.dart' as _i7; +import 'package:island/screens/chat/room.dart' as _i8; +import 'package:island/screens/chat/room_detail.dart' as _i6; +import 'package:island/screens/creators/hub.dart' as _i10; +import 'package:island/screens/creators/posts/list.dart' as _i11; +import 'package:island/screens/creators/publishers.dart' as _i12; +import 'package:island/screens/creators/stickers/pack_detail.dart' as _i15; +import 'package:island/screens/creators/stickers/stickers.dart' as _i14; +import 'package:island/screens/explore.dart' as _i17; +import 'package:island/screens/notification.dart' as _i19; +import 'package:island/screens/posts/compose.dart' as _i20; +import 'package:island/screens/posts/compose_article.dart' as _i4; +import 'package:island/screens/posts/detail.dart' as _i21; +import 'package:island/screens/posts/pub_profile.dart' as _i22; +import 'package:island/screens/realm/detail.dart' as _i23; +import 'package:island/screens/realm/realms.dart' as _i13; +import 'package:island/screens/settings.dart' as _i25; +import 'package:island/screens/wallet.dart' as _i28; /// generated route for /// [_i1.AccountProfileScreen] -class AccountProfileRoute extends _i28.PageRouteInfo { +class AccountProfileRoute extends _i29.PageRouteInfo { AccountProfileRoute({ - _i29.Key? key, + _i30.Key? key, required String name, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( AccountProfileRoute.name, args: AccountProfileRouteArgs(key: key, name: name), @@ -58,7 +59,7 @@ class AccountProfileRoute extends _i28.PageRouteInfo { static const String name = 'AccountProfileRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; @@ -74,7 +75,7 @@ class AccountProfileRoute extends _i28.PageRouteInfo { class AccountProfileRouteArgs { const AccountProfileRouteArgs({this.key, required this.name}); - final _i29.Key? key; + final _i30.Key? key; final String name; @@ -96,11 +97,11 @@ class AccountProfileRouteArgs { /// generated route for /// [_i2.AccountScreen] -class AccountRoute extends _i28.PageRouteInfo { +class AccountRoute extends _i29.PageRouteInfo { AccountRoute({ - _i30.Key? key, + _i31.Key? key, bool isAside = false, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( AccountRoute.name, args: AccountRouteArgs(key: key, isAside: isAside), @@ -109,7 +110,7 @@ class AccountRoute extends _i28.PageRouteInfo { static const String name = 'AccountRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final args = data.argsAs( @@ -123,7 +124,7 @@ class AccountRoute extends _i28.PageRouteInfo { class AccountRouteArgs { const AccountRouteArgs({this.key, this.isAside = false}); - final _i30.Key? key; + final _i31.Key? key; final bool isAside; @@ -145,13 +146,13 @@ class AccountRouteArgs { /// generated route for /// [_i3.AccountSettingsScreen] -class AccountSettingsRoute extends _i28.PageRouteInfo { - const AccountSettingsRoute({List<_i28.PageRouteInfo>? children}) +class AccountSettingsRoute extends _i29.PageRouteInfo { + const AccountSettingsRoute({List<_i29.PageRouteInfo>? children}) : super(AccountSettingsRoute.name, initialChildren: children); static const String name = 'AccountSettingsRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { return const _i3.AccountSettingsScreen(); @@ -161,13 +162,13 @@ class AccountSettingsRoute extends _i28.PageRouteInfo { /// generated route for /// [_i2.AccountShellScreen] -class AccountShellRoute extends _i28.PageRouteInfo { - const AccountShellRoute({List<_i28.PageRouteInfo>? children}) +class AccountShellRoute extends _i29.PageRouteInfo { + const AccountShellRoute({List<_i29.PageRouteInfo>? children}) : super(AccountShellRoute.name, initialChildren: children); static const String name = 'AccountShellRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { return const _i2.AccountShellScreen(); @@ -176,12 +177,138 @@ class AccountShellRoute extends _i28.PageRouteInfo { } /// generated route for -/// [_i4.CallScreen] -class CallRoute extends _i28.PageRouteInfo { +/// [_i4.ArticleComposeScreen] +class ArticleComposeRoute extends _i29.PageRouteInfo { + ArticleComposeRoute({ + _i30.Key? key, + _i32.SnPost? originalPost, + _i32.SnPost? repliedPost, + _i32.SnPost? forwardedPost, + List<_i29.PageRouteInfo>? children, + }) : super( + ArticleComposeRoute.name, + args: ArticleComposeRouteArgs( + key: key, + originalPost: originalPost, + repliedPost: repliedPost, + forwardedPost: forwardedPost, + ), + initialChildren: children, + ); + + static const String name = 'ArticleComposeRoute'; + + static _i29.PageInfo page = _i29.PageInfo( + name, + builder: (data) { + final args = data.argsAs( + orElse: () => const ArticleComposeRouteArgs(), + ); + return _i4.ArticleComposeScreen( + key: args.key, + originalPost: args.originalPost, + ); + }, + ); +} + +class ArticleComposeRouteArgs { + const ArticleComposeRouteArgs({ + this.key, + this.originalPost, + this.repliedPost, + this.forwardedPost, + }); + + final _i30.Key? key; + + final _i32.SnPost? originalPost; + + final _i32.SnPost? repliedPost; + + final _i32.SnPost? forwardedPost; + + @override + String toString() { + return 'ArticleComposeRouteArgs{key: $key, originalPost: $originalPost, repliedPost: $repliedPost, forwardedPost: $forwardedPost}'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! ArticleComposeRouteArgs) return false; + return key == other.key && + originalPost == other.originalPost && + repliedPost == other.repliedPost && + forwardedPost == other.forwardedPost; + } + + @override + int get hashCode => + key.hashCode ^ + originalPost.hashCode ^ + repliedPost.hashCode ^ + forwardedPost.hashCode; +} + +/// generated route for +/// [_i4.ArticleEditScreen] +class ArticleEditRoute extends _i29.PageRouteInfo { + ArticleEditRoute({ + _i30.Key? key, + required String id, + List<_i29.PageRouteInfo>? children, + }) : super( + ArticleEditRoute.name, + args: ArticleEditRouteArgs(key: key, id: id), + rawPathParams: {'id': id}, + initialChildren: children, + ); + + static const String name = 'ArticleEditRoute'; + + static _i29.PageInfo page = _i29.PageInfo( + name, + builder: (data) { + final pathParams = data.inheritedPathParams; + final args = data.argsAs( + orElse: () => ArticleEditRouteArgs(id: pathParams.getString('id')), + ); + return _i4.ArticleEditScreen(key: args.key, id: args.id); + }, + ); +} + +class ArticleEditRouteArgs { + const ArticleEditRouteArgs({this.key, required this.id}); + + final _i30.Key? key; + + final String id; + + @override + String toString() { + return 'ArticleEditRouteArgs{key: $key, id: $id}'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! ArticleEditRouteArgs) return false; + return key == other.key && id == other.id; + } + + @override + int get hashCode => key.hashCode ^ id.hashCode; +} + +/// generated route for +/// [_i5.CallScreen] +class CallRoute extends _i29.PageRouteInfo { CallRoute({ - _i29.Key? key, + _i30.Key? key, required String roomId, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( CallRoute.name, args: CallRouteArgs(key: key, roomId: roomId), @@ -191,14 +318,14 @@ class CallRoute extends _i28.PageRouteInfo { static const String name = 'CallRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => CallRouteArgs(roomId: pathParams.getString('id')), ); - return _i4.CallScreen(key: args.key, roomId: args.roomId); + return _i5.CallScreen(key: args.key, roomId: args.roomId); }, ); } @@ -206,7 +333,7 @@ class CallRoute extends _i28.PageRouteInfo { class CallRouteArgs { const CallRouteArgs({this.key, required this.roomId}); - final _i29.Key? key; + final _i30.Key? key; final String roomId; @@ -227,12 +354,12 @@ class CallRouteArgs { } /// generated route for -/// [_i5.ChatDetailScreen] -class ChatDetailRoute extends _i28.PageRouteInfo { +/// [_i6.ChatDetailScreen] +class ChatDetailRoute extends _i29.PageRouteInfo { ChatDetailRoute({ - _i29.Key? key, + _i30.Key? key, required String id, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( ChatDetailRoute.name, args: ChatDetailRouteArgs(key: key, id: id), @@ -242,14 +369,14 @@ class ChatDetailRoute extends _i28.PageRouteInfo { static const String name = 'ChatDetailRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => ChatDetailRouteArgs(id: pathParams.getString('id')), ); - return _i5.ChatDetailScreen(key: args.key, id: args.id); + return _i6.ChatDetailScreen(key: args.key, id: args.id); }, ); } @@ -257,7 +384,7 @@ class ChatDetailRoute extends _i28.PageRouteInfo { class ChatDetailRouteArgs { const ChatDetailRouteArgs({this.key, required this.id}); - final _i29.Key? key; + final _i30.Key? key; final String id; @@ -278,12 +405,12 @@ class ChatDetailRouteArgs { } /// generated route for -/// [_i6.ChatListScreen] -class ChatListRoute extends _i28.PageRouteInfo { +/// [_i7.ChatListScreen] +class ChatListRoute extends _i29.PageRouteInfo { ChatListRoute({ - _i29.Key? key, + _i30.Key? key, bool isAside = false, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( ChatListRoute.name, args: ChatListRouteArgs(key: key, isAside: isAside), @@ -292,13 +419,13 @@ class ChatListRoute extends _i28.PageRouteInfo { static const String name = 'ChatListRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final args = data.argsAs( orElse: () => const ChatListRouteArgs(), ); - return _i6.ChatListScreen(key: args.key, isAside: args.isAside); + return _i7.ChatListScreen(key: args.key, isAside: args.isAside); }, ); } @@ -306,7 +433,7 @@ class ChatListRoute extends _i28.PageRouteInfo { class ChatListRouteArgs { const ChatListRouteArgs({this.key, this.isAside = false}); - final _i29.Key? key; + final _i30.Key? key; final bool isAside; @@ -327,12 +454,12 @@ class ChatListRouteArgs { } /// generated route for -/// [_i7.ChatRoomScreen] -class ChatRoomRoute extends _i28.PageRouteInfo { +/// [_i8.ChatRoomScreen] +class ChatRoomRoute extends _i29.PageRouteInfo { ChatRoomRoute({ - _i30.Key? key, + _i31.Key? key, required String id, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( ChatRoomRoute.name, args: ChatRoomRouteArgs(key: key, id: id), @@ -342,14 +469,14 @@ class ChatRoomRoute extends _i28.PageRouteInfo { static const String name = 'ChatRoomRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => ChatRoomRouteArgs(id: pathParams.getString('id')), ); - return _i7.ChatRoomScreen(key: args.key, id: args.id); + return _i8.ChatRoomScreen(key: args.key, id: args.id); }, ); } @@ -357,7 +484,7 @@ class ChatRoomRoute extends _i28.PageRouteInfo { class ChatRoomRouteArgs { const ChatRoomRouteArgs({this.key, required this.id}); - final _i30.Key? key; + final _i31.Key? key; final String id; @@ -378,44 +505,44 @@ class ChatRoomRouteArgs { } /// generated route for -/// [_i6.ChatShellScreen] -class ChatShellRoute extends _i28.PageRouteInfo { - const ChatShellRoute({List<_i28.PageRouteInfo>? children}) +/// [_i7.ChatShellScreen] +class ChatShellRoute extends _i29.PageRouteInfo { + const ChatShellRoute({List<_i29.PageRouteInfo>? children}) : super(ChatShellRoute.name, initialChildren: children); static const String name = 'ChatShellRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { - return const _i6.ChatShellScreen(); + return const _i7.ChatShellScreen(); }, ); } /// generated route for -/// [_i8.CreateAccountScreen] -class CreateAccountRoute extends _i28.PageRouteInfo { - const CreateAccountRoute({List<_i28.PageRouteInfo>? children}) +/// [_i9.CreateAccountScreen] +class CreateAccountRoute extends _i29.PageRouteInfo { + const CreateAccountRoute({List<_i29.PageRouteInfo>? children}) : super(CreateAccountRoute.name, initialChildren: children); static const String name = 'CreateAccountRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { - return const _i8.CreateAccountScreen(); + return const _i9.CreateAccountScreen(); }, ); } /// generated route for -/// [_i9.CreatorHubScreen] -class CreatorHubRoute extends _i28.PageRouteInfo { +/// [_i10.CreatorHubScreen] +class CreatorHubRoute extends _i29.PageRouteInfo { CreatorHubRoute({ - _i29.Key? key, + _i30.Key? key, bool isAside = false, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( CreatorHubRoute.name, args: CreatorHubRouteArgs(key: key, isAside: isAside), @@ -424,13 +551,13 @@ class CreatorHubRoute extends _i28.PageRouteInfo { static const String name = 'CreatorHubRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final args = data.argsAs( orElse: () => const CreatorHubRouteArgs(), ); - return _i9.CreatorHubScreen(key: args.key, isAside: args.isAside); + return _i10.CreatorHubScreen(key: args.key, isAside: args.isAside); }, ); } @@ -438,7 +565,7 @@ class CreatorHubRoute extends _i28.PageRouteInfo { class CreatorHubRouteArgs { const CreatorHubRouteArgs({this.key, this.isAside = false}); - final _i29.Key? key; + final _i30.Key? key; final bool isAside; @@ -459,29 +586,29 @@ class CreatorHubRouteArgs { } /// generated route for -/// [_i9.CreatorHubShellScreen] -class CreatorHubShellRoute extends _i28.PageRouteInfo { - const CreatorHubShellRoute({List<_i28.PageRouteInfo>? children}) +/// [_i10.CreatorHubShellScreen] +class CreatorHubShellRoute extends _i29.PageRouteInfo { + const CreatorHubShellRoute({List<_i29.PageRouteInfo>? children}) : super(CreatorHubShellRoute.name, initialChildren: children); static const String name = 'CreatorHubShellRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { - return const _i9.CreatorHubShellScreen(); + return const _i10.CreatorHubShellScreen(); }, ); } /// generated route for -/// [_i10.CreatorPostListScreen] +/// [_i11.CreatorPostListScreen] class CreatorPostListRoute - extends _i28.PageRouteInfo { + extends _i29.PageRouteInfo { CreatorPostListRoute({ - _i29.Key? key, + _i30.Key? key, required String pubName, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( CreatorPostListRoute.name, args: CreatorPostListRouteArgs(key: key, pubName: pubName), @@ -491,7 +618,7 @@ class CreatorPostListRoute static const String name = 'CreatorPostListRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; @@ -500,7 +627,7 @@ class CreatorPostListRoute () => CreatorPostListRouteArgs(pubName: pathParams.getString('name')), ); - return _i10.CreatorPostListScreen(key: args.key, pubName: args.pubName); + return _i11.CreatorPostListScreen(key: args.key, pubName: args.pubName); }, ); } @@ -508,7 +635,7 @@ class CreatorPostListRoute class CreatorPostListRouteArgs { const CreatorPostListRouteArgs({this.key, required this.pubName}); - final _i29.Key? key; + final _i30.Key? key; final String pubName; @@ -529,9 +656,9 @@ class CreatorPostListRouteArgs { } /// generated route for -/// [_i6.EditChatScreen] -class EditChatRoute extends _i28.PageRouteInfo { - EditChatRoute({_i29.Key? key, String? id, List<_i28.PageRouteInfo>? children}) +/// [_i7.EditChatScreen] +class EditChatRoute extends _i29.PageRouteInfo { + EditChatRoute({_i30.Key? key, String? id, List<_i29.PageRouteInfo>? children}) : super( EditChatRoute.name, args: EditChatRouteArgs(key: key, id: id), @@ -541,14 +668,14 @@ class EditChatRoute extends _i28.PageRouteInfo { static const String name = 'EditChatRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => EditChatRouteArgs(id: pathParams.optString('id')), ); - return _i6.EditChatScreen(key: args.key, id: args.id); + return _i7.EditChatScreen(key: args.key, id: args.id); }, ); } @@ -556,7 +683,7 @@ class EditChatRoute extends _i28.PageRouteInfo { class EditChatRouteArgs { const EditChatRouteArgs({this.key, this.id}); - final _i29.Key? key; + final _i30.Key? key; final String? id; @@ -577,12 +704,12 @@ class EditChatRouteArgs { } /// generated route for -/// [_i11.EditPublisherScreen] -class EditPublisherRoute extends _i28.PageRouteInfo { +/// [_i12.EditPublisherScreen] +class EditPublisherRoute extends _i29.PageRouteInfo { EditPublisherRoute({ - _i29.Key? key, + _i30.Key? key, String? name, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( EditPublisherRoute.name, args: EditPublisherRouteArgs(key: key, name: name), @@ -592,14 +719,14 @@ class EditPublisherRoute extends _i28.PageRouteInfo { static const String name = 'EditPublisherRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => EditPublisherRouteArgs(name: pathParams.optString('id')), ); - return _i11.EditPublisherScreen(key: args.key, name: args.name); + return _i12.EditPublisherScreen(key: args.key, name: args.name); }, ); } @@ -607,7 +734,7 @@ class EditPublisherRoute extends _i28.PageRouteInfo { class EditPublisherRouteArgs { const EditPublisherRouteArgs({this.key, this.name}); - final _i29.Key? key; + final _i30.Key? key; final String? name; @@ -628,12 +755,12 @@ class EditPublisherRouteArgs { } /// generated route for -/// [_i12.EditRealmScreen] -class EditRealmRoute extends _i28.PageRouteInfo { +/// [_i13.EditRealmScreen] +class EditRealmRoute extends _i29.PageRouteInfo { EditRealmRoute({ - _i29.Key? key, + _i30.Key? key, String? slug, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( EditRealmRoute.name, args: EditRealmRouteArgs(key: key, slug: slug), @@ -643,14 +770,14 @@ class EditRealmRoute extends _i28.PageRouteInfo { static const String name = 'EditRealmRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => EditRealmRouteArgs(slug: pathParams.optString('slug')), ); - return _i12.EditRealmScreen(key: args.key, slug: args.slug); + return _i13.EditRealmScreen(key: args.key, slug: args.slug); }, ); } @@ -658,7 +785,7 @@ class EditRealmRoute extends _i28.PageRouteInfo { class EditRealmRouteArgs { const EditRealmRouteArgs({this.key, this.slug}); - final _i29.Key? key; + final _i30.Key? key; final String? slug; @@ -679,14 +806,14 @@ class EditRealmRouteArgs { } /// generated route for -/// [_i13.EditStickerPacksScreen] +/// [_i14.EditStickerPacksScreen] class EditStickerPacksRoute - extends _i28.PageRouteInfo { + extends _i29.PageRouteInfo { EditStickerPacksRoute({ - _i29.Key? key, + _i30.Key? key, required String pubName, String? packId, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( EditStickerPacksRoute.name, args: EditStickerPacksRouteArgs( @@ -700,7 +827,7 @@ class EditStickerPacksRoute static const String name = 'EditStickerPacksRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; @@ -711,7 +838,7 @@ class EditStickerPacksRoute packId: pathParams.optString('packId'), ), ); - return _i13.EditStickerPacksScreen( + return _i14.EditStickerPacksScreen( key: args.key, pubName: args.pubName, packId: args.packId, @@ -727,7 +854,7 @@ class EditStickerPacksRouteArgs { this.packId, }); - final _i29.Key? key; + final _i30.Key? key; final String pubName; @@ -752,13 +879,13 @@ class EditStickerPacksRouteArgs { } /// generated route for -/// [_i14.EditStickersScreen] -class EditStickersRoute extends _i28.PageRouteInfo { +/// [_i15.EditStickersScreen] +class EditStickersRoute extends _i29.PageRouteInfo { EditStickersRoute({ - _i29.Key? key, + _i30.Key? key, required String packId, required String? id, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( EditStickersRoute.name, args: EditStickersRouteArgs(key: key, packId: packId, id: id), @@ -768,7 +895,7 @@ class EditStickersRoute extends _i28.PageRouteInfo { static const String name = 'EditStickersRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; @@ -779,7 +906,7 @@ class EditStickersRoute extends _i28.PageRouteInfo { id: pathParams.optString('id'), ), ); - return _i14.EditStickersScreen( + return _i15.EditStickersScreen( key: args.key, packId: args.packId, id: args.id, @@ -795,7 +922,7 @@ class EditStickersRouteArgs { required this.id, }); - final _i29.Key? key; + final _i30.Key? key; final String packId; @@ -818,12 +945,12 @@ class EditStickersRouteArgs { } /// generated route for -/// [_i15.EventCalanderScreen] -class EventCalanderRoute extends _i28.PageRouteInfo { +/// [_i16.EventCalanderScreen] +class EventCalanderRoute extends _i29.PageRouteInfo { EventCalanderRoute({ - _i29.Key? key, + _i30.Key? key, required String name, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( EventCalanderRoute.name, args: EventCalanderRouteArgs(key: key, name: name), @@ -833,7 +960,7 @@ class EventCalanderRoute extends _i28.PageRouteInfo { static const String name = 'EventCalanderRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; @@ -841,7 +968,7 @@ class EventCalanderRoute extends _i28.PageRouteInfo { orElse: () => EventCalanderRouteArgs(name: pathParams.getString('name')), ); - return _i15.EventCalanderScreen(key: args.key, name: args.name); + return _i16.EventCalanderScreen(key: args.key, name: args.name); }, ); } @@ -849,7 +976,7 @@ class EventCalanderRoute extends _i28.PageRouteInfo { class EventCalanderRouteArgs { const EventCalanderRouteArgs({this.key, required this.name}); - final _i29.Key? key; + final _i30.Key? key; final String name; @@ -870,12 +997,12 @@ class EventCalanderRouteArgs { } /// generated route for -/// [_i16.ExploreScreen] -class ExploreRoute extends _i28.PageRouteInfo { +/// [_i17.ExploreScreen] +class ExploreRoute extends _i29.PageRouteInfo { ExploreRoute({ - _i29.Key? key, + _i30.Key? key, bool isAside = false, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( ExploreRoute.name, args: ExploreRouteArgs(key: key, isAside: isAside), @@ -884,13 +1011,13 @@ class ExploreRoute extends _i28.PageRouteInfo { static const String name = 'ExploreRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final args = data.argsAs( orElse: () => const ExploreRouteArgs(), ); - return _i16.ExploreScreen(key: args.key, isAside: args.isAside); + return _i17.ExploreScreen(key: args.key, isAside: args.isAside); }, ); } @@ -898,7 +1025,7 @@ class ExploreRoute extends _i28.PageRouteInfo { class ExploreRouteArgs { const ExploreRouteArgs({this.key, this.isAside = false}); - final _i29.Key? key; + final _i30.Key? key; final bool isAside; @@ -919,93 +1046,93 @@ class ExploreRouteArgs { } /// generated route for -/// [_i16.ExploreShellScreen] -class ExploreShellRoute extends _i28.PageRouteInfo { - const ExploreShellRoute({List<_i28.PageRouteInfo>? children}) +/// [_i17.ExploreShellScreen] +class ExploreShellRoute extends _i29.PageRouteInfo { + const ExploreShellRoute({List<_i29.PageRouteInfo>? children}) : super(ExploreShellRoute.name, initialChildren: children); static const String name = 'ExploreShellRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { - return const _i16.ExploreShellScreen(); + return const _i17.ExploreShellScreen(); }, ); } /// generated route for -/// [_i17.LoginScreen] -class LoginRoute extends _i28.PageRouteInfo { - const LoginRoute({List<_i28.PageRouteInfo>? children}) +/// [_i18.LoginScreen] +class LoginRoute extends _i29.PageRouteInfo { + const LoginRoute({List<_i29.PageRouteInfo>? children}) : super(LoginRoute.name, initialChildren: children); static const String name = 'LoginRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { - return const _i17.LoginScreen(); + return const _i18.LoginScreen(); }, ); } /// generated route for -/// [_i6.NewChatScreen] -class NewChatRoute extends _i28.PageRouteInfo { - const NewChatRoute({List<_i28.PageRouteInfo>? children}) +/// [_i7.NewChatScreen] +class NewChatRoute extends _i29.PageRouteInfo { + const NewChatRoute({List<_i29.PageRouteInfo>? children}) : super(NewChatRoute.name, initialChildren: children); static const String name = 'NewChatRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { - return const _i6.NewChatScreen(); + return const _i7.NewChatScreen(); }, ); } /// generated route for -/// [_i11.NewPublisherScreen] -class NewPublisherRoute extends _i28.PageRouteInfo { - const NewPublisherRoute({List<_i28.PageRouteInfo>? children}) +/// [_i12.NewPublisherScreen] +class NewPublisherRoute extends _i29.PageRouteInfo { + const NewPublisherRoute({List<_i29.PageRouteInfo>? children}) : super(NewPublisherRoute.name, initialChildren: children); static const String name = 'NewPublisherRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { - return const _i11.NewPublisherScreen(); + return const _i12.NewPublisherScreen(); }, ); } /// generated route for -/// [_i12.NewRealmScreen] -class NewRealmRoute extends _i28.PageRouteInfo { - const NewRealmRoute({List<_i28.PageRouteInfo>? children}) +/// [_i13.NewRealmScreen] +class NewRealmRoute extends _i29.PageRouteInfo { + const NewRealmRoute({List<_i29.PageRouteInfo>? children}) : super(NewRealmRoute.name, initialChildren: children); static const String name = 'NewRealmRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { - return const _i12.NewRealmScreen(); + return const _i13.NewRealmScreen(); }, ); } /// generated route for -/// [_i13.NewStickerPacksScreen] +/// [_i14.NewStickerPacksScreen] class NewStickerPacksRoute - extends _i28.PageRouteInfo { + extends _i29.PageRouteInfo { NewStickerPacksRoute({ - _i29.Key? key, + _i30.Key? key, required String pubName, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( NewStickerPacksRoute.name, args: NewStickerPacksRouteArgs(key: key, pubName: pubName), @@ -1015,7 +1142,7 @@ class NewStickerPacksRoute static const String name = 'NewStickerPacksRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; @@ -1024,7 +1151,7 @@ class NewStickerPacksRoute () => NewStickerPacksRouteArgs(pubName: pathParams.getString('name')), ); - return _i13.NewStickerPacksScreen(key: args.key, pubName: args.pubName); + return _i14.NewStickerPacksScreen(key: args.key, pubName: args.pubName); }, ); } @@ -1032,7 +1159,7 @@ class NewStickerPacksRoute class NewStickerPacksRouteArgs { const NewStickerPacksRouteArgs({this.key, required this.pubName}); - final _i29.Key? key; + final _i30.Key? key; final String pubName; @@ -1053,12 +1180,12 @@ class NewStickerPacksRouteArgs { } /// generated route for -/// [_i14.NewStickersScreen] -class NewStickersRoute extends _i28.PageRouteInfo { +/// [_i15.NewStickersScreen] +class NewStickersRoute extends _i29.PageRouteInfo { NewStickersRoute({ - _i29.Key? key, + _i30.Key? key, required String packId, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( NewStickersRoute.name, args: NewStickersRouteArgs(key: key, packId: packId), @@ -1068,7 +1195,7 @@ class NewStickersRoute extends _i28.PageRouteInfo { static const String name = 'NewStickersRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; @@ -1076,7 +1203,7 @@ class NewStickersRoute extends _i28.PageRouteInfo { orElse: () => NewStickersRouteArgs(packId: pathParams.getString('packId')), ); - return _i14.NewStickersScreen(key: args.key, packId: args.packId); + return _i15.NewStickersScreen(key: args.key, packId: args.packId); }, ); } @@ -1084,7 +1211,7 @@ class NewStickersRoute extends _i28.PageRouteInfo { class NewStickersRouteArgs { const NewStickersRouteArgs({this.key, required this.packId}); - final _i29.Key? key; + final _i30.Key? key; final String packId; @@ -1105,30 +1232,31 @@ class NewStickersRouteArgs { } /// generated route for -/// [_i18.NotificationScreen] -class NotificationRoute extends _i28.PageRouteInfo { - const NotificationRoute({List<_i28.PageRouteInfo>? children}) +/// [_i19.NotificationScreen] +class NotificationRoute extends _i29.PageRouteInfo { + const NotificationRoute({List<_i29.PageRouteInfo>? children}) : super(NotificationRoute.name, initialChildren: children); static const String name = 'NotificationRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { - return const _i18.NotificationScreen(); + return const _i19.NotificationScreen(); }, ); } /// generated route for -/// [_i19.PostComposeScreen] -class PostComposeRoute extends _i28.PageRouteInfo { +/// [_i20.PostComposeScreen] +class PostComposeRoute extends _i29.PageRouteInfo { PostComposeRoute({ - _i29.Key? key, - _i31.SnPost? originalPost, - _i31.SnPost? repliedPost, - _i31.SnPost? forwardedPost, - List<_i28.PageRouteInfo>? children, + _i30.Key? key, + _i32.SnPost? originalPost, + _i32.SnPost? repliedPost, + _i32.SnPost? forwardedPost, + int? type, + List<_i29.PageRouteInfo>? children, }) : super( PostComposeRoute.name, args: PostComposeRouteArgs( @@ -1136,23 +1264,27 @@ class PostComposeRoute extends _i28.PageRouteInfo { originalPost: originalPost, repliedPost: repliedPost, forwardedPost: forwardedPost, + type: type, ), + rawQueryParams: {'type': type}, initialChildren: children, ); static const String name = 'PostComposeRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { + final queryParams = data.queryParams; final args = data.argsAs( - orElse: () => const PostComposeRouteArgs(), + orElse: () => PostComposeRouteArgs(type: queryParams.optInt('type')), ); - return _i19.PostComposeScreen( + return _i20.PostComposeScreen( key: args.key, originalPost: args.originalPost, repliedPost: args.repliedPost, forwardedPost: args.forwardedPost, + type: args.type, ); }, ); @@ -1164,19 +1296,22 @@ class PostComposeRouteArgs { this.originalPost, this.repliedPost, this.forwardedPost, + this.type, }); - final _i29.Key? key; + final _i30.Key? key; - final _i31.SnPost? originalPost; + final _i32.SnPost? originalPost; - final _i31.SnPost? repliedPost; + final _i32.SnPost? repliedPost; - final _i31.SnPost? forwardedPost; + final _i32.SnPost? forwardedPost; + + final int? type; @override String toString() { - return 'PostComposeRouteArgs{key: $key, originalPost: $originalPost, repliedPost: $repliedPost, forwardedPost: $forwardedPost}'; + return 'PostComposeRouteArgs{key: $key, originalPost: $originalPost, repliedPost: $repliedPost, forwardedPost: $forwardedPost, type: $type}'; } @override @@ -1186,7 +1321,8 @@ class PostComposeRouteArgs { return key == other.key && originalPost == other.originalPost && repliedPost == other.repliedPost && - forwardedPost == other.forwardedPost; + forwardedPost == other.forwardedPost && + type == other.type; } @override @@ -1194,16 +1330,17 @@ class PostComposeRouteArgs { key.hashCode ^ originalPost.hashCode ^ repliedPost.hashCode ^ - forwardedPost.hashCode; + forwardedPost.hashCode ^ + type.hashCode; } /// generated route for -/// [_i20.PostDetailScreen] -class PostDetailRoute extends _i28.PageRouteInfo { +/// [_i21.PostDetailScreen] +class PostDetailRoute extends _i29.PageRouteInfo { PostDetailRoute({ - _i29.Key? key, + _i30.Key? key, required String id, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( PostDetailRoute.name, args: PostDetailRouteArgs(key: key, id: id), @@ -1213,14 +1350,14 @@ class PostDetailRoute extends _i28.PageRouteInfo { static const String name = 'PostDetailRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => PostDetailRouteArgs(id: pathParams.getString('id')), ); - return _i20.PostDetailScreen(key: args.key, id: args.id); + return _i21.PostDetailScreen(key: args.key, id: args.id); }, ); } @@ -1228,7 +1365,7 @@ class PostDetailRoute extends _i28.PageRouteInfo { class PostDetailRouteArgs { const PostDetailRouteArgs({this.key, required this.id}); - final _i29.Key? key; + final _i30.Key? key; final String id; @@ -1249,12 +1386,12 @@ class PostDetailRouteArgs { } /// generated route for -/// [_i19.PostEditScreen] -class PostEditRoute extends _i28.PageRouteInfo { +/// [_i20.PostEditScreen] +class PostEditRoute extends _i29.PageRouteInfo { PostEditRoute({ - _i29.Key? key, + _i30.Key? key, required String id, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( PostEditRoute.name, args: PostEditRouteArgs(key: key, id: id), @@ -1264,14 +1401,14 @@ class PostEditRoute extends _i28.PageRouteInfo { static const String name = 'PostEditRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => PostEditRouteArgs(id: pathParams.getString('id')), ); - return _i19.PostEditScreen(key: args.key, id: args.id); + return _i20.PostEditScreen(key: args.key, id: args.id); }, ); } @@ -1279,7 +1416,7 @@ class PostEditRoute extends _i28.PageRouteInfo { class PostEditRouteArgs { const PostEditRouteArgs({this.key, required this.id}); - final _i29.Key? key; + final _i30.Key? key; final String id; @@ -1300,13 +1437,13 @@ class PostEditRouteArgs { } /// generated route for -/// [_i21.PublisherProfileScreen] +/// [_i22.PublisherProfileScreen] class PublisherProfileRoute - extends _i28.PageRouteInfo { + extends _i29.PageRouteInfo { PublisherProfileRoute({ - _i29.Key? key, + _i30.Key? key, required String name, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( PublisherProfileRoute.name, args: PublisherProfileRouteArgs(key: key, name: name), @@ -1316,7 +1453,7 @@ class PublisherProfileRoute static const String name = 'PublisherProfileRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; @@ -1324,7 +1461,7 @@ class PublisherProfileRoute orElse: () => PublisherProfileRouteArgs(name: pathParams.getString('name')), ); - return _i21.PublisherProfileScreen(key: args.key, name: args.name); + return _i22.PublisherProfileScreen(key: args.key, name: args.name); }, ); } @@ -1332,7 +1469,7 @@ class PublisherProfileRoute class PublisherProfileRouteArgs { const PublisherProfileRouteArgs({this.key, required this.name}); - final _i29.Key? key; + final _i30.Key? key; final String name; @@ -1353,12 +1490,12 @@ class PublisherProfileRouteArgs { } /// generated route for -/// [_i22.RealmDetailScreen] -class RealmDetailRoute extends _i28.PageRouteInfo { +/// [_i23.RealmDetailScreen] +class RealmDetailRoute extends _i29.PageRouteInfo { RealmDetailRoute({ - _i29.Key? key, + _i30.Key? key, required String slug, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( RealmDetailRoute.name, args: RealmDetailRouteArgs(key: key, slug: slug), @@ -1368,14 +1505,14 @@ class RealmDetailRoute extends _i28.PageRouteInfo { static const String name = 'RealmDetailRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => RealmDetailRouteArgs(slug: pathParams.getString('slug')), ); - return _i22.RealmDetailScreen(key: args.key, slug: args.slug); + return _i23.RealmDetailScreen(key: args.key, slug: args.slug); }, ); } @@ -1383,7 +1520,7 @@ class RealmDetailRoute extends _i28.PageRouteInfo { class RealmDetailRouteArgs { const RealmDetailRouteArgs({this.key, required this.slug}); - final _i29.Key? key; + final _i30.Key? key; final String slug; @@ -1404,62 +1541,62 @@ class RealmDetailRouteArgs { } /// generated route for -/// [_i12.RealmListScreen] -class RealmListRoute extends _i28.PageRouteInfo { - const RealmListRoute({List<_i28.PageRouteInfo>? children}) +/// [_i13.RealmListScreen] +class RealmListRoute extends _i29.PageRouteInfo { + const RealmListRoute({List<_i29.PageRouteInfo>? children}) : super(RealmListRoute.name, initialChildren: children); static const String name = 'RealmListRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { - return const _i12.RealmListScreen(); + return const _i13.RealmListScreen(); }, ); } /// generated route for -/// [_i23.RelationshipScreen] -class RelationshipRoute extends _i28.PageRouteInfo { - const RelationshipRoute({List<_i28.PageRouteInfo>? children}) +/// [_i24.RelationshipScreen] +class RelationshipRoute extends _i29.PageRouteInfo { + const RelationshipRoute({List<_i29.PageRouteInfo>? children}) : super(RelationshipRoute.name, initialChildren: children); static const String name = 'RelationshipRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { - return const _i23.RelationshipScreen(); + return const _i24.RelationshipScreen(); }, ); } /// generated route for -/// [_i24.SettingsScreen] -class SettingsRoute extends _i28.PageRouteInfo { - const SettingsRoute({List<_i28.PageRouteInfo>? children}) +/// [_i25.SettingsScreen] +class SettingsRoute extends _i29.PageRouteInfo { + const SettingsRoute({List<_i29.PageRouteInfo>? children}) : super(SettingsRoute.name, initialChildren: children); static const String name = 'SettingsRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { - return const _i24.SettingsScreen(); + return const _i25.SettingsScreen(); }, ); } /// generated route for -/// [_i14.StickerPackDetailScreen] +/// [_i15.StickerPackDetailScreen] class StickerPackDetailRoute - extends _i28.PageRouteInfo { + extends _i29.PageRouteInfo { StickerPackDetailRoute({ - _i29.Key? key, + _i30.Key? key, required String pubName, required String id, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( StickerPackDetailRoute.name, args: StickerPackDetailRouteArgs(key: key, pubName: pubName, id: id), @@ -1469,7 +1606,7 @@ class StickerPackDetailRoute static const String name = 'StickerPackDetailRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; @@ -1480,7 +1617,7 @@ class StickerPackDetailRoute id: pathParams.getString('packId'), ), ); - return _i14.StickerPackDetailScreen( + return _i15.StickerPackDetailScreen( key: args.key, pubName: args.pubName, id: args.id, @@ -1496,7 +1633,7 @@ class StickerPackDetailRouteArgs { required this.id, }); - final _i29.Key? key; + final _i30.Key? key; final String pubName; @@ -1519,12 +1656,12 @@ class StickerPackDetailRouteArgs { } /// generated route for -/// [_i13.StickersScreen] -class StickersRoute extends _i28.PageRouteInfo { +/// [_i14.StickersScreen] +class StickersRoute extends _i29.PageRouteInfo { StickersRoute({ - _i29.Key? key, + _i30.Key? key, required String pubName, - List<_i28.PageRouteInfo>? children, + List<_i29.PageRouteInfo>? children, }) : super( StickersRoute.name, args: StickersRouteArgs(key: key, pubName: pubName), @@ -1534,14 +1671,14 @@ class StickersRoute extends _i28.PageRouteInfo { static const String name = 'StickersRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => StickersRouteArgs(pubName: pathParams.getString('name')), ); - return _i13.StickersScreen(key: args.key, pubName: args.pubName); + return _i14.StickersScreen(key: args.key, pubName: args.pubName); }, ); } @@ -1549,7 +1686,7 @@ class StickersRoute extends _i28.PageRouteInfo { class StickersRouteArgs { const StickersRouteArgs({this.key, required this.pubName}); - final _i29.Key? key; + final _i30.Key? key; final String pubName; @@ -1570,14 +1707,14 @@ class StickersRouteArgs { } /// generated route for -/// [_i25.TabsNavigationWidget] +/// [_i26.TabsNavigationWidget] class TabsNavigationWidget - extends _i28.PageRouteInfo { + extends _i29.PageRouteInfo { TabsNavigationWidget({ - _i29.Key? key, - required _i29.Widget child, - required _i32.AppRouter router, - List<_i28.PageRouteInfo>? children, + _i30.Key? key, + required _i30.Widget child, + required _i33.AppRouter router, + List<_i29.PageRouteInfo>? children, }) : super( TabsNavigationWidget.name, args: TabsNavigationWidgetArgs(key: key, child: child, router: router), @@ -1586,11 +1723,11 @@ class TabsNavigationWidget static const String name = 'TabsNavigationWidget'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { final args = data.argsAs(); - return _i25.TabsNavigationWidget( + return _i26.TabsNavigationWidget( key: args.key, child: args.child, router: args.router, @@ -1606,11 +1743,11 @@ class TabsNavigationWidgetArgs { required this.router, }); - final _i29.Key? key; + final _i30.Key? key; - final _i29.Widget child; + final _i30.Widget child; - final _i32.AppRouter router; + final _i33.AppRouter router; @override String toString() { @@ -1629,33 +1766,33 @@ class TabsNavigationWidgetArgs { } /// generated route for -/// [_i26.UpdateProfileScreen] -class UpdateProfileRoute extends _i28.PageRouteInfo { - const UpdateProfileRoute({List<_i28.PageRouteInfo>? children}) +/// [_i27.UpdateProfileScreen] +class UpdateProfileRoute extends _i29.PageRouteInfo { + const UpdateProfileRoute({List<_i29.PageRouteInfo>? children}) : super(UpdateProfileRoute.name, initialChildren: children); static const String name = 'UpdateProfileRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { - return const _i26.UpdateProfileScreen(); + return const _i27.UpdateProfileScreen(); }, ); } /// generated route for -/// [_i27.WalletScreen] -class WalletRoute extends _i28.PageRouteInfo { - const WalletRoute({List<_i28.PageRouteInfo>? children}) +/// [_i28.WalletScreen] +class WalletRoute extends _i29.PageRouteInfo { + const WalletRoute({List<_i29.PageRouteInfo>? children}) : super(WalletRoute.name, initialChildren: children); static const String name = 'WalletRoute'; - static _i28.PageInfo page = _i28.PageInfo( + static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { - return const _i27.WalletScreen(); + return const _i28.WalletScreen(); }, ); } diff --git a/lib/screens/creators/posts/list.dart b/lib/screens/creators/posts/list.dart index 5227151..c7edcc5 100644 --- a/lib/screens/creators/posts/list.dart +++ b/lib/screens/creators/posts/list.dart @@ -1,9 +1,12 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/widgets/app_scaffold.dart'; +import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/post/post_list.dart'; +import 'package:material_symbols_icons/symbols.dart'; @RoutePage() class CreatorPostListScreen extends HookConsumerWidget { @@ -15,13 +18,62 @@ class CreatorPostListScreen extends HookConsumerWidget { @override 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( appBar: AppBar(title: Text('posts').tr()), body: CustomScrollView( + key: ValueKey(refreshKey.value), slivers: [ SliverPostList(pubName: pubName, itemType: PostItemType.creator), ], ), + floatingActionButton: FloatingActionButton( + onPressed: showCreatePostSheet, + child: const Icon(Symbols.add), + ), ); } } diff --git a/lib/screens/posts/compose.dart b/lib/screens/posts/compose.dart index cef2760..81d3914 100644 --- a/lib/screens/posts/compose.dart +++ b/lib/screens/posts/compose.dart @@ -1,28 +1,22 @@ 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:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:island/models/file.dart'; import 'package:island/models/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/posts/detail.dart'; -import 'package:island/services/file.dart'; +import 'package:island/screens/posts/compose_article.dart'; import 'package:island/services/responsive.dart'; -import 'package:island/widgets/alert.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/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/screens/posts/detail.dart'; +import 'package:island/widgets/post/compose_settings_sheet.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:pasteboard/pasteboard.dart'; import 'package:styled_widget/styled_widget.dart'; @RoutePage() @@ -54,338 +48,69 @@ class PostComposeScreen extends HookConsumerWidget { final SnPost? originalPost; final SnPost? repliedPost; final SnPost? forwardedPost; + final int? type; const PostComposeScreen({ super.key, this.originalPost, this.repliedPost, this.forwardedPost, + @QueryParam('type') this.type, }); @override Widget build(BuildContext context, WidgetRef ref) { - // Extract common theme and localization to avoid repeated lookups + // 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 currentPublisher = useState(null); + final state = useMemoized( + () => ComposeLogic.createState( + originalPost: originalPost, + forwardedPost: forwardedPost, + ), + [originalPost, forwardedPost], + ); // Initialize publisher once when data is available useEffect(() { if (publishers.value?.isNotEmpty ?? false) { - currentPublisher.value = publishers.value!.first; + state.currentPublisher.value = publishers.value!.first; } return null; }, [publishers]); - // State management - final attachments = useState>( - 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() ?? - [], - ); - 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), - ); - final visibility = useState(originalPost?.visibility ?? 0); - final submitting = useState(false); - final attachmentProgress = useState>({}); + // Dispose state when widget is disposed + useEffect(() { + return () => ComposeLogic.dispose(state); + }, []); - // Media handling functions - Future 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), - ), - ]; - } + // Helper methods - Future 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), - ]; - } - - // Helper method to get mimetype from file type - String getMimeTypeFromFileType(UniversalFileType type) { - return switch (type) { - UniversalFileType.image => 'image/unknown', - UniversalFileType.video => 'video/unknown', - UniversalFileType.audio => 'audio/unknown', - UniversalFileType.file => 'application/octet-stream', - }; - } - - // Attachment management functions - Future uploadAttachment(int index) async { - final attachment = 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 - attachmentProgress.value = {...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, _) { - attachmentProgress.value = { - ...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(attachments.value); - clone[index] = UniversalFile(data: cloudFile, type: attachment.type); - attachments.value = clone; - } catch (err) { - showErrorAlert(err); - } finally { - // Clean up progress state - attachmentProgress.value = {...attachmentProgress.value}..remove(index); - } - } - - // Helper method to move attachment in the list - List moveAttachment( - List 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; - } - - Future 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; - } - - // Form submission - Future performAction() async { - if (submitting.value) return; - - try { - submitting.value = true; - - // Upload any local attachments first - await Future.wait( - attachments.value - .asMap() - .entries - .where((entry) => entry.value.isOnDevice) - .map((entry) => uploadAttachment(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': titleController.text, - 'description': descriptionController.text, - 'content': contentController.text, - 'visibility': visibility.value, - '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, - }; - - // Send request - await client.request( - endpoint, - data: payload, - options: Options( - headers: {'X-Pub': currentPublisher.value?.name}, - method: isNewPost ? 'POST' : 'PATCH', - ), - ); - - if (context.mounted) { - context.maybePop(true); - } - } catch (err) { - showErrorAlert(err); - } finally { - submitting.value = false; - } - } - - // Clipboard handling - Future 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; - final isSubmit = event.logicalKey == LogicalKeyboardKey.enter; - - if (isPaste && isModifierPressed) { - handlePaste(); - } else if (isSubmit && isModifierPressed && !submitting.value) { - performAction(); - } - } - - // Helper method to build visibility option - Widget buildVisibilityOption( - BuildContext context, - int value, - IconData icon, - String textKey, - ) { - return ListTile( - leading: Icon(icon), - title: Text(textKey.tr()), - onTap: () { - visibility.value = value; - Navigator.pop(context); - }, - selected: visibility.value == value, - ); - } - - // Helper method to get the appropriate icon for each visibility status - IconData getVisibilityIcon(int visibilityValue) { - switch (visibilityValue) { - case 1: // Friends - return Symbols.group; - case 2: // Unlisted - return Symbols.link_off; - case 3: // Private - return Symbols.lock; - default: // Public (0) or unknown - return Symbols.public; - } - } - - // Helper method to get the translation key for each visibility status - String getVisibilityText(int visibilityValue) { - switch (visibilityValue) { - case 1: // Friends - return 'postVisibilityFriends'; - case 2: // Unlisted - return 'postVisibilityUnlisted'; - case 3: // Private - return 'postVisibilityPrivate'; - default: // Public (0) or unknown - return 'postVisibilityPublic'; - } - } - - // Visibility handling - void showVisibilityModal() { - showDialog( + void showSettingsSheet() { + showModalBottomSheet( context: context, + isScrollControlled: true, builder: - (context) => AlertDialog( - title: Text('postVisibility'.tr()), - content: 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', - ), - ], - ), + (context) => ComposeSettingsSheet( + titleController: state.titleController, + descriptionController: state.descriptionController, + visibility: state.visibility, + onVisibilityChanged: () { + // Trigger rebuild if needed + }, ), ); } - // Show keyboard shortcuts dialog void showKeyboardShortcutsDialog() { showDialog( context: context, @@ -412,47 +137,55 @@ class PostComposeScreen extends HookConsumerWidget { ); } - // Helper method to build wide attachment grid - Widget buildWideAttachmentGrid( - BoxConstraints constraints, - List attachments, - Map progress, - ) { - return Wrap( - spacing: 8, - runSpacing: 8, - children: [ - for (var idx = 0; idx < attachments.length; idx++) - SizedBox( - width: constraints.maxWidth / 2 - 4, - child: AttachmentPreview( - item: attachments[idx], - progress: progress[idx], - onRequestUpload: () => uploadAttachment(idx), - onDelete: () => deleteAttachment(idx), - onMove: (delta) => moveAttachment(attachments, idx, delta), - ), - ), - ], + Widget buildWideAttachmentGrid() { + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + itemCount: state.attachments.value.length, + 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, + ); + }, + ); + }, ); } - // Helper method to build narrow attachment list - Widget buildNarrowAttachmentList( - List attachments, - Map progress, - ) { + Widget buildNarrowAttachmentList() { return Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 8, children: [ - for (var idx = 0; idx < attachments.length; idx++) - AttachmentPreview( - item: attachments[idx], - progress: progress[idx], - onRequestUpload: () => uploadAttachment(idx), - onDelete: () => deleteAttachment(idx), - onMove: (delta) => moveAttachment(attachments, idx, delta), + 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, + ); + }, + ), ), ], ); @@ -467,6 +200,11 @@ class PostComposeScreen extends HookConsumerWidget { ? Text(originalPost != null ? 'editPost'.tr() : 'newPost'.tr()) : null, actions: [ + IconButton( + icon: const Icon(Symbols.settings), + onPressed: showSettingsSheet, + tooltip: 'postSettings'.tr(), + ), if (isWideScreen(context)) Tooltip( message: 'keyboard_shortcuts'.tr(), @@ -475,21 +213,37 @@ class PostComposeScreen extends HookConsumerWidget { onPressed: showKeyboardShortcutsDialog, ), ), - IconButton( - onPressed: submitting.value ? null : performAction, - icon: - submitting.value - ? SizedBox( - width: 28, - height: 28, - child: const CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2.5, - ), - ).center() - : Icon( - originalPost != null ? Symbols.edit : Symbols.upload, - ), + ValueListenableBuilder( + valueListenable: state.submitting, + builder: (context, submitting, _) { + return IconButton( + onPressed: + submitting + ? null + : () => ComposeLogic.performAction( + ref, + state, + context, + originalPost: originalPost, + repliedPost: repliedPost, + forwardedPost: forwardedPost, + postType: 0, // Regular post type + ), + icon: + submitting + ? SizedBox( + width: 28, + height: 28, + child: const CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2.5, + ), + ).center() + : Icon( + originalPost != null ? Symbols.edit : Symbols.upload, + ), + ); + }, ), const Gap(8), ], @@ -498,20 +252,7 @@ class PostComposeScreen extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ // Reply/Forward info section - if (repliedPost != null) - _buildInfoBanner( - context, - Symbols.reply, - 'reply', - repliedPost!.publisher.nick, - ), - if (forwardedPost != null) - _buildInfoBanner( - context, - Symbols.forward, - 'forward', - forwardedPost!.publisher.nick, - ), + _buildInfoBanner(context), // Main content area Expanded( @@ -522,10 +263,10 @@ class PostComposeScreen extends HookConsumerWidget { // Publisher profile picture GestureDetector( child: ProfilePictureWidget( - fileId: currentPublisher.value?.picture?.id, + fileId: state.currentPublisher.value?.picture?.id, radius: 20, fallbackIcon: - currentPublisher.value == null + state.currentPublisher.value == null ? Symbols.question_mark : null, ), @@ -533,9 +274,11 @@ class PostComposeScreen extends HookConsumerWidget { showModalBottomSheet( isScrollControlled: true, context: context, - builder: (context) => PublisherModal(), + builder: (context) => const PublisherModal(), ).then((value) { - if (value is SnPublisher) currentPublisher.value = value; + if (value != null) { + state.currentPublisher.value = value; + } }); }, ).padding(top: 16), @@ -543,89 +286,31 @@ class PostComposeScreen extends HookConsumerWidget { // Post content form Expanded( child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(vertical: 16), + padding: const EdgeInsets.symmetric(vertical: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Visibility selector - Row( - children: [ - OutlinedButton( - onPressed: showVisibilityModal, - style: OutlinedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - side: BorderSide( - color: colorScheme.primary.withOpacity(0.5), - ), - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - visualDensity: const VisualDensity( - vertical: -2, - horizontal: -4, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - getVisibilityIcon(visibility.value), - size: 16, - color: colorScheme.primary, - ), - const SizedBox(width: 6), - Text( - getVisibilityText(visibility.value).tr(), - style: TextStyle( - fontSize: 14, - color: colorScheme.primary, - ), - ), - ], - ), - ), - ], - ).padding(bottom: 6), - - // Title field - TextField( - controller: titleController, - decoration: InputDecoration.collapsed( - hintText: 'postTitle'.tr(), - ), - style: const TextStyle(fontSize: 16), - onTapOutside: - (_) => - FocusManager.instance.primaryFocus?.unfocus(), - ), - - // Description field - TextField( - controller: descriptionController, - decoration: InputDecoration.collapsed( - hintText: 'postDescription'.tr(), - ), - style: const TextStyle(fontSize: 16), - onTapOutside: - (_) => - FocusManager.instance.primaryFocus?.unfocus(), - ), - - const Gap(8), - - // Content field with keyboard listener + // Content field with borderless design RawKeyboardListener( 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( - controller: contentController, - style: const TextStyle(fontSize: 14), + controller: state.contentController, + style: theme.textTheme.bodyMedium, decoration: InputDecoration( border: InputBorder.none, - hintText: 'postPlaceholder'.tr(), - isDense: true, + hintText: 'postContent'.tr(), + contentPadding: const EdgeInsets.all(8), ), maxLines: null, onTapOutside: @@ -642,15 +327,8 @@ class PostComposeScreen extends HookConsumerWidget { builder: (context, constraints) { final isWide = isWideScreen(context); return isWide - ? buildWideAttachmentGrid( - constraints, - attachments.value, - attachmentProgress.value, - ) - : buildNarrowAttachmentList( - attachments.value, - attachmentProgress.value, - ); + ? buildWideAttachmentGrid() + : buildNarrowAttachmentList(); }, ), ], @@ -667,12 +345,12 @@ class PostComposeScreen extends HookConsumerWidget { child: Row( children: [ IconButton( - onPressed: pickPhotoMedia, + onPressed: () => ComposeLogic.pickPhotoMedia(ref, state), icon: const Icon(Symbols.add_a_photo), color: colorScheme.primary, ), IconButton( - onPressed: pickVideoMedia, + onPressed: () => ComposeLogic.pickVideoMedia(ref, state), icon: const Icon(Symbols.videocam), color: colorScheme.primary, ), @@ -688,30 +366,36 @@ class PostComposeScreen extends HookConsumerWidget { ); } - // Helper method to build info banner for replied/forwarded posts - Widget _buildInfoBanner( - BuildContext context, - IconData icon, - String labelKey, - String publisherNick, - ) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5), - child: Row( - children: [ - Icon(icon, size: 16), - const Gap(8), - Expanded( - child: Text( - '${'labelKey'.tr()}: $publisherNick', - style: Theme.of(context).textTheme.bodySmall, - maxLines: 1, - overflow: TextOverflow.ellipsis, + 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(); } } diff --git a/lib/screens/posts/compose_article.dart b/lib/screens/posts/compose_article.dart new file mode 100644 index 0000000..478deec --- /dev/null +++ b/lib/screens/posts/compose_article.dart @@ -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( + 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, + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/post/compose_settings_sheet.dart b/lib/widgets/post/compose_settings_sheet.dart new file mode 100644 index 0000000..09b4308 --- /dev/null +++ b/lib/widgets/post/compose_settings_sheet.dart @@ -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 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, + ), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/post/compose_shared.dart b/lib/widgets/post/compose_shared.dart new file mode 100644 index 0000000..c01fb04 --- /dev/null +++ b/lib/widgets/post/compose_shared.dart @@ -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> attachments; + final TextEditingController titleController; + final TextEditingController descriptionController; + final TextEditingController contentController; + final ValueNotifier visibility; + final ValueNotifier submitting; + final ValueNotifier> attachmentProgress; + final ValueNotifier 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>( + 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(originalPost?.visibility ?? 0), + submitting: ValueNotifier(false), + attachmentProgress: ValueNotifier>({}), + currentPublisher: ValueNotifier(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 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 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 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 moveAttachment( + List 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 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 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 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(); + } +}