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: