feat: integrate responsive framework for improved layout handling
This commit is contained in:
parent
0a512919a5
commit
353aa883d7
|
|
@ -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
|
||||
|
|
|
|||
14
lib/app.dart
14
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'),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -17,6 +17,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<void> Function() onPickPdf;
|
||||
|
|
@ -58,6 +59,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
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
|
||||
|
|
@ -306,7 +308,9 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
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);
|
||||
|
|
@ -360,6 +364,24 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
_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);
|
||||
|
|
@ -368,29 +390,65 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<PdfToolbar> {
|
|||
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<PdfToolbar> {
|
|||
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<PdfToolbar> {
|
|||
),
|
||||
],
|
||||
),
|
||||
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<PdfToolbar> {
|
|||
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ dependencies:
|
|||
riverpod_annotation: ^2.6.1
|
||||
colorfilter_generator: ^0.0.8
|
||||
flutter_box_transform: ^0.4.7
|
||||
responsive_framework: ^1.5.1
|
||||
# disable_web_context_menu: ^1.1.0
|
||||
# ml_linalg: ^13.12.6
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue