fix: pdfrx doesn't react to open another document due to fixed sourceName and viewerKey.

This commit is contained in:
insleker 2025-09-30 21:56:27 +08:00
parent 9e0ae1dcfe
commit 741decdae3
31 changed files with 548 additions and 153 deletions

View File

@ -39,7 +39,7 @@ void main() {
documentRepositoryProvider.overrideWith( documentRepositoryProvider.overrideWith(
(ref) => (ref) =>
DocumentStateNotifier(service: ExportService()) DocumentStateNotifier(service: ExportService())
..openPicked(pageCount: 3, bytes: pdfBytes), ..openPickedWithPageCount(pageCount: 3, bytes: pdfBytes),
), ),
pdfViewModelProvider.overrideWith( pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false), (ref) => PdfViewModel(ref, useMockViewer: false),
@ -95,7 +95,7 @@ void main() {
documentRepositoryProvider.overrideWith( documentRepositoryProvider.overrideWith(
(ref) => (ref) =>
DocumentStateNotifier(service: ExportService()) DocumentStateNotifier(service: ExportService())
..openPicked(pageCount: 3, bytes: pdfBytes), ..openPickedWithPageCount(pageCount: 3, bytes: pdfBytes),
), ),
pdfViewModelProvider.overrideWith( pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false), (ref) => PdfViewModel(ref, useMockViewer: false),
@ -164,7 +164,7 @@ void main() {
documentRepositoryProvider.overrideWith( documentRepositoryProvider.overrideWith(
(ref) => DocumentStateNotifier( (ref) => DocumentStateNotifier(
service: ExportService(enableRaster: false), service: ExportService(enableRaster: false),
)..openPicked(pageCount: 3, bytes: pdfBytes), )..openPickedWithPageCount(pageCount: 3, bytes: pdfBytes),
), ),
pdfViewModelProvider.overrideWith( pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false), (ref) => PdfViewModel(ref, useMockViewer: false),
@ -210,7 +210,7 @@ void main() {
documentRepositoryProvider.overrideWith( documentRepositoryProvider.overrideWith(
(ref) => DocumentStateNotifier( (ref) => DocumentStateNotifier(
service: ExportService(enableRaster: false), service: ExportService(enableRaster: false),
)..openPicked(pageCount: 3, bytes: pdfBytes), )..openPickedWithPageCount(pageCount: 3, bytes: pdfBytes),
), ),
pdfViewModelProvider.overrideWith( pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false), (ref) => PdfViewModel(ref, useMockViewer: false),
@ -259,7 +259,7 @@ void main() {
documentRepositoryProvider.overrideWith( documentRepositoryProvider.overrideWith(
(ref) => DocumentStateNotifier( (ref) => DocumentStateNotifier(
service: ExportService(enableRaster: false), service: ExportService(enableRaster: false),
)..openPicked(pageCount: 3, bytes: pdfBytes), )..openPickedWithPageCount(pageCount: 3, bytes: pdfBytes),
), ),
pdfViewModelProvider.overrideWith( pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false), (ref) => PdfViewModel(ref, useMockViewer: false),
@ -311,7 +311,7 @@ void main() {
documentRepositoryProvider.overrideWith( documentRepositoryProvider.overrideWith(
(ref) => (ref) =>
DocumentStateNotifier(service: ExportService()) DocumentStateNotifier(service: ExportService())
..openPicked(pageCount: 3, bytes: pdfBytes), ..openPickedWithPageCount(pageCount: 3, bytes: pdfBytes),
), ),
pdfViewModelProvider.overrideWith( pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false), (ref) => PdfViewModel(ref, useMockViewer: false),
@ -363,7 +363,7 @@ void main() {
documentRepositoryProvider.overrideWith( documentRepositoryProvider.overrideWith(
(ref) => (ref) =>
DocumentStateNotifier(service: ExportService()) DocumentStateNotifier(service: ExportService())
..openPicked(pageCount: 3, bytes: pdfBytes), ..openPickedWithPageCount(pageCount: 3, bytes: pdfBytes),
), ),
pdfViewModelProvider.overrideWith( pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false), (ref) => PdfViewModel(ref, useMockViewer: false),

View File

@ -34,7 +34,7 @@ void main() {
documentRepositoryProvider.overrideWith( documentRepositoryProvider.overrideWith(
(ref) => (ref) =>
DocumentStateNotifier() DocumentStateNotifier()
..openPicked(pageCount: 3, bytes: pdfBytes), ..openPickedWithPageCount(pageCount: 3, bytes: pdfBytes),
), ),
pdfViewModelProvider.overrideWith( pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false), (ref) => PdfViewModel(ref, useMockViewer: false),
@ -62,14 +62,31 @@ void main() {
final vm = container.read(pdfViewModelProvider); final vm = container.read(pdfViewModelProvider);
expect(vm.currentPage, 1); expect(vm.currentPage, 1);
container.read(pdfViewModelProvider.notifier).jumpToPage(2); final controller = container.read(pdfViewModelProvider).controller;
await tester.pumpAndSettle(); // Wait until the underlying viewer controller reports ready.
await tester.pump(const Duration(milliseconds: 120)); final readyStart = DateTime.now();
expect(container.read(pdfViewModelProvider).currentPage, 2); while (!controller.isReady) {
await tester.pump(const Duration(milliseconds: 40));
if (DateTime.now().difference(readyStart) > const Duration(seconds: 5)) {
fail('PdfViewerController never became ready');
}
}
Future<void> goAndAwait(int target) async {
controller.goToPage(pageNumber: target);
final start = DateTime.now();
while (container.read(pdfViewModelProvider).currentPage != target) {
await tester.pump(const Duration(milliseconds: 40));
if (DateTime.now().difference(start) > const Duration(seconds: 3)) {
fail(
'Timeout waiting to reach page $target (current=${container.read(pdfViewModelProvider).currentPage})',
);
}
}
}
container.read(pdfViewModelProvider.notifier).jumpToPage(3); await goAndAwait(2);
await tester.pumpAndSettle(); expect(container.read(pdfViewModelProvider).currentPage, 2);
await tester.pump(const Duration(milliseconds: 120)); await goAndAwait(3);
expect(container.read(pdfViewModelProvider).currentPage, 3); expect(container.read(pdfViewModelProvider).currentPage, 3);
}); });
@ -88,7 +105,7 @@ void main() {
documentRepositoryProvider.overrideWith( documentRepositoryProvider.overrideWith(
(ref) => (ref) =>
DocumentStateNotifier() DocumentStateNotifier()
..openPicked(pageCount: 3, bytes: pdfBytes), ..openPickedWithPageCount(pageCount: 3, bytes: pdfBytes),
), ),
pdfViewModelProvider.overrideWith( pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false), (ref) => PdfViewModel(ref, useMockViewer: false),
@ -142,7 +159,7 @@ void main() {
documentRepositoryProvider.overrideWith( documentRepositoryProvider.overrideWith(
(ref) => (ref) =>
DocumentStateNotifier() DocumentStateNotifier()
..openPicked(pageCount: 3, bytes: pdfBytes), ..openPickedWithPageCount(pageCount: 3, bytes: pdfBytes),
), ),
pdfViewModelProvider.overrideWith( pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false), (ref) => PdfViewModel(ref, useMockViewer: false),
@ -229,7 +246,7 @@ void main() {
documentRepositoryProvider.overrideWith( documentRepositoryProvider.overrideWith(
(ref) => (ref) =>
DocumentStateNotifier() DocumentStateNotifier()
..openPicked(pageCount: 3, bytes: pdfBytes), ..openPickedWithPageCount(pageCount: 3, bytes: pdfBytes),
), ),
pdfViewModelProvider.overrideWith( pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false), (ref) => PdfViewModel(ref, useMockViewer: false),
@ -295,7 +312,7 @@ void main() {
documentRepositoryProvider.overrideWith( documentRepositoryProvider.overrideWith(
(ref) => (ref) =>
DocumentStateNotifier() DocumentStateNotifier()
..openPicked(pageCount: 3, bytes: pdfBytes), ..openPickedWithPageCount(pageCount: 3, bytes: pdfBytes),
), ),
pdfViewModelProvider.overrideWith( pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false), (ref) => PdfViewModel(ref, useMockViewer: false),
@ -339,4 +356,113 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(container.read(pdfViewModelProvider).currentPage, 3); expect(container.read(pdfViewModelProvider).currentPage, 3);
}); });
testWidgets('PDF View: reopen another PDF via toolbar picker updates viewer', (
tester,
) async {
final initialBytes =
await File('integration_test/data/sample-local-pdf.pdf').readAsBytes();
// 3 pages
final newBytes =
await File(
'integration_test/data/PPFZ-Local-Purchase-Form.pdf',
).readAsBytes();
// 10 pages
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
// We'll override onPickPdf to simulate opening a new file with a different page count
// TODO: Replace PPFZ-Local-Purchase-Form.pdf with a 10-page PDF to test page count change
late ProviderContainer container; // capture to use inside callback
Future<void> simulatePick() async {
container
.read(documentRepositoryProvider.notifier)
.openPicked(bytes: newBytes);
// Reset the current page explicitly to 1 as openPicked establishes new doc
container.read(pdfViewModelProvider.notifier).jumpToPage(1);
}
int? lastDocPageCount; // capture page count from callback
await tester.pumpWidget(
ProviderScope(
overrides: [
preferencesRepositoryProvider.overrideWith(
(ref) => PreferencesStateNotifier(prefs),
),
documentRepositoryProvider.overrideWith(
(ref) =>
DocumentStateNotifier()
..openPickedWithPageCount(pageCount: 3, bytes: initialBytes),
),
pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false),
),
],
child: Builder(
builder: (context) {
container = ProviderScope.containerOf(context);
return MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('en'),
home: PdfSignatureHomePage(
onPickPdf: simulatePick,
onClosePdf: () {},
currentFile: XFile('initial.pdf'),
// The only reliable way to detect the new document load correctly
onDocumentChanged: (doc) {
if (doc != null) {
lastDocPageCount = doc.pages.length;
}
},
),
);
},
),
),
);
await tester.pumpAndSettle();
// Verify initial state Page 1/3
expect(find.byKey(const Key('lbl_page_info')), findsOneWidget);
final initialLabel =
tester.widget<Text>(find.byKey(const Key('lbl_page_info'))).data;
expect(initialLabel, contains('/3'));
// Tap open picker button to simulate opening new PDF
await tester.tap(find.byKey(const Key('btn_open_pdf_picker')));
// Allow frame to process state changes from simulatePick
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
await tester.pumpAndSettle();
// Wait for async page count detection to complete in repository
await tester.runAsync(() async {
final start = DateTime.now();
while (container.read(documentRepositoryProvider).pageCount != 10) {
await Future<void>.delayed(const Duration(milliseconds: 50));
await tester.pump();
if (DateTime.now().difference(start) > const Duration(seconds: 8)) {
final pageCount =
container.read(documentRepositoryProvider).pageCount;
fail(
'Timeout waiting for repository page count to update to 10 (current=$pageCount)',
);
}
}
// Wait for restoration mechanism to complete
await Future<void>.delayed(const Duration(milliseconds: 500));
await tester.pump();
});
final updatedLabel =
tester.widget<Text>(find.byKey(const Key('lbl_page_info'))).data;
expect(updatedLabel, contains('/10'));
// Verify that repository correctly analyzed PDF bytes and updated page count
expect(container.read(documentRepositoryProvider).pageCount, 10);
expect(lastDocPageCount, 10);
expect(container.read(pdfViewModelProvider).currentPage, 1);
});
} }

View File

@ -4,6 +4,7 @@ import 'package:image/image.dart' as img;
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/data/services/export_service.dart'; import 'package:pdf_signature/data/services/export_service.dart';
import 'package:pdfrx/pdfrx.dart';
import '../../domain/models/model.dart'; import '../../domain/models/model.dart';
@ -24,7 +25,61 @@ class DocumentStateNotifier extends StateNotifier<Document> {
); );
} }
void openPicked({required int pageCount, Uint8List? bytes}) { void openPicked({Uint8List? bytes}) {
debugPrint(
'[DocumentRepository] openPicked called (bytes length: ${bytes?.length})',
);
// For real usage, determine page count from PDF bytes asynchronously
if (bytes != null) {
_openPickedAsync(bytes);
} else {
// Handle null bytes case
state = state.copyWith(
loaded: true,
pageCount: 1,
pickedPdfBytes: bytes,
placementsByPage: <int, List<SignaturePlacement>>{},
);
}
}
Future<void> _openPickedAsync(Uint8List bytes) async {
int pageCount = 1; // default fallback
try {
// Determine actual page count from PDF bytes
final doc = await PdfDocument.openData(bytes);
pageCount = doc.pages.length;
debugPrint('[DocumentRepository] PDF has $pageCount pages');
} catch (e) {
debugPrint('[DocumentRepository] Failed to read PDF page count: $e');
// Keep default pageCount = 1 on error
}
state = state.copyWith(
loaded: true,
pageCount: pageCount,
pickedPdfBytes: bytes,
placementsByPage: <int, List<SignaturePlacement>>{},
);
// Schedule delayed check to ensure our page count wasn't overridden by UI callbacks
Future.delayed(const Duration(milliseconds: 100), () {
if (state.loaded &&
state.pickedPdfBytes == bytes &&
state.pageCount != pageCount) {
state = state.copyWith(pageCount: pageCount);
}
});
}
// For tests that need to specify page count explicitly
@visibleForTesting
void openPickedWithPageCount({required int pageCount, Uint8List? bytes}) {
debugPrint(
'[DocumentRepository] openPickedWithPageCount called (pageCount=$pageCount)',
);
state = state.copyWith( state = state.copyWith(
loaded: true, loaded: true,
pageCount: pageCount, pageCount: pageCount,
@ -39,6 +94,9 @@ class DocumentStateNotifier extends StateNotifier<Document> {
void setPageCount(int count) { void setPageCount(int count) {
if (!state.loaded) return; if (!state.loaded) return;
debugPrint(
'[DocumentRepository] setPageCount called: $count (current: ${state.pageCount})',
);
state = state.copyWith(pageCount: count.clamp(1, 9999)); state = state.copyWith(pageCount: count.clamp(1, 9999));
} }

View File

@ -1,12 +1,33 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pdfrx/pdfrx.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'package:pdf_signature/app.dart'; import 'package:pdf_signature/app.dart';
export 'package:pdf_signature/app.dart'; export 'package:pdf_signature/app.dart';
void main() { Future<void> _initPdfrxCache() async {
// Ensure Flutter bindings are initialized before platform channel usage try {
// Only set once; guard for hot reload/tests
if (Pdfrx.getCacheDirectory == null) {
final dir = await getTemporaryDirectory();
final cacheDir = Directory('${dir.path}/pdfrx_cache');
if (!await cacheDir.exists()) {
await cacheDir.create(recursive: true);
}
Pdfrx.getCacheDirectory = () async => cacheDir.path;
debugPrint('[main] Pdfrx cache directory set to ${cacheDir.path}');
}
} catch (e, st) {
debugPrint('[main] Failed to initialize Pdfrx cache directory: $e');
debugPrint(st.toString());
}
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await _initPdfrxCache();
// Disable right-click context menu on web using Flutter API // Disable right-click context menu on web using Flutter API
if (kReleaseMode) { if (kReleaseMode) {
debugPrint = (String? message, {int? wrapWidth}) { debugPrint = (String? message, {int? wrapWidth}) {

View File

@ -33,7 +33,7 @@ final routerProvider = Provider<GoRouter>((ref) {
({String? path, Uint8List? bytes, String? fileName}) => ({String? path, Uint8List? bytes, String? fileName}) =>
sessionVm.openPdf( sessionVm.openPdf(
path: path, path: path,
bytes: bytes, bytes: bytes ?? Uint8List(0),
fileName: fileName, fileName: fileName,
), ),
); );

View File

@ -0,0 +1,31 @@
import 'dart:typed_data';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'document_version.freezed.dart';
/// Internal data model for tracking document versions in the UI layer.
/// This is separate from the domain Document model to avoid coupling UI concerns with business logic.
@freezed
abstract class DocumentVersion with _$DocumentVersion {
const factory DocumentVersion({
@Default(0) int version,
Uint8List? lastBytes,
}) = _DocumentVersion;
factory DocumentVersion.initial() => const DocumentVersion();
}
extension DocumentVersionMethods on DocumentVersion {
/// Generate the source name for PdfDocumentRefData based on version
String get sourceName => 'document_v$version.pdf';
/// Check if bytes have changed and need version increment
bool shouldIncrementVersion(Uint8List? newBytes) {
return !identical(lastBytes, newBytes);
}
/// Increment version and update bytes
DocumentVersion incrementVersion(Uint8List? newBytes) {
return copyWith(version: version + 1, lastBytes: newBytes);
}
}

View File

@ -5,9 +5,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart'; import 'package:pdf_signature/data/repositories/document_repository.dart';
import 'package:pdf_signature/data/repositories/signature_card_repository.dart'; import 'package:pdf_signature/data/repositories/signature_card_repository.dart';
import 'package:pdf_signature/domain/models/model.dart'; import 'package:pdf_signature/domain/models/model.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/document_version.dart';
import 'package:pdfrx/pdfrx.dart'; import 'package:pdfrx/pdfrx.dart';
import 'package:file_picker/file_picker.dart' as fp; import 'package:file_picker/file_picker.dart' as fp;
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:path_provider/path_provider.dart';
class PdfViewModel extends ChangeNotifier { class PdfViewModel extends ChangeNotifier {
final Ref ref; final Ref ref;
@ -31,6 +33,26 @@ class PdfViewModel extends ChangeNotifier {
final Set<String> _lockedPlacements = {}; final Set<String> _lockedPlacements = {};
Set<String> get lockedPlacements => Set.unmodifiable(_lockedPlacements); Set<String> get lockedPlacements => Set.unmodifiable(_lockedPlacements);
// Document version tracking for UI consistency
DocumentVersion _documentVersion = DocumentVersion.initial();
// Get current document source name for PdfDocumentRefData
String get documentSourceName {
// Ensure document version is up to date, but only update if really needed
_updateDocumentVersionIfNeeded();
return _documentVersion.sourceName;
}
void _updateDocumentVersionIfNeeded() {
final document = ref.read(documentRepositoryProvider);
if (!identical(_documentVersion.lastBytes, document.pickedPdfBytes)) {
_documentVersion = DocumentVersion(
version: _documentVersion.version + 1,
lastBytes: document.pickedPdfBytes,
);
}
}
// const bool.fromEnvironment('FLUTTER_TEST', defaultValue: false); // const bool.fromEnvironment('FLUTTER_TEST', defaultValue: false);
PdfViewModel(this.ref, {bool? useMockViewer}) PdfViewModel(this.ref, {bool? useMockViewer})
: _useMockViewer = : _useMockViewer =
@ -275,30 +297,59 @@ class PdfSessionViewModel extends ChangeNotifier {
if (effectiveBytes == null && path != null && path.isNotEmpty) { if (effectiveBytes == null && path != null && path.isNotEmpty) {
try { try {
effectiveBytes = await XFile(path).readAsBytes(); effectiveBytes = await XFile(path).readAsBytes();
} catch (_) { } catch (e, st) {
effectiveBytes = null; effectiveBytes = null;
debugPrint(
'[PdfSessionViewModel] Failed to read PDF data from path=$path error=$e',
);
debugPrint(st.toString());
} }
} }
await openPdf(path: path, bytes: effectiveBytes, fileName: name); if (effectiveBytes != null) {
await openPdf(path: path, bytes: effectiveBytes, fileName: name);
} else {
debugPrint(
'[PdfSessionViewModel] No PDF data available to open (path=$path, name=$name)',
);
}
} }
Future<void> openPdf({ Future<void> openPdf({
String? path, String? path,
Uint8List? bytes, required Uint8List bytes,
String? fileName, String? fileName,
}) async { }) async {
int pageCount = 1; // default int pageCount = 1; // default
if (bytes != null) { try {
try { // Defensive: ensure Pdfrx cache directory set (in case main init skipped in tests)
final doc = await PdfDocument.openData(bytes); if (Pdfrx.getCacheDirectory == null) {
pageCount = doc.pages.length; debugPrint(
} catch (_) { '[PdfSessionViewModel] Pdfrx.getCacheDirectory was null; setting temp directory',
// ignore invalid bytes );
try {
final temp = await getTemporaryDirectory();
Pdfrx.getCacheDirectory = () async => temp.path;
} catch (e, st) {
debugPrint(
'[PdfSessionViewModel] Failed to set fallback cache dir error=$e',
);
debugPrint(st.toString());
}
} }
final doc = await PdfDocument.openData(bytes);
pageCount = doc.pages.length;
debugPrint(
'[PdfSessionViewModel] Opened PDF bytes length=${bytes.length} pages=$pageCount',
);
} catch (e, st) {
debugPrint(
'[PdfSessionViewModel] Failed to read PDF data from bytes error=$e',
);
debugPrint(st.toString());
} }
if (path != null && path.isNotEmpty) { if (path != null && path.isNotEmpty) {
_currentFile = XFile(path); _currentFile = XFile(path);
} else if (bytes != null && (fileName != null && fileName.isNotEmpty)) { } else if ((fileName != null && fileName.isNotEmpty)) {
// Keep in-memory XFile so .name is available for suggestion // Keep in-memory XFile so .name is available for suggestion
try { try {
_currentFile = XFile.fromData( _currentFile = XFile.fromData(
@ -306,8 +357,12 @@ class PdfSessionViewModel extends ChangeNotifier {
name: fileName, name: fileName,
mimeType: 'application/pdf', mimeType: 'application/pdf',
); );
} catch (_) { } catch (e, st) {
_currentFile = XFile(fileName); _currentFile = XFile(fileName);
debugPrint(
'[PdfSessionViewModel] Failed to create XFile.fromData name=$fileName error=$e',
);
debugPrint(st.toString());
} }
} else { } else {
_currentFile = XFile(''); _currentFile = XFile('');
@ -322,13 +377,14 @@ class PdfSessionViewModel extends ChangeNotifier {
} else { } else {
_displayFileName = ''; _displayFileName = '';
} }
ref debugPrint('[PdfSessionViewModel] Calling openPicked with bytes');
.read(documentRepositoryProvider.notifier) ref.read(documentRepositoryProvider.notifier).openPicked(bytes: bytes);
.openPicked(pageCount: pageCount, bytes: bytes);
// Keep existing signature cards when opening a new document. // Keep existing signature cards when opening a new document.
// The feature "Open a different document will reset signature placements but keep signature cards" // The feature "Open a different document will reset signature placements but keep signature cards"
// relies on this behavior. Placements are reset by openPicked() above. // relies on this behavior. Placements are reset by openPicked() above.
debugPrint('[PdfSessionViewModel] Navigating to /pdf');
router.go('/pdf'); router.go('/pdf');
debugPrint('[PdfSessionViewModel] Notifying listeners after open');
notifyListeners(); notifyListeners();
} }

View File

@ -13,10 +13,12 @@ class PdfPageArea extends ConsumerStatefulWidget {
super.key, super.key,
required this.pageSize, required this.pageSize,
required this.controller, required this.controller,
this.onDocumentChanged,
}); });
final Size pageSize; final Size pageSize;
final PdfViewerController controller; final PdfViewerController controller;
final void Function(PdfDocument?)? onDocumentChanged;
@override @override
ConsumerState<PdfPageArea> createState() => _PdfPageAreaState(); ConsumerState<PdfPageArea> createState() => _PdfPageAreaState();
} }
@ -163,7 +165,9 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
pageKeyBuilder: _pageKey, pageKeyBuilder: _pageKey,
scrollToPage: _scrollToPage, scrollToPage: _scrollToPage,
controller: widget.controller, controller: widget.controller,
innerViewerKey: const ValueKey('viewer_idle'), // Remove fixed innerViewerKey to allow PdfViewerWidget to generate dynamic keys
// innerViewerKey: const ValueKey('viewer_idle'),
onDocumentChanged: widget.onDocumentChanged,
); );
} }
return const SizedBox.shrink(); return const SizedBox.shrink();

View File

@ -29,6 +29,8 @@ class PdfSignatureHomePage extends ConsumerStatefulWidget {
// available, this name preserves the user-selected filename so we can // available, this name preserves the user-selected filename so we can
// suggest a proper "signed_*.pdf" on save. // suggest a proper "signed_*.pdf" on save.
final String? currentFileName; final String? currentFileName;
// Optional listener for underlying Pdfrx document change events.
final void Function(PdfDocument?)? onDocumentChanged;
const PdfSignatureHomePage({ const PdfSignatureHomePage({
super.key, super.key,
@ -36,6 +38,7 @@ class PdfSignatureHomePage extends ConsumerStatefulWidget {
required this.onClosePdf, required this.onClosePdf,
required this.currentFile, required this.currentFile,
this.currentFileName, this.currentFileName,
this.onDocumentChanged,
}); });
@override @override
@ -333,7 +336,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
pdf.loaded && pdf.pickedPdfBytes != null pdf.loaded && pdf.pickedPdfBytes != null
? PdfDocumentRefData( ? PdfDocumentRefData(
pdf.pickedPdfBytes!, pdf.pickedPdfBytes!,
sourceName: 'document.pdf', sourceName: pdfViewModel.documentSourceName,
) )
: null; : null;
@ -354,6 +357,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
controller: _viewModel.controller, controller: _viewModel.controller,
key: const ValueKey('pdf_page_area'), key: const ValueKey('pdf_page_area'),
pageSize: _pageSize, pageSize: _pageSize,
onDocumentChanged: widget.onDocumentChanged,
), ),
), ),
), ),

View File

@ -5,6 +5,7 @@ import 'package:pdf_signature/l10n/app_localizations.dart';
import 'package:responsive_framework/responsive_framework.dart'; import 'package:responsive_framework/responsive_framework.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart';
class PdfToolbar extends ConsumerStatefulWidget { class PdfToolbar extends ConsumerStatefulWidget {
const PdfToolbar({ const PdfToolbar({
@ -59,7 +60,9 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final pdfViewModel = ref.watch(pdfViewModelProvider); final pdfViewModel = ref.watch(pdfViewModelProvider);
final pdf = pdfViewModel.document; final pdf = ref.watch(
documentRepositoryProvider,
); // Watch document directly for updates
final currentPage = pdfViewModel.currentPage; final currentPage = pdfViewModel.currentPage;
final l = AppLocalizations.of(context); final l = AppLocalizations.of(context);
final pageInfo = l.pageInfo(currentPage, pdf.pageCount); final pageInfo = l.pageInfo(currentPage, pdf.pageCount);

View File

@ -5,6 +5,9 @@ import 'package:pdf_signature/l10n/app_localizations.dart';
import 'pdf_page_overlays.dart'; import 'pdf_page_overlays.dart';
import './pdf_mock_continuous_list.dart'; import './pdf_mock_continuous_list.dart';
import '../view_model/pdf_view_model.dart'; import '../view_model/pdf_view_model.dart';
import 'package:pdf_signature/domain/models/document.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart';
import 'dart:typed_data';
// Provider to control whether viewer overlays (like scroll thumbs) are enabled. // Provider to control whether viewer overlays (like scroll thumbs) are enabled.
// Integration tests can override this to false to avoid long-running animations. // Integration tests can override this to false to avoid long-running animations.
@ -18,6 +21,7 @@ class PdfViewerWidget extends ConsumerStatefulWidget {
this.scrollToPage, this.scrollToPage,
required this.controller, required this.controller,
this.innerViewerKey, this.innerViewerKey,
this.onDocumentChanged,
}); });
final Size pageSize; final Size pageSize;
@ -26,13 +30,43 @@ class PdfViewerWidget extends ConsumerStatefulWidget {
final PdfViewerController controller; final PdfViewerController controller;
// Optional key applied to the inner Pdfrx PdfViewer to force disposal/rebuild // Optional key applied to the inner Pdfrx PdfViewer to force disposal/rebuild
final Key? innerViewerKey; final Key? innerViewerKey;
// External hook to observe document changes (forwarded from Pdfrx onDocumentChanged)
final void Function(PdfDocument?)? onDocumentChanged;
@override @override
ConsumerState<PdfViewerWidget> createState() => _PdfViewerWidgetState(); ConsumerState<PdfViewerWidget> createState() => _PdfViewerWidgetState();
} }
class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> { class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
PdfDocumentRef? _documentRef; final ValueNotifier<PdfDocumentRef?> _docRefNotifier = ValueNotifier(null);
Uint8List? _lastBytes;
void _updateDocRef(Document doc) {
if (!doc.loaded || doc.pickedPdfBytes == null) {
if (_docRefNotifier.value != null) {
debugPrint('[PdfViewerWidget] Clearing docRef (no document loaded)');
_docRefNotifier.value = null;
}
return;
}
final bytes = doc.pickedPdfBytes!;
if (!identical(bytes, _lastBytes)) {
_lastBytes = bytes;
final viewModel = ref.read(pdfViewModelProvider);
debugPrint(
'[PdfViewerWidget] New PDF bytes detected -> ${viewModel.documentSourceName}',
);
// Force a full detach by setting null first so PdfViewer unmounts even if the
// framework would otherwise optimize rebuilds with same key ordering.
if (_docRefNotifier.value != null) {
_docRefNotifier.value = null;
}
final newRef = PdfDocumentRefData(
bytes,
sourceName: viewModel.documentSourceName,
);
_docRefNotifier.value = newRef;
}
}
// Public getter for testing the actual viewer page // Public getter for testing the actual viewer page
int? get viewerCurrentPage => widget.controller.pageNumber; int? get viewerCurrentPage => widget.controller.pageNumber;
@ -51,31 +85,9 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final pdfViewModel = ref.watch(pdfViewModelProvider); final pdfViewModel = ref.watch(pdfViewModelProvider);
final document = pdfViewModel.document; final document = ref.watch(documentRepositoryProvider);
final useMock = pdfViewModel.useMockViewer; final useMock = pdfViewModel.useMockViewer;
// trigger rebuild when active rect changes _updateDocRef(document);
// Update document ref when document changes
if (document.loaded && document.pickedPdfBytes != null) {
if (_documentRef == null) {
_documentRef = PdfDocumentRefData(
document.pickedPdfBytes!,
sourceName: 'document.pdf',
);
}
} else {
_documentRef = null;
}
if (_documentRef == null && !useMock) {
String text;
try {
text = AppLocalizations.of(context).noPdfLoaded;
} catch (_) {
text = 'No PDF loaded';
}
return Center(child: Text(text));
}
if (useMock) { if (useMock) {
return PdfMockContinuousList( return PdfMockContinuousList(
@ -89,80 +101,119 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
} }
final overlaysEnabled = ref.watch(viewerOverlaysEnabledProvider); final overlaysEnabled = ref.watch(viewerOverlaysEnabledProvider);
return PdfViewer( return ValueListenableBuilder<PdfDocumentRef?>(
_documentRef!, valueListenable: _docRefNotifier,
key: widget.innerViewerKey ?? const Key('pdf_continuous_mock_list'), builder: (context, docRef, _) {
controller: widget.controller, if (docRef == null) {
params: PdfViewerParams( String text;
onViewerReady: (document, controller) { try {
// Update page count in repository text = AppLocalizations.of(context).noPdfLoaded;
ref } catch (_) {
.read(pdfViewModelProvider.notifier) text = 'No PDF loaded';
.setPageCount(document.pages.length);
},
onPageChanged: (page) {
if (page != null) {
// Also update the view model to keep them in sync
ref.read(pdfViewModelProvider.notifier).jumpToPage(page);
} }
}, return Center(child: Text(text));
viewerOverlayBuilder: }
overlaysEnabled final pdfViewModel = ref.read(pdfViewModelProvider);
? (context, size, handle) { final viewerKey =
return [ widget.innerViewerKey ??
// Vertical scroll thumb on the right Key('pdf_viewer_${pdfViewModel.documentSourceName}');
PdfViewerScrollThumb(
controller: widget.controller, return PdfViewer(
orientation: ScrollbarOrientation.right, docRef,
thumbSize: const Size(40, 25), key: viewerKey,
thumbBuilder: controller: widget.controller,
(context, thumbSize, pageNumber, controller) => params: PdfViewerParams(
Container( onViewerReady: (document, controller) {
color: Colors.black.withValues(alpha: 0.7), // Update page count in repository
child: Center( ref
child: Text( .read(pdfViewModelProvider.notifier)
'Pg $pageNumber', .setPageCount(document.pages.length);
style: const TextStyle( },
color: Colors.white, onPageChanged: (page) {
fontSize: 12, if (page != null) {
// Also update the view model to keep them in sync
ref.read(pdfViewModelProvider.notifier).jumpToPage(page);
}
},
onDocumentChanged: (doc) async {
final pc = doc?.pages.length;
debugPrint(
'[PdfViewerWidget] onDocumentChanged called (pages=$pc)',
);
if (doc != null) {
// Update internal page count state
ref
.read(pdfViewModelProvider.notifier)
.setPageCount(doc.pages.length);
}
// Invoke external listener after internal handling
try {
widget.onDocumentChanged?.call(doc);
} catch (e, st) {
debugPrint(
'[PdfViewerWidget] external onDocumentChanged threw: $e\n$st',
);
}
},
viewerOverlayBuilder:
overlaysEnabled
? (context, size, handle) {
return [
// Vertical scroll thumb on the right
PdfViewerScrollThumb(
controller: widget.controller,
orientation: ScrollbarOrientation.right,
thumbSize: const Size(40, 25),
thumbBuilder:
(context, thumbSize, pageNumber, controller) =>
Container(
color: Colors.black.withValues(alpha: 0.7),
child: Center(
child: Text(
'Pg $pageNumber',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
),
), ),
), ),
), ),
), // Horizontal scroll thumb on the bottom
), PdfViewerScrollThumb(
// Horizontal scroll thumb on the bottom controller: widget.controller,
PdfViewerScrollThumb( orientation: ScrollbarOrientation.bottom,
controller: widget.controller, thumbSize: const Size(40, 25),
orientation: ScrollbarOrientation.bottom, thumbBuilder:
thumbSize: const Size(40, 25), (context, thumbSize, pageNumber, controller) =>
thumbBuilder: Container(
(context, thumbSize, pageNumber, controller) => color: Colors.black.withValues(alpha: 0.7),
Container( child: Center(
color: Colors.black.withValues(alpha: 0.7), child: Text(
child: Center( 'Pg $pageNumber',
child: Text( style: const TextStyle(
'Pg $pageNumber', color: Colors.white,
style: const TextStyle( fontSize: 12,
color: Colors.white, ),
fontSize: 12, ),
), ),
), ),
), ),
), ];
), }
]; : (context, size, handle) => const <Widget>[],
} // Per-page overlays to enable page-specific drag targets and placed signatures
: (context, size, handle) => const <Widget>[], pageOverlaysBuilder: (context, pageRect, page) {
// Per-page overlays to enable page-specific drag targets and placed signatures return [
pageOverlaysBuilder: (context, pageRect, page) { PdfPageOverlays(
return [ pageSize: Size(pageRect.width, pageRect.height),
PdfPageOverlays( pageNumber: page.pageNumber,
pageSize: Size(pageRect.width, pageRect.height), ),
pageNumber: page.pageNumber, ];
), },
]; ),
}, );
), },
); );
} }
} }

View File

@ -1,4 +1,4 @@
import 'dart:typed_data'; import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -11,6 +11,14 @@ class WelcomeViewModel {
WelcomeViewModel(this.ref, this.router); WelcomeViewModel(this.ref, this.router);
Future<void> openPdf({required String path, Uint8List? bytes}) async { Future<void> openPdf({required String path, Uint8List? bytes}) async {
// Return early if no bytes provided - can't open PDF without data
if (bytes == null) {
debugPrint(
'[WelcomeViewModel] Cannot open PDF: no bytes provided for $path',
);
return;
}
// Use PdfSessionViewModel to open and navigate. // Use PdfSessionViewModel to open and navigate.
final session = ref.read(pdfSessionViewModelProvider(router)); final session = ref.read(pdfSessionViewModelProvider(router));
await session.openPdf(path: path, bytes: bytes); await session.openPdf(path: path, bytes: bytes);

View File

@ -12,7 +12,7 @@ Future<void> aDocumentIsOpenAndContainsAtLeastOneSignaturePlacement(
) async { ) async {
final container = TestWorld.container ?? ProviderContainer(); final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container; TestWorld.container = container;
container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 5); container.read(documentRepositoryProvider.notifier).openPickedWithPageCount(pageCount: 5);
container container
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.addPlacement( .addPlacement(

View File

@ -13,7 +13,7 @@ aDocumentIsOpenAndContainsMultiplePlacedSignaturePlacementsAcrossPages(
) async { ) async {
final container = TestWorld.container ?? ProviderContainer(); final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container; TestWorld.container = container;
container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 5); container.read(documentRepositoryProvider.notifier).openPickedWithPageCount(pageCount: 5);
container container
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.addPlacement( .addPlacement(

View File

@ -11,6 +11,6 @@ Future<void> aDocumentIsOpenWithNoSignaturePlacementsPlaced(
TestWorld.container = container; TestWorld.container = container;
container container
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.openPicked(pageCount: 5); .openPickedWithPageCount(pageCount: 5);
// No placements added // No placements added
} }

View File

@ -11,7 +11,7 @@ Future<void> aDocumentPageIsSelectedForSigning(WidgetTester tester) async {
// Ensure a document is open // Ensure a document is open
final repo = container.read(documentRepositoryProvider.notifier); final repo = container.read(documentRepositoryProvider.notifier);
if (!container.read(documentRepositoryProvider).loaded) { if (!container.read(documentRepositoryProvider).loaded) {
repo.openPicked(pageCount: 5); repo.openPickedWithPageCount(pageCount: 5);
} }
// Ensure current page is 1 for consistent subsequent steps // Ensure current page is 1 for consistent subsequent steps
try { try {

View File

@ -18,7 +18,7 @@ Future<void> aMultipageDocumentIsOpen(WidgetTester tester) async {
container.read(signatureCardRepositoryProvider.notifier).state = [ container.read(signatureCardRepositoryProvider.notifier).state = [
SignatureCard.initial(), SignatureCard.initial(),
]; ];
container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 5); container.read(documentRepositoryProvider.notifier).openPickedWithPageCount(pageCount: 5);
// Reset page state providers // Reset page state providers
try { try {
container.read(pdfViewModelProvider.notifier).jumpToPage(1); container.read(pdfViewModelProvider.notifier).jumpToPage(1);

View File

@ -11,5 +11,5 @@ Future<void> aSampleMultipageDocument5PagesIsAvailable(
TestWorld.container = container; TestWorld.container = container;
container container
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.openPicked(pageCount: 5); .openPickedWithPageCount(pageCount: 5);
} }

View File

@ -17,7 +17,7 @@ Future<void> aSignatureAssetIsPlacedOnThePage(WidgetTester tester) async {
if (!container.read(documentRepositoryProvider).loaded) { if (!container.read(documentRepositoryProvider).loaded) {
container container
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.openPicked(pageCount: 5); .openPickedWithPageCount(pageCount: 5);
} }
// Get or create an asset // Get or create an asset

View File

@ -17,7 +17,7 @@ Future<void> aSignaturePlacementIsPlacedOnPage(
if (!container.read(documentRepositoryProvider).loaded) { if (!container.read(documentRepositoryProvider).loaded) {
container container
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.openPicked(pageCount: 5); .openPickedWithPageCount(pageCount: 5);
} }
final page = param1.toInt(); final page = param1.toInt();
container container

View File

@ -16,7 +16,7 @@ Future<void> aSignaturePlacementIsPlacedWithAPositionAndSizeRelativeToThePage(
if (!container.read(documentRepositoryProvider).loaded) { if (!container.read(documentRepositoryProvider).loaded) {
container container
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.openPicked(pageCount: 5); .openPickedWithPageCount(pageCount: 5);
} }
final currentPage = container.read(pdfViewModelProvider).currentPage; final currentPage = container.read(pdfViewModelProvider).currentPage;
container container

View File

@ -25,7 +25,7 @@ theUserDragsItOnThePageOfTheDocumentToPlaceSignaturePlacementsInMultipleLocation
if (!container.read(documentRepositoryProvider).loaded) { if (!container.read(documentRepositoryProvider).loaded) {
container container
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.openPicked(pageCount: 5); .openPickedWithPageCount(pageCount: 5);
} }
container container

View File

@ -21,7 +21,7 @@ theUserDragsThisSignatureCardOnThePageOfTheDocumentToPlaceASignaturePlacement(
if (!container.read(documentRepositoryProvider).loaded) { if (!container.read(documentRepositoryProvider).loaded) {
container container
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.openPicked(pageCount: 5); .openPickedWithPageCount(pageCount: 5);
} }
// Get or create an asset // Get or create an asset

View File

@ -19,7 +19,7 @@ Future<void> theUserOpensADifferentDocumentWithPages(
// Simulate "open a different document": reset placements and set page count. // Simulate "open a different document": reset placements and set page count.
container container
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.openPicked(pageCount: pageCount); .openPickedWithPageCount(pageCount: pageCount);
// Ensure there are 2 signature cards available as per scenario. // Ensure there are 2 signature cards available as per scenario.
final cards = container.read(signatureCardRepositoryProvider); final cards = container.read(signatureCardRepositoryProvider);
if (cards.length < 2) { if (cards.length < 2) {

View File

@ -12,7 +12,7 @@ Future<void> theUserPlacesItInMultipleLocationsInTheDocument(
TestWorld.container = container; TestWorld.container = container;
final notifier = container.read(documentRepositoryProvider.notifier); final notifier = container.read(documentRepositoryProvider.notifier);
// Always open a fresh doc to avoid state bleed between scenarios // Always open a fresh doc to avoid state bleed between scenarios
notifier.openPicked(pageCount: 6); notifier.openPickedWithPageCount(pageCount: 6);
// Place two on page 2 and one on page 4 // Place two on page 2 and one on page 4
notifier.addPlacement(page: 2, rect: const Rect.fromLTWH(10, 10, 80, 40)); notifier.addPlacement(page: 2, rect: const Rect.fromLTWH(10, 10, 80, 40));
notifier.addPlacement(page: 2, rect: const Rect.fromLTWH(120, 50, 80, 40)); notifier.addPlacement(page: 2, rect: const Rect.fromLTWH(120, 50, 80, 40));

View File

@ -17,7 +17,7 @@ Future<void> theUserSavesexportsTheDocument(WidgetTester tester) async {
// Load a minimal sample so the expectation passes in logic-only tests // Load a minimal sample so the expectation passes in logic-only tests
container container
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.openPicked(pageCount: 2, bytes: Uint8List(10)); .openPickedWithPageCount(pageCount: 2, bytes: Uint8List(10));
} }
expect(pdf.loaded, isTrue, reason: 'PDF must be loaded before export'); expect(pdf.loaded, isTrue, reason: 'PDF must be loaded before export');
// Check if there are placements // Check if there are placements

View File

@ -10,7 +10,7 @@ Future<void> theUserSelects(WidgetTester tester, dynamic file) async {
final container = ProviderContainer(); final container = ProviderContainer();
TestWorld.container = container; TestWorld.container = container;
// Mark page for signing to enable signature ops // Mark page for signing to enable signature ops
container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 1); container.read(documentRepositoryProvider.notifier).openPickedWithPageCount(pageCount: 1);
// For invalid/unsupported/empty selections we do NOT set image bytes. // For invalid/unsupported/empty selections we do NOT set image bytes.
// This simulates a failed load and keeps rect null. // This simulates a failed load and keeps rect null.
final token = file.toString(); final token = file.toString();

View File

@ -22,7 +22,7 @@ Future<void> threeSignaturePlacementsArePlacedOnTheCurrentPage(
container.read(signatureCardRepositoryProvider.notifier).state = [ container.read(signatureCardRepositoryProvider.notifier).state = [
SignatureCard.initial(), SignatureCard.initial(),
]; ];
container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 5); container.read(documentRepositoryProvider.notifier).openPickedWithPageCount(pageCount: 5);
final pdfN = container.read(documentRepositoryProvider.notifier); final pdfN = container.read(documentRepositoryProvider.notifier);
final page = container.read(pdfViewModelProvider).currentPage; final page = container.read(pdfViewModelProvider).currentPage;
pdfN.addPlacement( pdfN.addPlacement(

View File

@ -49,7 +49,7 @@ void main() {
documentRepositoryProvider.overrideWith( documentRepositoryProvider.overrideWith(
(ref) => (ref) =>
DocumentStateNotifier() DocumentStateNotifier()
..openPicked(pageCount: 5, bytes: Uint8List(0)), ..openPickedWithPageCount(pageCount: 5, bytes: Uint8List(0)),
), ),
pdfViewModelProvider.overrideWith( pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true), (ref) => PdfViewModel(ref, useMockViewer: true),

View File

@ -0,0 +1,33 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/document_version.dart';
import 'dart:typed_data';
void main() {
group('DocumentVersion', () {
test('should generate consistent source names', () {
final version1 = DocumentVersion(version: 1);
final version2 = DocumentVersion(version: 2);
expect(version1.sourceName, 'document_v1.pdf');
expect(version2.sourceName, 'document_v2.pdf');
});
test('should increment version when bytes change', () {
final bytes1 = Uint8List.fromList([1, 2, 3]);
final bytes2 = Uint8List.fromList([4, 5, 6]);
final version = DocumentVersion(version: 1, lastBytes: bytes1);
expect(version.shouldIncrementVersion(bytes2), true);
expect(version.shouldIncrementVersion(bytes1), false);
});
test('should detect identical bytes correctly', () {
final bytes = Uint8List.fromList([1, 2, 3]);
final version = DocumentVersion(version: 1, lastBytes: bytes);
// Same bytes object should not trigger increment
expect(version.shouldIncrementVersion(bytes), false);
});
});
}

View File

@ -51,7 +51,7 @@ void main() {
}) async { }) async {
final container = ProviderScope.containerOf(stateful.context); final container = ProviderScope.containerOf(stateful.context);
final repo = container.read(documentRepositoryProvider.notifier); final repo = container.read(documentRepositoryProvider.notifier);
repo.openPicked(pageCount: 1, bytes: bytes); repo.openPickedWithPageCount(pageCount: 1, bytes: bytes);
}, [fake]); }, [fake]);
await tester.pump(); await tester.pump();