refactor: unify signature placement handling with new model structure
This commit is contained in:
parent
f74b724712
commit
4f149656bd
|
@ -1,12 +1,12 @@
|
|||
# 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.
|
||||
|
||||
* 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/*`)
|
||||
* 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...
|
||||
* 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);
|
||||
container
|
||||
.read(pdfProvider.notifier)
|
||||
.addPlacement(page: pdf.currentPage, rect: r, image: imageId);
|
||||
.addPlacement(page: pdf.currentPage, rect: r, imageId: imageId);
|
||||
container.read(signatureProvider.notifier).clearActiveOverlay();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
|
|
|
@ -1,6 +1,35 @@
|
|||
import 'dart:typed_data';
|
||||
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 {
|
||||
final bool loaded;
|
||||
final int pageCount;
|
||||
|
@ -8,10 +37,8 @@ class PdfState {
|
|||
final String? pickedPdfPath;
|
||||
final Uint8List? pickedPdfBytes;
|
||||
final int? signedPage;
|
||||
// Multiple signature placements per page, stored as UI-space rects (e.g., 400x560)
|
||||
final Map<int, List<Rect>> placementsByPage;
|
||||
// For each placement, store the assigned image identifier (e.g., filename) in the same index order.
|
||||
final Map<int, List<String>> placementImageByPage;
|
||||
// Multiple signature placements per page, each combines geometry and optional image id.
|
||||
final Map<int, List<SignaturePlacement>> placementsByPage;
|
||||
// UI state: selected placement index on the current page (if any)
|
||||
final int? selectedPlacementIndex;
|
||||
const PdfState({
|
||||
|
@ -22,7 +49,6 @@ class PdfState {
|
|||
this.pickedPdfBytes,
|
||||
this.signedPage,
|
||||
this.placementsByPage = const {},
|
||||
this.placementImageByPage = const {},
|
||||
this.selectedPlacementIndex,
|
||||
});
|
||||
factory PdfState.initial() => const PdfState(
|
||||
|
@ -32,7 +58,6 @@ class PdfState {
|
|||
pickedPdfBytes: null,
|
||||
signedPage: null,
|
||||
placementsByPage: {},
|
||||
placementImageByPage: {},
|
||||
selectedPlacementIndex: null,
|
||||
);
|
||||
PdfState copyWith({
|
||||
|
@ -42,8 +67,7 @@ class PdfState {
|
|||
String? pickedPdfPath,
|
||||
Uint8List? pickedPdfBytes,
|
||||
int? signedPage,
|
||||
Map<int, List<Rect>>? placementsByPage,
|
||||
Map<int, List<String>>? placementImageByPage,
|
||||
Map<int, List<SignaturePlacement>>? placementsByPage,
|
||||
int? selectedPlacementIndex,
|
||||
}) => PdfState(
|
||||
loaded: loaded ?? this.loaded,
|
||||
|
@ -53,7 +77,6 @@ class PdfState {
|
|||
pickedPdfBytes: pickedPdfBytes ?? this.pickedPdfBytes,
|
||||
signedPage: signedPage ?? this.signedPage,
|
||||
placementsByPage: placementsByPage ?? this.placementsByPage,
|
||||
placementImageByPage: placementImageByPage ?? this.placementImageByPage,
|
||||
selectedPlacementIndex:
|
||||
selectedPlacementIndex ?? this.selectedPlacementIndex,
|
||||
);
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:pdf/widgets.dart' as pw;
|
|||
import 'package:pdf/pdf.dart' as pdf;
|
||||
import 'package:printing/printing.dart' as printing;
|
||||
import 'package:image/image.dart' as img;
|
||||
import '../model/model.dart';
|
||||
|
||||
// NOTE:
|
||||
// - 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 Size uiPageSize,
|
||||
required Uint8List? signatureImageBytes,
|
||||
Map<int, List<Rect>>? placementsByPage,
|
||||
Map<int, List<String>>? placementImageByPage,
|
||||
Map<int, List<SignaturePlacement>>? placementsByPage,
|
||||
Map<String, Uint8List>? libraryBytes,
|
||||
double targetDpi = 144.0,
|
||||
}) async {
|
||||
|
@ -55,7 +55,6 @@ class ExportService {
|
|||
uiPageSize: uiPageSize,
|
||||
signatureImageBytes: signatureImageBytes,
|
||||
placementsByPage: placementsByPage,
|
||||
placementImageByPage: placementImageByPage,
|
||||
libraryBytes: libraryBytes,
|
||||
targetDpi: targetDpi,
|
||||
);
|
||||
|
@ -76,8 +75,7 @@ class ExportService {
|
|||
required Rect? signatureRectUi,
|
||||
required Size uiPageSize,
|
||||
required Uint8List? signatureImageBytes,
|
||||
Map<int, List<Rect>>? placementsByPage,
|
||||
Map<int, List<String>>? placementImageByPage,
|
||||
Map<int, List<SignaturePlacement>>? placementsByPage,
|
||||
Map<String, Uint8List>? libraryBytes,
|
||||
double targetDpi = 144.0,
|
||||
}) async {
|
||||
|
@ -104,12 +102,8 @@ class ExportService {
|
|||
(placementsByPage != null && placementsByPage.isNotEmpty);
|
||||
final pagePlacements =
|
||||
hasMulti
|
||||
? (placementsByPage[pageIndex] ?? const <Rect>[])
|
||||
: const <Rect>[];
|
||||
final pageImageIds =
|
||||
hasMulti
|
||||
? (placementImageByPage?[pageIndex] ?? const <String>[])
|
||||
: const <String>[];
|
||||
? (placementsByPage[pageIndex] ?? const <SignaturePlacement>[])
|
||||
: const <SignaturePlacement>[];
|
||||
final shouldStampSingle =
|
||||
!hasMulti &&
|
||||
signedPage != null &&
|
||||
|
@ -147,18 +141,18 @@ class ExportService {
|
|||
// Multi-placement stamping: per-placement image from libraryBytes
|
||||
if (hasMulti && pagePlacements.isNotEmpty) {
|
||||
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 top = r.top / uiPageSize.height * heightPts;
|
||||
final w = r.width / uiPageSize.width * widthPts;
|
||||
final h = r.height / uiPageSize.height * heightPts;
|
||||
Uint8List? bytes;
|
||||
if (i < pageImageIds.length) {
|
||||
final id = pageImageIds[i];
|
||||
final id = placement.imageId;
|
||||
if (id != null) {
|
||||
bytes = libraryBytes?[id];
|
||||
}
|
||||
bytes ??=
|
||||
signatureImageBytes; // fallback to single image if provided
|
||||
bytes ??= signatureImageBytes; // fallback
|
||||
if (bytes != null && bytes.isNotEmpty) {
|
||||
pw.MemoryImage? imgObj;
|
||||
try {
|
||||
|
@ -176,10 +170,16 @@ class ExportService {
|
|||
height: h,
|
||||
child: pw.FittedBox(
|
||||
fit: pw.BoxFit.contain,
|
||||
child: pw.Transform.rotate(
|
||||
angle:
|
||||
placement.rotationDeg *
|
||||
3.1415926535 /
|
||||
180.0,
|
||||
child: pw.Image(imgObj),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -222,11 +222,9 @@ class ExportService {
|
|||
final hasMulti =
|
||||
(placementsByPage != null && placementsByPage.isNotEmpty);
|
||||
final pagePlacements =
|
||||
hasMulti ? (placementsByPage[1] ?? const <Rect>[]) : const <Rect>[];
|
||||
final pageImageIds =
|
||||
hasMulti
|
||||
? (placementImageByPage?[1] ?? const <String>[])
|
||||
: const <String>[];
|
||||
? (placementsByPage[1] ?? const <SignaturePlacement>[])
|
||||
: const <SignaturePlacement>[];
|
||||
final shouldStampSingle =
|
||||
!hasMulti &&
|
||||
signedPage != null &&
|
||||
|
@ -270,18 +268,18 @@ class ExportService {
|
|||
// Multi-placement stamping on fallback page
|
||||
if (hasMulti && pagePlacements.isNotEmpty) {
|
||||
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 top = r.top / uiPageSize.height * heightPts;
|
||||
final w = r.width / uiPageSize.width * widthPts;
|
||||
final h = r.height / uiPageSize.height * heightPts;
|
||||
Uint8List? bytes;
|
||||
if (i < pageImageIds.length) {
|
||||
final id = pageImageIds[i];
|
||||
final id = placement.imageId;
|
||||
if (id != null) {
|
||||
bytes = libraryBytes?[id];
|
||||
}
|
||||
bytes ??=
|
||||
signatureImageBytes; // fallback to single image if provided
|
||||
bytes ??= signatureImageBytes; // fallback
|
||||
if (bytes != null && bytes.isNotEmpty) {
|
||||
pw.MemoryImage? imgObj;
|
||||
try {
|
||||
|
@ -313,10 +311,14 @@ class ExportService {
|
|||
height: h,
|
||||
child: pw.FittedBox(
|
||||
fit: pw.BoxFit.contain,
|
||||
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,
|
||||
signedPage: null,
|
||||
placementsByPage: {},
|
||||
placementImageByPage: {},
|
||||
selectedPlacementIndex: null,
|
||||
);
|
||||
}
|
||||
|
@ -35,7 +34,6 @@ class PdfController extends StateNotifier<PdfState> {
|
|||
pickedPdfBytes: bytes,
|
||||
signedPage: null,
|
||||
placementsByPage: {},
|
||||
placementImageByPage: {},
|
||||
selectedPlacementIndex: null,
|
||||
);
|
||||
}
|
||||
|
@ -67,49 +65,54 @@ class PdfController extends StateNotifier<PdfState> {
|
|||
void addPlacement({
|
||||
required int page,
|
||||
required Rect rect,
|
||||
String image = 'default.png',
|
||||
String? imageId = 'default.png',
|
||||
double rotationDeg = 0.0,
|
||||
}) {
|
||||
if (!state.loaded) return;
|
||||
final p = page.clamp(1, state.pageCount);
|
||||
final map = Map<int, List<Rect>>.from(state.placementsByPage);
|
||||
final list = List<Rect>.from(map[p] ?? const []);
|
||||
list.add(rect);
|
||||
map[p] = list;
|
||||
// Sync image mapping list
|
||||
final imgMap = Map<int, List<String>>.from(state.placementImageByPage);
|
||||
final imgList = List<String>.from(imgMap[p] ?? const []);
|
||||
imgList.add(image);
|
||||
imgMap[p] = imgList;
|
||||
state = state.copyWith(
|
||||
placementsByPage: map,
|
||||
placementImageByPage: imgMap,
|
||||
selectedPlacementIndex: null,
|
||||
final map = Map<int, List<SignaturePlacement>>.from(state.placementsByPage);
|
||||
final list = List<SignaturePlacement>.from(map[p] ?? const []);
|
||||
list.add(
|
||||
SignaturePlacement(
|
||||
rect: rect,
|
||||
imageId: imageId,
|
||||
rotationDeg: rotationDeg,
|
||||
),
|
||||
);
|
||||
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}) {
|
||||
if (!state.loaded) return;
|
||||
final p = page.clamp(1, state.pageCount);
|
||||
final map = Map<int, List<Rect>>.from(state.placementsByPage);
|
||||
final list = List<Rect>.from(map[p] ?? const []);
|
||||
final map = Map<int, List<SignaturePlacement>>.from(state.placementsByPage);
|
||||
final list = List<SignaturePlacement>.from(map[p] ?? const []);
|
||||
if (index >= 0 && index < list.length) {
|
||||
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) {
|
||||
map.remove(p);
|
||||
imgMap.remove(p);
|
||||
} else {
|
||||
map[p] = list;
|
||||
imgMap[p] = imgList;
|
||||
}
|
||||
state = state.copyWith(
|
||||
placementsByPage: map,
|
||||
placementImageByPage: imgMap,
|
||||
selectedPlacementIndex: null,
|
||||
);
|
||||
}
|
||||
|
@ -123,17 +126,20 @@ class PdfController extends StateNotifier<PdfState> {
|
|||
}) {
|
||||
if (!state.loaded) return;
|
||||
final p = page.clamp(1, state.pageCount);
|
||||
final map = Map<int, List<Rect>>.from(state.placementsByPage);
|
||||
final list = List<Rect>.from(map[p] ?? const []);
|
||||
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] = rect;
|
||||
final existing = list[index];
|
||||
list[index] = existing.copyWith(rect: rect);
|
||||
map[p] = list;
|
||||
state = state.copyWith(placementsByPage: map);
|
||||
}
|
||||
}
|
||||
|
||||
List<Rect> placementsOn(int page) {
|
||||
return List<Rect>.from(state.placementsByPage[page] ?? const []);
|
||||
List<SignaturePlacement> placementsOn(int page) {
|
||||
return List<SignaturePlacement>.from(
|
||||
state.placementsByPage[page] ?? const [],
|
||||
);
|
||||
}
|
||||
|
||||
void selectPlacement(int? index) {
|
||||
|
@ -161,9 +167,9 @@ class PdfController extends StateNotifier<PdfState> {
|
|||
|
||||
// Convenience to get image name for a placement
|
||||
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;
|
||||
return list[index];
|
||||
return list[index].imageId;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../signature/view_model/signature_controller.dart';
|
||||
import '../../../../data/model/model.dart';
|
||||
import '../view_model/pdf_controller.dart';
|
||||
import 'signature_overlay.dart';
|
||||
|
||||
|
@ -30,12 +31,13 @@ class PdfPageOverlays extends ConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final pdf = ref.watch(pdfProvider);
|
||||
final sig = ref.watch(signatureProvider);
|
||||
final placed = pdf.placementsByPage[pageNumber] ?? const <Rect>[];
|
||||
final placed =
|
||||
pdf.placementsByPage[pageNumber] ?? const <SignaturePlacement>[];
|
||||
final widgets = <Widget>[];
|
||||
|
||||
for (int i = 0; i < placed.length; i++) {
|
||||
// Stored as UI-space rects (SignatureController.pageSize).
|
||||
final uiRect = placed[i];
|
||||
final uiRect = placed[i].rect;
|
||||
widgets.add(
|
||||
SignatureOverlay(
|
||||
pageSize: pageSize,
|
||||
|
|
|
@ -174,7 +174,6 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
uiPageSize: SignatureController.pageSize,
|
||||
signatureImageBytes: rotated,
|
||||
placementsByPage: pdf.placementsByPage,
|
||||
placementImageByPage: pdf.placementImageByPage,
|
||||
libraryBytes: {
|
||||
for (final a in ref.read(signatureLibraryProvider)) a.id: a.bytes,
|
||||
},
|
||||
|
@ -211,7 +210,6 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
uiPageSize: SignatureController.pageSize,
|
||||
signatureImageBytes: rotated,
|
||||
placementsByPage: pdf.placementsByPage,
|
||||
placementImageByPage: pdf.placementImageByPage,
|
||||
libraryBytes: {
|
||||
for (final a in ref.read(signatureLibraryProvider)) a.id: a.bytes,
|
||||
},
|
||||
|
@ -242,7 +240,6 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
uiPageSize: SignatureController.pageSize,
|
||||
signatureImageBytes: rotated,
|
||||
placementsByPage: pdf.placementsByPage,
|
||||
placementImageByPage: pdf.placementImageByPage,
|
||||
libraryBytes: {
|
||||
for (final a in ref.read(signatureLibraryProvider))
|
||||
a.id: a.bytes,
|
||||
|
|
|
@ -253,11 +253,12 @@ class _SignatureImage extends ConsumerWidget {
|
|||
final processed = ref.watch(processedSignatureImageProvider);
|
||||
bytes = processed ?? sig.imageBytes;
|
||||
} else if (placedIndex != null) {
|
||||
// Use the image assigned to this placement
|
||||
final imgId = ref
|
||||
.read(pdfProvider)
|
||||
.placementImageByPage[pageNumber]
|
||||
?.elementAt(placedIndex!);
|
||||
final placementList = ref.read(pdfProvider).placementsByPage[pageNumber];
|
||||
final placement =
|
||||
(placementList != null && placedIndex! < placementList.length)
|
||||
? placementList[placedIndex!]
|
||||
: null;
|
||||
final imgId = placement?.imageId;
|
||||
if (imgId != null) {
|
||||
final lib = ref.watch(signatureLibraryProvider);
|
||||
for (final a in lib) {
|
||||
|
@ -267,7 +268,6 @@ class _SignatureImage extends ConsumerWidget {
|
|||
}
|
||||
}
|
||||
}
|
||||
// Fallback to current processed
|
||||
bytes ??= ref.read(processedSignatureImageProvider) ?? sig.imageBytes;
|
||||
}
|
||||
|
||||
|
@ -281,9 +281,19 @@ class _SignatureImage extends ConsumerWidget {
|
|||
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(
|
||||
bytes: bytes,
|
||||
rotationDeg: interactive ? sig.rotation : 0.0,
|
||||
rotationDeg: rotationDeg,
|
||||
enableAngleAwareScale: interactive,
|
||||
fit: BoxFit.contain,
|
||||
wrapInRepaintBoundary: true,
|
||||
|
|
|
@ -21,17 +21,13 @@ class SignatureController extends StateNotifier<SignatureState> {
|
|||
@visibleForTesting
|
||||
void placeDefaultRect() {
|
||||
final w = 120.0, h = 60.0;
|
||||
state = state.copyWith(
|
||||
rect: Rect.fromCenter(
|
||||
center: Offset(
|
||||
(pageSize.width / 2) * (Random().nextDouble() * 1.5 + 1),
|
||||
(pageSize.height / 2) * (Random().nextDouble() * 1.5 + 1),
|
||||
),
|
||||
width: w,
|
||||
height: h,
|
||||
),
|
||||
editingEnabled: true,
|
||||
);
|
||||
final rand = Random();
|
||||
// Generate a center within 10%..90% of each axis to reduce off-screen risk
|
||||
final cx = pageSize.width * (0.1 + rand.nextDouble() * 0.8);
|
||||
final cy = pageSize.height * (0.1 + rand.nextDouble() * 0.8);
|
||||
Rect r = Rect.fromCenter(center: Offset(cx, cy), width: w, height: h);
|
||||
r = _clampRectToPage(r);
|
||||
state = state.copyWith(rect: r, editingEnabled: true);
|
||||
}
|
||||
|
||||
void loadSample() {
|
||||
|
@ -181,37 +177,20 @@ class SignatureController extends StateNotifier<SignatureState> {
|
|||
if (!pdf.loaded) return null;
|
||||
// Bind the processed image at placement time (so placed preview matches adjustments).
|
||||
// If processed bytes exist, always create a new asset for this placement.
|
||||
String id = '';
|
||||
// Compose final bytes for placement: apply adjustments (processed) then rotation.
|
||||
Uint8List? srcBytes = ref.read(processedSignatureImageProvider);
|
||||
srcBytes ??= state.imageBytes;
|
||||
// If still null, fall back to asset reference only.
|
||||
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';
|
||||
}
|
||||
// Prefer reusing an existing library asset id when the active overlay is
|
||||
// based on a library item. If there is no library asset, do NOT create
|
||||
// a new library card here — keep the placement's image id empty so the
|
||||
// UI and exporter will fall back to using the processed/current bytes.
|
||||
String id = state.assetId ?? '';
|
||||
// Store as UI-space rect (consistent with export and rendering paths)
|
||||
ref
|
||||
.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
|
||||
final idx =
|
||||
(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.
|
||||
// Useful in widget tests where obtaining a WidgetRef is not straightforward.
|
||||
@visibleForTesting
|
||||
Rect? confirmCurrentSignatureWithContainer(ProviderContainer container) {
|
||||
final r = state.rect;
|
||||
if (r == null) return null;
|
||||
final pdf = container.read(pdfProvider);
|
||||
if (!pdf.loaded) return null;
|
||||
String id = '';
|
||||
Uint8List? srcBytes = container.read(processedSignatureImageProvider);
|
||||
srcBytes ??= state.imageBytes;
|
||||
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';
|
||||
}
|
||||
// Reuse existing library id if present; otherwise leave empty so the
|
||||
// placement will reference the current bytes via fallback paths.
|
||||
String id = state.assetId ?? '';
|
||||
container
|
||||
.read(pdfProvider.notifier)
|
||||
.addPlacement(page: pdf.currentPage, rect: r, image: id);
|
||||
.addPlacement(
|
||||
page: pdf.currentPage,
|
||||
rect: r,
|
||||
imageId: id,
|
||||
rotationDeg: state.rotation,
|
||||
);
|
||||
final idx =
|
||||
(container
|
||||
.read(pdfProvider)
|
||||
|
|
|
@ -21,5 +21,5 @@ Future<void> aSignatureIsPlacedOnPage(WidgetTester tester, num page) async {
|
|||
final Rect r = container.read(signatureProvider).rect!;
|
||||
container
|
||||
.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 before = container.read(pdfProvider.notifier).placementsOn(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).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);
|
||||
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 list = container.read(pdfProvider.notifier).placementsOn(1);
|
||||
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
|
||||
final changed = before[0].inflate(5);
|
||||
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);
|
||||
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);
|
||||
expect(list.length, greaterThanOrEqualTo(2));
|
||||
// Independence is modeled by distinct rects; ensure not equal and both within page
|
||||
expect(list[0], isNot(equals(list[1])));
|
||||
for (final r in list.take(2)) {
|
||||
expect(r.left, greaterThanOrEqualTo(0));
|
||||
expect(r.top, greaterThanOrEqualTo(0));
|
||||
expect(list[0].rect, isNot(equals(list[1].rect)));
|
||||
for (final p in list.take(2)) {
|
||||
expect(p.rect.left, greaterThanOrEqualTo(0));
|
||||
expect(p.rect.top, greaterThanOrEqualTo(0));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,5 +31,5 @@ Future<void> theUserNavigatesToPageAndPlacesAnotherSignature(
|
|||
final Rect r = container.read(signatureProvider).rect!;
|
||||
container
|
||||
.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);
|
||||
container
|
||||
.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!;
|
||||
container
|
||||
.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);
|
||||
expect(processed, isNotNull);
|
||||
final pdf = container3.read(pdfProvider);
|
||||
final imgId = pdf.placementImageByPage[pdf.currentPage]?.first;
|
||||
final imgId = pdf.placementsByPage[pdf.currentPage]?.first.imageId;
|
||||
expect(imgId, isNotNull);
|
||||
final lib = container3.read(signatureLibraryProvider);
|
||||
final match = lib.firstWhere((a) => a.id == imgId);
|
||||
|
|
Loading…
Reference in New Issue