Compare commits
No commits in common. "b2bf489af09e367ee52ca0dbc34fa87a50c4c7e9" and "0a512919a54d5f416e0bc68a6bb2d067dee7a48d" have entirely different histories.
b2bf489af0
...
0a512919a5
|
|
@ -22,9 +22,8 @@ flutter analyze
|
||||||
# > run unit tests and widget tests
|
# > run unit tests and widget tests
|
||||||
flutter test
|
flutter test
|
||||||
# > run integration tests
|
# > run integration tests
|
||||||
# flutter test integration_test/ -d <device_id>
|
flutter test integration_test/ -d <device_id>
|
||||||
# Examples: --device=windows | --device=linux | --device=macos | --device=chrome
|
# dart run tool/run_integration_tests.dart --device=linux (necessary for linux)
|
||||||
dart run tool/run_integration_tests.dart --device=<device_id>
|
|
||||||
|
|
||||||
# dart run tool/gen_view_wireframe_md.dart
|
# dart run tool/gen_view_wireframe_md.dart
|
||||||
# flutter pub run dead_code_analyzer
|
# flutter pub run dead_code_analyzer
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ pluginManagement {
|
||||||
plugins {
|
plugins {
|
||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.7.0" apply false
|
id("com.android.application") version "8.7.0" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|
|
||||||
|
|
@ -94,5 +94,3 @@ 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
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:file_selector/file_selector.dart' as fs;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
@ -63,7 +63,7 @@ void main() {
|
||||||
home: PdfSignatureHomePage(
|
home: PdfSignatureHomePage(
|
||||||
onPickPdf: () async {},
|
onPickPdf: () async {},
|
||||||
onClosePdf: () {},
|
onClosePdf: () {},
|
||||||
currentFile: XFile('test.pdf'),
|
currentFile: fs.XFile('test.pdf'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -119,7 +119,7 @@ void main() {
|
||||||
home: PdfSignatureHomePage(
|
home: PdfSignatureHomePage(
|
||||||
onPickPdf: () async {},
|
onPickPdf: () async {},
|
||||||
onClosePdf: () {},
|
onClosePdf: () {},
|
||||||
currentFile: XFile('test.pdf'),
|
currentFile: fs.XFile('test.pdf'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -177,7 +177,7 @@ void main() {
|
||||||
home: PdfSignatureHomePage(
|
home: PdfSignatureHomePage(
|
||||||
onPickPdf: () async {},
|
onPickPdf: () async {},
|
||||||
onClosePdf: () {},
|
onClosePdf: () {},
|
||||||
currentFile: XFile('test.pdf'),
|
currentFile: fs.XFile('test.pdf'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -223,7 +223,7 @@ void main() {
|
||||||
home: PdfSignatureHomePage(
|
home: PdfSignatureHomePage(
|
||||||
onPickPdf: () async {},
|
onPickPdf: () async {},
|
||||||
onClosePdf: () {},
|
onClosePdf: () {},
|
||||||
currentFile: XFile('test.pdf'),
|
currentFile: fs.XFile('test.pdf'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -272,7 +272,7 @@ void main() {
|
||||||
home: PdfSignatureHomePage(
|
home: PdfSignatureHomePage(
|
||||||
onPickPdf: () async {},
|
onPickPdf: () async {},
|
||||||
onClosePdf: () {},
|
onClosePdf: () {},
|
||||||
currentFile: XFile('test.pdf'),
|
currentFile: fs.XFile('test.pdf'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -324,7 +324,7 @@ void main() {
|
||||||
home: PdfSignatureHomePage(
|
home: PdfSignatureHomePage(
|
||||||
onPickPdf: () async {},
|
onPickPdf: () async {},
|
||||||
onClosePdf: () {},
|
onClosePdf: () {},
|
||||||
currentFile: XFile('test.pdf'),
|
currentFile: fs.XFile('test.pdf'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -390,7 +390,7 @@ void main() {
|
||||||
home: PdfSignatureHomePage(
|
home: PdfSignatureHomePage(
|
||||||
onPickPdf: () async {},
|
onPickPdf: () async {},
|
||||||
onClosePdf: () {},
|
onClosePdf: () {},
|
||||||
currentFile: XFile('test.pdf'),
|
currentFile: fs.XFile('test.pdf'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:file_selector/file_selector.dart' as fs;
|
||||||
|
|
||||||
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
|
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
|
||||||
import 'package:pdf_signature/ui/features/pdf/widgets/pages_sidebar.dart';
|
import 'package:pdf_signature/ui/features/pdf/widgets/pages_sidebar.dart';
|
||||||
|
|
@ -47,7 +47,7 @@ void main() {
|
||||||
home: PdfSignatureHomePage(
|
home: PdfSignatureHomePage(
|
||||||
onPickPdf: () async {},
|
onPickPdf: () async {},
|
||||||
onClosePdf: () {},
|
onClosePdf: () {},
|
||||||
currentFile: XFile('test.pdf'),
|
currentFile: fs.XFile('test.pdf'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -101,7 +101,7 @@ void main() {
|
||||||
home: PdfSignatureHomePage(
|
home: PdfSignatureHomePage(
|
||||||
onPickPdf: () async {},
|
onPickPdf: () async {},
|
||||||
onClosePdf: () {},
|
onClosePdf: () {},
|
||||||
currentFile: XFile('test.pdf'),
|
currentFile: fs.XFile('test.pdf'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -155,7 +155,7 @@ void main() {
|
||||||
home: PdfSignatureHomePage(
|
home: PdfSignatureHomePage(
|
||||||
onPickPdf: () async {},
|
onPickPdf: () async {},
|
||||||
onClosePdf: () {},
|
onClosePdf: () {},
|
||||||
currentFile: XFile('test.pdf'),
|
currentFile: fs.XFile('test.pdf'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -242,7 +242,7 @@ void main() {
|
||||||
home: PdfSignatureHomePage(
|
home: PdfSignatureHomePage(
|
||||||
onPickPdf: () async {},
|
onPickPdf: () async {},
|
||||||
onClosePdf: () {},
|
onClosePdf: () {},
|
||||||
currentFile: XFile('test.pdf'),
|
currentFile: fs.XFile('test.pdf'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -308,7 +308,7 @@ void main() {
|
||||||
home: PdfSignatureHomePage(
|
home: PdfSignatureHomePage(
|
||||||
onPickPdf: () async {},
|
onPickPdf: () async {},
|
||||||
onClosePdf: () {},
|
onClosePdf: () {},
|
||||||
currentFile: XFile('test.pdf'),
|
currentFile: fs.XFile('test.pdf'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
14
lib/app.dart
14
lib/app.dart
|
|
@ -5,7 +5,6 @@ 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});
|
||||||
|
|
@ -57,7 +56,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);
|
||||||
final content = Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(AppLocalizations.of(context).appTitle),
|
title: Text(AppLocalizations.of(context).appTitle),
|
||||||
actions: [
|
actions: [
|
||||||
|
|
@ -79,17 +78,6 @@ 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'),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import 'dart:io' show Platform;
|
import 'package:file_selector/file_selector.dart' as fs;
|
||||||
import 'package:file_picker/file_picker.dart' as fp;
|
|
||||||
import 'package:path_provider/path_provider.dart' as pp;
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
@ -74,51 +72,13 @@ class PdfExportViewModel extends ChangeNotifier {
|
||||||
static Future<String?> _defaultSavePathPickerWithSuggestedName(
|
static Future<String?> _defaultSavePathPickerWithSuggestedName(
|
||||||
String suggestedName,
|
String suggestedName,
|
||||||
) async {
|
) async {
|
||||||
// Prefer native save dialog via file_picker on all non-web platforms.
|
final group = fs.XTypeGroup(label: 'PDF', extensions: ['pdf']);
|
||||||
// If the user cancels (null) simply bubble up null. If an exception occurs
|
final location = await fs.getSaveLocation(
|
||||||
// (unsupported platform or plugin issue), fall back to an app documents path.
|
acceptedTypeGroups: [group],
|
||||||
try {
|
suggestedName: suggestedName,
|
||||||
final result = await fp.FilePicker.platform.saveFile(
|
confirmButtonText: 'Save',
|
||||||
dialogTitle: 'Please select an output file:',
|
|
||||||
fileName: suggestedName,
|
|
||||||
type: fp.FileType.custom,
|
|
||||||
allowedExtensions: const ['pdf'],
|
|
||||||
// lockParentWindow is ignored on mobile; useful on desktop.
|
|
||||||
lockParentWindow: true,
|
|
||||||
);
|
|
||||||
return result; // null if canceled
|
|
||||||
} catch (_) {
|
|
||||||
// Fall through to app documents fallback below.
|
|
||||||
}
|
|
||||||
|
|
||||||
debugPrint(
|
|
||||||
'Fallback: select a folder and build path with suggested name (mobile platform)',
|
|
||||||
);
|
);
|
||||||
|
return location?.path; // null if user cancels
|
||||||
// On some mobile providers, saveFile may not present a picker or returns null.
|
|
||||||
// Offer a folder picker and compose the final path.
|
|
||||||
if (Platform.isAndroid || Platform.isIOS) {
|
|
||||||
final dir = await fp.FilePicker.platform.getDirectoryPath(
|
|
||||||
dialogTitle: 'Select folder to save',
|
|
||||||
lockParentWindow: true,
|
|
||||||
);
|
|
||||||
if (dir != null && dir.trim().isNotEmpty) {
|
|
||||||
final d = dir.trim();
|
|
||||||
final needsSep = !(d.endsWith('/') || d.endsWith('\\'));
|
|
||||||
return (needsSep ? (d + '/') : d) + suggestedName;
|
|
||||||
}
|
|
||||||
// User canceled directory selection; bubble up null.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
debugPrint('Fallback: build a default path (web platform)');
|
|
||||||
try {
|
|
||||||
final dir = await pp.getApplicationDocumentsDirectory();
|
|
||||||
return '${dir.path}/$suggestedName';
|
|
||||||
} catch (_) {
|
|
||||||
// Last resort: let the caller handle a null path
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:file_selector/file_selector.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:pdf_signature/data/repositories/document_repository.dart';
|
import 'package:pdf_signature/data/repositories/document_repository.dart';
|
||||||
import 'package:pdf_signature/data/repositories/signature_card_repository.dart';
|
import 'package:pdf_signature/data/repositories/signature_card_repository.dart';
|
||||||
import 'package:pdf_signature/domain/models/model.dart';
|
import 'package:pdf_signature/domain/models/model.dart';
|
||||||
import 'package:pdfrx/pdfrx.dart';
|
import 'package:pdfrx/pdfrx.dart';
|
||||||
import 'package:file_picker/file_picker.dart' as fp;
|
import 'package:file_selector/file_selector.dart' as fs;
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class PdfViewModel extends ChangeNotifier {
|
class PdfViewModel extends ChangeNotifier {
|
||||||
|
|
@ -247,7 +247,7 @@ final pdfViewModelProvider = ChangeNotifierProvider<PdfViewModel>((ref) {
|
||||||
class PdfSessionViewModel extends ChangeNotifier {
|
class PdfSessionViewModel extends ChangeNotifier {
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
final GoRouter router;
|
final GoRouter router;
|
||||||
XFile _currentFile = XFile('');
|
fs.XFile _currentFile = fs.XFile('');
|
||||||
// Keep a human display name in addition to XFile, because on Linux via
|
// Keep a human display name in addition to XFile, because on Linux via
|
||||||
// xdg-desktop-portal the path can look like /run/user/.../doc/<UUID>, and
|
// xdg-desktop-portal the path can look like /run/user/.../doc/<UUID>, and
|
||||||
// XFile.name derives from that basename, yielding a random UUID instead of
|
// XFile.name derives from that basename, yielding a random UUID instead of
|
||||||
|
|
@ -257,29 +257,21 @@ class PdfSessionViewModel extends ChangeNotifier {
|
||||||
|
|
||||||
PdfSessionViewModel({required this.ref, required this.router});
|
PdfSessionViewModel({required this.ref, required this.router});
|
||||||
|
|
||||||
XFile get currentFile => _currentFile;
|
fs.XFile get currentFile => _currentFile;
|
||||||
String get displayFileName => _displayFileName;
|
String get displayFileName => _displayFileName;
|
||||||
|
|
||||||
Future<void> pickAndOpenPdf() async {
|
Future<void> pickAndOpenPdf() async {
|
||||||
final result = await fp.FilePicker.platform.pickFiles(
|
final typeGroup = const fs.XTypeGroup(label: 'PDF', extensions: ['pdf']);
|
||||||
type: fp.FileType.custom,
|
final XFile? file = await fs.openFile(acceptedTypeGroups: [typeGroup]);
|
||||||
allowedExtensions: const ['pdf'],
|
if (file != null) {
|
||||||
withData: true,
|
Uint8List? bytes;
|
||||||
);
|
|
||||||
if (result == null || result.files.isEmpty) return;
|
|
||||||
final picked = result.files.single;
|
|
||||||
final String name = picked.name;
|
|
||||||
final String? path = picked.path;
|
|
||||||
final Uint8List? bytes = picked.bytes;
|
|
||||||
Uint8List? effectiveBytes = bytes;
|
|
||||||
if (effectiveBytes == null && path != null && path.isNotEmpty) {
|
|
||||||
try {
|
try {
|
||||||
effectiveBytes = await XFile(path).readAsBytes();
|
bytes = await file.readAsBytes();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
effectiveBytes = null;
|
bytes = null;
|
||||||
}
|
}
|
||||||
|
await openPdf(path: file.path, bytes: bytes, fileName: file.name);
|
||||||
}
|
}
|
||||||
await openPdf(path: path, bytes: effectiveBytes, fileName: name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> openPdf({
|
Future<void> openPdf({
|
||||||
|
|
@ -297,20 +289,20 @@ class PdfSessionViewModel extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (path != null && path.isNotEmpty) {
|
if (path != null && path.isNotEmpty) {
|
||||||
_currentFile = XFile(path);
|
_currentFile = fs.XFile(path);
|
||||||
} else if (bytes != null && (fileName != null && fileName.isNotEmpty)) {
|
} else if (bytes != null && (fileName != null && fileName.isNotEmpty)) {
|
||||||
// Keep in-memory XFile so .name is available for suggestion
|
// Keep in-memory XFile so .name is available for suggestion
|
||||||
try {
|
try {
|
||||||
_currentFile = XFile.fromData(
|
_currentFile = fs.XFile.fromData(
|
||||||
bytes,
|
bytes,
|
||||||
name: fileName,
|
name: fileName,
|
||||||
mimeType: 'application/pdf',
|
mimeType: 'application/pdf',
|
||||||
);
|
);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
_currentFile = XFile(fileName);
|
_currentFile = fs.XFile(fileName);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_currentFile = XFile('');
|
_currentFile = fs.XFile('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update display name: prefer explicit fileName (from picker/drop),
|
// Update display name: prefer explicit fileName (from picker/drop),
|
||||||
|
|
@ -333,7 +325,7 @@ class PdfSessionViewModel extends ChangeNotifier {
|
||||||
void closePdf() {
|
void closePdf() {
|
||||||
ref.read(documentRepositoryProvider.notifier).close();
|
ref.read(documentRepositoryProvider.notifier).close();
|
||||||
ref.read(signatureCardRepositoryProvider.notifier).clearAll();
|
ref.read(signatureCardRepositoryProvider.notifier).clearAll();
|
||||||
_currentFile = XFile('');
|
_currentFile = fs.XFile('');
|
||||||
_displayFileName = '';
|
_displayFileName = '';
|
||||||
router.go('/');
|
router.go('/');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
|
||||||
|
|
@ -93,18 +93,14 @@ class ThumbnailsView extends ConsumerWidget {
|
||||||
padding: const EdgeInsets.all(6),
|
padding: const EdgeInsets.all(6),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
ConstrainedBox(
|
SizedBox(
|
||||||
constraints: const BoxConstraints(maxHeight: 180),
|
height: 180,
|
||||||
child: AspectRatio(
|
child: ClipRRect(
|
||||||
// A4 portrait aspect: width:height ≈ 1:1.4142
|
borderRadius: BorderRadius.circular(4),
|
||||||
aspectRatio: 1 / 1.4142,
|
child: PdfPageView(
|
||||||
child: ClipRRect(
|
document: document,
|
||||||
borderRadius: BorderRadius.circular(4),
|
pageNumber: pageNumber,
|
||||||
child: PdfPageView(
|
alignment: Alignment.center,
|
||||||
document: document,
|
|
||||||
pageNumber: pageNumber,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:file_selector/file_selector.dart' as fs;
|
||||||
import 'package:file_picker/file_picker.dart' as fp;
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
@ -18,12 +17,11 @@ 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;
|
||||||
final VoidCallback onClosePdf;
|
final VoidCallback onClosePdf;
|
||||||
final XFile currentFile;
|
final fs.XFile currentFile;
|
||||||
// Optional display name for the currently opened file. On Linux
|
// Optional display name for the currently opened file. On Linux
|
||||||
// xdg-desktop-portal, XFile.name/path can be a UUID-like value. When
|
// xdg-desktop-portal, XFile.name/path can be a UUID-like value. When
|
||||||
// available, this name preserves the user-selected filename so we can
|
// available, this name preserves the user-selected filename so we can
|
||||||
|
|
@ -60,7 +58,6 @@ 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
|
||||||
|
|
@ -114,18 +111,15 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<img.Image?> _loadSignatureFromFile() async {
|
Future<img.Image?> _loadSignatureFromFile() async {
|
||||||
final result = await fp.FilePicker.platform.pickFiles(
|
final typeGroup = fs.XTypeGroup(
|
||||||
type: fp.FileType.custom,
|
label:
|
||||||
allowedExtensions: const ['png', 'jpg', 'jpeg', 'webp'],
|
Localizations.of<AppLocalizations>(context, AppLocalizations)?.image,
|
||||||
withData: true,
|
extensions: ['png', 'jpg', 'jpeg', 'webp'],
|
||||||
);
|
);
|
||||||
if (result == null || result.files.isEmpty) return null;
|
final file = await fs.openFile(acceptedTypeGroups: [typeGroup]);
|
||||||
final picked = result.files.single;
|
if (file == null) return null;
|
||||||
final Uint8List? bytes =
|
final bytes = await file.readAsBytes();
|
||||||
picked.bytes ??
|
|
||||||
(picked.path != null ? await XFile(picked.path!).readAsBytes() : null);
|
|
||||||
try {
|
try {
|
||||||
if (bytes == null) return null;
|
|
||||||
var sigImage = img.decodeImage(bytes);
|
var sigImage = img.decodeImage(bytes);
|
||||||
return _toStdSignatureImage(sigImage);
|
return _toStdSignatureImage(sigImage);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
|
@ -312,9 +306,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
max: _pagesMax,
|
max: _pagesMax,
|
||||||
builder:
|
builder:
|
||||||
(context, area) => Offstage(
|
(context, area) => Offstage(
|
||||||
offstage:
|
offstage: !_showPagesSidebar,
|
||||||
!(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);
|
||||||
|
|
@ -368,24 +360,6 @@ 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);
|
||||||
|
|
@ -394,65 +368,29 @@ 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];
|
||||||
final wantPagesVisible = _showPagesSidebar && canShowPagesSidebar;
|
if (_showPagesSidebar) {
|
||||||
final isPagesHidden =
|
left.max = _pagesMax;
|
||||||
(left.max == 1 && left.min == 0 && (left.size ?? 1) == 1);
|
left.min = _pagesMin;
|
||||||
if (wantPagesVisible) {
|
left.size = _lastPagesWidth.clamp(_pagesMin, _pagesMax);
|
||||||
// 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 {
|
} else {
|
||||||
// Only collapse if currently visible; remember current size for restore.
|
_lastPagesWidth = left.size ?? _lastPagesWidth;
|
||||||
if (!isPagesHidden) {
|
left.min = 0;
|
||||||
_lastPagesWidth = left.size ?? _lastPagesWidth;
|
left.max = 1;
|
||||||
left.min = 0;
|
left.size = 1; // effectively hidden
|
||||||
left.max = 1;
|
|
||||||
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 {
|
|
||||||
right.max = _signaturesMax;
|
|
||||||
right.min = _signaturesMin;
|
|
||||||
_lastSignaturesWidth = right.size ?? _lastSignaturesWidth;
|
|
||||||
}
|
|
||||||
} else {
|
} 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ 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';
|
||||||
|
|
||||||
|
|
@ -68,16 +67,6 @@ 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(
|
||||||
|
|
@ -93,15 +82,14 @@ 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),
|
||||||
Flexible(
|
ConstrainedBox(
|
||||||
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,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -142,68 +130,62 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE))
|
Wrap(
|
||||||
Wrap(
|
spacing: 6,
|
||||||
spacing: 6,
|
runSpacing: 4,
|
||||||
runSpacing: 4,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
children: [
|
||||||
children: [
|
Text(l.goTo),
|
||||||
Text(l.goTo),
|
SizedBox(
|
||||||
SizedBox(
|
width: gotoWidth,
|
||||||
width: gotoWidth,
|
child: TextField(
|
||||||
child: TextField(
|
key: const Key('txt_goto'),
|
||||||
key: const Key('txt_goto'),
|
controller: _goToController,
|
||||||
controller: _goToController,
|
keyboardType: TextInputType.number,
|
||||||
keyboardType: TextInputType.number,
|
inputFormatters: [
|
||||||
inputFormatters: [
|
FilteringTextInputFormatter.digitsOnly,
|
||||||
FilteringTextInputFormatter.digitsOnly,
|
],
|
||||||
],
|
enabled: !widget.disabled,
|
||||||
enabled: !widget.disabled,
|
decoration: InputDecoration(
|
||||||
decoration: InputDecoration(
|
isDense: true,
|
||||||
isDense: true,
|
hintText: '1..${pdf.pageCount}',
|
||||||
hintText: '1..${pdf.pageCount}',
|
|
||||||
),
|
|
||||||
onSubmitted: (_) => _submitGoTo(),
|
|
||||||
),
|
),
|
||||||
|
onSubmitted: (_) => _submitGoTo(),
|
||||||
),
|
),
|
||||||
if (!compact)
|
),
|
||||||
IconButton(
|
if (!compact)
|
||||||
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(
|
IconButton(
|
||||||
key: const Key('btn_zoom_out'),
|
key: const Key('btn_goto_apply'),
|
||||||
tooltip: 'Zoom out',
|
tooltip: l.goTo,
|
||||||
onPressed: widget.disabled ? null : widget.onZoomOut,
|
icon: const Icon(Icons.arrow_forward),
|
||||||
icon: const Icon(Icons.zoom_out),
|
onPressed: widget.disabled ? null : _submitGoTo,
|
||||||
),
|
),
|
||||||
Text(
|
],
|
||||||
//if not null
|
),
|
||||||
widget.zoomLevel != null
|
const SizedBox(width: 8),
|
||||||
? '${widget.zoomLevel}%'
|
Wrap(
|
||||||
: '',
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
style: const TextStyle(fontSize: 12),
|
children: [
|
||||||
),
|
IconButton(
|
||||||
IconButton(
|
key: const Key('btn_zoom_out'),
|
||||||
key: const Key('btn_zoom_in'),
|
tooltip: 'Zoom out',
|
||||||
tooltip: 'Zoom in',
|
onPressed: widget.disabled ? null : widget.onZoomOut,
|
||||||
onPressed: widget.disabled ? null : widget.onZoomIn,
|
icon: const Icon(Icons.zoom_out),
|
||||||
icon: const Icon(Icons.zoom_in),
|
),
|
||||||
),
|
Text(
|
||||||
],
|
//if not null
|
||||||
),
|
widget.zoomLevel != null ? '${widget.zoomLevel}%' : '',
|
||||||
SizedBox(width: 6),
|
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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -212,21 +194,19 @@ 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',
|
onPressed: widget.disabled ? null : widget.onTogglePagesSidebar,
|
||||||
onPressed: widget.disabled ? null : widget.onTogglePagesSidebar,
|
icon: Icon(
|
||||||
icon: Icon(
|
Icons.view_sidebar,
|
||||||
Icons.view_sidebar,
|
color:
|
||||||
color:
|
widget.showPagesSidebar
|
||||||
widget.showPagesSidebar
|
? Theme.of(context).colorScheme.primary
|
||||||
? Theme.of(context).colorScheme.primary
|
: null,
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
),
|
||||||
],
|
const SizedBox(width: 8),
|
||||||
Expanded(child: center),
|
Expanded(child: center),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ dependencies:
|
||||||
flutter_riverpod: ^2.6.1
|
flutter_riverpod: ^2.6.1
|
||||||
shared_preferences: ^2.5.3
|
shared_preferences: ^2.5.3
|
||||||
flutter_dotenv: ^6.0.0
|
flutter_dotenv: ^6.0.0
|
||||||
|
file_selector: ^1.0.3
|
||||||
path_provider: ^2.1.5
|
path_provider: ^2.1.5
|
||||||
pdfrx: ^2.1.9
|
pdfrx: ^2.1.9
|
||||||
pdf: ^3.10.8
|
pdf: ^3.10.8
|
||||||
|
|
@ -58,8 +59,6 @@ dependencies:
|
||||||
riverpod_annotation: ^2.6.1
|
riverpod_annotation: ^2.6.1
|
||||||
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
|
|
||||||
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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:file_selector/file_selector.dart' as fs;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
@ -14,26 +14,6 @@ import 'package:pdf_signature/data/repositories/document_repository.dart';
|
||||||
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
|
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
|
||||||
import 'package:pdf_signature/l10n/app_localizations.dart';
|
import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||||
|
|
||||||
// A fake export VM that always reports success, so this widget test doesn't
|
|
||||||
// depend on PDF validity or platform specifics.
|
|
||||||
bool exported = false;
|
|
||||||
|
|
||||||
class _FakePdfExportViewModel extends PdfExportViewModel {
|
|
||||||
_FakePdfExportViewModel(Ref ref)
|
|
||||||
: super(ref, savePathPicker: () async => 'C:/tmp/output.pdf');
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> exportToPath({
|
|
||||||
required String outputPath,
|
|
||||||
required Size uiPageSize,
|
|
||||||
required Uint8List? signatureImageBytes,
|
|
||||||
double targetDpi = 144.0,
|
|
||||||
}) async {
|
|
||||||
exported = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('Save uses file selector (via provider) and injected exporter', (
|
testWidgets('Save uses file selector (via provider) and injected exporter', (
|
||||||
tester,
|
tester,
|
||||||
|
|
@ -55,7 +35,10 @@ void main() {
|
||||||
(ref) => PdfViewModel(ref, useMockViewer: true),
|
(ref) => PdfViewModel(ref, useMockViewer: true),
|
||||||
),
|
),
|
||||||
pdfExportViewModelProvider.overrideWith(
|
pdfExportViewModelProvider.overrideWith(
|
||||||
(ref) => _FakePdfExportViewModel(ref),
|
(ref) => PdfExportViewModel(
|
||||||
|
ref,
|
||||||
|
savePathPicker: () async => 'C:/tmp/output.pdf',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
|
|
@ -64,7 +47,7 @@ void main() {
|
||||||
home: PdfSignatureHomePage(
|
home: PdfSignatureHomePage(
|
||||||
onPickPdf: () async {},
|
onPickPdf: () async {},
|
||||||
onClosePdf: () {},
|
onClosePdf: () {},
|
||||||
currentFile: XFile(''),
|
currentFile: fs.XFile(''),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -74,10 +57,10 @@ void main() {
|
||||||
|
|
||||||
// Trigger save directly (mark toggle no longer required)
|
// Trigger save directly (mark toggle no longer required)
|
||||||
await tester.tap(find.byKey(const Key('btn_save_pdf')));
|
await tester.tap(find.byKey(const Key('btn_save_pdf')));
|
||||||
// Pump a bit to allow async export flow to run.
|
await tester.pumpAndSettle();
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
|
||||||
await tester.pump(const Duration(milliseconds: 200));
|
// Expect success UI (localized)
|
||||||
// Basic assertion: export was invoked
|
expect(find.textContaining('Saved:'), findsOneWidget);
|
||||||
expect(exported, isTrue);
|
// Basic assertion: a save flow completed and snackbar showed
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:file_selector/file_selector.dart' as fs;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
@ -36,7 +36,7 @@ Future<void> pumpWithOpenPdf(WidgetTester tester) async {
|
||||||
home: PdfSignatureHomePage(
|
home: PdfSignatureHomePage(
|
||||||
onPickPdf: () async {},
|
onPickPdf: () async {},
|
||||||
onClosePdf: () {},
|
onClosePdf: () {},
|
||||||
currentFile: XFile(''),
|
currentFile: fs.XFile(''),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -413,7 +413,7 @@ Future<void> pumpWithOpenPdfAndSig(WidgetTester tester) async {
|
||||||
home: PdfSignatureHomePage(
|
home: PdfSignatureHomePage(
|
||||||
onPickPdf: () async {},
|
onPickPdf: () async {},
|
||||||
onClosePdf: () {},
|
onClosePdf: () {},
|
||||||
currentFile: XFile(''),
|
currentFile: fs.XFile(''),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:file_selector/file_selector.dart' as fs;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
@ -38,7 +38,7 @@ void main() {
|
||||||
home: PdfSignatureHomePage(
|
home: PdfSignatureHomePage(
|
||||||
onPickPdf: () async {},
|
onPickPdf: () async {},
|
||||||
onClosePdf: () {},
|
onClosePdf: () {},
|
||||||
currentFile: XFile(''),
|
currentFile: fs.XFile(''),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,7 @@ import 'dart:io';
|
||||||
/// --reporter=compact
|
/// --reporter=compact
|
||||||
/// --pattern=*.dart (all files in integration_test/)
|
/// --pattern=*.dart (all files in integration_test/)
|
||||||
Future<int> main(List<String> args) async {
|
Future<int> main(List<String> args) async {
|
||||||
// Default device depends on host OS for a better out-of-the-box experience.
|
String device = 'linux';
|
||||||
String device =
|
|
||||||
Platform.isWindows
|
|
||||||
? 'windows'
|
|
||||||
: Platform.isMacOS
|
|
||||||
? 'macos'
|
|
||||||
: Platform.isLinux
|
|
||||||
? 'linux'
|
|
||||||
: 'chrome';
|
|
||||||
String reporter = 'compact';
|
String reporter = 'compact';
|
||||||
String pattern = '*.dart';
|
String pattern = '*.dart';
|
||||||
|
|
||||||
|
|
@ -92,34 +84,16 @@ Future<int> main(List<String> args) async {
|
||||||
return 3;
|
return 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize and map device aliases (helpful on Windows/macOS)
|
|
||||||
device = _normalizedDeviceId(device);
|
|
||||||
|
|
||||||
// Preflight: ensure `flutter` is invokable in this environment.
|
|
||||||
final flutterOk = await _checkFlutterAvailable();
|
|
||||||
if (!flutterOk) {
|
|
||||||
stderr.writeln(
|
|
||||||
'Could not execute `flutter`. Ensure Flutter is installed and on PATH.',
|
|
||||||
);
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout.writeln(
|
stdout.writeln(
|
||||||
'Running ${selected.length} integration test file(s) sequentially on device: $device...',
|
'Running ${selected.length} integration test file(s) sequentially...',
|
||||||
);
|
);
|
||||||
final results = <String, int>{};
|
final results = <String, int>{};
|
||||||
|
|
||||||
for (final f in selected) {
|
for (final f in selected) {
|
||||||
// Convert to forward slashes for tool compatibility across platforms.
|
final rel = f.path;
|
||||||
final rel = f.path.replaceAll('\\', '/');
|
|
||||||
stdout.writeln('\n=== Running: $rel ===');
|
stdout.writeln('\n=== Running: $rel ===');
|
||||||
final args = <String>['test', rel, '-d', device, '-r', reporter];
|
final args = <String>['test', rel, '-d', device, '-r', reporter];
|
||||||
stdout.writeln('> flutter ${args.join(' ')}');
|
final proc = await Process.start('flutter', args);
|
||||||
final proc = await Process.start(
|
|
||||||
'flutter',
|
|
||||||
args,
|
|
||||||
runInShell: Platform.isWindows, // ensures flutter.bat resolves on Windows
|
|
||||||
);
|
|
||||||
// Pipe output live
|
// Pipe output live
|
||||||
unawaited(proc.stdout.transform(utf8.decoder).forEach(stdout.write));
|
unawaited(proc.stdout.transform(utf8.decoder).forEach(stdout.write));
|
||||||
unawaited(proc.stderr.transform(utf8.decoder).forEach(stderr.write));
|
unawaited(proc.stderr.transform(utf8.decoder).forEach(stderr.write));
|
||||||
|
|
@ -130,12 +104,8 @@ Future<int> main(List<String> args) async {
|
||||||
} else {
|
} else {
|
||||||
stderr.writeln('=== FAILED (exit $code): $rel ===');
|
stderr.writeln('=== FAILED (exit $code): $rel ===');
|
||||||
}
|
}
|
||||||
// Small pause between launches to let desktop/device settle (slightly longer for desktop)
|
// Small pause between launches to let desktop/device settle
|
||||||
await Future<void>.delayed(
|
await Future<void>.delayed(const Duration(milliseconds: 300));
|
||||||
Platform.isWindows || Platform.isMacOS || Platform.isLinux
|
|
||||||
? const Duration(milliseconds: 1200)
|
|
||||||
: const Duration(milliseconds: 300),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout.writeln('\nSummary:');
|
stdout.writeln('\nSummary:');
|
||||||
|
|
@ -148,38 +118,3 @@ Future<int> main(List<String> args) async {
|
||||||
|
|
||||||
return failures == 0 ? 0 : 1;
|
return failures == 0 ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _normalizedDeviceId(String input) {
|
|
||||||
final lower = input.toLowerCase();
|
|
||||||
switch (lower) {
|
|
||||||
case 'win':
|
|
||||||
case 'windows':
|
|
||||||
case 'windows-desktop':
|
|
||||||
return 'windows';
|
|
||||||
case 'mac':
|
|
||||||
case 'macos':
|
|
||||||
case 'darwin':
|
|
||||||
return 'macos';
|
|
||||||
case 'linux':
|
|
||||||
case 'gnu/linux':
|
|
||||||
return 'linux';
|
|
||||||
case 'web':
|
|
||||||
case 'chrome':
|
|
||||||
case 'browser':
|
|
||||||
return 'chrome';
|
|
||||||
default:
|
|
||||||
return input; // assume caller provided a concrete device id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> _checkFlutterAvailable() async {
|
|
||||||
try {
|
|
||||||
final result = await Process.run('flutter', const [
|
|
||||||
'--version',
|
|
||||||
'--suppress-analytics',
|
|
||||||
], runInShell: Platform.isWindows);
|
|
||||||
return result.exitCode == 0;
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue