refactor: unify signature placement handling with new model structure
This commit is contained in:
parent
f74b724712
commit
4f149656bd
|
@ -1,12 +1,12 @@
|
||||||
# AGENTS
|
# AGENTS
|
||||||
|
|
||||||
Always read `README.md` and `docs/meta-arch.md` when new chat created.
|
Always read [`README.md`](README.md) and [`meta-arch.md`](docs/meta-arch.md) when new chat created.
|
||||||
|
|
||||||
Additionally read relevant files depends on task.
|
Additionally read relevant files depends on task.
|
||||||
|
|
||||||
* If want to modify use cases (files at `test/features/*.feature`)
|
* If want to modify use cases (files at `test/features/*.feature`)
|
||||||
* read `docs/FRs.md`
|
* read [`FRs.md`](docs/FRs.md)
|
||||||
* If want to modify code (implement or test) of `View` of MVVM (UI widget) (files at `lib/ui/features/*/widgets/*`)
|
* If want to modify code (implement or test) of `View` of MVVM (UI widget) (files at `lib/ui/features/*/widgets/*`)
|
||||||
* read `docs/wireframe.md`, `docs/NFRs.md`, `test/features/*.feature`
|
* read [`wireframe.md`](docs/wireframe.md), [`NFRs.md`](docs/NFRs.md), `test/features/*.feature`
|
||||||
* If want to modify code (implement or test) of non-View e.g. `Model`, `View Model`, services...
|
* If want to modify code (implement or test) of non-View e.g. `Model`, `View Model`, services...
|
||||||
* read `test/features/*.feature`, `docs/NFRs.md`
|
* read `test/features/*.feature`, [`NFRs.md`](docs/NFRs.md)
|
||||||
|
|
|
@ -126,7 +126,7 @@ void main() {
|
||||||
final pdf = container.read(pdfProvider);
|
final pdf = container.read(pdfProvider);
|
||||||
container
|
container
|
||||||
.read(pdfProvider.notifier)
|
.read(pdfProvider.notifier)
|
||||||
.addPlacement(page: pdf.currentPage, rect: r, image: imageId);
|
.addPlacement(page: pdf.currentPage, rect: r, imageId: imageId);
|
||||||
container.read(signatureProvider.notifier).clearActiveOverlay();
|
container.read(signatureProvider.notifier).clearActiveOverlay();
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,35 @@
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
/// Represents a single signature placement on a page combining both the
|
||||||
|
/// geometric rectangle (UI coordinate space) and the identifier of the
|
||||||
|
/// image/signature asset assigned to that placement.
|
||||||
|
class SignaturePlacement {
|
||||||
|
final Rect rect;
|
||||||
|
|
||||||
|
/// Rotation in degrees to apply when rendering/exporting this placement.
|
||||||
|
final double rotationDeg;
|
||||||
|
|
||||||
|
/// Identifier of the image (e.g., filename / asset id) assigned to this placement.
|
||||||
|
/// Nullable to allow a placement reserved before an image is chosen.
|
||||||
|
final String? imageId;
|
||||||
|
const SignaturePlacement({
|
||||||
|
required this.rect,
|
||||||
|
this.imageId,
|
||||||
|
this.rotationDeg = 0.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
SignaturePlacement copyWith({
|
||||||
|
Rect? rect,
|
||||||
|
String? imageId,
|
||||||
|
double? rotationDeg,
|
||||||
|
}) => SignaturePlacement(
|
||||||
|
rect: rect ?? this.rect,
|
||||||
|
imageId: imageId ?? this.imageId,
|
||||||
|
rotationDeg: rotationDeg ?? this.rotationDeg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class PdfState {
|
class PdfState {
|
||||||
final bool loaded;
|
final bool loaded;
|
||||||
final int pageCount;
|
final int pageCount;
|
||||||
|
@ -8,10 +37,8 @@ class PdfState {
|
||||||
final String? pickedPdfPath;
|
final String? pickedPdfPath;
|
||||||
final Uint8List? pickedPdfBytes;
|
final Uint8List? pickedPdfBytes;
|
||||||
final int? signedPage;
|
final int? signedPage;
|
||||||
// Multiple signature placements per page, stored as UI-space rects (e.g., 400x560)
|
// Multiple signature placements per page, each combines geometry and optional image id.
|
||||||
final Map<int, List<Rect>> placementsByPage;
|
final Map<int, List<SignaturePlacement>> placementsByPage;
|
||||||
// For each placement, store the assigned image identifier (e.g., filename) in the same index order.
|
|
||||||
final Map<int, List<String>> placementImageByPage;
|
|
||||||
// UI state: selected placement index on the current page (if any)
|
// UI state: selected placement index on the current page (if any)
|
||||||
final int? selectedPlacementIndex;
|
final int? selectedPlacementIndex;
|
||||||
const PdfState({
|
const PdfState({
|
||||||
|
@ -22,7 +49,6 @@ class PdfState {
|
||||||
this.pickedPdfBytes,
|
this.pickedPdfBytes,
|
||||||
this.signedPage,
|
this.signedPage,
|
||||||
this.placementsByPage = const {},
|
this.placementsByPage = const {},
|
||||||
this.placementImageByPage = const {},
|
|
||||||
this.selectedPlacementIndex,
|
this.selectedPlacementIndex,
|
||||||
});
|
});
|
||||||
factory PdfState.initial() => const PdfState(
|
factory PdfState.initial() => const PdfState(
|
||||||
|
@ -32,7 +58,6 @@ class PdfState {
|
||||||
pickedPdfBytes: null,
|
pickedPdfBytes: null,
|
||||||
signedPage: null,
|
signedPage: null,
|
||||||
placementsByPage: {},
|
placementsByPage: {},
|
||||||
placementImageByPage: {},
|
|
||||||
selectedPlacementIndex: null,
|
selectedPlacementIndex: null,
|
||||||
);
|
);
|
||||||
PdfState copyWith({
|
PdfState copyWith({
|
||||||
|
@ -42,8 +67,7 @@ class PdfState {
|
||||||
String? pickedPdfPath,
|
String? pickedPdfPath,
|
||||||
Uint8List? pickedPdfBytes,
|
Uint8List? pickedPdfBytes,
|
||||||
int? signedPage,
|
int? signedPage,
|
||||||
Map<int, List<Rect>>? placementsByPage,
|
Map<int, List<SignaturePlacement>>? placementsByPage,
|
||||||
Map<int, List<String>>? placementImageByPage,
|
|
||||||
int? selectedPlacementIndex,
|
int? selectedPlacementIndex,
|
||||||
}) => PdfState(
|
}) => PdfState(
|
||||||
loaded: loaded ?? this.loaded,
|
loaded: loaded ?? this.loaded,
|
||||||
|
@ -53,7 +77,6 @@ class PdfState {
|
||||||
pickedPdfBytes: pickedPdfBytes ?? this.pickedPdfBytes,
|
pickedPdfBytes: pickedPdfBytes ?? this.pickedPdfBytes,
|
||||||
signedPage: signedPage ?? this.signedPage,
|
signedPage: signedPage ?? this.signedPage,
|
||||||
placementsByPage: placementsByPage ?? this.placementsByPage,
|
placementsByPage: placementsByPage ?? this.placementsByPage,
|
||||||
placementImageByPage: placementImageByPage ?? this.placementImageByPage,
|
|
||||||
selectedPlacementIndex:
|
selectedPlacementIndex:
|
||||||
selectedPlacementIndex ?? this.selectedPlacementIndex,
|
selectedPlacementIndex ?? this.selectedPlacementIndex,
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,6 +6,7 @@ import 'package:pdf/widgets.dart' as pw;
|
||||||
import 'package:pdf/pdf.dart' as pdf;
|
import 'package:pdf/pdf.dart' as pdf;
|
||||||
import 'package:printing/printing.dart' as printing;
|
import 'package:printing/printing.dart' as printing;
|
||||||
import 'package:image/image.dart' as img;
|
import 'package:image/image.dart' as img;
|
||||||
|
import '../model/model.dart';
|
||||||
|
|
||||||
// NOTE:
|
// NOTE:
|
||||||
// - This exporter uses a raster snapshot of the UI (RepaintBoundary) and embeds it into a new PDF.
|
// - This exporter uses a raster snapshot of the UI (RepaintBoundary) and embeds it into a new PDF.
|
||||||
|
@ -32,8 +33,7 @@ class ExportService {
|
||||||
required Rect? signatureRectUi,
|
required Rect? signatureRectUi,
|
||||||
required Size uiPageSize,
|
required Size uiPageSize,
|
||||||
required Uint8List? signatureImageBytes,
|
required Uint8List? signatureImageBytes,
|
||||||
Map<int, List<Rect>>? placementsByPage,
|
Map<int, List<SignaturePlacement>>? placementsByPage,
|
||||||
Map<int, List<String>>? placementImageByPage,
|
|
||||||
Map<String, Uint8List>? libraryBytes,
|
Map<String, Uint8List>? libraryBytes,
|
||||||
double targetDpi = 144.0,
|
double targetDpi = 144.0,
|
||||||
}) async {
|
}) async {
|
||||||
|
@ -55,7 +55,6 @@ class ExportService {
|
||||||
uiPageSize: uiPageSize,
|
uiPageSize: uiPageSize,
|
||||||
signatureImageBytes: signatureImageBytes,
|
signatureImageBytes: signatureImageBytes,
|
||||||
placementsByPage: placementsByPage,
|
placementsByPage: placementsByPage,
|
||||||
placementImageByPage: placementImageByPage,
|
|
||||||
libraryBytes: libraryBytes,
|
libraryBytes: libraryBytes,
|
||||||
targetDpi: targetDpi,
|
targetDpi: targetDpi,
|
||||||
);
|
);
|
||||||
|
@ -76,8 +75,7 @@ class ExportService {
|
||||||
required Rect? signatureRectUi,
|
required Rect? signatureRectUi,
|
||||||
required Size uiPageSize,
|
required Size uiPageSize,
|
||||||
required Uint8List? signatureImageBytes,
|
required Uint8List? signatureImageBytes,
|
||||||
Map<int, List<Rect>>? placementsByPage,
|
Map<int, List<SignaturePlacement>>? placementsByPage,
|
||||||
Map<int, List<String>>? placementImageByPage,
|
|
||||||
Map<String, Uint8List>? libraryBytes,
|
Map<String, Uint8List>? libraryBytes,
|
||||||
double targetDpi = 144.0,
|
double targetDpi = 144.0,
|
||||||
}) async {
|
}) async {
|
||||||
|
@ -104,12 +102,8 @@ class ExportService {
|
||||||
(placementsByPage != null && placementsByPage.isNotEmpty);
|
(placementsByPage != null && placementsByPage.isNotEmpty);
|
||||||
final pagePlacements =
|
final pagePlacements =
|
||||||
hasMulti
|
hasMulti
|
||||||
? (placementsByPage[pageIndex] ?? const <Rect>[])
|
? (placementsByPage[pageIndex] ?? const <SignaturePlacement>[])
|
||||||
: const <Rect>[];
|
: const <SignaturePlacement>[];
|
||||||
final pageImageIds =
|
|
||||||
hasMulti
|
|
||||||
? (placementImageByPage?[pageIndex] ?? const <String>[])
|
|
||||||
: const <String>[];
|
|
||||||
final shouldStampSingle =
|
final shouldStampSingle =
|
||||||
!hasMulti &&
|
!hasMulti &&
|
||||||
signedPage != null &&
|
signedPage != null &&
|
||||||
|
@ -147,18 +141,18 @@ class ExportService {
|
||||||
// Multi-placement stamping: per-placement image from libraryBytes
|
// Multi-placement stamping: per-placement image from libraryBytes
|
||||||
if (hasMulti && pagePlacements.isNotEmpty) {
|
if (hasMulti && pagePlacements.isNotEmpty) {
|
||||||
for (var i = 0; i < pagePlacements.length; i++) {
|
for (var i = 0; i < pagePlacements.length; i++) {
|
||||||
final r = pagePlacements[i];
|
final placement = pagePlacements[i];
|
||||||
|
final r = placement.rect;
|
||||||
final left = r.left / uiPageSize.width * widthPts;
|
final left = r.left / uiPageSize.width * widthPts;
|
||||||
final top = r.top / uiPageSize.height * heightPts;
|
final top = r.top / uiPageSize.height * heightPts;
|
||||||
final w = r.width / uiPageSize.width * widthPts;
|
final w = r.width / uiPageSize.width * widthPts;
|
||||||
final h = r.height / uiPageSize.height * heightPts;
|
final h = r.height / uiPageSize.height * heightPts;
|
||||||
Uint8List? bytes;
|
Uint8List? bytes;
|
||||||
if (i < pageImageIds.length) {
|
final id = placement.imageId;
|
||||||
final id = pageImageIds[i];
|
if (id != null) {
|
||||||
bytes = libraryBytes?[id];
|
bytes = libraryBytes?[id];
|
||||||
}
|
}
|
||||||
bytes ??=
|
bytes ??= signatureImageBytes; // fallback
|
||||||
signatureImageBytes; // fallback to single image if provided
|
|
||||||
if (bytes != null && bytes.isNotEmpty) {
|
if (bytes != null && bytes.isNotEmpty) {
|
||||||
pw.MemoryImage? imgObj;
|
pw.MemoryImage? imgObj;
|
||||||
try {
|
try {
|
||||||
|
@ -176,7 +170,13 @@ class ExportService {
|
||||||
height: h,
|
height: h,
|
||||||
child: pw.FittedBox(
|
child: pw.FittedBox(
|
||||||
fit: pw.BoxFit.contain,
|
fit: pw.BoxFit.contain,
|
||||||
child: pw.Image(imgObj),
|
child: pw.Transform.rotate(
|
||||||
|
angle:
|
||||||
|
placement.rotationDeg *
|
||||||
|
3.1415926535 /
|
||||||
|
180.0,
|
||||||
|
child: pw.Image(imgObj),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -222,11 +222,9 @@ class ExportService {
|
||||||
final hasMulti =
|
final hasMulti =
|
||||||
(placementsByPage != null && placementsByPage.isNotEmpty);
|
(placementsByPage != null && placementsByPage.isNotEmpty);
|
||||||
final pagePlacements =
|
final pagePlacements =
|
||||||
hasMulti ? (placementsByPage[1] ?? const <Rect>[]) : const <Rect>[];
|
|
||||||
final pageImageIds =
|
|
||||||
hasMulti
|
hasMulti
|
||||||
? (placementImageByPage?[1] ?? const <String>[])
|
? (placementsByPage[1] ?? const <SignaturePlacement>[])
|
||||||
: const <String>[];
|
: const <SignaturePlacement>[];
|
||||||
final shouldStampSingle =
|
final shouldStampSingle =
|
||||||
!hasMulti &&
|
!hasMulti &&
|
||||||
signedPage != null &&
|
signedPage != null &&
|
||||||
|
@ -270,18 +268,18 @@ class ExportService {
|
||||||
// Multi-placement stamping on fallback page
|
// Multi-placement stamping on fallback page
|
||||||
if (hasMulti && pagePlacements.isNotEmpty) {
|
if (hasMulti && pagePlacements.isNotEmpty) {
|
||||||
for (var i = 0; i < pagePlacements.length; i++) {
|
for (var i = 0; i < pagePlacements.length; i++) {
|
||||||
final r = pagePlacements[i];
|
final placement = pagePlacements[i];
|
||||||
|
final r = placement.rect;
|
||||||
final left = r.left / uiPageSize.width * widthPts;
|
final left = r.left / uiPageSize.width * widthPts;
|
||||||
final top = r.top / uiPageSize.height * heightPts;
|
final top = r.top / uiPageSize.height * heightPts;
|
||||||
final w = r.width / uiPageSize.width * widthPts;
|
final w = r.width / uiPageSize.width * widthPts;
|
||||||
final h = r.height / uiPageSize.height * heightPts;
|
final h = r.height / uiPageSize.height * heightPts;
|
||||||
Uint8List? bytes;
|
Uint8List? bytes;
|
||||||
if (i < pageImageIds.length) {
|
final id = placement.imageId;
|
||||||
final id = pageImageIds[i];
|
if (id != null) {
|
||||||
bytes = libraryBytes?[id];
|
bytes = libraryBytes?[id];
|
||||||
}
|
}
|
||||||
bytes ??=
|
bytes ??= signatureImageBytes; // fallback
|
||||||
signatureImageBytes; // fallback to single image if provided
|
|
||||||
if (bytes != null && bytes.isNotEmpty) {
|
if (bytes != null && bytes.isNotEmpty) {
|
||||||
pw.MemoryImage? imgObj;
|
pw.MemoryImage? imgObj;
|
||||||
try {
|
try {
|
||||||
|
@ -313,7 +311,11 @@ class ExportService {
|
||||||
height: h,
|
height: h,
|
||||||
child: pw.FittedBox(
|
child: pw.FittedBox(
|
||||||
fit: pw.BoxFit.contain,
|
fit: pw.BoxFit.contain,
|
||||||
child: pw.Image(imgObj),
|
child: pw.Transform.rotate(
|
||||||
|
angle:
|
||||||
|
placement.rotationDeg * 3.1415926535 / 180.0,
|
||||||
|
child: pw.Image(imgObj),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -17,7 +17,6 @@ class PdfController extends StateNotifier<PdfState> {
|
||||||
pickedPdfPath: null,
|
pickedPdfPath: null,
|
||||||
signedPage: null,
|
signedPage: null,
|
||||||
placementsByPage: {},
|
placementsByPage: {},
|
||||||
placementImageByPage: {},
|
|
||||||
selectedPlacementIndex: null,
|
selectedPlacementIndex: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -35,7 +34,6 @@ class PdfController extends StateNotifier<PdfState> {
|
||||||
pickedPdfBytes: bytes,
|
pickedPdfBytes: bytes,
|
||||||
signedPage: null,
|
signedPage: null,
|
||||||
placementsByPage: {},
|
placementsByPage: {},
|
||||||
placementImageByPage: {},
|
|
||||||
selectedPlacementIndex: null,
|
selectedPlacementIndex: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -67,49 +65,54 @@ class PdfController extends StateNotifier<PdfState> {
|
||||||
void addPlacement({
|
void addPlacement({
|
||||||
required int page,
|
required int page,
|
||||||
required Rect rect,
|
required Rect rect,
|
||||||
String image = 'default.png',
|
String? imageId = 'default.png',
|
||||||
|
double rotationDeg = 0.0,
|
||||||
}) {
|
}) {
|
||||||
if (!state.loaded) return;
|
if (!state.loaded) return;
|
||||||
final p = page.clamp(1, state.pageCount);
|
final p = page.clamp(1, state.pageCount);
|
||||||
final map = Map<int, List<Rect>>.from(state.placementsByPage);
|
final map = Map<int, List<SignaturePlacement>>.from(state.placementsByPage);
|
||||||
final list = List<Rect>.from(map[p] ?? const []);
|
final list = List<SignaturePlacement>.from(map[p] ?? const []);
|
||||||
list.add(rect);
|
list.add(
|
||||||
map[p] = list;
|
SignaturePlacement(
|
||||||
// Sync image mapping list
|
rect: rect,
|
||||||
final imgMap = Map<int, List<String>>.from(state.placementImageByPage);
|
imageId: imageId,
|
||||||
final imgList = List<String>.from(imgMap[p] ?? const []);
|
rotationDeg: rotationDeg,
|
||||||
imgList.add(image);
|
),
|
||||||
imgMap[p] = imgList;
|
|
||||||
state = state.copyWith(
|
|
||||||
placementsByPage: map,
|
|
||||||
placementImageByPage: imgMap,
|
|
||||||
selectedPlacementIndex: null,
|
|
||||||
);
|
);
|
||||||
|
map[p] = list;
|
||||||
|
state = state.copyWith(placementsByPage: map, selectedPlacementIndex: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updatePlacementRotation({
|
||||||
|
required int page,
|
||||||
|
required int index,
|
||||||
|
required double rotationDeg,
|
||||||
|
}) {
|
||||||
|
if (!state.loaded) return;
|
||||||
|
final p = page.clamp(1, state.pageCount);
|
||||||
|
final map = Map<int, List<SignaturePlacement>>.from(state.placementsByPage);
|
||||||
|
final list = List<SignaturePlacement>.from(map[p] ?? const []);
|
||||||
|
if (index >= 0 && index < list.length) {
|
||||||
|
list[index] = list[index].copyWith(rotationDeg: rotationDeg);
|
||||||
|
map[p] = list;
|
||||||
|
state = state.copyWith(placementsByPage: map);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void removePlacement({required int page, required int index}) {
|
void removePlacement({required int page, required int index}) {
|
||||||
if (!state.loaded) return;
|
if (!state.loaded) return;
|
||||||
final p = page.clamp(1, state.pageCount);
|
final p = page.clamp(1, state.pageCount);
|
||||||
final map = Map<int, List<Rect>>.from(state.placementsByPage);
|
final map = Map<int, List<SignaturePlacement>>.from(state.placementsByPage);
|
||||||
final list = List<Rect>.from(map[p] ?? const []);
|
final list = List<SignaturePlacement>.from(map[p] ?? const []);
|
||||||
if (index >= 0 && index < list.length) {
|
if (index >= 0 && index < list.length) {
|
||||||
list.removeAt(index);
|
list.removeAt(index);
|
||||||
// Sync image mapping
|
|
||||||
final imgMap = Map<int, List<String>>.from(state.placementImageByPage);
|
|
||||||
final imgList = List<String>.from(imgMap[p] ?? const []);
|
|
||||||
if (index >= 0 && index < imgList.length) {
|
|
||||||
imgList.removeAt(index);
|
|
||||||
}
|
|
||||||
if (list.isEmpty) {
|
if (list.isEmpty) {
|
||||||
map.remove(p);
|
map.remove(p);
|
||||||
imgMap.remove(p);
|
|
||||||
} else {
|
} else {
|
||||||
map[p] = list;
|
map[p] = list;
|
||||||
imgMap[p] = imgList;
|
|
||||||
}
|
}
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
placementsByPage: map,
|
placementsByPage: map,
|
||||||
placementImageByPage: imgMap,
|
|
||||||
selectedPlacementIndex: null,
|
selectedPlacementIndex: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -123,17 +126,20 @@ class PdfController extends StateNotifier<PdfState> {
|
||||||
}) {
|
}) {
|
||||||
if (!state.loaded) return;
|
if (!state.loaded) return;
|
||||||
final p = page.clamp(1, state.pageCount);
|
final p = page.clamp(1, state.pageCount);
|
||||||
final map = Map<int, List<Rect>>.from(state.placementsByPage);
|
final map = Map<int, List<SignaturePlacement>>.from(state.placementsByPage);
|
||||||
final list = List<Rect>.from(map[p] ?? const []);
|
final list = List<SignaturePlacement>.from(map[p] ?? const []);
|
||||||
if (index >= 0 && index < list.length) {
|
if (index >= 0 && index < list.length) {
|
||||||
list[index] = rect;
|
final existing = list[index];
|
||||||
|
list[index] = existing.copyWith(rect: rect);
|
||||||
map[p] = list;
|
map[p] = list;
|
||||||
state = state.copyWith(placementsByPage: map);
|
state = state.copyWith(placementsByPage: map);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Rect> placementsOn(int page) {
|
List<SignaturePlacement> placementsOn(int page) {
|
||||||
return List<Rect>.from(state.placementsByPage[page] ?? const []);
|
return List<SignaturePlacement>.from(
|
||||||
|
state.placementsByPage[page] ?? const [],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void selectPlacement(int? index) {
|
void selectPlacement(int? index) {
|
||||||
|
@ -161,9 +167,9 @@ class PdfController extends StateNotifier<PdfState> {
|
||||||
|
|
||||||
// Convenience to get image name for a placement
|
// Convenience to get image name for a placement
|
||||||
String? imageOfPlacement({required int page, required int index}) {
|
String? imageOfPlacement({required int page, required int index}) {
|
||||||
final list = state.placementImageByPage[page] ?? const [];
|
final list = state.placementsByPage[page] ?? const [];
|
||||||
if (index < 0 || index >= list.length) return null;
|
if (index < 0 || index >= list.length) return null;
|
||||||
return list[index];
|
return list[index].imageId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../signature/view_model/signature_controller.dart';
|
import '../../signature/view_model/signature_controller.dart';
|
||||||
|
import '../../../../data/model/model.dart';
|
||||||
import '../view_model/pdf_controller.dart';
|
import '../view_model/pdf_controller.dart';
|
||||||
import 'signature_overlay.dart';
|
import 'signature_overlay.dart';
|
||||||
|
|
||||||
|
@ -30,12 +31,13 @@ class PdfPageOverlays extends ConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final pdf = ref.watch(pdfProvider);
|
final pdf = ref.watch(pdfProvider);
|
||||||
final sig = ref.watch(signatureProvider);
|
final sig = ref.watch(signatureProvider);
|
||||||
final placed = pdf.placementsByPage[pageNumber] ?? const <Rect>[];
|
final placed =
|
||||||
|
pdf.placementsByPage[pageNumber] ?? const <SignaturePlacement>[];
|
||||||
final widgets = <Widget>[];
|
final widgets = <Widget>[];
|
||||||
|
|
||||||
for (int i = 0; i < placed.length; i++) {
|
for (int i = 0; i < placed.length; i++) {
|
||||||
// Stored as UI-space rects (SignatureController.pageSize).
|
// Stored as UI-space rects (SignatureController.pageSize).
|
||||||
final uiRect = placed[i];
|
final uiRect = placed[i].rect;
|
||||||
widgets.add(
|
widgets.add(
|
||||||
SignatureOverlay(
|
SignatureOverlay(
|
||||||
pageSize: pageSize,
|
pageSize: pageSize,
|
||||||
|
|
|
@ -174,7 +174,6 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
uiPageSize: SignatureController.pageSize,
|
uiPageSize: SignatureController.pageSize,
|
||||||
signatureImageBytes: rotated,
|
signatureImageBytes: rotated,
|
||||||
placementsByPage: pdf.placementsByPage,
|
placementsByPage: pdf.placementsByPage,
|
||||||
placementImageByPage: pdf.placementImageByPage,
|
|
||||||
libraryBytes: {
|
libraryBytes: {
|
||||||
for (final a in ref.read(signatureLibraryProvider)) a.id: a.bytes,
|
for (final a in ref.read(signatureLibraryProvider)) a.id: a.bytes,
|
||||||
},
|
},
|
||||||
|
@ -211,7 +210,6 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
uiPageSize: SignatureController.pageSize,
|
uiPageSize: SignatureController.pageSize,
|
||||||
signatureImageBytes: rotated,
|
signatureImageBytes: rotated,
|
||||||
placementsByPage: pdf.placementsByPage,
|
placementsByPage: pdf.placementsByPage,
|
||||||
placementImageByPage: pdf.placementImageByPage,
|
|
||||||
libraryBytes: {
|
libraryBytes: {
|
||||||
for (final a in ref.read(signatureLibraryProvider)) a.id: a.bytes,
|
for (final a in ref.read(signatureLibraryProvider)) a.id: a.bytes,
|
||||||
},
|
},
|
||||||
|
@ -242,7 +240,6 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
uiPageSize: SignatureController.pageSize,
|
uiPageSize: SignatureController.pageSize,
|
||||||
signatureImageBytes: rotated,
|
signatureImageBytes: rotated,
|
||||||
placementsByPage: pdf.placementsByPage,
|
placementsByPage: pdf.placementsByPage,
|
||||||
placementImageByPage: pdf.placementImageByPage,
|
|
||||||
libraryBytes: {
|
libraryBytes: {
|
||||||
for (final a in ref.read(signatureLibraryProvider))
|
for (final a in ref.read(signatureLibraryProvider))
|
||||||
a.id: a.bytes,
|
a.id: a.bytes,
|
||||||
|
|
|
@ -253,11 +253,12 @@ class _SignatureImage extends ConsumerWidget {
|
||||||
final processed = ref.watch(processedSignatureImageProvider);
|
final processed = ref.watch(processedSignatureImageProvider);
|
||||||
bytes = processed ?? sig.imageBytes;
|
bytes = processed ?? sig.imageBytes;
|
||||||
} else if (placedIndex != null) {
|
} else if (placedIndex != null) {
|
||||||
// Use the image assigned to this placement
|
final placementList = ref.read(pdfProvider).placementsByPage[pageNumber];
|
||||||
final imgId = ref
|
final placement =
|
||||||
.read(pdfProvider)
|
(placementList != null && placedIndex! < placementList.length)
|
||||||
.placementImageByPage[pageNumber]
|
? placementList[placedIndex!]
|
||||||
?.elementAt(placedIndex!);
|
: null;
|
||||||
|
final imgId = placement?.imageId;
|
||||||
if (imgId != null) {
|
if (imgId != null) {
|
||||||
final lib = ref.watch(signatureLibraryProvider);
|
final lib = ref.watch(signatureLibraryProvider);
|
||||||
for (final a in lib) {
|
for (final a in lib) {
|
||||||
|
@ -267,7 +268,6 @@ class _SignatureImage extends ConsumerWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fallback to current processed
|
|
||||||
bytes ??= ref.read(processedSignatureImageProvider) ?? sig.imageBytes;
|
bytes ??= ref.read(processedSignatureImageProvider) ?? sig.imageBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,9 +281,19 @@ class _SignatureImage extends ConsumerWidget {
|
||||||
return Center(child: Text(label));
|
return Center(child: Text(label));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use live rotation for interactive overlay; stored rotation for placed
|
||||||
|
double rotationDeg = 0.0;
|
||||||
|
if (interactive) {
|
||||||
|
rotationDeg = sig.rotation;
|
||||||
|
} else if (placedIndex != null) {
|
||||||
|
final placementList = ref.read(pdfProvider).placementsByPage[pageNumber];
|
||||||
|
if (placementList != null && placedIndex! < placementList.length) {
|
||||||
|
rotationDeg = placementList[placedIndex!].rotationDeg;
|
||||||
|
}
|
||||||
|
}
|
||||||
return RotatedSignatureImage(
|
return RotatedSignatureImage(
|
||||||
bytes: bytes,
|
bytes: bytes,
|
||||||
rotationDeg: interactive ? sig.rotation : 0.0,
|
rotationDeg: rotationDeg,
|
||||||
enableAngleAwareScale: interactive,
|
enableAngleAwareScale: interactive,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
wrapInRepaintBoundary: true,
|
wrapInRepaintBoundary: true,
|
||||||
|
|
|
@ -21,17 +21,13 @@ class SignatureController extends StateNotifier<SignatureState> {
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
void placeDefaultRect() {
|
void placeDefaultRect() {
|
||||||
final w = 120.0, h = 60.0;
|
final w = 120.0, h = 60.0;
|
||||||
state = state.copyWith(
|
final rand = Random();
|
||||||
rect: Rect.fromCenter(
|
// Generate a center within 10%..90% of each axis to reduce off-screen risk
|
||||||
center: Offset(
|
final cx = pageSize.width * (0.1 + rand.nextDouble() * 0.8);
|
||||||
(pageSize.width / 2) * (Random().nextDouble() * 1.5 + 1),
|
final cy = pageSize.height * (0.1 + rand.nextDouble() * 0.8);
|
||||||
(pageSize.height / 2) * (Random().nextDouble() * 1.5 + 1),
|
Rect r = Rect.fromCenter(center: Offset(cx, cy), width: w, height: h);
|
||||||
),
|
r = _clampRectToPage(r);
|
||||||
width: w,
|
state = state.copyWith(rect: r, editingEnabled: true);
|
||||||
height: h,
|
|
||||||
),
|
|
||||||
editingEnabled: true,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadSample() {
|
void loadSample() {
|
||||||
|
@ -181,37 +177,20 @@ class SignatureController extends StateNotifier<SignatureState> {
|
||||||
if (!pdf.loaded) return null;
|
if (!pdf.loaded) return null;
|
||||||
// Bind the processed image at placement time (so placed preview matches adjustments).
|
// Bind the processed image at placement time (so placed preview matches adjustments).
|
||||||
// If processed bytes exist, always create a new asset for this placement.
|
// If processed bytes exist, always create a new asset for this placement.
|
||||||
String id = '';
|
// Prefer reusing an existing library asset id when the active overlay is
|
||||||
// Compose final bytes for placement: apply adjustments (processed) then rotation.
|
// based on a library item. If there is no library asset, do NOT create
|
||||||
Uint8List? srcBytes = ref.read(processedSignatureImageProvider);
|
// a new library card here — keep the placement's image id empty so the
|
||||||
srcBytes ??= state.imageBytes;
|
// UI and exporter will fall back to using the processed/current bytes.
|
||||||
// If still null, fall back to asset reference only.
|
String id = state.assetId ?? '';
|
||||||
if (srcBytes != null && srcBytes.isNotEmpty) {
|
|
||||||
final rot = state.rotation % 360;
|
|
||||||
Uint8List finalBytes = srcBytes;
|
|
||||||
if (rot != 0) {
|
|
||||||
try {
|
|
||||||
final decoded = img.decodeImage(srcBytes);
|
|
||||||
if (decoded != null) {
|
|
||||||
var out = img.copyRotate(
|
|
||||||
decoded,
|
|
||||||
angle: rot,
|
|
||||||
interpolation: img.Interpolation.linear,
|
|
||||||
);
|
|
||||||
finalBytes = Uint8List.fromList(img.encodePng(out, level: 6));
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
id = ref
|
|
||||||
.read(signatureLibraryProvider.notifier)
|
|
||||||
.add(finalBytes, name: 'image');
|
|
||||||
} else {
|
|
||||||
id = state.assetId ?? 'default.png';
|
|
||||||
}
|
|
||||||
// Store as UI-space rect (consistent with export and rendering paths)
|
// Store as UI-space rect (consistent with export and rendering paths)
|
||||||
ref
|
ref
|
||||||
.read(pdfProvider.notifier)
|
.read(pdfProvider.notifier)
|
||||||
.addPlacement(page: pdf.currentPage, rect: r, image: id);
|
.addPlacement(
|
||||||
|
page: pdf.currentPage,
|
||||||
|
rect: r,
|
||||||
|
imageId: id,
|
||||||
|
rotationDeg: state.rotation,
|
||||||
|
);
|
||||||
// Newly placed index is the last one on the page
|
// Newly placed index is the last one on the page
|
||||||
final idx =
|
final idx =
|
||||||
(ref.read(pdfProvider).placementsByPage[pdf.currentPage]?.length ?? 1) -
|
(ref.read(pdfProvider).placementsByPage[pdf.currentPage]?.length ?? 1) -
|
||||||
|
@ -227,39 +206,23 @@ class SignatureController extends StateNotifier<SignatureState> {
|
||||||
|
|
||||||
// Test/helper variant: confirm using a ProviderContainer instead of WidgetRef.
|
// Test/helper variant: confirm using a ProviderContainer instead of WidgetRef.
|
||||||
// Useful in widget tests where obtaining a WidgetRef is not straightforward.
|
// Useful in widget tests where obtaining a WidgetRef is not straightforward.
|
||||||
|
@visibleForTesting
|
||||||
Rect? confirmCurrentSignatureWithContainer(ProviderContainer container) {
|
Rect? confirmCurrentSignatureWithContainer(ProviderContainer container) {
|
||||||
final r = state.rect;
|
final r = state.rect;
|
||||||
if (r == null) return null;
|
if (r == null) return null;
|
||||||
final pdf = container.read(pdfProvider);
|
final pdf = container.read(pdfProvider);
|
||||||
if (!pdf.loaded) return null;
|
if (!pdf.loaded) return null;
|
||||||
String id = '';
|
// Reuse existing library id if present; otherwise leave empty so the
|
||||||
Uint8List? srcBytes = container.read(processedSignatureImageProvider);
|
// placement will reference the current bytes via fallback paths.
|
||||||
srcBytes ??= state.imageBytes;
|
String id = state.assetId ?? '';
|
||||||
if (srcBytes != null && srcBytes.isNotEmpty) {
|
|
||||||
final rot = state.rotation % 360;
|
|
||||||
Uint8List finalBytes = srcBytes;
|
|
||||||
if (rot != 0) {
|
|
||||||
try {
|
|
||||||
final decoded = img.decodeImage(srcBytes);
|
|
||||||
if (decoded != null) {
|
|
||||||
var out = img.copyRotate(
|
|
||||||
decoded,
|
|
||||||
angle: rot,
|
|
||||||
interpolation: img.Interpolation.linear,
|
|
||||||
);
|
|
||||||
finalBytes = Uint8List.fromList(img.encodePng(out, level: 6));
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
id = container
|
|
||||||
.read(signatureLibraryProvider.notifier)
|
|
||||||
.add(finalBytes, name: 'image');
|
|
||||||
} else {
|
|
||||||
id = state.assetId ?? 'default.png';
|
|
||||||
}
|
|
||||||
container
|
container
|
||||||
.read(pdfProvider.notifier)
|
.read(pdfProvider.notifier)
|
||||||
.addPlacement(page: pdf.currentPage, rect: r, image: id);
|
.addPlacement(
|
||||||
|
page: pdf.currentPage,
|
||||||
|
rect: r,
|
||||||
|
imageId: id,
|
||||||
|
rotationDeg: state.rotation,
|
||||||
|
);
|
||||||
final idx =
|
final idx =
|
||||||
(container
|
(container
|
||||||
.read(pdfProvider)
|
.read(pdfProvider)
|
||||||
|
|
|
@ -21,5 +21,5 @@ Future<void> aSignatureIsPlacedOnPage(WidgetTester tester, num page) async {
|
||||||
final Rect r = container.read(signatureProvider).rect!;
|
final Rect r = container.read(signatureProvider).rect!;
|
||||||
container
|
container
|
||||||
.read(pdfProvider.notifier)
|
.read(pdfProvider.notifier)
|
||||||
.addPlacement(page: page.toInt(), rect: r, image: 'default.png');
|
.addPlacement(page: page.toInt(), rect: r, imageId: 'default.png');
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,11 @@ Future<void> adjustingOneInstanceDoesNotAffectTheOthers(
|
||||||
final container = TestWorld.container ?? ProviderContainer();
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
final before = container.read(pdfProvider.notifier).placementsOn(2);
|
final before = container.read(pdfProvider.notifier).placementsOn(2);
|
||||||
expect(before.length, greaterThanOrEqualTo(2));
|
expect(before.length, greaterThanOrEqualTo(2));
|
||||||
final modified = before[0].translate(5, 0).inflate(3);
|
final modified = before[0].rect.translate(5, 0).inflate(3);
|
||||||
container.read(pdfProvider.notifier).removePlacement(page: 2, index: 0);
|
container.read(pdfProvider.notifier).removePlacement(page: 2, index: 0);
|
||||||
container.read(pdfProvider.notifier).addPlacement(page: 2, rect: modified);
|
container
|
||||||
|
.read(pdfProvider.notifier)
|
||||||
|
.addPlacement(page: 2, rect: modified, imageId: before[0].imageId);
|
||||||
final after = container.read(pdfProvider.notifier).placementsOn(2);
|
final after = container.read(pdfProvider.notifier).placementsOn(2);
|
||||||
expect(after.any((r) => r == before[1]), isTrue);
|
expect(after.any((p) => p.rect == before[1].rect), isTrue);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,11 +11,18 @@ Future<void> draggingOrResizingOneDoesNotChangeTheOther(
|
||||||
final container = TestWorld.container ?? ProviderContainer();
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
final list = container.read(pdfProvider.notifier).placementsOn(1);
|
final list = container.read(pdfProvider.notifier).placementsOn(1);
|
||||||
expect(list.length, greaterThanOrEqualTo(2));
|
expect(list.length, greaterThanOrEqualTo(2));
|
||||||
final before = List<Rect>.from(list.take(2));
|
final before = List<Rect>.from(list.take(2).map((p) => p.rect));
|
||||||
// Simulate changing the first only
|
// Simulate changing the first only
|
||||||
final changed = before[0].inflate(5);
|
final changed = before[0].inflate(5);
|
||||||
container.read(pdfProvider.notifier).removePlacement(page: 1, index: 0);
|
container.read(pdfProvider.notifier).removePlacement(page: 1, index: 0);
|
||||||
container.read(pdfProvider.notifier).addPlacement(page: 1, rect: changed);
|
container
|
||||||
|
.read(pdfProvider.notifier)
|
||||||
|
.addPlacement(
|
||||||
|
page: 1,
|
||||||
|
rect: changed,
|
||||||
|
imageId: list[1].imageId,
|
||||||
|
rotationDeg: list[1].rotationDeg,
|
||||||
|
);
|
||||||
final after = container.read(pdfProvider.notifier).placementsOn(1);
|
final after = container.read(pdfProvider.notifier).placementsOn(1);
|
||||||
expect(after.any((r) => r == before[1]), isTrue);
|
expect(after.any((p) => p.rect == before[1]), isTrue);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,9 @@ Future<void> eachSignatureCanBeDraggedAndResizedIndependently(
|
||||||
final list = container.read(pdfProvider.notifier).placementsOn(1);
|
final list = container.read(pdfProvider.notifier).placementsOn(1);
|
||||||
expect(list.length, greaterThanOrEqualTo(2));
|
expect(list.length, greaterThanOrEqualTo(2));
|
||||||
// Independence is modeled by distinct rects; ensure not equal and both within page
|
// Independence is modeled by distinct rects; ensure not equal and both within page
|
||||||
expect(list[0], isNot(equals(list[1])));
|
expect(list[0].rect, isNot(equals(list[1].rect)));
|
||||||
for (final r in list.take(2)) {
|
for (final p in list.take(2)) {
|
||||||
expect(r.left, greaterThanOrEqualTo(0));
|
expect(p.rect.left, greaterThanOrEqualTo(0));
|
||||||
expect(r.top, greaterThanOrEqualTo(0));
|
expect(p.rect.top, greaterThanOrEqualTo(0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,5 +31,5 @@ Future<void> theUserNavigatesToPageAndPlacesAnotherSignature(
|
||||||
final Rect r = container.read(signatureProvider).rect!;
|
final Rect r = container.read(signatureProvider).rect!;
|
||||||
container
|
container
|
||||||
.read(pdfProvider.notifier)
|
.read(pdfProvider.notifier)
|
||||||
.addPlacement(page: page.toInt(), rect: r, image: 'default.png');
|
.addPlacement(page: page.toInt(), rect: r, imageId: 'default.png');
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,5 +54,5 @@ Future<void> theUserPlacesASignatureFromPictureOnPage(
|
||||||
((TestWorld.placeFromPictureCallCount <= 1) ? 1 : 3);
|
((TestWorld.placeFromPictureCallCount <= 1) ? 1 : 3);
|
||||||
container
|
container
|
||||||
.read(pdfProvider.notifier)
|
.read(pdfProvider.notifier)
|
||||||
.addPlacement(page: page, rect: r, image: name);
|
.addPlacement(page: page, rect: r, imageId: name);
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,5 +30,5 @@ Future<void> theUserPlacesASignatureOnPage(
|
||||||
final Rect r = container.read(signatureProvider).rect!;
|
final Rect r = container.read(signatureProvider).rect!;
|
||||||
container
|
container
|
||||||
.read(pdfProvider.notifier)
|
.read(pdfProvider.notifier)
|
||||||
.addPlacement(page: page.toInt(), rect: r, image: 'default.png');
|
.addPlacement(page: page.toInt(), rect: r, imageId: 'default.png');
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,7 +119,7 @@ void main() {
|
||||||
final processed = container3.read(processedSignatureImageProvider);
|
final processed = container3.read(processedSignatureImageProvider);
|
||||||
expect(processed, isNotNull);
|
expect(processed, isNotNull);
|
||||||
final pdf = container3.read(pdfProvider);
|
final pdf = container3.read(pdfProvider);
|
||||||
final imgId = pdf.placementImageByPage[pdf.currentPage]?.first;
|
final imgId = pdf.placementsByPage[pdf.currentPage]?.first.imageId;
|
||||||
expect(imgId, isNotNull);
|
expect(imgId, isNotNull);
|
||||||
final lib = container3.read(signatureLibraryProvider);
|
final lib = container3.read(signatureLibraryProvider);
|
||||||
final match = lib.firstWhere((a) => a.id == imgId);
|
final match = lib.firstWhere((a) => a.id == imgId);
|
||||||
|
|
Loading…
Reference in New Issue