Merge branch 'feat/ui' into feat/mobile
This commit is contained in:
commit
b2bf489af0
|
|
@ -94,3 +94,5 @@ Some rule of thumb:
|
||||||
* whole app use its image object as image representation.
|
* whole app use its image object as image representation.
|
||||||
* aware that minimize, encode/decode usage, because its has poor performance on web
|
* 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.
|
* `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/routing/router.dart';
|
||||||
import 'package:pdf_signature/ui/features/preferences/widgets/settings_screen.dart';
|
import 'package:pdf_signature/ui/features/preferences/widgets/settings_screen.dart';
|
||||||
import 'data/repositories/preferences_repository.dart';
|
import 'data/repositories/preferences_repository.dart';
|
||||||
|
import 'package:responsive_framework/responsive_framework.dart';
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
const MyApp({super.key});
|
const MyApp({super.key});
|
||||||
|
|
@ -56,7 +57,7 @@ class MyApp extends StatelessWidget {
|
||||||
routerConfig: ref.watch(routerProvider),
|
routerConfig: ref.watch(routerProvider),
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
final router = ref.watch(routerProvider);
|
final router = ref.watch(routerProvider);
|
||||||
return Scaffold(
|
final content = Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(AppLocalizations.of(context).appTitle),
|
title: Text(AppLocalizations.of(context).appTitle),
|
||||||
actions: [
|
actions: [
|
||||||
|
|
@ -78,6 +79,17 @@ class MyApp extends StatelessWidget {
|
||||||
),
|
),
|
||||||
body: child,
|
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,8 +93,11 @@ class ThumbnailsView extends ConsumerWidget {
|
||||||
padding: const EdgeInsets.all(6),
|
padding: const EdgeInsets.all(6),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
ConstrainedBox(
|
||||||
height: 180,
|
constraints: const BoxConstraints(maxHeight: 180),
|
||||||
|
child: AspectRatio(
|
||||||
|
// A4 portrait aspect: width:height ≈ 1:1.4142
|
||||||
|
aspectRatio: 1 / 1.4142,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
child: PdfPageView(
|
child: PdfPageView(
|
||||||
|
|
@ -104,6 +107,7 @@ class ThumbnailsView extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text('$pageNumber', style: theme.textTheme.bodySmall),
|
Text('$pageNumber', style: theme.textTheme.bodySmall),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import 'package:pdf_signature/utils/download.dart';
|
||||||
import '../view_model/pdf_view_model.dart';
|
import '../view_model/pdf_view_model.dart';
|
||||||
import 'package:image/image.dart' as img;
|
import 'package:image/image.dart' as img;
|
||||||
import 'package:pdf_signature/data/repositories/document_repository.dart';
|
import 'package:pdf_signature/data/repositories/document_repository.dart';
|
||||||
|
import 'package:responsive_framework/responsive_framework.dart';
|
||||||
|
|
||||||
class PdfSignatureHomePage extends ConsumerStatefulWidget {
|
class PdfSignatureHomePage extends ConsumerStatefulWidget {
|
||||||
final Future<void> Function() onPickPdf;
|
final Future<void> Function() onPickPdf;
|
||||||
|
|
@ -59,6 +60,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
final double _signaturesMin = 140;
|
final double _signaturesMin = 140;
|
||||||
final double _signaturesMax = 250;
|
final double _signaturesMax = 250;
|
||||||
late PdfViewModel _viewModel;
|
late PdfViewModel _viewModel;
|
||||||
|
bool? _lastCanShowPagesSidebar;
|
||||||
|
|
||||||
// Exposed for tests to trigger the invalid-file SnackBar without UI.
|
// Exposed for tests to trigger the invalid-file SnackBar without UI.
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
|
|
@ -310,7 +312,9 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
max: _pagesMax,
|
max: _pagesMax,
|
||||||
builder:
|
builder:
|
||||||
(context, area) => Offstage(
|
(context, area) => Offstage(
|
||||||
offstage: !_showPagesSidebar,
|
offstage:
|
||||||
|
!(ResponsiveBreakpoints.of(context).largerThan(MOBILE) &&
|
||||||
|
_showPagesSidebar),
|
||||||
child: Consumer(
|
child: Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final pdfViewModel = ref.watch(pdfViewModelProvider);
|
final pdfViewModel = ref.watch(pdfViewModelProvider);
|
||||||
|
|
@ -364,6 +368,24 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
_applySidebarVisibility();
|
_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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_viewModel.controller.removeListener(_onControllerChanged);
|
_viewModel.controller.removeListener(_onControllerChanged);
|
||||||
|
|
@ -372,31 +394,67 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _applySidebarVisibility() {
|
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
|
// Left pages sidebar
|
||||||
final left = _splitController.areas[0];
|
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.max = _pagesMax;
|
||||||
left.min = _pagesMin;
|
left.min = _pagesMin;
|
||||||
left.size = _lastPagesWidth.clamp(_pagesMin, _pagesMax);
|
left.size = _lastPagesWidth.clamp(_pagesMin, _pagesMax);
|
||||||
} else {
|
} 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;
|
_lastPagesWidth = left.size ?? _lastPagesWidth;
|
||||||
left.min = 0;
|
left.min = 0;
|
||||||
left.max = 1;
|
left.max = 1;
|
||||||
left.size = 1; // effectively hidden
|
left.size = 1; // effectively hidden
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Right signatures sidebar
|
// Right signatures sidebar
|
||||||
final right = _splitController.areas[2];
|
final right = _splitController.areas[2];
|
||||||
|
final isSignaturesHidden =
|
||||||
|
(right.max == 1 && right.min == 0 && (right.size ?? 1) == 1);
|
||||||
if (_showSignaturesSidebar) {
|
if (_showSignaturesSidebar) {
|
||||||
|
if (isSignaturesHidden) {
|
||||||
right.max = _signaturesMax;
|
right.max = _signaturesMax;
|
||||||
right.min = _signaturesMin;
|
right.min = _signaturesMin;
|
||||||
right.size = _lastSignaturesWidth.clamp(_signaturesMin, _signaturesMax);
|
right.size = _lastSignaturesWidth.clamp(_signaturesMin, _signaturesMax);
|
||||||
} else {
|
} else {
|
||||||
|
right.max = _signaturesMax;
|
||||||
|
right.min = _signaturesMin;
|
||||||
|
_lastSignaturesWidth = right.size ?? _lastSignaturesWidth;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!isSignaturesHidden) {
|
||||||
_lastSignaturesWidth = right.size ?? _lastSignaturesWidth;
|
_lastSignaturesWidth = right.size ?? _lastSignaturesWidth;
|
||||||
right.min = 0;
|
right.min = 0;
|
||||||
right.max = 1;
|
right.max = 1;
|
||||||
right.size = 1;
|
right.size = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:pdf_signature/l10n/app_localizations.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';
|
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) {
|
builder: (context, constraints) {
|
||||||
final bool compact = constraints.maxWidth < 260;
|
final bool compact = constraints.maxWidth < 260;
|
||||||
final double gotoWidth = 50;
|
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
|
// Center content of the toolbar
|
||||||
final center = Wrap(
|
final center = Wrap(
|
||||||
|
|
@ -82,16 +93,17 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.insert_drive_file, size: 18),
|
const Icon(Icons.insert_drive_file, size: 18),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
ConstrainedBox(
|
Flexible(
|
||||||
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 220),
|
constraints: const BoxConstraints(maxWidth: 220),
|
||||||
child: Text(
|
child: Text(
|
||||||
// if filePath not null
|
fileDisplay,
|
||||||
widget.filePath != null
|
maxLines: 1,
|
||||||
? widget.filePath!
|
softWrap: false,
|
||||||
: 'No file selected',
|
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -130,6 +142,7 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE))
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 6,
|
spacing: 6,
|
||||||
runSpacing: 4,
|
runSpacing: 4,
|
||||||
|
|
@ -162,6 +175,8 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) ...[
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Wrap(
|
Wrap(
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
|
@ -174,7 +189,9 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
//if not null
|
//if not null
|
||||||
widget.zoomLevel != null ? '${widget.zoomLevel}%' : '',
|
widget.zoomLevel != null
|
||||||
|
? '${widget.zoomLevel}%'
|
||||||
|
: '',
|
||||||
style: const TextStyle(fontSize: 12),
|
style: const TextStyle(fontSize: 12),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|
@ -187,6 +204,7 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
),
|
),
|
||||||
SizedBox(width: 6),
|
SizedBox(width: 6),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
@ -194,6 +212,7 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
|
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) ...[
|
||||||
IconButton(
|
IconButton(
|
||||||
key: const Key('btn_toggle_pages_sidebar'),
|
key: const Key('btn_toggle_pages_sidebar'),
|
||||||
tooltip: 'Toggle pages overview',
|
tooltip: 'Toggle pages overview',
|
||||||
|
|
@ -207,6 +226,7 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
Expanded(child: center),
|
Expanded(child: center),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ dependencies:
|
||||||
colorfilter_generator: ^0.0.8
|
colorfilter_generator: ^0.0.8
|
||||||
flutter_box_transform: ^0.4.7
|
flutter_box_transform: ^0.4.7
|
||||||
file_picker: ^10.3.3
|
file_picker: ^10.3.3
|
||||||
|
responsive_framework: ^1.5.1
|
||||||
# disable_web_context_menu: ^1.1.0
|
# disable_web_context_menu: ^1.1.0
|
||||||
# ml_linalg: ^13.12.6
|
# ml_linalg: ^13.12.6
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue