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)
* only FOSS libs can use
* 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:image/image.dart' as img;
import 'dart:io';
import 'package:file_selector/file_selector.dart' as fs;
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/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_providers.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/pdf_screen.dart';
@ -50,17 +50,23 @@ void main() {
documentRepositoryProvider.overrideWith(
(ref) => DocumentStateNotifier()..openPicked(pageCount: 3),
),
useMockViewerProvider.overrideWith((ref) => false),
pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false),
),
exportServiceProvider.overrideWith((_) => fake),
savePathPickerProvider.overrideWith(
(_) => () async => 'C:/tmp/output.pdf',
),
],
child: const MaterialApp(
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
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);
return cardRepo;
}),
useMockViewerProvider.overrideWithValue(false),
pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false),
),
],
child: const MaterialApp(
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
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.
final ctx = tester.element(find.byType(PdfSignatureHomePage));
final container = ProviderScope.containerOf(ctx);
final r = container.read(activeRectProvider)!;
final r = container.read(pdfViewModelProvider).activeRect!;
final lib = container.read(signatureAssetRepositoryProvider);
final asset = lib.isNotEmpty ? lib.first : null;
final currentPage = container.read(pdfViewModelProvider);
final currentPage = container.read(pdfViewModelProvider).currentPage;
container
.read(documentRepositoryProvider.notifier)
.addPlacement(page: currentPage, rect: r, asset: asset);
// 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();
container.read(signatureVisibilityProvider.notifier).state = true;
// container.read(signatureVisibilityProvider.notifier).state = true;
await tester.pumpAndSettle();
final placed = find.byKey(const Key('placed_signature_0'));
@ -192,13 +205,19 @@ void main() {
DocumentStateNotifier()
..openPicked(pageCount: 3, bytes: pdfBytes),
),
useMockViewerProvider.overrideWithValue(false),
pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false),
),
],
child: const MaterialApp(
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'),
home: PdfSignatureHomePage(),
home: PdfSignatureHomePage(
onPickPdf: () async {},
onClosePdf: () {},
currentFile: fs.XFile('test.pdf'),
),
),
),
);
@ -232,13 +251,19 @@ void main() {
DocumentStateNotifier()
..openPicked(pageCount: 3, bytes: pdfBytes),
),
useMockViewerProvider.overrideWithValue(false),
pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false),
),
],
child: const MaterialApp(
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'),
home: PdfSignatureHomePage(),
home: PdfSignatureHomePage(
onPickPdf: () async {},
onClosePdf: () {},
currentFile: fs.XFile('test.pdf'),
),
),
),
);
@ -275,13 +300,19 @@ void main() {
DocumentStateNotifier()
..openPicked(pageCount: 3, bytes: pdfBytes),
),
useMockViewerProvider.overrideWithValue(false),
pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false),
),
],
child: const MaterialApp(
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'),
home: PdfSignatureHomePage(),
home: PdfSignatureHomePage(
onPickPdf: () async {},
onClosePdf: () {},
currentFile: fs.XFile('test.pdf'),
),
),
),
);
@ -321,13 +352,19 @@ void main() {
DocumentStateNotifier()
..openPicked(pageCount: 3, bytes: pdfBytes),
),
useMockViewerProvider.overrideWithValue(false),
pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false),
),
],
child: const MaterialApp(
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
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 'dart:io';
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/view_model/pdf_providers.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:shared_preferences/shared_preferences.dart';
@ -37,13 +37,19 @@ void main() {
DocumentStateNotifier()
..openPicked(pageCount: 3, bytes: pdfBytes),
),
useMockViewerProvider.overrideWithValue(false),
pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false),
),
],
child: const MaterialApp(
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'),
home: PdfSignatureHomePage(),
home: PdfSignatureHomePage(
onPickPdf: () async {},
onClosePdf: () {},
currentFile: fs.XFile('test.pdf'),
),
),
),
);
@ -85,13 +91,19 @@ void main() {
DocumentStateNotifier()
..openPicked(pageCount: 3, bytes: pdfBytes),
),
useMockViewerProvider.overrideWithValue(false),
pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false),
),
],
child: const MaterialApp(
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'),
home: PdfSignatureHomePage(),
home: PdfSignatureHomePage(
onPickPdf: () async {},
onClosePdf: () {},
currentFile: fs.XFile('test.pdf'),
),
),
),
);
@ -133,13 +145,19 @@ void main() {
DocumentStateNotifier()
..openPicked(pageCount: 3, bytes: pdfBytes),
),
useMockViewerProvider.overrideWithValue(false),
pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false),
),
],
child: const MaterialApp(
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'),
home: PdfSignatureHomePage(),
home: PdfSignatureHomePage(
onPickPdf: () async {},
onClosePdf: () {},
currentFile: fs.XFile('test.pdf'),
),
),
),
);
@ -182,13 +200,19 @@ void main() {
DocumentStateNotifier()
..openPicked(pageCount: 3, bytes: pdfBytes),
),
useMockViewerProvider.overrideWithValue(false),
pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false),
),
],
child: const MaterialApp(
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
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_localized_locales/flutter_localized_locales.dart';
import 'package:pdf_signature/l10n/app_localizations.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.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/routing/router.dart';
import 'package:pdf_signature/ui/features/preferences/widgets/settings_screen.dart';
import 'data/repositories/preferences_repository.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
@ -42,7 +40,7 @@ class MyApp extends StatelessWidget {
data: (_) {
final themeMode = ref.watch(themeModeProvider);
final appLocale = ref.watch(localeProvider);
return MaterialApp(
return MaterialApp.router(
onGenerateTitle: (ctx) => AppLocalizations.of(ctx).appTitle,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
@ -63,27 +61,27 @@ class MyApp extends StatelessWidget {
...AppLocalizations.localizationsDelegates,
LocaleNamesLocalizationsDelegate(),
],
home: Builder(
builder:
(ctx) => Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(ctx).appTitle),
actions: [
OutlinedButton.icon(
key: const Key('btn_appbar_settings'),
icon: const Icon(Icons.settings),
label: Text(AppLocalizations.of(ctx).settings),
onPressed:
() => showDialog<bool>(
context: ctx,
builder: (_) => const SettingsDialog(),
),
),
],
routerConfig: ref.watch(routerProvider),
builder: (context, child) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).appTitle),
actions: [
OutlinedButton.icon(
key: const Key('btn_appbar_settings'),
icon: const Icon(Icons.settings),
label: Text(AppLocalizations.of(context).settings),
onPressed:
() => showDialog<bool>(
context: context,
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: {});
}
void openPicked({
required int pageCount,
Uint8List? bytes,
}) {
void openPicked({required int pageCount, Uint8List? bytes}) {
state = state.copyWith(
loaded: true,
pageCount: pageCount,
@ -27,6 +24,10 @@ class DocumentStateNotifier extends StateNotifier<Document> {
);
}
void close() {
state = Document.initial();
}
void setPageCount(int count) {
if (!state.loaded) return;
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:pdfrx/pdfrx.dart';
class PdfViewModel extends StateNotifier<int> {
class PdfViewModel extends ChangeNotifier {
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) {
state = page.clamp(1, document.pageCount);
currentPage = page;
}
Future<void> openPdf({required String path, Uint8List? bytes}) async {
@ -30,37 +56,125 @@ class PdfViewModel extends StateNotifier<int> {
ref
.read(documentRepositoryProvider.notifier)
.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();
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);
});

View File

@ -1,11 +1,112 @@
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 {
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
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 'pdf_page_overlays.dart';
import '../view_model/pdf_providers.dart';
import 'package:pdf_signature/data/repositories/signature_asset_repository.dart';
// using only adjusted overlay, no direct model imports needed
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.
@visibleForTesting
@ -57,7 +56,6 @@ class _PdfMockContinuousListState extends ConsumerState<PdfMockContinuousList> {
final pendingPage = widget.pendingPage;
final scrollToPage = widget.scrollToPage;
final clearPending = widget.clearPending;
final visible = ref.watch(signatureVisibilityProvider);
final assets = ref.watch(signatureAssetRepositoryProvider);
if (pendingPage != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
@ -111,7 +109,7 @@ class _PdfMockContinuousListState extends ConsumerState<PdfMockContinuousList> {
// Add placement to the document
ref
.read(documentRepositoryProvider.notifier)
.read(pdfViewModelProvider.notifier)
.addPlacement(
page: pageNum,
rect: rect,
@ -151,88 +149,75 @@ class _PdfMockContinuousListState extends ConsumerState<PdfMockContinuousList> {
);
},
),
visible
? Stack(
children: [
PdfPageOverlays(
pageSize: pageSize,
pageNumber: pageNum,
onDragSignature: widget.onDragSignature,
onResizeSignature: widget.onResizeSignature,
onConfirmSignature: widget.onConfirmSignature,
onClearActiveOverlay: widget.onClearActiveOverlay,
onSelectPlaced: widget.onSelectPlaced,
),
// For tests expecting an active overlay, draw a mock
// overlay on page 1 when library has at least one asset
if (pageNum == 1 && assets.isNotEmpty)
LayoutBuilder(
builder: (context, constraints) {
final left =
_activeRect.left * constraints.maxWidth;
final top =
_activeRect.top * constraints.maxHeight;
final width =
_activeRect.width * constraints.maxWidth;
final height =
_activeRect.height *
constraints.maxHeight;
// Publish rect for tests/other UI to observe
WidgetsBinding.instance.addPostFrameCallback((
_,
) {
if (!mounted) return;
ref
.read(activeRectProvider.notifier)
.state = _activeRect;
});
return Stack(
children: [
Positioned(
left: left,
top: top,
width: width,
height: height,
child: GestureDetector(
key: const Key('signature_overlay'),
// Removed onPanUpdate to allow scrolling
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
color: Colors.red,
width: 2,
),
),
child: const SizedBox.expand(),
Stack(
children: [
PdfPageOverlays(
pageSize: pageSize,
pageNumber: pageNum,
onDragSignature: widget.onDragSignature,
onResizeSignature: widget.onResizeSignature,
onConfirmSignature: widget.onConfirmSignature,
onClearActiveOverlay: widget.onClearActiveOverlay,
onSelectPlaced: widget.onSelectPlaced,
),
// For tests expecting an active overlay, draw a mock
// overlay on page 1 when library has at least one asset
if (pageNum == 1 && assets.isNotEmpty)
LayoutBuilder(
builder: (context, constraints) {
final left =
_activeRect.left * constraints.maxWidth;
final top =
_activeRect.top * constraints.maxHeight;
final width =
_activeRect.width * constraints.maxWidth;
final height =
_activeRect.height * constraints.maxHeight;
// Publish rect for tests/other UI to observe
return Stack(
children: [
Positioned(
left: left,
top: top,
width: width,
height: height,
child: GestureDetector(
key: const Key('signature_overlay'),
// Removed onPanUpdate to allow scrolling
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
color: Colors.red,
width: 2,
),
),
child: const SizedBox.expand(),
),
// resize handle bottom-right
Positioned(
left: left + width - 14,
top: top + height - 14,
width: 14,
height: 14,
child: GestureDetector(
key: const Key('signature_handle'),
// Removed onPanUpdate to allow scrolling
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.red,
),
),
),
),
),
// resize handle bottom-right
Positioned(
left: left + width - 14,
top: top + height - 14,
width: 14,
height: 14,
child: GestureDetector(
key: const Key('signature_handle'),
// Removed onPanUpdate to allow scrolling
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.red),
),
),
],
);
},
),
],
)
: const SizedBox.shrink(),
),
),
],
);
},
),
],
),
],
),
),

View File

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

View File

@ -1,10 +1,9 @@
import 'package:flutter/material.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 'package:pdf_signature/data/repositories/document_repository.dart';
import 'signature_overlay.dart';
import '../view_model/pdf_providers.dart';
/// Builds all overlays for a given page: placed signatures and the active one.
class PdfPageOverlays extends ConsumerWidget {
@ -29,9 +28,11 @@ class PdfPageOverlays extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final pdf = ref.watch(documentRepositoryProvider);
final pdfViewModel = ref.watch(pdfViewModelProvider);
final pdf = pdfViewModel.document;
final placed =
pdf.placementsByPage[pageNumber] ?? const <SignaturePlacement>[];
final activeRect = pdfViewModel.activeRect;
final widgets = <Widget>[];
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)
final activeRect = ref.watch(activeRectProvider);
final useMock = ref.watch(useMockViewerProvider);
// TODO:Add active overlay if present and not using mock (mock has its own)
final useMock = pdfViewModel.useMockViewer;
if (!useMock && activeRect != null) {
widgets.add(
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: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 'draw_canvas.dart';
import 'pdf_toolbar.dart';
@ -19,7 +17,16 @@ import 'ui_services.dart';
import '../view_model/pdf_view_model.dart';
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
ConsumerState<PdfSignatureHomePage> createState() =>
@ -31,7 +38,6 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
bool _showPagesSidebar = true;
bool _showSignaturesSidebar = true;
int _zoomLevel = 100; // percentage for display only
fs.XFile _file = fs.XFile('');
// Split view controller to manage resizable sidebars without remounting the center area.
late final MultiSplitViewController _splitController;
@ -43,6 +49,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
final double _pagesMax = 250;
final double _signaturesMin = 140;
final double _signaturesMax = 250;
late PdfViewModel _viewModel;
// Exposed for tests to trigger the invalid-file SnackBar without UI.
@visibleForTesting
@ -55,38 +62,17 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
}
Future<void> _pickPdf() async {
final typeGroup = const fs.XTypeGroup(label: 'PDF', extensions: ['pdf']);
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);
}
await widget.onPickPdf();
}
void _closePdf() {
widget.onClosePdf();
}
void _jumpToPage(int page) {
final controller = ref.read(pdfViewerControllerProvider);
final current = ref.read(currentPageProvider);
final pdf = ref.read(documentRepositoryProvider);
final controller = _viewModel.controller;
final current = _viewModel.currentPage;
final pdf = _viewModel.document;
int target;
if (page == -1) {
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
if (current != target) {
ref.read(currentPageProvider.notifier).state = target;
// Also notify view model (if used elsewhere) via its public API
try {
ref.read(pdfViewModelProvider.notifier).jumpToPage(target);
_viewModel.jumpToPage(target);
} catch (_) {
// ignore if provider not available
}
@ -153,7 +138,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
Future<void> _saveSignedPdf() async {
ref.read(exportingProvider.notifier).state = true;
try {
final pdf = ref.read(documentRepositoryProvider);
final pdf = _viewModel.document;
final messenger = ScaffoldMessenger.of(context);
if (!pdf.loaded) {
messenger.showSnackBar(
@ -219,6 +204,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
void initState() {
super.initState();
// Build areas once with builders; keep these instances stable.
_viewModel = ref.read(pdfViewModelProvider.notifier);
_areas = [
Area(
size: _lastPagesWidth,
@ -227,7 +213,26 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
builder:
(context, area) => Offstage(
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(
@ -235,6 +240,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
builder:
(context, area) => RepaintBoundary(
child: PdfPageArea(
controller: _viewModel.controller,
key: const ValueKey('pdf_page_area'),
pageSize: _pageSize,
onDragSignature: _onDragSignature,
@ -300,15 +306,9 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
@override
Widget build(BuildContext context) {
// Provide controller override so descendants can access it.
return ProviderScope(
overrides: [pdfViewerControllerProvider.overrideWithValue(_controller)],
child: _buildScaffold(context),
);
return _buildScaffold(context);
}
late final PdfViewerController _controller = PdfViewerController();
Widget _buildScaffold(BuildContext context) {
final isExporting = ref.watch(exportingProvider);
final l = AppLocalizations.of(context);
@ -323,6 +323,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
PdfToolbar(
disabled: isExporting,
onPickPdf: _pickPdf,
onClosePdf: _closePdf,
onJumpToPage: _jumpToPage,
onZoomOut: () {
setState(() {
@ -335,7 +336,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
});
},
zoomLevel: _zoomLevel,
fileName: _file.name,
filePath: widget.currentFile.path,
showPagesSidebar: _showPagesSidebar,
showSignaturesSidebar: _showSignaturesSidebar,
onTogglePagesSidebar:

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/l10n/app_localizations.dart';
import 'signature_drawer.dart';
import '../../signature/widgets/signature_drawer.dart';
import 'ui_services.dart';
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_card_repository.dart';
import 'image_editor_dialog.dart';
import '../../signature/widgets/signature_card.dart';
import '../view_model/pdf_providers.dart';
import '../../pdf/widgets/image_editor_dialog.dart';
import 'signature_card.dart';
/// Data for drag-and-drop is in signature_drag_data.dart
@ -66,9 +65,7 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
);
},
onTap: () {
ref
.read(activeRectProvider.notifier)
.state = const Rect.fromLTWH(0.2, 0.2, 0.3, 0.15);
// state = const Rect.fromLTWH(0.2, 0.2, 0.3, 0.15);
},
),
),

View File

@ -1,8 +1,6 @@
import 'dart:typed_data';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart';
import 'package:pdf_signature/data/repositories/signature_card_repository.dart';
import 'package:pdfrx/pdfrx.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
class WelcomeViewModel {
final Ref ref;
@ -10,19 +8,9 @@ class WelcomeViewModel {
WelcomeViewModel(this.ref);
Future<void> openPdf({required 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
}
}
ref
.read(documentRepositoryProvider.notifier)
.openPicked(pageCount: pageCount, bytes: bytes);
ref.read(signatureCardRepositoryProvider.notifier).clearAll();
await ref
.read(pdfViewModelProvider.notifier)
.openPdf(path: path, bytes: bytes);
}
}

View File

@ -1,12 +1,10 @@
import 'dart:typed_data';
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/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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
// 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.
Future<void> handleDroppedFiles(
Reader read,
Future<void> Function({String? path, Uint8List? bytes, String? fileName})
onOpenPdf,
Iterable<DropReadable> files,
) async {
if (files.isEmpty) return;
@ -47,11 +46,23 @@ Future<void> handleDroppedFiles(
bytes = null;
}
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 {
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
ConsumerState<WelcomeScreen> createState() => _WelcomeScreenState();
@ -61,19 +72,7 @@ class _WelcomeScreenState extends ConsumerState<WelcomeScreen> {
bool _dragging = false;
Future<void> _pickPdf() 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 ref
.read(welcomeViewModelProvider)
.openPdf(path: file.path, bytes: bytes);
}
await widget.onPickPdf();
}
@override
@ -113,7 +112,7 @@ class _WelcomeScreenState extends ConsumerState<WelcomeScreen> {
final adapters = desktopFiles.map<DropReadable>(
(f) => _DropReadableFromDesktop(f),
);
await handleDroppedFiles(ref.read, adapters);
await handleDroppedFiles(widget.onOpenPdf, adapters);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),

View File

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

View File

@ -2,11 +2,11 @@ import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter_test/flutter_test.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:pdf_signature/app.dart';
import 'package:pdf_signature/data/repositories/preferences_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/data/services/export_service.dart';
import 'package:pdf_signature/domain/models/model.dart';
@ -48,7 +48,9 @@ Future<ProviderContainer> pumpApp(
documentRepositoryProvider.overrideWith(
(ref) => DocumentStateNotifier()..openSample(),
),
useMockViewerProvider.overrideWith((ref) => true),
pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true),
),
exportServiceProvider.overrideWith((ref) => fakeExport),
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_asset_repository.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';
@ -21,7 +21,7 @@ Future<void> aMultipageDocumentIsOpen(WidgetTester tester) async {
container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 5);
// Reset page state providers
try {
container.read(currentPageProvider.notifier).state = 1;
container.read(pdfViewModelProvider.notifier).jumpToPage(1);
} catch (_) {}
try {
container.read(pdfViewModelProvider.notifier).jumpToPage(1);

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import 'package:flutter_test/flutter_test.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 '_world.dart';
@ -8,11 +8,10 @@ import '_world.dart';
Future<void> pageIsDisplayed(WidgetTester tester, num param1) async {
final expected = param1.toInt();
final c = TestWorld.container ?? ProviderContainer();
final vm = c.read(pdfViewModelProvider);
final legacy = c.read(currentPageProvider);
final currentPage = c.read(pdfViewModelProvider).currentPage;
expect(
vm == expected || legacy == expected,
currentPage == expected,
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_card_repository.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';
class _BridgedSignatureCardStateNotifier extends SignatureCardStateNotifier {
@ -37,7 +38,9 @@ Future<void> theAppLaunches(WidgetTester tester) async {
documentRepositoryProvider.overrideWith(
(ref) => DocumentStateNotifier()..openSample(),
),
useMockViewerProvider.overrideWith((ref) => true),
pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true),
),
// Bridge: automatically mirror assets into signature cards so legacy
// feature steps that expect SignatureCard widgets keep working even
// 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: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_providers.dart';
import '_world.dart';
/// 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 pdf = c.read(documentRepositoryProvider);
expect(pdf.pageCount, last);
final vm = c.read(pdfViewModelProvider);
final legacy = c.read(currentPageProvider);
final currentPage = c.read(pdfViewModelProvider).currentPage;
expect(
vm == last || legacy == last,
currentPage == last,
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_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 '_world.dart';
@ -9,12 +9,10 @@ Future<void> theUserClicksTheGoToApplyButton(WidgetTester tester) async {
final c = TestWorld.container ?? ProviderContainer();
final pending = TestWorld.pendingGoTo;
if (pending != null) {
try {
c.read(currentPageProvider.notifier).state = pending;
} catch (_) {}
try {
c.read(pdfViewModelProvider.notifier).jumpToPage(pending);
} catch (_) {}
assert(c.read(pdfViewModelProvider).currentPage == pending);
await tester.pump();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import 'dart:typed_data';
import 'package:file_selector/file_selector.dart' as fs;
import 'package:flutter/material.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/ui/features/pdf/widgets/ui_services.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/ui/features/pdf/widgets/pdf_screen.dart';
@ -58,7 +59,9 @@ void main() {
DocumentStateNotifier()
..openPicked(pageCount: 5, bytes: Uint8List(0)),
),
useMockViewerProvider.overrideWith((ref) => true),
pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true),
),
exportServiceProvider.overrideWith((_) => fake),
savePathPickerProvider.overrideWith(
(_) => () async => 'C:/tmp/output.pdf',
@ -67,7 +70,11 @@ void main() {
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
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 'package:file_selector/file_selector.dart' as fs;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
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/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/data/repositories/document_repository.dart';
import 'package:pdf_signature/data/repositories/signature_asset_repository.dart';
@ -22,13 +23,19 @@ Future<void> pumpWithOpenPdf(WidgetTester tester) async {
documentRepositoryProvider.overrideWith(
(ref) => DocumentStateNotifier()..openSample(),
),
useMockViewerProvider.overrideWithValue(true),
pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true),
),
exportingProvider.overrideWith((ref) => false),
],
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
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;
}),
// In new model, interactive overlay not implemented; keep library empty
useMockViewerProvider.overrideWithValue(true),
pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true),
),
exportingProvider.overrideWith((ref) => false),
],
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
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_test/flutter_test.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/view_model/pdf_view_model.dart';
import 'package:pdf_signature/data/repositories/document_repository.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';
@ -23,7 +24,9 @@ void main() {
await tester.pumpWidget(
ProviderScope(
overrides: [
useMockViewerProvider.overrideWithValue(true),
pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true),
),
documentRepositoryProvider.overrideWith(
(ref) => _TestPdfController(),
),
@ -32,7 +35,11 @@ void main() {
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
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_test/flutter_test.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/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/l10n/app_localizations.dart';
@ -25,14 +26,16 @@ void main() {
await tester.pumpWidget(
ProviderScope(
overrides: [
useMockViewerProvider.overrideWithValue(true),
pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true),
),
documentRepositoryProvider.overrideWith((ref) => ctrl),
],
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('en'),
home: const Scaffold(
home: Scaffold(
body: Center(
child: SizedBox(
width: 800,
@ -44,6 +47,7 @@ void main() {
onConfirmSignature: _noop,
onClearActiveOverlay: _noop,
onSelectPlaced: _noopInt,
controller: PdfViewerController(),
),
),
),

View File

@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.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/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/l10n/app_localizations.dart';
@ -25,7 +26,9 @@ void main() {
await tester.pumpWidget(
ProviderScope(
overrides: [
useMockViewerProvider.overrideWithValue(true),
pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true),
),
// Continuous mode is always-on; no page view override needed
documentRepositoryProvider.overrideWith((ref) => ctrl),
],
@ -33,7 +36,7 @@ void main() {
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('en'),
home: const Scaffold(
home: Scaffold(
body: Center(
child: SizedBox(
width: 800,
@ -45,6 +48,7 @@ void main() {
onConfirmSignature: _noop,
onClearActiveOverlay: _noop,
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/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/domain/models/model.dart';
@ -24,7 +25,9 @@ void main() {
await tester.pumpWidget(
ProviderScope(
overrides: [
useMockViewerProvider.overrideWithValue(true),
pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true),
),
documentRepositoryProvider.overrideWith(
(ref) => _TestPdfController(),
),
@ -33,18 +36,19 @@ void main() {
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('en'),
home: const Scaffold(
home: Scaffold(
body: Center(
child: SizedBox(
width: 800,
height: 520,
child: PdfPageArea(
pageSize: Size(676, 400),
pageSize: const Size(676, 400),
onDragSignature: _noopOffset,
onResizeSignature: _noopOffset,
onConfirmSignature: _noop,
onClearActiveOverlay: _noop,
onSelectPlaced: _noopInt,
controller: PdfViewerController(),
),
),
),
@ -66,7 +70,9 @@ void main() {
// Use a persistent container across rebuilds
final container = ProviderContainer(
overrides: [
useMockViewerProvider.overrideWithValue(true),
pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true),
),
documentRepositoryProvider.overrideWith(
(ref) => DocumentStateNotifier()..openSample(),
),
@ -83,13 +89,14 @@ void main() {
child: SizedBox(
width: width,
// Keep aspect ratio consistent with uiPageSize
child: const PdfPageArea(
child: PdfPageArea(
pageSize: uiPageSize,
onDragSignature: _noopOffset,
onResizeSignature: _noopOffset,
onConfirmSignature: _noop,
onClearActiveOverlay: _noop,
onSelectPlaced: _noopInt,
controller: PdfViewerController(),
),
),
),

View File

@ -26,11 +26,15 @@ void main() {
tester,
) async {
await tester.pumpWidget(
const ProviderScope(
ProviderScope(
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
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 fake = _FakeDropReadable('sample.pdf', '/tmp/sample.pdf', bytes);
// Use the top-level helper with the WidgetRef.read function
await handleDroppedFiles(stateful.ref.read, [fake]);
// Call handleDroppedFiles with the onOpenPdf callback from the widget
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();
final container = ProviderScope.containerOf(stateful.context);