feat: integrate responsive framework for improved layout handling

This commit is contained in:
insleker 2025-09-22 13:34:49 +08:00
parent 0a512919a5
commit 353aa883d7
6 changed files with 194 additions and 97 deletions

View File

@ -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

View File

@ -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'),
],
);
},
);
},

View File

@ -93,8 +93,11 @@ class ThumbnailsView extends ConsumerWidget {
padding: const EdgeInsets.all(6),
child: Column(
children: [
SizedBox(
height: 180,
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(
@ -104,6 +107,7 @@ class ThumbnailsView extends ConsumerWidget {
),
),
),
),
const SizedBox(height: 4),
Text('$pageNumber', style: theme.textTheme.bodySmall),
],

View File

@ -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,31 +390,67 @@ 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) {
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 {
// 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) {
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 {
if (!isSignaturesHidden) {
_lastSignaturesWidth = right.size ?? _lastSignaturesWidth;
right.min = 0;
right.max = 1;
right.size = 1;
}
}
}
@override
Widget build(BuildContext context) {

View File

@ -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,16 +93,17 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
children: [
const Icon(Icons.insert_drive_file, size: 18),
const SizedBox(width: 6),
ConstrainedBox(
Flexible(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 220),
child: Text(
// if filePath not null
widget.filePath != null
? widget.filePath!
: 'No file selected',
fileDisplay,
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
),
@ -130,6 +142,7 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
),
],
),
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE))
Wrap(
spacing: 6,
runSpacing: 4,
@ -162,6 +175,8 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
),
],
),
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) ...[
const SizedBox(width: 8),
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
@ -174,7 +189,9 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
),
Text(
//if not null
widget.zoomLevel != null ? '${widget.zoomLevel}%' : '',
widget.zoomLevel != null
? '${widget.zoomLevel}%'
: '',
style: const TextStyle(fontSize: 12),
),
IconButton(
@ -187,6 +204,7 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
),
SizedBox(width: 6),
],
],
),
],
],
@ -194,6 +212,7 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
return Row(
children: [
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) ...[
IconButton(
key: const Key('btn_toggle_pages_sidebar'),
tooltip: 'Toggle pages overview',
@ -207,6 +226,7 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
),
),
const SizedBox(width: 8),
],
Expanded(child: center),
const SizedBox(width: 8),
IconButton(

View File

@ -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