Compare commits
No commits in common. "0969ec29310e105d9e0be2b7dcc56b63e2944fce" and "51bf7ed979419cdb4d282d5c6311f40182b09742" have entirely different histories.
0969ec2931
...
51bf7ed979
|
@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
import 'package:pdf_signature/data/services/export_service.dart';
|
import 'package:pdf_signature/data/services/export_service.dart';
|
||||||
import 'package:pdf_signature/data/services/export_providers.dart';
|
import 'package:pdf_signature/data/services/providers.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/pdf/widgets/pdf_screen.dart';
|
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
|
||||||
import 'package:pdf_signature/l10n/app_localizations.dart';
|
import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||||
|
|
|
@ -5,7 +5,7 @@ import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||||
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
|
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 'data/services/preferences_providers.dart';
|
import 'ui/features/preferences/providers.dart';
|
||||||
import 'package:pdf_signature/ui/features/preferences/widgets/settings_screen.dart';
|
import 'package:pdf_signature/ui/features/preferences/widgets/settings_screen.dart';
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
|
|
|
@ -69,8 +69,6 @@ class SignatureState {
|
||||||
final double rotation;
|
final double rotation;
|
||||||
final List<List<Offset>> strokes;
|
final List<List<Offset>> strokes;
|
||||||
final Uint8List? imageBytes;
|
final Uint8List? imageBytes;
|
||||||
// The ID of the signature asset the current overlay is based on (from library)
|
|
||||||
final String? assetId;
|
|
||||||
// When true, the active signature overlay is movable/resizable and should not be exported.
|
// When true, the active signature overlay is movable/resizable and should not be exported.
|
||||||
// When false, the overlay is confirmed (unmovable) and eligible for export.
|
// When false, the overlay is confirmed (unmovable) and eligible for export.
|
||||||
final bool editingEnabled;
|
final bool editingEnabled;
|
||||||
|
@ -83,7 +81,6 @@ class SignatureState {
|
||||||
this.rotation = 0.0,
|
this.rotation = 0.0,
|
||||||
required this.strokes,
|
required this.strokes,
|
||||||
this.imageBytes,
|
this.imageBytes,
|
||||||
this.assetId,
|
|
||||||
this.editingEnabled = false,
|
this.editingEnabled = false,
|
||||||
});
|
});
|
||||||
factory SignatureState.initial() => const SignatureState(
|
factory SignatureState.initial() => const SignatureState(
|
||||||
|
@ -95,7 +92,6 @@ class SignatureState {
|
||||||
rotation: 0.0,
|
rotation: 0.0,
|
||||||
strokes: [],
|
strokes: [],
|
||||||
imageBytes: null,
|
imageBytes: null,
|
||||||
assetId: null,
|
|
||||||
editingEnabled: false,
|
editingEnabled: false,
|
||||||
);
|
);
|
||||||
SignatureState copyWith({
|
SignatureState copyWith({
|
||||||
|
@ -107,7 +103,6 @@ class SignatureState {
|
||||||
double? rotation,
|
double? rotation,
|
||||||
List<List<Offset>>? strokes,
|
List<List<Offset>>? strokes,
|
||||||
Uint8List? imageBytes,
|
Uint8List? imageBytes,
|
||||||
String? assetId,
|
|
||||||
bool? editingEnabled,
|
bool? editingEnabled,
|
||||||
}) => SignatureState(
|
}) => SignatureState(
|
||||||
rect: rect ?? this.rect,
|
rect: rect ?? this.rect,
|
||||||
|
@ -118,7 +113,6 @@ class SignatureState {
|
||||||
rotation: rotation ?? this.rotation,
|
rotation: rotation ?? this.rotation,
|
||||||
strokes: strokes ?? this.strokes,
|
strokes: strokes ?? this.strokes,
|
||||||
imageBytes: imageBytes ?? this.imageBytes,
|
imageBytes: imageBytes ?? this.imageBytes,
|
||||||
assetId: assetId ?? this.assetId,
|
|
||||||
editingEnabled: editingEnabled ?? this.editingEnabled,
|
editingEnabled: editingEnabled ?? this.editingEnabled,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,8 +33,6 @@ class ExportService {
|
||||||
required Size uiPageSize,
|
required Size uiPageSize,
|
||||||
required Uint8List? signatureImageBytes,
|
required Uint8List? signatureImageBytes,
|
||||||
Map<int, List<Rect>>? placementsByPage,
|
Map<int, List<Rect>>? placementsByPage,
|
||||||
Map<int, List<String>>? placementImageByPage,
|
|
||||||
Map<String, Uint8List>? libraryBytes,
|
|
||||||
double targetDpi = 144.0,
|
double targetDpi = 144.0,
|
||||||
}) async {
|
}) async {
|
||||||
// print(
|
// print(
|
||||||
|
@ -55,8 +53,6 @@ class ExportService {
|
||||||
uiPageSize: uiPageSize,
|
uiPageSize: uiPageSize,
|
||||||
signatureImageBytes: signatureImageBytes,
|
signatureImageBytes: signatureImageBytes,
|
||||||
placementsByPage: placementsByPage,
|
placementsByPage: placementsByPage,
|
||||||
placementImageByPage: placementImageByPage,
|
|
||||||
libraryBytes: libraryBytes,
|
|
||||||
targetDpi: targetDpi,
|
targetDpi: targetDpi,
|
||||||
);
|
);
|
||||||
if (bytes == null) return false;
|
if (bytes == null) return false;
|
||||||
|
@ -77,8 +73,6 @@ class ExportService {
|
||||||
required Size uiPageSize,
|
required Size uiPageSize,
|
||||||
required Uint8List? signatureImageBytes,
|
required Uint8List? signatureImageBytes,
|
||||||
Map<int, List<Rect>>? placementsByPage,
|
Map<int, List<Rect>>? placementsByPage,
|
||||||
Map<int, List<String>>? placementImageByPage,
|
|
||||||
Map<String, Uint8List>? libraryBytes,
|
|
||||||
double targetDpi = 144.0,
|
double targetDpi = 144.0,
|
||||||
}) async {
|
}) async {
|
||||||
final out = pw.Document(version: pdf.PdfVersion.pdf_1_4, compress: false);
|
final out = pw.Document(version: pdf.PdfVersion.pdf_1_4, compress: false);
|
||||||
|
@ -106,10 +100,6 @@ class ExportService {
|
||||||
hasMulti
|
hasMulti
|
||||||
? (placementsByPage[pageIndex] ?? const <Rect>[])
|
? (placementsByPage[pageIndex] ?? const <Rect>[])
|
||||||
: const <Rect>[];
|
: const <Rect>[];
|
||||||
final pageImageIds =
|
|
||||||
hasMulti
|
|
||||||
? (placementImageByPage?[pageIndex] ?? const <String>[])
|
|
||||||
: const <String>[];
|
|
||||||
final shouldStampSingle =
|
final shouldStampSingle =
|
||||||
!hasMulti &&
|
!hasMulti &&
|
||||||
signedPage != null &&
|
signedPage != null &&
|
||||||
|
@ -117,7 +107,12 @@ class ExportService {
|
||||||
signatureRectUi != null &&
|
signatureRectUi != null &&
|
||||||
signatureImageBytes != null &&
|
signatureImageBytes != null &&
|
||||||
signatureImageBytes.isNotEmpty;
|
signatureImageBytes.isNotEmpty;
|
||||||
if (shouldStampSingle) {
|
final shouldStampMulti =
|
||||||
|
hasMulti &&
|
||||||
|
pagePlacements.isNotEmpty &&
|
||||||
|
signatureImageBytes != null &&
|
||||||
|
signatureImageBytes.isNotEmpty;
|
||||||
|
if (shouldStampSingle || shouldStampMulti) {
|
||||||
try {
|
try {
|
||||||
sigImgObj = pw.MemoryImage(signatureImageBytes);
|
sigImgObj = pw.MemoryImage(signatureImageBytes);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
@ -144,40 +139,22 @@ class ExportService {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
// Multi-placement stamping: per-placement image from libraryBytes
|
if (sigImgObj != null) {
|
||||||
if (hasMulti && pagePlacements.isNotEmpty) {
|
if (hasMulti && pagePlacements.isNotEmpty) {
|
||||||
for (var i = 0; i < pagePlacements.length; i++) {
|
for (final r in pagePlacements) {
|
||||||
final r = pagePlacements[i];
|
|
||||||
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;
|
|
||||||
if (i < pageImageIds.length) {
|
|
||||||
final id = pageImageIds[i];
|
|
||||||
bytes = libraryBytes?[id];
|
|
||||||
}
|
|
||||||
bytes ??=
|
|
||||||
signatureImageBytes; // fallback to single image if provided
|
|
||||||
if (bytes != null && bytes.isNotEmpty) {
|
|
||||||
pw.MemoryImage? imgObj;
|
|
||||||
try {
|
|
||||||
imgObj = pw.MemoryImage(bytes);
|
|
||||||
} catch (_) {
|
|
||||||
imgObj = null;
|
|
||||||
}
|
|
||||||
if (imgObj != null) {
|
|
||||||
children.add(
|
children.add(
|
||||||
pw.Positioned(
|
pw.Positioned(
|
||||||
left: left,
|
left: left,
|
||||||
top: top,
|
top: top,
|
||||||
child: pw.Image(imgObj, width: w, height: h),
|
child: pw.Image(sigImgObj, width: w, height: h),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
} else if (shouldStampSingle) {
|
||||||
}
|
|
||||||
} else if (shouldStampSingle && sigImgObj != null) {
|
|
||||||
final r = signatureRectUi;
|
final r = signatureRectUi;
|
||||||
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;
|
||||||
|
@ -191,6 +168,7 @@ class ExportService {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return pw.Stack(children: children);
|
return pw.Stack(children: children);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -209,10 +187,6 @@ class ExportService {
|
||||||
(placementsByPage != null && placementsByPage.isNotEmpty);
|
(placementsByPage != null && placementsByPage.isNotEmpty);
|
||||||
final pagePlacements =
|
final pagePlacements =
|
||||||
hasMulti ? (placementsByPage[1] ?? const <Rect>[]) : const <Rect>[];
|
hasMulti ? (placementsByPage[1] ?? const <Rect>[]) : const <Rect>[];
|
||||||
final pageImageIds =
|
|
||||||
hasMulti
|
|
||||||
? (placementImageByPage?[1] ?? const <String>[])
|
|
||||||
: const <String>[];
|
|
||||||
final shouldStampSingle =
|
final shouldStampSingle =
|
||||||
!hasMulti &&
|
!hasMulti &&
|
||||||
signedPage != null &&
|
signedPage != null &&
|
||||||
|
@ -220,7 +194,12 @@ class ExportService {
|
||||||
signatureRectUi != null &&
|
signatureRectUi != null &&
|
||||||
signatureImageBytes != null &&
|
signatureImageBytes != null &&
|
||||||
signatureImageBytes.isNotEmpty;
|
signatureImageBytes.isNotEmpty;
|
||||||
if (shouldStampSingle) {
|
final shouldStampMulti =
|
||||||
|
hasMulti &&
|
||||||
|
pagePlacements.isNotEmpty &&
|
||||||
|
signatureImageBytes != null &&
|
||||||
|
signatureImageBytes.isNotEmpty;
|
||||||
|
if (shouldStampSingle || shouldStampMulti) {
|
||||||
try {
|
try {
|
||||||
// If it's already PNG, keep as-is to preserve alpha; otherwise decode/encode PNG
|
// If it's already PNG, keep as-is to preserve alpha; otherwise decode/encode PNG
|
||||||
final asStr = String.fromCharCodes(signatureImageBytes.take(8));
|
final asStr = String.fromCharCodes(signatureImageBytes.take(8));
|
||||||
|
@ -253,54 +232,22 @@ class ExportService {
|
||||||
color: pdf.PdfColors.white,
|
color: pdf.PdfColors.white,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
// Multi-placement stamping on fallback page
|
if (sigImgObj != null) {
|
||||||
if (hasMulti && pagePlacements.isNotEmpty) {
|
if (hasMulti && pagePlacements.isNotEmpty) {
|
||||||
for (var i = 0; i < pagePlacements.length; i++) {
|
for (final r in pagePlacements) {
|
||||||
final r = pagePlacements[i];
|
|
||||||
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;
|
|
||||||
if (i < pageImageIds.length) {
|
|
||||||
final id = pageImageIds[i];
|
|
||||||
bytes = libraryBytes?[id];
|
|
||||||
}
|
|
||||||
bytes ??=
|
|
||||||
signatureImageBytes; // fallback to single image if provided
|
|
||||||
if (bytes != null && bytes.isNotEmpty) {
|
|
||||||
pw.MemoryImage? imgObj;
|
|
||||||
try {
|
|
||||||
// Ensure PNG for transparency if not already
|
|
||||||
final asStr = String.fromCharCodes(bytes.take(8));
|
|
||||||
final isPng =
|
|
||||||
bytes.length > 8 &&
|
|
||||||
bytes[0] == 0x89 &&
|
|
||||||
asStr.startsWith('\u0089PNG');
|
|
||||||
if (isPng) {
|
|
||||||
imgObj = pw.MemoryImage(bytes);
|
|
||||||
} else {
|
|
||||||
final decoded = img.decodeImage(bytes);
|
|
||||||
if (decoded != null) {
|
|
||||||
final png = img.encodePng(decoded, level: 6);
|
|
||||||
imgObj = pw.MemoryImage(Uint8List.fromList(png));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
imgObj = null;
|
|
||||||
}
|
|
||||||
if (imgObj != null) {
|
|
||||||
children.add(
|
children.add(
|
||||||
pw.Positioned(
|
pw.Positioned(
|
||||||
left: left,
|
left: left,
|
||||||
top: top,
|
top: top,
|
||||||
child: pw.Image(imgObj, width: w, height: h),
|
child: pw.Image(sigImgObj, width: w, height: h),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
} else if (shouldStampSingle) {
|
||||||
}
|
|
||||||
} else if (shouldStampSingle && sigImgObj != null) {
|
|
||||||
final r = signatureRectUi;
|
final r = signatureRectUi;
|
||||||
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;
|
||||||
|
@ -314,6 +261,7 @@ class ExportService {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return pw.Stack(children: children);
|
return pw.Stack(children: children);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -169,44 +169,6 @@ final pdfProvider = StateNotifierProvider<PdfController, PdfState>(
|
||||||
(ref) => PdfController(),
|
(ref) => PdfController(),
|
||||||
);
|
);
|
||||||
|
|
||||||
/// A simple library of signature images available to the user in the sidebar.
|
|
||||||
class SignatureAsset {
|
|
||||||
final String id; // unique id
|
|
||||||
final Uint8List bytes;
|
|
||||||
final String? name; // optional display name (e.g., filename)
|
|
||||||
const SignatureAsset({required this.id, required this.bytes, this.name});
|
|
||||||
}
|
|
||||||
|
|
||||||
class SignatureLibraryController extends StateNotifier<List<SignatureAsset>> {
|
|
||||||
SignatureLibraryController() : super(const []);
|
|
||||||
|
|
||||||
String add(Uint8List bytes, {String? name}) {
|
|
||||||
// Always add a new asset (allow duplicates). This lets users create multiple cards
|
|
||||||
// even when loading the same image repeatedly for different adjustments/usages.
|
|
||||||
if (bytes.isEmpty) return '';
|
|
||||||
final id = DateTime.now().microsecondsSinceEpoch.toString();
|
|
||||||
state = List.of(state)
|
|
||||||
..add(SignatureAsset(id: id, bytes: bytes, name: name));
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
void remove(String id) {
|
|
||||||
state = state.where((a) => a.id != id).toList(growable: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
SignatureAsset? byId(String id) {
|
|
||||||
for (final a in state) {
|
|
||||||
if (a.id == id) return a;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final signatureLibraryProvider =
|
|
||||||
StateNotifierProvider<SignatureLibraryController, List<SignatureAsset>>(
|
|
||||||
(ref) => SignatureLibraryController(),
|
|
||||||
);
|
|
||||||
|
|
||||||
class SignatureController extends StateNotifier<SignatureState> {
|
class SignatureController extends StateNotifier<SignatureState> {
|
||||||
SignatureController() : super(SignatureState.initial());
|
SignatureController() : super(SignatureState.initial());
|
||||||
static const Size pageSize = Size(400, 560);
|
static const Size pageSize = Size(400, 560);
|
||||||
|
@ -329,7 +291,7 @@ class SignatureController extends StateNotifier<SignatureState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void setImageBytes(Uint8List bytes) {
|
void setImageBytes(Uint8List bytes) {
|
||||||
state = state.copyWith(imageBytes: bytes, assetId: null);
|
state = state.copyWith(imageBytes: bytes);
|
||||||
if (state.rect == null) {
|
if (state.rect == null) {
|
||||||
placeDefaultRect();
|
placeDefaultRect();
|
||||||
}
|
}
|
||||||
|
@ -337,15 +299,6 @@ class SignatureController extends StateNotifier<SignatureState> {
|
||||||
state = state.copyWith(editingEnabled: true);
|
state = state.copyWith(editingEnabled: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select image from the shared signature library
|
|
||||||
void setImageFromLibrary({required String assetId}) {
|
|
||||||
state = state.copyWith(assetId: assetId);
|
|
||||||
if (state.rect == null) {
|
|
||||||
placeDefaultRect();
|
|
||||||
}
|
|
||||||
state = state.copyWith(editingEnabled: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearImage() {
|
void clearImage() {
|
||||||
state = state.copyWith(imageBytes: null, rect: null, editingEnabled: false);
|
state = state.copyWith(imageBytes: null, rect: null, editingEnabled: false);
|
||||||
}
|
}
|
||||||
|
@ -365,25 +318,6 @@ class SignatureController extends StateNotifier<SignatureState> {
|
||||||
final pdf = ref.read(pdfProvider);
|
final pdf = ref.read(pdfProvider);
|
||||||
if (!pdf.loaded) return null;
|
if (!pdf.loaded) return null;
|
||||||
ref.read(pdfProvider.notifier).addPlacement(page: pdf.currentPage, rect: r);
|
ref.read(pdfProvider.notifier).addPlacement(page: pdf.currentPage, rect: r);
|
||||||
// Assign image id to this placement (last index)
|
|
||||||
final idx =
|
|
||||||
(ref.read(pdfProvider).placementsByPage[pdf.currentPage]?.length ?? 1) -
|
|
||||||
1;
|
|
||||||
String? id = state.assetId;
|
|
||||||
if (id == null) {
|
|
||||||
final bytes =
|
|
||||||
ref.read(processedSignatureImageProvider) ?? state.imageBytes;
|
|
||||||
if (bytes != null) {
|
|
||||||
id = ref
|
|
||||||
.read(signatureLibraryProvider.notifier)
|
|
||||||
.add(bytes, name: 'image');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (id != null && id.isNotEmpty && idx >= 0) {
|
|
||||||
ref
|
|
||||||
.read(pdfProvider.notifier)
|
|
||||||
.assignImageToPlacement(page: pdf.currentPage, index: idx, image: id);
|
|
||||||
}
|
|
||||||
// Freeze editing: keep rect for preview but disable interaction
|
// Freeze editing: keep rect for preview but disable interaction
|
||||||
state = state.copyWith(editingEnabled: false);
|
state = state.copyWith(editingEnabled: false);
|
||||||
return r;
|
return r;
|
||||||
|
@ -405,19 +339,7 @@ final signatureProvider =
|
||||||
/// Returns null if no image is loaded. The output is a PNG to preserve alpha.
|
/// Returns null if no image is loaded. The output is a PNG to preserve alpha.
|
||||||
final processedSignatureImageProvider = Provider<Uint8List?>((ref) {
|
final processedSignatureImageProvider = Provider<Uint8List?>((ref) {
|
||||||
final s = ref.watch(signatureProvider);
|
final s = ref.watch(signatureProvider);
|
||||||
// If active overlay is based on a library asset, pull its bytes
|
final bytes = s.imageBytes;
|
||||||
Uint8List? bytes;
|
|
||||||
if (s.assetId != null) {
|
|
||||||
final lib = ref.watch(signatureLibraryProvider);
|
|
||||||
for (final a in lib) {
|
|
||||||
if (a.id == s.assetId) {
|
|
||||||
bytes = a.bytes;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bytes = s.imageBytes;
|
|
||||||
}
|
|
||||||
if (bytes == null || bytes.isEmpty) return null;
|
if (bytes == null || bytes.isEmpty) return null;
|
||||||
|
|
||||||
// Decode (supports PNG/JPEG, etc.)
|
// Decode (supports PNG/JPEG, etc.)
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'pdf_pages_overview.dart';
|
|
||||||
|
|
||||||
class PagesSidebar extends StatelessWidget {
|
|
||||||
const PagesSidebar({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Card(margin: EdgeInsets.zero, child: const PdfPagesOverview());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +1,15 @@
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
|
||||||
import 'package:flutter/material.dart';
|
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:pdfrx/pdfrx.dart';
|
import 'package:pdfrx/pdfrx.dart';
|
||||||
|
|
||||||
import '../../../../data/services/export_providers.dart';
|
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 '../../../../data/services/preferences_providers.dart';
|
import '../../preferences/providers.dart';
|
||||||
import 'signature_drag_data.dart';
|
import 'signature_drawer.dart';
|
||||||
import 'image_editor_dialog.dart';
|
import 'image_editor_dialog.dart';
|
||||||
|
|
||||||
class PdfPageArea extends ConsumerStatefulWidget {
|
class PdfPageArea extends ConsumerStatefulWidget {
|
||||||
|
@ -333,10 +332,25 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
|
||||||
final target = _pendingPage ?? pdf.currentPage;
|
final target = _pendingPage ?? pdf.currentPage;
|
||||||
_pendingPage = null;
|
_pendingPage = null;
|
||||||
_scrollRetryCount = 0;
|
_scrollRetryCount = 0;
|
||||||
// Defer navigation to the next frame to ensure controller state is fully ready.
|
_programmaticTargetPage = target;
|
||||||
|
controller.goToPage(pageNumber: target, anchor: PdfPageAnchor.top);
|
||||||
|
// Fallback: if the viewer doesn't emit onPageChanged (e.g., already at target),
|
||||||
|
// ensure we don't keep blocking provider-driven jumps.
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
_scrollToPage(target);
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (_programmaticTargetPage == target) {
|
||||||
|
_programmaticTargetPage = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Also ensure a scroll attempt is queued in case current state suppressed earlier.
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (_visiblePage != ref.read(pdfProvider).currentPage) {
|
||||||
|
_scrollToPage(ref.read(pdfProvider).currentPage);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onPageChanged: (n) {
|
onPageChanged: (n) {
|
||||||
|
@ -379,13 +393,6 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
|
||||||
// Assume drop targets the current visible page; compute relative center
|
// Assume drop targets the current visible page; compute relative center
|
||||||
final cx = (local.dx / size.width) * widget.pageSize.width;
|
final cx = (local.dx / size.width) * widget.pageSize.width;
|
||||||
final cy = (local.dy / size.height) * widget.pageSize.height;
|
final cy = (local.dy / size.height) * widget.pageSize.height;
|
||||||
final data = details.data;
|
|
||||||
if (data is SignatureDragData && data.assetId != null) {
|
|
||||||
// Set current overlay to use this asset
|
|
||||||
ref
|
|
||||||
.read(signatureProvider.notifier)
|
|
||||||
.setImageFromLibrary(assetId: data.assetId!);
|
|
||||||
}
|
|
||||||
ref.read(signatureProvider.notifier).placeAtCenter(Offset(cx, cy));
|
ref.read(signatureProvider.notifier).placeAtCenter(Offset(cx, cy));
|
||||||
ref
|
ref
|
||||||
.read(pdfProvider.notifier)
|
.read(pdfProvider.notifier)
|
||||||
|
@ -551,32 +558,10 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
|
||||||
children: [
|
children: [
|
||||||
Consumer(
|
Consumer(
|
||||||
builder: (context, ref, _) {
|
builder: (context, ref, _) {
|
||||||
Uint8List? bytes;
|
|
||||||
if (interactive) {
|
|
||||||
final processed = ref.watch(
|
final processed = ref.watch(
|
||||||
processedSignatureImageProvider,
|
processedSignatureImageProvider,
|
||||||
);
|
);
|
||||||
bytes = processed ?? sig.imageBytes;
|
final bytes = processed ?? sig.imageBytes;
|
||||||
} else if (placedIndex != null) {
|
|
||||||
// Use the image assigned to this placement
|
|
||||||
final imgId = ref
|
|
||||||
.read(pdfProvider)
|
|
||||||
.placementImageByPage[pageNumber]
|
|
||||||
?.elementAt(placedIndex);
|
|
||||||
if (imgId != null) {
|
|
||||||
final lib = ref.watch(signatureLibraryProvider);
|
|
||||||
for (final a in lib) {
|
|
||||||
if (a.id == imgId) {
|
|
||||||
bytes = a.bytes;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Fallback to current processed
|
|
||||||
bytes ??=
|
|
||||||
ref.read(processedSignatureImageProvider) ??
|
|
||||||
sig.imageBytes;
|
|
||||||
}
|
|
||||||
if (bytes == null) {
|
if (bytes == null) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:pdfrx/pdfrx.dart';
|
import 'package:pdfrx/pdfrx.dart';
|
||||||
|
|
||||||
import '../../../../data/services/export_providers.dart';
|
import '../../../../data/services/providers.dart';
|
||||||
import '../view_model/view_model.dart';
|
import '../view_model/view_model.dart';
|
||||||
|
|
||||||
class PdfPagesOverview extends ConsumerWidget {
|
class PdfPagesOverview extends ConsumerWidget {
|
||||||
|
|
|
@ -6,15 +6,15 @@ 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 'package:pdfrx/pdfrx.dart';
|
||||||
import 'package:multi_split_view/multi_split_view.dart';
|
|
||||||
|
|
||||||
import '../../../../data/services/export_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 'pages_sidebar.dart';
|
import 'pdf_pages_overview.dart';
|
||||||
import 'signatures_sidebar.dart';
|
import 'signature_drawer.dart';
|
||||||
|
// adjustments are available via ImageEditorDialog
|
||||||
|
|
||||||
class PdfSignatureHomePage extends ConsumerStatefulWidget {
|
class PdfSignatureHomePage extends ConsumerStatefulWidget {
|
||||||
const PdfSignatureHomePage({super.key});
|
const PdfSignatureHomePage({super.key});
|
||||||
|
@ -31,17 +31,6 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
bool _showSignaturesSidebar = true;
|
bool _showSignaturesSidebar = true;
|
||||||
int _zoomLevel = 100; // percentage for display only
|
int _zoomLevel = 100; // percentage for display only
|
||||||
|
|
||||||
// Split view controller to manage resizable sidebars without remounting the center area.
|
|
||||||
late final MultiSplitViewController _splitController;
|
|
||||||
late final List<Area> _areas;
|
|
||||||
double _lastPagesWidth = 160;
|
|
||||||
double _lastSignaturesWidth = 220;
|
|
||||||
// Configurable sidebar constraints
|
|
||||||
final double _pagesMin = 100;
|
|
||||||
final double _pagesMax = 250;
|
|
||||||
final double _signaturesMin = 140;
|
|
||||||
final double _signaturesMax = 250;
|
|
||||||
|
|
||||||
// Exposed for tests to trigger the invalid-file SnackBar without UI.
|
// Exposed for tests to trigger the invalid-file SnackBar without UI.
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
void debugShowInvalidSignatureSnackBar() {
|
void debugShowInvalidSignatureSnackBar() {
|
||||||
|
@ -67,13 +56,15 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
ref.read(pdfProvider.notifier).jumpTo(page);
|
ref.read(pdfProvider.notifier).jumpTo(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uint8List?> _loadSignatureFromFile() async {
|
// Zoom is managed by pdfrx viewer (Ctrl +/- etc.). No custom zoom here.
|
||||||
|
|
||||||
|
Future<void> _loadSignatureFromFile() async {
|
||||||
final typeGroup = const fs.XTypeGroup(
|
final typeGroup = const fs.XTypeGroup(
|
||||||
label: 'Image',
|
label: 'Image',
|
||||||
extensions: ['png', 'jpg', 'jpeg', 'webp'],
|
extensions: ['png', 'jpg', 'jpeg', 'webp'],
|
||||||
);
|
);
|
||||||
final file = await fs.openFile(acceptedTypeGroups: [typeGroup]);
|
final file = await fs.openFile(acceptedTypeGroups: [typeGroup]);
|
||||||
if (file == null) return null;
|
if (file == null) return;
|
||||||
final bytes = await file.readAsBytes();
|
final bytes = await file.readAsBytes();
|
||||||
final sig = ref.read(signatureProvider.notifier);
|
final sig = ref.read(signatureProvider.notifier);
|
||||||
sig.setImageBytes(bytes);
|
sig.setImageBytes(bytes);
|
||||||
|
@ -81,9 +72,10 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
if (p.loaded) {
|
if (p.loaded) {
|
||||||
ref.read(pdfProvider.notifier).setSignedPage(p.currentPage);
|
ref.read(pdfProvider.notifier).setSignedPage(p.currentPage);
|
||||||
}
|
}
|
||||||
return bytes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// _createNewSignature was removed as the toolbar no longer exposes this action.
|
||||||
|
|
||||||
void _confirmSignature() {
|
void _confirmSignature() {
|
||||||
ref.read(signatureProvider.notifier).confirmCurrentSignature(ref);
|
ref.read(signatureProvider.notifier).confirmCurrentSignature(ref);
|
||||||
}
|
}
|
||||||
|
@ -100,7 +92,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
ref.read(pdfProvider.notifier).selectPlacement(index);
|
ref.read(pdfProvider.notifier).selectPlacement(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uint8List?> _openDrawCanvas() async {
|
Future<void> _openDrawCanvas() async {
|
||||||
final result = await showModalBottomSheet<Uint8List>(
|
final result = await showModalBottomSheet<Uint8List>(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
|
@ -114,7 +106,6 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
ref.read(pdfProvider.notifier).setSignedPage(p.currentPage);
|
ref.read(pdfProvider.notifier).setSignedPage(p.currentPage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _saveSignedPdf() async {
|
Future<void> _saveSignedPdf() async {
|
||||||
|
@ -147,10 +138,6 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
uiPageSize: SignatureController.pageSize,
|
uiPageSize: SignatureController.pageSize,
|
||||||
signatureImageBytes: processed ?? sig.imageBytes,
|
signatureImageBytes: processed ?? sig.imageBytes,
|
||||||
placementsByPage: pdf.placementsByPage,
|
placementsByPage: pdf.placementsByPage,
|
||||||
placementImageByPage: pdf.placementImageByPage,
|
|
||||||
libraryBytes: {
|
|
||||||
for (final a in ref.read(signatureLibraryProvider)) a.id: a.bytes,
|
|
||||||
},
|
|
||||||
targetDpi: targetDpi,
|
targetDpi: targetDpi,
|
||||||
);
|
);
|
||||||
if (bytes != null) {
|
if (bytes != null) {
|
||||||
|
@ -180,10 +167,6 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
uiPageSize: SignatureController.pageSize,
|
uiPageSize: SignatureController.pageSize,
|
||||||
signatureImageBytes: processed ?? sig.imageBytes,
|
signatureImageBytes: processed ?? sig.imageBytes,
|
||||||
placementsByPage: pdf.placementsByPage,
|
placementsByPage: pdf.placementsByPage,
|
||||||
placementImageByPage: pdf.placementImageByPage,
|
|
||||||
libraryBytes: {
|
|
||||||
for (final a in ref.read(signatureLibraryProvider)) a.id: a.bytes,
|
|
||||||
},
|
|
||||||
targetDpi: targetDpi,
|
targetDpi: targetDpi,
|
||||||
);
|
);
|
||||||
if (useMock) {
|
if (useMock) {
|
||||||
|
@ -207,11 +190,6 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
uiPageSize: SignatureController.pageSize,
|
uiPageSize: SignatureController.pageSize,
|
||||||
signatureImageBytes: processed ?? sig.imageBytes,
|
signatureImageBytes: processed ?? sig.imageBytes,
|
||||||
placementsByPage: pdf.placementsByPage,
|
placementsByPage: pdf.placementsByPage,
|
||||||
placementImageByPage: pdf.placementImageByPage,
|
|
||||||
libraryBytes: {
|
|
||||||
for (final a in ref.read(signatureLibraryProvider))
|
|
||||||
a.id: a.bytes,
|
|
||||||
},
|
|
||||||
targetDpi: targetDpi,
|
targetDpi: targetDpi,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -258,94 +236,11 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
// Build areas once with builders; keep these instances stable.
|
|
||||||
_areas = [
|
|
||||||
Area(
|
|
||||||
size: _lastPagesWidth,
|
|
||||||
min: _pagesMin,
|
|
||||||
max: _pagesMax,
|
|
||||||
builder:
|
|
||||||
(context, area) => Offstage(
|
|
||||||
offstage: !_showPagesSidebar,
|
|
||||||
child: const PagesSidebar(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Area(
|
|
||||||
flex: 1,
|
|
||||||
builder:
|
|
||||||
(context, area) => RepaintBoundary(
|
|
||||||
child: PdfPageArea(
|
|
||||||
key: const ValueKey('pdf_page_area'),
|
|
||||||
pageSize: _pageSize,
|
|
||||||
viewerController: _viewerController,
|
|
||||||
onDragSignature: _onDragSignature,
|
|
||||||
onResizeSignature: _onResizeSignature,
|
|
||||||
onConfirmSignature: _confirmSignature,
|
|
||||||
onClearActiveOverlay:
|
|
||||||
() =>
|
|
||||||
ref
|
|
||||||
.read(signatureProvider.notifier)
|
|
||||||
.clearActiveOverlay(),
|
|
||||||
onSelectPlaced: _onSelectPlaced,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Area(
|
|
||||||
size: _lastSignaturesWidth,
|
|
||||||
min: _signaturesMin,
|
|
||||||
max: _signaturesMax,
|
|
||||||
builder:
|
|
||||||
(context, area) => Offstage(
|
|
||||||
offstage: !_showSignaturesSidebar,
|
|
||||||
child: SignaturesSidebar(
|
|
||||||
onLoadSignatureFromFile: _loadSignatureFromFile,
|
|
||||||
onOpenDrawCanvas: _openDrawCanvas,
|
|
||||||
onSave: _saveSignedPdf,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
_splitController = MultiSplitViewController(areas: _areas);
|
|
||||||
// Apply initial collapse if needed
|
|
||||||
_applySidebarVisibility();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_splitController.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _applySidebarVisibility() {
|
|
||||||
// Left pages sidebar
|
|
||||||
final left = _splitController.areas[0];
|
|
||||||
if (_showPagesSidebar) {
|
|
||||||
left.max = _pagesMax;
|
|
||||||
left.min = _pagesMin;
|
|
||||||
left.size = _lastPagesWidth.clamp(_pagesMin, _pagesMax);
|
|
||||||
} else {
|
|
||||||
_lastPagesWidth = left.size ?? _lastPagesWidth;
|
|
||||||
left.min = 0;
|
|
||||||
left.max = 1;
|
|
||||||
left.size = 1; // effectively hidden
|
|
||||||
}
|
|
||||||
// Right signatures sidebar
|
|
||||||
final right = _splitController.areas[2];
|
|
||||||
if (_showSignaturesSidebar) {
|
|
||||||
right.max = _signaturesMax;
|
|
||||||
right.min = _signaturesMin;
|
|
||||||
right.size = _lastSignaturesWidth.clamp(_signaturesMin, _signaturesMax);
|
|
||||||
} else {
|
|
||||||
_lastSignaturesWidth = right.size ?? _lastSignaturesWidth;
|
|
||||||
right.min = 0;
|
|
||||||
right.max = 1;
|
|
||||||
right.size = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isExporting = ref.watch(exportingProvider);
|
final isExporting = ref.watch(exportingProvider);
|
||||||
|
@ -378,26 +273,93 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
_zoomLevel = (_zoomLevel + 10).clamp(10, 800);
|
_zoomLevel = (_zoomLevel + 10).clamp(10, 800);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
zoomLevel: _zoomLevel,
|
// zoomLevel omitted to avoid compact overflows in tight tests
|
||||||
fileName: ref.watch(pdfProvider).pickedPdfPath,
|
fileName: ref.watch(pdfProvider).pickedPdfPath,
|
||||||
showPagesSidebar: _showPagesSidebar,
|
showPagesSidebar: _showPagesSidebar,
|
||||||
showSignaturesSidebar: _showSignaturesSidebar,
|
showSignaturesSidebar: _showSignaturesSidebar,
|
||||||
onTogglePagesSidebar:
|
onTogglePagesSidebar:
|
||||||
() => setState(() {
|
() => setState(() {
|
||||||
_showPagesSidebar = !_showPagesSidebar;
|
_showPagesSidebar = !_showPagesSidebar;
|
||||||
_applySidebarVisibility();
|
|
||||||
}),
|
}),
|
||||||
onToggleSignaturesSidebar:
|
onToggleSignaturesSidebar:
|
||||||
() => setState(() {
|
() => setState(() {
|
||||||
_showSignaturesSidebar = !_showSignaturesSidebar;
|
_showSignaturesSidebar = !_showSignaturesSidebar;
|
||||||
_applySidebarVisibility();
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: MultiSplitView(
|
child: Row(
|
||||||
controller: _splitController,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
axis: Axis.horizontal,
|
children: [
|
||||||
|
if (_showPagesSidebar)
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 140,
|
||||||
|
maxWidth: 180,
|
||||||
|
),
|
||||||
|
child: Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: const PdfPagesOverview(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_showPagesSidebar) const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: AbsorbPointer(
|
||||||
|
absorbing: isExporting,
|
||||||
|
child: PdfPageArea(
|
||||||
|
pageSize: _pageSize,
|
||||||
|
viewerController: _viewerController,
|
||||||
|
onDragSignature: _onDragSignature,
|
||||||
|
onResizeSignature: _onResizeSignature,
|
||||||
|
onConfirmSignature: _confirmSignature,
|
||||||
|
onClearActiveOverlay:
|
||||||
|
() =>
|
||||||
|
ref
|
||||||
|
.read(signatureProvider.notifier)
|
||||||
|
.clearActiveOverlay(),
|
||||||
|
onSelectPlaced: _onSelectPlaced,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_showSignaturesSidebar) const SizedBox(width: 12),
|
||||||
|
if (_showSignaturesSidebar)
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 140,
|
||||||
|
maxWidth: 250,
|
||||||
|
),
|
||||||
|
child: AbsorbPointer(
|
||||||
|
absorbing: isExporting,
|
||||||
|
child: Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: SignatureDrawer(
|
||||||
|
disabled: isExporting,
|
||||||
|
onLoadSignatureFromFile:
|
||||||
|
_loadSignatureFromFile,
|
||||||
|
onOpenDrawCanvas: _openDrawCanvas,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: ElevatedButton(
|
||||||
|
key: const Key('btn_save_pdf'),
|
||||||
|
onPressed:
|
||||||
|
isExporting ? null : _saveSignedPdf,
|
||||||
|
child: Text(l.saveSignedPdf),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'package:flutter/services.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 '../../../../data/services/export_providers.dart';
|
import '../../../../data/services/providers.dart';
|
||||||
import '../view_model/view_model.dart';
|
import '../view_model/view_model.dart';
|
||||||
|
|
||||||
class PdfToolbar extends ConsumerStatefulWidget {
|
class PdfToolbar extends ConsumerStatefulWidget {
|
||||||
|
@ -94,8 +94,8 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (pdf.loaded) ...[
|
if (pdf.loaded) ...[
|
||||||
Wrap(
|
Row(
|
||||||
spacing: 8,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
key: const Key('btn_prev'),
|
key: const Key('btn_prev'),
|
||||||
|
@ -156,19 +156,25 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
onPressed: widget.disabled ? null : widget.onZoomOut,
|
onPressed: widget.disabled ? null : widget.onZoomOut,
|
||||||
icon: const Icon(Icons.zoom_out),
|
icon: const Icon(Icons.zoom_out),
|
||||||
),
|
),
|
||||||
Text(
|
|
||||||
//if not null
|
|
||||||
widget.zoomLevel != null ? '${widget.zoomLevel}%' : '',
|
|
||||||
style: const TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
key: const Key('btn_zoom_in'),
|
key: const Key('btn_zoom_in'),
|
||||||
tooltip: 'Zoom in',
|
tooltip: 'Zoom in',
|
||||||
onPressed: widget.disabled ? null : widget.onZoomIn,
|
onPressed: widget.disabled ? null : widget.onZoomIn,
|
||||||
icon: const Icon(Icons.zoom_in),
|
icon: const Icon(Icons.zoom_in),
|
||||||
),
|
),
|
||||||
SizedBox(width: 6),
|
if (!compact && widget.zoomLevel != null) ...[
|
||||||
|
const SizedBox(width: 6),
|
||||||
// show zoom ratio
|
// show zoom ratio
|
||||||
|
Text(
|
||||||
|
'${widget.zoomLevel}%',
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
Text(l.dpi),
|
Text(l.dpi),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
DropdownButton<double>(
|
DropdownButton<double>(
|
||||||
|
|
|
@ -1,153 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import '../view_model/view_model.dart';
|
|
||||||
import 'signature_drag_data.dart';
|
|
||||||
|
|
||||||
class SignatureCard extends StatelessWidget {
|
|
||||||
const SignatureCard({
|
|
||||||
super.key,
|
|
||||||
required this.asset,
|
|
||||||
required this.disabled,
|
|
||||||
required this.onDelete,
|
|
||||||
this.onTap,
|
|
||||||
this.onAdjust,
|
|
||||||
this.useCurrentBytesForDrag = false,
|
|
||||||
});
|
|
||||||
final SignatureAsset asset;
|
|
||||||
final bool disabled;
|
|
||||||
final VoidCallback onDelete;
|
|
||||||
final VoidCallback? onTap;
|
|
||||||
final VoidCallback? onAdjust;
|
|
||||||
final bool useCurrentBytesForDrag;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final img = Image.memory(asset.bytes, fit: BoxFit.contain);
|
|
||||||
Widget base = SizedBox(
|
|
||||||
width: 96,
|
|
||||||
height: 64,
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Positioned.fill(
|
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: Theme.of(context).dividerColor),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Padding(padding: const EdgeInsets.all(6), child: img),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
right: 0,
|
|
||||||
top: 0,
|
|
||||||
child: IconButton(
|
|
||||||
icon: const Icon(Icons.close, size: 16),
|
|
||||||
onPressed: disabled ? null : onDelete,
|
|
||||||
tooltip: 'Remove',
|
|
||||||
padding: const EdgeInsets.all(2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Widget child = onTap != null ? InkWell(onTap: onTap, child: base) : base;
|
|
||||||
// Add context menu for adjust/delete on right-click or long-press
|
|
||||||
child = GestureDetector(
|
|
||||||
key: const Key('gd_signature_card_area'),
|
|
||||||
behavior: HitTestBehavior.opaque,
|
|
||||||
onSecondaryTapDown:
|
|
||||||
disabled
|
|
||||||
? null
|
|
||||||
: (details) async {
|
|
||||||
final selected = await showMenu<String>(
|
|
||||||
context: context,
|
|
||||||
position: RelativeRect.fromLTRB(
|
|
||||||
details.globalPosition.dx,
|
|
||||||
details.globalPosition.dy,
|
|
||||||
details.globalPosition.dx,
|
|
||||||
details.globalPosition.dy,
|
|
||||||
),
|
|
||||||
items: const [
|
|
||||||
PopupMenuItem(
|
|
||||||
key: Key('mi_signature_adjust'),
|
|
||||||
value: 'adjust',
|
|
||||||
child: Text('Adjust graphic'),
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
key: Key('mi_signature_delete'),
|
|
||||||
value: 'delete',
|
|
||||||
child: Text('Delete'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
if (selected == 'adjust') {
|
|
||||||
onAdjust?.call();
|
|
||||||
} else if (selected == 'delete') {
|
|
||||||
onDelete();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongPressStart:
|
|
||||||
disabled
|
|
||||||
? null
|
|
||||||
: (details) async {
|
|
||||||
final selected = await showMenu<String>(
|
|
||||||
context: context,
|
|
||||||
position: RelativeRect.fromLTRB(
|
|
||||||
details.globalPosition.dx,
|
|
||||||
details.globalPosition.dy,
|
|
||||||
details.globalPosition.dx,
|
|
||||||
details.globalPosition.dy,
|
|
||||||
),
|
|
||||||
items: const [
|
|
||||||
PopupMenuItem(
|
|
||||||
key: Key('mi_signature_adjust'),
|
|
||||||
value: 'adjust',
|
|
||||||
child: Text('Adjust graphic'),
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
key: Key('mi_signature_delete'),
|
|
||||||
value: 'delete',
|
|
||||||
child: Text('Delete'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
if (selected == 'adjust') {
|
|
||||||
onAdjust?.call();
|
|
||||||
} else if (selected == 'delete') {
|
|
||||||
onDelete();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
if (disabled) return child;
|
|
||||||
return Draggable<SignatureDragData>(
|
|
||||||
data:
|
|
||||||
useCurrentBytesForDrag
|
|
||||||
? const SignatureDragData()
|
|
||||||
: SignatureDragData(assetId: asset.id),
|
|
||||||
feedback: Opacity(
|
|
||||||
opacity: 0.9,
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints.tightFor(width: 160, height: 100),
|
|
||||||
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(asset.bytes, fit: BoxFit.contain),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
childWhenDragging: Opacity(opacity: 0.5, child: child),
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
class SignatureDragData {
|
|
||||||
final String? assetId; // null means use current processed signature
|
|
||||||
const SignatureDragData({this.assetId});
|
|
||||||
}
|
|
|
@ -3,12 +3,14 @@ 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 '../../../../data/services/export_providers.dart';
|
import '../../../../data/services/providers.dart';
|
||||||
import '../view_model/view_model.dart';
|
import '../view_model/view_model.dart';
|
||||||
import 'image_editor_dialog.dart';
|
import 'image_editor_dialog.dart';
|
||||||
import 'signature_card.dart';
|
|
||||||
|
|
||||||
/// Data for drag-and-drop is in signature_drag_data.dart
|
/// Data passed when dragging a signature card.
|
||||||
|
class SignatureDragData {
|
||||||
|
const SignatureDragData();
|
||||||
|
}
|
||||||
|
|
||||||
class SignatureDrawer extends ConsumerStatefulWidget {
|
class SignatureDrawer extends ConsumerStatefulWidget {
|
||||||
const SignatureDrawer({
|
const SignatureDrawer({
|
||||||
|
@ -19,107 +21,121 @@ class SignatureDrawer extends ConsumerStatefulWidget {
|
||||||
});
|
});
|
||||||
|
|
||||||
final bool disabled;
|
final bool disabled;
|
||||||
// Return the loaded bytes (if any) so we can add the exact image to the library immediately.
|
final VoidCallback onLoadSignatureFromFile;
|
||||||
final Future<Uint8List?> Function() onLoadSignatureFromFile;
|
final VoidCallback onOpenDrawCanvas;
|
||||||
// Return the drawn bytes (if any) so we can add it to the library immediately.
|
|
||||||
final Future<Uint8List?> Function() onOpenDrawCanvas;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<SignatureDrawer> createState() => _SignatureDrawerState();
|
ConsumerState<SignatureDrawer> createState() => _SignatureDrawerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
|
class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
|
||||||
|
Future<void> _openSignatureMenuAt(Offset globalPosition) async {
|
||||||
|
final l = AppLocalizations.of(context);
|
||||||
|
final selected = await showMenu<String>(
|
||||||
|
context: context,
|
||||||
|
position: RelativeRect.fromLTRB(
|
||||||
|
globalPosition.dx,
|
||||||
|
globalPosition.dy,
|
||||||
|
globalPosition.dx,
|
||||||
|
globalPosition.dy,
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
PopupMenuItem(
|
||||||
|
key: const Key('mi_signature_delete'),
|
||||||
|
value: 'delete',
|
||||||
|
child: Text(l.delete),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
key: const Key('mi_signature_adjust'),
|
||||||
|
value: 'adjust',
|
||||||
|
child: const Text('Adjust graphic'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (selected) {
|
||||||
|
case 'delete':
|
||||||
|
ref.read(signatureProvider.notifier).clearActiveOverlay();
|
||||||
|
ref.read(signatureProvider.notifier).clearImage();
|
||||||
|
break;
|
||||||
|
case 'adjust':
|
||||||
|
if (!mounted) return;
|
||||||
|
// Open ImageEditorDialog
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const ImageEditorDialog(),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l = AppLocalizations.of(context);
|
final l = AppLocalizations.of(context);
|
||||||
final sig = ref.watch(signatureProvider);
|
final sig = ref.watch(signatureProvider);
|
||||||
final processed = ref.watch(processedSignatureImageProvider);
|
final processed = ref.watch(processedSignatureImageProvider);
|
||||||
final bytes = processed ?? sig.imageBytes;
|
final bytes = processed ?? sig.imageBytes;
|
||||||
final library = ref.watch(signatureLibraryProvider);
|
|
||||||
final isExporting = ref.watch(exportingProvider);
|
final isExporting = ref.watch(exportingProvider);
|
||||||
final disabled = widget.disabled || isExporting;
|
final disabled = widget.disabled || isExporting;
|
||||||
|
|
||||||
return Column(
|
return Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
if (library.isNotEmpty) ...[
|
// Header
|
||||||
for (final a in library) ...[
|
Padding(
|
||||||
Card(
|
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
|
||||||
margin: EdgeInsets.zero,
|
child: Text(
|
||||||
child: Padding(
|
l.signature,
|
||||||
padding: const EdgeInsets.all(12),
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
child: SignatureCard(
|
),
|
||||||
key: ValueKey('sig_card_${a.id}'),
|
),
|
||||||
asset: a,
|
// Existing signature card (draggable when bytes available)
|
||||||
disabled: disabled,
|
Padding(
|
||||||
onDelete:
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
() => ref
|
child: DecoratedBox(
|
||||||
.read(signatureLibraryProvider.notifier)
|
decoration: BoxDecoration(
|
||||||
.remove(a.id),
|
border: Border.all(color: Theme.of(context).dividerColor),
|
||||||
onAdjust: () async {
|
borderRadius: BorderRadius.circular(8),
|
||||||
ref
|
),
|
||||||
.read(signatureProvider.notifier)
|
child: GestureDetector(
|
||||||
.setImageFromLibrary(assetId: a.id);
|
key: const Key('gd_signature_card_area'),
|
||||||
if (!mounted) return;
|
behavior: HitTestBehavior.opaque,
|
||||||
await showDialog(
|
onSecondaryTapDown: (details) {
|
||||||
context: context,
|
if (bytes != null && !disabled) {
|
||||||
builder: (_) => const ImageEditorDialog(),
|
_openSignatureMenuAt(details.globalPosition);
|
||||||
);
|
|
||||||
},
|
|
||||||
onTap: () {
|
|
||||||
final sel = ref.read(pdfProvider).selectedPlacementIndex;
|
|
||||||
final page = ref.read(pdfProvider).currentPage;
|
|
||||||
if (sel != null) {
|
|
||||||
ref
|
|
||||||
.read(pdfProvider.notifier)
|
|
||||||
.assignImageToPlacement(
|
|
||||||
page: page,
|
|
||||||
index: sel,
|
|
||||||
image: a.id,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ref
|
|
||||||
.read(signatureProvider.notifier)
|
|
||||||
.setImageFromLibrary(assetId: a.id);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onLongPressStart: (details) {
|
||||||
|
if (bytes != null && !disabled) {
|
||||||
|
_openSignatureMenuAt(details.globalPosition);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: SizedBox(
|
||||||
|
height: 120,
|
||||||
|
child:
|
||||||
|
bytes == null
|
||||||
|
? Center(
|
||||||
|
child: Text(
|
||||||
|
l.noPdfLoaded,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: _DraggableSignaturePreview(
|
||||||
|
bytes: bytes,
|
||||||
|
disabled: disabled,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
],
|
const Divider(height: 1),
|
||||||
],
|
// New signature card
|
||||||
if (library.isEmpty)
|
Padding(
|
||||||
Card(
|
|
||||||
margin: EdgeInsets.zero,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
child:
|
|
||||||
bytes == null
|
|
||||||
? Text(l.noSignatureLoaded)
|
|
||||||
: SignatureCard(
|
|
||||||
asset: SignatureAsset(id: '', bytes: bytes, name: ''),
|
|
||||||
disabled: disabled,
|
|
||||||
useCurrentBytesForDrag: true,
|
|
||||||
onDelete: () {
|
|
||||||
ref
|
|
||||||
.read(signatureProvider.notifier)
|
|
||||||
.clearActiveOverlay();
|
|
||||||
ref.read(signatureProvider.notifier).clearImage();
|
|
||||||
},
|
|
||||||
onAdjust: () async {
|
|
||||||
if (!mounted) return;
|
|
||||||
await showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (_) => const ImageEditorDialog(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Card(
|
|
||||||
margin: EdgeInsets.zero,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
@ -136,41 +152,13 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
key: const Key('btn_drawer_load_signature'),
|
key: const Key('btn_drawer_load_signature'),
|
||||||
onPressed:
|
onPressed:
|
||||||
disabled
|
disabled ? null : widget.onLoadSignatureFromFile,
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
final loaded =
|
|
||||||
await widget.onLoadSignatureFromFile();
|
|
||||||
final b =
|
|
||||||
loaded ??
|
|
||||||
ref.read(processedSignatureImageProvider) ??
|
|
||||||
ref.read(signatureProvider).imageBytes;
|
|
||||||
if (b != null) {
|
|
||||||
ref
|
|
||||||
.read(signatureLibraryProvider.notifier)
|
|
||||||
.add(b, name: 'image');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.image_outlined),
|
icon: const Icon(Icons.image_outlined),
|
||||||
label: Text(l.loadSignatureFromFile),
|
label: Text(l.loadSignatureFromFile),
|
||||||
),
|
),
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
key: const Key('btn_drawer_draw_signature'),
|
key: const Key('btn_drawer_draw_signature'),
|
||||||
onPressed:
|
onPressed: disabled ? null : widget.onOpenDrawCanvas,
|
||||||
disabled
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
final drawn = await widget.onOpenDrawCanvas();
|
|
||||||
final b =
|
|
||||||
drawn ??
|
|
||||||
ref.read(processedSignatureImageProvider) ??
|
|
||||||
ref.read(signatureProvider).imageBytes;
|
|
||||||
if (b != null) {
|
|
||||||
ref
|
|
||||||
.read(signatureLibraryProvider.notifier)
|
|
||||||
.add(b, name: 'drawing');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.gesture),
|
icon: const Icon(Icons.gesture),
|
||||||
label: Text(l.drawSignature),
|
label: Text(l.drawSignature),
|
||||||
),
|
),
|
||||||
|
@ -179,8 +167,51 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
// Adjustments are accessed via "Adjust graphic" in the popup menu
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
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/export_providers.dart';
|
|
||||||
import 'signature_drawer.dart';
|
|
||||||
|
|
||||||
class SignaturesSidebar extends ConsumerWidget {
|
|
||||||
const SignaturesSidebar({
|
|
||||||
super.key,
|
|
||||||
required this.onLoadSignatureFromFile,
|
|
||||||
required this.onOpenDrawCanvas,
|
|
||||||
required this.onSave,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Future<Uint8List?> Function() onLoadSignatureFromFile;
|
|
||||||
final Future<Uint8List?> Function() onOpenDrawCanvas;
|
|
||||||
final VoidCallback onSave;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final l = AppLocalizations.of(context);
|
|
||||||
final isExporting = ref.watch(exportingProvider);
|
|
||||||
return AbsorbPointer(
|
|
||||||
absorbing: isExporting,
|
|
||||||
child: Card(
|
|
||||||
margin: EdgeInsets.zero,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: SignatureDrawer(
|
|
||||||
disabled: isExporting,
|
|
||||||
onLoadSignatureFromFile: onLoadSignatureFromFile,
|
|
||||||
onOpenDrawCanvas: onOpenDrawCanvas,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
child: ElevatedButton(
|
|
||||||
key: const Key('btn_save_pdf'),
|
|
||||||
onPressed: isExporting ? null : onSave,
|
|
||||||
child: Text(l.saveSignedPdf),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
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 '../../../../data/services/preferences_providers.dart';
|
import '../providers.dart';
|
||||||
|
|
||||||
class SettingsDialog extends ConsumerStatefulWidget {
|
class SettingsDialog extends ConsumerStatefulWidget {
|
||||||
const SettingsDialog({super.key});
|
const SettingsDialog({super.key});
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:ui';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.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';
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'package:pdf_signature/data/services/export_service.dart';
|
import 'package:pdf_signature/data/services/export_service.dart';
|
||||||
import 'package:pdf_signature/data/services/export_providers.dart';
|
import 'package:pdf_signature/data/services/providers.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/pdf/widgets/pdf_screen.dart';
|
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
|
||||||
import 'package:pdf_signature/l10n/app_localizations.dart';
|
import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||||
|
|
|
@ -6,9 +6,9 @@ import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
|
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/data/services/export_providers.dart';
|
import 'package:pdf_signature/data/services/providers.dart';
|
||||||
import 'package:pdf_signature/l10n/app_localizations.dart';
|
import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||||
import 'package:pdf_signature/data/services/preferences_providers.dart';
|
import 'package:pdf_signature/ui/features/preferences/providers.dart';
|
||||||
|
|
||||||
Future<void> pumpWithOpenPdf(WidgetTester tester) async {
|
Future<void> pumpWithOpenPdf(WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
|
|
|
@ -5,7 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
|
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/data/model/model.dart';
|
import 'package:pdf_signature/data/model/model.dart';
|
||||||
import 'package:pdf_signature/data/services/export_providers.dart';
|
import 'package:pdf_signature/data/services/providers.dart';
|
||||||
import 'package:pdf_signature/l10n/app_localizations.dart';
|
import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class _TestPdfController extends PdfController {
|
class _TestPdfController extends PdfController {
|
||||||
|
|
|
@ -4,10 +4,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_page_area.dart';
|
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_page_area.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/data/services/export_providers.dart';
|
import 'package:pdf_signature/data/services/providers.dart';
|
||||||
import 'package:pdf_signature/l10n/app_localizations.dart';
|
import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||||
import 'package:pdf_signature/data/model/model.dart';
|
import 'package:pdf_signature/data/model/model.dart';
|
||||||
import 'package:pdf_signature/data/services/preferences_providers.dart';
|
import 'package:pdf_signature/ui/features/preferences/providers.dart';
|
||||||
|
|
||||||
class _TestPdfController extends PdfController {
|
class _TestPdfController extends PdfController {
|
||||||
_TestPdfController() : super() {
|
_TestPdfController() : super() {
|
||||||
|
|
|
@ -4,10 +4,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_page_area.dart';
|
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_page_area.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/data/services/export_providers.dart';
|
import 'package:pdf_signature/data/services/providers.dart';
|
||||||
import 'package:pdf_signature/l10n/app_localizations.dart';
|
import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||||
import 'package:pdf_signature/data/model/model.dart';
|
import 'package:pdf_signature/data/model/model.dart';
|
||||||
import 'package:pdf_signature/data/services/preferences_providers.dart';
|
import 'package:pdf_signature/ui/features/preferences/providers.dart';
|
||||||
|
|
||||||
class _TestPdfController extends PdfController {
|
class _TestPdfController extends PdfController {
|
||||||
_TestPdfController() : super() {
|
_TestPdfController() : super() {
|
||||||
|
|
Loading…
Reference in New Issue