diff --git a/docs/meta-arch.md b/docs/meta-arch.md index bc49733..e445d16 100644 --- a/docs/meta-arch.md +++ b/docs/meta-arch.md @@ -94,3 +94,5 @@ Some rule of thumb: * whole app use its image object as image representation. * aware that minimize, encode/decode usage, because its has poor performance on web * `ColorFilterGenerator` can not replace `adjustColor` due to custom background removal algorithm need at last stage. It is GPU based, and offscreen-render then readback is not ideal. +* [responsive_framework] + * RWD support diff --git a/lib/app.dart b/lib/app.dart index a1a98f6..fef80cf 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -5,6 +5,7 @@ import 'package:pdf_signature/l10n/app_localizations.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'; +import 'package:responsive_framework/responsive_framework.dart'; class MyApp extends StatelessWidget { const MyApp({super.key}); @@ -56,7 +57,7 @@ class MyApp extends StatelessWidget { routerConfig: ref.watch(routerProvider), builder: (context, child) { final router = ref.watch(routerProvider); - return Scaffold( + final content = Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context).appTitle), actions: [ @@ -78,6 +79,17 @@ class MyApp extends StatelessWidget { ), body: child, ); + + // Apply Responsive Framework globally for layout and scrolling. + return ResponsiveBreakpoints.builder( + child: ClampingScrollWrapper.builder(context, content), + breakpoints: const [ + Breakpoint(start: 0, end: 450, name: MOBILE), + Breakpoint(start: 451, end: 800, name: TABLET), + Breakpoint(start: 801, end: 1920, name: DESKTOP), + Breakpoint(start: 1921, end: double.infinity, name: '4K'), + ], + ); }, ); }, diff --git a/lib/ui/features/pdf/widgets/pages_sidebar.dart b/lib/ui/features/pdf/widgets/pages_sidebar.dart index f2717da..64b94e0 100644 --- a/lib/ui/features/pdf/widgets/pages_sidebar.dart +++ b/lib/ui/features/pdf/widgets/pages_sidebar.dart @@ -93,14 +93,18 @@ class ThumbnailsView extends ConsumerWidget { 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, + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 180), + child: AspectRatio( + // A4 portrait aspect: width:height ≈ 1:1.4142 + aspectRatio: 1 / 1.4142, + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: PdfPageView( + document: document, + pageNumber: pageNumber, + alignment: Alignment.center, + ), ), ), ), diff --git a/lib/ui/features/pdf/widgets/pdf_screen.dart b/lib/ui/features/pdf/widgets/pdf_screen.dart index c5093da..59da41d 100644 --- a/lib/ui/features/pdf/widgets/pdf_screen.dart +++ b/lib/ui/features/pdf/widgets/pdf_screen.dart @@ -18,6 +18,7 @@ import 'package:pdf_signature/utils/download.dart'; import '../view_model/pdf_view_model.dart'; import 'package:image/image.dart' as img; import 'package:pdf_signature/data/repositories/document_repository.dart'; +import 'package:responsive_framework/responsive_framework.dart'; class PdfSignatureHomePage extends ConsumerStatefulWidget { final Future Function() onPickPdf; @@ -59,6 +60,7 @@ class _PdfSignatureHomePageState extends ConsumerState { final double _signaturesMin = 140; final double _signaturesMax = 250; late PdfViewModel _viewModel; + bool? _lastCanShowPagesSidebar; // Exposed for tests to trigger the invalid-file SnackBar without UI. @visibleForTesting @@ -310,7 +312,9 @@ class _PdfSignatureHomePageState extends ConsumerState { max: _pagesMax, builder: (context, area) => Offstage( - offstage: !_showPagesSidebar, + offstage: + !(ResponsiveBreakpoints.of(context).largerThan(MOBILE) && + _showPagesSidebar), child: Consumer( builder: (context, ref, child) { final pdfViewModel = ref.watch(pdfViewModelProvider); @@ -364,6 +368,24 @@ class _PdfSignatureHomePageState extends ConsumerState { _applySidebarVisibility(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Detect breakpoint changes from Responsive Framework and update areas once. + bool canShowPagesSidebar = true; + try { + canShowPagesSidebar = ResponsiveBreakpoints.of( + context, + ).largerThan(MOBILE); + } catch (_) { + canShowPagesSidebar = true; + } + if (_lastCanShowPagesSidebar != canShowPagesSidebar) { + _lastCanShowPagesSidebar = canShowPagesSidebar; + _applySidebarVisibility(); + } + } + @override void dispose() { _viewModel.controller.removeListener(_onControllerChanged); @@ -372,29 +394,65 @@ class _PdfSignatureHomePageState extends ConsumerState { } void _applySidebarVisibility() { + // Respect responsive layout: disable Pages sidebar on MOBILE. + bool canShowPagesSidebar = true; + try { + canShowPagesSidebar = ResponsiveBreakpoints.of( + context, + ).largerThan(MOBILE); + } catch (_) { + // If ResponsiveBreakpoints isn't available yet (e.g., during early init), + // fall back to allowing sidebars to avoid crashes; builders also guard. + canShowPagesSidebar = true; + } + // Left pages sidebar final left = _splitController.areas[0]; - if (_showPagesSidebar) { - left.max = _pagesMax; - left.min = _pagesMin; - left.size = _lastPagesWidth.clamp(_pagesMin, _pagesMax); + final wantPagesVisible = _showPagesSidebar && canShowPagesSidebar; + final isPagesHidden = + (left.max == 1 && left.min == 0 && (left.size ?? 1) == 1); + if (wantPagesVisible) { + // Only expand if currently hidden; otherwise keep user's size. + if (isPagesHidden) { + left.max = _pagesMax; + left.min = _pagesMin; + left.size = _lastPagesWidth.clamp(_pagesMin, _pagesMax); + } else { + left.max = _pagesMax; + left.min = _pagesMin; + // Preserve current size (user may have adjusted it). + _lastPagesWidth = left.size ?? _lastPagesWidth; + } } else { - _lastPagesWidth = left.size ?? _lastPagesWidth; - left.min = 0; - left.max = 1; - left.size = 1; // effectively hidden + // Only collapse if currently visible; remember current size for restore. + if (!isPagesHidden) { + _lastPagesWidth = left.size ?? _lastPagesWidth; + left.min = 0; + left.max = 1; + left.size = 1; // effectively hidden + } } // Right signatures sidebar final right = _splitController.areas[2]; + final isSignaturesHidden = + (right.max == 1 && right.min == 0 && (right.size ?? 1) == 1); if (_showSignaturesSidebar) { - right.max = _signaturesMax; - right.min = _signaturesMin; - right.size = _lastSignaturesWidth.clamp(_signaturesMin, _signaturesMax); + if (isSignaturesHidden) { + right.max = _signaturesMax; + right.min = _signaturesMin; + right.size = _lastSignaturesWidth.clamp(_signaturesMin, _signaturesMax); + } else { + right.max = _signaturesMax; + right.min = _signaturesMin; + _lastSignaturesWidth = right.size ?? _lastSignaturesWidth; + } } else { - _lastSignaturesWidth = right.size ?? _lastSignaturesWidth; - right.min = 0; - right.max = 1; - right.size = 1; + if (!isSignaturesHidden) { + _lastSignaturesWidth = right.size ?? _lastSignaturesWidth; + right.min = 0; + right.max = 1; + right.size = 1; + } } } diff --git a/lib/ui/features/pdf/widgets/pdf_toolbar.dart b/lib/ui/features/pdf/widgets/pdf_toolbar.dart index d79e4fe..5cf971a 100644 --- a/lib/ui/features/pdf/widgets/pdf_toolbar.dart +++ b/lib/ui/features/pdf/widgets/pdf_toolbar.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; +import 'package:responsive_framework/responsive_framework.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; @@ -67,6 +68,16 @@ class _PdfToolbarState extends ConsumerState { builder: (context, constraints) { final bool compact = constraints.maxWidth < 260; final double gotoWidth = 50; + final bool isLargerThanMobile = ResponsiveBreakpoints.of( + context, + ).largerThan(MOBILE); + final String fileDisplay = () { + final path = widget.filePath; + if (path == null || path.isEmpty) return 'No file selected'; + if (isLargerThanMobile) return path; + // Extract file name for mobile (supports both / and \ separators) + return path.split('/').last.split('\\').last; + }(); // Center content of the toolbar final center = Wrap( @@ -82,14 +93,15 @@ class _PdfToolbarState extends ConsumerState { children: [ const Icon(Icons.insert_drive_file, size: 18), const SizedBox(width: 6), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 220), - child: Text( - // if filePath not null - widget.filePath != null - ? widget.filePath! - : 'No file selected', - overflow: TextOverflow.ellipsis, + Flexible( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 220), + child: Text( + fileDisplay, + maxLines: 1, + softWrap: false, + overflow: TextOverflow.ellipsis, + ), ), ), ], @@ -130,62 +142,68 @@ class _PdfToolbarState extends ConsumerState { ), ], ), - Wrap( - spacing: 6, - runSpacing: 4, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text(l.goTo), - SizedBox( - width: gotoWidth, - child: TextField( - key: const Key('txt_goto'), - controller: _goToController, - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], - enabled: !widget.disabled, - decoration: InputDecoration( - isDense: true, - hintText: '1..${pdf.pageCount}', + if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) + Wrap( + spacing: 6, + runSpacing: 4, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text(l.goTo), + SizedBox( + width: gotoWidth, + child: TextField( + key: const Key('txt_goto'), + controller: _goToController, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + enabled: !widget.disabled, + decoration: InputDecoration( + isDense: true, + hintText: '1..${pdf.pageCount}', + ), + onSubmitted: (_) => _submitGoTo(), ), - onSubmitted: (_) => _submitGoTo(), ), - ), - if (!compact) + if (!compact) + IconButton( + key: const Key('btn_goto_apply'), + tooltip: l.goTo, + icon: const Icon(Icons.arrow_forward), + onPressed: widget.disabled ? null : _submitGoTo, + ), + ], + ), + + if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) ...[ + const SizedBox(width: 8), + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ IconButton( - key: const Key('btn_goto_apply'), - tooltip: l.goTo, - icon: const Icon(Icons.arrow_forward), - onPressed: widget.disabled ? null : _submitGoTo, + key: const Key('btn_zoom_out'), + tooltip: 'Zoom out', + onPressed: widget.disabled ? null : widget.onZoomOut, + icon: const Icon(Icons.zoom_out), ), - ], - ), - const SizedBox(width: 8), - Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - key: const Key('btn_zoom_out'), - tooltip: 'Zoom out', - onPressed: widget.disabled ? null : widget.onZoomOut, - icon: const Icon(Icons.zoom_out), - ), - Text( - //if not null - widget.zoomLevel != null ? '${widget.zoomLevel}%' : '', - style: const TextStyle(fontSize: 12), - ), - IconButton( - key: const Key('btn_zoom_in'), - tooltip: 'Zoom in', - onPressed: widget.disabled ? null : widget.onZoomIn, - icon: const Icon(Icons.zoom_in), - ), - ], - ), - SizedBox(width: 6), + Text( + //if not null + widget.zoomLevel != null + ? '${widget.zoomLevel}%' + : '', + style: const TextStyle(fontSize: 12), + ), + IconButton( + key: const Key('btn_zoom_in'), + tooltip: 'Zoom in', + onPressed: widget.disabled ? null : widget.onZoomIn, + icon: const Icon(Icons.zoom_in), + ), + ], + ), + SizedBox(width: 6), + ], ], ), ], @@ -194,19 +212,21 @@ class _PdfToolbarState extends ConsumerState { return Row( children: [ - IconButton( - key: const Key('btn_toggle_pages_sidebar'), - tooltip: 'Toggle pages overview', - onPressed: widget.disabled ? null : widget.onTogglePagesSidebar, - icon: Icon( - Icons.view_sidebar, - color: - widget.showPagesSidebar - ? Theme.of(context).colorScheme.primary - : null, + if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) ...[ + IconButton( + key: const Key('btn_toggle_pages_sidebar'), + tooltip: 'Toggle pages overview', + onPressed: widget.disabled ? null : widget.onTogglePagesSidebar, + icon: Icon( + Icons.view_sidebar, + color: + widget.showPagesSidebar + ? Theme.of(context).colorScheme.primary + : null, + ), ), - ), - const SizedBox(width: 8), + const SizedBox(width: 8), + ], Expanded(child: center), const SizedBox(width: 8), IconButton( diff --git a/pubspec.yaml b/pubspec.yaml index fdb7f0a..b13e7d1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,6 +59,7 @@ dependencies: colorfilter_generator: ^0.0.8 flutter_box_transform: ^0.4.7 file_picker: ^10.3.3 + responsive_framework: ^1.5.1 # disable_web_context_menu: ^1.1.0 # ml_linalg: ^13.12.6