feat(frontend): implement print queue and fix document download issues

This commit is contained in:
ssbg04 committed 2026-07-15 01:22:33 +08:00
1 parent fe8f9be585
commit c8780c53db
7 files changed
+231 -64

No files matched your search

@@ -1,3 +1,5 @@
import 'dart:io';
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../../core/network/api_constants.dart';
@@ -600,5 +602,33 @@ class DocumentRepository {
throw Exception(msg);
}
}
Future<List<int>> downloadDocumentBytes(int documentId) async {
try {
final options = await _getAuthOptions();
options.responseType = ResponseType.bytes;
final response = await _dio.get(
'/documents/$documentId/view',
queryParameters: {'download': 'true'},
options: options,
);
return response.data as List<int>;
} on DioException catch (e) {
String msg = 'Failed to download document.';
if (e.response?.data != null) {
try {
// If response is bytes, we might need to decode it to string to parse JSON
if (e.response!.data is List<int>) {
final str = utf8.decode(e.response!.data as List<int>);
final json = jsonDecode(str);
msg = json['message'] ?? msg;
} else if (e.response!.data is Map) {
msg = e.response!.data['message'] ?? msg;
}
} catch (_) {}
}
throw Exception(msg);
}
}
}
@@ -291,7 +291,9 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
if (token == null) return;
final url = '${ApiConstants.baseUrl}/documents/${document.id}/view?token=$token&download=true';
showSuccessDialog(context, message: 'Download started...');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Download started...'), duration: Duration(seconds: 2)),
);
await DownloadService.downloadFile(url: url, fileName: document.fileName);
if (!context.mounted) return;
showSuccessDialog(context, message: 'Document downloaded successfully.');
@@ -407,7 +409,9 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
final token = await const FlutterSecureStorage().read(key: 'jwt_token');
if (token == null) return;
showSuccessDialog(context, message: 'Downloading ${_selectedDocumentIds.length} documents...');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Downloading ${_selectedDocumentIds.length} documents...'), duration: const Duration(seconds: 2)),
);
final docs = ref.read(documentPageProvider).value?.documents ?? [];
int successCount = 0;
@@ -1,5 +1,10 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:printing/printing.dart';
import 'package:syncfusion_flutter_pdf/pdf.dart';
import 'package:wolt_modal_sheet/wolt_modal_sheet.dart';
import '../../../../core/constants/app_colors.dart';
import '../../../../core/constants/app_sizes.dart';
import '../../../../domain/repositories/document_repository.dart'
@@ -8,7 +13,6 @@ import '../../../shared/buttons/primary_button.dart';
import '../../../shared/dialogs/success_dialog.dart';
import '../../../shared/dialogs/error_dialog.dart';
import '../../../providers/document_provider.dart';
import 'package:wolt_modal_sheet/wolt_modal_sheet.dart';
class PrintQueueModal extends ConsumerStatefulWidget {
const PrintQueueModal({super.key});
@@ -41,6 +45,74 @@ class _PrintQueueModalState extends ConsumerState<PrintQueueModal> {
setState(() => _isPrinting = true);
try {
final docRepo = ref.read(documentRepositoryProvider);
// Merge all documents into a single PDF
final combinedPdf = PdfDocument();
for (var item in items) {
final bytes = await docRepo.downloadDocumentBytes(item.documentId);
final isPdf = item.fileName.toLowerCase().endsWith('.pdf');
if (isPdf) {
final loadedPdf = PdfDocument(inputBytes: bytes);
for (int i = 0; i < loadedPdf.pages.count; i++) {
final template = loadedPdf.pages[i].createTemplate();
final page = combinedPdf.pages.add();
page.graphics.drawPdfTemplate(template, const Offset(0, 0));
}
loadedPdf.dispose();
} else {
// Assume image
final page = combinedPdf.pages.add();
final pdfImage = PdfBitmap(bytes);
// Calculate scale to fit page while maintaining aspect ratio
final clientSize = page.getClientSize();
final imgWidth = pdfImage.width.toDouble();
final imgHeight = pdfImage.height.toDouble();
final ratio = imgWidth / imgHeight;
final clientRatio = clientSize.width / clientSize.height;
double drawWidth = clientSize.width;
double drawHeight = clientSize.height;
if (ratio > clientRatio) {
drawHeight = drawWidth / ratio;
} else {
drawWidth = drawHeight * ratio;
}
page.graphics.drawImage(
pdfImage,
Rect.fromLTWH(
(clientSize.width - drawWidth) / 2,
(clientSize.height - drawHeight) / 2,
drawWidth,
drawHeight
)
);
}
}
final List<int> combinedBytes = await combinedPdf.save();
combinedPdf.dispose();
// Send to printer
final result = await Printing.layoutPdf(
onLayout: (format) async => Uint8List.fromList(combinedBytes),
name: 'Batch_Print_${DateTime.now().millisecondsSinceEpoch}',
);
// If the user cancelled the print dialog, we might not want to clear the queue
if (!result) {
if (!mounted) return;
setState(() => _isPrinting = false);
return; // User cancelled print dialog
}
// Log history and clear queue in backend
await ref.read(printQueueMutationProvider.notifier).executePrint();
if (!mounted) return;
@@ -113,69 +185,69 @@ class _PrintQueueModalState extends ConsumerState<PrintQueueModal> {
children: [
// ── Header ──
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.primaryGreen.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.print,
color: AppColors.primaryGreen, size: 22),
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.primaryGreen.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
const SizedBox(width: AppSizes.p12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Print List',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary)),
Text('Staged documents. Note: This is only a list of documents to be printed or requested.',
style: TextStyle(
fontSize: 11,
color: AppColors.textSecondary,
fontStyle: FontStyle.italic)),
],
),
),
IconButton(
icon: const Icon(Icons.close,
color: AppColors.textSecondary),
onPressed: () => Navigator.of(context).pop(),
),
],
),
const Divider(height: AppSizes.p32),
// ── Queue Content ──
queueAsync.when(
loading: () => const Padding(
padding: EdgeInsets.symmetric(vertical: 40),
child: Center(
child: CircularProgressIndicator(
color: AppColors.primaryGreen),
child: const Icon(Icons.print,
color: AppColors.primaryGreen, size: 22),
),
const SizedBox(width: AppSizes.p12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Print List',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary)),
Text('Staged documents. Note: This is only a list of documents to be printed or requested.',
style: TextStyle(
fontSize: 11,
color: AppColors.textSecondary,
fontStyle: FontStyle.italic)),
],
),
),
error: (e, _) => _buildErrorState(e.toString()),
data: (items) => items.isEmpty
? _buildEmptyState()
: _buildQueueList(items),
),
IconButton(
icon: const Icon(Icons.close,
color: AppColors.textSecondary),
onPressed: () => Navigator.of(context).pop(),
),
],
),
const Divider(height: AppSizes.p32),
// ── Footer ──
queueAsync.maybeWhen(
data: (items) => items.isNotEmpty
? _buildFooter(items, isMobile)
: const SizedBox.shrink(),
orElse: () => const SizedBox.shrink(),
// ── Queue Content ──
queueAsync.when(
loading: () => const Padding(
padding: EdgeInsets.symmetric(vertical: 40),
child: Center(
child: CircularProgressIndicator(
color: AppColors.primaryGreen),
),
),
],
),
error: (e, _) => _buildErrorState(e.toString()),
data: (items) => items.isEmpty
? _buildEmptyState()
: _buildQueueList(items),
),
// ── Footer ──
queueAsync.maybeWhen(
data: (items) => items.isNotEmpty
? _buildFooter(items, isMobile)
: const SizedBox.shrink(),
orElse: () => const SizedBox.shrink(),
),
],
),
);
),
);
}
Widget _buildQueueList(List<PrintQueueItem> items) {
@@ -267,7 +339,6 @@ class _PrintQueueModalState extends ConsumerState<PrintQueueModal> {
);
}
Widget _buildFooter(List<PrintQueueItem> items, bool isMobile) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -297,7 +368,7 @@ class _PrintQueueModalState extends ConsumerState<PrintQueueModal> {
const SizedBox(width: 8),
Expanded(
child: PrimaryButton(
label: 'PRINT',
label: _isPrinting ? 'PREPARING...' : 'PRINT',
isLoading: _isPrinting,
onPressed: () => _handlePrintAll(items),
),
@@ -332,9 +403,9 @@ class _PrintQueueModalState extends ConsumerState<PrintQueueModal> {
),
const SizedBox(width: AppSizes.p8),
SizedBox(
width: 130,
width: 140,
child: PrimaryButton(
label: 'PRINT',
label: _isPrinting ? 'PREPARING...' : 'PRINT',
isLoading: _isPrinting,
onPressed: () => _handlePrintAll(items),
),
+56
View File
@@ -57,6 +57,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
barcode:
dependency: transitive
description:
name: barcode
sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4"
url: "https://pub.dev"
source: hosted
version: "2.2.9"
bidi:
dependency: transitive
description:
name: bidi
sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d"
url: "https://pub.dev"
source: hosted
version: "2.0.13"
boolean_selector:
dependency: transitive
description:
@@ -768,6 +784,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider:
dependency: "direct main"
description:
@@ -816,6 +840,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
pdf:
dependency: "direct main"
description:
name: pdf
sha256: e47a275b267873d5944ad5f5ff0dcc7ac2e36c02b3046a0ffac9b72fd362c44b
url: "https://pub.dev"
source: hosted
version: "3.12.0"
pdf_widget_wrapper:
dependency: transitive
description:
name: pdf_widget_wrapper
sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5
url: "https://pub.dev"
source: hosted
version: "1.0.4"
permission_handler:
dependency: "direct main"
description:
@@ -896,6 +936,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.2"
printing:
dependency: "direct main"
description:
name: printing
sha256: "689170c9ddb1bda85826466ba80378aa8993486d3c959a71cd7d2d80cb606692"
url: "https://pub.dev"
source: hosted
version: "5.14.3"
pub_semver:
dependency: transitive
description:
@@ -904,6 +952,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.0"
qr:
dependency: transitive
description:
name: qr
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
record_use:
dependency: transitive
description:
+2
View File
@@ -57,6 +57,8 @@ dependencies:
google_mlkit_document_scanner: ^0.4.1
data_table_2: ^2.7.2
wolt_modal_sheet: ^0.11.0
printing: ^5.14.3
pdf: ^3.12.0
dev_dependencies:
flutter_test:
@@ -9,6 +9,7 @@
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <printing/printing_plugin.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <syncfusion_pdfviewer_windows/syncfusion_pdfviewer_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
@@ -21,6 +22,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
PrintingPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PrintingPlugin"));
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
SyncfusionPdfviewerWindowsPluginRegisterWithRegistrar(
@@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
flutter_secure_storage_windows
permission_handler_windows
printing
screen_retriever_windows
syncfusion_pdfviewer_windows
url_launcher_windows