From 1d52b8b5ed96c850829ab0d76daa073dba88c9f6 Mon Sep 17 00:00:00 2001
From: LittleSheep <littlesheep.code@hotmail.com>
Date: Sat, 26 Apr 2025 18:35:17 +0800
Subject: [PATCH] :recycle: Rebuilt the context menu
---
lib/screens/explore.dart | 7 +-
lib/screens/posts/compose.dart | 2 +-
lib/screens/posts/detail.dart | 2 +-
lib/widgets/content/video.native.dart | 22 ++++--
lib/widgets/context_menu.dart | 84 ++++++++++++++++++++
lib/widgets/post/post_item.dart | 106 +++++++++++++-------------
6 files changed, 164 insertions(+), 59 deletions(-)
create mode 100644 lib/widgets/context_menu.dart
diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart
index 21670f82..22a642d9 100644
--- a/lib/screens/explore.dart
+++ b/lib/screens/explore.dart
@@ -48,7 +48,12 @@ class ExploreScreen extends ConsumerWidget {
onFetchData: controller.fetchMore,
itemBuilder: (context, index) {
final post = controller.posts[index];
- return PostItem(item: post);
+ return PostItem(
+ item: post,
+ onRefresh: (_) {
+ ref.invalidate(postListProvider);
+ },
+ );
},
separatorBuilder: (_, __) => const Divider(height: 1),
),
diff --git a/lib/screens/posts/compose.dart b/lib/screens/posts/compose.dart
index 7ac71b48..5912a076 100644
--- a/lib/screens/posts/compose.dart
+++ b/lib/screens/posts/compose.dart
@@ -131,7 +131,7 @@ class PostComposeScreen extends HookConsumerWidget {
attachmentProgress.value = {...attachmentProgress.value, index: 0};
final cloudFile =
await putMediaToCloud(
- fileData: attachment,
+ fileData: attachment.data,
atk: atk,
baseUrl: baseUrl,
filename: attachment.data.name ?? 'Post media',
diff --git a/lib/screens/posts/detail.dart b/lib/screens/posts/detail.dart
index 603f4f25..2d4e7241 100644
--- a/lib/screens/posts/detail.dart
+++ b/lib/screens/posts/detail.dart
@@ -38,7 +38,7 @@ class PostDetailScreen extends HookConsumerWidget {
children: [
Column(
children: [
- PostItem(item: post!),
+ PostItem(item: post!, isOpenable: false),
const Divider(height: 1),
Expanded(child: PostRepliesList(postId: id)),
Gap(MediaQuery.of(context).padding.bottom),
diff --git a/lib/widgets/content/video.native.dart b/lib/widgets/content/video.native.dart
index 9236df7c..59cf4361 100644
--- a/lib/widgets/content/video.native.dart
+++ b/lib/widgets/content/video.native.dart
@@ -3,11 +3,14 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:island/pods/config.dart';
+import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
-class UniversalVideo extends StatefulWidget {
+class UniversalVideo extends ConsumerStatefulWidget {
final String uri;
final double aspectRatio;
const UniversalVideo({
@@ -17,10 +20,10 @@ class UniversalVideo extends StatefulWidget {
});
@override
- State<UniversalVideo> createState() => _UniversalVideoState();
+ ConsumerState<UniversalVideo> createState() => _UniversalVideoState();
}
-class _UniversalVideoState extends State<UniversalVideo> {
+class _UniversalVideoState extends ConsumerState<UniversalVideo> {
Player? _player;
VideoController? _videoController;
@@ -35,9 +38,18 @@ class _UniversalVideoState extends State<UniversalVideo> {
final inCacheInfo = await DefaultCacheManager().getFileFromCache(url);
if (inCacheInfo == null) {
log('[MediaPlayer] Miss cache: $url');
+ final baseUrl = ref.watch(serverUrlProvider);
+ final atk = await getFreshAtk(
+ ref.watch(tokenPairProvider),
+ baseUrl,
+ onRefreshed: (atk, rtk) {
+ setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
+ ref.invalidate(tokenPairProvider);
+ },
+ );
final fileStream = DefaultCacheManager().getFileStream(
url,
- // headers: {'Authorization': 'Bearer ${await ua.atk}'},
+ headers: {'Authorization': 'Bearer $atk'},
withProgress: true,
);
await for (var fileInfo in fileStream) {
@@ -55,7 +67,7 @@ class _UniversalVideoState extends State<UniversalVideo> {
return;
}
- _player!.open(Media(uri));
+ _player!.open(Media(uri), play: false);
}
@override
diff --git a/lib/widgets/context_menu.dart b/lib/widgets/context_menu.dart
new file mode 100644
index 00000000..416860f2
--- /dev/null
+++ b/lib/widgets/context_menu.dart
@@ -0,0 +1,84 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+
+typedef ContextMenuBuilder =
+ Widget Function(BuildContext context, Offset offset);
+
+class ContextMenuRegion extends HookWidget {
+ final Offset? mobileAnchor;
+ final Widget child;
+ final ContextMenuBuilder contextMenuBuilder;
+ const ContextMenuRegion({
+ super.key,
+ required this.child,
+ required this.contextMenuBuilder,
+ this.mobileAnchor,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final contextMenuController = useMemoized(() => ContextMenuController());
+ final mobileOffset = useState<Offset?>(null);
+
+ bool canBeTouchScreen = switch (defaultTargetPlatform) {
+ TargetPlatform.android || TargetPlatform.iOS => true,
+ _ => false,
+ };
+
+ void showMenu(Offset position) {
+ contextMenuController.show(
+ context: context,
+ contextMenuBuilder: (BuildContext context) {
+ return contextMenuBuilder(context, position);
+ },
+ );
+ }
+
+ void hideMenu() {
+ contextMenuController.remove();
+ }
+
+ void onSecondaryTapUp(TapUpDetails details) {
+ showMenu(details.globalPosition);
+ }
+
+ void onTap() {
+ if (!contextMenuController.isShown) {
+ return;
+ }
+ hideMenu();
+ }
+
+ void onLongPressStart(LongPressStartDetails details) {
+ mobileOffset.value = details.globalPosition;
+ }
+
+ void onLongPress() {
+ assert(mobileOffset.value != null);
+ showMenu(mobileAnchor ?? mobileOffset.value!);
+ mobileOffset.value = null;
+ }
+
+ useEffect(() {
+ return () {
+ hideMenu();
+ };
+ }, []);
+
+ return TapRegion(
+ behavior: HitTestBehavior.opaque,
+ child: GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onSecondaryTapUp: onSecondaryTapUp,
+ onTap: onTap,
+ onLongPress: canBeTouchScreen ? onLongPress : null,
+ onLongPressStart: canBeTouchScreen ? onLongPressStart : null,
+ child: child,
+ ),
+ onTapOutside: (_) {
+ hideMenu();
+ },
+ );
+ }
+}
diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart
index e14d1fd4..cdbc1f53 100644
--- a/lib/widgets/post/post_item.dart
+++ b/lib/widgets/post/post_item.dart
@@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart';
+import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:island/models/post.dart';
@@ -6,18 +7,20 @@ import 'package:island/route.gr.dart';
import 'package:island/widgets/content/cloud_file_collection.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/markdown.dart';
-import 'package:lucide_icons/lucide_icons.dart';
+import 'package:island/widgets/context_menu.dart';
import 'package:styled_widget/styled_widget.dart';
class PostItem extends StatelessWidget {
final SnPost item;
final EdgeInsets? padding;
final bool isOpenable;
+ final Function? onRefresh;
const PostItem({
super.key,
required this.item,
this.padding,
this.isOpenable = true,
+ this.onRefresh,
});
@override
@@ -25,59 +28,60 @@ class PostItem extends StatelessWidget {
final renderingPadding =
padding ?? EdgeInsets.symmetric(horizontal: 12, vertical: 16);
- return CupertinoContextMenu.builder(
- actions: [
- CupertinoContextMenuAction(
- trailingIcon: LucideIcons.edit,
- onPressed: () {
- context.router.push(PostEditRoute(id: item.id));
- },
- child: Text('Edit'),
- ),
- ],
- builder: (context, animation) {
- return Material(
- color: Theme.of(context).colorScheme.surface,
- child: SingleChildScrollView(
- physics: const NeverScrollableScrollPhysics(),
- child: Padding(
- padding: renderingPadding,
- child: Column(
- spacing: 8,
- children: [
- Row(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- spacing: 12,
- children: [
- ProfilePictureWidget(item: item.publisher.picture),
- Expanded(
- child: GestureDetector(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(item.publisher.nick).bold(),
- if (item.content.isNotEmpty)
- MarkdownTextContent(content: item.content),
- ],
- ),
- onTap: () {
- if (isOpenable) {
- context.router.push(PostDetailRoute(id: item.id));
- }
- },
- ),
- ),
- ],
- ),
- if (item.attachments.isNotEmpty)
- CloudFileList(files: item.attachments),
- ],
- ),
+ return ContextMenuRegion(
+ contextMenuBuilder: (_, offset) {
+ return AdaptiveTextSelectionToolbar.buttonItems(
+ anchors: TextSelectionToolbarAnchors(primaryAnchor: offset),
+ buttonItems: <ContextMenuButtonItem>[
+ ContextMenuButtonItem(
+ onPressed: () {
+ ContextMenuController.removeAny();
+ context.router.push(PostEditRoute(id: item.id)).then((value) {
+ if (value != null) {
+ onRefresh?.call();
+ }
+ });
+ },
+ label: 'edit'.tr(),
),
- ),
+ ],
);
},
+ child: Padding(
+ padding: renderingPadding,
+ child: Column(
+ spacing: 8,
+ children: [
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ spacing: 12,
+ children: [
+ ProfilePictureWidget(item: item.publisher.picture),
+ Expanded(
+ child: GestureDetector(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(item.publisher.nick).bold(),
+ if (item.content.isNotEmpty)
+ MarkdownTextContent(content: item.content),
+ ],
+ ),
+ onTap: () {
+ if (isOpenable) {
+ context.router.push(PostDetailRoute(id: item.id));
+ }
+ },
+ ),
+ ),
+ ],
+ ),
+ if (item.attachments.isNotEmpty)
+ CloudFileList(files: item.attachments),
+ ],
+ ),
+ ),
);
}
}