feat: migrate pdf state to viewmodel abstraction

This commit is contained in:
insleker 2025-09-12 18:59:27 +08:00
parent 7336ca4d57
commit 5549f08b4c
49 changed files with 796 additions and 594 deletions

View File

@ -3,3 +3,4 @@
* support multiple platforms (windows, linux, android, web) * support multiple platforms (windows, linux, android, web)
* only FOSS libs can use * only FOSS libs can use
* should not exceed 350 lines of code per file * should not exceed 350 lines of code per file
* Direct Passing is better than Singleton(e.g.Provider) especially for `view`, `viewModel`.

View File

@ -5,6 +5,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import 'package:image/image.dart' as img; import 'package:image/image.dart' as img;
import 'dart:io'; import 'dart:io';
import 'package:file_selector/file_selector.dart' as fs;
import 'package:pdf_signature/data/services/export_service.dart'; import 'package:pdf_signature/data/services/export_service.dart';
@ -13,7 +14,6 @@ import 'package:pdf_signature/data/repositories/signature_card_repository.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart'; import 'package:pdf_signature/data/repositories/document_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/pdf_view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/ui_services.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/ui_services.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pages_sidebar.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pages_sidebar.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
@ -50,17 +50,23 @@ void main() {
documentRepositoryProvider.overrideWith( documentRepositoryProvider.overrideWith(
(ref) => DocumentStateNotifier()..openPicked(pageCount: 3), (ref) => DocumentStateNotifier()..openPicked(pageCount: 3),
), ),
useMockViewerProvider.overrideWith((ref) => false), pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false),
),
exportServiceProvider.overrideWith((_) => fake), exportServiceProvider.overrideWith((_) => fake),
savePathPickerProvider.overrideWith( savePathPickerProvider.overrideWith(
(_) => () async => 'C:/tmp/output.pdf', (_) => () async => 'C:/tmp/output.pdf',
), ),
], ],
child: const MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'), locale: Locale('en'),
home: PdfSignatureHomePage(), home: PdfSignatureHomePage(
onPickPdf: () async {},
onClosePdf: () {},
currentFile: fs.XFile('test.pdf'),
),
), ),
), ),
); );
@ -120,13 +126,19 @@ void main() {
cardRepo.addWithAsset(asset, 0.0); cardRepo.addWithAsset(asset, 0.0);
return cardRepo; return cardRepo;
}), }),
useMockViewerProvider.overrideWithValue(false), pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false),
),
], ],
child: const MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'), locale: Locale('en'),
home: PdfSignatureHomePage(), home: PdfSignatureHomePage(
onPickPdf: () async {},
onClosePdf: () {},
currentFile: fs.XFile('test.pdf'),
),
), ),
), ),
); );
@ -145,17 +157,18 @@ void main() {
// Programmatically simulate confirm: add placement with current rect and bound image, then clear active overlay. // Programmatically simulate confirm: add placement with current rect and bound image, then clear active overlay.
final ctx = tester.element(find.byType(PdfSignatureHomePage)); final ctx = tester.element(find.byType(PdfSignatureHomePage));
final container = ProviderScope.containerOf(ctx); final container = ProviderScope.containerOf(ctx);
final r = container.read(activeRectProvider)!; final r = container.read(pdfViewModelProvider).activeRect!;
final lib = container.read(signatureAssetRepositoryProvider); final lib = container.read(signatureAssetRepositoryProvider);
final asset = lib.isNotEmpty ? lib.first : null; final asset = lib.isNotEmpty ? lib.first : null;
final currentPage = container.read(pdfViewModelProvider); final currentPage = container.read(pdfViewModelProvider).currentPage;
container container
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.addPlacement(page: currentPage, rect: r, asset: asset); .addPlacement(page: currentPage, rect: r, asset: asset);
// Clear active overlay by hiding signatures temporarily // Clear active overlay by hiding signatures temporarily
container.read(signatureVisibilityProvider.notifier).state = false; // Note: signatureVisibilityProvider was removed in migration
// container.read(signatureVisibilityProvider.notifier).state = false;
await tester.pump(); await tester.pump();
container.read(signatureVisibilityProvider.notifier).state = true; // container.read(signatureVisibilityProvider.notifier).state = true;
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final placed = find.byKey(const Key('placed_signature_0')); final placed = find.byKey(const Key('placed_signature_0'));
@ -192,13 +205,19 @@ void main() {
DocumentStateNotifier() DocumentStateNotifier()
..openPicked(pageCount: 3, bytes: pdfBytes), ..openPicked(pageCount: 3, bytes: pdfBytes),
), ),
useMockViewerProvider.overrideWithValue(false), pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false),
),
], ],
child: const MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'), locale: Locale('en'),
home: PdfSignatureHomePage(), home: PdfSignatureHomePage(
onPickPdf: () async {},
onClosePdf: () {},
currentFile: fs.XFile('test.pdf'),
),
), ),
), ),
); );
@ -232,13 +251,19 @@ void main() {
DocumentStateNotifier() DocumentStateNotifier()
..openPicked(pageCount: 3, bytes: pdfBytes), ..openPicked(pageCount: 3, bytes: pdfBytes),
), ),
useMockViewerProvider.overrideWithValue(false), pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false),
),
], ],
child: const MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'), locale: Locale('en'),
home: PdfSignatureHomePage(), home: PdfSignatureHomePage(
onPickPdf: () async {},
onClosePdf: () {},
currentFile: fs.XFile('test.pdf'),
),
), ),
), ),
); );
@ -275,13 +300,19 @@ void main() {
DocumentStateNotifier() DocumentStateNotifier()
..openPicked(pageCount: 3, bytes: pdfBytes), ..openPicked(pageCount: 3, bytes: pdfBytes),
), ),
useMockViewerProvider.overrideWithValue(false), pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false),
),
], ],
child: const MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'), locale: Locale('en'),
home: PdfSignatureHomePage(), home: PdfSignatureHomePage(
onPickPdf: () async {},
onClosePdf: () {},
currentFile: fs.XFile('test.pdf'),
),
), ),
), ),
); );
@ -321,13 +352,19 @@ void main() {
DocumentStateNotifier() DocumentStateNotifier()
..openPicked(pageCount: 3, bytes: pdfBytes), ..openPicked(pageCount: 3, bytes: pdfBytes),
), ),
useMockViewerProvider.overrideWithValue(false), pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false),
),
], ],
child: const MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'), locale: Locale('en'),
home: PdfSignatureHomePage(), home: PdfSignatureHomePage(
onPickPdf: () async {},
onClosePdf: () {},
currentFile: fs.XFile('test.pdf'),
),
), ),
), ),
); );

View File

@ -4,9 +4,9 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import 'dart:io'; import 'dart:io';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:file_selector/file_selector.dart' as fs;
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/pdf_providers.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pages_sidebar.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pages_sidebar.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:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -37,13 +37,19 @@ void main() {
DocumentStateNotifier() DocumentStateNotifier()
..openPicked(pageCount: 3, bytes: pdfBytes), ..openPicked(pageCount: 3, bytes: pdfBytes),
), ),
useMockViewerProvider.overrideWithValue(false), pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false),
),
], ],
child: const MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'), locale: Locale('en'),
home: PdfSignatureHomePage(), home: PdfSignatureHomePage(
onPickPdf: () async {},
onClosePdf: () {},
currentFile: fs.XFile('test.pdf'),
),
), ),
), ),
); );
@ -85,13 +91,19 @@ void main() {
DocumentStateNotifier() DocumentStateNotifier()
..openPicked(pageCount: 3, bytes: pdfBytes), ..openPicked(pageCount: 3, bytes: pdfBytes),
), ),
useMockViewerProvider.overrideWithValue(false), pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false),
),
], ],
child: const MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'), locale: Locale('en'),
home: PdfSignatureHomePage(), home: PdfSignatureHomePage(
onPickPdf: () async {},
onClosePdf: () {},
currentFile: fs.XFile('test.pdf'),
),
), ),
), ),
); );
@ -133,13 +145,19 @@ void main() {
DocumentStateNotifier() DocumentStateNotifier()
..openPicked(pageCount: 3, bytes: pdfBytes), ..openPicked(pageCount: 3, bytes: pdfBytes),
), ),
useMockViewerProvider.overrideWithValue(false), pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false),
),
], ],
child: const MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'), locale: Locale('en'),
home: PdfSignatureHomePage(), home: PdfSignatureHomePage(
onPickPdf: () async {},
onClosePdf: () {},
currentFile: fs.XFile('test.pdf'),
),
), ),
), ),
); );
@ -182,13 +200,19 @@ void main() {
DocumentStateNotifier() DocumentStateNotifier()
..openPicked(pageCount: 3, bytes: pdfBytes), ..openPicked(pageCount: 3, bytes: pdfBytes),
), ),
useMockViewerProvider.overrideWithValue(false), pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false),
),
], ],
child: const MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'), locale: Locale('en'),
home: PdfSignatureHomePage(), home: PdfSignatureHomePage(
onPickPdf: () async {},
onClosePdf: () {},
currentFile: fs.XFile('test.pdf'),
),
), ),
), ),
); );

View File

@ -2,11 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_localized_locales/flutter_localized_locales.dart'; import 'package:flutter_localized_locales/flutter_localized_locales.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; import 'package:pdf_signature/routing/router.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart';
import 'package:pdf_signature/ui/features/welcome/widgets/welcome_screen.dart';
import 'data/repositories/preferences_repository.dart';
import 'package:pdf_signature/ui/features/preferences/widgets/settings_screen.dart'; import 'package:pdf_signature/ui/features/preferences/widgets/settings_screen.dart';
import 'data/repositories/preferences_repository.dart';
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({super.key}); const MyApp({super.key});
@ -42,7 +40,7 @@ class MyApp extends StatelessWidget {
data: (_) { data: (_) {
final themeMode = ref.watch(themeModeProvider); final themeMode = ref.watch(themeModeProvider);
final appLocale = ref.watch(localeProvider); final appLocale = ref.watch(localeProvider);
return MaterialApp( return MaterialApp.router(
onGenerateTitle: (ctx) => AppLocalizations.of(ctx).appTitle, onGenerateTitle: (ctx) => AppLocalizations.of(ctx).appTitle,
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(
@ -63,27 +61,27 @@ class MyApp extends StatelessWidget {
...AppLocalizations.localizationsDelegates, ...AppLocalizations.localizationsDelegates,
LocaleNamesLocalizationsDelegate(), LocaleNamesLocalizationsDelegate(),
], ],
home: Builder( routerConfig: ref.watch(routerProvider),
builder: builder: (context, child) {
(ctx) => Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(ctx).appTitle), title: Text(AppLocalizations.of(context).appTitle),
actions: [ actions: [
OutlinedButton.icon( OutlinedButton.icon(
key: const Key('btn_appbar_settings'), key: const Key('btn_appbar_settings'),
icon: const Icon(Icons.settings), icon: const Icon(Icons.settings),
label: Text(AppLocalizations.of(ctx).settings), label: Text(AppLocalizations.of(context).settings),
onPressed: onPressed:
() => showDialog<bool>( () => showDialog<bool>(
context: ctx, context: context,
builder: (_) => const SettingsDialog(), builder: (_) => const SettingsDialog(),
), ),
), ),
], ],
), ),
body: const _RootHomeSwitcher(), body: child,
), );
), },
); );
}, },
); );
@ -92,16 +90,3 @@ class MyApp extends StatelessWidget {
); );
} }
} }
class _RootHomeSwitcher extends ConsumerWidget {
const _RootHomeSwitcher();
@override
Widget build(BuildContext context, WidgetRef ref) {
final pdf = ref.watch(documentRepositoryProvider);
if (!pdf.loaded) {
return const WelcomeScreen();
}
return const PdfSignatureHomePage();
}
}

View File

@ -15,10 +15,7 @@ class DocumentStateNotifier extends StateNotifier<Document> {
state = state.copyWith(loaded: true, pageCount: 5, placementsByPage: {}); state = state.copyWith(loaded: true, pageCount: 5, placementsByPage: {});
} }
void openPicked({ void openPicked({required int pageCount, Uint8List? bytes}) {
required int pageCount,
Uint8List? bytes,
}) {
state = state.copyWith( state = state.copyWith(
loaded: true, loaded: true,
pageCount: pageCount, pageCount: pageCount,
@ -27,6 +24,10 @@ class DocumentStateNotifier extends StateNotifier<Document> {
); );
} }
void close() {
state = Document.initial();
}
void setPageCount(int count) { void setPageCount(int count) {
if (!state.loaded) return; if (!state.loaded) return;
state = state.copyWith(pageCount: count.clamp(1, 9999)); state = state.copyWith(pageCount: count.clamp(1, 9999));

121
lib/routing/router.dart Normal file
View File

@ -0,0 +1,121 @@
import 'dart:typed_data';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
import 'package:pdf_signature/ui/features/welcome/widgets/welcome_screen.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart';
import 'package:pdf_signature/data/repositories/signature_card_repository.dart';
import 'package:file_selector/file_selector.dart' as fs;
import 'package:pdfrx/pdfrx.dart';
class PdfManager {
final DocumentStateNotifier _documentNotifier;
final SignatureCardStateNotifier _signatureCardNotifier;
final GoRouter _router;
fs.XFile _currentFile = fs.XFile('');
PdfManager({
required DocumentStateNotifier documentNotifier,
required SignatureCardStateNotifier signatureCardNotifier,
required GoRouter router,
}) : _documentNotifier = documentNotifier,
_signatureCardNotifier = signatureCardNotifier,
_router = router;
fs.XFile get currentFile => _currentFile;
Future<void> openPdf({String? path, Uint8List? bytes}) async {
int pageCount = 1; // default
if (bytes != null) {
try {
final doc = await PdfDocument.openData(bytes);
pageCount = doc.pages.length;
} catch (_) {
// ignore
}
}
// Update file reference if path is provided
if (path != null) {
_currentFile = fs.XFile(path);
}
_documentNotifier.openPicked(pageCount: pageCount, bytes: bytes);
_signatureCardNotifier.clearAll();
// Navigate to PDF screen after successfully opening PDF
_router.go('/pdf');
}
void closePdf() {
_documentNotifier.close();
_signatureCardNotifier.clearAll();
_currentFile = fs.XFile('');
// Navigate back to welcome screen when closing PDF
_router.go('/');
}
Future<void> pickAndOpenPdf() async {
final typeGroup = const fs.XTypeGroup(label: 'PDF', extensions: ['pdf']);
final file = await fs.openFile(acceptedTypeGroups: [typeGroup]);
if (file != null) {
Uint8List? bytes;
try {
bytes = await file.readAsBytes();
} catch (_) {
bytes = null;
}
await openPdf(path: file.path, bytes: bytes);
}
}
}
final routerProvider = Provider<GoRouter>((ref) {
// Create PdfManager instance with dependencies
final documentNotifier = ref.read(documentRepositoryProvider.notifier);
final signatureCardNotifier = ref.read(
signatureCardRepositoryProvider.notifier,
);
// Create a late variable for the router
late final GoRouter router;
// Create PdfManager with router dependency (will be set after router creation)
late final PdfManager pdfManager;
router = GoRouter(
routes: [
GoRoute(
path: '/',
builder:
(context, state) => WelcomeScreen(
onPickPdf: () => pdfManager.pickAndOpenPdf(),
onOpenPdf:
({String? path, Uint8List? bytes, String? fileName}) =>
pdfManager.openPdf(path: path, bytes: bytes),
),
),
GoRoute(
path: '/pdf',
builder:
(context, state) => PdfSignatureHomePage(
onPickPdf: () => pdfManager.pickAndOpenPdf(),
onClosePdf: () => pdfManager.closePdf(),
currentFile: pdfManager.currentFile,
),
),
],
initialLocation: '/',
);
// Now create PdfManager with the router
pdfManager = PdfManager(
documentNotifier: documentNotifier,
signatureCardNotifier: signatureCardNotifier,
router: router,
);
return router;
});

View File

@ -1,30 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdfrx/pdfrx.dart';
/// Whether to use a mock continuous viewer (ListView) instead of a real PDF viewer.
/// Tests will override this to true.
final useMockViewerProvider = Provider<bool>(
(ref) => const bool.fromEnvironment('FLUTTER_TEST', defaultValue: false),
);
/// Global visibility toggle for signature overlays (placed items). Kept simple for tests.
final signatureVisibilityProvider = StateProvider<bool>((ref) => true);
/// Whether resizing keeps the current aspect ratio for the active overlay
final aspectLockedProvider = StateProvider<bool>((ref) => false);
/// Current active overlay rect (normalized 0..1) for the mock viewer.
/// Integration tests can read this to confirm or compute placements.
final activeRectProvider = StateProvider<Rect?>((ref) => null);
/// Exposes the PdfViewerController so toolbar / thumbnails can invoke navigation.
/// It must be overridden at runtime by the hosting screen (e.g. `PdfSignatureHomePage`).
// Default controller (can be overridden by a screen to ensure a stable instance within its subtree).
final PdfViewerController _defaultPdfViewerController = PdfViewerController();
final pdfViewerControllerProvider = Provider<PdfViewerController>((ref) {
return _defaultPdfViewerController;
});
/// Current page (1-based). Updated by PdfViewer via onPageChanged.
final currentPageProvider = StateProvider<int>((ref) => 1);

View File

@ -6,15 +6,41 @@ 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:pdfrx/pdfrx.dart'; import 'package:pdfrx/pdfrx.dart';
class PdfViewModel extends StateNotifier<int> { class PdfViewModel extends ChangeNotifier {
final Ref ref; final Ref ref;
PdfViewerController _controller = PdfViewerController();
PdfViewerController get controller => _controller;
int _currentPage = 1;
late final bool _useMockViewer;
PdfViewModel(this.ref) : super(1); // Active rect for signature placement overlay
Rect? _activeRect;
Rect? get activeRect => _activeRect;
set activeRect(Rect? value) {
_activeRect = value;
notifyListeners();
}
Document get document => ref.read(documentRepositoryProvider); // const bool.fromEnvironment('FLUTTER_TEST', defaultValue: false);
PdfViewModel(this.ref, {bool? useMockViewer})
: _useMockViewer =
useMockViewer ??
bool.fromEnvironment('FLUTTER_TEST', defaultValue: false);
bool get useMockViewer => _useMockViewer;
int get currentPage => _currentPage;
set currentPage(int value) {
_currentPage = value.clamp(1, document.pageCount);
notifyListeners();
}
Document get document => ref.watch(documentRepositoryProvider);
void jumpToPage(int page) { void jumpToPage(int page) {
state = page.clamp(1, document.pageCount); currentPage = page;
} }
Future<void> openPdf({required String path, Uint8List? bytes}) async { Future<void> openPdf({required String path, Uint8List? bytes}) async {
@ -30,37 +56,125 @@ class PdfViewModel extends StateNotifier<int> {
ref ref
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.openPicked(pageCount: pageCount, bytes: bytes); .openPicked(pageCount: pageCount, bytes: bytes);
clearAllSignatureCards();
currentPage = 1; // Reset current page to 1
}
// Document repository methods
void closeDocument() {
ref.read(documentRepositoryProvider.notifier).close();
}
void setPageCount(int count) {
ref.read(documentRepositoryProvider.notifier).setPageCount(count);
}
void addPlacement({
required int page,
required Rect rect,
SignatureAsset? asset,
double rotationDeg = 0.0,
GraphicAdjust? graphicAdjust,
}) {
ref
.read(documentRepositoryProvider.notifier)
.addPlacement(
page: page,
rect: rect,
asset: asset,
rotationDeg: rotationDeg,
graphicAdjust: graphicAdjust,
);
}
void updatePlacementRotation({
required int page,
required int index,
required double rotationDeg,
}) {
ref
.read(documentRepositoryProvider.notifier)
.updatePlacementRotation(
page: page,
index: index,
rotationDeg: rotationDeg,
);
}
void removePlacement({required int page, required int index}) {
ref
.read(documentRepositoryProvider.notifier)
.removePlacement(page: page, index: index);
}
void updatePlacementRect({
required int page,
required int index,
required Rect rect,
}) {
ref
.read(documentRepositoryProvider.notifier)
.updatePlacementRect(page: page, index: index, rect: rect);
}
List<SignaturePlacement> placementsOn(int page) {
return ref.read(documentRepositoryProvider.notifier).placementsOn(page);
}
SignatureAsset? assetOfPlacement({required int page, required int index}) {
return ref
.read(documentRepositoryProvider.notifier)
.assetOfPlacement(page: page, index: index);
}
Future<void> exportDocument({
required String outputPath,
required Size uiPageSize,
required Uint8List? signatureImageBytes,
}) async {
await ref
.read(documentRepositoryProvider.notifier)
.exportDocument(
outputPath: outputPath,
uiPageSize: uiPageSize,
signatureImageBytes: signatureImageBytes,
);
}
// Signature card repository methods
List<SignatureCard> get signatureCards =>
ref.read(signatureCardRepositoryProvider);
void addSignatureCard(SignatureCard card) {
ref.read(signatureCardRepositoryProvider.notifier).add(card);
}
void addSignatureCardWithAsset(SignatureAsset asset, double rotationDeg) {
ref
.read(signatureCardRepositoryProvider.notifier)
.addWithAsset(asset, rotationDeg);
}
void updateSignatureCard(
SignatureCard card,
double? rotationDeg,
GraphicAdjust? graphicAdjust,
) {
ref
.read(signatureCardRepositoryProvider.notifier)
.update(card, rotationDeg, graphicAdjust);
}
void removeSignatureCard(SignatureCard card) {
ref.read(signatureCardRepositoryProvider.notifier).remove(card);
}
void clearAllSignatureCards() {
ref.read(signatureCardRepositoryProvider.notifier).clearAll(); ref.read(signatureCardRepositoryProvider.notifier).clearAll();
state = 1; // Reset current page to 1
}
Future<Uint8List?> loadSignatureFromFile() async {
// This would need file picker, but since it's UI logic, perhaps keep in widget
// For now, return null
return null;
}
void confirmSignature() {
// Need to implement based on original logic
}
void onDragSignature(Offset delta) {
// Implement drag
}
void onResizeSignature(Offset delta) {
// Implement resize
}
void onSelectPlaced(int? index) {
// ref.read(documentRepositoryProvider.notifier).selectPlacement(index);
}
Future<void> saveSignedPdf() async {
// Implement save logic
} }
} }
final pdfViewModelProvider = StateNotifierProvider<PdfViewModel, int>((ref) { final pdfViewModelProvider = ChangeNotifierProvider<PdfViewModel>((ref) {
return PdfViewModel(ref); return PdfViewModel(ref);
}); });

View File

@ -1,11 +1,112 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'thumbnails_view.dart'; import 'package:pdfrx/pdfrx.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class ThumbnailsView extends ConsumerWidget {
const ThumbnailsView({
super.key,
required this.documentRef,
required this.controller,
required this.currentPage,
});
final PdfDocumentRefData documentRef;
final PdfViewerController controller;
final int currentPage;
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
return Container(
color: theme.colorScheme.surface,
child: PdfDocumentViewBuilder(
documentRef: documentRef,
builder: (context, document) {
final pageCount = document?.pages.length ?? 0;
return ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
itemCount: pageCount,
separatorBuilder: (_, _) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final pageNumber = index + 1;
final isSelected = currentPage == pageNumber;
return InkWell(
onTap: () {
controller.goToPage(
pageNumber: pageNumber,
anchor: PdfPageAnchor.top,
);
},
child: DecoratedBox(
decoration: BoxDecoration(
color:
isSelected
? theme.colorScheme.primaryContainer
: theme.cardColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color:
isSelected
? theme.colorScheme.primary
: theme.dividerColor,
),
),
child: Padding(
padding: const EdgeInsets.all(6),
child: Column(
children: [
SizedBox(
height: 180,
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: PdfPageView(
document: document,
pageNumber: pageNumber,
alignment: Alignment.center,
),
),
),
const SizedBox(height: 4),
Text('$pageNumber', style: theme.textTheme.bodySmall),
],
),
),
),
);
},
);
},
),
);
}
}
class PagesSidebar extends StatelessWidget { class PagesSidebar extends StatelessWidget {
const PagesSidebar({super.key}); const PagesSidebar({
super.key,
required this.documentRef,
required this.controller,
required this.currentPage,
});
final PdfDocumentRefData? documentRef;
final PdfViewerController controller;
final int currentPage;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card(margin: EdgeInsets.zero, child: const ThumbnailsView()); if (documentRef == null) {
return Card(margin: EdgeInsets.zero, child: const SizedBox.shrink());
}
return Card(
margin: EdgeInsets.zero,
child: ThumbnailsView(
documentRef: documentRef!,
controller: controller,
currentPage: currentPage,
),
);
} }
} }

View File

@ -4,11 +4,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
import 'pdf_page_overlays.dart'; import 'pdf_page_overlays.dart';
import '../view_model/pdf_providers.dart';
import 'package:pdf_signature/data/repositories/signature_asset_repository.dart'; import 'package:pdf_signature/data/repositories/signature_asset_repository.dart';
// using only adjusted overlay, no direct model imports needed // using only adjusted overlay, no direct model imports needed
import '../../signature/widgets/signature_drag_data.dart'; import '../../signature/widgets/signature_drag_data.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart'; import '../view_model/pdf_view_model.dart';
/// Mocked continuous viewer for tests or platforms without real viewer. /// Mocked continuous viewer for tests or platforms without real viewer.
@visibleForTesting @visibleForTesting
@ -57,7 +56,6 @@ class _PdfMockContinuousListState extends ConsumerState<PdfMockContinuousList> {
final pendingPage = widget.pendingPage; final pendingPage = widget.pendingPage;
final scrollToPage = widget.scrollToPage; final scrollToPage = widget.scrollToPage;
final clearPending = widget.clearPending; final clearPending = widget.clearPending;
final visible = ref.watch(signatureVisibilityProvider);
final assets = ref.watch(signatureAssetRepositoryProvider); final assets = ref.watch(signatureAssetRepositoryProvider);
if (pendingPage != null) { if (pendingPage != null) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@ -111,7 +109,7 @@ class _PdfMockContinuousListState extends ConsumerState<PdfMockContinuousList> {
// Add placement to the document // Add placement to the document
ref ref
.read(documentRepositoryProvider.notifier) .read(pdfViewModelProvider.notifier)
.addPlacement( .addPlacement(
page: pageNum, page: pageNum,
rect: rect, rect: rect,
@ -151,8 +149,7 @@ class _PdfMockContinuousListState extends ConsumerState<PdfMockContinuousList> {
); );
}, },
), ),
visible Stack(
? Stack(
children: [ children: [
PdfPageOverlays( PdfPageOverlays(
pageSize: pageSize, pageSize: pageSize,
@ -175,17 +172,8 @@ class _PdfMockContinuousListState extends ConsumerState<PdfMockContinuousList> {
final width = final width =
_activeRect.width * constraints.maxWidth; _activeRect.width * constraints.maxWidth;
final height = final height =
_activeRect.height * _activeRect.height * constraints.maxHeight;
constraints.maxHeight;
// Publish rect for tests/other UI to observe // Publish rect for tests/other UI to observe
WidgetsBinding.instance.addPostFrameCallback((
_,
) {
if (!mounted) return;
ref
.read(activeRectProvider.notifier)
.state = _activeRect;
});
return Stack( return Stack(
children: [ children: [
Positioned( Positioned(
@ -219,9 +207,7 @@ class _PdfMockContinuousListState extends ConsumerState<PdfMockContinuousList> {
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
border: Border.all( border: Border.all(color: Colors.red),
color: Colors.red,
),
), ),
), ),
), ),
@ -231,8 +217,7 @@ class _PdfMockContinuousListState extends ConsumerState<PdfMockContinuousList> {
}, },
), ),
], ],
) ),
: const SizedBox.shrink(),
], ],
), ),
), ),

View File

@ -3,10 +3,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
// Real viewer removed in migration; mock continuous list is used in tests. // Real viewer removed in migration; mock continuous list is used in tests.
import 'package:pdf_signature/data/repositories/document_repository.dart';
import 'pdf_viewer_widget.dart'; import 'pdf_viewer_widget.dart';
import 'package:pdfrx/pdfrx.dart';
import '../view_model/pdf_view_model.dart'; import '../view_model/pdf_view_model.dart';
import '../view_model/pdf_providers.dart';
class PdfPageArea extends ConsumerStatefulWidget { class PdfPageArea extends ConsumerStatefulWidget {
const PdfPageArea({ const PdfPageArea({
@ -17,6 +16,7 @@ class PdfPageArea extends ConsumerStatefulWidget {
required this.onConfirmSignature, required this.onConfirmSignature,
required this.onClearActiveOverlay, required this.onClearActiveOverlay,
required this.onSelectPlaced, required this.onSelectPlaced,
required this.controller,
}); });
final Size pageSize; final Size pageSize;
@ -26,6 +26,7 @@ class PdfPageArea extends ConsumerStatefulWidget {
final VoidCallback onConfirmSignature; final VoidCallback onConfirmSignature;
final VoidCallback onClearActiveOverlay; final VoidCallback onClearActiveOverlay;
final ValueChanged<int?> onSelectPlaced; final ValueChanged<int?> onSelectPlaced;
final PdfViewerController controller;
@override @override
ConsumerState<PdfPageArea> createState() => _PdfPageAreaState(); ConsumerState<PdfPageArea> createState() => _PdfPageAreaState();
} }
@ -117,20 +118,21 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final pdf = ref.watch(documentRepositoryProvider); final pdfViewModel = ref.watch(pdfViewModelProvider);
final pdf = pdfViewModel.document;
const pageViewMode = 'continuous'; const pageViewMode = 'continuous';
// React to PdfViewModel (source of truth for current page) // React to PdfViewModel (source of truth for current page)
ref.listen<int>(pdfViewModelProvider, (prev, next) { ref.listen(pdfViewModelProvider, (prev, next) {
if (prev != next) { if (prev?.currentPage != next.currentPage) {
_scrollToPage(next); _scrollToPage(next.currentPage);
} }
}); });
// React to provider currentPage changes (e.g., user tapped overview) // React to provider currentPage changes (e.g., user tapped overview)
ref.listen(currentPageProvider, (prev, next) { ref.listen(pdfViewModelProvider, (prev, next) {
if (_suppressProviderListen) return; if (_suppressProviderListen) return;
if (prev != next) { if (prev?.currentPage != next.currentPage) {
final target = next; final target = next.currentPage;
// If we're already navigating to this target, ignore; otherwise allow new target. // If we're already navigating to this target, ignore; otherwise allow new target.
if (_programmaticTargetPage != null && if (_programmaticTargetPage != null &&
_programmaticTargetPage == target) { _programmaticTargetPage == target) {
@ -159,7 +161,6 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
// Use real PDF viewer // Use real PDF viewer
if (isContinuous) { if (isContinuous) {
final controller = ref.watch(pdfViewerControllerProvider);
return PdfViewerWidget( return PdfViewerWidget(
pageSize: widget.pageSize, pageSize: widget.pageSize,
onDragSignature: widget.onDragSignature, onDragSignature: widget.onDragSignature,
@ -169,7 +170,7 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
onSelectPlaced: widget.onSelectPlaced, onSelectPlaced: widget.onSelectPlaced,
pageKeyBuilder: _pageKey, pageKeyBuilder: _pageKey,
scrollToPage: _scrollToPage, scrollToPage: _scrollToPage,
controller: controller, controller: widget.controller,
); );
} }
return const SizedBox.shrink(); return const SizedBox.shrink();

View File

@ -1,10 +1,9 @@
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/ui/features/pdf/view_model/pdf_view_model.dart';
import '../../../../domain/models/model.dart'; import '../../../../domain/models/model.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart';
import 'signature_overlay.dart'; import 'signature_overlay.dart';
import '../view_model/pdf_providers.dart';
/// Builds all overlays for a given page: placed signatures and the active one. /// Builds all overlays for a given page: placed signatures and the active one.
class PdfPageOverlays extends ConsumerWidget { class PdfPageOverlays extends ConsumerWidget {
@ -29,9 +28,11 @@ class PdfPageOverlays extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final pdf = ref.watch(documentRepositoryProvider); final pdfViewModel = ref.watch(pdfViewModelProvider);
final pdf = pdfViewModel.document;
final placed = final placed =
pdf.placementsByPage[pageNumber] ?? const <SignaturePlacement>[]; pdf.placementsByPage[pageNumber] ?? const <SignaturePlacement>[];
final activeRect = pdfViewModel.activeRect;
final widgets = <Widget>[]; final widgets = <Widget>[];
for (int i = 0; i < placed.length; i++) { for (int i = 0; i < placed.length; i++) {
@ -48,9 +49,9 @@ class PdfPageOverlays extends ConsumerWidget {
); );
} }
// Add active overlay if present and not using mock (mock has its own) // TODO:Add active overlay if present and not using mock (mock has its own)
final activeRect = ref.watch(activeRectProvider);
final useMock = ref.watch(useMockViewerProvider); final useMock = pdfViewModel.useMockViewer;
if (!useMock && activeRect != null) { if (!useMock && activeRect != null) {
widgets.add( widgets.add(
LayoutBuilder( LayoutBuilder(

View File

@ -1,86 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdfrx/pdfrx.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart';
import '../view_model/pdf_providers.dart';
class PdfPagesOverview extends ConsumerWidget {
const PdfPagesOverview({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final pdf = ref.watch(documentRepositoryProvider);
final controller = ref.watch(pdfViewerControllerProvider);
final theme = Theme.of(context);
if (!pdf.loaded || pdf.pickedPdfBytes == null)
return const SizedBox.shrink();
final documentRef = PdfDocumentRefData(
pdf.pickedPdfBytes!,
sourceName: 'document.pdf',
);
return Container(
color: theme.colorScheme.surface,
child: PdfDocumentViewBuilder(
documentRef: documentRef,
builder: (context, document) {
final pageCount = document?.pages.length ?? 0;
return ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
itemCount: pageCount,
separatorBuilder: (_, _) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final pageNumber = index + 1;
final isSelected = ref.watch(currentPageProvider) == pageNumber;
return InkWell(
onTap: () {
controller.goToPage(
pageNumber: pageNumber,
anchor: PdfPageAnchor.top,
);
},
child: DecoratedBox(
decoration: BoxDecoration(
color:
isSelected
? theme.colorScheme.primaryContainer
: theme.cardColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color:
isSelected
? theme.colorScheme.primary
: theme.dividerColor,
),
),
child: Padding(
padding: const EdgeInsets.all(6),
child: Column(
children: [
SizedBox(
height: 180,
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: PdfPageView(
document: document,
pageNumber: pageNumber,
alignment: Alignment.center,
),
),
),
const SizedBox(height: 4),
Text('$pageNumber', style: theme.textTheme.bodySmall),
],
),
),
),
);
},
);
},
),
);
}
}

View File

@ -7,8 +7,6 @@ import 'package:pdf_signature/data/repositories/preferences_repository.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
import 'package:multi_split_view/multi_split_view.dart'; import 'package:multi_split_view/multi_split_view.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart';
import '../view_model/pdf_providers.dart';
import 'package:pdfrx/pdfrx.dart'; import 'package:pdfrx/pdfrx.dart';
import 'draw_canvas.dart'; import 'draw_canvas.dart';
import 'pdf_toolbar.dart'; import 'pdf_toolbar.dart';
@ -19,7 +17,16 @@ import 'ui_services.dart';
import '../view_model/pdf_view_model.dart'; import '../view_model/pdf_view_model.dart';
class PdfSignatureHomePage extends ConsumerStatefulWidget { class PdfSignatureHomePage extends ConsumerStatefulWidget {
const PdfSignatureHomePage({super.key}); final Future<void> Function() onPickPdf;
final VoidCallback onClosePdf;
final fs.XFile currentFile;
const PdfSignatureHomePage({
super.key,
required this.onPickPdf,
required this.onClosePdf,
required this.currentFile,
});
@override @override
ConsumerState<PdfSignatureHomePage> createState() => ConsumerState<PdfSignatureHomePage> createState() =>
@ -31,7 +38,6 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
bool _showPagesSidebar = true; bool _showPagesSidebar = true;
bool _showSignaturesSidebar = true; bool _showSignaturesSidebar = true;
int _zoomLevel = 100; // percentage for display only int _zoomLevel = 100; // percentage for display only
fs.XFile _file = fs.XFile('');
// Split view controller to manage resizable sidebars without remounting the center area. // Split view controller to manage resizable sidebars without remounting the center area.
late final MultiSplitViewController _splitController; late final MultiSplitViewController _splitController;
@ -43,6 +49,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
final double _pagesMax = 250; final double _pagesMax = 250;
final double _signaturesMin = 140; final double _signaturesMin = 140;
final double _signaturesMax = 250; final double _signaturesMax = 250;
late PdfViewModel _viewModel;
// Exposed for tests to trigger the invalid-file SnackBar without UI. // Exposed for tests to trigger the invalid-file SnackBar without UI.
@visibleForTesting @visibleForTesting
@ -55,38 +62,17 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
} }
Future<void> _pickPdf() async { Future<void> _pickPdf() async {
final typeGroup = const fs.XTypeGroup(label: 'PDF', extensions: ['pdf']); await widget.onPickPdf();
final file = await fs.openFile(acceptedTypeGroups: [typeGroup]);
if (file != null) {
setState(() {
_file = file;
});
Uint8List? bytes;
try {
bytes = await file.readAsBytes();
} catch (_) {
bytes = null;
}
// infer page count if possible
int pageCount = 1;
if (bytes != null) {
try {
final doc = await PdfDocument.openData(bytes);
pageCount = doc.pages.length;
} catch (_) {
// ignore
}
}
ref
.read(documentRepositoryProvider.notifier)
.openPicked(pageCount: pageCount, bytes: bytes);
} }
void _closePdf() {
widget.onClosePdf();
} }
void _jumpToPage(int page) { void _jumpToPage(int page) {
final controller = ref.read(pdfViewerControllerProvider); final controller = _viewModel.controller;
final current = ref.read(currentPageProvider); final current = _viewModel.currentPage;
final pdf = ref.read(documentRepositoryProvider); final pdf = _viewModel.document;
int target; int target;
if (page == -1) { if (page == -1) {
target = (current - 1).clamp(1, pdf.pageCount); target = (current - 1).clamp(1, pdf.pageCount);
@ -95,10 +81,9 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
} }
// Update reactive page providers so UI/tests reflect navigation even if controller is a stub // Update reactive page providers so UI/tests reflect navigation even if controller is a stub
if (current != target) { if (current != target) {
ref.read(currentPageProvider.notifier).state = target;
// Also notify view model (if used elsewhere) via its public API // Also notify view model (if used elsewhere) via its public API
try { try {
ref.read(pdfViewModelProvider.notifier).jumpToPage(target); _viewModel.jumpToPage(target);
} catch (_) { } catch (_) {
// ignore if provider not available // ignore if provider not available
} }
@ -153,7 +138,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
Future<void> _saveSignedPdf() async { Future<void> _saveSignedPdf() async {
ref.read(exportingProvider.notifier).state = true; ref.read(exportingProvider.notifier).state = true;
try { try {
final pdf = ref.read(documentRepositoryProvider); final pdf = _viewModel.document;
final messenger = ScaffoldMessenger.of(context); final messenger = ScaffoldMessenger.of(context);
if (!pdf.loaded) { if (!pdf.loaded) {
messenger.showSnackBar( messenger.showSnackBar(
@ -219,6 +204,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
void initState() { void initState() {
super.initState(); super.initState();
// Build areas once with builders; keep these instances stable. // Build areas once with builders; keep these instances stable.
_viewModel = ref.read(pdfViewModelProvider.notifier);
_areas = [ _areas = [
Area( Area(
size: _lastPagesWidth, size: _lastPagesWidth,
@ -227,7 +213,26 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
builder: builder:
(context, area) => Offstage( (context, area) => Offstage(
offstage: !_showPagesSidebar, offstage: !_showPagesSidebar,
child: const PagesSidebar(), child: Consumer(
builder: (context, ref, child) {
final pdfViewModel = ref.watch(pdfViewModelProvider);
final pdf = pdfViewModel.document;
final documentRef =
pdf.loaded && pdf.pickedPdfBytes != null
? PdfDocumentRefData(
pdf.pickedPdfBytes!,
sourceName: 'document.pdf',
)
: null;
return PagesSidebar(
documentRef: documentRef,
controller: _viewModel.controller,
currentPage: _viewModel.currentPage,
);
},
),
), ),
), ),
Area( Area(
@ -235,6 +240,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
builder: builder:
(context, area) => RepaintBoundary( (context, area) => RepaintBoundary(
child: PdfPageArea( child: PdfPageArea(
controller: _viewModel.controller,
key: const ValueKey('pdf_page_area'), key: const ValueKey('pdf_page_area'),
pageSize: _pageSize, pageSize: _pageSize,
onDragSignature: _onDragSignature, onDragSignature: _onDragSignature,
@ -300,15 +306,9 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Provide controller override so descendants can access it. return _buildScaffold(context);
return ProviderScope(
overrides: [pdfViewerControllerProvider.overrideWithValue(_controller)],
child: _buildScaffold(context),
);
} }
late final PdfViewerController _controller = PdfViewerController();
Widget _buildScaffold(BuildContext context) { Widget _buildScaffold(BuildContext context) {
final isExporting = ref.watch(exportingProvider); final isExporting = ref.watch(exportingProvider);
final l = AppLocalizations.of(context); final l = AppLocalizations.of(context);
@ -323,6 +323,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
PdfToolbar( PdfToolbar(
disabled: isExporting, disabled: isExporting,
onPickPdf: _pickPdf, onPickPdf: _pickPdf,
onClosePdf: _closePdf,
onJumpToPage: _jumpToPage, onJumpToPage: _jumpToPage,
onZoomOut: () { onZoomOut: () {
setState(() { setState(() {
@ -335,7 +336,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
}); });
}, },
zoomLevel: _zoomLevel, zoomLevel: _zoomLevel,
fileName: _file.name, filePath: widget.currentFile.path,
showPagesSidebar: _showPagesSidebar, showPagesSidebar: _showPagesSidebar,
showSignaturesSidebar: _showSignaturesSidebar, showSignaturesSidebar: _showSignaturesSidebar,
onTogglePagesSidebar: onTogglePagesSidebar:

View File

@ -3,19 +3,19 @@ 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 'package:pdf_signature/data/repositories/document_repository.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
import '../view_model/pdf_providers.dart';
class PdfToolbar extends ConsumerStatefulWidget { class PdfToolbar extends ConsumerStatefulWidget {
const PdfToolbar({ const PdfToolbar({
super.key, super.key,
required this.disabled, required this.disabled,
required this.onPickPdf, required this.onPickPdf,
required this.onClosePdf,
required this.onJumpToPage, required this.onJumpToPage,
required this.onZoomOut, required this.onZoomOut,
required this.onZoomIn, required this.onZoomIn,
this.zoomLevel, this.zoomLevel,
this.fileName, this.filePath,
required this.showPagesSidebar, required this.showPagesSidebar,
required this.showSignaturesSidebar, required this.showSignaturesSidebar,
required this.onTogglePagesSidebar, required this.onTogglePagesSidebar,
@ -24,8 +24,9 @@ class PdfToolbar extends ConsumerStatefulWidget {
final bool disabled; final bool disabled;
final VoidCallback onPickPdf; final VoidCallback onPickPdf;
final VoidCallback onClosePdf;
final ValueChanged<int> onJumpToPage; final ValueChanged<int> onJumpToPage;
final String? fileName; final String? filePath;
final VoidCallback onZoomOut; final VoidCallback onZoomOut;
final VoidCallback onZoomIn; final VoidCallback onZoomIn;
// Current zoom level as a percentage (e.g., 100 for 100%) // Current zoom level as a percentage (e.g., 100 for 100%)
@ -56,8 +57,9 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final pdf = ref.watch(documentRepositoryProvider); final pdfViewModel = ref.watch(pdfViewModelProvider);
final currentPage = ref.watch(currentPageProvider); final pdf = pdfViewModel.document;
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);
@ -83,9 +85,9 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 220), constraints: const BoxConstraints(maxWidth: 220),
child: Text( child: Text(
// if filename not null // if filePath not null
widget.fileName != null widget.filePath != null
? widget.fileName! ? widget.filePath!
: 'No file selected', : 'No file selected',
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -94,6 +96,12 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
), ),
), ),
if (pdf.loaded) ...[ if (pdf.loaded) ...[
IconButton(
key: const Key('btn_close_pdf'),
onPressed: widget.disabled ? null : widget.onClosePdf,
icon: const Icon(Icons.close),
tooltip: l.close,
),
Wrap( Wrap(
spacing: 8, spacing: 8,
children: [ children: [

View File

@ -1,11 +1,9 @@
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:pdfrx/pdfrx.dart'; import 'package:pdfrx/pdfrx.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; 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_providers.dart';
import '../view_model/pdf_view_model.dart'; import '../view_model/pdf_view_model.dart';
class PdfViewerWidget extends ConsumerStatefulWidget { class PdfViewerWidget extends ConsumerStatefulWidget {
@ -55,11 +53,10 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final document = ref.watch(documentRepositoryProvider); final pdfViewModel = ref.watch(pdfViewModelProvider);
final useMock = ref.watch(useMockViewerProvider); final document = pdfViewModel.document;
ref.watch(activeRectProvider); // trigger rebuild when active rect changes final useMock = pdfViewModel.useMockViewer;
// Watch to rebuild on page change // trigger rebuild when active rect changes
ref.watch(currentPageProvider);
// Update document ref when document changes // Update document ref when document changes
if (document.loaded && document.pickedPdfBytes != null) { if (document.loaded && document.pickedPdfBytes != null) {
@ -109,12 +106,11 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
onViewerReady: (document, controller) { onViewerReady: (document, controller) {
// Update page count in repository // Update page count in repository
ref ref
.read(documentRepositoryProvider.notifier) .read(pdfViewModelProvider.notifier)
.setPageCount(document.pages.length); .setPageCount(document.pages.length);
}, },
onPageChanged: (page) { onPageChanged: (page) {
if (page != null) { if (page != null) {
ref.read(currentPageProvider.notifier).state = page;
// Also update the view model to keep them in sync // Also update the view model to keep them in sync
ref.read(pdfViewModelProvider.notifier).jumpToPage(page); ref.read(pdfViewModelProvider.notifier).jumpToPage(page);
} }
@ -123,7 +119,7 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
return [ return [
PdfPageOverlays( PdfPageOverlays(
pageSize: widget.pageSize, pageSize: widget.pageSize,
pageNumber: ref.watch(currentPageProvider), pageNumber: pdfViewModel.currentPage,
onDragSignature: widget.onDragSignature, onDragSignature: widget.onDragSignature,
onResizeSignature: widget.onResizeSignature, onResizeSignature: widget.onResizeSignature,
onConfirmSignature: widget.onConfirmSignature, onConfirmSignature: widget.onConfirmSignature,

View File

@ -3,7 +3,7 @@ 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 'signature_drawer.dart'; import '../../signature/widgets/signature_drawer.dart';
import 'ui_services.dart'; import 'ui_services.dart';
class SignaturesSidebar extends ConsumerWidget { class SignaturesSidebar extends ConsumerWidget {

View File

@ -1,86 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdfrx/pdfrx.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart';
import '../view_model/pdf_providers.dart';
class ThumbnailsView extends ConsumerWidget {
const ThumbnailsView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final pdf = ref.watch(documentRepositoryProvider);
final controller = ref.watch(pdfViewerControllerProvider);
final theme = Theme.of(context);
if (!pdf.loaded || pdf.pickedPdfBytes == null)
return const SizedBox.shrink();
final documentRef = PdfDocumentRefData(
pdf.pickedPdfBytes!,
sourceName: 'document.pdf',
);
return Container(
color: theme.colorScheme.surface,
child: PdfDocumentViewBuilder(
documentRef: documentRef,
builder: (context, document) {
final pageCount = document?.pages.length ?? 0;
return ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
itemCount: pageCount,
separatorBuilder: (_, _) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final pageNumber = index + 1;
final isSelected = ref.watch(currentPageProvider) == pageNumber;
return InkWell(
onTap: () {
controller.goToPage(
pageNumber: pageNumber,
anchor: PdfPageAnchor.top,
);
},
child: DecoratedBox(
decoration: BoxDecoration(
color:
isSelected
? theme.colorScheme.primaryContainer
: theme.cardColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color:
isSelected
? theme.colorScheme.primary
: theme.dividerColor,
),
),
child: Padding(
padding: const EdgeInsets.all(6),
child: Column(
children: [
SizedBox(
height: 180,
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: PdfPageView(
document: document,
pageNumber: pageNumber,
alignment: Alignment.center,
),
),
),
const SizedBox(height: 4),
Text('$pageNumber', style: theme.textTheme.bodySmall),
],
),
),
),
);
},
);
},
),
);
}
}

View File

@ -6,9 +6,8 @@ import 'package:pdf_signature/l10n/app_localizations.dart';
import 'package:pdf_signature/data/repositories/signature_asset_repository.dart'; import 'package:pdf_signature/data/repositories/signature_asset_repository.dart';
import 'package:pdf_signature/data/repositories/signature_card_repository.dart'; import 'package:pdf_signature/data/repositories/signature_card_repository.dart';
import 'image_editor_dialog.dart'; import '../../pdf/widgets/image_editor_dialog.dart';
import '../../signature/widgets/signature_card.dart'; import 'signature_card.dart';
import '../view_model/pdf_providers.dart';
/// Data for drag-and-drop is in signature_drag_data.dart /// Data for drag-and-drop is in signature_drag_data.dart
@ -66,9 +65,7 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
); );
}, },
onTap: () { onTap: () {
ref // state = const Rect.fromLTWH(0.2, 0.2, 0.3, 0.15);
.read(activeRectProvider.notifier)
.state = const Rect.fromLTWH(0.2, 0.2, 0.3, 0.15);
}, },
), ),
), ),

View File

@ -1,8 +1,6 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
import 'package:pdf_signature/data/repositories/signature_card_repository.dart';
import 'package:pdfrx/pdfrx.dart';
class WelcomeViewModel { class WelcomeViewModel {
final Ref ref; final Ref ref;
@ -10,19 +8,9 @@ class WelcomeViewModel {
WelcomeViewModel(this.ref); WelcomeViewModel(this.ref);
Future<void> openPdf({required String path, Uint8List? bytes}) async { Future<void> openPdf({required String path, Uint8List? bytes}) async {
int pageCount = 1; // default await ref
if (bytes != null) { .read(pdfViewModelProvider.notifier)
try { .openPdf(path: path, bytes: bytes);
final doc = await PdfDocument.openData(bytes);
pageCount = doc.pages.length;
} catch (_) {
// ignore
}
}
ref
.read(documentRepositoryProvider.notifier)
.openPicked(pageCount: pageCount, bytes: bytes);
ref.read(signatureCardRepositoryProvider.notifier).clearAll();
} }
} }

View File

@ -1,12 +1,10 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:desktop_drop/desktop_drop.dart'; import 'package:desktop_drop/desktop_drop.dart';
import 'package:file_selector/file_selector.dart' as fs;
import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/foundation.dart' show kIsWeb;
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:pdf_signature/ui/features/welcome/view_model/welcome_view_model.dart';
// Abstraction to make drop handling testable without constructing // Abstraction to make drop handling testable without constructing
// platform-specific DropItem types in widget tests. // platform-specific DropItem types in widget tests.
@ -32,7 +30,8 @@ typedef Reader = T Function<T>(ProviderListenable<T> provider);
// Select first .pdf file (case-insensitive) or fall back to first entry. // Select first .pdf file (case-insensitive) or fall back to first entry.
Future<void> handleDroppedFiles( Future<void> handleDroppedFiles(
Reader read, Future<void> Function({String? path, Uint8List? bytes, String? fileName})
onOpenPdf,
Iterable<DropReadable> files, Iterable<DropReadable> files,
) async { ) async {
if (files.isEmpty) return; if (files.isEmpty) return;
@ -47,11 +46,23 @@ Future<void> handleDroppedFiles(
bytes = null; bytes = null;
} }
final String path = pdf.path ?? pdf.name; final String path = pdf.path ?? pdf.name;
await read(welcomeViewModelProvider).openPdf(path: path, bytes: bytes); await onOpenPdf(path: path, bytes: bytes);
} }
class WelcomeScreen extends ConsumerStatefulWidget { class WelcomeScreen extends ConsumerStatefulWidget {
const WelcomeScreen({super.key}); final Future<void> Function() onPickPdf;
final Future<void> Function({
String? path,
Uint8List? bytes,
String? fileName,
})
onOpenPdf;
const WelcomeScreen({
super.key,
required this.onPickPdf,
required this.onOpenPdf,
});
@override @override
ConsumerState<WelcomeScreen> createState() => _WelcomeScreenState(); ConsumerState<WelcomeScreen> createState() => _WelcomeScreenState();
@ -61,19 +72,7 @@ class _WelcomeScreenState extends ConsumerState<WelcomeScreen> {
bool _dragging = false; bool _dragging = false;
Future<void> _pickPdf() async { Future<void> _pickPdf() async {
final typeGroup = const fs.XTypeGroup(label: 'PDF', extensions: ['pdf']); await widget.onPickPdf();
final file = await fs.openFile(acceptedTypeGroups: [typeGroup]);
if (file != null) {
Uint8List? bytes;
try {
bytes = await file.readAsBytes();
} catch (_) {
bytes = null;
}
await ref
.read(welcomeViewModelProvider)
.openPdf(path: file.path, bytes: bytes);
}
} }
@override @override
@ -113,7 +112,7 @@ class _WelcomeScreenState extends ConsumerState<WelcomeScreen> {
final adapters = desktopFiles.map<DropReadable>( final adapters = desktopFiles.map<DropReadable>(
(f) => _DropReadableFromDesktop(f), (f) => _DropReadableFromDesktop(f),
); );
await handleDroppedFiles(ref.read, adapters); await handleDroppedFiles(widget.onOpenPdf, adapters);
}, },
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 150), duration: const Duration(milliseconds: 150),

View File

@ -70,6 +70,7 @@ dev_dependencies:
freezed: ^3.0.0 freezed: ^3.0.0
custom_lint: ^0.7.6 custom_lint: ^0.7.6
riverpod_lint: ^2.6.5 riverpod_lint: ^2.6.5
go_router_builder: ^4.0.1
# The "flutter_lints" package below contains a set of recommended lints to # The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is # encourage good coding practices. The lint set provided by the package is
@ -80,6 +81,7 @@ dev_dependencies:
msix: ^3.16.12 msix: ^3.16.12
json_serializable: ^6.11.0 json_serializable: ^6.11.0
dead_code_analyzer: ^1.1.0 dead_code_analyzer: ^1.1.0
faker_dart: ^0.2.3
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

View File

@ -2,11 +2,11 @@ import 'dart:typed_data';
import 'dart:ui'; 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/pdf_view_model.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:pdf_signature/app.dart'; import 'package:pdf_signature/app.dart';
import 'package:pdf_signature/data/repositories/preferences_repository.dart'; import 'package:pdf_signature/data/repositories/preferences_repository.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart'; import 'package:pdf_signature/data/repositories/document_repository.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/ui_services.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/ui_services.dart';
import 'package:pdf_signature/data/services/export_service.dart'; import 'package:pdf_signature/data/services/export_service.dart';
import 'package:pdf_signature/domain/models/model.dart'; import 'package:pdf_signature/domain/models/model.dart';
@ -48,7 +48,9 @@ Future<ProviderContainer> pumpApp(
documentRepositoryProvider.overrideWith( documentRepositoryProvider.overrideWith(
(ref) => DocumentStateNotifier()..openSample(), (ref) => DocumentStateNotifier()..openSample(),
), ),
useMockViewerProvider.overrideWith((ref) => true), pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true),
),
exportServiceProvider.overrideWith((ref) => fakeExport), exportServiceProvider.overrideWith((ref) => fakeExport),
savePathPickerProvider.overrideWith((ref) => () async => 'out.pdf'), savePathPickerProvider.overrideWith((ref) => () async => 'out.pdf'),
], ],

View File

@ -4,7 +4,7 @@ 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/data/repositories/signature_asset_repository.dart'; import 'package:pdf_signature/data/repositories/signature_asset_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/pdf_providers.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 '_world.dart'; import '_world.dart';
@ -21,7 +21,7 @@ Future<void> aMultipageDocumentIsOpen(WidgetTester tester) async {
container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 5); container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 5);
// Reset page state providers // Reset page state providers
try { try {
container.read(currentPageProvider.notifier).state = 1; container.read(pdfViewModelProvider.notifier).jumpToPage(1);
} catch (_) {} } catch (_) {}
try { try {
container.read(pdfViewModelProvider.notifier).jumpToPage(1); container.read(pdfViewModelProvider.notifier).jumpToPage(1);

View File

@ -36,7 +36,7 @@ Future<void> aSignatureAssetIsPlacedOnThePage(WidgetTester tester) async {
} }
// Place it on the current page // Place it on the current page
final currentPage = container.read(pdfViewModelProvider); final currentPage = container.read(pdfViewModelProvider).currentPage;
container container
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.addPlacement( .addPlacement(

View File

@ -13,7 +13,7 @@ Future<void> aSignaturePlacementIsPlacedWithAPositionAndSizeRelativeToThePage(
) async { ) async {
final container = TestWorld.container ?? ProviderContainer(); final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container; TestWorld.container = container;
final currentPage = container.read(pdfViewModelProvider); final currentPage = container.read(pdfViewModelProvider).currentPage;
container container
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.addPlacement( .addPlacement(

View File

@ -9,7 +9,7 @@ Future<void> draggingOrResizingOneDoesNotChangeTheOther(
WidgetTester tester, WidgetTester tester,
) async { ) async {
final container = TestWorld.container ?? ProviderContainer(); final container = TestWorld.container ?? ProviderContainer();
final page = container.read(pdfViewModelProvider); final page = container.read(pdfViewModelProvider).currentPage;
final list = container final list = container
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.placementsOn(page); .placementsOn(page);

View File

@ -1,6 +1,6 @@
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/pdf_providers.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 '_world.dart'; import '_world.dart';
@ -8,11 +8,10 @@ import '_world.dart';
Future<void> pageIsDisplayed(WidgetTester tester, num param1) async { Future<void> pageIsDisplayed(WidgetTester tester, num param1) async {
final expected = param1.toInt(); final expected = param1.toInt();
final c = TestWorld.container ?? ProviderContainer(); final c = TestWorld.container ?? ProviderContainer();
final vm = c.read(pdfViewModelProvider); final currentPage = c.read(pdfViewModelProvider).currentPage;
final legacy = c.read(currentPageProvider);
expect( expect(
vm == expected || legacy == expected, currentPage == expected,
true, true,
reason: 'Expected page $expected but got vm=$vm current=$legacy', reason: 'Expected page $expected but got current=$currentPage',
); );
} }

View File

@ -7,7 +7,8 @@ import 'package:pdf_signature/data/repositories/document_repository.dart';
import 'package:pdf_signature/data/repositories/signature_asset_repository.dart'; import 'package:pdf_signature/data/repositories/signature_asset_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/pdf_providers.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
import '_world.dart'; import '_world.dart';
class _BridgedSignatureCardStateNotifier extends SignatureCardStateNotifier { class _BridgedSignatureCardStateNotifier extends SignatureCardStateNotifier {
@ -37,7 +38,9 @@ Future<void> theAppLaunches(WidgetTester tester) async {
documentRepositoryProvider.overrideWith( documentRepositoryProvider.overrideWith(
(ref) => DocumentStateNotifier()..openSample(), (ref) => DocumentStateNotifier()..openSample(),
), ),
useMockViewerProvider.overrideWith((ref) => true), pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true),
),
// Bridge: automatically mirror assets into signature cards so legacy // Bridge: automatically mirror assets into signature cards so legacy
// feature steps that expect SignatureCard widgets keep working even // feature steps that expect SignatureCard widgets keep working even
// though the production UI currently only stores raw assets. // though the production UI currently only stores raw assets.

View File

@ -2,7 +2,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/repositories/document_repository.dart'; import 'package:pdf_signature/data/repositories/document_repository.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/ui/features/pdf/view_model/pdf_providers.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the last page is displayed (page {5}) /// Usage: the last page is displayed (page {5})
@ -11,11 +11,10 @@ Future<void> theLastPageIsDisplayedPage(WidgetTester tester, num param1) async {
final c = TestWorld.container ?? ProviderContainer(); final c = TestWorld.container ?? ProviderContainer();
final pdf = c.read(documentRepositoryProvider); final pdf = c.read(documentRepositoryProvider);
expect(pdf.pageCount, last); expect(pdf.pageCount, last);
final vm = c.read(pdfViewModelProvider); final currentPage = c.read(pdfViewModelProvider).currentPage;
final legacy = c.read(currentPageProvider);
expect( expect(
vm == last || legacy == last, currentPage == last,
true, true,
reason: 'Expected last page $last but got vm=$vm current=$legacy', reason: 'Expected last page $last but got current=$currentPage',
); );
} }

View File

@ -1,6 +1,6 @@
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/pdf_providers.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 '_world.dart'; import '_world.dart';
@ -9,12 +9,10 @@ Future<void> theUserClicksTheGoToApplyButton(WidgetTester tester) async {
final c = TestWorld.container ?? ProviderContainer(); final c = TestWorld.container ?? ProviderContainer();
final pending = TestWorld.pendingGoTo; final pending = TestWorld.pendingGoTo;
if (pending != null) { if (pending != null) {
try {
c.read(currentPageProvider.notifier).state = pending;
} catch (_) {}
try { try {
c.read(pdfViewModelProvider.notifier).jumpToPage(pending); c.read(pdfViewModelProvider.notifier).jumpToPage(pending);
} catch (_) {} } catch (_) {}
assert(c.read(pdfViewModelProvider).currentPage == pending);
await tester.pump(); await tester.pump();
} }
} }

View File

@ -1,6 +1,6 @@
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/pdf_providers.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 '_world.dart'; import '_world.dart';
@ -11,9 +11,6 @@ Future<void> theUserClicksTheThumbnailForPage(
) async { ) async {
final page = param1.toInt(); final page = param1.toInt();
final c = TestWorld.container ?? ProviderContainer(); final c = TestWorld.container ?? ProviderContainer();
try {
c.read(currentPageProvider.notifier).state = page;
} catch (_) {}
try { try {
c.read(pdfViewModelProvider.notifier).jumpToPage(page); c.read(pdfViewModelProvider.notifier).jumpToPage(page);
} catch (_) {} } catch (_) {}

View File

@ -10,7 +10,7 @@ Future<void> theUserDeletesOneSelectedSignaturePlacement(
) async { ) async {
final container = TestWorld.container ?? ProviderContainer(); final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container; TestWorld.container = container;
final currentPage = container.read(pdfViewModelProvider); final currentPage = container.read(pdfViewModelProvider).currentPage;
final placements = container final placements = container
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.placementsOn(currentPage); .placementsOn(currentPage);

View File

@ -12,7 +12,7 @@ Future<void> theUserDragsHandlesToResizeAndDragsToReposition(
final container = TestWorld.container ?? ProviderContainer(); final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container; TestWorld.container = container;
final pdfN = container.read(documentRepositoryProvider.notifier); final pdfN = container.read(documentRepositoryProvider.notifier);
final currentPage = container.read(pdfViewModelProvider); final currentPage = container.read(pdfViewModelProvider).currentPage;
final placements = pdfN.placementsOn(currentPage); final placements = pdfN.placementsOn(currentPage);
if (placements.isNotEmpty) { if (placements.isNotEmpty) {

View File

@ -48,7 +48,7 @@ theUserDragsThisSignatureCardOnThePageOfTheDocumentToPlaceASignaturePlacement(
final drop_card = temp_card; final drop_card = temp_card;
// Place it on the current page // Place it on the current page
final currentPage = container.read(pdfViewModelProvider); final currentPage = container.read(pdfViewModelProvider).currentPage;
container container
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.addPlacement( .addPlacement(

View File

@ -1,6 +1,6 @@
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/pdf_providers.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 '_world.dart'; import '_world.dart';
@ -15,7 +15,7 @@ Future<void> theUserEntersIntoTheGoToInputAndAppliesIt(
final clamped = final clamped =
value < 1 ? 1 : value; // upper bound validated in last-page check step value < 1 ? 1 : value; // upper bound validated in last-page check step
try { try {
c.read(currentPageProvider.notifier).state = clamped; c.read(pdfViewModelProvider.notifier).jumpToPage(clamped);
} catch (_) {} } catch (_) {}
try { try {
c.read(pdfViewModelProvider.notifier).jumpToPage(clamped); c.read(pdfViewModelProvider.notifier).jumpToPage(clamped);

View File

@ -1,6 +1,6 @@
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/pdf_providers.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 '_world.dart'; import '_world.dart';
@ -9,10 +9,7 @@ Future<void> theUserJumpsToPage(WidgetTester tester, num param1) async {
final page = param1.toInt(); final page = param1.toInt();
final c = TestWorld.container ?? ProviderContainer(); final c = TestWorld.container ?? ProviderContainer();
try { try {
c.read(currentPageProvider.notifier).state = page; c.read(pdfViewModelProvider).jumpToPage(page);
} catch (_) {}
try {
c.read(pdfViewModelProvider.notifier).jumpToPage(page);
} catch (_) {} } catch (_) {}
await tester.pump(); await tester.pump();
} }

View File

@ -4,7 +4,6 @@ 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/repositories/document_repository.dart'; import 'package:pdf_signature/data/repositories/document_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/pdf_providers.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 '_world.dart'; import '_world.dart';
@ -17,9 +16,6 @@ Future<void> theUserNavigatesToPageAndPlacesAnotherSignaturePlacement(
TestWorld.container = container; TestWorld.container = container;
final page = param1.toInt(); final page = param1.toInt();
// Update page providers directly (repository jumpTo is a no-op now) // Update page providers directly (repository jumpTo is a no-op now)
try {
container.read(currentPageProvider.notifier).state = page;
} catch (_) {}
try { try {
container.read(pdfViewModelProvider.notifier).jumpToPage(page); container.read(pdfViewModelProvider.notifier).jumpToPage(page);
} catch (_) {} } catch (_) {}

View File

@ -14,7 +14,7 @@ Future<void> theUserPlacesTwoSignaturePlacementsOnTheSamePage(
final container = TestWorld.container ?? ProviderContainer(); final container = TestWorld.container ?? ProviderContainer();
TestWorld.container = container; TestWorld.container = container;
// pdfViewModelProvider returns 1-based current page // pdfViewModelProvider returns 1-based current page
final page = container.read(pdfViewModelProvider); final page = container.read(pdfViewModelProvider).currentPage;
container container
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.addPlacement( .addPlacement(

View File

@ -1,6 +1,6 @@
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/pdf_providers.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 '_world.dart'; import '_world.dart';
@ -12,9 +12,6 @@ Future<void> theUserTypesIntoTheGoToInputAndPressesEnter(
final target = param1.toInt(); final target = param1.toInt();
final c = TestWorld.container ?? ProviderContainer(); final c = TestWorld.container ?? ProviderContainer();
TestWorld.container = c; TestWorld.container = c;
try {
c.read(currentPageProvider.notifier).state = target;
} catch (_) {}
try { try {
c.read(pdfViewModelProvider.notifier).jumpToPage(target); c.read(pdfViewModelProvider.notifier).jumpToPage(target);
} catch (_) {} } catch (_) {}

View File

@ -8,7 +8,7 @@ import '_world.dart';
Future<void> theUserUsesRotateControls(WidgetTester tester) async { Future<void> theUserUsesRotateControls(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer(); final container = TestWorld.container ?? ProviderContainer();
final pdfN = container.read(documentRepositoryProvider.notifier); final pdfN = container.read(documentRepositoryProvider.notifier);
final currentPage = container.read(pdfViewModelProvider); final currentPage = container.read(pdfViewModelProvider).currentPage;
final placements = pdfN.placementsOn(currentPage); final placements = pdfN.placementsOn(currentPage);
if (placements.isNotEmpty) { if (placements.isNotEmpty) {
pdfN.updatePlacementRotation( pdfN.updatePlacementRotation(

View File

@ -24,7 +24,7 @@ Future<void> threeSignaturePlacementsArePlacedOnTheCurrentPage(
]; ];
container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 5); container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 5);
final pdfN = container.read(documentRepositoryProvider.notifier); final pdfN = container.read(documentRepositoryProvider.notifier);
final page = container.read(pdfViewModelProvider); final page = container.read(pdfViewModelProvider).currentPage;
pdfN.addPlacement( pdfN.addPlacement(
page: page, page: page,
rect: Rect.fromLTWH(10, 10, 50, 50), rect: Rect.fromLTWH(10, 10, 50, 50),

View File

@ -1,4 +1,5 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:file_selector/file_selector.dart' as fs;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -8,7 +9,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:pdf_signature/data/services/export_service.dart'; import 'package:pdf_signature/data/services/export_service.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/ui_services.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/ui_services.dart';
import 'package:pdf_signature/data/repositories/preferences_repository.dart'; import 'package:pdf_signature/data/repositories/preferences_repository.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart'; import 'package:pdf_signature/data/repositories/document_repository.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
@ -58,7 +59,9 @@ void main() {
DocumentStateNotifier() DocumentStateNotifier()
..openPicked(pageCount: 5, bytes: Uint8List(0)), ..openPicked(pageCount: 5, bytes: Uint8List(0)),
), ),
useMockViewerProvider.overrideWith((ref) => true), pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true),
),
exportServiceProvider.overrideWith((_) => fake), exportServiceProvider.overrideWith((_) => fake),
savePathPickerProvider.overrideWith( savePathPickerProvider.overrideWith(
(_) => () async => 'C:/tmp/output.pdf', (_) => () async => 'C:/tmp/output.pdf',
@ -67,7 +70,11 @@ void main() {
child: MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
home: const PdfSignatureHomePage(), home: PdfSignatureHomePage(
onPickPdf: () async {},
onClosePdf: () {},
currentFile: fs.XFile(''),
),
), ),
), ),
); );

View File

@ -1,11 +1,12 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:file_selector/file_selector.dart' as fs;
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:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:image/image.dart' as img; import 'package:image/image.dart' as img;
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/pdf_providers.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/ui_services.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/ui_services.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_asset_repository.dart'; import 'package:pdf_signature/data/repositories/signature_asset_repository.dart';
@ -22,13 +23,19 @@ Future<void> pumpWithOpenPdf(WidgetTester tester) async {
documentRepositoryProvider.overrideWith( documentRepositoryProvider.overrideWith(
(ref) => DocumentStateNotifier()..openSample(), (ref) => DocumentStateNotifier()..openSample(),
), ),
useMockViewerProvider.overrideWithValue(true), pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true),
),
exportingProvider.overrideWith((ref) => false), exportingProvider.overrideWith((ref) => false),
], ],
child: MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
home: const PdfSignatureHomePage(), home: PdfSignatureHomePage(
onPickPdf: () async {},
onClosePdf: () {},
currentFile: fs.XFile(''),
),
), ),
), ),
); );
@ -388,13 +395,19 @@ Future<void> pumpWithOpenPdfAndSig(WidgetTester tester) async {
return cardRepo; return cardRepo;
}), }),
// In new model, interactive overlay not implemented; keep library empty // In new model, interactive overlay not implemented; keep library empty
useMockViewerProvider.overrideWithValue(true), pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true),
),
exportingProvider.overrideWith((ref) => false), exportingProvider.overrideWith((ref) => false),
], ],
child: MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
home: const PdfSignatureHomePage(), home: PdfSignatureHomePage(
onPickPdf: () async {},
onClosePdf: () {},
currentFile: fs.XFile(''),
),
), ),
), ),
); );

View File

@ -1,11 +1,12 @@
import 'package:file_selector/file_selector.dart' as fs;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
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/widgets/pdf_screen.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart'; import 'package:pdf_signature/data/repositories/document_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/pdf_providers.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
@ -23,7 +24,9 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
ProviderScope( ProviderScope(
overrides: [ overrides: [
useMockViewerProvider.overrideWithValue(true), pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true),
),
documentRepositoryProvider.overrideWith( documentRepositoryProvider.overrideWith(
(ref) => _TestPdfController(), (ref) => _TestPdfController(),
), ),
@ -32,7 +35,11 @@ void main() {
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('en'), locale: const Locale('en'),
home: const PdfSignatureHomePage(), home: PdfSignatureHomePage(
onPickPdf: () async {},
onClosePdf: () {},
currentFile: fs.XFile(''),
),
), ),
), ),
); );

View File

@ -1,10 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
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:pdfrx/pdfrx.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/data/repositories/document_repository.dart'; import 'package:pdf_signature/data/repositories/document_repository.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.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/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
@ -25,14 +26,16 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
ProviderScope( ProviderScope(
overrides: [ overrides: [
useMockViewerProvider.overrideWithValue(true), pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true),
),
documentRepositoryProvider.overrideWith((ref) => ctrl), documentRepositoryProvider.overrideWith((ref) => ctrl),
], ],
child: MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('en'), locale: const Locale('en'),
home: const Scaffold( home: Scaffold(
body: Center( body: Center(
child: SizedBox( child: SizedBox(
width: 800, width: 800,
@ -44,6 +47,7 @@ void main() {
onConfirmSignature: _noop, onConfirmSignature: _noop,
onClearActiveOverlay: _noop, onClearActiveOverlay: _noop,
onSelectPlaced: _noopInt, onSelectPlaced: _noopInt,
controller: PdfViewerController(),
), ),
), ),
), ),

View File

@ -1,10 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
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:pdfrx/pdfrx.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/data/repositories/document_repository.dart'; import 'package:pdf_signature/data/repositories/document_repository.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.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/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
@ -25,7 +26,9 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
ProviderScope( ProviderScope(
overrides: [ overrides: [
useMockViewerProvider.overrideWithValue(true), pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true),
),
// Continuous mode is always-on; no page view override needed // Continuous mode is always-on; no page view override needed
documentRepositoryProvider.overrideWith((ref) => ctrl), documentRepositoryProvider.overrideWith((ref) => ctrl),
], ],
@ -33,7 +36,7 @@ void main() {
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('en'), locale: const Locale('en'),
home: const Scaffold( home: Scaffold(
body: Center( body: Center(
child: SizedBox( child: SizedBox(
width: 800, width: 800,
@ -45,6 +48,7 @@ void main() {
onConfirmSignature: _noop, onConfirmSignature: _noop,
onClearActiveOverlay: _noop, onClearActiveOverlay: _noop,
onSelectPlaced: _noopInt, onSelectPlaced: _noopInt,
controller: PdfViewerController(),
), ),
), ),
), ),

View File

@ -6,7 +6,8 @@ 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/data/repositories/document_repository.dart'; import 'package:pdf_signature/data/repositories/document_repository.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
import 'package:pdfrx/pdfrx.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
import 'package:pdf_signature/domain/models/model.dart'; import 'package:pdf_signature/domain/models/model.dart';
@ -24,7 +25,9 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
ProviderScope( ProviderScope(
overrides: [ overrides: [
useMockViewerProvider.overrideWithValue(true), pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true),
),
documentRepositoryProvider.overrideWith( documentRepositoryProvider.overrideWith(
(ref) => _TestPdfController(), (ref) => _TestPdfController(),
), ),
@ -33,18 +36,19 @@ void main() {
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('en'), locale: const Locale('en'),
home: const Scaffold( home: Scaffold(
body: Center( body: Center(
child: SizedBox( child: SizedBox(
width: 800, width: 800,
height: 520, height: 520,
child: PdfPageArea( child: PdfPageArea(
pageSize: Size(676, 400), pageSize: const Size(676, 400),
onDragSignature: _noopOffset, onDragSignature: _noopOffset,
onResizeSignature: _noopOffset, onResizeSignature: _noopOffset,
onConfirmSignature: _noop, onConfirmSignature: _noop,
onClearActiveOverlay: _noop, onClearActiveOverlay: _noop,
onSelectPlaced: _noopInt, onSelectPlaced: _noopInt,
controller: PdfViewerController(),
), ),
), ),
), ),
@ -66,7 +70,9 @@ void main() {
// Use a persistent container across rebuilds // Use a persistent container across rebuilds
final container = ProviderContainer( final container = ProviderContainer(
overrides: [ overrides: [
useMockViewerProvider.overrideWithValue(true), pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true),
),
documentRepositoryProvider.overrideWith( documentRepositoryProvider.overrideWith(
(ref) => DocumentStateNotifier()..openSample(), (ref) => DocumentStateNotifier()..openSample(),
), ),
@ -83,13 +89,14 @@ void main() {
child: SizedBox( child: SizedBox(
width: width, width: width,
// Keep aspect ratio consistent with uiPageSize // Keep aspect ratio consistent with uiPageSize
child: const PdfPageArea( child: PdfPageArea(
pageSize: uiPageSize, pageSize: uiPageSize,
onDragSignature: _noopOffset, onDragSignature: _noopOffset,
onResizeSignature: _noopOffset, onResizeSignature: _noopOffset,
onConfirmSignature: _noop, onConfirmSignature: _noop,
onClearActiveOverlay: _noop, onClearActiveOverlay: _noop,
onSelectPlaced: _noopInt, onSelectPlaced: _noopInt,
controller: PdfViewerController(),
), ),
), ),
), ),

View File

@ -26,11 +26,15 @@ void main() {
tester, tester,
) async { ) async {
await tester.pumpWidget( await tester.pumpWidget(
const ProviderScope( ProviderScope(
child: MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
home: WelcomeScreen(), home: WelcomeScreen(
onPickPdf: () async {},
onOpenPdf:
({String? path, Uint8List? bytes, String? fileName}) async {},
),
), ),
), ),
); );
@ -39,8 +43,16 @@ void main() {
final bytes = Uint8List.fromList([1, 2, 3, 4]); final bytes = Uint8List.fromList([1, 2, 3, 4]);
final fake = _FakeDropReadable('sample.pdf', '/tmp/sample.pdf', bytes); final fake = _FakeDropReadable('sample.pdf', '/tmp/sample.pdf', bytes);
// Use the top-level helper with the WidgetRef.read function // Call handleDroppedFiles with the onOpenPdf callback from the widget
await handleDroppedFiles(stateful.ref.read, [fake]); await handleDroppedFiles(({
String? path,
Uint8List? bytes,
String? fileName,
}) async {
final container = ProviderScope.containerOf(stateful.context);
final repo = container.read(documentRepositoryProvider.notifier);
repo.openPicked(pageCount: 1, bytes: bytes);
}, [fake]);
await tester.pump(); await tester.pump();
final container = ProviderScope.containerOf(stateful.context); final container = ProviderScope.containerOf(stateful.context);