context menu

This commit is contained in:
quantulr
2023-05-17 00:04:36 +08:00
parent eb918803d4
commit bfaff7f7fa
6 changed files with 277 additions and 143 deletions

View File

@ -0,0 +1,97 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
typedef ContextMenuBuilder = Widget Function(
BuildContext context, Offset offset);
/// Shows and hides the context menu based on user gestures.
///
/// By default, shows the menu on right clicks and long presses.
class ContextMenuRegion extends StatefulWidget {
/// Creates an instance of [ContextMenuRegion].
const ContextMenuRegion({
super.key,
required this.child,
required this.contextMenuBuilder,
});
/// Builds the context menu.
final ContextMenuBuilder contextMenuBuilder;
/// The child widget that will be listened to for gestures.
final Widget child;
@override
State<ContextMenuRegion> createState() => _ContextMenuRegionState();
}
class _ContextMenuRegionState extends State<ContextMenuRegion> {
Offset? _longPressOffset;
final ContextMenuController _contextMenuController = ContextMenuController();
static bool get _longPressEnabled {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
return true;
case TargetPlatform.macOS:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return false;
}
}
void _onSecondaryTapUp(TapUpDetails details) {
_show(details.globalPosition);
}
void _onTap() {
if (!_contextMenuController.isShown) {
return;
}
_hide();
}
void _onLongPressStart(LongPressStartDetails details) {
_longPressOffset = details.globalPosition;
}
void _onLongPress() {
assert(_longPressOffset != null);
_show(_longPressOffset!);
_longPressOffset = null;
}
void _show(Offset position) {
_contextMenuController.show(
context: context,
contextMenuBuilder: (context) {
return widget.contextMenuBuilder(context, position);
},
);
}
void _hide() {
_contextMenuController.remove();
}
@override
void dispose() {
_hide();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onSecondaryTapUp: _onSecondaryTapUp,
onTap: _onTap,
onLongPress: _longPressEnabled ? _onLongPress : null,
onLongPressStart: _longPressEnabled ? _onLongPressStart : null,
child: widget.child,
);
}
}

View File

@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
// import 'dart:html' as html;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -30,7 +31,7 @@ void main() async {
await windowManager.focus(); await windowManager.focus();
}); });
} }
}else{ } else {
dio.options.baseUrl = '/api'; dio.options.baseUrl = '/api';
} }
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();

View File

@ -5,14 +5,15 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:momo/lib/context_menu_region.dart';
import 'package:momo/models/image_list_resp.dart'; import 'package:momo/models/image_list_resp.dart';
import 'package:momo/models/image_resp.dart'; import 'package:momo/models/image_resp.dart';
import 'package:momo/provider/rerender.dart'; import 'package:momo/provider/rerender.dart';
import 'package:momo/request/http_client.dart'; import 'package:momo/request/http_client.dart';
/// A builder that includes an Offset to draw the context menu at. /// A builder that includes an Offset to draw the context menu at.
typedef ContextMenuBuilder = Widget Function( // typedef ContextMenuBuilder = Widget Function(
BuildContext context, Offset offset); // BuildContext context, Offset offset);
class Gallery extends ConsumerWidget { class Gallery extends ConsumerWidget {
const Gallery({Key? key}) : super(key: key); const Gallery({Key? key}) : super(key: key);
@ -92,118 +93,151 @@ class ImageItem extends ConsumerStatefulWidget {
} }
class _ImageItemState extends ConsumerState<ImageItem> { class _ImageItemState extends ConsumerState<ImageItem> {
final ContextMenuController _contextMenuController = ContextMenuController(); // final ContextMenuController _contextMenuController = ContextMenuController();
@override @override
void dispose() { void dispose() {
// TODO: implement dispose // TODO: implement dispose
if (_contextMenuController.isShown) { // if (_contextMenuController.isShown) {
_contextMenuController.remove(); // _contextMenuController.remove();
} // }
super.dispose(); super.dispose();
} }
void _showContextMenu(Offset position) { // void _showContextMenu(Offset position) {
_contextMenuController.show( // _contextMenuController.show(
context: context, // context: context,
contextMenuBuilder: (BuildContext context) { // contextMenuBuilder: (BuildContext context) {
return AdaptiveTextSelectionToolbar.buttonItems( // return AdaptiveTextSelectionToolbar.buttonItems(
anchors: TextSelectionToolbarAnchors( // anchors: TextSelectionToolbarAnchors(
primaryAnchor: position, // primaryAnchor: position,
), // ),
buttonItems: <ContextMenuButtonItem>[ // buttonItems: <ContextMenuButtonItem>[
ContextMenuButtonItem( // ContextMenuButtonItem(
onPressed: () { // onPressed: () {
ContextMenuController.removeAny(); // ContextMenuController.removeAny();
showDialog( // showDialog(
context: context, // context: context,
builder: (BuildContext context) => AlertDialog( // builder: (BuildContext context) =>
title: const Text('删除图片'), // AlertDialog(
content: const Text("确认删除图片"), // title: const Text('删除图片'),
actions: [ // content: const Text("确认删除图片"),
TextButton( // actions: [
onPressed: () => Navigator.pop(context, 'Cancel'), // TextButton(
child: const Text('Cancel'), // onPressed: () => Navigator.pop(context, 'Cancel'),
), // child: const Text('Cancel'),
TextButton( // ),
onPressed: () { // TextButton(
Navigator.pop(context, 'OK'); // onPressed: () {
dio // Navigator.pop(context, 'OK');
.delete("/image/delete/${widget.image.id}") // dio
.then((resp) { // .delete("/image/delete/${widget.image.id}")
ref // .then((resp) {
.read(uniqueIdProvider.notifier) // ref
.updateId(); // .read(uniqueIdProvider.notifier)
// context.go("/"); // .updateId();
}); // // context.go("/");
}, // });
child: const Text('OK'), // },
), // child: const Text('OK'),
], // ),
)); // ],
// _showDialog(context); // ));
}, // // _showDialog(context);
label: '删除', // },
), // label: '删除',
ContextMenuButtonItem( // ),
onPressed: () { // ContextMenuButtonItem(
ContextMenuController.removeAny(); // onPressed: () {
// _showDialog(context); // ContextMenuController.removeAny();
Clipboard.setData(ClipboardData( // // _showDialog(context);
text: // Clipboard.setData(ClipboardData(
"${dio.options.baseUrl}/image/${widget.image.file_path}")); // text:
ScaffoldMessenger.of(context).showSnackBar(const SnackBar( // "${dio.options.baseUrl}/image/${widget.image.file_path}"));
content: Row( // ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
crossAxisAlignment: CrossAxisAlignment.center, // content: Row(
children: [ // crossAxisAlignment: CrossAxisAlignment.center,
Icon( // children: [
Icons.check_circle, // Icon(
color: Colors.white, // Icons.check_circle,
), // color: Colors.white,
SizedBox( // ),
width: 10, // SizedBox(
), // width: 10,
Text("已拷贝到剪贴板") // ),
], // Text("已拷贝到剪贴板")
), // ],
backgroundColor: Colors.lightGreen, // ),
)); // backgroundColor: Colors.lightGreen,
}, // ));
label: '复制链接', // },
), // label: '复制链接',
ContextMenuButtonItem( // ),
onPressed: () { // ContextMenuButtonItem(
ContextMenuController.removeAny(); // onPressed: () {
// _showDialog(context); // ContextMenuController.removeAny();
}, // // _showDialog(context);
label: '查看详情', // },
), // label: '查看详情',
], // ),
); // ],
}, // );
); // },
} // );
// }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return ContextMenuRegion(
onSecondaryTapUp: (TapUpDetails details) { contextMenuBuilder: (BuildContext context, Offset offset) {
_showContextMenu(details.globalPosition); return AdaptiveTextSelectionToolbar.buttonItems(
}, anchors: TextSelectionToolbarAnchors(
onTap: () { primaryAnchor: offset,
context.go(Uri( ),
path: "/detail", buttonItems: <ContextMenuButtonItem>[
queryParameters: {"id": "${widget.image.id}"}).toString()); ContextMenuButtonItem(
}, onPressed: () {
child: Image.network( ContextMenuController.removeAny();
kIsWeb Clipboard.setData(ClipboardData(
? "/images-api/?url=${dio.options.baseUrl}/image/${widget.image.file_path}&w=512" text:
: 'https://raichi.hodokencho.com/images-api/?url=${dio.options.baseUrl}/image/${widget.image.file_path}&w=512', "https://raichi.hodokencho.com/api/image/${widget.image.file_path}"));
fit: BoxFit.cover, ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
errorBuilder: (BuildContext context, o, err) { content: Row(
return const Icon(Icons.error_outline); crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.check_circle,
color: Colors.white,
),
SizedBox(
width: 10,
),
Text("已拷贝到剪贴板")
],
),
backgroundColor: Colors.lightGreen,
));
},
label: '复制链接',
),
],
);
}, },
), child: GestureDetector(
); onTap: () {
context.go(Uri(
path: "/detail",
queryParameters: {"id": "${widget.image.id}"}).toString());
},
child: Image.network(
kIsWeb
? "/images-api/?url=${dio.options.baseUrl}/image/${widget.image.file_path}&w=512"
: 'https://raichi.hodokencho.com/images-api/?url=${dio.options.baseUrl}/image/${widget.image.file_path}&w=512',
fit: BoxFit.cover,
errorBuilder: (BuildContext context, o, err) {
return const Icon(Icons.error_outline);
},
),
));
} }
} }

View File

@ -169,14 +169,6 @@ packages:
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted source: hosted
version: "4.1.0" version: "4.1.0"
context_menus:
dependency: "direct main"
description:
name: context_menus
sha256: "25313f2a17dc936f541f8012761648cb58d936c5d6f6bf7282f137a4b9dedddb"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.0.2"
convert: convert:
dependency: transitive dependency: transitive
description: description:

View File

@ -49,7 +49,6 @@ dependencies:
dio: ^5.0.0 dio: ^5.0.0
http_parser: ^4.0.2 http_parser: ^4.0.2
mime: ^1.0.4 mime: ^1.0.4
context_menus: ^1.0.2
extended_image: extended_image:
git: git:
url: https://github.com/fluttercandies/extended_image.git url: https://github.com/fluttercandies/extended_image.git

View File

@ -1,46 +1,49 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<!-- <!--
If you are serving your web app in a path other than the root, change the If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from. href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for The path provided below has to start and end with a slash "/" in order for
it to work correctly. it to work correctly.
For more details: For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`. the `--base-href` argument provided to `flutter build`.
--> -->
<base href="$FLUTTER_BASE_HREF"> <base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible"> <meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project."> <meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons --> <!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="momo"> <meta name="apple-mobile-web-app-title" content="momo">
<link rel="apple-touch-icon" href="icons/Icon-192.png"> <link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/> <link rel="icon" type="image/png" href="favicon.png"/>
<title>momo</title> <title>momo</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<script> <script>
// The value below is injected by flutter build, do not touch. // The value below is injected by flutter build, do not touch.
var serviceWorkerVersion = null; var serviceWorkerVersion = null;
</script>
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script> </script>
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script>
</head> </head>
<body> <body>
<script> <script>
window.addEventListener('load', function(ev) { window.addEventListener('load', function(ev) {
// Download main.dart.js // Download main.dart.js
_flutter.loader.loadEntrypoint({ _flutter.loader.loadEntrypoint({
@ -53,6 +56,14 @@
return appRunner.runApp(); return appRunner.runApp();
}); });
}); });
</script>
</script>
<script>
document.body.addEventListener('contextmenu', (event) => {
event.preventDefault();
});
</script>
</body> </body>
</html> </html>