implement pdf export

handle correct storage management for android
This commit is contained in:
Lukas Heiligenbrunner 2022-10-31 18:29:58 +00:00
commit 65c851a416
13 changed files with 207 additions and 31 deletions

View File

@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android { android {
compileSdkVersion flutter.compileSdkVersion compileSdkVersion 33
ndkVersion flutter.ndkVersion ndkVersion flutter.ndkVersion
compileOptions { compileOptions {

View File

@ -1,6 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.heili.notes"> package="eu.heili.notes">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application <application
android:requestLegacyExternalStorage="true"
android:label="notes" android:label="notes"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">

View File

@ -20,9 +20,7 @@ class _AppState extends State<App> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider( return ChangeNotifierProvider(
create: (ctx) { create: (ctx) {
final notifier = FileChangeNotifier(); return FileChangeNotifier()..loadAllNotes();
notifier.loadAllNotes();
return notifier;
}, },
child: Scaffold( child: Scaffold(
floatingActionButton: _fab(), floatingActionButton: _fab(),
@ -48,27 +46,28 @@ class _AppState extends State<App> {
switch (activePage) { switch (activePage) {
case View.all: case View.all:
case View.folders: case View.folders:
return FloatingActionButton( return Consumer<FileChangeNotifier>(
builder: (ctx, notifier, child) => FloatingActionButton(
onPressed: () async { onPressed: () async {
final now = DateTime.now(); final now = DateTime.now();
final name = final name =
'note-${now.year}_${now.month}_${now.day}-${now.hour}_${now.minute}'; 'note-${now.year}_${now.month}_${now.day}-${now.hour}_${now.minute}_${now.second}';
final filename = '$name.dbnote'; final filename = '$name.dbnote';
Navigator.push( Navigator.push(
context, ctx,
MaterialPageRoute( MaterialPageRoute(
builder: (ctx) => DrawingPage( builder: (ctx) => DrawingPage(
filePath: filename, filePath: filename,
name: name, name: name,
), ),
), ),
).then((value) => ).then((v) => notifier.loadAllNotes());
Provider.of<FileChangeNotifier>(context, listen: false)
.loadAllNotes());
}, },
backgroundColor: const Color(0xff3f3f3f), backgroundColor: const Color(0xff3f3f3f),
child: const Icon(Icons.edit_calendar_outlined, color: Colors.orange), child:
const Icon(Icons.edit_calendar_outlined, color: Colors.orange),
),
); );
default: default:
return Container(); return Container();

View File

@ -5,6 +5,7 @@ import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../export/export_pdf.dart';
import '../savesystem/note_file.dart'; import '../savesystem/note_file.dart';
import '../widgets/icon_material_button.dart'; import '../widgets/icon_material_button.dart';
import '../widgets/tool_bar.dart'; import '../widgets/tool_bar.dart';
@ -72,6 +73,7 @@ class _DrawingPageState extends State<DrawingPage> {
color: const Color.fromRGBO(255, 255, 255, .85), color: const Color.fromRGBO(255, 255, 255, .85),
onPressed: () { onPressed: () {
// todo implement // todo implement
exportPDF(controller.strokes, '${widget.name}.pdf');
}, },
), ),
IconMaterialButton( IconMaterialButton(

View File

@ -128,6 +128,7 @@ class PaintController extends ChangeNotifier {
Point p = Point(offset, newWidth); Point p = Point(offset, newWidth);
strokes.last.addPoint(p); strokes.last.addPoint(p);
// todo do a batch commit per stroke
file.addPoint(strokes.last.id, p); file.addPoint(strokes.last.id, p);
break; break;
case Pen.selector: case Pen.selector:

View File

@ -12,6 +12,10 @@ class FileChangeNotifier extends ChangeNotifier {
Future<List<NoteTileData>> loadAllNotes() async { Future<List<NoteTileData>> loadAllNotes() async {
final path = await getSavePath(); final path = await getSavePath();
if (!(await path.exists())) {
await path.create(recursive: true);
}
final dta = await path final dta = await path
.list() .list()
.where((fsentity) => fsentity.path.endsWith('.dbnote')) .where((fsentity) => fsentity.path.endsWith('.dbnote'))

View File

@ -0,0 +1,58 @@
import 'dart:io';
import 'dart:ui';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import '../canvas/document_types.dart';
import '../savesystem/path.dart';
const _a4width = 210 * PdfPageFormat.mm;
const _a4height = 297 * PdfPageFormat.mm;
class _StrokePDFPaint extends pw.Widget {
List<Stroke> strokes;
@override
void layout(pw.Context context, pw.BoxConstraints constraints,
{bool parentUsesSize = false}) {
box =
PdfRect.fromPoints(PdfPoint.zero, const PdfPoint(_a4width, _a4height));
}
@override
void paint(pw.Context context) {
super.paint(context);
for (final stroke in strokes) {
context.canvas.setStrokeColor(PdfColor.fromInt(stroke.color.value));
for (int i = 0; i < stroke.points.length - 1; i++) {
Offset pt1 = stroke.points[i].point * PdfPageFormat.mm;
pt1 = Offset(pt1.dx, _a4width - pt1.dy);
Offset pt2 = stroke.points[i + 1].point * PdfPageFormat.mm;
pt2 = Offset(pt2.dx, _a4width - pt2.dy);
context.canvas.setLineWidth(stroke.points[i].thickness);
context.canvas.drawLine(pt1.dx, pt1.dy, pt2.dx, pt2.dy);
context.canvas.strokePath();
}
}
}
_StrokePDFPaint(this.strokes);
}
void exportPDF(List<Stroke> strokes, String name) async {
final pdf = pw.Document();
const PdfPageFormat a4 = PdfPageFormat(_a4width, _a4height);
pdf.addPage(pw.MultiPage(
pageFormat: a4,
build: (context) => [_StrokePDFPaint(strokes)],
));
final path = await getSavePath();
final file = File('${path.path}${Platform.pathSeparator}$name');
await file.writeAsBytes(await pdf.save(), flush: true);
}

View File

@ -1,15 +1,26 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'app.dart'; import 'app.dart';
void main() { void main() async {
if (defaultTargetPlatform != TargetPlatform.android && if (defaultTargetPlatform != TargetPlatform.android &&
defaultTargetPlatform != TargetPlatform.iOS) { defaultTargetPlatform != TargetPlatform.iOS) {
sqfliteFfiInit(); sqfliteFfiInit();
databaseFactory = databaseFactoryFfi; databaseFactory = databaseFactoryFfi;
} }
WidgetsFlutterBinding.ensureInitialized();
Map<Permission, PermissionStatus> statuses =
await [Permission.manageExternalStorage, Permission.storage].request();
if (statuses.containsValue(PermissionStatus.denied)) {
// todo some error handling
}
runApp(const MaterialApp(home: App())); runApp(const MaterialApp(home: App()));
} }

View File

@ -34,7 +34,9 @@ class _AllNotesPageState extends State<AllNotesPage> {
IconMaterialButton( IconMaterialButton(
icon: const Icon(Icons.picture_as_pdf_outlined), icon: const Icon(Icons.picture_as_pdf_outlined),
color: const Color.fromRGBO(255, 255, 255, .85), color: const Color.fromRGBO(255, 255, 255, .85),
onPressed: () {}, onPressed: () async {
// todo implement pdf import
},
), ),
IconMaterialButton( IconMaterialButton(
icon: const Icon(Icons.search), icon: const Icon(Icons.search),

View File

@ -18,11 +18,18 @@ class NoteFile {
final path = (await getSavePath()).path + Platform.pathSeparator + filepath; final path = (await getSavePath()).path + Platform.pathSeparator + filepath;
_db = await openDatabase( _db = await openDatabase(
path, path,
onCreate: (db, version) { onCreate: (db, version) async {
return db.execute( Batch batch = db.batch();
'CREATE TABLE strokes(id integer primary key autoincrement, color INTEGER, elevation INTEGER);'
'CREATE TABLE points(id integer primary key autoincrement, x INTEGER, y INTEGER, thickness REAL, strokeid INTEGER)', batch.execute('DROP TABLE IF EXISTS strokes;');
); batch.execute('DROP TABLE IF EXISTS points;');
batch.execute(
'CREATE TABLE strokes(id integer primary key autoincrement, color INTEGER, elevation INTEGER)');
batch.execute(
'CREATE TABLE points(id integer primary key autoincrement, x INTEGER, y INTEGER, thickness REAL, strokeid INTEGER)');
await batch.commit();
return;
}, },
// Set the version. This executes the onCreate function and provides a // Set the version. This executes the onCreate function and provides a
// path to perform database upgrades and downgrades. // path to perform database upgrades and downgrades.

View File

@ -7,8 +7,11 @@ Future<Directory> getSavePath() async {
Directory dbpath; Directory dbpath;
if (defaultTargetPlatform == TargetPlatform.android || if (defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS) { defaultTargetPlatform == TargetPlatform.iOS) {
final dir =
(await getExternalStorageDirectory())?.parent.parent.parent.parent ??
(await getApplicationDocumentsDirectory());
dbpath = Directory( dbpath = Directory(
'${(await getApplicationDocumentsDirectory()).path}${Platform.pathSeparator}notes'); '${dir.path}${Platform.pathSeparator}Documents${Platform.pathSeparator}notes');
} else { } else {
dbpath = Directory( dbpath = Directory(
'${(await getApplicationDocumentsDirectory()).path}${Platform.pathSeparator}notes'); '${(await getApplicationDocumentsDirectory()).path}${Platform.pathSeparator}notes');

View File

@ -8,6 +8,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.1" version: "0.2.1"
archive:
dependency: transitive
description:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "3.3.2"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -15,6 +22,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.9.0" version: "2.9.0"
barcode:
dependency: transitive
description:
name: barcode
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.3"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -50,6 +64,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.0.3" version: "0.0.3"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.2"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -109,6 +130,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.0.5" version: "0.0.5"
image:
dependency: transitive
description:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.2"
js: js:
dependency: transitive dependency: transitive
description: description:
@ -221,6 +249,48 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.3" version: "2.1.3"
pdf:
dependency: "direct main"
description:
name: pdf
url: "https://pub.dartlang.org"
source: hosted
version: "3.8.4"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
url: "https://pub.dartlang.org"
source: hosted
version: "10.2.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
url: "https://pub.dartlang.org"
source: hosted
version: "10.2.0"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
url: "https://pub.dartlang.org"
source: hosted
version: "9.0.7"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "3.9.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
@ -256,6 +326,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.4" version: "6.0.4"
qr:
dependency: transitive
description:
name: qr
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -338,6 +415,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.12" version: "0.4.12"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:

View File

@ -36,6 +36,8 @@ dependencies:
sqflite_common_ffi: ^2.2.0+1 sqflite_common_ffi: ^2.2.0+1
path_provider: ^2.0.11 path_provider: ^2.0.11
provider: ^6.0.4 provider: ^6.0.4
pdf: ^3.8.4
permission_handler: ^10.2.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: