From db0912b12fed8c4f8f58fb4d3f66f78114c10554 Mon Sep 17 00:00:00 2001 From: insleker Date: Tue, 2 Sep 2025 15:19:36 +0800 Subject: [PATCH] feat: partially update UI view to new design --- docs/wireframe.md | 13 +- lib/app.dart | 23 +- lib/l10n/app_de.arb | 1 + lib/l10n/app_en.arb | 2 + lib/l10n/app_es.arb | 1 + lib/l10n/app_fr.arb | 1 + lib/l10n/app_ja.arb | 1 + lib/l10n/app_ko.arb | 1 + lib/l10n/app_uk.arb | 1 + lib/l10n/app_zh.arb | 1 + lib/l10n/app_zh_CN.arb | 1 + lib/l10n/app_zh_TW.arb | 1 + .../features/pdf/view_model/view_model.dart | 10 + .../features/pdf/widgets/pdf_page_area.dart | 49 +- lib/ui/features/pdf/widgets/pdf_screen.dart | 439 +++++++++++------- lib/ui/features/pdf/widgets/pdf_toolbar.dart | 187 +++++--- .../pdf/widgets/signature_drawer.dart | 206 ++++++++ .../welcome/widgets/welcome_screen.dart | 30 +- 18 files changed, 697 insertions(+), 271 deletions(-) create mode 100644 lib/ui/features/pdf/widgets/signature_drawer.dart diff --git a/docs/wireframe.md b/docs/wireframe.md index 4902cc1..7c21a32 100644 --- a/docs/wireframe.md +++ b/docs/wireframe.md @@ -44,17 +44,20 @@ Route: root --> opened Design notes: - Top: A small toolbar sits at the top edge with file name text, open pdf file button, previous/next page widgets and zoom controls. - - Navigation: Previous page, Next page, and a page number input (e.g., “2 / 10”) with jump-on-Enter. - - Zoom: Zoom out, Zoom level dropdown (percent), Zoom in, Fit width, Fit page, Reset zoom. - - Optional: Find/search within PDF (if supported by engine). + - On the far left of the toolbar there is a button that can turn the document pages overview sidebar on and off. + - On the far right of the toolbar there is a button that can turn the signature cards overview sidebar on and off. + - Navigation: Previous page, Next page, and a page number input (e.g., “2 / 10”) with jump-on-Enter. + - Zoom: Zoom out, Zoom level dropdown (percent), Zoom in, Fit width, Fit page, Reset zoom. + - Optional: Find/search within PDF (if supported by engine). - Left pane: vertical strip of page thumbnails (e.g., page1, page2, page3). Clicking a thumbnail navigates to that page; the current page is visually indicated. - Center: main PDF viewer shows the active page. + - wheel to scroll pages. - Ctrl/Cmd + wheel to zoom. - Right pane: signatures drawer displaying saved signatures as cards. - able to drag and drop signature cards onto the PDF as placed signatures. - Each signature card shows a preview. - long tap/right-click will show menu with options to delete, adjust graphic of image. - - "adjust graphic" opens a simple image editor, which can remove backgrounds. + - "adjust graphic" opens a simple image editor, which can remove backgrounds, Rotate (rotation handle). - There is an empty card with "new signature" prompt and 2 buttons: "from file" and "draw". - "from file" opens a file picker to select an image as a signature card. - "draw" opens a simple drawing interface (draw canvas) to create a signature card. @@ -62,7 +65,7 @@ Design notes: Signature controls (after placing on page): - Select to show bounding box with resize handles and a small inline action bar. -- Actions: Move (drag), Resize (corner/side handles), Rotate (rotation handle), Duplicate, Delete (trash icon or Delete key). +- Actions: Move (drag), Resize (corner/side handles), Delete (trash icon or Delete key). - Lock: Lock/Unlock position. - Keyboard: Arrow keys to nudge (Shift for 10px); Shift-resize to keep aspect; Esc to cancel; Ctrl/Cmd+D to duplicate; Del/Backspace to delete. diff --git a/lib/app.dart b/lib/app.dart index 6a1cb89..84bbc39 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -6,6 +6,7 @@ import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/ui/features/welcome/widgets/welcome_screen.dart'; import 'ui/features/preferences/providers.dart'; +import 'package:pdf_signature/ui/features/preferences/widgets/settings_screen.dart'; class MyApp extends StatelessWidget { const MyApp({super.key}); @@ -62,7 +63,27 @@ class MyApp extends StatelessWidget { ...AppLocalizations.localizationsDelegates, LocaleNamesLocalizationsDelegate(), ], - home: const _RootHomeSwitcher(), + home: Builder( + builder: + (ctx) => Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(ctx).appTitle), + actions: [ + OutlinedButton.icon( + key: const Key('btn_appbar_settings'), + icon: const Icon(Icons.settings), + label: Text(AppLocalizations.of(ctx).settings), + onPressed: + () => showDialog( + context: ctx, + builder: (_) => const SettingsDialog(), + ), + ), + ], + ), + body: const _RootHomeSwitcher(), + ), + ), ); }, ); diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 47129d9..a43ed67 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -26,6 +26,7 @@ "longPressOrRightClickTheSignatureToConfirmOrDelete": "Drücken Sie lange oder klicken Sie mit der rechten Maustaste auf die Signatur, um sie zu bestätigen oder zu löschen.", "next": "Weiter", "noPdfLoaded": "Keine PDF-Datei geladen", + "noSignatureLoaded": "Keine Signatur geladen", "nothingToSaveYet": "Noch nichts zu speichern", "openPdf": "PDF öffnen...", "pageInfo": "Seite {current}/{total}", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6cd67e5..e82cd40 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -61,6 +61,8 @@ "@next": {}, "noPdfLoaded": "No PDF loaded", "@noPdfLoaded": {}, + "noSignatureLoaded": "No signature loaded", + "@noSignatureLoaded": {}, "nothingToSaveYet": "Nothing to save yet", "@nothingToSaveYet": {}, "openPdf": "Open PDF...", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index b0c7f80..423b127 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -26,6 +26,7 @@ "longPressOrRightClickTheSignatureToConfirmOrDelete": "Pulse prolongadamente o haga clic derecho en la firma para confirmar o eliminar.", "next": "Siguiente", "noPdfLoaded": "No se ha cargado ningún PDF", + "noSignatureLoaded": "No se ha cargado ninguna firma", "nothingToSaveYet": "Aún no hay nada que guardar", "openPdf": "Abrir PDF...", "pageInfo": "Página {current}/{total}", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 01336c4..3321eb1 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -26,6 +26,7 @@ "longPressOrRightClickTheSignatureToConfirmOrDelete": "Appuyez longuement ou cliquez droit sur la signature pour la confirmer ou la supprimer.", "next": "Suivant", "noPdfLoaded": "Aucun PDF chargé", + "noSignatureLoaded": "Aucune signature chargée", "nothingToSaveYet": "Rien à enregistrer pour le moment", "openPdf": "Ouvrir un PDF...", "pageInfo": "Page {current}/{total}", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 174dc9f..eb55c31 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -26,6 +26,7 @@ "longPressOrRightClickTheSignatureToConfirmOrDelete": "署名を長押しまたは右クリックして、確認または削除します。", "next": "次へ", "noPdfLoaded": "PDFが読み込まれていません", + "noSignatureLoaded": "署名は読み込まれていません", "nothingToSaveYet": "まだ保存するものがありません", "openPdf": "PDFを開く…", "pageInfo": "ページ {current}/{total}", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 617ae83..22e8834 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -26,6 +26,7 @@ "longPressOrRightClickTheSignatureToConfirmOrDelete": "서명을 길게 누르거나 마우스 오른쪽 버튼을 클릭하여 확인하거나 삭제합니다.", "next": "다음", "noPdfLoaded": "로드된 PDF 없음", + "noSignatureLoaded": "서명이 로드되지 않았습니다", "nothingToSaveYet": "아직 저장할 내용이 없습니다.", "openPdf": "PDF 열기...", "pageInfo": "{current}/{total} 페이지", diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 0f35263..23aea3a 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -26,6 +26,7 @@ "longPressOrRightClickTheSignatureToConfirmOrDelete": "Довго натисніть або клацніть правою кнопкою миші на підпис, щоб підтвердити або видалити.", "next": "Далі", "noPdfLoaded": "PDF не завантажено", + "noSignatureLoaded": "Не завантажено жодного підпису", "nothingToSaveYet": "Ще нічого не потрібно зберігати", "openPdf": "Відкрити PDF...", "pageInfo": "Сторінка {current}/{total}", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 6f6d41b..426e4e7 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -27,6 +27,7 @@ "longPressOrRightClickTheSignatureToConfirmOrDelete": "長按或右鍵點擊簽名以確認或刪除。", "next": "下一頁", "noPdfLoaded": "尚未載入 PDF", + "noSignatureLoaded": "没有加载签名", "nothingToSaveYet": "尚無可儲存的內容", "openPdf": "開啟 PDF…", "pageInfo": "第 {current}/{total} 頁", diff --git a/lib/l10n/app_zh_CN.arb b/lib/l10n/app_zh_CN.arb index e31d96a..9ec0a6e 100644 --- a/lib/l10n/app_zh_CN.arb +++ b/lib/l10n/app_zh_CN.arb @@ -26,6 +26,7 @@ "longPressOrRightClickTheSignatureToConfirmOrDelete": "长按或右键单击签名以确认或删除。", "next": "下一步", "noPdfLoaded": "未加载 PDF", + "noSignatureLoaded": "未加载签名", "nothingToSaveYet": "尚无内容保存", "openPdf": "打开 PDF...", "pageInfo": "第 {current} 页 / 共 {total} 页", diff --git a/lib/l10n/app_zh_TW.arb b/lib/l10n/app_zh_TW.arb index 8b4f233..f41bbd4 100644 --- a/lib/l10n/app_zh_TW.arb +++ b/lib/l10n/app_zh_TW.arb @@ -27,6 +27,7 @@ "longPressOrRightClickTheSignatureToConfirmOrDelete": "長按或右鍵點擊簽名以確認或刪除。", "next": "下一頁", "noPdfLoaded": "尚未載入 PDF", + "noSignatureLoaded": "未載入任何簽名", "nothingToSaveYet": "尚無可儲存的內容", "openPdf": "開啟 PDF…", "pageInfo": "第 {current}/{total} 頁", diff --git a/lib/ui/features/pdf/view_model/view_model.dart b/lib/ui/features/pdf/view_model/view_model.dart index 8c400da..225d1df 100644 --- a/lib/ui/features/pdf/view_model/view_model.dart +++ b/lib/ui/features/pdf/view_model/view_model.dart @@ -251,6 +251,16 @@ class SignatureController extends StateNotifier { state = state.copyWith(editingEnabled: true); } + void clearImage() { + state = state.copyWith(imageBytes: null, rect: null, editingEnabled: false); + } + + void placeAtCenter(Offset center, {double width = 120, double height = 60}) { + Rect r = Rect.fromCenter(center: center, width: width, height: height); + r = _clampRectToPage(r); + state = state.copyWith(rect: r, editingEnabled: true); + } + // Confirm current signature: freeze editing and place it on the PDF as an immutable overlay. // Returns the Rect placed, or null if no rect to confirm. Rect? confirmCurrentSignature(WidgetRef ref) { diff --git a/lib/ui/features/pdf/widgets/pdf_page_area.dart b/lib/ui/features/pdf/widgets/pdf_page_area.dart index 69e5bb8..f9ae429 100644 --- a/lib/ui/features/pdf/widgets/pdf_page_area.dart +++ b/lib/ui/features/pdf/widgets/pdf_page_area.dart @@ -9,21 +9,22 @@ import '../../../../data/services/providers.dart'; import '../../../../data/model/model.dart'; import '../view_model/view_model.dart'; import '../../preferences/providers.dart'; +import 'signature_drawer.dart'; class PdfPageArea extends ConsumerStatefulWidget { const PdfPageArea({ super.key, required this.pageSize, - this.controller, required this.onDragSignature, required this.onResizeSignature, required this.onConfirmSignature, required this.onClearActiveOverlay, required this.onSelectPlaced, + this.viewerController, }); final Size pageSize; - final TransformationController? controller; + final PdfViewerController? viewerController; final ValueChanged onDragSignature; final ValueChanged onResizeSignature; final VoidCallback onConfirmSignature; @@ -35,7 +36,8 @@ class PdfPageArea extends ConsumerStatefulWidget { class _PdfPageAreaState extends ConsumerState { final Map _pageKeys = {}; - final PdfViewerController _viewerController = PdfViewerController(); + late final PdfViewerController _viewerController = + widget.viewerController ?? PdfViewerController(); // Guards to avoid scroll feedback between provider and viewer int? _programmaticTargetPage; bool _suppressProviderListen = false; @@ -58,6 +60,8 @@ class _PdfPageAreaState extends ConsumerState { }); } + // No dispose required for PdfViewerController (managed by owner if any) + GlobalKey _pageKey(int page) => _pageKeys.putIfAbsent( page, () => GlobalKey(debugLabel: 'cont_page_$page'), @@ -216,7 +220,7 @@ class _PdfPageAreaState extends ConsumerState { } }); } - return SingleChildScrollView( + final content = SingleChildScrollView( key: const Key('pdf_continuous_mock_list'), padding: const EdgeInsets.symmetric(vertical: 8), child: Column( @@ -270,6 +274,7 @@ class _PdfPageAreaState extends ConsumerState { }), ), ); + return content; }, ); } @@ -277,14 +282,14 @@ class _PdfPageAreaState extends ConsumerState { // Real continuous mode (pdfrx): copy example patterns // https://github.com/espresso3389/pdfrx/blob/2cc32c1e2aa2a054602d20a5e7cf60bcc2d6a889/packages/pdfrx/example/viewer/lib/main.dart if (pdf.pickedPdfPath != null && isContinuous) { - return PdfViewer.file( + final viewer = PdfViewer.file( pdf.pickedPdfPath!, controller: _viewerController, params: PdfViewerParams( pageAnchor: PdfPageAnchor.top, keyHandlerParams: PdfViewerKeyHandlerParams(autofocus: true), maxScale: 8, - // scrollByMouseWheel: 0.6, + scrollByMouseWheel: 0.6, // Add overlay scroll thumbs (vertical on right, horizontal on bottom) viewerOverlayBuilder: (context, size, handleLinkTap) => [ @@ -294,7 +299,7 @@ class _PdfPageAreaState extends ConsumerState { thumbSize: const Size(40, 24), thumbBuilder: (context, thumbSize, pageNumber, controller) => Container( - color: Colors.black.withOpacity(0.7), + color: Colors.black.withValues(alpha: 0.7), child: Center( child: Text( pageNumber.toString(), @@ -309,7 +314,7 @@ class _PdfPageAreaState extends ConsumerState { thumbSize: const Size(40, 24), thumbBuilder: (context, thumbSize, pageNumber, controller) => Container( - color: Colors.black.withOpacity(0.7), + color: Colors.black.withValues(alpha: 0.7), child: Center( child: Text( pageNumber.toString(), @@ -375,6 +380,34 @@ class _PdfPageAreaState extends ConsumerState { }, ), ); + // Accept drops of signature card over the viewer + final drop = DragTarget( + onWillAcceptWithDetails: (details) => details.data is SignatureDragData, + onAcceptWithDetails: (details) { + // Map the local position to UI page coordinates of the visible page + final box = context.findRenderObject() as RenderBox?; + if (box == null) return; + final local = box.globalToLocal(details.offset); + final size = box.size; + // Assume drop targets the current visible page; compute relative center + final cx = (local.dx / size.width) * widget.pageSize.width; + final cy = (local.dy / size.height) * widget.pageSize.height; + ref.read(signatureProvider.notifier).placeAtCenter(Offset(cx, cy)); + ref + .read(pdfProvider.notifier) + .setSignedPage(ref.read(pdfProvider).currentPage); + }, + builder: + (context, candidateData, rejected) => Stack( + fit: StackFit.expand, + children: [ + viewer, + if (candidateData.isNotEmpty) + Container(color: Colors.blue.withValues(alpha: 0.08)), + ], + ), + ); + return drop; } return const SizedBox.shrink(); diff --git a/lib/ui/features/pdf/widgets/pdf_screen.dart b/lib/ui/features/pdf/widgets/pdf_screen.dart index aea2668..c6dc7d1 100644 --- a/lib/ui/features/pdf/widgets/pdf_screen.dart +++ b/lib/ui/features/pdf/widgets/pdf_screen.dart @@ -5,15 +5,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:printing/printing.dart' as printing; +import 'package:pdfrx/pdfrx.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 'pdf_pages_overview.dart'; -import '../../preferences/widgets/settings_screen.dart'; +import 'signature_drawer.dart'; +import 'adjustments_panel.dart'; class PdfSignatureHomePage extends ConsumerStatefulWidget { const PdfSignatureHomePage({super.key}); @@ -25,7 +26,9 @@ class PdfSignatureHomePage extends ConsumerStatefulWidget { class _PdfSignatureHomePageState extends ConsumerState { static const Size _pageSize = SignatureController.pageSize; - final TransformationController _ivController = TransformationController(); + final PdfViewerController _viewerController = PdfViewerController(); + bool _showPagesSidebar = true; + bool _showSignaturesSidebar = true; // Exposed for tests to trigger the invalid-file SnackBar without UI. @visibleForTesting @@ -52,6 +55,8 @@ class _PdfSignatureHomePageState extends ConsumerState { ref.read(pdfProvider.notifier).jumpTo(page); } + // Zoom is managed by pdfrx viewer (Ctrl +/- etc.). No custom zoom here. + Future _loadSignatureFromFile() async { final typeGroup = const fs.XTypeGroup( label: 'Image', @@ -68,25 +73,7 @@ class _PdfSignatureHomePageState extends ConsumerState { } } - void _createNewSignature() { - final sig = ref.read(signatureProvider.notifier); - if (ref.read(pdfProvider).loaded) { - sig.placeDefaultRect(); - ref - .read(pdfProvider.notifier) - .setSignedPage(ref.read(pdfProvider).currentPage); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - AppLocalizations.of( - context, - ).longPressOrRightClickTheSignatureToConfirmOrDelete, - ), - duration: const Duration(seconds: 3), - ), - ); - } - } + // _createNewSignature was removed as the toolbar no longer exposes this action. void _confirmSignature() { ref.read(signatureProvider.notifier).confirmCurrentSignature(ref); @@ -250,7 +237,6 @@ class _PdfSignatureHomePageState extends ConsumerState { @override void dispose() { - _ivController.dispose(); super.dispose(); } @@ -259,64 +245,62 @@ class _PdfSignatureHomePageState extends ConsumerState { final isExporting = ref.watch(exportingProvider); final l = AppLocalizations.of(context); return Scaffold( - appBar: AppBar( - title: Text(l.appTitle), - actions: [ - IconButton( - key: const Key('btn_appbar_settings'), - tooltip: l.settings, - onPressed: - () => showDialog( - context: context, - builder: (_) => const SettingsDialog(), - ), - icon: const Icon(Icons.settings), - ), - ], - ), body: Padding( padding: const EdgeInsets.all(12), child: Stack( children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.stretch, + Column( children: [ - // Left: pages overview (thumbnails + navigation) - ConstrainedBox( - constraints: const BoxConstraints( - minWidth: 140, - maxWidth: 180, - ), - child: Card( - margin: EdgeInsets.zero, - child: const PdfPagesOverview(), - ), + // Full-width toolbar row + PdfToolbar( + disabled: isExporting, + onPickPdf: _pickPdf, + onJumpToPage: _jumpToPage, + onZoomOut: () { + if (_viewerController.isReady) { + _viewerController.zoomDown(); + } + }, + onZoomIn: () { + if (_viewerController.isReady) { + _viewerController.zoomUp(); + } + }, + fileName: ref.watch(pdfProvider).pickedPdfPath, + showPagesSidebar: _showPagesSidebar, + showSignaturesSidebar: _showSignaturesSidebar, + onTogglePagesSidebar: + () => setState(() { + _showPagesSidebar = !_showPagesSidebar; + }), + onToggleSignaturesSidebar: + () => setState(() { + _showSignaturesSidebar = !_showSignaturesSidebar; + }), ), - const SizedBox(width: 12), + const SizedBox(height: 8), Expanded( - child: Column( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - PdfToolbar( - disabled: isExporting, - onOpenSettings: - () => showDialog( - context: context, - builder: (_) => const SettingsDialog(), - ), - onPickPdf: _pickPdf, - onJumpToPage: _jumpToPage, - onSave: _saveSignedPdf, - onLoadSignatureFromFile: _loadSignatureFromFile, - onCreateSignature: _createNewSignature, - onOpenDrawCanvas: _openDrawCanvas, - ), - const SizedBox(height: 8), + if (_showPagesSidebar) + ConstrainedBox( + constraints: const BoxConstraints( + minWidth: 140, + maxWidth: 180, + ), + child: Card( + margin: EdgeInsets.zero, + child: const PdfPagesOverview(), + ), + ), + if (_showPagesSidebar) const SizedBox(width: 12), Expanded( child: AbsorbPointer( absorbing: isExporting, child: PdfPageArea( pageSize: _pageSize, - controller: _ivController, + viewerController: _viewerController, onDragSignature: _onDragSignature, onResizeSignature: _onResizeSignature, onConfirmSignature: _confirmSignature, @@ -329,108 +313,243 @@ class _PdfSignatureHomePageState extends ConsumerState { ), ), ), - ], - ), - ), - const SizedBox(width: 12), - ConstrainedBox( - constraints: const BoxConstraints( - minWidth: 280, - maxWidth: 360, - ), - child: Consumer( - builder: (context, ref, _) { - final sig = ref.watch(signatureProvider); - if (sig.rect != null) { - return AbsorbPointer( - absorbing: isExporting, - child: Card( - margin: EdgeInsets.zero, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Signature preview - Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - AppLocalizations.of(context).signature, - style: - Theme.of( - context, - ).textTheme.titleSmall, - ), - const SizedBox(height: 8), - DecoratedBox( - decoration: BoxDecoration( - border: Border.all( - color: - Theme.of(context).dividerColor, + if (_showSignaturesSidebar) const SizedBox(width: 12), + if (_showSignaturesSidebar) + ConstrainedBox( + constraints: const BoxConstraints( + minWidth: 280, + maxWidth: 360, + ), + child: Consumer( + builder: (context, ref, _) { + final sig = ref.watch(signatureProvider); + final bytes = + ref.watch(processedSignatureImageProvider) ?? + sig.imageBytes; + return AbsorbPointer( + absorbing: isExporting, + child: Card( + margin: EdgeInsets.zero, + child: LayoutBuilder( + builder: (context, cons) { + return SingleChildScrollView( + padding: EdgeInsets.zero, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: cons.maxHeight, ), - borderRadius: BorderRadius.circular( - 8, - ), - ), - child: AspectRatio( - aspectRatio: 3 / 1, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Consumer( - builder: (context, ref, _) { - final bytes = - ref.watch( - processedSignatureImageProvider, - ) ?? - sig.imageBytes; - if (bytes == null) { - return Center( - child: Text( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all( + 12, + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( AppLocalizations.of( context, - ).noPdfLoaded, + ).signature, + style: + Theme.of(context) + .textTheme + .titleSmall, ), - ); - } - return Image.memory( - bytes, - fit: BoxFit.contain, - ); - }, - ), + const SizedBox(height: 8), + DecoratedBox( + decoration: BoxDecoration( + border: Border.all( + color: + Theme.of( + context, + ).dividerColor, + ), + borderRadius: + BorderRadius.circular( + 8, + ), + ), + child: AspectRatio( + aspectRatio: 3 / 1, + child: Padding( + padding: + const EdgeInsets.all( + 8.0, + ), + child: Builder( + builder: (context) { + final placeholder = Center( + child: Text( + AppLocalizations.of( + context, + ).noSignatureLoaded, + ), + ); + if (bytes == + null || + bytes + .isEmpty) { + return placeholder; + } + final img = + Image.memory( + bytes, + fit: + BoxFit + .contain, + ); + return Draggable< + Object + >( + data: + const SignatureDragData(), + feedback: Opacity( + opacity: 0.85, + child: ConstrainedBox( + constraints: + const BoxConstraints.tightFor( + width: + 160, + height: + 80, + ), + child: DecoratedBox( + decoration: BoxDecoration( + color: + Colors.white, + borderRadius: + BorderRadius.circular( + 6, + ), + boxShadow: const [ + BoxShadow( + blurRadius: + 8, + color: + Colors.black26, + ), + ], + ), + child: Padding( + padding: + const EdgeInsets.all( + 6.0, + ), + child: Image.memory( + bytes, + fit: + BoxFit.contain, + ), + ), + ), + ), + ), + childWhenDragging: + Opacity( + opacity: + 0.5, + child: + img, + ), + child: img, + ); + }, + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 4), + Padding( + padding: + const EdgeInsets.symmetric( + horizontal: 12, + ), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: [ + OutlinedButton.icon( + key: const Key( + 'btn_load_signature_picker', + ), + onPressed: + !ref + .read( + pdfProvider, + ) + .loaded + ? null + : _loadSignatureFromFile, + icon: const Icon( + Icons.image_outlined, + ), + label: Text( + AppLocalizations.of( + context, + ).loadSignatureFromFile, + ), + ), + OutlinedButton.icon( + key: const Key( + 'btn_draw_signature', + ), + onPressed: + !ref + .read( + pdfProvider, + ) + .loaded + ? null + : _openDrawCanvas, + icon: const Icon( + Icons.gesture, + ), + label: Text( + AppLocalizations.of( + context, + ).drawSignature, + ), + ), + ], + ), + ), + const Divider(height: 1), + Padding( + padding: const EdgeInsets.all( + 12, + ), + child: AdjustmentsPanel( + sig: sig, + ), + ), + const Divider(height: 1), + ElevatedButton( + key: const Key('btn_save_pdf'), + onPressed: + isExporting + ? null + : _saveSignedPdf, + child: Text(l.saveSignedPdf), + ), + ], ), ), - ), - ], + ); + }, ), ), - const Divider(height: 1), - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(12), - child: AdjustmentsPanel(sig: sig), - ), - ), - ], - ), - ), - ); - } - return Card( - margin: EdgeInsets.zero, - child: Center( - child: Padding( - padding: const EdgeInsets.all(12), - child: Text( - AppLocalizations.of(context).signature, - style: Theme.of(context).textTheme.bodyMedium, - ), + ); + }, ), ), - ); - }, + ], ), ), ], diff --git a/lib/ui/features/pdf/widgets/pdf_toolbar.dart b/lib/ui/features/pdf/widgets/pdf_toolbar.dart index c7620b9..634d41b 100644 --- a/lib/ui/features/pdf/widgets/pdf_toolbar.dart +++ b/lib/ui/features/pdf/widgets/pdf_toolbar.dart @@ -10,23 +10,27 @@ class PdfToolbar extends ConsumerStatefulWidget { 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, + required this.onZoomOut, + required this.onZoomIn, + this.fileName, + required this.showPagesSidebar, + required this.showSignaturesSidebar, + required this.onTogglePagesSidebar, + required this.onToggleSignaturesSidebar, }); final bool disabled; - final VoidCallback onOpenSettings; final VoidCallback onPickPdf; final ValueChanged onJumpToPage; - final VoidCallback onSave; - final VoidCallback onLoadSignatureFromFile; - final VoidCallback onCreateSignature; - final VoidCallback onOpenDrawCanvas; + final String? fileName; + final VoidCallback onZoomOut; + final VoidCallback onZoomIn; + final bool showPagesSidebar; + final bool showSignaturesSidebar; + final VoidCallback onTogglePagesSidebar; + final VoidCallback onToggleSignaturesSidebar; @override ConsumerState createState() => _PdfToolbarState(); @@ -57,21 +61,34 @@ class _PdfToolbarState extends ConsumerState { return LayoutBuilder( builder: (context, constraints) { final bool compact = constraints.maxWidth < 260; - final double gotoWidth = compact ? 60 : 100; - return Wrap( + final double gotoWidth = 50; + + // Center content of the toolbar + final center = Wrap( spacing: 8, runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ - OutlinedButton( - key: const Key('btn_open_settings'), - onPressed: widget.disabled ? null : widget.onOpenSettings, - child: Text(l.settings), - ), OutlinedButton( key: const Key('btn_open_pdf_picker'), onPressed: widget.disabled ? null : widget.onPickPdf, - child: Text(l.openPdf), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.insert_drive_file, size: 18), + const SizedBox(width: 6), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 220), + child: Text( + // if filename not null + widget.fileName != null + ? widget.fileName! + : 'No file selected', + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), ), if (pdf.loaded) ...[ Row( @@ -86,6 +103,7 @@ class _PdfToolbarState extends ConsumerState { icon: const Icon(Icons.chevron_left), tooltip: l.prev, ), + // Current page label Text(pageInfo, key: const Key('lbl_page_info')), IconButton( key: const Key('btn_next'), @@ -96,36 +114,51 @@ class _PdfToolbarState extends ConsumerState { icon: const Icon(Icons.chevron_right), tooltip: l.next, ), - ], - ), - Wrap( - spacing: 6, - runSpacing: 4, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text(l.goTo), - SizedBox( - width: gotoWidth, - child: TextField( - key: const Key('txt_goto'), - controller: _goToController, - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - enabled: !widget.disabled, - decoration: InputDecoration( - isDense: true, - hintText: '1..${pdf.pageCount}', + Wrap( + spacing: 6, + runSpacing: 4, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text(l.goTo), + SizedBox( + width: gotoWidth, + child: TextField( + key: const Key('txt_goto'), + controller: _goToController, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + enabled: !widget.disabled, + decoration: InputDecoration( + isDense: true, + hintText: '1..${pdf.pageCount}', + ), + onSubmitted: (_) => _submitGoTo(), + ), ), - onSubmitted: (_) => _submitGoTo(), - ), + if (!compact) + IconButton( + key: const Key('btn_goto_apply'), + tooltip: l.goTo, + icon: const Icon(Icons.arrow_forward), + onPressed: widget.disabled ? null : _submitGoTo, + ), + ], + ), + const SizedBox(width: 8), + IconButton( + key: const Key('btn_zoom_out'), + tooltip: 'Zoom out', + onPressed: widget.disabled ? null : widget.onZoomOut, + icon: const Icon(Icons.zoom_out), + ), + IconButton( + key: const Key('btn_zoom_in'), + tooltip: 'Zoom in', + onPressed: widget.disabled ? null : widget.onZoomIn, + icon: const Icon(Icons.zoom_in), ), - if (!compact) - IconButton( - key: const Key('btn_goto_apply'), - tooltip: l.goTo, - icon: const Icon(Icons.arrow_forward), - onPressed: widget.disabled ? null : _submitGoTo, - ), ], ), Row( @@ -156,38 +189,42 @@ class _PdfToolbarState extends ConsumerState { ), ], ), - ElevatedButton( - key: const Key('btn_save_pdf'), - onPressed: widget.disabled ? null : widget.onSave, - child: Text(l.saveSignedPdf), - ), - OutlinedButton( - key: const Key('btn_load_signature_picker'), - onPressed: - widget.disabled || !pdf.loaded - ? null - : widget.onLoadSignatureFromFile, - child: Text(l.loadSignatureFromFile), - ), - OutlinedButton( - key: const Key('btn_create_signature'), - onPressed: - widget.disabled || !pdf.loaded - ? null - : widget.onCreateSignature, - child: Text(l.createNewSignature), - ), - ElevatedButton( - key: const Key('btn_draw_signature'), - onPressed: - widget.disabled || !pdf.loaded - ? null - : widget.onOpenDrawCanvas, - child: Text(l.drawSignature), - ), ], ], ); + + return Row( + children: [ + IconButton( + key: const Key('btn_toggle_pages_sidebar'), + tooltip: 'Toggle pages overview', + onPressed: widget.disabled ? null : widget.onTogglePagesSidebar, + icon: Icon( + Icons.view_sidebar, + color: + widget.showPagesSidebar + ? Theme.of(context).colorScheme.primary + : null, + ), + ), + const SizedBox(width: 8), + Expanded(child: center), + const SizedBox(width: 8), + IconButton( + key: const Key('btn_toggle_signatures_sidebar'), + tooltip: 'Toggle signatures drawer', + onPressed: + widget.disabled ? null : widget.onToggleSignaturesSidebar, + icon: Icon( + Icons.view_sidebar, + color: + widget.showSignaturesSidebar + ? Theme.of(context).colorScheme.primary + : null, + ), + ), + ], + ); }, ); } diff --git a/lib/ui/features/pdf/widgets/signature_drawer.dart b/lib/ui/features/pdf/widgets/signature_drawer.dart new file mode 100644 index 0000000..d4fbab8 --- /dev/null +++ b/lib/ui/features/pdf/widgets/signature_drawer.dart @@ -0,0 +1,206 @@ +import 'dart:typed_data'; +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'; +import 'adjustments_panel.dart'; + +/// Data passed when dragging a signature card. +class SignatureDragData { + const SignatureDragData(); +} + +class SignatureDrawer extends ConsumerStatefulWidget { + const SignatureDrawer({ + super.key, + required this.disabled, + required this.onLoadSignatureFromFile, + required this.onOpenDrawCanvas, + }); + + final bool disabled; + final VoidCallback onLoadSignatureFromFile; + final VoidCallback onOpenDrawCanvas; + + @override + ConsumerState createState() => _SignatureDrawerState(); +} + +class _SignatureDrawerState extends ConsumerState { + @override + Widget build(BuildContext context) { + final l = AppLocalizations.of(context); + final sig = ref.watch(signatureProvider); + final processed = ref.watch(processedSignatureImageProvider); + final bytes = processed ?? sig.imageBytes; + final isExporting = ref.watch(exportingProvider); + final disabled = widget.disabled || isExporting; + + return Card( + margin: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Header + Padding( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), + child: Text( + l.signature, + style: Theme.of(context).textTheme.titleSmall, + ), + ), + // Existing signature card (draggable when bytes available) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).dividerColor), + borderRadius: BorderRadius.circular(8), + ), + child: SizedBox( + height: 120, + child: + bytes == null + ? Center( + child: Text( + l.noPdfLoaded, + textAlign: TextAlign.center, + ), + ) + : _DraggableSignaturePreview( + bytes: bytes, + disabled: disabled, + ), + ), + ), + ), + // Actions under the card + if (bytes != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + children: [ + PopupMenuButton( + key: const Key('popup_signature_card'), + tooltip: l.settings, + onSelected: (v) { + switch (v) { + case 'delete': + ref + .read(signatureProvider.notifier) + .clearActiveOverlay(); + ref.read(signatureProvider.notifier).clearImage(); + break; + default: + break; + } + }, + itemBuilder: + (ctx) => [ + PopupMenuItem( + key: const Key('mi_signature_delete'), + value: 'delete', + child: Text(l.delete), + ), + ], + child: IconButton( + icon: const Icon(Icons.more_horiz), + onPressed: disabled ? null : () {}, + ), + ), + const SizedBox(width: 4), + Text(AppLocalizations.of(context).createNewSignature), + ], + ), + ), + const SizedBox(height: 12), + const Divider(height: 1), + // New signature card + Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + l.createNewSignature, + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + OutlinedButton.icon( + key: const Key('btn_drawer_load_signature'), + onPressed: + disabled ? null : widget.onLoadSignatureFromFile, + icon: const Icon(Icons.image_outlined), + label: Text(l.loadSignatureFromFile), + ), + OutlinedButton.icon( + key: const Key('btn_drawer_draw_signature'), + onPressed: disabled ? null : widget.onOpenDrawCanvas, + icon: const Icon(Icons.gesture), + label: Text(l.drawSignature), + ), + ], + ), + ], + ), + ), + const Divider(height: 1), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(12), + child: AdjustmentsPanel(sig: sig), + ), + ), + ], + ), + ); + } +} + +class _DraggableSignaturePreview extends StatelessWidget { + const _DraggableSignaturePreview({ + required this.bytes, + required this.disabled, + }); + final Uint8List bytes; + final bool disabled; + + @override + Widget build(BuildContext context) { + final child = Padding( + padding: const EdgeInsets.all(8.0), + child: Image.memory(bytes, fit: BoxFit.contain), + ); + if (disabled) return child; + return Draggable( + data: const SignatureDragData(), + feedback: Opacity( + opacity: 0.8, + child: ConstrainedBox( + constraints: const BoxConstraints.tightFor(width: 160, height: 80), + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(6), + boxShadow: const [ + BoxShadow(blurRadius: 8, color: Colors.black26), + ], + ), + child: Padding( + padding: const EdgeInsets.all(6.0), + child: Image.memory(bytes, fit: BoxFit.contain), + ), + ), + ), + ), + childWhenDragging: Opacity(opacity: 0.5, child: child), + child: child, + ); + } +} diff --git a/lib/ui/features/welcome/widgets/welcome_screen.dart b/lib/ui/features/welcome/widgets/welcome_screen.dart index 7e46ca2..42f535f 100644 --- a/lib/ui/features/welcome/widgets/welcome_screen.dart +++ b/lib/ui/features/welcome/widgets/welcome_screen.dart @@ -8,7 +8,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; import '../../pdf/view_model/view_model.dart'; -import '../../preferences/widgets/settings_screen.dart'; +// Settings dialog is provided via global AppBar in MyApp // Abstraction to make drop handling testable without constructing // platform-specific DropItem types in widget tests. @@ -131,33 +131,19 @@ class _WelcomeScreenState extends ConsumerState { ), color: _dragging - ? Theme.of(context).colorScheme.primary.withOpacity(0.05) + ? Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.05) : Colors.transparent, ), child: content, ), ); - return Scaffold( - appBar: AppBar( - title: Text(l.appTitle), - actions: [ - IconButton( - tooltip: l.settings, - onPressed: - () => showDialog( - context: context, - builder: (_) => const SettingsDialog(), - ), - icon: const Icon(Icons.settings), - ), - ], - ), - body: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 560), - child: dropZone, - ), + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 560), + child: dropZone, ), ); }