4 Commits

Author SHA1 Message Date
2d93d1a9d7 added bookmarks page 2025-09-19 21:54:47 +02:00
a4d760a970 added logic to save collections and display them 2025-09-19 21:04:57 +02:00
d0feca1ba8 added persistence using shared preferences 2025-09-19 20:43:38 +02:00
12459bb4cb removed hive imports 2025-09-19 01:04:03 +02:00
13 changed files with 390 additions and 30 deletions

2
android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,2 @@
-keep class io.flutter.plugins.sharedpreferences.** { *; }
-keep class io.flutter.plugins.sharedpreferences.LegacySharedPreferencesPlugin { *; }

View File

@@ -5,6 +5,7 @@ import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin
class MainActivity: FlutterActivity() {
private var sharedText: String? = null
@@ -31,6 +32,8 @@ class MainActivity: FlutterActivity() {
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
flutterEngine.plugins.add(SharedPreferencesPlugin())
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method == "getSharedText") {

3
devtools_options.yaml Normal file
View File

@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View File

@@ -1,7 +1,11 @@
import 'package:flutter/material.dart';
import 'pages/bookmarks_page.dart';
import 'pages/collections_page.dart';
import 'service/storage.dart';
void main() {
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Storage.initialize();
runApp(const MapsBookmarks());
}
@@ -15,7 +19,10 @@ class MapsBookmarks extends StatelessWidget {
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
initialRoute: CollectionsPage.routeName,
routes: {CollectionsPage.routeName: (context) => const CollectionsPage()},
routes: {
CollectionsPage.routeName: (context) => const CollectionsPage(),
BookmarksPage.routeName: (context) => const BookmarksPage(),
},
);
}
}

View File

@@ -1,13 +1,35 @@
import 'package:hive/hive.dart';
class Bookmark {
Bookmark({
required this.collectionId,
required this.name,
required this.link,
required this.description,
int? createdAt,
}) : createdAt = createdAt ?? DateTime.now().millisecondsSinceEpoch;
class Bookmark extends HiveObject {
Bookmark({required this.name, required this.link});
factory Bookmark.fromJson(Map<String, dynamic> json) =>
Bookmark(name: json['name'] as String, link: json['link'] as String);
factory Bookmark.fromJson(Map<String, dynamic> json) => Bookmark(
collectionId: json['collectionId'] as int,
name: json['name'] as String,
link: json['link'] as String,
description: json['description'] as String,
createdAt: json['createdAt'] as int,
);
int collectionId;
String link;
String name;
String description;
int createdAt;
Map<String, dynamic> toJson() => {'name': name, 'link': link};
int get id => createdAt;
DateTime get createdDate => DateTime.fromMillisecondsSinceEpoch(createdAt);
Map<String, dynamic> toJson() => {
'collectionId': collectionId,
'name': name,
'link': link,
'description': description,
'createdAt': createdAt,
};
}

View File

@@ -1,12 +1,18 @@
import 'package:hive/hive.dart';
class Collection {
Collection({required this.name, int? createdAt})
: createdAt = createdAt ?? DateTime.now().millisecondsSinceEpoch;
class Collection extends HiveObject {
Collection({required this.name});
factory Collection.fromJson(Map<String, dynamic> json) =>
Collection(name: json['name'] as String);
factory Collection.fromJson(Map<String, dynamic> json) => Collection(
name: json['name'] as String,
createdAt: json['createdAt'] as int,
);
String name;
int createdAt; // used as Id with millisecondsSinceEpoch
Map<String, dynamic> toJson() => {'name': name};
int get id => createdAt;
DateTime get createdDate => DateTime.fromMillisecondsSinceEpoch(createdAt);
Map<String, dynamic> toJson() => {'name': name, 'createdAt': createdAt};
}

View File

@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import '../service/bookmarks_provider.dart';
import '../service/storage.dart';
class BookmarksPage extends StatelessWidget {
const BookmarksPage({super.key});
static const String routeName = '/bookmarks';
@override
Widget build(BuildContext context) {
if (BookmarksProvider.selectedCollectionId == null) {
Navigator.of(context).pop();
}
final bookmarks = Storage.loadBookmarksForCollection(
BookmarksProvider.selectedCollectionId!,
);
BookmarksProvider.selectedCollectionId == null;
return Scaffold(
appBar: AppBar(
title: Text(
Storage.loadCollections()
.firstWhere((c) => c.id == BookmarksProvider.selectedCollectionId)
.name,
),
),
body: ListView(
children: bookmarks.map((e) => ListTile(title: Text(e.name))).toList(),
),
);
}
}

View File

@@ -1,6 +1,10 @@
import 'package:flutter/material.dart';
import '../model/collection.dart';
import '../service/bookmarks_provider.dart';
import '../service/storage.dart';
import '../widgets/create_bookmark_collection_dialog.dart';
import 'bookmarks_page.dart';
class CollectionsPage extends StatefulWidget {
const CollectionsPage({super.key});
@@ -11,23 +15,43 @@ class CollectionsPage extends StatefulWidget {
}
class _CollectionsPageState extends State<CollectionsPage> {
final collections = Storage.loadCollections();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
onPressed: () => showDialog(
context: context,
builder: (context) => CreateBookmarkCollectionDialog(),
),
onPressed: onAddButtonPressed,
child: Icon(Icons.add),
),
body: ListView(),
body: ListView.builder(
itemBuilder: itemBuilder,
itemCount: collections.length,
),
);
}
void onAddButtonPressed() => showDialog(
context: context,
builder: (context) => CreateBookmarkCollectionDialog(),
builder: (context) =>
CreateBookmarkCollectionDialog(onSavePressed: onCollectionSaved),
);
void onCollectionSaved(String name) {
collections.add(Collection(name: name));
setState(() {});
Storage.saveCollections(collections);
}
Widget itemBuilder(BuildContext context, int index) {
final collection = collections.elementAt(index);
return ListTile(
title: Text(collection.name),
onTap: () {
BookmarksProvider.selectedCollectionId = collection.id;
Navigator.pushNamed(context, BookmarksPage.routeName);
},
);
}
}

