diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f0499dc..f0babb0 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -42,6 +42,12 @@ + + get routes => [ RedirectRoute(path: '/', redirectTo: '/explore'), - AutoRoute(page: ExploreRoute.page, path: '/explore'), + AutoRoute( + page: ExploreShellRoute.page, + path: '/explore', + 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: AccountShellRoute.page, path: '/account', @@ -64,9 +73,6 @@ class AppRouter extends RootStackRouter { AutoRoute(page: LoginRoute.page, path: '/auth/login'), AutoRoute(page: CreateAccountRoute.page, path: '/auth/create-account'), AutoRoute(page: SettingsRoute.page, path: '/settings'), - AutoRoute(page: PostComposeRoute.page, path: '/posts/compose'), - AutoRoute(page: PostDetailRoute.page, path: '/posts/:id'), - AutoRoute(page: PostEditRoute.page, path: '/posts/:id/edit'), AutoRoute(page: NewRealmRoute.page, path: '/realms/new'), AutoRoute(page: RealmDetailRoute.page, path: '/realms/:slug'), AutoRoute(page: EditRealmRoute.page, path: '/realms/:slug/edit'), diff --git a/lib/route.gr.dart b/lib/route.gr.dart index 8acedbc..37d873d 100644 --- a/lib/route.gr.dart +++ b/lib/route.gr.dart @@ -598,16 +598,55 @@ class EditStickersRouteArgs { /// generated route for /// [_i13.ExploreScreen] -class ExploreRoute extends _i26.PageRouteInfo { - const ExploreRoute({List<_i26.PageRouteInfo>? children}) - : super(ExploreRoute.name, initialChildren: children); +class ExploreRoute extends _i26.PageRouteInfo { + ExploreRoute({ + _i27.Key? key, + bool isAside = false, + List<_i26.PageRouteInfo>? children, + }) : super( + ExploreRoute.name, + args: ExploreRouteArgs(key: key, isAside: isAside), + initialChildren: children, + ); static const String name = 'ExploreRoute'; static _i26.PageInfo page = _i26.PageInfo( name, builder: (data) { - return const _i13.ExploreScreen(); + final args = data.argsAs( + orElse: () => const ExploreRouteArgs(), + ); + return _i13.ExploreScreen(key: args.key, isAside: args.isAside); + }, + ); +} + +class ExploreRouteArgs { + const ExploreRouteArgs({this.key, this.isAside = false}); + + final _i27.Key? key; + + final bool isAside; + + @override + String toString() { + return 'ExploreRouteArgs{key: $key, isAside: $isAside}'; + } +} + +/// generated route for +/// [_i13.ExploreShellScreen] +class ExploreShellRoute extends _i26.PageRouteInfo { + const ExploreShellRoute({List<_i26.PageRouteInfo>? children}) + : super(ExploreShellRoute.name, initialChildren: children); + + static const String name = 'ExploreShellRoute'; + + static _i26.PageInfo page = _i26.PageInfo( + name, + builder: (data) { + return const _i13.ExploreShellScreen(); }, ); } diff --git a/lib/screens/account.dart b/lib/screens/account.dart index b9665fc..4c02c98 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -31,9 +31,9 @@ class AccountShellScreen extends HookConsumerWidget { isRoot: true, child: Row( children: [ - SizedBox(width: 360, child: AccountScreen(isAside: true)), + Flexible(flex: 2, child: AccountScreen(isAside: true)), VerticalDivider(width: 1), - Expanded(child: AutoRouter()), + Flexible(flex: 3, child: AutoRouter()), ], ), ); diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index 5fc0fc6..2827d5c 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -30,6 +31,7 @@ import 'package:super_sliver_list/super_sliver_list.dart'; import 'package:uuid/uuid.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:super_clipboard/super_clipboard.dart'; import 'chat.dart'; part 'room.g.dart'; @@ -667,6 +669,9 @@ class ChatRoomScreen extends HookConsumerWidget { clone.insert(idx + delta, clone.removeAt(idx)); attachments.value = clone; }, + onAttachmentsChanged: (newAttachments) { + attachments.value = newAttachments; + }, ), error: (_, __) => const SizedBox.shrink(), loading: () => const SizedBox.shrink(), @@ -690,6 +695,7 @@ class _ChatInput extends ConsumerWidget { final Function(int) onUploadAttachment; final Function(int) onDeleteAttachment; final Function(int, int) onMoveAttachment; + final Function(List) onAttachmentsChanged; const _ChatInput({ required this.messageController, @@ -704,14 +710,22 @@ class _ChatInput extends ConsumerWidget { required this.onUploadAttachment, required this.onDeleteAttachment, required this.onMoveAttachment, + required this.onAttachmentsChanged, }); void _handleKeyPress(BuildContext context, WidgetRef ref, RawKeyEvent event) { if (event is! RawKeyDownEvent) return; + final isPaste = event.logicalKey == LogicalKeyboardKey.keyV; + final isModifierPressed = event.isMetaPressed || event.isControlPressed; + + if (isPaste && isModifierPressed) { + _handlePaste(); + return; + } + final enterToSend = ref.read(appSettingsProvider).enterToSend; final isEnter = event.logicalKey == LogicalKeyboardKey.enter; - final isModifierPressed = event.isMetaPressed || event.isControlPressed; if (isEnter) { if (enterToSend && !isModifierPressed) { @@ -722,6 +736,36 @@ class _ChatInput extends ConsumerWidget { } } + Future _handlePaste() async { + final clipboard = SystemClipboard.instance; + if (clipboard == null) return; + + final reader = await clipboard.read(); + if (reader.canProvide(Formats.png)) { + reader.getFile(Formats.png, (file) async { + final stream = file.getStream(); + final bytes = await stream.toList(); + final imageBytes = bytes.expand((e) => e).toList(); + + // Create a temporary file to store the image + final tempDir = Directory.systemTemp; + final tempFile = File( + '${tempDir.path}/pasted_image_${DateTime.now().millisecondsSinceEpoch}.png', + ); + await tempFile.writeAsBytes(imageBytes); + + // Add the file to attachments + onAttachmentsChanged([ + ...attachments, + UniversalFile( + data: XFile(tempFile.path), + type: UniversalFileType.image, + ), + ]); + }); + } + } + @override Widget build(BuildContext context, WidgetRef ref) { final enterToSend = ref.watch(appSettingsProvider).enterToSend; @@ -748,7 +792,7 @@ class _ChatInput extends ConsumerWidget { }, separatorBuilder: (_, __) => const Gap(8), ), - ), + ).padding(top: 12), if (messageReplyingTo != null || messageForwardingTo != null || messageEditingTo != null) diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index 233d906..2328d70 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -21,15 +21,44 @@ import 'package:island/pods/network.dart'; part 'explore.g.dart'; @RoutePage() -class ExploreScreen extends ConsumerWidget { - const ExploreScreen({super.key}); +class ExploreShellScreen extends ConsumerWidget { + const ExploreShellScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final activitiesNotifier = ref.watch(activityListNotifierProvider.notifier); - final isWide = isWideScreen(context); + if (isWide) { + return AppBackground( + isRoot: true, + child: Row( + children: [ + Flexible(flex: 2, child: ExploreScreen(isAside: true)), + VerticalDivider(width: 1), + Flexible(flex: 3, child: AutoRouter()), + ], + ), + ); + } + + return AppBackground(isRoot: true, child: AutoRouter()); + } +} + +@RoutePage() +class ExploreScreen extends ConsumerWidget { + final bool isAside; + const ExploreScreen({super.key, this.isAside = false}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isWide = isWideScreen(context); + if (isWide && !isAside) { + return const EmptyPageHolder(); + } + + final activitiesNotifier = ref.watch(activityListNotifierProvider.notifier); + return TourTriggerWidget( child: AppScaffold( appBar: AppBar(title: const Text('explore').tr()), @@ -53,38 +82,11 @@ class ExploreScreen extends ConsumerWidget { notifierRefreshable: activityListNotifierProvider.notifier, contentBuilder: (data, widgetCount, endItemView) => Center( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: kWideScreenWidth - 160, - ), - child: - isWide - ? Card( - elevation: 8, - margin: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - color: Theme.of(context) - .colorScheme - .surfaceContainerLow - .withOpacity(0.8), - child: _ActivityListView( - data: data, - widgetCount: widgetCount, - endItemView: endItemView, - activitiesNotifier: activitiesNotifier, - ), - ) - : _ActivityListView( - data: data, - widgetCount: widgetCount, - endItemView: endItemView, - activitiesNotifier: activitiesNotifier, - ), + child: _ActivityListView( + data: data, + widgetCount: widgetCount, + endItemView: endItemView, + activitiesNotifier: activitiesNotifier, ), ), ), diff --git a/lib/screens/posts/compose.dart b/lib/screens/posts/compose.dart index 0d76321..edf09bf 100644 --- a/lib/screens/posts/compose.dart +++ b/lib/screens/posts/compose.dart @@ -6,6 +6,7 @@ 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'; @@ -23,6 +24,7 @@ import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/post/publishers_modal.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:styled_widget/styled_widget.dart'; +import 'package:super_clipboard/super_clipboard.dart'; @RoutePage() class PostEditScreen extends HookConsumerWidget { @@ -215,6 +217,47 @@ class PostComposeScreen extends HookConsumerWidget { } } + Future _handlePaste() async { + final clipboard = SystemClipboard.instance; + if (clipboard == null) return; + + final reader = await clipboard.read(); + if (reader.canProvide(Formats.png)) { + reader.getFile(Formats.png, (file) async { + final stream = file.getStream(); + final bytes = await stream.toList(); + final imageBytes = bytes.expand((e) => e).toList(); + + // Create a temporary file to store the image + final tempDir = Directory.systemTemp; + final tempFile = File( + '${tempDir.path}/pasted_image_${DateTime.now().millisecondsSinceEpoch}.png', + ); + await tempFile.writeAsBytes(imageBytes); + + // Add the file to attachments + attachments.value = [ + ...attachments.value, + UniversalFile( + data: XFile(tempFile.path), + type: UniversalFileType.image, + ), + ]; + }); + } + } + + void _handleKeyPress(RawKeyEvent event) { + if (event is! RawKeyDownEvent) return; + + final isPaste = event.logicalKey == LogicalKeyboardKey.keyV; + final isModifierPressed = event.isMetaPressed || event.isControlPressed; + + if (isPaste && isModifierPressed) { + _handlePaste(); + } + } + return AppScaffold( appBar: AppBar( leading: const PageBackButton(), @@ -291,17 +334,22 @@ class PostComposeScreen extends HookConsumerWidget { FocusManager.instance.primaryFocus?.unfocus(), ), const Gap(8), - TextField( - controller: contentController, - style: TextStyle(fontSize: 14), - decoration: InputDecoration( - border: InputBorder.none, - hintText: 'postPlaceholder'.tr(), - isDense: true, + RawKeyboardListener( + focusNode: FocusNode(), + onKey: _handleKeyPress, + child: TextField( + controller: contentController, + style: TextStyle(fontSize: 14), + decoration: InputDecoration( + border: InputBorder.none, + hintText: 'postPlaceholder'.tr(), + isDense: true, + ), + onTapOutside: + (_) => + FocusManager.instance.primaryFocus + ?.unfocus(), ), - onTapOutside: - (_) => - FocusManager.instance.primaryFocus?.unfocus(), ), const Gap(8), Column( diff --git a/lib/screens/posts/detail.dart b/lib/screens/posts/detail.dart index 1058de8..10e5acd 100644 --- a/lib/screens/posts/detail.dart +++ b/lib/screens/posts/detail.dart @@ -36,19 +36,25 @@ class PostDetailScreen extends HookConsumerWidget { appBar: AppBar(title: const Text('Post')), body: post.when( data: (post) { - final content = Stack( + return Stack( fit: StackFit.expand, children: [ - Column( - children: [ - PostItem( - item: post!, - isOpenable: false, - backgroundColor: isWide ? Colors.transparent : null, + CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Column( + children: [ + PostItem( + item: post!, + isOpenable: false, + backgroundColor: isWide ? Colors.transparent : null, + ), + const Divider(height: 1), + ], + ), ), - const Divider(height: 1), - Expanded(child: PostRepliesList(postId: id)), - Gap(MediaQuery.of(context).padding.bottom), + PostRepliesList(postId: id), + SliverGap(MediaQuery.of(context).padding.bottom + 80), ], ), Positioned( @@ -67,30 +73,6 @@ class PostDetailScreen extends HookConsumerWidget { ), ], ); - - return isWide - ? Center( - child: Card( - elevation: 8, - margin: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - color: Theme.of( - context, - ).colorScheme.surfaceContainerLow.withOpacity(0.8), - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: kWideScreenWidth - 160, - ), - child: content, - ), - ), - ) - : content; }, loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Text('Error: $e'), diff --git a/lib/widgets/post/post_replies.dart b/lib/widgets/post/post_replies.dart index f0b4c36..adb33ec 100644 --- a/lib/widgets/post/post_replies.dart +++ b/lib/widgets/post/post_replies.dart @@ -18,56 +18,48 @@ class PostRepliesList extends HookConsumerWidget { final postAsync = ref.watch(postRepliesProvider(postId)); final isWide = isWideScreen(context); - return RefreshIndicator( - onRefresh: - () => Future.sync((() { - ref.invalidate(postRepliesProvider(postId)); - })), - child: postAsync.when( - data: - (controller) => RefreshIndicator( - onRefresh: - () => Future.sync((() { - ref.invalidate(postRepliesProvider(postId)); - })), - child: InfiniteList( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom, - ), - itemCount: controller.posts.length, - isLoading: controller.isLoading, - hasReachedMax: controller.hasReachedMax, - onFetchData: controller.fetchMore, - itemBuilder: (context, index) { - final post = controller.posts[index]; - return PostItem( - item: post, - backgroundColor: isWide ? Colors.transparent : null, - ); - }, - separatorBuilder: (_, __) => const Divider(height: 1), - emptyBuilder: (context) { - return Column( - children: [ - Text( - 'No replies', - textAlign: TextAlign.center, - ).fontSize(18).bold(), - Text('Why not start a discussion?'), - ], - ).padding(vertical: 16); - }, - ), - ), - loading: () => const Center(child: CircularProgressIndicator()), - error: - (e, _) => ResponseErrorWidget( + return postAsync.when( + data: + (controller) => SliverInfiniteList( + itemCount: controller.posts.length, + isLoading: controller.isLoading, + hasReachedMax: controller.hasReachedMax, + onFetchData: controller.fetchMore, + itemBuilder: (context, index) { + final post = controller.posts[index]; + return PostItem( + item: post, + backgroundColor: isWide ? Colors.transparent : null, + ); + }, + separatorBuilder: (_, __) => const Divider(height: 1), + emptyBuilder: (context) { + return SliverToBoxAdapter( + child: Column( + children: [ + Text( + 'No replies', + textAlign: TextAlign.center, + ).fontSize(18).bold(), + Text('Why not start a discussion?'), + ], + ).padding(vertical: 16), + ); + }, + ), + loading: + () => SliverFillRemaining( + child: const Center(child: CircularProgressIndicator()), + ), + error: + (e, _) => SliverFillRemaining( + child: ResponseErrorWidget( error: e, onRetry: () { ref.invalidate(postRepliesProvider(postId)); }, ), - ), + ), ); } } diff --git a/pubspec.lock b/pubspec.lock index 28b1be2..1fceac3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1802,6 +1802,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.1" + super_clipboard: + dependency: "direct main" + description: + name: super_clipboard + sha256: "5203c881d24033c3e6154c2ae01afd94e7f0a3201280373f28e540f1defa3f40" + url: "https://pub.dev" + source: hosted + version: "0.9.0-dev.6" super_context_menu: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index c828152..332ec1d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -100,6 +100,7 @@ dependencies: photo_view: ^0.15.0 dismissible_page: ^1.0.2 super_sliver_list: ^0.4.1 + super_clipboard: ^0.9.0-dev.6 dev_dependencies: flutter_test: