feat: disable context menu in web
This commit is contained in:
parent
34f6abad32
commit
eee75f6fdb
|
|
@ -23,6 +23,7 @@ flutter analyze
|
||||||
flutter test
|
flutter test
|
||||||
# > run integration tests
|
# > run integration tests
|
||||||
flutter test integration_test/ -d <device_id>
|
flutter test integration_test/ -d <device_id>
|
||||||
|
# dart run tool/run_integration_tests.dart --device=linux
|
||||||
|
|
||||||
# dart run tool/gen_view_wireframe_md.dart
|
# dart run tool/gen_view_wireframe_md.dart
|
||||||
# flutter pub run dead_code_analyzer
|
# flutter pub run dead_code_analyzer
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,10 @@ void main() {
|
||||||
),
|
),
|
||||||
exportServiceProvider.overrideWith((_) => fake),
|
exportServiceProvider.overrideWith((_) => fake),
|
||||||
savePathPickerProvider.overrideWith(
|
savePathPickerProvider.overrideWith(
|
||||||
(_) => () async => 'C:/tmp/output.pdf',
|
(_) => () async {
|
||||||
|
final dir = Directory.systemTemp.createTempSync('pdfsig_');
|
||||||
|
return '${dir.path}/output.pdf';
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
|
|
@ -431,7 +434,10 @@ void main() {
|
||||||
),
|
),
|
||||||
exportServiceProvider.overrideWith((ref) => LightweightExporter()),
|
exportServiceProvider.overrideWith((ref) => LightweightExporter()),
|
||||||
savePathPickerProvider.overrideWith(
|
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(
|
child: MaterialApp(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,15 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:pdf_signature/app.dart';
|
import 'package:pdf_signature/app.dart';
|
||||||
export '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());
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,9 @@ class PdfPageOverlays extends ConsumerWidget {
|
||||||
// TODO:Add active overlay if present and not using mock (mock has its own)
|
// TODO:Add active overlay if present and not using mock (mock has its own)
|
||||||
|
|
||||||
final useMock = pdfViewModel.useMockViewer;
|
final useMock = pdfViewModel.useMockViewer;
|
||||||
if (!useMock && activeRect != null) {
|
if (!useMock &&
|
||||||
|
activeRect != null &&
|
||||||
|
pageNumber == pdfViewModel.currentPage) {
|
||||||
widgets.add(
|
widgets.add(
|
||||||
LayoutBuilder(
|
LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
|
||||||
color: Colors.black.withValues(alpha: 0.7),
|
color: Colors.black.withValues(alpha: 0.7),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
pageNumber.toString(),
|
'Pg $pageNumber',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|
@ -146,7 +146,7 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
|
||||||
color: Colors.black.withValues(alpha: 0.7),
|
color: Colors.black.withValues(alpha: 0.7),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
pageNumber.toString(),
|
'Pg $pageNumber',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,45 @@ class SignatureOverlay extends ConsumerWidget {
|
||||||
rect.height * pageH,
|
rect.height * pageH,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Future<void> _showContextMenu(Offset position) async {
|
||||||
|
final pdfViewModel = ref.read(pdfViewModelProvider.notifier);
|
||||||
|
final isLocked = ref
|
||||||
|
.watch(pdfViewModelProvider)
|
||||||
|
.isPlacementLocked(page: pageNumber, index: placedIndex);
|
||||||
|
final selected = await showMenu<String>(
|
||||||
|
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(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
TransformableBox(
|
TransformableBox(
|
||||||
|
|
@ -110,55 +149,10 @@ class SignatureOverlay extends ConsumerWidget {
|
||||||
height: rectPx.height,
|
height: rectPx.height,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
onSecondaryTapDown: (details) async {
|
onSecondaryTapDown:
|
||||||
final pdfViewModel = ref.read(pdfViewModelProvider.notifier);
|
(details) => _showContextMenu(details.globalPosition),
|
||||||
final isLocked = ref
|
onLongPressStart:
|
||||||
.watch(pdfViewModelProvider)
|
(details) => _showContextMenu(details.globalPosition),
|
||||||
.isPlacementLocked(page: pageNumber, index: placedIndex);
|
|
||||||
|
|
||||||
final selected = await showMenu<String>(
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,34 @@ class SignatureCard extends ConsumerWidget {
|
||||||
final bool useCurrentBytesForDrag;
|
final bool useCurrentBytesForDrag;
|
||||||
final double rotationDeg;
|
final double rotationDeg;
|
||||||
final domain.GraphicAdjust graphicAdjust;
|
final domain.GraphicAdjust graphicAdjust;
|
||||||
|
Future<void> _showContextMenu(BuildContext context, Offset position) async {
|
||||||
|
final selected = await showMenu<String>(
|
||||||
|
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
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -91,65 +119,11 @@ class SignatureCard extends ConsumerWidget {
|
||||||
onSecondaryTapDown:
|
onSecondaryTapDown:
|
||||||
disabled
|
disabled
|
||||||
? null
|
? null
|
||||||
: (details) async {
|
: (details) => _showContextMenu(context, details.globalPosition),
|
||||||
final selected = await showMenu<String>(
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongPressStart:
|
onLongPressStart:
|
||||||
disabled
|
disabled
|
||||||
? null
|
? null
|
||||||
: (details) async {
|
: (details) => _showContextMenu(context, details.globalPosition),
|
||||||
final selected = await showMenu<String>(
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
if (disabled) return child;
|
if (disabled) return child;
|
||||||
|
|
|
||||||
|
|
@ -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 'package:pdf_signature/domain/models/model.dart' hide SignatureCard;
|
||||||
import 'image_editor_dialog.dart';
|
import 'image_editor_dialog.dart';
|
||||||
import 'signature_card.dart';
|
import 'signature_card.dart';
|
||||||
|
import '../../pdf/view_model/pdf_view_model.dart';
|
||||||
|
|
||||||
/// Data for drag-and-drop is in signature_drag_data.dart
|
/// Data for drag-and-drop is in signature_drag_data.dart
|
||||||
|
|
||||||
|
|
@ -77,7 +78,11 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onTap: () {
|
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);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ dependencies:
|
||||||
riverpod_annotation: ^2.6.1
|
riverpod_annotation: ^2.6.1
|
||||||
colorfilter_generator: ^0.0.8
|
colorfilter_generator: ^0.0.8
|
||||||
flutter_box_transform: ^0.4.7
|
flutter_box_transform: ^0.4.7
|
||||||
|
# disable_web_context_menu: ^1.1.0
|
||||||
# ml_linalg: ^13.12.6
|
# ml_linalg: ^13.12.6
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|
|
||||||
|
|
@ -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=<id>] [--reporter=<name>] [--pattern=<glob>]
|
||||||
|
///
|
||||||
|
/// Defaults:
|
||||||
|
/// --device=linux
|
||||||
|
/// --reporter=compact
|
||||||
|
/// --pattern=*.dart (all files in integration_test/)
|
||||||
|
Future<int> main(List<String> 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<File>()
|
||||||
|
.toList())
|
||||||
|
..sort((a, b) => a.path.compareTo(b.path));
|
||||||
|
|
||||||
|
List<File> 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 = <String, int>{};
|
||||||
|
|
||||||
|
for (final f in selected) {
|
||||||
|
final rel = f.path;
|
||||||
|
stdout.writeln('\n=== Running: $rel ===');
|
||||||
|
final args = <String>['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<void>.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;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue