Skip to content

Commit

Permalink
add saving to pdf
Browse files Browse the repository at this point in the history
  • Loading branch information
ttulka committed Dec 9, 2021
1 parent f45eb57 commit dd48d7b
Show file tree
Hide file tree
Showing 11 changed files with 135 additions and 54 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
.buildlog/
.history
.svn/
*.dmg

# IntelliJ related
*.iml
Expand Down
6 changes: 4 additions & 2 deletions lib/l10n/app_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"menuCategories": "Kategorien",
"menuAbout": "About",
"aboutTitle": "Über diese Anwendung",
"aboutContent": "Kostenlose und Open-Source-Software für Lehrer, um Schülerbeobachtungen zu schreiben.\n\nDokumente werden nur lokal gespeichert - keine Daten werden jemals Ihren Laptop verlassen!\n\nSiehe den Link oben, um weitere Informationen zu erhalten diese Anwendung.\n\n\n***\n\n\nVersion alpha-0.0.0\n\nErstellt von Tomas Tulka (ttulka.com)\n\nDiese Software wird unter der MIT-Lizenz entwickelt und vertrieben\n(https://opensource.org/licenses/MIT).",
"aboutContent": "Kostenlose und Open-Source-Software für Lehrer, um Schülerbeobachtungen zu schreiben.\n\nDokumente werden nur lokal gespeichert - keine Daten werden jemals Ihren Laptop verlassen!\n\nSiehe den Link oben, um weitere Informationen zu erhalten diese Anwendung.\n\n\n***\n\n\nVersion alpha-0.1.0\n\nErstellt von Tomas Tulka (ttulka.com)\n\nDiese Software wird unter der MIT-Lizenz entwickelt und vertrieben\n(https://opensource.org/licenses/MIT).",
"aboutLink": "https://github.com/ttulka/observations",
"formSave": "Speichern",
"formRequired": "Erforderlich",
Expand All @@ -18,6 +18,7 @@
"editSuccess": "Erfolgreich bearbeitet.",
"removeSuccess": "Erfolgreich entfernt.",
"copySuccess": "Erfolgreich kopiert.",
"saveSuccess": "Erfolgreich gespeichert.",
"removeAlertTitle": "Entfernungsbestätigung",
"removeAlertText": "Möchten Sie das Element wirklich entfernen?",
"removeAlertOk": "Entfernen",
Expand Down Expand Up @@ -49,5 +50,6 @@
"editStudentTitle": "Bearbeiten eines Schülers",
"editStudentHint": "Diesen Schüler bearbeiten",
"removeStudentHint": "Diesen Schüler entfernen",
"printStudentObservationsHint": "Alle Schülerbeobachtungen drucken"
"printStudentObservationsHint": "Schülerbeobachtungen drucken",
"pdfStudentObservationsHint": "Schülerbeobachtungen als PDF spreichern"
}
6 changes: 4 additions & 2 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"menuCategories": "Categories",
"menuAbout": "About",
"aboutTitle": "About this application",
"aboutContent": "Free and open-source software for school teachers to write student observations.\n\nDocuments stored only locally - no piece of data will ever leave your laptop!\n\nCheck out the link above to see more info about this application.\n\n\n***\n\n\nVersion alpha-0.0.0\n\nCreated by Tomas Tulka (ttulka.com)\n\nThis software is developed and distributed under the MIT License\n(https://opensource.org/licenses/MIT).",
"aboutContent": "Free and open-source software for school teachers to write student observations.\n\nDocuments stored only locally - no piece of data will ever leave your laptop!\n\nCheck out the link above to see more info about this application.\n\n\n***\n\n\nVersion alpha-0.1.0\n\nCreated by Tomas Tulka (ttulka.com)\n\nThis software is developed and distributed under the MIT License\n(https://opensource.org/licenses/MIT).",
"aboutLink": "https://github.com/ttulka/observations",
"formSave": "Save",
"formRequired": "Required",
Expand All @@ -18,6 +18,7 @@
"editSuccess": "Edited successfully.",
"removeSuccess": "Removed successfully.",
"copySuccess": "Copied successfully.",
"saveSuccess": "Saved successfully.",
"removeAlertTitle": "Removal Confirmation",
"removeAlertText": "Are you sure you want to remove the item?",
"removeAlertOk": "Remove",
Expand Down Expand Up @@ -49,5 +50,6 @@
"editStudentTitle": "Edit a student",
"editStudentHint": "Edit this student",
"removeStudentHint": "Remove this student",
"printStudentObservationsHint": "Print all student's observations"
"printStudentObservationsHint": "Print student's observations",
"pdfStudentObservationsHint": "Save student's observations as PDF"
}
5 changes: 4 additions & 1 deletion lib/observation/compose.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ class ComposeObservationDialog extends StatelessWidget {
onPressed: () async {
if (currentObservation != null) {
await showPrintDialog(context, [currentObservation!],
classroom: classroom, student: student, headers: await headers, htmlConvert: await htmlConvert);
classroom: classroom,
student: student,
printHeaders: await headers,
htmlConvert: await htmlConvert);
}
},
),
Expand Down
15 changes: 15 additions & 0 deletions lib/student/list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,21 @@ class StudentListItem extends StatelessWidget {
classroom: classroom, student: student, htmlConvert: await printingConvertToHtmlActive);
},
),
IconButton(
icon: const Icon(Icons.picture_as_pdf),
tooltip: AppLocalizations.of(context)!.pdfStudentObservationsHint,
splashRadius: 20,
onPressed: () async {
final observations = await loadObservations(student);
final result = await showSaveDialog(context, observations,
classroom: classroom, student: student, htmlConvert: await printingConvertToHtmlActive);
if (result) {
ScaffoldMessenger.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.saveSuccess)));
}
},
),
IconButton(
icon: const Icon(Icons.edit),
tooltip: AppLocalizations.of(context)!.editStudentHint,
Expand Down
138 changes: 94 additions & 44 deletions lib/utils/printing.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import 'dart:io';
import 'dart:typed_data';

import 'package:file_picker/file_picker.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter/services.dart' show rootBundle;
Expand All @@ -13,71 +17,117 @@ import '../category/domain.dart';
import '../classroom/domain.dart';
import '../student/domain.dart';

// share this flag for both dialogs as they must never appear simultaneously
var _showPrintDialog_justPriting = false;

Future<void> showPrintDialog(BuildContext context, List<Observation> observations,
{required Classroom classroom, required Student student, bool headers = true, bool htmlConvert = true}) async {
{required Classroom classroom, required Student student, bool printHeaders = true, bool htmlConvert = true}) async {
if (_showPrintDialog_justPriting) {
return;
}
_showPrintDialog_justPriting = true;
Logger.debug('Printing starts...');
try {
_showPrintDialog_justPriting = true;

final info = await Printing.info();
if (!info.canPrint) Logger.error('!!! PRINTING NOT SUPPORTED');
if (!info.directPrint) Logger.error('!!! DIRECT PRINTING NOT SUPPORTED');
if (!info.canConvertHtml) Logger.error('!!! CONVERTING NOT SUPPORTED');
if (!info.canPrint) {
await showAlert(context, AppLocalizations.of(context)!.printNotSupported);
return;
}
observations.sort((a, b) => a.category.priority.compareTo(b.category.priority));

final Function composeHeader = (Student student, Classroom classroom, Category category) =>
'${student.familyName}, ${student.givenName} (${classroom.name}): ${category.localizedName(AppLocalizations.of(context)!)}';
final pdf = await _toPdf(observations, student, classroom,
context: context, htmlConvert: htmlConvert, printHeaders: printHeaders, info: info);

await Printing.layoutPdf(onLayout: (PdfPageFormat format) => pdf);
} finally {
_showPrintDialog_justPriting = false;
}
}

Future<bool> showSaveDialog(BuildContext context, List<Observation> observations,
{required Classroom classroom, required Student student, bool printHeaders = true, bool htmlConvert = true}) async {
if (_showPrintDialog_justPriting) {
return false;
}
Logger.debug('Saving starts...');
try {
_showPrintDialog_justPriting = true;

String? outputFile = await FilePicker.platform.saveFile(
//dialogTitle: 'Please select an output file:',
fileName: _toFileNamePdf(student),
);
if (outputFile != null) {
final info = await Printing.info();

final pdf = await _toPdf(observations, student, classroom,
context: context, htmlConvert: htmlConvert, printHeaders: printHeaders, info: info);

if (!info.canConvertHtml || !htmlConvert) {
final List<pw.Widget> pdf = [];
final headerColor = PdfColor.fromHex('#666666');
for (Observation o in observations) {
if (o.content.length > 20) {
await File(outputFile).writeAsBytes(pdf);
return true;
}
} finally {
_showPrintDialog_justPriting = false;
}
return false;
}

String _toFileNamePdf(Student student) =>
'${student.familyName}_${student.givenName}.pdf'.replaceAll(RegExp(r'\s'), '_');

Future<Uint8List> _toPdf(List<Observation> observations, Student student, Classroom classroom,
{required BuildContext context,
required bool htmlConvert,
required bool printHeaders,
required PrintingInfo info}) async {
if (!info.canPrint) Logger.error('!!! PRINTING NOT SUPPORTED');
if (!info.directPrint) Logger.error('!!! DIRECT PRINTING NOT SUPPORTED');
if (!info.canConvertHtml) Logger.error('!!! CONVERTING NOT SUPPORTED');

observations.sort((a, b) => a.category.priority.compareTo(b.category.priority));

if (!info.canConvertHtml || !htmlConvert) {
final List<pw.Widget> pdf = [];
final headerColor = PdfColor.fromHex('#666666');
for (Observation o in observations) {
if (o.content.length > 20) {
if (printHeaders) {
pdf.add(pw.Header(
text: composeHeader(student, classroom, o.category),
text: _composeHeader(student, classroom, o.category, context),
padding: const pw.EdgeInsets.symmetric(vertical: 12, horizontal: 16),
textStyle: pw.TextStyle(color: headerColor, decorationColor: headerColor)));
pdf.addAll(deltaToPdf(o.content));
}
pdf.addAll(deltaToPdf(o.content));
}
final doc = pw.Document(
theme: pw.ThemeData.withFont(
base: pw.Font.ttf(await rootBundle.load("assets/OpenSans-Regular.ttf")),
bold: pw.Font.ttf(await rootBundle.load("assets/OpenSans-Bold.ttf")),
italic: pw.Font.ttf(await rootBundle.load("assets/OpenSans-Italic.ttf")),
boldItalic: pw.Font.ttf(await rootBundle.load("assets/OpenSans-BoldItalic.ttf"))));
doc.addPage(pw.MultiPage(pageFormat: PdfPageFormat.a4, build: (pw.Context context) => pdf));

await Printing.layoutPdf(onLayout: (PdfPageFormat format) => doc.save());
return;
}
final doc = pw.Document(
theme: pw.ThemeData.withFont(
base: pw.Font.ttf(await rootBundle.load("assets/OpenSans-Regular.ttf")),
bold: pw.Font.ttf(await rootBundle.load("assets/OpenSans-Bold.ttf")),
italic: pw.Font.ttf(await rootBundle.load("assets/OpenSans-Italic.ttf")),
boldItalic: pw.Font.ttf(await rootBundle.load("assets/OpenSans-BoldItalic.ttf"))));
doc.addPage(pw.MultiPage(pageFormat: PdfPageFormat.a4, build: (pw.Context context) => pdf));

final html = observations
.where((o) => o.content.length > 20)
.map((o) =>
(headers
? '<p style="color: #666666; padding: 5px; padding-top: 20px"><b>' +
composeHeader(student, classroom, o.category) +
'</b></p><hr>'
: '') +
deltaToHtml(o.content))
.join('<p></p>');

final pdf = await Printing.convertHtml(
format: PdfPageFormat.standard,
html: '<html><body>$html</body></html>',
).timeout(const Duration(seconds: 5));

await Printing.layoutPdf(onLayout: (PdfPageFormat format) => pdf);
} finally {
_showPrintDialog_justPriting = false;
return doc.save();
}

// converting to HTML first, then from HTML to PDF
final html = observations
.where((o) => o.content.length > 20)
.map((o) =>
(printHeaders
? '<p style="color: #666666; padding: 5px; padding-top: 20px"><b>' +
_composeHeader(student, classroom, o.category, context) +
'</b></p><hr>'
: '') +
deltaToHtml(o.content))
.join('<p></p>');

return await Printing.convertHtml(
format: PdfPageFormat.standard,
html: '<html><body>$html</body></html>',
).timeout(const Duration(seconds: 5));
}

String _composeHeader(Student student, Classroom classroom, Category category, BuildContext context) =>
'${student.familyName}, ${student.givenName} (${classroom.name}): ${category.localizedName(AppLocalizations.of(context)!)}';
Binary file added logo.icns
Binary file not shown.
2 changes: 1 addition & 1 deletion macos/Runner/Configs/AppInfo.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// 'flutter create' template.

// The application's name. By default this is also the title of the Flutter window.
PRODUCT_NAME = observations
PRODUCT_NAME = Student Observations

// The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = com.ttulka.observations
Expand Down
7 changes: 7 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.2"
file_picker:
dependency: "direct main"
description:
name: file_picker
url: "https://pub.dartlang.org"
source: hosted
version: "4.2.7"
flutter:
dependency: "direct main"
description: flutter
Expand Down
5 changes: 3 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: observations
description: Student observations
description: Student Observations

# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
Expand All @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1
version: 0.0.0+2

environment:
sdk: ">=2.14.0 <3.0.0"
Expand All @@ -42,6 +42,7 @@ dependencies:
path_provider: ^2.0.7
pdf: ^3.6.3
printing: ^5.6.3
file_picker: ^4.2.7
window_size:
git:
url: git://github.com/google/flutter-desktop-embedding.git
Expand Down
4 changes: 2 additions & 2 deletions windows/runner/Runner.rc
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,12 @@ BEGIN
BLOCK "040904e4"
BEGIN
VALUE "CompanyName", "Tomas Tulka (ttulka.com)" "\0"
VALUE "FileDescription", "Student observations" "\0"
VALUE "FileDescription", "Student Observations" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "observations" "\0"
VALUE "LegalCopyright", "MIT License" "\0"
VALUE "OriginalFilename", "observations.exe" "\0"
VALUE "ProductName", "observations" "\0"
VALUE "ProductName", "Student Observations" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"
END
END
Expand Down

0 comments on commit dd48d7b

Please sign in to comment.