feat: partially update UI view to new design
This commit is contained in:
parent
d3df15d695
commit
db0912b12f
|
@ -44,17 +44,20 @@ Route: root --> opened
|
||||||
|
|
||||||
Design notes:
|
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.
|
- Top: A small toolbar sits at the top edge with file name text, open pdf file button, previous/next page widgets and zoom controls.
|
||||||
|
- 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.
|
- 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.
|
- Zoom: Zoom out, Zoom level dropdown (percent), Zoom in, Fit width, Fit page, Reset zoom.
|
||||||
- Optional: Find/search within PDF (if supported by engine).
|
- 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.
|
- 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.
|
- Center: main PDF viewer shows the active page.
|
||||||
|
- wheel to scroll pages.
|
||||||
- Ctrl/Cmd + wheel to zoom.
|
- Ctrl/Cmd + wheel to zoom.
|
||||||
- Right pane: signatures drawer displaying saved signatures as cards.
|
- Right pane: signatures drawer displaying saved signatures as cards.
|
||||||
- able to drag and drop signature cards onto the PDF as placed signatures.
|
- able to drag and drop signature cards onto the PDF as placed signatures.
|
||||||
- Each signature card shows a preview.
|
- Each signature card shows a preview.
|
||||||
- long tap/right-click will show menu with options to delete, adjust graphic of image.
|
- 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".
|
- 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.
|
- "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.
|
- "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):
|
Signature controls (after placing on page):
|
||||||
- Select to show bounding box with resize handles and a small inline action bar.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|
||||||
|
|
23
lib/app.dart
23
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/pdf/view_model/view_model.dart';
|
||||||
import 'package:pdf_signature/ui/features/welcome/widgets/welcome_screen.dart';
|
import 'package:pdf_signature/ui/features/welcome/widgets/welcome_screen.dart';
|
||||||
import 'ui/features/preferences/providers.dart';
|
import 'ui/features/preferences/providers.dart';
|
||||||
|
import 'package:pdf_signature/ui/features/preferences/widgets/settings_screen.dart';
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
const MyApp({super.key});
|
const MyApp({super.key});
|
||||||
|
@ -62,7 +63,27 @@ class MyApp extends StatelessWidget {
|
||||||
...AppLocalizations.localizationsDelegates,
|
...AppLocalizations.localizationsDelegates,
|
||||||
LocaleNamesLocalizationsDelegate(),
|
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<bool>(
|
||||||
|
context: ctx,
|
||||||
|
builder: (_) => const SettingsDialog(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: const _RootHomeSwitcher(),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -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.",
|
"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",
|
"next": "Weiter",
|
||||||
"noPdfLoaded": "Keine PDF-Datei geladen",
|
"noPdfLoaded": "Keine PDF-Datei geladen",
|
||||||
|
"noSignatureLoaded": "Keine Signatur geladen",
|
||||||
"nothingToSaveYet": "Noch nichts zu speichern",
|
"nothingToSaveYet": "Noch nichts zu speichern",
|
||||||
"openPdf": "PDF öffnen...",
|
"openPdf": "PDF öffnen...",
|
||||||
"pageInfo": "Seite {current}/{total}",
|
"pageInfo": "Seite {current}/{total}",
|
||||||
|
|
|
@ -61,6 +61,8 @@
|
||||||
"@next": {},
|
"@next": {},
|
||||||
"noPdfLoaded": "No PDF loaded",
|
"noPdfLoaded": "No PDF loaded",
|
||||||
"@noPdfLoaded": {},
|
"@noPdfLoaded": {},
|
||||||
|
"noSignatureLoaded": "No signature loaded",
|
||||||
|
"@noSignatureLoaded": {},
|
||||||
"nothingToSaveYet": "Nothing to save yet",
|
"nothingToSaveYet": "Nothing to save yet",
|
||||||
"@nothingToSaveYet": {},
|
"@nothingToSaveYet": {},
|
||||||
"openPdf": "Open PDF...",
|
"openPdf": "Open PDF...",
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
"longPressOrRightClickTheSignatureToConfirmOrDelete": "Pulse prolongadamente o haga clic derecho en la firma para confirmar o eliminar.",
|
"longPressOrRightClickTheSignatureToConfirmOrDelete": "Pulse prolongadamente o haga clic derecho en la firma para confirmar o eliminar.",
|
||||||
"next": "Siguiente",
|
"next": "Siguiente",
|
||||||
"noPdfLoaded": "No se ha cargado ningún PDF",
|
"noPdfLoaded": "No se ha cargado ningún PDF",
|
||||||
|
"noSignatureLoaded": "No se ha cargado ninguna firma",
|
||||||
"nothingToSaveYet": "Aún no hay nada que guardar",
|
"nothingToSaveYet": "Aún no hay nada que guardar",
|
||||||
"openPdf": "Abrir PDF...",
|
"openPdf": "Abrir PDF...",
|
||||||
"pageInfo": "Página {current}/{total}",
|
"pageInfo": "Página {current}/{total}",
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
"longPressOrRightClickTheSignatureToConfirmOrDelete": "Appuyez longuement ou cliquez droit sur la signature pour la confirmer ou la supprimer.",
|
"longPressOrRightClickTheSignatureToConfirmOrDelete": "Appuyez longuement ou cliquez droit sur la signature pour la confirmer ou la supprimer.",
|
||||||
"next": "Suivant",
|
"next": "Suivant",
|
||||||
"noPdfLoaded": "Aucun PDF chargé",
|
"noPdfLoaded": "Aucun PDF chargé",
|
||||||
|
"noSignatureLoaded": "Aucune signature chargée",
|
||||||
"nothingToSaveYet": "Rien à enregistrer pour le moment",
|
"nothingToSaveYet": "Rien à enregistrer pour le moment",
|
||||||
"openPdf": "Ouvrir un PDF...",
|
"openPdf": "Ouvrir un PDF...",
|
||||||
"pageInfo": "Page {current}/{total}",
|
"pageInfo": "Page {current}/{total}",
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
"longPressOrRightClickTheSignatureToConfirmOrDelete": "署名を長押しまたは右クリックして、確認または削除します。",
|
"longPressOrRightClickTheSignatureToConfirmOrDelete": "署名を長押しまたは右クリックして、確認または削除します。",
|
||||||
"next": "次へ",
|
"next": "次へ",
|
||||||
"noPdfLoaded": "PDFが読み込まれていません",
|
"noPdfLoaded": "PDFが読み込まれていません",
|
||||||
|
"noSignatureLoaded": "署名は読み込まれていません",
|
||||||
"nothingToSaveYet": "まだ保存するものがありません",
|
"nothingToSaveYet": "まだ保存するものがありません",
|
||||||
"openPdf": "PDFを開く…",
|
"openPdf": "PDFを開く…",
|
||||||
"pageInfo": "ページ {current}/{total}",
|
"pageInfo": "ページ {current}/{total}",
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
"longPressOrRightClickTheSignatureToConfirmOrDelete": "서명을 길게 누르거나 마우스 오른쪽 버튼을 클릭하여 확인하거나 삭제합니다.",
|
"longPressOrRightClickTheSignatureToConfirmOrDelete": "서명을 길게 누르거나 마우스 오른쪽 버튼을 클릭하여 확인하거나 삭제합니다.",
|
||||||
"next": "다음",
|
"next": "다음",
|
||||||
"noPdfLoaded": "로드된 PDF 없음",
|
"noPdfLoaded": "로드된 PDF 없음",
|
||||||
|
"noSignatureLoaded": "서명이 로드되지 않았습니다",
|
||||||
"nothingToSaveYet": "아직 저장할 내용이 없습니다.",
|
"nothingToSaveYet": "아직 저장할 내용이 없습니다.",
|
||||||
"openPdf": "PDF 열기...",
|
"openPdf": "PDF 열기...",
|
||||||
"pageInfo": "{current}/{total} 페이지",
|
"pageInfo": "{current}/{total} 페이지",
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
"longPressOrRightClickTheSignatureToConfirmOrDelete": "Довго натисніть або клацніть правою кнопкою миші на підпис, щоб підтвердити або видалити.",
|
"longPressOrRightClickTheSignatureToConfirmOrDelete": "Довго натисніть або клацніть правою кнопкою миші на підпис, щоб підтвердити або видалити.",
|
||||||
"next": "Далі",
|
"next": "Далі",
|
||||||
"noPdfLoaded": "PDF не завантажено",
|
"noPdfLoaded": "PDF не завантажено",
|
||||||
|
"noSignatureLoaded": "Не завантажено жодного підпису",
|
||||||
"nothingToSaveYet": "Ще нічого не потрібно зберігати",
|
"nothingToSaveYet": "Ще нічого не потрібно зберігати",
|
||||||
"openPdf": "Відкрити PDF...",
|
"openPdf": "Відкрити PDF...",
|
||||||
"pageInfo": "Сторінка {current}/{total}",
|
"pageInfo": "Сторінка {current}/{total}",
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
"longPressOrRightClickTheSignatureToConfirmOrDelete": "長按或右鍵點擊簽名以確認或刪除。",
|
"longPressOrRightClickTheSignatureToConfirmOrDelete": "長按或右鍵點擊簽名以確認或刪除。",
|
||||||
"next": "下一頁",
|
"next": "下一頁",
|
||||||
"noPdfLoaded": "尚未載入 PDF",
|
"noPdfLoaded": "尚未載入 PDF",
|
||||||
|
"noSignatureLoaded": "没有加载签名",
|
||||||
"nothingToSaveYet": "尚無可儲存的內容",
|
"nothingToSaveYet": "尚無可儲存的內容",
|
||||||
"openPdf": "開啟 PDF…",
|
"openPdf": "開啟 PDF…",
|
||||||
"pageInfo": "第 {current}/{total} 頁",
|
"pageInfo": "第 {current}/{total} 頁",
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
"longPressOrRightClickTheSignatureToConfirmOrDelete": "长按或右键单击签名以确认或删除。",
|
"longPressOrRightClickTheSignatureToConfirmOrDelete": "长按或右键单击签名以确认或删除。",
|
||||||
"next": "下一步",
|
"next": "下一步",
|
||||||
"noPdfLoaded": "未加载 PDF",
|
"noPdfLoaded": "未加载 PDF",
|
||||||
|
"noSignatureLoaded": "未加载签名",
|
||||||
"nothingToSaveYet": "尚无内容保存",
|
"nothingToSaveYet": "尚无内容保存",
|
||||||
"openPdf": "打开 PDF...",
|
"openPdf": "打开 PDF...",
|
||||||
"pageInfo": "第 {current} 页 / 共 {total} 页",
|
"pageInfo": "第 {current} 页 / 共 {total} 页",
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
"longPressOrRightClickTheSignatureToConfirmOrDelete": "長按或右鍵點擊簽名以確認或刪除。",
|
"longPressOrRightClickTheSignatureToConfirmOrDelete": "長按或右鍵點擊簽名以確認或刪除。",
|
||||||
"next": "下一頁",
|
"next": "下一頁",
|
||||||
"noPdfLoaded": "尚未載入 PDF",
|
"noPdfLoaded": "尚未載入 PDF",
|
||||||
|
"noSignatureLoaded": "未載入任何簽名",
|
||||||
"nothingToSaveYet": "尚無可儲存的內容",
|
"nothingToSaveYet": "尚無可儲存的內容",
|
||||||
"openPdf": "開啟 PDF…",
|
"openPdf": "開啟 PDF…",
|
||||||
"pageInfo": "第 {current}/{total} 頁",
|
"pageInfo": "第 {current}/{total} 頁",
|
||||||
|
|
|
@ -251,6 +251,16 @@ class SignatureController extends StateNotifier<SignatureState> {
|
||||||
state = state.copyWith(editingEnabled: true);
|
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.
|
// 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.
|
// Returns the Rect placed, or null if no rect to confirm.
|
||||||
Rect? confirmCurrentSignature(WidgetRef ref) {
|
Rect? confirmCurrentSignature(WidgetRef ref) {
|
||||||
|
|
|
@ -9,21 +9,22 @@ import '../../../../data/services/providers.dart';
|
||||||
import '../../../../data/model/model.dart';
|
import '../../../../data/model/model.dart';
|
||||||
import '../view_model/view_model.dart';
|
import '../view_model/view_model.dart';
|
||||||
import '../../preferences/providers.dart';
|
import '../../preferences/providers.dart';
|
||||||
|
import 'signature_drawer.dart';
|
||||||
|
|
||||||
class PdfPageArea extends ConsumerStatefulWidget {
|
class PdfPageArea extends ConsumerStatefulWidget {
|
||||||
const PdfPageArea({
|
const PdfPageArea({
|
||||||
super.key,
|
super.key,
|
||||||
required this.pageSize,
|
required this.pageSize,
|
||||||
this.controller,
|
|
||||||
required this.onDragSignature,
|
required this.onDragSignature,
|
||||||
required this.onResizeSignature,
|
required this.onResizeSignature,
|
||||||
required this.onConfirmSignature,
|
required this.onConfirmSignature,
|
||||||
required this.onClearActiveOverlay,
|
required this.onClearActiveOverlay,
|
||||||
required this.onSelectPlaced,
|
required this.onSelectPlaced,
|
||||||
|
this.viewerController,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Size pageSize;
|
final Size pageSize;
|
||||||
final TransformationController? controller;
|
final PdfViewerController? viewerController;
|
||||||
final ValueChanged<Offset> onDragSignature;
|
final ValueChanged<Offset> onDragSignature;
|
||||||
final ValueChanged<Offset> onResizeSignature;
|
final ValueChanged<Offset> onResizeSignature;
|
||||||
final VoidCallback onConfirmSignature;
|
final VoidCallback onConfirmSignature;
|
||||||
|
@ -35,7 +36,8 @@ class PdfPageArea extends ConsumerStatefulWidget {
|
||||||
|
|
||||||
class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
|
class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
|
||||||
final Map<int, GlobalKey> _pageKeys = {};
|
final Map<int, GlobalKey> _pageKeys = {};
|
||||||
final PdfViewerController _viewerController = PdfViewerController();
|
late final PdfViewerController _viewerController =
|
||||||
|
widget.viewerController ?? PdfViewerController();
|
||||||
// Guards to avoid scroll feedback between provider and viewer
|
// Guards to avoid scroll feedback between provider and viewer
|
||||||
int? _programmaticTargetPage;
|
int? _programmaticTargetPage;
|
||||||
bool _suppressProviderListen = false;
|
bool _suppressProviderListen = false;
|
||||||
|
@ -58,6 +60,8 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No dispose required for PdfViewerController (managed by owner if any)
|
||||||
|
|
||||||
GlobalKey _pageKey(int page) => _pageKeys.putIfAbsent(
|
GlobalKey _pageKey(int page) => _pageKeys.putIfAbsent(
|
||||||
page,
|
page,
|
||||||
() => GlobalKey(debugLabel: 'cont_page_$page'),
|
() => GlobalKey(debugLabel: 'cont_page_$page'),
|
||||||
|
@ -216,7 +220,7 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return SingleChildScrollView(
|
final content = SingleChildScrollView(
|
||||||
key: const Key('pdf_continuous_mock_list'),
|
key: const Key('pdf_continuous_mock_list'),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@ -270,6 +274,7 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
return content;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -277,14 +282,14 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
|
||||||
// Real continuous mode (pdfrx): copy example patterns
|
// Real continuous mode (pdfrx): copy example patterns
|
||||||
// https://github.com/espresso3389/pdfrx/blob/2cc32c1e2aa2a054602d20a5e7cf60bcc2d6a889/packages/pdfrx/example/viewer/lib/main.dart
|
// https://github.com/espresso3389/pdfrx/blob/2cc32c1e2aa2a054602d20a5e7cf60bcc2d6a889/packages/pdfrx/example/viewer/lib/main.dart
|
||||||
if (pdf.pickedPdfPath != null && isContinuous) {
|
if (pdf.pickedPdfPath != null && isContinuous) {
|
||||||
return PdfViewer.file(
|
final viewer = PdfViewer.file(
|
||||||
pdf.pickedPdfPath!,
|
pdf.pickedPdfPath!,
|
||||||
controller: _viewerController,
|
controller: _viewerController,
|
||||||
params: PdfViewerParams(
|
params: PdfViewerParams(
|
||||||
pageAnchor: PdfPageAnchor.top,
|
pageAnchor: PdfPageAnchor.top,
|
||||||
keyHandlerParams: PdfViewerKeyHandlerParams(autofocus: true),
|
keyHandlerParams: PdfViewerKeyHandlerParams(autofocus: true),
|
||||||
maxScale: 8,
|
maxScale: 8,
|
||||||
// scrollByMouseWheel: 0.6,
|
scrollByMouseWheel: 0.6,
|
||||||
// Add overlay scroll thumbs (vertical on right, horizontal on bottom)
|
// Add overlay scroll thumbs (vertical on right, horizontal on bottom)
|
||||||
viewerOverlayBuilder:
|
viewerOverlayBuilder:
|
||||||
(context, size, handleLinkTap) => [
|
(context, size, handleLinkTap) => [
|
||||||
|
@ -294,7 +299,7 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
|
||||||
thumbSize: const Size(40, 24),
|
thumbSize: const Size(40, 24),
|
||||||
thumbBuilder:
|
thumbBuilder:
|
||||||
(context, thumbSize, pageNumber, controller) => Container(
|
(context, thumbSize, pageNumber, controller) => Container(
|
||||||
color: Colors.black.withOpacity(0.7),
|
color: Colors.black.withValues(alpha: 0.7),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
pageNumber.toString(),
|
pageNumber.toString(),
|
||||||
|
@ -309,7 +314,7 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
|
||||||
thumbSize: const Size(40, 24),
|
thumbSize: const Size(40, 24),
|
||||||
thumbBuilder:
|
thumbBuilder:
|
||||||
(context, thumbSize, pageNumber, controller) => Container(
|
(context, thumbSize, pageNumber, controller) => Container(
|
||||||
color: Colors.black.withOpacity(0.7),
|
color: Colors.black.withValues(alpha: 0.7),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
pageNumber.toString(),
|
pageNumber.toString(),
|
||||||
|
@ -375,6 +380,34 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
// Accept drops of signature card over the viewer
|
||||||
|
final drop = DragTarget<Object>(
|
||||||
|
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();
|
return const SizedBox.shrink();
|
||||||
|
|
|
@ -5,15 +5,16 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:pdf_signature/l10n/app_localizations.dart';
|
import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||||
import 'package:printing/printing.dart' as printing;
|
import 'package:printing/printing.dart' as printing;
|
||||||
|
import 'package:pdfrx/pdfrx.dart';
|
||||||
|
|
||||||
import '../../../../data/services/providers.dart';
|
import '../../../../data/services/providers.dart';
|
||||||
import '../view_model/view_model.dart';
|
import '../view_model/view_model.dart';
|
||||||
import 'draw_canvas.dart';
|
import 'draw_canvas.dart';
|
||||||
import 'pdf_toolbar.dart';
|
import 'pdf_toolbar.dart';
|
||||||
import 'pdf_page_area.dart';
|
import 'pdf_page_area.dart';
|
||||||
import 'adjustments_panel.dart';
|
|
||||||
import 'pdf_pages_overview.dart';
|
import 'pdf_pages_overview.dart';
|
||||||
import '../../preferences/widgets/settings_screen.dart';
|
import 'signature_drawer.dart';
|
||||||
|
import 'adjustments_panel.dart';
|
||||||
|
|
||||||
class PdfSignatureHomePage extends ConsumerStatefulWidget {
|
class PdfSignatureHomePage extends ConsumerStatefulWidget {
|
||||||
const PdfSignatureHomePage({super.key});
|
const PdfSignatureHomePage({super.key});
|
||||||
|
@ -25,7 +26,9 @@ class PdfSignatureHomePage extends ConsumerStatefulWidget {
|
||||||
|
|
||||||
class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
static const Size _pageSize = SignatureController.pageSize;
|
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.
|
// Exposed for tests to trigger the invalid-file SnackBar without UI.
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
|
@ -52,6 +55,8 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
ref.read(pdfProvider.notifier).jumpTo(page);
|
ref.read(pdfProvider.notifier).jumpTo(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zoom is managed by pdfrx viewer (Ctrl +/- etc.). No custom zoom here.
|
||||||
|
|
||||||
Future<void> _loadSignatureFromFile() async {
|
Future<void> _loadSignatureFromFile() async {
|
||||||
final typeGroup = const fs.XTypeGroup(
|
final typeGroup = const fs.XTypeGroup(
|
||||||
label: 'Image',
|
label: 'Image',
|
||||||
|
@ -68,25 +73,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _createNewSignature() {
|
// _createNewSignature was removed as the toolbar no longer exposes this action.
|
||||||
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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _confirmSignature() {
|
void _confirmSignature() {
|
||||||
ref.read(signatureProvider.notifier).confirmCurrentSignature(ref);
|
ref.read(signatureProvider.notifier).confirmCurrentSignature(ref);
|
||||||
|
@ -250,7 +237,6 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_ivController.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,29 +245,45 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
final isExporting = ref.watch(exportingProvider);
|
final isExporting = ref.watch(exportingProvider);
|
||||||
final l = AppLocalizations.of(context);
|
final l = AppLocalizations.of(context);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(l.appTitle),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
key: const Key('btn_appbar_settings'),
|
|
||||||
tooltip: l.settings,
|
|
||||||
onPressed:
|
|
||||||
() => showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (_) => const SettingsDialog(),
|
|
||||||
),
|
|
||||||
icon: const Icon(Icons.settings),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Column(
|
||||||
|
children: [
|
||||||
|
// 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(height: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
// Left: pages overview (thumbnails + navigation)
|
if (_showPagesSidebar)
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: const BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
|
@ -292,31 +294,13 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
child: const PdfPagesOverview(),
|
child: const PdfPagesOverview(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
if (_showPagesSidebar) const SizedBox(width: 12),
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
PdfToolbar(
|
|
||||||
disabled: isExporting,
|
|
||||||
onOpenSettings:
|
|
||||||
() => showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (_) => const SettingsDialog(),
|
|
||||||
),
|
|
||||||
onPickPdf: _pickPdf,
|
|
||||||
onJumpToPage: _jumpToPage,
|
|
||||||
onSave: _saveSignedPdf,
|
|
||||||
onLoadSignatureFromFile: _loadSignatureFromFile,
|
|
||||||
onCreateSignature: _createNewSignature,
|
|
||||||
onOpenDrawCanvas: _openDrawCanvas,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: AbsorbPointer(
|
child: AbsorbPointer(
|
||||||
absorbing: isExporting,
|
absorbing: isExporting,
|
||||||
child: PdfPageArea(
|
child: PdfPageArea(
|
||||||
pageSize: _pageSize,
|
pageSize: _pageSize,
|
||||||
controller: _ivController,
|
viewerController: _viewerController,
|
||||||
onDragSignature: _onDragSignature,
|
onDragSignature: _onDragSignature,
|
||||||
onResizeSignature: _onResizeSignature,
|
onResizeSignature: _onResizeSignature,
|
||||||
onConfirmSignature: _confirmSignature,
|
onConfirmSignature: _confirmSignature,
|
||||||
|
@ -329,10 +313,8 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
if (_showSignaturesSidebar) const SizedBox(width: 12),
|
||||||
),
|
if (_showSignaturesSidebar)
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: const BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
minWidth: 280,
|
minWidth: 280,
|
||||||
|
@ -341,62 +323,139 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
child: Consumer(
|
child: Consumer(
|
||||||
builder: (context, ref, _) {
|
builder: (context, ref, _) {
|
||||||
final sig = ref.watch(signatureProvider);
|
final sig = ref.watch(signatureProvider);
|
||||||
if (sig.rect != null) {
|
final bytes =
|
||||||
|
ref.watch(processedSignatureImageProvider) ??
|
||||||
|
sig.imageBytes;
|
||||||
return AbsorbPointer(
|
return AbsorbPointer(
|
||||||
absorbing: isExporting,
|
absorbing: isExporting,
|
||||||
child: Card(
|
child: Card(
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, cons) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: cons.maxHeight,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
// Signature preview
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(
|
||||||
|
12,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment:
|
crossAxisAlignment:
|
||||||
CrossAxisAlignment.start,
|
CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).signature,
|
AppLocalizations.of(
|
||||||
style:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
context,
|
||||||
).textTheme.titleSmall,
|
).signature,
|
||||||
|
style:
|
||||||
|
Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleSmall,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
DecoratedBox(
|
DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color:
|
color:
|
||||||
Theme.of(context).dividerColor,
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).dividerColor,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius:
|
||||||
|
BorderRadius.circular(
|
||||||
8,
|
8,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 3 / 1,
|
aspectRatio: 3 / 1,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding:
|
||||||
child: Consumer(
|
const EdgeInsets.all(
|
||||||
builder: (context, ref, _) {
|
8.0,
|
||||||
final bytes =
|
),
|
||||||
ref.watch(
|
child: Builder(
|
||||||
processedSignatureImageProvider,
|
builder: (context) {
|
||||||
) ??
|
final placeholder = Center(
|
||||||
sig.imageBytes;
|
|
||||||
if (bytes == null) {
|
|
||||||
return Center(
|
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(
|
AppLocalizations.of(
|
||||||
context,
|
context,
|
||||||
).noPdfLoaded,
|
).noSignatureLoaded,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
if (bytes ==
|
||||||
|
null ||
|
||||||
|
bytes
|
||||||
|
.isEmpty) {
|
||||||
|
return placeholder;
|
||||||
}
|
}
|
||||||
return Image.memory(
|
final img =
|
||||||
|
Image.memory(
|
||||||
bytes,
|
bytes,
|
||||||
fit: BoxFit.contain,
|
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,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -406,27 +465,84 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
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),
|
const Divider(height: 1),
|
||||||
Expanded(
|
Padding(
|
||||||
child: SingleChildScrollView(
|
padding: const EdgeInsets.all(
|
||||||
padding: const EdgeInsets.all(12),
|
12,
|
||||||
child: AdjustmentsPanel(sig: sig),
|
|
||||||
),
|
),
|
||||||
|
child: AdjustmentsPanel(
|
||||||
|
sig: sig,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
ElevatedButton(
|
||||||
|
key: const Key('btn_save_pdf'),
|
||||||
|
onPressed:
|
||||||
|
isExporting
|
||||||
|
? null
|
||||||
|
: _saveSignedPdf,
|
||||||
|
child: Text(l.saveSignedPdf),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -435,6 +551,9 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
if (isExporting)
|
if (isExporting)
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Container(
|
child: Container(
|
||||||
|
|
|
@ -10,23 +10,27 @@ class PdfToolbar extends ConsumerStatefulWidget {
|
||||||
const PdfToolbar({
|
const PdfToolbar({
|
||||||
super.key,
|
super.key,
|
||||||
required this.disabled,
|
required this.disabled,
|
||||||
required this.onOpenSettings,
|
|
||||||
required this.onPickPdf,
|
required this.onPickPdf,
|
||||||
required this.onJumpToPage,
|
required this.onJumpToPage,
|
||||||
required this.onSave,
|
required this.onZoomOut,
|
||||||
required this.onLoadSignatureFromFile,
|
required this.onZoomIn,
|
||||||
required this.onCreateSignature,
|
this.fileName,
|
||||||
required this.onOpenDrawCanvas,
|
required this.showPagesSidebar,
|
||||||
|
required this.showSignaturesSidebar,
|
||||||
|
required this.onTogglePagesSidebar,
|
||||||
|
required this.onToggleSignaturesSidebar,
|
||||||
});
|
});
|
||||||
|
|
||||||
final bool disabled;
|
final bool disabled;
|
||||||
final VoidCallback onOpenSettings;
|
|
||||||
final VoidCallback onPickPdf;
|
final VoidCallback onPickPdf;
|
||||||
final ValueChanged<int> onJumpToPage;
|
final ValueChanged<int> onJumpToPage;
|
||||||
final VoidCallback onSave;
|
final String? fileName;
|
||||||
final VoidCallback onLoadSignatureFromFile;
|
final VoidCallback onZoomOut;
|
||||||
final VoidCallback onCreateSignature;
|
final VoidCallback onZoomIn;
|
||||||
final VoidCallback onOpenDrawCanvas;
|
final bool showPagesSidebar;
|
||||||
|
final bool showSignaturesSidebar;
|
||||||
|
final VoidCallback onTogglePagesSidebar;
|
||||||
|
final VoidCallback onToggleSignaturesSidebar;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<PdfToolbar> createState() => _PdfToolbarState();
|
ConsumerState<PdfToolbar> createState() => _PdfToolbarState();
|
||||||
|
@ -57,21 +61,34 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final bool compact = constraints.maxWidth < 260;
|
final bool compact = constraints.maxWidth < 260;
|
||||||
final double gotoWidth = compact ? 60 : 100;
|
final double gotoWidth = 50;
|
||||||
return Wrap(
|
|
||||||
|
// Center content of the toolbar
|
||||||
|
final center = Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
OutlinedButton(
|
|
||||||
key: const Key('btn_open_settings'),
|
|
||||||
onPressed: widget.disabled ? null : widget.onOpenSettings,
|
|
||||||
child: Text(l.settings),
|
|
||||||
),
|
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
key: const Key('btn_open_pdf_picker'),
|
key: const Key('btn_open_pdf_picker'),
|
||||||
onPressed: widget.disabled ? null : widget.onPickPdf,
|
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) ...[
|
if (pdf.loaded) ...[
|
||||||
Row(
|
Row(
|
||||||
|
@ -86,6 +103,7 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
icon: const Icon(Icons.chevron_left),
|
icon: const Icon(Icons.chevron_left),
|
||||||
tooltip: l.prev,
|
tooltip: l.prev,
|
||||||
),
|
),
|
||||||
|
// Current page label
|
||||||
Text(pageInfo, key: const Key('lbl_page_info')),
|
Text(pageInfo, key: const Key('lbl_page_info')),
|
||||||
IconButton(
|
IconButton(
|
||||||
key: const Key('btn_next'),
|
key: const Key('btn_next'),
|
||||||
|
@ -96,8 +114,6 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
icon: const Icon(Icons.chevron_right),
|
icon: const Icon(Icons.chevron_right),
|
||||||
tooltip: l.next,
|
tooltip: l.next,
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 6,
|
spacing: 6,
|
||||||
runSpacing: 4,
|
runSpacing: 4,
|
||||||
|
@ -110,7 +126,9 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
key: const Key('txt_goto'),
|
key: const Key('txt_goto'),
|
||||||
controller: _goToController,
|
controller: _goToController,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter.digitsOnly,
|
||||||
|
],
|
||||||
enabled: !widget.disabled,
|
enabled: !widget.disabled,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
isDense: true,
|
isDense: true,
|
||||||
|
@ -128,6 +146,21 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
@ -156,38 +189,42 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<SignatureDrawer> createState() => _SignatureDrawerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
|
||||||
|
@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<String>(
|
||||||
|
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<SignatureDragData>(
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:pdf_signature/l10n/app_localizations.dart';
|
import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||||
|
|
||||||
import '../../pdf/view_model/view_model.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
|
// Abstraction to make drop handling testable without constructing
|
||||||
// platform-specific DropItem types in widget tests.
|
// platform-specific DropItem types in widget tests.
|
||||||
|
@ -131,34 +131,20 @@ class _WelcomeScreenState extends ConsumerState<WelcomeScreen> {
|
||||||
),
|
),
|
||||||
color:
|
color:
|
||||||
_dragging
|
_dragging
|
||||||
? Theme.of(context).colorScheme.primary.withOpacity(0.05)
|
? Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primary.withValues(alpha: 0.05)
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
),
|
),
|
||||||
child: content,
|
child: content,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Center(
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(l.appTitle),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
tooltip: l.settings,
|
|
||||||
onPressed:
|
|
||||||
() => showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (_) => const SettingsDialog(),
|
|
||||||
),
|
|
||||||
icon: const Icon(Icons.settings),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: Center(
|
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 560),
|
constraints: const BoxConstraints(maxWidth: 560),
|
||||||
child: dropZone,
|
child: dropZone,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue