diff --git a/lib/model/collection.dart b/lib/model/collection.dart index c3dac35..a6b6371 100644 --- a/lib/model/collection.dart +++ b/lib/model/collection.dart @@ -7,12 +7,28 @@ class Collection { createdAt: json['createdAt'] as int, ); - String name; int createdAt; // used as Id with millisecondsSinceEpoch + String name; + + @override + bool operator ==(Object other) { + if (other is Collection) { + return hashCode == other.hashCode; + } else { + return false; + } + } + + @override + int get hashCode => id.hashCode; int get id => createdAt; DateTime get createdDate => DateTime.fromMillisecondsSinceEpoch(createdAt); Map toJson() => {'name': name, 'createdAt': createdAt}; + + Collection copyWith({String? name}) { + return Collection(name: name ?? this.name, createdAt: createdAt); + } } diff --git a/lib/pages/collection_page.dart b/lib/pages/collection_page.dart index ef151ed..5273efa 100644 --- a/lib/pages/collection_page.dart +++ b/lib/pages/collection_page.dart @@ -21,6 +21,14 @@ class CollectionPage extends StatefulWidget { class _CollectionPageState extends State { MapsLinkMetadata? selectedMapsLink; + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (selectedMapsLink != null) onAddButtonPressed(); + }); + } + void onAddButtonPressed() => showDialog( context: context, builder: (context) => CreateBookmarkDialog( @@ -43,21 +51,13 @@ class _CollectionPageState extends State { ), ); - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (selectedMapsLink != null) onAddButtonPressed(); - }); - } - void onBookmarkSaved(Bookmark bookmark) { Storage.addOrUpdateBookmark(bookmark); setState(() {}); context.read().removeCurrentMapsLink(); } - Widget bookmarkListBuilder(BuildContext context, Bookmark bookmark) { + Widget bookmarksListItemBuilder(BuildContext context, Bookmark bookmark) { return ListTile( title: Text(bookmark.name), onTap: () => launchUrlFromString(bookmark.link), @@ -95,7 +95,7 @@ class _CollectionPageState extends State { ), body: ListView.builder( itemBuilder: (context, index) => - bookmarkListBuilder(context, bookmarks.elementAt(index)), + bookmarksListItemBuilder(context, bookmarks.elementAt(index)), itemCount: bookmarks.length, ), floatingActionButton: FloatingActionButton( diff --git a/lib/pages/collections_list_page.dart b/lib/pages/collections_list_page.dart index 624f20c..e4a6782 100644 --- a/lib/pages/collections_list_page.dart +++ b/lib/pages/collections_list_page.dart @@ -10,6 +10,7 @@ import 'collection_page.dart'; class CollectionsListPage extends StatefulWidget { const CollectionsListPage({super.key}); + static const String routeName = '/collections'; @override @@ -17,11 +18,63 @@ class CollectionsListPage extends StatefulWidget { } class _CollectionsListPageState extends State { - final collections = Storage.loadCollections(); bool addingNewBookmark = false; + Widget bottomSheetBuilder(BuildContext context) { + final titleTextFieldController = TextEditingController( + text: context + .read() + .currentMapsLinkMetadata! + .placeName, + ); + return SizedBox( + height: 200, + child: TextField(controller: titleTextFieldController), + ); + } + + void onAddButtonPressed() => showDialog( + context: context, + builder: (context) => + CreateBookmarkCollectionDialog(onSavePressed: onCollectionSaved), + ); + + void onCollectionSaved(Collection collection) { + Storage.addOrUpdateCollection(collection); + setState(() {}); + } + + Widget collectionsListItemBuilder( + BuildContext context, + Collection collection, + ) { + return ListTile( + title: Text(collection.name), + onTap: () => navigateToCollection(collection.id), + onLongPress: () => onEditCollection(collection), + ); + } + + void navigateToCollection(int collectionId) { + BookmarksProvider.selectedCollectionId = collectionId; + Navigator.pushNamed(context, CollectionPage.routeName); + } + + void onEditCollection(Collection selectedCollection) => showDialog( + context: context, + builder: (context) => CreateBookmarkCollectionDialog( + selectedCollection: selectedCollection, + onSavePressed: onCollectionSaved, + onDeletePressed: () { + Storage.deleteCollection(selectedCollection); + setState(() {}); + }, + ), + ); + @override Widget build(BuildContext context) { + final collections = Storage.loadCollections(); final provider = context.watch(); addingNewBookmark = provider.currentMapsLinkMetadata != null; return Scaffold( @@ -40,58 +93,10 @@ class _CollectionsListPageState extends State { child: Icon(Icons.add), ), body: ListView.builder( - itemBuilder: itemBuilder, + itemBuilder: (context, index) => + collectionsListItemBuilder(context, collections.elementAt(index)), itemCount: collections.length, ), - // bottomSheet: provider.currentMapsLinkMetadata == null - // ? null - // : BottomSheet(onClosing: () {}, builder: bottomSheetBuilder), ); } - - Widget bottomSheetBuilder(BuildContext context) { - final titleTextFieldController = TextEditingController( - text: context - .read() - .currentMapsLinkMetadata! - .placeName, - ); - return SizedBox( - height: 200, - child: TextField(controller: titleTextFieldController), - ); - } - - void onAddButtonPressed() => - showDialog( - context: context, - builder: (context) => - CreateBookmarkCollectionDialog(onSavePressed: onCollectionSaved), - ).whenComplete(() { - if (addingNewBookmark) navigateToCollection(collections.last.id); - }); - - void onCollectionSaved(String name) { - collections.add(Collection(name: name)); - Storage.saveCollections(collections); - setState(() {}); - } - - Widget itemBuilder(BuildContext context, int index) { - final collection = collections.elementAt(index); - return ListTile( - title: Text(collection.name), - onTap: () => navigateToCollection(collection.id), - ); - } - - void navigateToCollection(int collectionId) { - BookmarksProvider.selectedCollectionId = collectionId; - Navigator.pushNamed(context, CollectionPage.routeName); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - } } diff --git a/lib/service/storage.dart b/lib/service/storage.dart index fe476dc..a4b57f2 100644 --- a/lib/service/storage.dart +++ b/lib/service/storage.dart @@ -6,10 +6,10 @@ import '../model/bookmark.dart'; import '../model/collection.dart'; class Storage { - static const String _collectionsKey = 'collections'; static const String _bookmarksKey = 'bookmarks'; - static const String _statsKey = 'stats'; + static const String _collectionsKey = 'collections'; static SharedPreferencesWithCache? _prefsWithCache; + static const String _statsKey = 'stats'; static Future initialize() async { _prefsWithCache = await SharedPreferencesWithCache.create( @@ -19,15 +19,6 @@ class Storage { ); } - static SharedPreferencesWithCache get _prefs { - if (_prefsWithCache == null) { - throw StateError( - 'BookmarkStorage not initialized. Call initialize() first.', - ); - } - return _prefsWithCache!; - } - static List loadCollections() { final jsonString = _prefs.getString(_collectionsKey) ?? '[]'; final jsonList = jsonDecode(jsonString) as List; @@ -36,12 +27,7 @@ class Storage { .toList(); } - static Future saveCollections(List collections) async { - final jsonList = collections.map((c) => c.toJson()).toList(); - await _prefs.setString(_collectionsKey, jsonEncode(jsonList)); - } - - static List loadAllBookmarks() { + static List loadBookmarks() { final jsonString = _prefs.getString(_bookmarksKey) ?? '[]'; final jsonList = jsonDecode(jsonString) as List; return jsonList @@ -49,42 +35,29 @@ class Storage { .toList(); } - static Future saveAllBookmarks(List bookmarks) async { + static Future saveCollections(List collections) async { + final jsonList = collections.map((c) => c.toJson()).toList(); + await _prefs.setString(_collectionsKey, jsonEncode(jsonList)); + } + + static Future saveBookmarks(List bookmarks) async { final jsonList = bookmarks.map((b) => b.toJson()).toList(); await _prefs.setString(_bookmarksKey, jsonEncode(jsonList)); } static List loadBookmarksForCollection(int collectionId) { - final allBookmarks = loadAllBookmarks(); + final allBookmarks = loadBookmarks(); return allBookmarks.where((b) => b.collectionId == collectionId).toList(); } static Future addBookmark(Bookmark bookmark) async { - final bookmarks = loadAllBookmarks(); + final bookmarks = loadBookmarks(); bookmarks.add(bookmark); - await saveAllBookmarks(bookmarks); - } - - static Future deleteBookmark(Bookmark bookmark) async { - final bookmarks = loadAllBookmarks(); - bookmarks.remove(bookmark); - await saveAllBookmarks(bookmarks); - } - - static Future deleteBookmarkById(int bookmarkId) async { - final bookmarks = loadAllBookmarks(); - bookmarks.removeWhere((b) => b.id == bookmarkId); - await saveAllBookmarks(bookmarks); - } - - static Future deleteBookmarksForCollection(int collectionId) async { - final bookmarks = loadAllBookmarks(); - bookmarks.removeWhere((b) => b.collectionId == collectionId); - await saveAllBookmarks(bookmarks); + await saveBookmarks(bookmarks); } static Future addOrUpdateBookmark(Bookmark bookmark) async { - final bookmarks = loadAllBookmarks(); + final bookmarks = loadBookmarks(); final index = bookmarks.indexWhere((b) => b.id == bookmark.id); if (index == -1) { @@ -92,7 +65,19 @@ class Storage { } else if (index >= 0) { bookmarks[index] = bookmark; } - await saveAllBookmarks(bookmarks); + await saveBookmarks(bookmarks); + } + + static Future addOrUpdateCollection(Collection collection) async { + final collections = loadCollections(); + final index = collections.indexWhere((b) => b.id == collection.id); + + if (index == -1) { + collections.add(collection); + } else if (index >= 0) { + collections[index] = collection; + } + await saveCollections(collections); } static Future updateBookmarkById( @@ -101,7 +86,7 @@ class Storage { String? description, String? link, }) async { - final bookmarks = loadAllBookmarks(); + final bookmarks = loadBookmarks(); final index = bookmarks.indexWhere((b) => b.id == bookmarkId); if (index == -1) return; @@ -110,7 +95,34 @@ class Storage { if (description != null) bookmarks[index].description = description; if (link != null) bookmarks[index].link = link; - await saveAllBookmarks(bookmarks); + await saveBookmarks(bookmarks); + } + + static Future deleteBookmark(Bookmark bookmark) async { + final bookmarks = loadBookmarks(); + bookmarks.remove(bookmark); + await saveBookmarks(bookmarks); + } + + static Future deleteBookmarkById(int bookmarkId) async { + final bookmarks = loadBookmarks(); + bookmarks.removeWhere((b) => b.id == bookmarkId); + await saveBookmarks(bookmarks); + } + + static Future deleteBookmarksForCollection(int collectionId) async { + final bookmarks = loadBookmarks(); + bookmarks.removeWhere((b) => b.collectionId == collectionId); + await saveBookmarks(bookmarks); + } + + static Future deleteCollection(Collection collection) async { + final collections = loadCollections(); + final bookmarks = loadBookmarks(); + bookmarks.removeWhere((bookmark) => bookmark.collectionId == collection.id); + collections.remove(collection); + await saveBookmarks(bookmarks); + await saveCollections(collections); } static Map getStats() { @@ -124,7 +136,7 @@ class Storage { static Future updateStats() async { final collections = loadCollections(); - final bookmarks = loadAllBookmarks(); + final bookmarks = loadBookmarks(); final stats = { 'totalCollections': collections.length, @@ -134,4 +146,13 @@ class Storage { await _prefs.setString(_statsKey, jsonEncode(stats)); } + + static SharedPreferencesWithCache get _prefs { + if (_prefsWithCache == null) { + throw StateError( + 'BookmarkStorage not initialized. Call initialize() first.', + ); + } + return _prefsWithCache!; + } } diff --git a/lib/widgets/create_bookmark_collection_dialog.dart b/lib/widgets/create_bookmark_collection_dialog.dart index 28ae232..cf1cb33 100644 --- a/lib/widgets/create_bookmark_collection_dialog.dart +++ b/lib/widgets/create_bookmark_collection_dialog.dart @@ -1,18 +1,35 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import '../model/collection.dart'; +import 'edit_dialog_widgets/edit_dialog_actions.dart' show EditDialogActions; +import 'edit_dialog_widgets/edit_dialog_title.dart'; + class CreateBookmarkCollectionDialog extends StatelessWidget { const CreateBookmarkCollectionDialog({ super.key, required this.onSavePressed, + this.onDeletePressed, + this.selectedCollection, }); - final void Function(String name) onSavePressed; + + final void Function()? onDeletePressed; + final void Function(Collection collection) onSavePressed; + final Collection? selectedCollection; @override Widget build(BuildContext context) { final nameController = TextEditingController(); + + if (selectedCollection != null) { + nameController.text = selectedCollection!.name; + } + return AlertDialog( - title: Text('Create Collection'), + title: EditDialogTitle( + dialogType: DialogType.collection, + onDeletePressed: onDeletePressed, + ), content: TextField( controller: nameController, autofocus: true, @@ -23,17 +40,20 @@ class CreateBookmarkCollectionDialog extends StatelessWidget { FilteringTextInputFormatter.deny(RegExp(r'\s\s+')), ], decoration: InputDecoration( + // TODO: Localize labelText: 'Collection Name', border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), ), ), actions: [ - FloatingActionButton( - onPressed: () { - onSavePressed(nameController.text); + EditDialogActions( + onSavePressed: () { + final bookmark = + selectedCollection?.copyWith(name: nameController.text) ?? + Collection(name: nameController.text); + onSavePressed(bookmark); Navigator.of(context).pop(); }, - child: Icon(Icons.save), ), ], ); diff --git a/lib/widgets/create_bookmark_dialog.dart b/lib/widgets/create_bookmark_dialog.dart index de3fb8b..7a36973 100644 --- a/lib/widgets/create_bookmark_dialog.dart +++ b/lib/widgets/create_bookmark_dialog.dart @@ -3,6 +3,8 @@ import 'package:flutter/services.dart'; import '../model/bookmark.dart'; import '../model/maps_link_metadata.dart'; +import 'edit_dialog_widgets/edit_dialog_actions.dart'; +import 'edit_dialog_widgets/edit_dialog_title.dart'; class CreateBookmarkDialog extends StatelessWidget { const CreateBookmarkDialog({ @@ -13,6 +15,7 @@ class CreateBookmarkDialog extends StatelessWidget { this.selectedBookmark, this.selectedMapsLink, }); + final void Function(Bookmark bookmark)? onSavePressed; final void Function()? onDeletePressed; final int collectionId; @@ -36,25 +39,10 @@ class CreateBookmarkDialog extends StatelessWidget { } return AlertDialog( - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Create Bookmark'), - - if (selectedBookmark != null) - TextButton( - onPressed: () { - onDeletePressed?.call(); - Navigator.of(context).pop(); - }, - child: Text( - 'Delete', - style: TextStyle(color: Theme.of(context).colorScheme.error), - ), - ), - ], + title: EditDialogTitle( + dialogType: DialogType.bookmark, + onDeletePressed: onDeletePressed, ), - content: SingleChildScrollView( child: Column( children: [ @@ -115,33 +103,23 @@ class CreateBookmarkDialog extends StatelessWidget { ), ), actions: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text('Cancel'), - ), - FloatingActionButton( - onPressed: () { - final bookmark = - selectedBookmark?.copyWith( - name: nameController.text, - link: linkController.text, - description: descriptionController.text, - ) ?? - Bookmark( - collectionId: collectionId, - name: nameController.text, - link: linkController.text, - description: descriptionController.text, - ); - onSavePressed?.call(bookmark); - Navigator.of(context).pop(); - }, - child: Icon(Icons.save), - ), - ], + EditDialogActions( + onSavePressed: () { + final bookmark = + selectedBookmark?.copyWith( + name: nameController.text, + link: linkController.text, + description: descriptionController.text, + ) ?? + Bookmark( + collectionId: collectionId, + name: nameController.text, + link: linkController.text, + description: descriptionController.text, + ); + onSavePressed?.call(bookmark); + Navigator.of(context).pop(); + }, ), ], ); diff --git a/lib/widgets/edit_dialog_widgets/edit_dialog_actions.dart b/lib/widgets/edit_dialog_widgets/edit_dialog_actions.dart new file mode 100644 index 0000000..4ff1ecc --- /dev/null +++ b/lib/widgets/edit_dialog_widgets/edit_dialog_actions.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart' + show TextButton, FloatingActionButton, Icons; +import 'package:flutter/widgets.dart'; + +class EditDialogActions extends StatelessWidget { + const EditDialogActions({super.key, required this.onSavePressed}); + final VoidCallback onSavePressed; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('Cancel'), + ), + FloatingActionButton(onPressed: onSavePressed, child: Icon(Icons.save)), + ], + ); + } +} diff --git a/lib/widgets/edit_dialog_widgets/edit_dialog_title.dart b/lib/widgets/edit_dialog_widgets/edit_dialog_title.dart new file mode 100644 index 0000000..076188b --- /dev/null +++ b/lib/widgets/edit_dialog_widgets/edit_dialog_title.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart' show TextButton, Theme; +import 'package:flutter/widgets.dart'; + +class EditDialogTitle extends StatelessWidget { + const EditDialogTitle({ + super.key, + this.onDeletePressed, + required this.dialogType, + }); + final VoidCallback? onDeletePressed; + final DialogType dialogType; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // TODO: Localize + if (dialogType == DialogType.bookmark) + Text('Create Bookmark') + else + Text('Create Collection'), + + if (onDeletePressed != null) + TextButton( + onPressed: () { + onDeletePressed!.call(); + Navigator.of(context).pop(); + }, + child: Text( + 'Delete', + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + ), + ], + ); + } +} + +enum DialogType { bookmark, collection }