feat: remember uploaded file name on web and use it when downloading
This commit is contained in:
parent
0c38178502
commit
5a03793b54
|
|
@ -31,7 +31,11 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||||
onPickPdf: () => sessionVm.pickAndOpenPdf(),
|
onPickPdf: () => sessionVm.pickAndOpenPdf(),
|
||||||
onOpenPdf:
|
onOpenPdf:
|
||||||
({String? path, Uint8List? bytes, String? fileName}) =>
|
({String? path, Uint8List? bytes, String? fileName}) =>
|
||||||
sessionVm.openPdf(path: path, bytes: bytes),
|
sessionVm.openPdf(
|
||||||
|
path: path,
|
||||||
|
bytes: bytes,
|
||||||
|
fileName: fileName,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -43,6 +47,7 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||||
onPickPdf: () => sessionVm.pickAndOpenPdf(),
|
onPickPdf: () => sessionVm.pickAndOpenPdf(),
|
||||||
onClosePdf: () => sessionVm.closePdf(),
|
onClosePdf: () => sessionVm.closePdf(),
|
||||||
currentFile: sessionVm.currentFile,
|
currentFile: sessionVm.currentFile,
|
||||||
|
currentFileName: sessionVm.displayFileName,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,27 @@ class PdfExportViewModel extends ChangeNotifier {
|
||||||
|
|
||||||
// Dependencies (injectable via constructor for tests)
|
// Dependencies (injectable via constructor for tests)
|
||||||
final ExportService _exporter;
|
final ExportService _exporter;
|
||||||
|
// Zero-arg picker retained for backward compatibility with tests.
|
||||||
final Future<String?> Function() _savePathPicker;
|
final Future<String?> Function() _savePathPicker;
|
||||||
|
// Preferred picker that accepts a suggested filename.
|
||||||
|
final Future<String?> Function(String suggestedName)
|
||||||
|
_savePathPickerWithSuggestedName;
|
||||||
|
|
||||||
PdfExportViewModel(
|
PdfExportViewModel(
|
||||||
this.ref, {
|
this.ref, {
|
||||||
ExportService? exporter,
|
ExportService? exporter,
|
||||||
Future<String?> Function()? savePathPicker,
|
Future<String?> Function()? savePathPicker,
|
||||||
|
Future<String?> Function(String suggestedName)?
|
||||||
|
savePathPickerWithSuggestedName,
|
||||||
}) : _exporter = exporter ?? ExportService(),
|
}) : _exporter = exporter ?? ExportService(),
|
||||||
_savePathPicker = savePathPicker ?? _defaultSavePathPicker;
|
_savePathPicker = savePathPicker ?? _defaultSavePathPicker,
|
||||||
|
// Prefer provided suggested-name picker; otherwise, if only zero-arg
|
||||||
|
// picker is given (tests), wrap it; else use default that honors name.
|
||||||
|
_savePathPickerWithSuggestedName =
|
||||||
|
savePathPickerWithSuggestedName ??
|
||||||
|
(savePathPicker != null
|
||||||
|
? ((_) => savePathPicker())
|
||||||
|
: _defaultSavePathPickerWithSuggestedName);
|
||||||
|
|
||||||
bool get exporting => _exporting;
|
bool get exporting => _exporting;
|
||||||
|
|
||||||
|
|
@ -35,11 +48,22 @@ class PdfExportViewModel extends ChangeNotifier {
|
||||||
return _savePathPicker();
|
return _savePathPicker();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Show save dialog with a suggested name and return the chosen path.
|
||||||
|
Future<String?> pickSavePathWithSuggestedName(String suggestedName) async {
|
||||||
|
return _savePathPickerWithSuggestedName(suggestedName);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<String?> _defaultSavePathPicker() async {
|
static Future<String?> _defaultSavePathPicker() async {
|
||||||
|
return _defaultSavePathPickerWithSuggestedName('signed.pdf');
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<String?> _defaultSavePathPickerWithSuggestedName(
|
||||||
|
String suggestedName,
|
||||||
|
) async {
|
||||||
final group = fs.XTypeGroup(label: 'PDF', extensions: ['pdf']);
|
final group = fs.XTypeGroup(label: 'PDF', extensions: ['pdf']);
|
||||||
final location = await fs.getSaveLocation(
|
final location = await fs.getSaveLocation(
|
||||||
acceptedTypeGroups: [group],
|
acceptedTypeGroups: [group],
|
||||||
suggestedName: 'signed.pdf',
|
suggestedName: suggestedName,
|
||||||
confirmButtonText: 'Save',
|
confirmButtonText: 'Save',
|
||||||
);
|
);
|
||||||
return location?.path; // null if user cancels
|
return location?.path; // null if user cancels
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
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';
|
||||||
|
|
@ -243,14 +244,21 @@ class PdfSessionViewModel extends ChangeNotifier {
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
final GoRouter router;
|
final GoRouter router;
|
||||||
fs.XFile _currentFile = fs.XFile('');
|
fs.XFile _currentFile = fs.XFile('');
|
||||||
|
// 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
|
||||||
|
// XFile.name derives from that basename, yielding a random UUID instead of
|
||||||
|
// the actual filename the user selected. We preserve the picker/drop name
|
||||||
|
// here to offer a sensible default like "signed_<original>.pdf".
|
||||||
|
String _displayFileName = '';
|
||||||
|
|
||||||
PdfSessionViewModel({required this.ref, required this.router});
|
PdfSessionViewModel({required this.ref, required this.router});
|
||||||
|
|
||||||
fs.XFile get currentFile => _currentFile;
|
fs.XFile get currentFile => _currentFile;
|
||||||
|
String get displayFileName => _displayFileName;
|
||||||
|
|
||||||
Future<void> pickAndOpenPdf() async {
|
Future<void> pickAndOpenPdf() async {
|
||||||
final typeGroup = const fs.XTypeGroup(label: 'PDF', extensions: ['pdf']);
|
final typeGroup = const fs.XTypeGroup(label: 'PDF', extensions: ['pdf']);
|
||||||
final file = await fs.openFile(acceptedTypeGroups: [typeGroup]);
|
final XFile? file = await fs.openFile(acceptedTypeGroups: [typeGroup]);
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
Uint8List? bytes;
|
Uint8List? bytes;
|
||||||
try {
|
try {
|
||||||
|
|
@ -258,11 +266,15 @@ class PdfSessionViewModel extends ChangeNotifier {
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
bytes = null;
|
bytes = null;
|
||||||
}
|
}
|
||||||
await openPdf(path: file.path, bytes: bytes);
|
await openPdf(path: file.path, bytes: bytes, fileName: file.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> openPdf({String? path, Uint8List? bytes}) async {
|
Future<void> openPdf({
|
||||||
|
String? path,
|
||||||
|
Uint8List? bytes,
|
||||||
|
String? fileName,
|
||||||
|
}) async {
|
||||||
int pageCount = 1; // default
|
int pageCount = 1; // default
|
||||||
if (bytes != null) {
|
if (bytes != null) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -272,8 +284,31 @@ class PdfSessionViewModel extends ChangeNotifier {
|
||||||
// ignore invalid bytes
|
// ignore invalid bytes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (path != null) {
|
if (path != null && path.isNotEmpty) {
|
||||||
_currentFile = fs.XFile(path);
|
_currentFile = fs.XFile(path);
|
||||||
|
} else if (bytes != null && (fileName != null && fileName.isNotEmpty)) {
|
||||||
|
// Keep in-memory XFile so .name is available for suggestion
|
||||||
|
try {
|
||||||
|
_currentFile = fs.XFile.fromData(
|
||||||
|
bytes,
|
||||||
|
name: fileName,
|
||||||
|
mimeType: 'application/pdf',
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
_currentFile = fs.XFile(fileName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_currentFile = fs.XFile('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update display name: prefer explicit fileName (from picker/drop),
|
||||||
|
// fall back to basename of path, otherwise empty.
|
||||||
|
if (fileName != null && fileName.isNotEmpty) {
|
||||||
|
_displayFileName = fileName;
|
||||||
|
} else if (path != null && path.isNotEmpty) {
|
||||||
|
_displayFileName = path.split('/').last.split('\\').last;
|
||||||
|
} else {
|
||||||
|
_displayFileName = '';
|
||||||
}
|
}
|
||||||
ref
|
ref
|
||||||
.read(documentRepositoryProvider.notifier)
|
.read(documentRepositoryProvider.notifier)
|
||||||
|
|
@ -287,6 +322,7 @@ class PdfSessionViewModel extends ChangeNotifier {
|
||||||
ref.read(documentRepositoryProvider.notifier).close();
|
ref.read(documentRepositoryProvider.notifier).close();
|
||||||
ref.read(signatureCardRepositoryProvider.notifier).clearAll();
|
ref.read(signatureCardRepositoryProvider.notifier).clearAll();
|
||||||
_currentFile = fs.XFile('');
|
_currentFile = fs.XFile('');
|
||||||
|
_displayFileName = '';
|
||||||
router.go('/');
|
router.go('/');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,18 @@ class PdfSignatureHomePage extends ConsumerStatefulWidget {
|
||||||
final Future<void> Function() onPickPdf;
|
final Future<void> Function() onPickPdf;
|
||||||
final VoidCallback onClosePdf;
|
final VoidCallback onClosePdf;
|
||||||
final fs.XFile currentFile;
|
final fs.XFile currentFile;
|
||||||
|
// Optional display name for the currently opened file. On Linux
|
||||||
|
// xdg-desktop-portal, XFile.name/path can be a UUID-like value. When
|
||||||
|
// available, this name preserves the user-selected filename so we can
|
||||||
|
// suggest a proper "signed_*.pdf" on save.
|
||||||
|
final String? currentFileName;
|
||||||
|
|
||||||
const PdfSignatureHomePage({
|
const PdfSignatureHomePage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onPickPdf,
|
required this.onPickPdf,
|
||||||
required this.onClosePdf,
|
required this.onClosePdf,
|
||||||
required this.currentFile,
|
required this.currentFile,
|
||||||
|
this.currentFileName,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -152,8 +158,23 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
String? savedPath;
|
String? savedPath;
|
||||||
|
|
||||||
|
// Derive a suggested filename based on the opened file. Prefer the
|
||||||
|
// provided display name if available (see Linux portal note above).
|
||||||
|
final display = widget.currentFileName;
|
||||||
|
final originalName =
|
||||||
|
(display != null && display.trim().isNotEmpty)
|
||||||
|
? display.trim()
|
||||||
|
: widget.currentFile.name.isNotEmpty
|
||||||
|
? widget.currentFile.name
|
||||||
|
: widget.currentFile.path.isNotEmpty
|
||||||
|
? widget.currentFile.path.split('/').last.split('\\').last
|
||||||
|
: 'document.pdf';
|
||||||
|
final suggested = _suggestSignedName(originalName);
|
||||||
|
|
||||||
if (!kIsWeb) {
|
if (!kIsWeb) {
|
||||||
final path = await ref.read(pdfExportViewModelProvider).pickSavePath();
|
final path = await ref
|
||||||
|
.read(pdfExportViewModelProvider)
|
||||||
|
.pickSavePathWithSuggestedName(suggested);
|
||||||
if (path == null || path.trim().isEmpty) return;
|
if (path == null || path.trim().isEmpty) return;
|
||||||
final fullPath = _ensurePdfExtension(path.trim());
|
final fullPath = _ensurePdfExtension(path.trim());
|
||||||
savedPath = fullPath;
|
savedPath = fullPath;
|
||||||
|
|
@ -179,9 +200,9 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
targetDpi: targetDpi,
|
targetDpi: targetDpi,
|
||||||
);
|
);
|
||||||
if (out != null) {
|
if (out != null) {
|
||||||
// Use a sensible default filename (cannot prompt path on web)
|
// Use suggested filename for browser download
|
||||||
ok = await downloadBytes(out, filename: 'signed.pdf');
|
ok = await downloadBytes(out, filename: suggested);
|
||||||
savedPath = 'signed.pdf';
|
savedPath = suggested;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!kIsWeb) {
|
if (!kIsWeb) {
|
||||||
|
|
@ -224,6 +245,15 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _suggestSignedName(String original) {
|
||||||
|
// Normalize to a base filename
|
||||||
|
final base = original.split('/').last.split('\\').last;
|
||||||
|
if (base.toLowerCase().endsWith('.pdf')) {
|
||||||
|
return 'signed_' + base;
|
||||||
|
}
|
||||||
|
return 'signed_' + base + '.pdf';
|
||||||
|
}
|
||||||
|
|
||||||
void _onControllerChanged() {
|
void _onControllerChanged() {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
if (_viewModel.controller.isReady) {
|
if (_viewModel.controller.isReady) {
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ Future<void> handleDroppedFiles(
|
||||||
bytes = null;
|
bytes = null;
|
||||||
}
|
}
|
||||||
final String path = pdf.path ?? pdf.name;
|
final String path = pdf.path ?? pdf.name;
|
||||||
await onOpenPdf(path: path, bytes: bytes);
|
await onOpenPdf(path: path, bytes: bytes, fileName: pdf.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
class WelcomeScreen extends ConsumerStatefulWidget {
|
class WelcomeScreen extends ConsumerStatefulWidget {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue