From d2355be9e3b2ba835214538e060001e846d2c467 Mon Sep 17 00:00:00 2001 From: Lukas Heiligenbrunner Date: Thu, 17 Nov 2022 10:39:06 +0000 Subject: [PATCH] Selection mode --- android/app/src/main/AndroidManifest.xml | 1 + lib/helpers/vibrate.dart | 11 + lib/pages/all_notes_page.dart | 271 +++++++++++++++++------ lib/savesystem/note_file.dart | 38 +++- lib/widgets/icon_text_button.dart | 33 +++ lib/widgets/note_tile.dart | 95 ++++++-- pubspec.lock | 7 + pubspec.yaml | 1 + 8 files changed, 367 insertions(+), 90 deletions(-) create mode 100644 lib/helpers/vibrate.dart create mode 100644 lib/widgets/icon_text_button.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index eed3fcc..b2db80c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ package="eu.heili.notes"> + shortVibrate() async { + if (defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS) { + if (await Vibration.hasVibrator() ?? false) { + Vibration.vibrate(duration: 50); + } + } +} diff --git a/lib/pages/all_notes_page.dart b/lib/pages/all_notes_page.dart index 079c0e7..b452fe4 100644 --- a/lib/pages/all_notes_page.dart +++ b/lib/pages/all_notes_page.dart @@ -1,9 +1,12 @@ +import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:provider/provider.dart'; import '../context/file_change_notifier.dart'; +import '../savesystem/note_file.dart'; import '../widgets/icon_material_button.dart'; +import '../widgets/icon_text_button.dart'; import '../widgets/note_tile.dart'; import '../widgets/wip_toast.dart'; @@ -16,6 +19,8 @@ class AllNotesPage extends StatefulWidget { class _AllNotesPageState extends State { FToast fToast = FToast(); + bool selectionMode = false; + List selectionIdx = []; @override void initState() { @@ -23,6 +28,78 @@ class _AllNotesPageState extends State { fToast.init(context); } + Widget _buildTopBar() { + if (selectionMode) { + return Row( + children: [ + const SizedBox( + width: 20, + height: 40, + ), + Text( + '${selectionIdx.length} selected', + style: const TextStyle( + color: Color.fromRGBO(255, 255, 255, .85), fontSize: 21), + ) + ], + ); + } else { + return Row( + children: [ + const SizedBox( + width: 20, + ), + const Text( + 'All notes', + style: TextStyle( + color: Color.fromRGBO(255, 255, 255, .85), fontSize: 21), + ), + Expanded(child: Container()), + IconMaterialButton( + icon: const Icon(Icons.picture_as_pdf_outlined), + color: const Color.fromRGBO(255, 255, 255, .85), + onPressed: () async { + // todo implement pdf import + fToast.showToast( + child: const WIPToast(), + gravity: ToastGravity.BOTTOM, + toastDuration: const Duration(seconds: 2), + ); + }, + iconSize: 22, + ), + IconMaterialButton( + icon: const Icon(Icons.search), + color: const Color.fromRGBO(255, 255, 255, .85), + onPressed: () { + fToast.showToast( + child: const WIPToast(), + gravity: ToastGravity.BOTTOM, + toastDuration: const Duration(seconds: 2), + ); + }, + iconSize: 22, + ), + IconMaterialButton( + icon: const Icon(Icons.more_vert), + color: const Color.fromRGBO(255, 255, 255, .85), + onPressed: () { + fToast.showToast( + child: const WIPToast(), + gravity: ToastGravity.BOTTOM, + toastDuration: const Duration(seconds: 2), + ); + }, + iconSize: 22, + ), + const SizedBox( + width: 15, + ) + ], + ); + } + } + @override Widget build(BuildContext context) { return Column( @@ -30,59 +107,7 @@ class _AllNotesPageState extends State { SizedBox( height: 25 + MediaQuery.of(context).viewPadding.top, ), - Row( - children: [ - const SizedBox( - width: 20, - ), - const Text( - 'All notes', - style: TextStyle( - color: Color.fromRGBO(255, 255, 255, .85), fontSize: 21), - ), - Expanded(child: Container()), - IconMaterialButton( - icon: const Icon(Icons.picture_as_pdf_outlined), - color: const Color.fromRGBO(255, 255, 255, .85), - onPressed: () async { - // todo implement pdf import - fToast.showToast( - child: const WIPToast(), - gravity: ToastGravity.BOTTOM, - toastDuration: const Duration(seconds: 2), - ); - }, - iconSize: 22, - ), - IconMaterialButton( - icon: const Icon(Icons.search), - color: const Color.fromRGBO(255, 255, 255, .85), - onPressed: () { - fToast.showToast( - child: const WIPToast(), - gravity: ToastGravity.BOTTOM, - toastDuration: const Duration(seconds: 2), - ); - }, - iconSize: 22, - ), - IconMaterialButton( - icon: const Icon(Icons.more_vert), - color: const Color.fromRGBO(255, 255, 255, .85), - onPressed: () { - fToast.showToast( - child: const WIPToast(), - gravity: ToastGravity.BOTTOM, - toastDuration: const Duration(seconds: 2), - ); - }, - iconSize: 22, - ), - const SizedBox( - width: 15, - ) - ], - ), + _buildTopBar(), Row( children: const [ SizedBox( @@ -90,26 +115,142 @@ class _AllNotesPageState extends State { ) ], ), - _buildNoteTiles() + _buildNoteTiles(), + if (selectionMode) + SizedBox( + height: 70, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + IconTextButton( + icon: const Icon(Icons.drive_file_move_outline), + color: const Color.fromRGBO(255, 255, 255, .85), + onPressed: () { + fToast.showToast( + child: const WIPToast(), + gravity: ToastGravity.BOTTOM, + toastDuration: const Duration(seconds: 2), + ); + }, + iconSize: 24, + text: 'Move', + ), + IconTextButton( + icon: const Icon(Icons.lock_outline), + color: const Color.fromRGBO(255, 255, 255, .85), + onPressed: () { + fToast.showToast( + child: const WIPToast(), + gravity: ToastGravity.BOTTOM, + toastDuration: const Duration(seconds: 2), + ); + }, + iconSize: 24, + text: 'Lock', + ), + IconTextButton( + icon: const Icon(Icons.share), + color: const Color.fromRGBO(255, 255, 255, .85), + onPressed: () { + fToast.showToast( + child: const WIPToast(), + gravity: ToastGravity.BOTTOM, + toastDuration: const Duration(seconds: 2), + ); + }, + iconSize: 24, + text: 'Share', + ), + IconTextButton( + icon: const Icon(FluentIcons.delete_20_filled), + color: const Color.fromRGBO(255, 255, 255, .85), + onPressed: () async { + // todo add popup to ask if really delete + + final filechangenotfier = + Provider.of(context, listen: false); + for (final s in selectionIdx) { + final dta = filechangenotfier.tiledata[s]; + // todo maybe optimize a bit and create not always new notefile instance + await NoteFile(dta.relativePath).delete(); + } + + await filechangenotfier.loadAllNotes(); + + setState(() { + selectionIdx = []; + selectionMode = false; + }); + }, + iconSize: 24, + text: 'Delete', + ), + IconTextButton( + icon: const Icon(Icons.more_vert), + color: const Color.fromRGBO(255, 255, 255, .85), + onPressed: () { + fToast.showToast( + child: const WIPToast(), + gravity: ToastGravity.BOTTOM, + toastDuration: const Duration(seconds: 2), + ); + }, + iconSize: 24, + text: 'More', + ), + ], + ), + ) ], ); } Widget _buildNoteTiles() { - return Expanded( - child: Consumer( - builder: (BuildContext context, value, Widget? child) { - return GridView.builder( + return Consumer( + builder: (BuildContext context, value, Widget? child) { + return Expanded( + child: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 5, - ), + crossAxisCount: 6, childAspectRatio: 0.7), + physics: const ScrollPhysics(), padding: const EdgeInsets.all(2), - itemBuilder: (BuildContext context, int idx) => - NoteTile(data: value.tiledata[idx]), + itemBuilder: (BuildContext context, int idx) => NoteTile( + data: value.tiledata[idx], + selectionMode: selectionMode, + selected: selectionIdx.contains(idx), + onSelectionChange: (selection) { + if (selection) { + if (!selectionMode) { + setState(() { + selectionMode = true; + }); + } + if (!selectionIdx.contains(idx)) { + final sel = selectionIdx; + sel.add(idx); + setState(() { + selectionIdx = sel; + }); + } + } else { + final sel = selectionIdx; + sel.remove(idx); + if (sel.isEmpty) { + setState(() { + selectionMode = false; + }); + } + setState(() { + selectionIdx = sel; + }); + } + }, + ), itemCount: value.tiledata.length, - ); - }, - ), + ), + ); + }, ); } } diff --git a/lib/savesystem/note_file.dart b/lib/savesystem/note_file.dart index 0d193d0..5bad148 100644 --- a/lib/savesystem/note_file.dart +++ b/lib/savesystem/note_file.dart @@ -6,21 +6,25 @@ import 'package:sqflite/sqflite.dart'; import 'path.dart'; class NoteFile { - late Database _db; + Database? _db; String filename; - late String _basePath; + String? _basePath; String? _newFileName; Database db() { - return _db; + assert(_db != null); + return _db!; } NoteFile(this.filename); Future init() async { - _basePath = (await getSavePath()).path; - final path = _basePath + Platform.pathSeparator + filename; + if (_basePath == null) { + await _initBasePath(); + } + + final path = _basePath! + Platform.pathSeparator + filename; _db = await openDatabase( path, onCreate: (db, version) async { @@ -44,7 +48,11 @@ class NoteFile { Future delete() async { await close(); - await File(_basePath + Platform.pathSeparator + filename).delete(); + + if (_basePath == null) { + await _initBasePath(); + } + await File(_basePath! + Platform.pathSeparator + filename).delete(); } void rename(String newname) { @@ -53,19 +61,27 @@ class NoteFile { Future close() async { // shrink the db file size - if (_db.isOpen) { - await _db.execute('VACUUM'); - await _db.close(); + if (_db != null && _db!.isOpen) { + await _db!.execute('VACUUM'); + await _db!.close(); } else { debugPrint('db file unexpectedly closed before shrinking'); } + if (_basePath == null) { + await _initBasePath(); + } + // perform qued file renaming operations if (_newFileName != null) { - File(_basePath + Platform.pathSeparator + filename) - .rename(_basePath + Platform.pathSeparator + _newFileName!); + File(_basePath! + Platform.pathSeparator + filename) + .rename(_basePath! + Platform.pathSeparator + _newFileName!); filename = _newFileName!; _newFileName = null; } } + + Future _initBasePath() async { + _basePath = (await getSavePath()).path; + } } diff --git a/lib/widgets/icon_text_button.dart b/lib/widgets/icon_text_button.dart new file mode 100644 index 0000000..321e491 --- /dev/null +++ b/lib/widgets/icon_text_button.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +import 'icon_material_button.dart'; + +class IconTextButton extends StatelessWidget { + const IconTextButton( + {Key? key, + required this.icon, + required this.color, + required this.onPressed, + required this.text, + this.iconSize}) + : super(key: key); + final Widget icon; + final Color color; + final void Function() onPressed; + final String text; + final double? iconSize; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + IconMaterialButton( + icon: icon, color: color, onPressed: onPressed, iconSize: iconSize), + Text( + text, + style: TextStyle(color: color), + ) + ], + ); + } +} diff --git a/lib/widgets/note_tile.dart b/lib/widgets/note_tile.dart index f49b651..ac781c9 100644 --- a/lib/widgets/note_tile.dart +++ b/lib/widgets/note_tile.dart @@ -1,35 +1,74 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import '../canvas/document_types.dart'; import '../canvas/drawing_page.dart'; +import '../helpers/vibrate.dart'; class NoteTile extends StatelessWidget { - const NoteTile({Key? key, required this.data}) : super(key: key); + const NoteTile( + {Key? key, + required this.data, + required this.selectionMode, + required this.selected, + required this.onSelectionChange}) + : super(key: key); final NoteMetaData data; + final bool selectionMode; + final bool selected; + final void Function(bool) onSelectionChange; @override Widget build(BuildContext context) { return GestureDetector( onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => DrawingPage(meta: data), - ), - ); + if (selectionMode) { + onSelectionChange(!selected); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => DrawingPage(meta: data), + ), + ); + } }, - child: SizedBox( - width: 100, + onLongPress: () async { + shortVibrate(); + onSelectionChange(!selected); + }, + child: Padding( + padding: const EdgeInsets.all(20), child: Column( children: [ - SizedBox( - height: 150, - width: 100, - child: Container( - color: Colors.white, + Expanded( + child: Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: AspectRatio( + aspectRatio: 1 / sqrt2, + child: Container( + color: Colors.white, + ), + ), + ), + if (selectionMode) + Padding( + padding: const EdgeInsets.only(left: 10, top: 10), + child: CustomPaint( + size: const Size(25, 25), + painter: _CirclePainter(selected), + ), + ) + ], ), ), + const SizedBox( + height: 5, + ), Text( data.name, style: const TextStyle(color: Colors.white), @@ -43,3 +82,31 @@ class NoteTile extends StatelessWidget { ); } } + +class _CirclePainter extends CustomPainter { + final bool selected; + + final _paint = Paint()..strokeWidth = .7; + + _CirclePainter(this.selected) { + if (selected) { + _paint.color = Colors.orange; + _paint.style = PaintingStyle.fill; + } else { + _paint.color = Colors.black; + _paint.style = PaintingStyle.stroke; + } + } + + @override + void paint(Canvas canvas, Size size) { + canvas.drawOval( + Rect.fromLTWH(0, 0, size.width, size.height), + _paint, + ); + } + + @override + bool shouldRepaint(_CirclePainter oldDelegate) => + oldDelegate.selected != selected; +} diff --git a/pubspec.lock b/pubspec.lock index 55c51a3..173518d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -441,6 +441,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + vibration: + dependency: "direct main" + description: + name: vibration + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.6" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 14f04a3..65dc836 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: pdf: ^3.8.4 permission_handler: ^10.2.0 fluttertoast: ^8.1.1 + vibration: ^1.7.6 dev_dependencies: flutter_test: