Selection mode

This commit is contained in:
Lukas Heiligenbrunner 2022-11-17 10:39:06 +00:00
parent 692a1577d1
commit d2355be9e3
8 changed files with 367 additions and 90 deletions

View File

@ -2,6 +2,7 @@
package="eu.heili.notes">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<application
android:requestLegacyExternalStorage="true"
android:label="notes"

11
lib/helpers/vibrate.dart Normal file
View File

@ -0,0 +1,11 @@
import 'package:flutter/foundation.dart';
import 'package:vibration/vibration.dart';
Future<void> shortVibrate() async {
if (defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS) {
if (await Vibration.hasVibrator() ?? false) {
Vibration.vibrate(duration: 50);
}
}
}

View File

@ -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<AllNotesPage> {
FToast fToast = FToast();
bool selectionMode = false;
List<int> selectionIdx = [];
@override
void initState() {
@ -23,6 +28,78 @@ class _AllNotesPageState extends State<AllNotesPage> {
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<AllNotesPage> {
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<AllNotesPage> {
)
],
),
_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<FileChangeNotifier>(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<FileChangeNotifier>(
builder: (BuildContext context, value, Widget? child) {
return GridView.builder(
return Consumer<FileChangeNotifier>(
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,
);
},
),
),
);
},
);
}
}

View File

@ -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<void> 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<void> 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<void> 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<void> _initBasePath() async {
_basePath = (await getSavePath()).path;
}
}

View File

@ -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),
)
],
);
}
}

View File

@ -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;
}

View File

@ -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:

View File

@ -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: