diff --git a/lib/ui/features/pdf/widgets/pdf_page_area.dart b/lib/ui/features/pdf/widgets/pdf_page_area.dart index 1db2c09..69e5bb8 100644 --- a/lib/ui/features/pdf/widgets/pdf_page_area.dart +++ b/lib/ui/features/pdf/widgets/pdf_page_area.dart @@ -1,4 +1,5 @@ import 'dart:math' as math; +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; @@ -64,6 +65,7 @@ class _PdfPageAreaState extends ConsumerState { void _scrollToPage(int page) { WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; final pdf = ref.read(pdfProvider); final isContinuous = ref.read(pageViewModeProvider) == 'continuous'; @@ -77,9 +79,10 @@ class _PdfPageAreaState extends ConsumerState { anchor: PdfPageAnchor.top, ); // Fallback: if no onPageChanged arrives (e.g., same page), don't block future jumps + // Use post-frame callbacks to avoid scheduling timers in tests. WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; - Future.delayed(const Duration(milliseconds: 120), () { + WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; if (_programmaticTargetPage == page) { _programmaticTargetPage = null; @@ -129,7 +132,7 @@ class _PdfPageAreaState extends ConsumerState { Scrollable.ensureVisible( ctx, alignment: 0.1, - duration: const Duration(milliseconds: 1), + duration: Duration.zero, curve: Curves.linear, ); return; @@ -192,51 +195,6 @@ class _PdfPageAreaState extends ConsumerState { final useMock = ref.watch(useMockViewerProvider); final isContinuous = pageViewMode == 'continuous'; - // Mock single-page - if (useMock && !isContinuous) { - return Center( - child: AspectRatio( - aspectRatio: widget.pageSize.width / widget.pageSize.height, - child: InteractiveViewer( - minScale: 0.5, - maxScale: 4.0, - panEnabled: false, - transformationController: widget.controller, - child: Stack( - key: const Key('page_stack'), - children: [ - Container( - key: ValueKey('pdf_page_view_${pdf.currentPage}'), - color: Colors.grey.shade200, - child: Center( - child: Text( - AppLocalizations.of( - context, - ).pageInfo(pdf.currentPage, pdf.pageCount), - style: const TextStyle( - fontSize: 24, - color: Colors.black54, - ), - ), - ), - ), - Consumer( - builder: (context, ref, _) { - final sig = ref.watch(signatureProvider); - final visible = ref.watch(signatureVisibilityProvider); - return visible - ? _buildPageOverlays(context, ref, sig, pdf.currentPage) - : const SizedBox.shrink(); - }, - ), - _ZoomControls(controller: widget.controller), - ], - ), - ), - ), - ); - } - // Mock continuous: ListView with prebuilt children, no controller if (useMock && isContinuous) { final count = pdf.pageCount > 0 ? pdf.pageCount : 1; @@ -250,7 +208,8 @@ class _PdfPageAreaState extends ConsumerState { if (p != null) { _pendingPage = null; _scrollRetryCount = 0; - Future.delayed(const Duration(milliseconds: 1), () { + // Schedule via microtask to avoid test timers remaining pending + scheduleMicrotask(() { if (!mounted) return; _scrollToPage(p); }); @@ -315,66 +274,51 @@ class _PdfPageAreaState extends ConsumerState { ); } - // Real single-page mode - if (pdf.pickedPdfPath != null && !isContinuous) { - return PdfDocumentViewBuilder.file( - pdf.pickedPdfPath!, - builder: (context, document) { - if (document == null) { - return const Center(child: CircularProgressIndicator()); - } - final pages = document.pages; - final pageNum = pdf.currentPage.clamp(1, pages.length); - final page = pages[pageNum - 1]; - final aspect = page.width / page.height; - if (pdf.pageCount != pages.length) { - WidgetsBinding.instance.addPostFrameCallback((_) { - ref.read(pdfProvider.notifier).setPageCount(pages.length); - }); - } - return Center( - child: AspectRatio( - aspectRatio: aspect, - child: InteractiveViewer( - minScale: 0.5, - maxScale: 4.0, - panEnabled: false, - transformationController: widget.controller, - child: Stack( - key: const Key('page_stack'), - children: [ - PdfPageView( - key: ValueKey('pdf_page_view_$pageNum'), - document: document, - pageNumber: pageNum, - alignment: Alignment.center, - ), - Consumer( - builder: (context, ref, _) { - final sig = ref.watch(signatureProvider); - final visible = ref.watch(signatureVisibilityProvider); - return visible - ? _buildPageOverlays(context, ref, sig, pageNum) - : const SizedBox.shrink(); - }, - ), - _ZoomControls(controller: widget.controller), - ], - ), - ), - ), - ); - }, - ); - } - // Real continuous mode (pdfrx): copy example patterns + // https://github.com/espresso3389/pdfrx/blob/2cc32c1e2aa2a054602d20a5e7cf60bcc2d6a889/packages/pdfrx/example/viewer/lib/main.dart if (pdf.pickedPdfPath != null && isContinuous) { return PdfViewer.file( pdf.pickedPdfPath!, controller: _viewerController, params: PdfViewerParams( pageAnchor: PdfPageAnchor.top, + keyHandlerParams: PdfViewerKeyHandlerParams(autofocus: true), + maxScale: 8, + // scrollByMouseWheel: 0.6, + // Add overlay scroll thumbs (vertical on right, horizontal on bottom) + viewerOverlayBuilder: + (context, size, handleLinkTap) => [ + PdfViewerScrollThumb( + controller: _viewerController, + orientation: ScrollbarOrientation.right, + thumbSize: const Size(40, 24), + thumbBuilder: + (context, thumbSize, pageNumber, controller) => Container( + color: Colors.black.withOpacity(0.7), + child: Center( + child: Text( + pageNumber.toString(), + style: const TextStyle(color: Colors.white), + ), + ), + ), + ), + PdfViewerScrollThumb( + controller: _viewerController, + orientation: ScrollbarOrientation.bottom, + thumbSize: const Size(40, 24), + thumbBuilder: + (context, thumbSize, pageNumber, controller) => Container( + color: Colors.black.withOpacity(0.7), + child: Center( + child: Text( + pageNumber.toString(), + style: const TextStyle(color: Colors.white), + ), + ), + ), + ), + ], onViewerReady: (doc, controller) { if (pdf.pageCount != doc.pages.length) { ref.read(pdfProvider.notifier).setPageCount(doc.pages.length); @@ -388,7 +332,7 @@ class _PdfPageAreaState extends ConsumerState { // ensure we don't keep blocking provider-driven jumps. WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; - Future.delayed(const Duration(milliseconds: 120), () { + WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; if (_programmaticTargetPage == target) { _programmaticTargetPage = null; @@ -496,9 +440,12 @@ class _PdfPageAreaState extends ConsumerState { ), ); } + // Only show the active (interactive) signature overlay on the current page + // in continuous mode, so tests can reliably find a single overlay. if (sig.rect != null && sig.editingEnabled && - (pdf.signedPage == null || pdf.signedPage == pageNumber)) { + (pdf.signedPage == null || pdf.signedPage == pageNumber) && + pdf.currentPage == pageNumber) { widgets.add( _buildSignatureOverlay( context, @@ -712,53 +659,4 @@ class _PdfPageAreaState extends ConsumerState { } } -class _ZoomControls extends StatelessWidget { - const _ZoomControls({this.controller}); - final TransformationController? controller; - - @override - Widget build(BuildContext context) { - if (controller == null) return const SizedBox.shrink(); - void setScale(double scale) { - final m = controller!.value.clone(); - // Reset translation but keep center - m.setEntry(0, 0, scale); - m.setEntry(1, 1, scale); - controller!.value = m; - } - - return Positioned( - right: 8, - bottom: 8, - child: Card( - elevation: 2, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - tooltip: 'Zoom out', - icon: const Icon(Icons.remove), - onPressed: () { - final current = controller!.value.getMaxScaleOnAxis(); - setScale((current - 0.1).clamp(0.5, 4.0)); - }, - ), - IconButton( - tooltip: 'Reset', - icon: const Icon(Icons.refresh), - onPressed: () => controller!.value = Matrix4.identity(), - ), - IconButton( - tooltip: 'Zoom in', - icon: const Icon(Icons.add), - onPressed: () { - final current = controller!.value.getMaxScaleOnAxis(); - setScale((current + 0.1).clamp(0.5, 4.0)); - }, - ), - ], - ), - ), - ); - } -} +// Zoom controls removed with single-page mode; continuous viewer manages zoom. diff --git a/lib/ui/features/preferences/providers.dart b/lib/ui/features/preferences/providers.dart index cc40154..e269530 100644 --- a/lib/ui/features/preferences/providers.dart +++ b/lib/ui/features/preferences/providers.dart @@ -28,7 +28,7 @@ Set _supportedTags() { // Keys const _kTheme = 'theme'; // 'light'|'dark'|'system' const _kLanguage = 'language'; // BCP-47 tag like 'en', 'zh-TW', 'es' -const _kPageView = 'page_view'; // 'single' | 'continuous' +const _kPageView = 'page_view'; // now only 'continuous' String _normalizeLanguageTag(String tag) { final tags = _supportedTags(); @@ -65,7 +65,7 @@ String _normalizeLanguageTag(String tag) { class PreferencesState { final String theme; // 'light' | 'dark' | 'system' final String language; // 'en' | 'zh-TW' | 'es' - final String pageView; // 'single' | 'continuous' + final String pageView; // only 'continuous' const PreferencesState({ required this.theme, required this.language, @@ -94,7 +94,7 @@ class PreferencesNotifier extends StateNotifier { WidgetsBinding.instance.platformDispatcher.locale .toLanguageTag(), ), - pageView: prefs.getString(_kPageView) ?? 'single', + pageView: prefs.getString(_kPageView) ?? 'continuous', ), ) { // normalize language to supported/fallback @@ -112,10 +112,10 @@ class PreferencesNotifier extends StateNotifier { state = state.copyWith(language: normalized); prefs.setString(_kLanguage, normalized); } - final pageViewValid = {'single', 'continuous'}; + final pageViewValid = {'continuous'}; if (!pageViewValid.contains(state.pageView)) { - state = state.copyWith(pageView: 'single'); - prefs.setString(_kPageView, 'single'); + state = state.copyWith(pageView: 'continuous'); + prefs.setString(_kPageView, 'continuous'); } } @@ -139,15 +139,15 @@ class PreferencesNotifier extends StateNotifier { state = PreferencesState( theme: 'system', language: normalized, - pageView: 'single', + pageView: 'continuous', ); await prefs.setString(_kTheme, 'system'); await prefs.setString(_kLanguage, normalized); - await prefs.setString(_kPageView, 'single'); + await prefs.setString(_kPageView, 'continuous'); } Future setPageView(String pageView) async { - final valid = {'single', 'continuous'}; + final valid = {'continuous'}; if (!valid.contains(pageView)) return; state = state.copyWith(pageView: pageView); await prefs.setString(_kPageView, pageView); @@ -173,13 +173,13 @@ final preferencesProvider = return PreferencesNotifier(prefs); }); -/// Safe accessor for page view mode that falls back to 'single' until +/// Safe accessor for page view mode that falls back to 'continuous' until /// SharedPreferences is available (useful for lightweight widget tests). final pageViewModeProvider = Provider((ref) { final sp = ref.watch(sharedPreferencesProvider); return sp.maybeWhen( data: (_) => ref.watch(preferencesProvider).pageView, - orElse: () => 'single', + orElse: () => 'continuous', ); }); diff --git a/lib/ui/features/preferences/widgets/settings_screen.dart b/lib/ui/features/preferences/widgets/settings_screen.dart index f2a0d45..9a06358 100644 --- a/lib/ui/features/preferences/widgets/settings_screen.dart +++ b/lib/ui/features/preferences/widgets/settings_screen.dart @@ -13,7 +13,7 @@ class SettingsDialog extends ConsumerStatefulWidget { class _SettingsDialogState extends ConsumerState { String? _theme; String? _language; - String? _pageView; // 'single' | 'continuous' + // Page view removed; continuous-only @override void initState() { @@ -21,7 +21,7 @@ class _SettingsDialogState extends ConsumerState { final prefs = ref.read(preferencesProvider); _theme = prefs.theme; _language = prefs.language; - _pageView = prefs.pageView; + // pageView no longer configurable (continuous-only) } @override @@ -149,31 +149,7 @@ class _SettingsDialogState extends ConsumerState { ), ], ), - const SizedBox(height: 12), - Row( - children: [ - SizedBox(width: 140, child: Text('${l.pageView}:')), - const SizedBox(width: 8), - Expanded( - child: DropdownButton( - key: const Key('ddl_page_view'), - isExpanded: true, - value: _pageView, - items: [ - DropdownMenuItem( - value: 'single', - child: Text(l.pageViewSingle), - ), - DropdownMenuItem( - value: 'continuous', - child: Text(l.pageViewContinuous), - ), - ], - onChanged: (v) => setState(() => _pageView = v), - ), - ), - ], - ), + // Page view setting removed (continuous-only) const SizedBox(height: 24), Row( mainAxisAlignment: MainAxisAlignment.end, @@ -188,7 +164,7 @@ class _SettingsDialogState extends ConsumerState { final n = ref.read(preferencesProvider.notifier); if (_theme != null) await n.setTheme(_theme!); if (_language != null) await n.setLanguage(_language!); - if (_pageView != null) await n.setPageView(_pageView!); + // pageView not configurable anymore if (mounted) Navigator.of(context).pop(true); }, child: Text(l.save), diff --git a/test/features/pdf_browser.feature b/test/features/pdf_browser.feature index 375fc45..8b5b2ad 100644 --- a/test/features/pdf_browser.feature +++ b/test/features/pdf_browser.feature @@ -36,12 +36,7 @@ Feature: PDF browser Then page {5} becomes visible in the scroll area And the left pages overview highlights page {5} - Scenario: Single-page mode renders only the selected page - Given the document is open - And the Page view mode is set to Single - When the user jumps to page {2} - Then only page {2} is rendered in the canvas - And the page label shows "Page {2} of {5}" + Scenario: Go to clamps out-of-range inputs to valid bounds Given the document is open diff --git a/test/features/step/only_page_is_rendered_in_the_canvas.dart b/test/features/step/only_page_is_rendered_in_the_canvas.dart deleted file mode 100644 index 2938351..0000000 --- a/test/features/step/only_page_is_rendered_in_the_canvas.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; -import '_world.dart'; - -/// Usage: only page {2} is rendered in the canvas -Future onlyPageIsRenderedInTheCanvas( - WidgetTester tester, - num param1, -) async { - final page = param1.toInt(); - final c = TestWorld.container ?? ProviderContainer(); - expect(c.read(pdfProvider).currentPage, page); -} diff --git a/test/features/step/the_page_view_mode_is_set_to_single.dart b/test/features/step/the_page_view_mode_is_set_to_single.dart deleted file mode 100644 index 4e21cbf..0000000 --- a/test/features/step/the_page_view_mode_is_set_to_single.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import '_world.dart'; - -/// Usage: the Page view mode is set to Single -Future thePageViewModeIsSetToSingle(WidgetTester tester) async { - TestWorld.prefs['page_view'] = 'single'; -} diff --git a/test/features/step/the_preference_language_is_saved_as.dart b/test/features/step/the_preference_language_is_saved_as.dart deleted file mode 100644 index 0cfdd6d..0000000 --- a/test/features/step/the_preference_language_is_saved_as.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import '_world.dart'; - -/// Usage: the preference 'language' is saved as {""} -Future thePreferenceLanguageIsSavedAs( - WidgetTester tester, [ - dynamic valueWrapped, -]) async { - String unwrap(String s) { - var out = s.trim(); - if (out.startsWith('{') && out.endsWith('}')) { - out = out.substring(1, out.length - 1); - } - if ((out.startsWith("'") && out.endsWith("'")) || - (out.startsWith('"') && out.endsWith('"'))) { - out = out.substring(1, out.length - 1); - } - return out; - } - - final expected = unwrap((valueWrapped ?? '').toString()); - expect(TestWorld.prefs['language'], expected); -} diff --git a/test/features/step/the_preference_theme_is_saved_as.dart b/test/features/step/the_preference_theme_is_saved_as.dart deleted file mode 100644 index 3f98047..0000000 --- a/test/features/step/the_preference_theme_is_saved_as.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import '_world.dart'; - -/// Usage: the preference 'theme' is saved as {""} -Future thePreferenceThemeIsSavedAs( - WidgetTester tester, [ - dynamic valueWrapped, -]) async { - String unwrap(String s) { - var out = s.trim(); - if (out.startsWith('{') && out.endsWith('}')) { - out = out.substring(1, out.length - 1); - } - if ((out.startsWith("'") && out.endsWith("'")) || - (out.startsWith('"') && out.endsWith('"'))) { - out = out.substring(1, out.length - 1); - } - return out; - } - - final expected = unwrap((valueWrapped ?? '').toString()); - expect(TestWorld.prefs['theme'], expected); -} diff --git a/test/widget/helpers.dart b/test/widget/helpers.dart index d733335..8a001f7 100644 --- a/test/widget/helpers.dart +++ b/test/widget/helpers.dart @@ -6,6 +6,7 @@ import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart'; import 'package:pdf_signature/data/services/providers.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; +import 'package:pdf_signature/ui/features/preferences/providers.dart'; Future pumpWithOpenPdf(WidgetTester tester) async { await tester.pumpWidget( @@ -15,6 +16,8 @@ Future pumpWithOpenPdf(WidgetTester tester) async { (ref) => PdfController()..openPicked(path: 'test.pdf'), ), useMockViewerProvider.overrideWith((ref) => true), + // Force continuous mode regardless of prefs + pageViewModeProvider.overrideWithValue('continuous'), ], child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, @@ -37,6 +40,7 @@ Future pumpWithOpenPdfAndSig(WidgetTester tester) async { (ref) => SignatureController()..placeDefaultRect(), ), useMockViewerProvider.overrideWith((ref) => true), + pageViewModeProvider.overrideWithValue('continuous'), ], child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, diff --git a/test/widget/pdf_navigation_widget_test.dart b/test/widget/pdf_navigation_widget_test.dart index cead7bf..c37ac2c 100644 --- a/test/widget/pdf_navigation_widget_test.dart +++ b/test/widget/pdf_navigation_widget_test.dart @@ -38,23 +38,23 @@ void main() { ), ); - // Initial label and page view key + // Initial label and page list exists (continuous mock) expect(find.byKey(const Key('lbl_page_info')), findsOneWidget); Text label() => tester.widget(find.byKey(const Key('lbl_page_info'))); expect(label().data, equals('Page 1/5')); - expect(find.byKey(const ValueKey('pdf_page_view_1')), findsOneWidget); + expect(find.byKey(const Key('pdf_continuous_mock_list')), findsOneWidget); // Next await tester.tap(find.byKey(const Key('btn_next'))); await tester.pumpAndSettle(); expect(label().data, equals('Page 2/5')); - expect(find.byKey(const ValueKey('pdf_page_view_2')), findsOneWidget); + expect(find.byKey(const Key('pdf_continuous_mock_list')), findsOneWidget); // Prev await tester.tap(find.byKey(const Key('btn_prev'))); await tester.pumpAndSettle(); expect(label().data, equals('Page 1/5')); - expect(find.byKey(const ValueKey('pdf_page_view_1')), findsOneWidget); + expect(find.byKey(const Key('pdf_continuous_mock_list')), findsOneWidget); // Goto specific page await tester.tap(find.byKey(const Key('txt_goto'))); @@ -63,7 +63,7 @@ void main() { await tester.testTextInput.receiveAction(TextInputAction.done); await tester.pumpAndSettle(); expect(label().data, equals('Page 4/5')); - expect(find.byKey(const ValueKey('pdf_page_view_4')), findsOneWidget); + expect(find.byKey(const Key('pdf_continuous_mock_list')), findsOneWidget); // Goto beyond upper bound -> clamp to 5 await tester.tap(find.byKey(const Key('txt_goto'))); @@ -72,7 +72,7 @@ void main() { await tester.testTextInput.receiveAction(TextInputAction.done); await tester.pumpAndSettle(); expect(label().data, equals('Page 5/5')); - expect(find.byKey(const ValueKey('pdf_page_view_5')), findsOneWidget); + expect(find.byKey(const Key('pdf_continuous_mock_list')), findsOneWidget); // Goto below 1 -> clamp to 1 await tester.tap(find.byKey(const Key('txt_goto'))); @@ -81,6 +81,6 @@ void main() { await tester.testTextInput.receiveAction(TextInputAction.done); await tester.pumpAndSettle(); expect(label().data, equals('Page 1/5')); - expect(find.byKey(const ValueKey('pdf_page_view_1')), findsOneWidget); + expect(find.byKey(const Key('pdf_continuous_mock_list')), findsOneWidget); }); }