From eee75f6fdbe07870e8e64d916383e912ed9de96f Mon Sep 17 00:00:00 2001 From: insleker Date: Thu, 18 Sep 2025 21:05:40 +0800 Subject: [PATCH] feat: disable context menu in web --- README.md | 1 + integration_test/export_flow_test.dart | 10 +- lib/main.dart | 12 +- .../pdf/widgets/pdf_page_overlays.dart | 4 +- .../pdf/widgets/pdf_viewer_widget.dart | 4 +- .../pdf/widgets/signature_overlay.dart | 92 +++++++------- .../signature/widgets/signature_card.dart | 86 +++++-------- .../signature/widgets/signature_drawer.dart | 7 +- pubspec.yaml | 1 + tool/run_integration_tests.dart | 120 ++++++++++++++++++ 10 files changed, 225 insertions(+), 112 deletions(-) create mode 100644 tool/run_integration_tests.dart diff --git a/README.md b/README.md index 335725a..55cd0e1 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ flutter analyze flutter test # > run integration tests flutter test integration_test/ -d +# dart run tool/run_integration_tests.dart --device=linux # dart run tool/gen_view_wireframe_md.dart # flutter pub run dead_code_analyzer diff --git a/integration_test/export_flow_test.dart b/integration_test/export_flow_test.dart index 5ebb5a0..de2d102 100644 --- a/integration_test/export_flow_test.dart +++ b/integration_test/export_flow_test.dart @@ -79,7 +79,10 @@ void main() { ), exportServiceProvider.overrideWith((_) => fake), savePathPickerProvider.overrideWith( - (_) => () async => 'C:/tmp/output.pdf', + (_) => () async { + final dir = Directory.systemTemp.createTempSync('pdfsig_'); + return '${dir.path}/output.pdf'; + }, ), ], child: MaterialApp( @@ -431,7 +434,10 @@ void main() { ), exportServiceProvider.overrideWith((ref) => LightweightExporter()), savePathPickerProvider.overrideWith( - (_) => () async => 'C:/tmp/output-after-export.pdf', + (_) => () async { + final dir = Directory.systemTemp.createTempSync('pdfsig_after_'); + return '${dir.path}/output-after-export.pdf'; + }, ), ], child: MaterialApp( diff --git a/lib/main.dart b/lib/main.dart index 4019f18..ca7cd3f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,15 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:pdf_signature/app.dart'; export 'package:pdf_signature/app.dart'; -void main() => runApp(const MyApp()); +void main() { + // Ensure Flutter bindings are initialized before platform channel usage + WidgetsFlutterBinding.ensureInitialized(); + // Disable right-click context menu on web using Flutter API + if (kIsWeb) { + BrowserContextMenu.disableContextMenu(); + } + runApp(const MyApp()); +} diff --git a/lib/ui/features/pdf/widgets/pdf_page_overlays.dart b/lib/ui/features/pdf/widgets/pdf_page_overlays.dart index 96616fd..a8a0ccd 100644 --- a/lib/ui/features/pdf/widgets/pdf_page_overlays.dart +++ b/lib/ui/features/pdf/widgets/pdf_page_overlays.dart @@ -112,7 +112,9 @@ class PdfPageOverlays extends ConsumerWidget { // TODO:Add active overlay if present and not using mock (mock has its own) final useMock = pdfViewModel.useMockViewer; - if (!useMock && activeRect != null) { + if (!useMock && + activeRect != null && + pageNumber == pdfViewModel.currentPage) { widgets.add( LayoutBuilder( builder: (context, constraints) { diff --git a/lib/ui/features/pdf/widgets/pdf_viewer_widget.dart b/lib/ui/features/pdf/widgets/pdf_viewer_widget.dart index 6c80319..719d2d4 100644 --- a/lib/ui/features/pdf/widgets/pdf_viewer_widget.dart +++ b/lib/ui/features/pdf/widgets/pdf_viewer_widget.dart @@ -127,7 +127,7 @@ class _PdfViewerWidgetState extends ConsumerState { color: Colors.black.withValues(alpha: 0.7), child: Center( child: Text( - pageNumber.toString(), + 'Pg $pageNumber', style: const TextStyle( color: Colors.white, fontSize: 12, @@ -146,7 +146,7 @@ class _PdfViewerWidgetState extends ConsumerState { color: Colors.black.withValues(alpha: 0.7), child: Center( child: Text( - pageNumber.toString(), + 'Pg $pageNumber', style: const TextStyle( color: Colors.white, fontSize: 12, diff --git a/lib/ui/features/pdf/widgets/signature_overlay.dart b/lib/ui/features/pdf/widgets/signature_overlay.dart index 662042b..cc8d651 100644 --- a/lib/ui/features/pdf/widgets/signature_overlay.dart +++ b/lib/ui/features/pdf/widgets/signature_overlay.dart @@ -40,6 +40,45 @@ class SignatureOverlay extends ConsumerWidget { rect.height * pageH, ); + Future _showContextMenu(Offset position) async { + final pdfViewModel = ref.read(pdfViewModelProvider.notifier); + final isLocked = ref + .watch(pdfViewModelProvider) + .isPlacementLocked(page: pageNumber, index: placedIndex); + final selected = await showMenu( + context: context, + position: RelativeRect.fromLTRB( + position.dx, + position.dy, + position.dx, + position.dy, + ), + items: [ + PopupMenuItem( + key: const Key('mi_placement_lock'), + value: isLocked ? 'unlock' : 'lock', + child: Text( + isLocked + ? AppLocalizations.of(context).unlock + : AppLocalizations.of(context).lock, + ), + ), + PopupMenuItem( + key: const Key('mi_placement_delete'), + value: 'delete', + child: Text(AppLocalizations.of(context).delete), + ), + ], + ); + if (selected == 'lock') { + pdfViewModel.lockPlacement(page: pageNumber, index: placedIndex); + } else if (selected == 'unlock') { + pdfViewModel.unlockPlacement(page: pageNumber, index: placedIndex); + } else if (selected == 'delete') { + pdfViewModel.removePlacement(page: pageNumber, index: placedIndex); + } + } + return Stack( children: [ TransformableBox( @@ -110,55 +149,10 @@ class SignatureOverlay extends ConsumerWidget { height: rectPx.height, child: GestureDetector( behavior: HitTestBehavior.translucent, - onSecondaryTapDown: (details) async { - final pdfViewModel = ref.read(pdfViewModelProvider.notifier); - final isLocked = ref - .watch(pdfViewModelProvider) - .isPlacementLocked(page: pageNumber, index: placedIndex); - - final selected = await showMenu( - context: context, - position: RelativeRect.fromLTRB( - details.globalPosition.dx, - details.globalPosition.dy, - details.globalPosition.dx, - details.globalPosition.dy, - ), - items: [ - PopupMenuItem( - key: const Key('mi_placement_lock'), - value: isLocked ? 'unlock' : 'lock', - child: Text( - isLocked - ? AppLocalizations.of(context).unlock - : AppLocalizations.of(context).lock, - ), - ), - PopupMenuItem( - key: const Key('mi_placement_delete'), - value: 'delete', - child: Text(AppLocalizations.of(context).delete), - ), - ], - ); - - if (selected == 'lock') { - pdfViewModel.lockPlacement( - page: pageNumber, - index: placedIndex, - ); - } else if (selected == 'unlock') { - pdfViewModel.unlockPlacement( - page: pageNumber, - index: placedIndex, - ); - } else if (selected == 'delete') { - pdfViewModel.removePlacement( - page: pageNumber, - index: placedIndex, - ); - } - }, + onSecondaryTapDown: + (details) => _showContextMenu(details.globalPosition), + onLongPressStart: + (details) => _showContextMenu(details.globalPosition), ), ), ], diff --git a/lib/ui/features/signature/widgets/signature_card.dart b/lib/ui/features/signature/widgets/signature_card.dart index 2152f4f..ca68456 100644 --- a/lib/ui/features/signature/widgets/signature_card.dart +++ b/lib/ui/features/signature/widgets/signature_card.dart @@ -27,6 +27,34 @@ class SignatureCard extends ConsumerWidget { final bool useCurrentBytesForDrag; final double rotationDeg; final domain.GraphicAdjust graphicAdjust; + Future _showContextMenu(BuildContext context, Offset position) async { + final selected = await showMenu( + context: context, + position: RelativeRect.fromLTRB( + position.dx, + position.dy, + position.dx, + position.dy, + ), + items: [ + PopupMenuItem( + key: const Key('mi_signature_adjust'), + value: 'adjust', + child: Text(AppLocalizations.of(context).adjustGraphic), + ), + PopupMenuItem( + key: const Key('mi_signature_delete'), + value: 'delete', + child: Text(AppLocalizations.of(context).delete), + ), + ], + ); + if (selected == 'adjust') { + onAdjust?.call(); + } else if (selected == 'delete') { + onDelete(); + } + } @override Widget build(BuildContext context, WidgetRef ref) { @@ -91,65 +119,11 @@ class SignatureCard extends ConsumerWidget { onSecondaryTapDown: disabled ? null - : (details) async { - final selected = await showMenu( - context: context, - position: RelativeRect.fromLTRB( - details.globalPosition.dx, - details.globalPosition.dy, - details.globalPosition.dx, - details.globalPosition.dy, - ), - items: [ - PopupMenuItem( - key: const Key('mi_signature_adjust'), - value: 'adjust', - child: Text(AppLocalizations.of(context).adjustGraphic), - ), - PopupMenuItem( - key: const Key('mi_signature_delete'), - value: 'delete', - child: Text(AppLocalizations.of(context).delete), - ), - ], - ); - if (selected == 'adjust') { - onAdjust?.call(); - } else if (selected == 'delete') { - onDelete(); - } - }, + : (details) => _showContextMenu(context, details.globalPosition), onLongPressStart: disabled ? null - : (details) async { - final selected = await showMenu( - context: context, - position: RelativeRect.fromLTRB( - details.globalPosition.dx, - details.globalPosition.dy, - details.globalPosition.dx, - details.globalPosition.dy, - ), - items: [ - PopupMenuItem( - key: const Key('mi_signature_adjust'), - value: 'adjust', - child: Text(AppLocalizations.of(context).adjustGraphic), - ), - PopupMenuItem( - key: const Key('mi_signature_delete'), - value: 'delete', - child: Text(AppLocalizations.of(context).delete), - ), - ], - ); - if (selected == 'adjust') { - onAdjust?.call(); - } else if (selected == 'delete') { - onDelete(); - } - }, + : (details) => _showContextMenu(context, details.globalPosition), child: child, ); if (disabled) return child; diff --git a/lib/ui/features/signature/widgets/signature_drawer.dart b/lib/ui/features/signature/widgets/signature_drawer.dart index d7efd59..a1662e3 100644 --- a/lib/ui/features/signature/widgets/signature_drawer.dart +++ b/lib/ui/features/signature/widgets/signature_drawer.dart @@ -9,6 +9,7 @@ import 'package:pdf_signature/data/repositories/signature_card_repository.dart'; import 'package:pdf_signature/domain/models/model.dart' hide SignatureCard; import 'image_editor_dialog.dart'; import 'signature_card.dart'; +import '../../pdf/view_model/pdf_view_model.dart'; /// Data for drag-and-drop is in signature_drag_data.dart @@ -77,7 +78,11 @@ class _SignatureDrawerState extends ConsumerState { } }, onTap: () { - // state = const Rect.fromLTWH(0.2, 0.2, 0.3, 0.15); + // Activate a default overlay rectangle on the current page + // so integration tests can find and size the active overlay. + ref + .read(pdfViewModelProvider.notifier) + .activeRect = const Rect.fromLTWH(0.2, 0.2, 0.3, 0.15); }, ), ), diff --git a/pubspec.yaml b/pubspec.yaml index d5b8ced..d3369b1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,6 +59,7 @@ dependencies: riverpod_annotation: ^2.6.1 colorfilter_generator: ^0.0.8 flutter_box_transform: ^0.4.7 + # disable_web_context_menu: ^1.1.0 # ml_linalg: ^13.12.6 dev_dependencies: diff --git a/tool/run_integration_tests.dart b/tool/run_integration_tests.dart new file mode 100644 index 0000000..a687f8b --- /dev/null +++ b/tool/run_integration_tests.dart @@ -0,0 +1,120 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +/// Runs each integration test file sequentially to avoid multi-app start issues on desktop. +/// +/// Usage: +/// dart tool/run_integration_tests.dart [--device=] [--reporter=] [--pattern=] +/// +/// Defaults: +/// --device=linux +/// --reporter=compact +/// --pattern=*.dart (all files in integration_test/) +Future main(List args) async { + String device = 'linux'; + String reporter = 'compact'; + String pattern = '*.dart'; + + for (int i = 0; i < args.length; i++) { + final a = args[i]; + if (a.startsWith('--device=')) { + device = a.substring(a.indexOf('=') + 1); + } else if (a == '--device' || a == '-d') { + if (i + 1 < args.length) { + device = args[++i]; + } + } else if (a.startsWith('-d=')) { + device = a.substring(a.indexOf('=') + 1); + } else if (a.startsWith('--reporter=')) { + reporter = a.substring(a.indexOf('=') + 1); + } else if (a == '--reporter' || a == '-r') { + if (i + 1 < args.length) { + reporter = args[++i]; + } + } else if (a.startsWith('--pattern=')) { + pattern = a.substring(a.indexOf('=') + 1); + } else if (a == '--pattern') { + if (i + 1 < args.length) { + pattern = args[++i]; + } + } + } + + final dir = Directory('integration_test'); + if (!await dir.exists()) { + stderr.writeln('integration_test/ not found. Run from the project root.'); + return 2; + } + + final files = + (await dir + .list() + .where((e) => e is File && e.path.endsWith('.dart')) + .cast() + .toList()) + ..sort((a, b) => a.path.compareTo(b.path)); + + List selected; + if (pattern == '*.dart') { + selected = files; + } else { + // very simple glob: supports prefix/suffix match + if (pattern.startsWith('*')) { + final suffix = pattern.substring(1); + selected = files.where((f) => f.path.endsWith(suffix)).toList(); + } else if (pattern.endsWith('*')) { + final prefix = pattern.substring(0, pattern.length - 1); + selected = + files + .where( + (f) => f.path + .split(Platform.pathSeparator) + .last + .startsWith(prefix), + ) + .toList(); + } else { + selected = files.where((f) => f.path.contains(pattern)).toList(); + } + } + + if (selected.isEmpty) { + stderr.writeln('No integration tests matched pattern: $pattern'); + return 3; + } + + stdout.writeln( + 'Running ${selected.length} integration test file(s) sequentially...', + ); + final results = {}; + + for (final f in selected) { + final rel = f.path; + stdout.writeln('\n=== Running: $rel ==='); + final args = ['test', rel, '-d', device, '-r', reporter]; + final proc = await Process.start('flutter', args); + // Pipe output live + unawaited(proc.stdout.transform(utf8.decoder).forEach(stdout.write)); + unawaited(proc.stderr.transform(utf8.decoder).forEach(stderr.write)); + final code = await proc.exitCode; + results[rel] = code; + if (code == 0) { + stdout.writeln('=== PASSED: $rel ==='); + } else { + stderr.writeln('=== FAILED (exit $code): $rel ==='); + } + // Small pause between launches to let desktop/device settle + await Future.delayed(const Duration(milliseconds: 300)); + } + + stdout.writeln('\nSummary:'); + var failures = 0; + for (final entry in results.entries) { + final status = entry.value == 0 ? 'PASS' : 'FAIL(${entry.value})'; + stdout.writeln(' - ${entry.key}: $status'); + if (entry.value != 0) failures += 1; + } + + return failures == 0 ? 0 : 1; +}