diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index 21670f8..22a642d 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 7ac71b4..5912a07 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 603f4f2..2d4e724 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 9236df7..59cf436 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 createState() => _UniversalVideoState(); + ConsumerState createState() => _UniversalVideoState(); } -class _UniversalVideoState extends State { +class _UniversalVideoState extends ConsumerState { Player? _player; VideoController? _videoController; @@ -35,9 +38,18 @@ class _UniversalVideoState extends State { 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 { 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 0000000..416860f --- /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(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 e14d1fd..cdbc1f5 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( + 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), + ], + ), + ), ); } }