From 3edc3229e910507870df24f6cba93c2105309d89 Mon Sep 17 00:00:00 2001 From: Julian Mutter Date: Sat, 15 Nov 2025 22:26:33 +0100 Subject: [PATCH] Add functionality to edit sheets with save and restore --- lib/edit_bottom_sheet.dart | 70 ++++++++++++++++++++++++++++++++++++++ lib/home_page.dart | 5 +++ lib/sheet.dart | 43 +++++++++++++++++++++-- lib/storage_helper.dart | 21 ++++++++++++ lib/upload_queue.dart | 66 +++++++++++++++++++++++++++++++++++ 5 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 lib/edit_bottom_sheet.dart create mode 100644 lib/upload_queue.dart diff --git a/lib/edit_bottom_sheet.dart b/lib/edit_bottom_sheet.dart new file mode 100644 index 0000000..7caece9 --- /dev/null +++ b/lib/edit_bottom_sheet.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:sheetless/sheet.dart'; + +typedef SheetEditedCallback = void Function(String newName, String newComposer); + +class EditItemBottomSheet extends StatefulWidget { + final Sheet sheet; + final SheetEditedCallback onSheetEdited; + + const EditItemBottomSheet({ + super.key, + required this.sheet, + required this.onSheetEdited, + }); + + @override + State createState() => _EditItemBottomSheetState(); +} + +class _EditItemBottomSheetState extends State { + late TextEditingController sheetNameController; + late TextEditingController composerNameController; + + @override + void initState() { + sheetNameController = TextEditingController(text: widget.sheet.name); + composerNameController = TextEditingController( + text: widget.sheet.composerName, + ); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.fromLTRB( + 16, + 16, + 16, + MediaQuery.of(context).viewInsets.bottom + 16, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: sheetNameController, + decoration: InputDecoration(labelText: "Sheet"), + ), + TextField( + controller: composerNameController, + decoration: InputDecoration(labelText: "Composer"), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + // TODO: check text fields are not empty + // TODO: save on pressing enter + widget.onSheetEdited( + sheetNameController.text, + composerNameController.text, + ); + Navigator.pop(context); + }, + child: Text("Save"), + ), + ], + ), + ); + } +} diff --git a/lib/home_page.dart b/lib/home_page.dart index 1fe94f4..20d6375 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -64,6 +64,11 @@ class _MyHomePageState extends State with FullScreenListener { log.info("${sheets.length} sheets fetched"); final sheetsSorted = await sortSheetsByAccessTime(sheets); log.info("${sheetsSorted.length} sheets sorted"); + + final changeQueue = await _storageHelper.readChangeQueue(); + changeQueue.applyToSheets(sheetsSorted); + log.info("${changeQueue.length()} changes applied"); + return sheetsSorted; } diff --git a/lib/sheet.dart b/lib/sheet.dart index 23dd01f..16ac268 100644 --- a/lib/sheet.dart +++ b/lib/sheet.dart @@ -2,13 +2,15 @@ import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:sheetless/edit_bottom_sheet.dart'; import 'package:sheetless/storage_helper.dart'; +import 'package:sheetless/upload_queue.dart'; class Sheet { final String uuid; - final String name; - final String composerUuid; - final String composerName; + String name; + String composerUuid; + String composerName; Sheet({ required this.uuid, @@ -92,6 +94,40 @@ class _SheetsWidgetState extends State { _searchController.clear(); } + void _openEditSheet(BuildContext context, Sheet sheet) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (_) => EditItemBottomSheet( + sheet: sheet, + onSheetEdited: (String newName, String newComposer) { + if (newName != sheet.name) { + storageHelper.writeChange( + Change( + type: ChangeType.sheetNameChange, + sheetUuid: sheet.uuid, + value: newName, + ), + ); + } + if (newComposer != sheet.composerName) { + storageHelper.writeChange( + Change( + type: ChangeType.composerNameChange, + sheetUuid: sheet.uuid, + value: newComposer, + ), + ); + } + setState(() { + sheet.name = newName; + sheet.composerName = newComposer; + }); + }, + ), + ); + } + @override Widget build(BuildContext context) { return Column( @@ -134,6 +170,7 @@ class _SheetsWidgetState extends State { widget.sheets.remove(sheet); widget.sheets.insert(0, sheet); }), + onLongPress: () => _openEditSheet(context, sheet), ); }, ), diff --git a/lib/storage_helper.dart b/lib/storage_helper.dart index 1fbb50a..8da4d70 100644 --- a/lib/storage_helper.dart +++ b/lib/storage_helper.dart @@ -1,5 +1,6 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive/hive.dart'; +import 'package:sheetless/upload_queue.dart'; enum SecureStorageKey { url, jwt, email } @@ -18,6 +19,7 @@ class Config { class StorageHelper { final sheetAccessTimesBox = "sheetAccessTimes"; final configBox = "config"; + final changeQueueBox = "changeQueue"; late FlutterSecureStorage secureStorage; @@ -60,4 +62,23 @@ class StorageHelper { final box = await Hive.openBox(sheetAccessTimesBox); await box.put(uuid, datetime.toIso8601String()); } + + Future writeChange(Change change) async { + final box = await Hive.openBox(changeQueueBox); + box.add(change.toMap()); + } + + Future readChangeQueue() async { + final box = await Hive.openBox(changeQueueBox); + ChangeQueue queue = ChangeQueue(); + for (Map map in box.values) { + queue.addChange(Change.fromMap(map)); + } + return queue; + } + + Future deleteOldestChange(Change change) async { + final box = await Hive.openBox(changeQueueBox); + box.deleteAt(0); + } } diff --git a/lib/upload_queue.dart b/lib/upload_queue.dart new file mode 100644 index 0000000..655530e --- /dev/null +++ b/lib/upload_queue.dart @@ -0,0 +1,66 @@ +import 'dart:collection'; + +import 'package:sheetless/sheet.dart'; + +enum ChangeType { + sheetNameChange, + composerNameChange, + addTagChange, + removeTagChange, +} + +class Change { + final ChangeType type; + final String sheetUuid; + final String value; + + Change({required this.type, required this.sheetUuid, required this.value}); + + Map toMap() => { + "type": type.index, + "sheetUuid": sheetUuid, + "value": value, + }; + + factory Change.fromMap(Map map) { + return Change( + type: ChangeType + .values[map["type"]], // TODO: this will create problems if new enums are added + sheetUuid: map["sheetUuid"], + value: map["value"], + ); + } +} + +class ChangeQueue { + // Queue with oldest change first + final Queue queue = Queue(); + + ChangeQueue() {} + + void addChange(Change change) { + queue.addLast(change); + } + + int length() { + return queue.length; + } + + void applyToSheets(List sheets) { + for (Change change in queue) { + var sheet = sheets.where((sheet) => sheet.uuid == change.sheetUuid).first; + switch (change.type) { + case ChangeType.sheetNameChange: + sheet.name = change.value; + case ChangeType.composerNameChange: + sheet.composerName = change.value; + case ChangeType.addTagChange: + // TODO: Handle this case. + throw UnimplementedError(); + case ChangeType.removeTagChange: + // TODO: Handle this case. + throw UnimplementedError(); + } + } + } +}