Merge branch 'selection_mode' into 'master'
Selection mode See merge request lukas/notes!3
This commit is contained in:
commit
6d3d74ebc7
@ -2,6 +2,7 @@
|
|||||||
package="eu.heili.notes">
|
package="eu.heili.notes">
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
<application
|
<application
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:label="notes"
|
android:label="notes"
|
||||||
|
11
lib/helpers/vibrate.dart
Normal file
11
lib/helpers/vibrate.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,12 @@
|
|||||||
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../context/file_change_notifier.dart';
|
import '../context/file_change_notifier.dart';
|
||||||
|
import '../savesystem/note_file.dart';
|
||||||
import '../widgets/icon_material_button.dart';
|
import '../widgets/icon_material_button.dart';
|
||||||
|
import '../widgets/icon_text_button.dart';
|
||||||
import '../widgets/note_tile.dart';
|
import '../widgets/note_tile.dart';
|
||||||
import '../widgets/wip_toast.dart';
|
import '../widgets/wip_toast.dart';
|
||||||
|
|
||||||
@ -16,6 +19,8 @@ class AllNotesPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _AllNotesPageState extends State<AllNotesPage> {
|
class _AllNotesPageState extends State<AllNotesPage> {
|
||||||
FToast fToast = FToast();
|
FToast fToast = FToast();
|
||||||
|
bool selectionMode = false;
|
||||||
|
List<int> selectionIdx = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -23,14 +28,23 @@ class _AllNotesPageState extends State<AllNotesPage> {
|
|||||||
fToast.init(context);
|
fToast.init(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Widget _buildTopBar() {
|
||||||
Widget build(BuildContext context) {
|
if (selectionMode) {
|
||||||
return Column(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
height: 25 + MediaQuery.of(context).viewPadding.top,
|
width: 20,
|
||||||
|
height: 40,
|
||||||
),
|
),
|
||||||
Row(
|
Text(
|
||||||
|
'${selectionIdx.length} selected',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color.fromRGBO(255, 255, 255, .85), fontSize: 21),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 20,
|
width: 20,
|
||||||
@ -82,7 +96,18 @@ class _AllNotesPageState extends State<AllNotesPage> {
|
|||||||
width: 15,
|
width: 15,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 25 + MediaQuery.of(context).viewPadding.top,
|
||||||
),
|
),
|
||||||
|
_buildTopBar(),
|
||||||
Row(
|
Row(
|
||||||
children: const [
|
children: const [
|
||||||
SizedBox(
|
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() {
|
Widget _buildNoteTiles() {
|
||||||
return Expanded(
|
return Consumer<FileChangeNotifier>(
|
||||||
child: Consumer<FileChangeNotifier>(
|
|
||||||
builder: (BuildContext context, value, Widget? child) {
|
builder: (BuildContext context, value, Widget? child) {
|
||||||
return GridView.builder(
|
return Expanded(
|
||||||
|
child: GridView.builder(
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 5,
|
crossAxisCount: 6, childAspectRatio: 0.7),
|
||||||
),
|
physics: const ScrollPhysics(),
|
||||||
padding: const EdgeInsets.all(2),
|
padding: const EdgeInsets.all(2),
|
||||||
itemBuilder: (BuildContext context, int idx) =>
|
itemBuilder: (BuildContext context, int idx) => NoteTile(
|
||||||
NoteTile(data: value.tiledata[idx]),
|
data: value.tiledata[idx],
|
||||||
itemCount: value.tiledata.length,
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,21 +6,25 @@ import 'package:sqflite/sqflite.dart';
|
|||||||
import 'path.dart';
|
import 'path.dart';
|
||||||
|
|
||||||
class NoteFile {
|
class NoteFile {
|
||||||
late Database _db;
|
Database? _db;
|
||||||
String filename;
|
String filename;
|
||||||
late String _basePath;
|
String? _basePath;
|
||||||
|
|
||||||
String? _newFileName;
|
String? _newFileName;
|
||||||
|
|
||||||
Database db() {
|
Database db() {
|
||||||
return _db;
|
assert(_db != null);
|
||||||
|
return _db!;
|
||||||
}
|
}
|
||||||
|
|
||||||
NoteFile(this.filename);
|
NoteFile(this.filename);
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
_basePath = (await getSavePath()).path;
|
if (_basePath == null) {
|
||||||
final path = _basePath + Platform.pathSeparator + filename;
|
await _initBasePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
final path = _basePath! + Platform.pathSeparator + filename;
|
||||||
_db = await openDatabase(
|
_db = await openDatabase(
|
||||||
path,
|
path,
|
||||||
onCreate: (db, version) async {
|
onCreate: (db, version) async {
|
||||||
@ -44,7 +48,11 @@ class NoteFile {
|
|||||||
|
|
||||||
Future<void> delete() async {
|
Future<void> delete() async {
|
||||||
await close();
|
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) {
|
void rename(String newname) {
|
||||||
@ -53,19 +61,27 @@ class NoteFile {
|
|||||||
|
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
// shrink the db file size
|
// shrink the db file size
|
||||||
if (_db.isOpen) {
|
if (_db != null && _db!.isOpen) {
|
||||||
await _db.execute('VACUUM');
|
await _db!.execute('VACUUM');
|
||||||
await _db.close();
|
await _db!.close();
|
||||||
} else {
|
} else {
|
||||||
debugPrint('db file unexpectedly closed before shrinking');
|
debugPrint('db file unexpectedly closed before shrinking');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_basePath == null) {
|
||||||
|
await _initBasePath();
|
||||||
|
}
|
||||||
|
|
||||||
// perform qued file renaming operations
|
// perform qued file renaming operations
|
||||||
if (_newFileName != null) {
|
if (_newFileName != null) {
|
||||||
File(_basePath + Platform.pathSeparator + filename)
|
File(_basePath! + Platform.pathSeparator + filename)
|
||||||
.rename(_basePath + Platform.pathSeparator + _newFileName!);
|
.rename(_basePath! + Platform.pathSeparator + _newFileName!);
|
||||||
filename = _newFileName!;
|
filename = _newFileName!;
|
||||||
_newFileName = null;
|
_newFileName = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _initBasePath() async {
|
||||||
|
_basePath = (await getSavePath()).path;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
33
lib/widgets/icon_text_button.dart
Normal file
33
lib/widgets/icon_text_button.dart
Normal 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),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,35 +1,74 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../canvas/document_types.dart';
|
import '../canvas/document_types.dart';
|
||||||
import '../canvas/drawing_page.dart';
|
import '../canvas/drawing_page.dart';
|
||||||
|
import '../helpers/vibrate.dart';
|
||||||
|
|
||||||
class NoteTile extends StatelessWidget {
|
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 NoteMetaData data;
|
||||||
|
final bool selectionMode;
|
||||||
|
final bool selected;
|
||||||
|
final void Function(bool) onSelectionChange;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
if (selectionMode) {
|
||||||
|
onSelectionChange(!selected);
|
||||||
|
} else {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => DrawingPage(meta: data),
|
builder: (context) => DrawingPage(meta: data),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: SizedBox(
|
onLongPress: () async {
|
||||||
width: 100,
|
shortVibrate();
|
||||||
|
onSelectionChange(!selected);
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
Expanded(
|
||||||
height: 150,
|
child: Stack(
|
||||||
width: 100,
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1 / sqrt2,
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Colors.white,
|
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(
|
Text(
|
||||||
data.name,
|
data.name,
|
||||||
style: const TextStyle(color: Colors.white),
|
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;
|
||||||
|
}
|
||||||
|
@ -441,6 +441,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
|
vibration:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: vibration
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.7.6"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -39,6 +39,7 @@ dependencies:
|
|||||||
pdf: ^3.8.4
|
pdf: ^3.8.4
|
||||||
permission_handler: ^10.2.0
|
permission_handler: ^10.2.0
|
||||||
fluttertoast: ^8.1.1
|
fluttertoast: ^8.1.1
|
||||||
|
vibration: ^1.7.6
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
Reference in New Issue
Block a user