feat: disable context menu in web
This commit is contained in:
parent
34f6abad32
commit
eee75f6fdb
|
|
@ -23,6 +23,7 @@ flutter analyze
|
|||
flutter test
|
||||
# > run integration tests
|
||||
flutter test integration_test/ -d <device_id>
|
||||
# dart run tool/run_integration_tests.dart --device=linux
|
||||
|
||||
# dart run tool/gen_view_wireframe_md.dart
|
||||
# flutter pub run dead_code_analyzer
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
|
|||
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<PdfViewerWidget> {
|
|||
color: Colors.black.withValues(alpha: 0.7),
|
||||
child: Center(
|
||||
child: Text(
|
||||
pageNumber.toString(),
|
||||
'Pg $pageNumber',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,45 @@ class SignatureOverlay extends ConsumerWidget {
|
|||
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(
|
||||
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<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,
|
||||
);
|
||||
}
|
||||
},
|
||||
onSecondaryTapDown:
|
||||
(details) => _showContextMenu(details.globalPosition),
|
||||
onLongPressStart:
|
||||
(details) => _showContextMenu(details.globalPosition),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -27,6 +27,34 @@ class SignatureCard extends ConsumerWidget {
|
|||
final bool useCurrentBytesForDrag;
|
||||
final double rotationDeg;
|
||||
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
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
|
@ -91,65 +119,11 @@ class SignatureCard extends ConsumerWidget {
|
|||
onSecondaryTapDown:
|
||||
disabled
|
||||
? null
|
||||
: (details) async {
|
||||
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();
|
||||
}
|
||||
},
|
||||
: (details) => _showContextMenu(context, details.globalPosition),
|
||||
onLongPressStart:
|
||||
disabled
|
||||
? null
|
||||
: (details) async {
|
||||
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();
|
||||
}
|
||||
},
|
||||
: (details) => _showContextMenu(context, details.globalPosition),
|
||||
child: 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 '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<SignatureDrawer> {
|
|||
}
|
||||
},
|
||||
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
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -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