View File

@@ -0,0 +1,3 @@
class BookmarksProvider {
static int? selectedCollectionId;
}

117
lib/service/storage.dart Normal file
View File

@@ -0,0 +1,117 @@
import 'dart:convert' show jsonDecode, jsonEncode;
import 'package:shared_preferences/shared_preferences.dart';
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 SharedPreferencesWithCache? _prefsWithCache;
static Future<void> initialize() async {
_prefsWithCache = await SharedPreferencesWithCache.create(
cacheOptions: const SharedPreferencesWithCacheOptions(
allowList: <String>{_collectionsKey, _bookmarksKey, _statsKey},
),
);
}
static SharedPreferencesWithCache get _prefs {
if (_prefsWithCache == null) {
throw StateError(
'BookmarkStorage not initialized. Call initialize() first.',
);
}
return _prefsWithCache!;
}
static List<Collection> loadCollections() {
final jsonString = _prefs.getString(_collectionsKey) ?? '[]';
final jsonList = jsonDecode(jsonString) as List;
return jsonList
.map((json) => Collection.fromJson(json as Map<String, dynamic>))
.toList();
}
static Future<void> saveCollections(List<Collection> collections) async {
final jsonList = collections.map((c) => c.toJson()).toList();
await _prefs.setString(_collectionsKey, jsonEncode(jsonList));
}
static List<Bookmark> loadAllBookmarks() {
final jsonString = _prefs.getString(_bookmarksKey) ?? '[]';
final jsonList = jsonDecode(jsonString) as List;
return jsonList
.map((json) => Bookmark.fromJson(json as Map<String, dynamic>))
.toList();
}
static Future<void> saveAllBookmarks(List<Bookmark> bookmarks) async {
final jsonList = bookmarks.map((b) => b.toJson()).toList();
await _prefs.setString(_bookmarksKey, jsonEncode(jsonList));
}
static List<Bookmark> loadBookmarksForCollection(int collectionId) {
final allBookmarks = loadAllBookmarks();
return allBookmarks.where((b) => b.collectionId == collectionId).toList();
}
static Future<void> addBookmark(Bookmark bookmark) async {
final bookmarks = loadAllBookmarks();
bookmarks.add(bookmark);
await saveAllBookmarks(bookmarks);
}
static Future<void> deleteBookmarkById(int bookmarkId) async {
final bookmarks = loadAllBookmarks();
bookmarks.removeWhere((b) => b.id == bookmarkId);
await saveAllBookmarks(bookmarks);
}
static Future<void> deleteBookmarksForCollection(int collectionId) async {
final bookmarks = loadAllBookmarks();
bookmarks.removeWhere((b) => b.collectionId == collectionId);
await saveAllBookmarks(bookmarks);
}
static Future<void> updateBookmarkById(
int bookmarkId, {
String? name,
String? description,
}) async {
final bookmarks = loadAllBookmarks();
final index = bookmarks.indexWhere((b) => b.id == bookmarkId);
if (index == -1) return;
if (name != null) bookmarks[index].name = name;
if (description != null) bookmarks[index].description = description;
await saveAllBookmarks(bookmarks);
}
static Map<String, int> getStats() {
final statsJson = _prefs.getString(_statsKey) ?? '{}';
final stats = jsonDecode(statsJson) as Map<String, dynamic>;
return {
'totalCollections': stats['totalCollections'] ?? 0,
'totalBookmarks': stats['totalBookmarks'] ?? 0,
};
}
static Future<void> updateStats() async {
final collections = loadCollections();
final bookmarks = loadAllBookmarks();
final stats = {
'totalCollections': collections.length,
'totalBookmarks': bookmarks.length,
'lastUpdated': DateTime.now().millisecondsSinceEpoch,
};
await _prefs.setString(_statsKey, jsonEncode(stats));
}
}

View File

@@ -2,13 +2,19 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class CreateBookmarkCollectionDialog extends StatelessWidget {
const CreateBookmarkCollectionDialog({super.key});
const CreateBookmarkCollectionDialog({
super.key,
required this.onSavePressed,
});
final void Function(String name) onSavePressed;
@override
Widget build(BuildContext context) {
final nameController = TextEditingController();
return AlertDialog(
title: Text('Create Collection'),
content: TextField(
controller: nameController,
autofocus: true,
maxLines: 1,
maxLength: 50,
@@ -22,7 +28,13 @@ class CreateBookmarkCollectionDialog extends StatelessWidget {
),
),
actions: [
FloatingActionButton(onPressed: () {}, child: Icon(Icons.save)),
FloatingActionButton(
onPressed: () {
onSavePressed(nameController.text);
Navigator.of(context).pop();
},
child: Icon(Icons.save),
),
],
);
}

View File

@@ -65,6 +65,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
flutter:
dependency: "direct main"
description: flutter
@@ -74,15 +90,20 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
version: "6.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
leak_tracker:
dependency: transitive
description:
@@ -111,10 +132,10 @@ packages:
dependency: transitive
description:
name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
url: "https://pub.dev"
source: hosted
version: "5.1.1"
version: "6.0.0"
matcher:
dependency: transitive
description:
@@ -147,6 +168,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
@@ -155,6 +200,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
url: "https://pub.dev"
source: hosted
version: "2.5.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74
url: "https://pub.dev"
source: hosted
version: "2.4.12"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine:
dependency: transitive
description: flutter
@@ -224,6 +333,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "15.0.2"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
sdks:
dart: ">=3.9.2 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
flutter: ">=3.29.0"

View File

@@ -14,12 +14,13 @@ dependencies:
cupertino_icons: ^1.0.8
android_intent_plus: ^6.0.0
shared_preferences: ^2.3.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter_lints: ^6.0.0
flutter: