A more robust pdf generation > preview > print workflow

This commit is contained in:
Marc Rejohn Castillano 2026-02-25 20:59:21 +08:00
parent 3a923ea7f6
commit db14ec3916
8 changed files with 483 additions and 330 deletions

Binary file not shown.

Binary file not shown.

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdfrx/pdfrx.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
@ -36,6 +37,8 @@ Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// The flag optionally hides annoying WASM warnings in your Chrome dev console
pdfrxFlutterInitialize(dismissPdfiumWasmWarnings: true);
// initialize Firebase before anything that uses messaging // initialize Firebase before anything that uses messaging
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
@ -192,7 +195,10 @@ class NotificationSoundObserver extends ProviderObserver {
controller.registerFcmToken(token); controller.registerFcmToken(token);
} }
}) })
.catchError((e) => debugPrint('getToken error: $e')); .catchError((e) {
debugPrint('getToken error: $e');
return null;
});
_tokenSub = FirebaseMessaging.instance.onTokenRefresh.listen((token) { _tokenSub = FirebaseMessaging.instance.onTokenRefresh.listen((token) {
debugPrint('token refreshed: $token'); debugPrint('token refreshed: $token');
controller.registerFcmToken(token); controller.registerFcmToken(token);
@ -210,7 +216,10 @@ class NotificationSoundObserver extends ProviderObserver {
controller.unregisterFcmToken(token); controller.unregisterFcmToken(token);
} }
}) })
.catchError((e) => debugPrint('getToken error: $e')); .catchError((e) {
debugPrint('getToken error: $e');
return null;
});
} }
} }
} }

View File

@ -7,6 +7,7 @@ import 'package:flutter_quill/flutter_quill.dart' as quill;
import 'package:pdf/widgets.dart' as pw; import 'package:pdf/widgets.dart' as pw;
import 'package:pdf/pdf.dart' as pdf; import 'package:pdf/pdf.dart' as pdf;
import 'package:printing/printing.dart'; import 'package:printing/printing.dart';
import 'package:pdfrx/pdfrx.dart';
import '../../models/task.dart'; import '../../models/task.dart';
import '../../models/ticket.dart'; import '../../models/ticket.dart';
@ -27,6 +28,14 @@ Future<Uint8List> buildTaskPdfBytes(
) async { ) async {
final logoData = await rootBundle.load('assets/crmc_logo.png'); final logoData = await rootBundle.load('assets/crmc_logo.png');
final logoImage = pw.MemoryImage(logoData.buffer.asUint8List()); final logoImage = pw.MemoryImage(logoData.buffer.asUint8List());
final regularFontData = await rootBundle.load(
'assets/fonts/Roboto-Regular.ttf',
);
final boldFontData = await rootBundle.load('assets/fonts/Roboto-Bold.ttf');
final regularFont = pw.Font.ttf(regularFontData);
final boldFont = pw.Font.ttf(boldFontData);
final doc = pw.Document(); final doc = pw.Document();
final created = AppTime.formatDate(task.createdAt); final created = AppTime.formatDate(task.createdAt);
@ -85,321 +94,325 @@ Future<Uint8List> buildTaskPdfBytes(
final profileById = {for (final p in profiles) p.id: p}; final profileById = {for (final p in profiles) p.id: p};
final assignedForTask = assignments.where((a) => a.taskId == task.id).toList() final assignedForTask = assignments.where((a) => a.taskId == task.id).toList()
..sort((a, b) => a.createdAt.compareTo(b.createdAt)); ..sort((a, b) => a.createdAt.compareTo(b.createdAt));
// Collect all unique assigned user IDs for this task and map to profile names
final assignedUserIds = {for (final a in assignedForTask) a.userId}; final assignedUserIds = {for (final a in assignedForTask) a.userId};
final performedBy = assignedUserIds.isEmpty final performedBy = assignedUserIds.isEmpty
? '' ? ''
: assignedUserIds.map((id) => profileById[id]?.fullName ?? id).join(', '); : assignedUserIds.map((id) => profileById[id]?.fullName ?? id).join(', ');
// Use MultiPage to avoid overflow on long content and apply embedded fonts via ThemeData
doc.addPage( doc.addPage(
pw.Page( pw.MultiPage(
pageFormat: format ?? pdf.PdfPageFormat.a4, pageFormat: format ?? pdf.PdfPageFormat.a4,
margin: pw.EdgeInsets.all(28), margin: pw.EdgeInsets.all(28),
build: (pw.Context ctx) { theme: pw.ThemeData.withFont(base: regularFont, bold: boldFont),
return pw.Column( footer: (pw.Context ctx) => pw.Container(
crossAxisAlignment: pw.CrossAxisAlignment.start, alignment: pw.Alignment.centerRight,
children: [ child: pw.Text(
pw.Center( 'MC-IHO-F-05 Rev. 2',
child: pw.Row( style: pw.TextStyle(fontSize: 9, color: pdf.PdfColors.grey),
mainAxisAlignment: pw.MainAxisAlignment.center, ),
),
build: (pw.Context ctx) => [
pw.Center(
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.center,
crossAxisAlignment: pw.CrossAxisAlignment.center,
children: [
pw.Container(width: 80, height: 80, child: pw.Image(logoImage)),
pw.SizedBox(width: 16),
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.center, crossAxisAlignment: pw.CrossAxisAlignment.center,
children: [ children: [
pw.Container( pw.Text(
width: 80, 'Republic of the Philippines',
height: 80, textAlign: pw.TextAlign.center,
child: pw.Image(logoImage),
), ),
pw.SizedBox(width: 16), pw.Text(
pw.Column( 'Department of Health',
crossAxisAlignment: pw.CrossAxisAlignment.center, textAlign: pw.TextAlign.center,
),
pw.Text(
'Regional and Medical Center',
textAlign: pw.TextAlign.center,
),
pw.SizedBox(height: 6),
pw.Text(
'Cotabato Regional and Medical Center',
textAlign: pw.TextAlign.center,
style: pw.TextStyle(fontWeight: pw.FontWeight.bold),
),
pw.Text(
'Integrated Hospital Operations and Management Program',
textAlign: pw.TextAlign.center,
),
pw.Text('(IHOMP)', textAlign: pw.TextAlign.center),
],
),
],
),
),
pw.SizedBox(height: 12),
pw.Center(
child: pw.Text(
'IT Job / Maintenance Request Form',
style: pw.TextStyle(fontSize: 16, fontWeight: pw.FontWeight.bold),
),
),
pw.SizedBox(height: 12),
pw.Row(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Expanded(
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Row(
children: [ children: [
pw.Text( pw.Text('Task Number: '),
'Republic of the Philippines', pw.Container(
textAlign: pw.TextAlign.center, padding: pw.EdgeInsets.only(bottom: 2),
decoration: pw.BoxDecoration(
border: pw.Border(
bottom: pw.BorderSide(
width: 0.8,
color: pdf.PdfColors.black,
),
),
),
child: pw.Text(task.taskNumber ?? task.id),
), ),
pw.Text( ],
'Department of Health', ),
textAlign: pw.TextAlign.center, pw.SizedBox(height: 8),
pw.Row(
children: [
pw.Text('Service: '),
pw.Container(
padding: pw.EdgeInsets.only(bottom: 2),
decoration: pw.BoxDecoration(
border: pw.Border(
bottom: pw.BorderSide(
width: 0.8,
color: pdf.PdfColors.black,
),
),
),
child: pw.Text(serviceName),
), ),
pw.Text( ],
'Regional and Medical Center', ),
textAlign: pw.TextAlign.center, pw.SizedBox(height: 8),
pw.Row(
children: [
pw.Text('Type: '),
pw.Container(
padding: pw.EdgeInsets.only(bottom: 2),
decoration: pw.BoxDecoration(
border: pw.Border(
bottom: pw.BorderSide(
width: 0.8,
color: pdf.PdfColors.black,
),
),
),
child: pw.Text(task.requestType ?? ''),
), ),
pw.SizedBox(height: 6),
pw.Text(
'Cotabato Regional and Medical Center',
textAlign: pw.TextAlign.center,
style: pw.TextStyle(fontWeight: pw.FontWeight.bold),
),
pw.Text(
'Integrated Hospital Operations and Management Program',
textAlign: pw.TextAlign.center,
),
pw.Text('(IHOMP)', textAlign: pw.TextAlign.center),
], ],
), ),
], ],
), ),
), ),
pw.SizedBox(height: 12), pw.SizedBox(width: 12),
pw.Center( pw.Container(
child: pw.Text( width: 180,
'IT Job / Maintenance Request Form', child: pw.Column(
style: pw.TextStyle( crossAxisAlignment: pw.CrossAxisAlignment.start,
fontSize: 16, children: [
fontWeight: pw.FontWeight.bold, pw.Row(
children: [
pw.Text('Filed At: '),
pw.Container(
padding: pw.EdgeInsets.only(bottom: 2),
decoration: pw.BoxDecoration(
border: pw.Border(
bottom: pw.BorderSide(
width: 0.8,
color: pdf.PdfColors.black,
),
),
),
child: pw.Text(created),
),
],
),
pw.SizedBox(height: 8),
pw.Row(
children: [
pw.Text('Office: '),
pw.Container(
padding: pw.EdgeInsets.only(bottom: 2),
decoration: pw.BoxDecoration(
border: pw.Border(
bottom: pw.BorderSide(
width: 0.8,
color: pdf.PdfColors.black,
),
),
),
child: pw.Text(officeName),
),
],
),
pw.SizedBox(height: 8),
pw.Row(
children: [
pw.Text('Category: '),
pw.Container(
padding: pw.EdgeInsets.only(bottom: 2),
decoration: pw.BoxDecoration(
border: pw.Border(
bottom: pw.BorderSide(
width: 0.8,
color: pdf.PdfColors.black,
),
),
),
child: pw.Text(task.requestCategory ?? ''),
),
],
),
],
),
),
],
),
pw.SizedBox(height: 12),
pw.Divider(thickness: 0.8, color: pdf.PdfColors.grey),
pw.SizedBox(height: 6),
pw.Center(
child: pw.Text(
task.title,
textAlign: pw.TextAlign.center,
style: pw.TextStyle(fontWeight: pw.FontWeight.bold),
),
),
pw.SizedBox(height: 6),
pw.Divider(thickness: 0.8, color: pdf.PdfColors.grey),
pw.SizedBox(height: 6),
pw.Text('Description:'),
pw.SizedBox(height: 6),
pw.Container(
padding: pw.EdgeInsets.all(6),
child: pw.Text(descriptionText),
),
pw.SizedBox(height: 12),
// Requested/Noted signature lines (bottom-aligned to match Performed/Received)
pw.Row(
crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
pw.Expanded(
child: pw.Container(
height: 56,
child: pw.Column(
mainAxisAlignment: pw.MainAxisAlignment.end,
children: [
pw.Container(height: 1, color: pdf.PdfColors.black),
pw.SizedBox(height: 6),
pw.Text(
requestedBy,
style: pw.TextStyle(fontWeight: pw.FontWeight.bold),
),
pw.Text('Requested By'),
],
), ),
), ),
), ),
pw.SizedBox(height: 12), pw.SizedBox(width: 12),
pw.Row( pw.Expanded(
crossAxisAlignment: pw.CrossAxisAlignment.start, child: pw.Container(
children: [ height: 56,
pw.Expanded( child: pw.Column(
child: pw.Column( mainAxisAlignment: pw.MainAxisAlignment.end,
crossAxisAlignment: pw.CrossAxisAlignment.start, children: [
children: [ pw.Container(height: 1, color: pdf.PdfColors.black),
pw.Row( pw.SizedBox(height: 6),
children: [ pw.Text(
pw.Text('Task Number: '), notedBy,
pw.Container( style: pw.TextStyle(fontWeight: pw.FontWeight.bold),
padding: pw.EdgeInsets.only(bottom: 2), ),
decoration: pw.BoxDecoration( pw.Text('Noted by Supervisor/Senior'),
border: pw.Border( ],
bottom: pw.BorderSide(
width: 0.8,
color: pdf.PdfColors.black,
),
),
),
child: pw.Text(task.taskNumber ?? task.id),
),
],
),
pw.SizedBox(height: 8),
pw.Row(
children: [
pw.Text('Service: '),
pw.Container(
padding: pw.EdgeInsets.only(bottom: 2),
decoration: pw.BoxDecoration(
border: pw.Border(
bottom: pw.BorderSide(
width: 0.8,
color: pdf.PdfColors.black,
),
),
),
child: pw.Text(serviceName),
),
],
),
pw.SizedBox(height: 8),
pw.Row(
children: [
pw.Text('Type: '),
pw.Container(
padding: pw.EdgeInsets.only(bottom: 2),
decoration: pw.BoxDecoration(
border: pw.Border(
bottom: pw.BorderSide(
width: 0.8,
color: pdf.PdfColors.black,
),
),
),
child: pw.Text(task.requestType ?? ''),
),
],
),
],
),
), ),
pw.SizedBox(width: 12),
pw.Container(
width: 180,
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Row(
children: [
pw.Text('Filed At: '),
pw.Container(
padding: pw.EdgeInsets.only(bottom: 2),
decoration: pw.BoxDecoration(
border: pw.Border(
bottom: pw.BorderSide(
width: 0.8,
color: pdf.PdfColors.black,
),
),
),
child: pw.Text(created),
),
],
),
pw.SizedBox(height: 8),
pw.Row(
children: [
pw.Text('Office: '),
pw.Container(
padding: pw.EdgeInsets.only(bottom: 2),
decoration: pw.BoxDecoration(
border: pw.Border(
bottom: pw.BorderSide(
width: 0.8,
color: pdf.PdfColors.black,
),
),
),
child: pw.Text(officeName),
),
],
),
pw.SizedBox(height: 8),
pw.Row(
children: [
pw.Text('Category: '),
pw.Container(
padding: pw.EdgeInsets.only(bottom: 2),
decoration: pw.BoxDecoration(
border: pw.Border(
bottom: pw.BorderSide(
width: 0.8,
color: pdf.PdfColors.black,
),
),
),
child: pw.Text(task.requestCategory ?? ''),
),
],
),
],
),
),
],
),
pw.SizedBox(height: 12),
pw.Divider(thickness: 0.8, color: pdf.PdfColors.grey),
pw.SizedBox(height: 6),
pw.Center(
child: pw.Text(
task.title,
textAlign: pw.TextAlign.center,
style: pw.TextStyle(fontWeight: pw.FontWeight.bold),
), ),
), ),
pw.SizedBox(height: 6), ],
pw.Divider(thickness: 0.8, color: pdf.PdfColors.grey), ),
pw.SizedBox(height: 6), pw.SizedBox(height: 12),
pw.Text('Description:'), pw.Text('Action Taken:'),
pw.SizedBox(height: 6), pw.SizedBox(height: 6),
pw.Container( pw.Container(
padding: pw.EdgeInsets.all(6), padding: pw.EdgeInsets.all(6),
child: pw.Text(descriptionText), child: pw.Text(actionTakenText),
),
pw.SizedBox(height: 6),
pw.Divider(thickness: 0.8, color: pdf.PdfColors.grey),
pw.SizedBox(height: 12),
// Historical timestamps side-by-side: Created / Started / Closed
pw.Row(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Expanded(
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text('Created At:'),
pw.SizedBox(height: 4),
pw.Text(
'${AppTime.formatDate(task.createdAt)} ${AppTime.formatTime(task.createdAt)}',
),
],
),
), ),
pw.SizedBox(height: 12), pw.SizedBox(width: 12),
// Requested/Noted signature lines (bottom-aligned to match Performed/Received) pw.Expanded(
pw.Row( child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.end, crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [ children: [
pw.Expanded( pw.Text('Started At:'),
child: pw.Container( pw.SizedBox(height: 4),
height: 56, pw.Text(
child: pw.Column( task.startedAt == null
mainAxisAlignment: pw.MainAxisAlignment.end, ? ''
children: [ : '${AppTime.formatDate(task.startedAt!)} ${AppTime.formatTime(task.startedAt!)}',
pw.Container(height: 1, color: pdf.PdfColors.black),
pw.SizedBox(height: 6),
pw.Text(
requestedBy,
style: pw.TextStyle(fontWeight: pw.FontWeight.bold),
),
pw.Text('Requested By'),
],
),
), ),
), ],
pw.SizedBox(width: 12), ),
pw.Expanded(
child: pw.Container(
height: 56,
child: pw.Column(
mainAxisAlignment: pw.MainAxisAlignment.end,
children: [
pw.Container(height: 1, color: pdf.PdfColors.black),
pw.SizedBox(height: 6),
pw.Text(
notedBy,
style: pw.TextStyle(fontWeight: pw.FontWeight.bold),
),
pw.Text('Noted by Supervisor/Senior'),
],
),
),
),
],
), ),
pw.SizedBox(height: 12), pw.SizedBox(width: 12),
pw.Text('Action Taken:'), pw.Expanded(
pw.SizedBox(height: 6), child: pw.Column(
pw.Container( crossAxisAlignment: pw.CrossAxisAlignment.start,
padding: pw.EdgeInsets.all(6), children: [
child: pw.Text(actionTakenText), pw.Text('Closed At:'),
pw.SizedBox(height: 4),
pw.Text(
task.completedAt == null
? ''
: '${AppTime.formatDate(task.completedAt!)} ${AppTime.formatTime(task.completedAt!)}',
),
],
),
), ),
pw.SizedBox(height: 6), ],
pw.Divider(thickness: 0.8, color: pdf.PdfColors.grey), ),
pw.SizedBox(height: 12), pw.SizedBox(height: 36),
// Historical timestamps side-by-side: Created / Started / Closed
pw.Row( // Wrap signature block to prevent awkward page breaks
crossAxisAlignment: pw.CrossAxisAlignment.start, pw.Wrap(
children: [ spacing: 6,
pw.Expanded( runSpacing: 6,
child: pw.Column( children: [
crossAxisAlignment: pw.CrossAxisAlignment.start, // Signature lines row (fixed)
children: [
pw.Text('Created At:'),
pw.SizedBox(height: 4),
pw.Text(
'${AppTime.formatDate(task.createdAt)} ${AppTime.formatTime(task.createdAt)}',
),
],
),
),
pw.SizedBox(width: 12),
pw.Expanded(
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text('Started At:'),
pw.SizedBox(height: 4),
pw.Text(
task.startedAt == null
? ''
: '${AppTime.formatDate(task.startedAt!)} ${AppTime.formatTime(task.startedAt!)}',
),
],
),
),
pw.SizedBox(width: 12),
pw.Expanded(
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text('Closed At:'),
pw.SizedBox(height: 4),
pw.Text(
task.completedAt == null
? ''
: '${AppTime.formatDate(task.completedAt!)} ${AppTime.formatTime(task.completedAt!)}',
),
],
),
),
],
),
pw.SizedBox(height: 36),
// Signature lines row (fixed) stays aligned regardless of name length
pw.Row( pw.Row(
children: [ children: [
pw.Expanded( pw.Expanded(
@ -419,8 +432,7 @@ Future<Uint8List> buildTaskPdfBytes(
), ),
], ],
), ),
pw.SizedBox(height: 6), // Names row
// Names row: performedBy can be long but won't move the signature line; center names under lines
pw.Row( pw.Row(
children: [ children: [
pw.Expanded( pw.Expanded(
@ -448,8 +460,7 @@ Future<Uint8List> buildTaskPdfBytes(
), ),
], ],
), ),
pw.SizedBox(height: 6), // Labels row
// Labels row (centered)
pw.Row( pw.Row(
children: [ children: [
pw.Expanded( pw.Expanded(
@ -464,18 +475,10 @@ Future<Uint8List> buildTaskPdfBytes(
), ),
], ],
), ),
pw.SizedBox(height: 12),
pw.Spacer(),
pw.Align(
alignment: pw.Alignment.centerRight,
child: pw.Text(
'MC-IHO-F-05 Rev. 2',
style: pw.TextStyle(fontSize: 9, color: pdf.PdfColors.grey),
),
),
], ],
); ),
}, pw.SizedBox(height: 12),
],
), ),
); );
@ -494,32 +497,137 @@ Future<void> showTaskPdfPreview(
) async { ) async {
await showDialog<void>( await showDialog<void>(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => TaskPdfDialog(
contentPadding: const EdgeInsets.all(8), task: task,
content: SizedBox( ticket: ticket,
width: 700, officeName: officeName,
height: 900, serviceName: serviceName,
child: PdfPreview( logs: logs,
build: (format) => buildTaskPdfBytes( assignments: assignments,
task, profiles: profiles,
ticket,
officeName,
serviceName,
logs,
assignments,
profiles,
format,
),
allowPrinting: true,
allowSharing: true,
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('Close'),
),
],
), ),
); );
} }
class TaskPdfDialog extends StatefulWidget {
final Task task;
final Ticket? ticket;
final String officeName;
final String serviceName;
final List<TaskActivityLog> logs;
final List<TaskAssignment> assignments;
final List<Profile> profiles;
const TaskPdfDialog({
super.key,
required this.task,
this.ticket,
required this.officeName,
required this.serviceName,
required this.logs,
required this.assignments,
required this.profiles,
});
@override
State<TaskPdfDialog> createState() => _TaskPdfDialogState();
}
class _TaskPdfDialogState extends State<TaskPdfDialog> {
Future<Uint8List>? _pdfFuture;
@override
void initState() {
super.initState();
// Initialize once
_pdfFuture = buildTaskPdfBytes(
widget.task,
widget.ticket,
widget.officeName,
widget.serviceName,
widget.logs,
widget.assignments,
widget.profiles,
null,
);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
contentPadding: EdgeInsets.zero,
content: SizedBox(
width: 800,
height: 900,
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
const Expanded(
child: Text(
'IT Job Preview',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
IconButton(
tooltip: 'Print',
icon: const Icon(Icons.print),
onPressed: () async {
final bytes = await _pdfFuture;
if (bytes == null) return;
await Printing.layoutPdf(
onLayout: (_) async => bytes,
name:
'Task - ${widget.task.taskNumber ?? widget.task.id}.pdf',
);
},
),
IconButton(
tooltip: 'Download',
icon: const Icon(Icons.download),
onPressed: () async {
final bytes = await _pdfFuture;
if (bytes == null) return;
await Printing.sharePdf(
bytes: bytes,
filename:
'Task - ${widget.task.taskNumber ?? widget.task.id}.pdf',
);
},
),
IconButton(
tooltip: 'Close',
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
],
),
),
const Divider(height: 1),
Expanded(
child: FutureBuilder<Uint8List>(
future: _pdfFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}
final data = snapshot.data;
if (data == null) return const SizedBox.shrink();
return PdfViewer.data(data, sourceName: 'task.pdf');
},
),
),
],
),
),
);
}
}

View File

@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
pdfium_flutter
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)

View File

@ -325,10 +325,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file name: file
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.4" version: "7.0.1"
file_picker: file_picker:
dependency: "direct main" dependency: "direct main"
description: description:
@ -981,6 +981,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "1.0.4"
pdfium_dart:
dependency: transitive
description:
name: pdfium_dart
sha256: f1683b9070ddc5c9189c6ee008c285791da66328ce1b882c7162d3393f3a4a74
url: "https://pub.dev"
source: hosted
version: "0.1.3"
pdfium_flutter:
dependency: transitive
description:
name: pdfium_flutter
sha256: "0c8b7d5d11d20a1486eade599648e907067568955bd14a1b06de076a968b60a1"
url: "https://pub.dev"
source: hosted
version: "0.1.9"
pdfrx:
dependency: "direct main"
description:
name: pdfrx
sha256: e32e0c786528eec2b3c56b43f59ef1debce3a27c7accd862b95413f949afcfa9
url: "https://pub.dev"
source: hosted
version: "2.2.24"
pdfrx_engine:
dependency: transitive
description:
name: pdfrx_engine
sha256: a8914433d1f6188b903c53d36b9d7dc908bfa89131591a9db22f1a22470d3a48
url: "https://pub.dev"
source: hosted
version: "0.3.9"
permission_handler: permission_handler:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -24,6 +24,7 @@ dependencies:
flutter_quill: ^11.5.0 flutter_quill: ^11.5.0
file_picker: ^10.3.10 file_picker: ^10.3.10
pdf: ^3.11.3 pdf: ^3.11.3
pdfrx: ^2.2.24
printing: ^5.14.2 printing: ^5.14.2
flutter_keyboard_visibility: ^5.4.1 flutter_keyboard_visibility: ^5.4.1
awesome_snackbar_content: ^0.1.8 awesome_snackbar_content: ^0.1.8
@ -43,6 +44,7 @@ flutter:
assets: assets:
- .env - .env
- assets/ - assets/
- assets/fonts/
flutter_launcher_icons: flutter_launcher_icons:
android: true android: true

View File

@ -15,6 +15,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
flutter_local_notifications_windows flutter_local_notifications_windows
pdfium_flutter
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)