diff --git a/lib/ui/features/pdf/widgets/adjustments_panel.dart b/lib/ui/features/pdf/widgets/adjustments_panel.dart new file mode 100644 index 0000000..0748df2 --- /dev/null +++ b/lib/ui/features/pdf/widgets/adjustments_panel.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:pdf_signature/l10n/app_localizations.dart'; + +import '../../../../data/model/model.dart'; +import '../view_model/view_model.dart'; + +class AdjustmentsPanel extends ConsumerWidget { + const AdjustmentsPanel({super.key, required this.sig}); + + final SignatureState sig; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Column( + key: const Key('adjustments_panel'), + children: [ + Row( + children: [ + Checkbox( + key: const Key('chk_aspect_lock'), + value: sig.aspectLocked, + onChanged: + (v) => ref + .read(signatureProvider.notifier) + .toggleAspect(v ?? false), + ), + Text(AppLocalizations.of(context).lockAspectRatio), + const SizedBox(width: 16), + Switch( + key: const Key('swt_bg_removal'), + value: sig.bgRemoval, + onChanged: + (v) => ref.read(signatureProvider.notifier).setBgRemoval(v), + ), + Text(AppLocalizations.of(context).backgroundRemoval), + ], + ), + Row( + children: [ + Text(AppLocalizations.of(context).contrast), + Expanded( + child: Slider( + key: const Key('sld_contrast'), + min: 0.0, + max: 2.0, + value: sig.contrast, + onChanged: + (v) => ref.read(signatureProvider.notifier).setContrast(v), + ), + ), + Text(sig.contrast.toStringAsFixed(2)), + ], + ), + Row( + children: [ + Text(AppLocalizations.of(context).brightness), + Expanded( + child: Slider( + key: const Key('sld_brightness'), + min: -1.0, + max: 1.0, + value: sig.brightness, + onChanged: + (v) => + ref.read(signatureProvider.notifier).setBrightness(v), + ), + ), + Text(sig.brightness.toStringAsFixed(2)), + ], + ), + ], + ); + } +} diff --git a/lib/ui/features/pdf/widgets/pdf_page_area.dart b/lib/ui/features/pdf/widgets/pdf_page_area.dart new file mode 100644 index 0000000..0749905 --- /dev/null +++ b/lib/ui/features/pdf/widgets/pdf_page_area.dart @@ -0,0 +1,373 @@ +import 'dart:math' as math; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:pdf_signature/l10n/app_localizations.dart'; +import 'package:pdfrx/pdfrx.dart'; + +import '../../../../data/services/providers.dart'; +import '../../../../data/model/model.dart'; +import '../view_model/view_model.dart'; + +class PdfPageArea extends ConsumerWidget { + const PdfPageArea({ + super.key, + required this.pageSize, + required this.onDragSignature, + required this.onResizeSignature, + required this.onConfirmSignature, + required this.onClearActiveOverlay, + required this.onSelectPlaced, + }); + + final Size pageSize; + final ValueChanged onDragSignature; + final ValueChanged onResizeSignature; + final VoidCallback onConfirmSignature; + final VoidCallback onClearActiveOverlay; + final ValueChanged onSelectPlaced; + + Future _showContextMenuForPlaced({ + required BuildContext context, + required WidgetRef ref, + required Offset globalPos, + required int index, + }) async { + onSelectPlaced(index); + final choice = await showMenu( + context: context, + position: RelativeRect.fromLTRB( + globalPos.dx, + globalPos.dy, + globalPos.dx, + globalPos.dy, + ), + items: const [ + PopupMenuItem( + key: Key('ctx_delete_signature'), + value: 'delete', + child: Text('Delete'), + ), + ], + ); + if (choice == 'delete') { + final currentPage = ref.read(pdfProvider).currentPage; + ref + .read(pdfProvider.notifier) + .removePlacement(page: currentPage, index: index); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final pdf = ref.watch(pdfProvider); + if (!pdf.loaded) { + return Center(child: Text(AppLocalizations.of(context).noPdfLoaded)); + } + final useMock = ref.watch(useMockViewerProvider); + if (useMock) { + return Center( + child: AspectRatio( + aspectRatio: pageSize.width / pageSize.height, + child: Stack( + key: const Key('page_stack'), + children: [ + Container( + key: ValueKey('pdf_page_view_${pdf.currentPage}'), + color: Colors.grey.shade200, + child: Center( + child: Text( + AppLocalizations.of( + context, + ).pageInfo(pdf.currentPage, pdf.pageCount), + style: const TextStyle(fontSize: 24, color: Colors.black54), + ), + ), + ), + Consumer( + builder: (context, ref, _) { + final sig = ref.watch(signatureProvider); + final visible = ref.watch(signatureVisibilityProvider); + return visible + ? _buildPageOverlays(context, ref, sig) + : const SizedBox.shrink(); + }, + ), + ], + ), + ), + ); + } + if (pdf.pickedPdfPath != null) { + return PdfDocumentViewBuilder.file( + pdf.pickedPdfPath!, + builder: (context, document) { + if (document == null) { + return const Center(child: CircularProgressIndicator()); + } + final pages = document.pages; + final pageNum = pdf.currentPage.clamp(1, pages.length); + final page = pages[pageNum - 1]; + final aspect = page.width / page.height; + if (pdf.pageCount != pages.length) { + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(pdfProvider.notifier).setPageCount(pages.length); + }); + } + return Center( + child: AspectRatio( + aspectRatio: aspect, + child: Stack( + key: const Key('page_stack'), + children: [ + PdfPageView( + key: ValueKey('pdf_page_view_$pageNum'), + document: document, + pageNumber: pageNum, + alignment: Alignment.center, + ), + Consumer( + builder: (context, ref, _) { + final sig = ref.watch(signatureProvider); + final visible = ref.watch(signatureVisibilityProvider); + return visible + ? _buildPageOverlays(context, ref, sig) + : const SizedBox.shrink(); + }, + ), + ], + ), + ), + ); + }, + ); + } + return const SizedBox.shrink(); + } + + Widget _buildPageOverlays( + BuildContext context, + WidgetRef ref, + SignatureState sig, + ) { + final pdf = ref.watch(pdfProvider); + final current = pdf.currentPage; + final placed = pdf.placementsByPage[current] ?? const []; + final widgets = []; + for (int i = 0; i < placed.length; i++) { + final r = placed[i]; + widgets.add( + _buildSignatureOverlay( + context, + ref, + sig, + r, + interactive: false, + placedIndex: i, + ), + ); + } + if (sig.rect != null && + sig.editingEnabled && + (pdf.signedPage == null || pdf.signedPage == current)) { + widgets.add( + _buildSignatureOverlay(context, ref, sig, sig.rect!, interactive: true), + ); + } + return Stack(children: widgets); + } + + Widget _buildSignatureOverlay( + BuildContext context, + WidgetRef ref, + SignatureState sig, + Rect r, { + bool interactive = true, + int? placedIndex, + }) { + return LayoutBuilder( + builder: (context, constraints) { + final scaleX = constraints.maxWidth / pageSize.width; + final scaleY = constraints.maxHeight / pageSize.height; + final left = r.left * scaleX; + final top = r.top * scaleY; + final width = r.width * scaleX; + final height = r.height * scaleY; + + return Stack( + children: [ + Positioned( + left: left, + top: top, + width: width, + height: height, + child: Builder( + builder: (context) { + final selectedIdx = + ref.read(pdfProvider).selectedPlacementIndex; + final bool isPlaced = placedIndex != null; + final bool isSelected = + isPlaced && selectedIdx == placedIndex; + final Color borderColor = + isPlaced ? Colors.red : Colors.indigo; + final double borderWidth = + isPlaced ? (isSelected ? 3.0 : 2.0) : 2.0; + Widget content = DecoratedBox( + decoration: BoxDecoration( + color: Color.fromRGBO( + 0, + 0, + 0, + 0.05 + math.min(0.25, (sig.contrast - 1.0).abs()), + ), + border: Border.all( + color: borderColor, + width: borderWidth, + ), + ), + child: Stack( + children: [ + Consumer( + builder: (context, ref, _) { + final processed = ref.watch( + processedSignatureImageProvider, + ); + final bytes = processed ?? sig.imageBytes; + if (bytes == null) { + return Center( + child: Text( + AppLocalizations.of(context).signature, + ), + ); + } + return Image.memory(bytes, fit: BoxFit.contain); + }, + ), + if (interactive) + Positioned( + right: 0, + bottom: 0, + child: GestureDetector( + key: const Key('signature_handle'), + behavior: HitTestBehavior.opaque, + onPanUpdate: + (d) => onResizeSignature( + Offset( + d.delta.dx / scaleX, + d.delta.dy / scaleY, + ), + ), + child: const Icon(Icons.open_in_full, size: 20), + ), + ), + ], + ), + ); + if (interactive && sig.editingEnabled) { + content = GestureDetector( + key: const Key('signature_overlay'), + behavior: HitTestBehavior.opaque, + onPanStart: (_) {}, + onPanUpdate: + (d) => onDragSignature( + Offset(d.delta.dx / scaleX, d.delta.dy / scaleY), + ), + onSecondaryTapDown: (d) { + final pos = d.globalPosition; + showMenu( + context: context, + position: RelativeRect.fromLTRB( + pos.dx, + pos.dy, + pos.dx, + pos.dy, + ), + items: const [ + PopupMenuItem( + key: Key('ctx_active_confirm'), + value: 'confirm', + child: Text('Confirm'), + ), + PopupMenuItem( + key: Key('ctx_active_delete'), + value: 'delete', + child: Text('Delete'), + ), + ], + ).then((choice) { + if (choice == 'confirm') { + onConfirmSignature(); + } else if (choice == 'delete') { + onClearActiveOverlay(); + } + }); + }, + onLongPressStart: (d) { + final pos = d.globalPosition; + showMenu( + context: context, + position: RelativeRect.fromLTRB( + pos.dx, + pos.dy, + pos.dx, + pos.dy, + ), + items: const [ + PopupMenuItem( + key: Key('ctx_active_confirm_lp'), + value: 'confirm', + child: Text('Confirm'), + ), + PopupMenuItem( + key: Key('ctx_active_delete_lp'), + value: 'delete', + child: Text('Delete'), + ), + ], + ).then((choice) { + if (choice == 'confirm') { + onConfirmSignature(); + } else if (choice == 'delete') { + onClearActiveOverlay(); + } + }); + }, + child: content, + ); + } else { + content = GestureDetector( + key: Key('placed_signature_${placedIndex ?? 'x'}'), + behavior: HitTestBehavior.opaque, + onTap: () => onSelectPlaced(placedIndex), + onSecondaryTapDown: (d) { + if (placedIndex != null) { + _showContextMenuForPlaced( + context: context, + ref: ref, + globalPos: d.globalPosition, + index: placedIndex, + ); + } + }, + onLongPressStart: (d) { + if (placedIndex != null) { + _showContextMenuForPlaced( + context: context, + ref: ref, + globalPos: d.globalPosition, + index: placedIndex, + ); + } + }, + child: content, + ); + } + return content; + }, + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/ui/features/pdf/widgets/pdf_screen.dart b/lib/ui/features/pdf/widgets/pdf_screen.dart index 5e5ceb3..0dc027d 100644 --- a/lib/ui/features/pdf/widgets/pdf_screen.dart +++ b/lib/ui/features/pdf/widgets/pdf_screen.dart @@ -1,17 +1,17 @@ -import 'dart:math' as math; import 'dart:typed_data'; import 'package:file_selector/file_selector.dart' as fs; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; -import 'package:pdfrx/pdfrx.dart'; import 'package:printing/printing.dart' as printing; -import '../../../../data/model/model.dart'; import '../../../../data/services/providers.dart'; import '../view_model/view_model.dart'; import 'draw_canvas.dart'; +import 'pdf_toolbar.dart'; +import 'pdf_page_area.dart'; +import 'adjustments_panel.dart'; import '../../preferences/widgets/settings_screen.dart'; class PdfSignatureHomePage extends ConsumerStatefulWidget { @@ -24,7 +24,6 @@ class PdfSignatureHomePage extends ConsumerStatefulWidget { class _PdfSignatureHomePageState extends ConsumerState { static const Size _pageSize = SignatureController.pageSize; - final GlobalKey _captureKey = GlobalKey(); // Exposed for tests to trigger the invalid-file SnackBar without UI. @visibleForTesting @@ -107,37 +106,6 @@ class _PdfSignatureHomePageState extends ConsumerState { ref.read(pdfProvider.notifier).selectPlacement(index); } - Future _showContextMenuForPlaced({ - required Offset globalPos, - required int index, - }) async { - // Opening the menu implicitly selects the item - _onSelectPlaced(index); - final choice = await showMenu( - context: context, - position: RelativeRect.fromLTRB( - globalPos.dx, - globalPos.dy, - globalPos.dx, - globalPos.dy, - ), - items: [ - const PopupMenuItem( - key: Key('ctx_delete_signature'), - value: 'delete', - child: Text('Delete'), - ), - ], - ); - if (choice == null) return; - if (choice == 'delete') { - final currentPage = ref.read(pdfProvider).currentPage; - ref - .read(pdfProvider.notifier) - .removePlacement(page: currentPage, index: index); - } - } - Future _openDrawCanvas() async { final result = await showModalBottomSheet( context: context, @@ -303,7 +271,6 @@ class _PdfSignatureHomePageState extends ConsumerState { @override Widget build(BuildContext context) { - final pdf = ref.watch(pdfProvider); final isExporting = ref.watch(exportingProvider); final l = AppLocalizations.of(context); return Scaffold( @@ -314,12 +281,36 @@ class _PdfSignatureHomePageState extends ConsumerState { children: [ Column( children: [ - _buildToolbar(pdf, disabled: isExporting), + PdfToolbar( + disabled: isExporting, + onOpenSettings: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const SettingsScreen()), + ); + }, + onPickPdf: _pickPdf, + onJumpToPage: _jumpToPage, + onSave: _saveSignedPdf, + onLoadSignatureFromFile: _loadSignatureFromFile, + onCreateSignature: _createNewSignature, + onOpenDrawCanvas: _openDrawCanvas, + ), const SizedBox(height: 8), Expanded( child: AbsorbPointer( absorbing: isExporting, - child: _buildPageArea(pdf), + child: PdfPageArea( + pageSize: _pageSize, + onDragSignature: _onDragSignature, + onResizeSignature: _onResizeSignature, + onConfirmSignature: _confirmSignature, + onClearActiveOverlay: + () => + ref + .read(signatureProvider.notifier) + .clearActiveOverlay(), + onSelectPlaced: _onSelectPlaced, + ), ), ), Consumer( @@ -328,7 +319,7 @@ class _PdfSignatureHomePageState extends ConsumerState { return sig.rect != null ? AbsorbPointer( absorbing: isExporting, - child: _buildAdjustmentsPanel(sig), + child: AdjustmentsPanel(sig: sig), ) : const SizedBox.shrink(); }, @@ -359,503 +350,4 @@ class _PdfSignatureHomePageState extends ConsumerState { ), ); } - - Widget _buildToolbar(PdfState pdf, {bool disabled = false}) { - final dpi = ref.watch(exportDpiProvider); - final l = AppLocalizations.of(context); - final pageInfo = l.pageInfo(pdf.currentPage, pdf.pageCount); - return Wrap( - spacing: 8, - runSpacing: 8, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - OutlinedButton( - key: const Key('btn_open_settings'), - onPressed: - disabled - ? null - : () { - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const SettingsScreen()), - ); - }, - child: Text(l.settings), - ), - OutlinedButton( - key: const Key('btn_open_pdf_picker'), - onPressed: disabled ? null : _pickPdf, - child: Text(l.openPdf), - ), - if (pdf.loaded) ...[ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - key: const Key('btn_prev'), - onPressed: - disabled ? null : () => _jumpToPage(pdf.currentPage - 1), - icon: const Icon(Icons.chevron_left), - tooltip: l.prev, - ), - Text(pageInfo, key: const Key('lbl_page_info')), - IconButton( - key: const Key('btn_next'), - onPressed: - disabled ? null : () => _jumpToPage(pdf.currentPage + 1), - icon: const Icon(Icons.chevron_right), - tooltip: l.next, - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(l.goTo), - SizedBox( - width: 60, - child: TextField( - key: const Key('txt_goto'), - keyboardType: TextInputType.number, - enabled: !disabled, - onSubmitted: (v) { - final n = int.tryParse(v); - if (n != null) _jumpToPage(n); - }, - ), - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(l.dpi), - const SizedBox(width: 8), - DropdownButton( - key: const Key('ddl_export_dpi'), - value: dpi, - items: - const [96.0, 144.0, 200.0, 300.0] - .map( - (v) => DropdownMenuItem( - value: v, - child: Text(v.toStringAsFixed(0)), - ), - ) - .toList(), - onChanged: - disabled - ? null - : (v) { - if (v != null) { - ref.read(exportDpiProvider.notifier).state = v; - } - }, - ), - ], - ), - // Removed: Mark for signing button - if (pdf.loaded) - ElevatedButton( - key: const Key('btn_save_pdf'), - onPressed: disabled ? null : _saveSignedPdf, - child: Text(l.saveSignedPdf), - ), - // Signature tools are available when a PDF is loaded - OutlinedButton( - key: const Key('btn_load_signature_picker'), - onPressed: disabled || !pdf.loaded ? null : _loadSignatureFromFile, - child: Text(l.loadSignatureFromFile), - ), - OutlinedButton( - key: const Key('btn_create_signature'), - onPressed: disabled || !pdf.loaded ? null : _createNewSignature, - child: const Text('Create new signature'), - ), - ElevatedButton( - key: const Key('btn_draw_signature'), - onPressed: disabled || !pdf.loaded ? null : _openDrawCanvas, - child: Text(l.drawSignature), - ), - // Confirm and Delete are available via context menus - ], - ], - ); - } - - Widget _buildPageArea(PdfState pdf) { - if (!pdf.loaded) { - return Center(child: Text(AppLocalizations.of(context).noPdfLoaded)); - } - final useMock = ref.watch(useMockViewerProvider); - if (useMock) { - return Center( - child: AspectRatio( - aspectRatio: _pageSize.width / _pageSize.height, - child: RepaintBoundary( - key: _captureKey, - child: Stack( - key: const Key('page_stack'), - children: [ - Container( - key: ValueKey('pdf_page_view_${pdf.currentPage}'), - color: Colors.grey.shade200, - child: Center( - child: Text( - AppLocalizations.of( - context, - ).pageInfo(pdf.currentPage, pdf.pageCount), - style: const TextStyle( - fontSize: 24, - color: Colors.black54, - ), - ), - ), - ), - Consumer( - builder: (context, ref, _) { - final sig = ref.watch(signatureProvider); - final visible = ref.watch(signatureVisibilityProvider); - return visible - ? _buildPageOverlays(sig) - : const SizedBox.shrink(); - }, - ), - ], - ), - ), - ), - ); - } - // If a real PDF path is selected, show actual viewer. Otherwise, keep mock sample. - if (pdf.pickedPdfPath != null) { - return PdfDocumentViewBuilder.file( - pdf.pickedPdfPath!, - builder: (context, document) { - if (document == null) { - return const Center(child: CircularProgressIndicator()); - } - final pages = document.pages; - final pageNum = pdf.currentPage.clamp(1, pages.length); - final page = pages[pageNum - 1]; - final aspect = page.width / page.height; - // Update page count in state if needed (post-frame to avoid build loop) - if (pdf.pageCount != pages.length) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - ref.read(pdfProvider.notifier).setPageCount(pages.length); - } - }); - } - return Center( - child: AspectRatio( - aspectRatio: aspect, - child: RepaintBoundary( - key: _captureKey, - child: Stack( - key: const Key('page_stack'), - children: [ - PdfPageView( - key: ValueKey('pdf_page_view_$pageNum'), - document: document, - pageNumber: pageNum, - alignment: Alignment.center, - ), - Consumer( - builder: (context, ref, _) { - final sig = ref.watch(signatureProvider); - final visible = ref.watch(signatureVisibilityProvider); - return visible - ? _buildPageOverlays(sig) - : const SizedBox.shrink(); - }, - ), - ], - ), - ), - ), - ); - }, - ); - } - // Fallback should not occur when not using mock; still return empty view - return const SizedBox.shrink(); - } - - Widget _buildSignatureOverlay( - SignatureState sig, - Rect r, { - bool interactive = true, - int? placedIndex, - }) { - return LayoutBuilder( - builder: (context, constraints) { - final scaleX = constraints.maxWidth / _pageSize.width; - final scaleY = constraints.maxHeight / _pageSize.height; - final left = r.left * scaleX; - final top = r.top * scaleY; - final width = r.width * scaleX; - final height = r.height * scaleY; - - return Stack( - children: [ - Positioned( - left: left, - top: top, - width: width, - height: height, - child: Builder( - builder: (context) { - final selectedIdx = - ref.read(pdfProvider).selectedPlacementIndex; - final bool isPlaced = placedIndex != null; - final bool isSelected = - isPlaced && selectedIdx == placedIndex; - final Color borderColor = - isPlaced ? Colors.red : Colors.indigo; - final double borderWidth = - isPlaced ? (isSelected ? 3.0 : 2.0) : 2.0; - Widget content = DecoratedBox( - decoration: BoxDecoration( - color: Color.fromRGBO( - 0, - 0, - 0, - 0.05 + math.min(0.25, (sig.contrast - 1.0).abs()), - ), - border: Border.all( - color: borderColor, - width: borderWidth, - ), - ), - child: Stack( - children: [ - Consumer( - builder: (context, ref, _) { - final processed = ref.watch( - processedSignatureImageProvider, - ); - final bytes = processed ?? sig.imageBytes; - if (bytes == null) { - return Center( - child: Text( - AppLocalizations.of(context).signature, - ), - ); - } - return Image.memory(bytes, fit: BoxFit.contain); - }, - ), - if (interactive) - Positioned( - right: 0, - bottom: 0, - child: GestureDetector( - key: const Key('signature_handle'), - behavior: HitTestBehavior.opaque, - onPanUpdate: - (d) => _onResizeSignature( - Offset( - d.delta.dx / scaleX, - d.delta.dy / scaleY, - ), - ), - child: const Icon(Icons.open_in_full, size: 20), - ), - ), - // No inline buttons for placed overlays; use context menu instead - ], - ), - ); - if (interactive && sig.editingEnabled) { - content = GestureDetector( - key: const Key('signature_overlay'), - behavior: HitTestBehavior.opaque, - onPanStart: (_) {}, - onPanUpdate: - (d) => _onDragSignature( - Offset(d.delta.dx / scaleX, d.delta.dy / scaleY), - ), - onSecondaryTapDown: (d) { - // Context menu for active signature: confirm or delete draft (clear) - final pos = d.globalPosition; - showMenu( - context: context, - position: RelativeRect.fromLTRB( - pos.dx, - pos.dy, - pos.dx, - pos.dy, - ), - items: [ - const PopupMenuItem( - key: Key('ctx_active_confirm'), - value: 'confirm', - child: Text('Confirm'), - ), - const PopupMenuItem( - key: Key('ctx_active_delete'), - value: 'delete', - child: Text('Delete'), - ), - ], - ).then((choice) { - if (choice == 'confirm') { - _confirmSignature(); - } else if (choice == 'delete') { - ref - .read(signatureProvider.notifier) - .clearActiveOverlay(); - } - }); - }, - onLongPressStart: (d) { - final pos = d.globalPosition; - showMenu( - context: context, - position: RelativeRect.fromLTRB( - pos.dx, - pos.dy, - pos.dx, - pos.dy, - ), - items: [ - const PopupMenuItem( - key: Key('ctx_active_confirm_lp'), - value: 'confirm', - child: Text('Confirm'), - ), - const PopupMenuItem( - key: Key('ctx_active_delete_lp'), - value: 'delete', - child: Text('Delete'), - ), - ], - ).then((choice) { - if (choice == 'confirm') { - _confirmSignature(); - } else if (choice == 'delete') { - ref - .read(signatureProvider.notifier) - .clearActiveOverlay(); - } - }); - }, - child: content, - ); - } else { - // For placed items: tap to select; long-press/right-click for context menu - content = GestureDetector( - key: Key('placed_signature_${placedIndex ?? 'x'}'), - behavior: HitTestBehavior.opaque, - onTap: () => _onSelectPlaced(placedIndex), - onSecondaryTapDown: (d) { - if (placedIndex != null) { - _showContextMenuForPlaced( - globalPos: d.globalPosition, - index: placedIndex, - ); - } - }, - onLongPressStart: (d) { - if (placedIndex != null) { - _showContextMenuForPlaced( - globalPos: d.globalPosition, - index: placedIndex, - ); - } - }, - child: content, - ); - } - return content; - }, - ), - ), - ], - ); - }, - ); - } - - Widget _buildPageOverlays(SignatureState sig) { - final pdf = ref.watch(pdfProvider); - final current = pdf.currentPage; - final placed = pdf.placementsByPage[current] ?? const []; - final widgets = []; - for (int i = 0; i < placed.length; i++) { - final r = placed[i]; - widgets.add( - _buildSignatureOverlay(sig, r, interactive: false, placedIndex: i), - ); - } - // Show the active editing rect only on the selected (signed) page - if (sig.rect != null && - sig.editingEnabled && - (pdf.signedPage == null || pdf.signedPage == current)) { - widgets.add(_buildSignatureOverlay(sig, sig.rect!, interactive: true)); - } - return Stack(children: widgets); - } - - Widget _buildAdjustmentsPanel(SignatureState sig) { - return Column( - key: const Key('adjustments_panel'), - children: [ - Row( - children: [ - Checkbox( - key: const Key('chk_aspect_lock'), - value: sig.aspectLocked, - onChanged: - (v) => ref - .read(signatureProvider.notifier) - .toggleAspect(v ?? false), - ), - Text(AppLocalizations.of(context).lockAspectRatio), - const SizedBox(width: 16), - Switch( - key: const Key('swt_bg_removal'), - value: sig.bgRemoval, - onChanged: - (v) => ref.read(signatureProvider.notifier).setBgRemoval(v), - ), - Text(AppLocalizations.of(context).backgroundRemoval), - ], - ), - Row( - children: [ - Text(AppLocalizations.of(context).contrast), - Expanded( - child: Slider( - key: const Key('sld_contrast'), - min: 0.0, - max: 2.0, - value: sig.contrast, - onChanged: - (v) => ref.read(signatureProvider.notifier).setContrast(v), - ), - ), - Text(sig.contrast.toStringAsFixed(2)), - ], - ), - Row( - children: [ - Text(AppLocalizations.of(context).brightness), - Expanded( - child: Slider( - key: const Key('sld_brightness'), - min: -1.0, - max: 1.0, - value: sig.brightness, - onChanged: - (v) => - ref.read(signatureProvider.notifier).setBrightness(v), - ), - ), - Text(sig.brightness.toStringAsFixed(2)), - ], - ), - ], - ); - } } diff --git a/lib/ui/features/pdf/widgets/pdf_toolbar.dart b/lib/ui/features/pdf/widgets/pdf_toolbar.dart new file mode 100644 index 0000000..2c39479 --- /dev/null +++ b/lib/ui/features/pdf/widgets/pdf_toolbar.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:pdf_signature/l10n/app_localizations.dart'; + +import '../../../../data/services/providers.dart'; +import '../view_model/view_model.dart'; + +class PdfToolbar extends ConsumerWidget { + const PdfToolbar({ + super.key, + required this.disabled, + required this.onOpenSettings, + required this.onPickPdf, + required this.onJumpToPage, + required this.onSave, + required this.onLoadSignatureFromFile, + required this.onCreateSignature, + required this.onOpenDrawCanvas, + }); + + final bool disabled; + final VoidCallback onOpenSettings; + final VoidCallback onPickPdf; + final ValueChanged onJumpToPage; + final VoidCallback onSave; + final VoidCallback onLoadSignatureFromFile; + final VoidCallback onCreateSignature; + final VoidCallback onOpenDrawCanvas; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final pdf = ref.watch(pdfProvider); + final dpi = ref.watch(exportDpiProvider); + final l = AppLocalizations.of(context); + final pageInfo = l.pageInfo(pdf.currentPage, pdf.pageCount); + + return Wrap( + spacing: 8, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + OutlinedButton( + key: const Key('btn_open_settings'), + onPressed: disabled ? null : onOpenSettings, + child: Text(l.settings), + ), + OutlinedButton( + key: const Key('btn_open_pdf_picker'), + onPressed: disabled ? null : onPickPdf, + child: Text(l.openPdf), + ), + if (pdf.loaded) ...[ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + key: const Key('btn_prev'), + onPressed: + disabled ? null : () => onJumpToPage(pdf.currentPage - 1), + icon: const Icon(Icons.chevron_left), + tooltip: l.prev, + ), + Text(pageInfo, key: const Key('lbl_page_info')), + IconButton( + key: const Key('btn_next'), + onPressed: + disabled ? null : () => onJumpToPage(pdf.currentPage + 1), + icon: const Icon(Icons.chevron_right), + tooltip: l.next, + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(l.goTo), + SizedBox( + width: 60, + child: TextField( + key: const Key('txt_goto'), + keyboardType: TextInputType.number, + enabled: !disabled, + onSubmitted: (v) { + final n = int.tryParse(v); + if (n != null) onJumpToPage(n); + }, + ), + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(l.dpi), + const SizedBox(width: 8), + DropdownButton( + key: const Key('ddl_export_dpi'), + value: dpi, + items: + const [96.0, 144.0, 200.0, 300.0] + .map( + (v) => DropdownMenuItem( + value: v, + child: Text(v.toStringAsFixed(0)), + ), + ) + .toList(), + onChanged: + disabled + ? null + : (v) { + if (v != null) { + ref.read(exportDpiProvider.notifier).state = v; + } + }, + ), + ], + ), + ElevatedButton( + key: const Key('btn_save_pdf'), + onPressed: disabled ? null : onSave, + child: Text(l.saveSignedPdf), + ), + OutlinedButton( + key: const Key('btn_load_signature_picker'), + onPressed: disabled || !pdf.loaded ? null : onLoadSignatureFromFile, + child: Text(l.loadSignatureFromFile), + ), + OutlinedButton( + key: const Key('btn_create_signature'), + onPressed: disabled || !pdf.loaded ? null : onCreateSignature, + child: const Text('Create new signature'), + ), + ElevatedButton( + key: const Key('btn_draw_signature'), + onPressed: disabled || !pdf.loaded ? null : onOpenDrawCanvas, + child: Text(l.drawSignature), + ), + ], + ], + ); + } +}