1- import 'dart:async' ;
21import 'dart:developer' ;
32import 'dart:io' ;
43
5- import 'package:flutter/services.dart' ;
64import 'package:freecodecamp/models/news/bookmarked_tutorial_model.dart' ;
75import 'package:freecodecamp/models/news/tutorial_model.dart' ;
6+ import 'package:freecodecamp/service/news/legacy/bookmark_sqlite_migrator.dart' ;
7+ import 'package:freecodecamp/service/storage/json_file_store.dart' ;
88import 'package:path/path.dart' as path;
9- import 'package:sqflite/sqflite.dart' ;
10-
11- const String bookmarksTableName = 'bookmarks' ;
9+ import 'package:path_provider/path_provider.dart' ;
1210
1311class BookmarksDatabaseService {
14- late Database _db;
15-
16- Future initialise () async {
17- String dbPath = await getDatabasesPath ();
18- String dbPathTutorials = path.join (dbPath, 'bookmarked-article.db' );
19- bool dbExists = await databaseExists (dbPathTutorials);
20-
21- if (! dbExists) {
22- // Making new copy from assets
23- log ('copying database from assets' );
24- try {
25- await Directory (
26- path.dirname (dbPathTutorials),
27- ).create (recursive: true );
28- } catch (error) {
29- log (error.toString ());
12+ BookmarksDatabaseService ({Directory ? storageDirectoryOverride})
13+ : _storageDirectoryOverride = storageDirectoryOverride;
14+
15+ final Directory ? _storageDirectoryOverride;
16+ final _legacyMigrator = const BookmarkSqliteMigrator ();
17+
18+ JsonFileStore ? _store;
19+ Future <void >? _initFuture;
20+
21+ Future <void > initialise () {
22+ _initFuture ?? = _initialiseInternal ();
23+ return _initFuture! ;
24+ }
25+
26+ Future <void > _initialiseInternal () async {
27+ final baseDir =
28+ _storageDirectoryOverride ?? await getApplicationDocumentsDirectory ();
29+
30+ final file = File (
31+ path.join (baseDir.path, 'storage' , 'bookmarked-articles.json' ),
32+ );
33+ final store = JsonFileStore (
34+ file: file,
35+ defaultValue: {
36+ 'version' : 1 ,
37+ 'migratedFromSqlite' : false ,
38+ 'bookmarks' : [],
39+ },
40+ );
41+ await store.ensureExists ();
42+ _store = store;
43+
44+ await _migrateFromSqliteIfNeeded ();
45+ }
46+
47+ Future <void > _migrateFromSqliteIfNeeded () async {
48+ final store = _store;
49+ if (store == null ) return ;
50+
51+ await store.updateAndWrite ((current) async {
52+ final migrated = current['migratedFromSqlite' ] == true ;
53+ final existing = current['bookmarks' ] ?? [];
54+ if (migrated) return current;
55+
56+ // If the JSON store already has data, don't overwrite it.
57+ if (existing.isNotEmpty) {
58+ return {
59+ ...current,
60+ 'migratedFromSqlite' : true ,
61+ };
3062 }
3163
32- ByteData data = await rootBundle.load (
33- path.join (
34- 'assets' ,
35- 'database' ,
36- 'bookmarked-article.db' ,
37- ),
38- );
39- List <int > bytes = data.buffer.asUint8List (
40- data.offsetInBytes,
41- data.lengthInBytes,
42- );
64+ final legacy = await _legacyMigrator.readBookmarks ();
65+ if (legacy.isEmpty) {
66+ return {
67+ ...current,
68+ 'migratedFromSqlite' : true ,
69+ };
70+ }
4371
44- await File (dbPathTutorials).writeAsBytes (bytes, flush: true );
45- }
72+ final normalized = legacy.map ((row) {
73+ return {
74+ 'articleTitle' : row['articleTitle' ],
75+ 'articleId' : row['articleId' ],
76+ 'articleText' : row['articleText' ],
77+ 'authorName' : row['authorName' ],
78+ };
79+ }).toList ();
4680
47- _db = await openDatabase (dbPathTutorials, version: 1 );
81+ log ('Migrated ${normalized .length } bookmarks from SQLite to JSON' );
82+ return {
83+ ...current,
84+ 'migratedFromSqlite' : true ,
85+ 'bookmarks' : normalized,
86+ };
87+ });
4888 }
4989
5090 Map <String , dynamic > tutorialToMap (dynamic tutorial) {
5191 if (tutorial is Tutorial ) {
5292 return {
53- 'articleTitle' : tutorial.title,
5493 'articleId' : tutorial.id,
94+ 'articleTitle' : tutorial.title,
95+ 'authorName' : tutorial.authorName,
5596 'articleText' : tutorial.text,
56- 'authorName' : tutorial.authorName
5797 };
5898 } else if (tutorial is BookmarkedTutorial ) {
5999 return {
60- 'articleTitle' : tutorial.tutorialTitle,
61100 'articleId' : tutorial.id,
101+ 'articleTitle' : tutorial.tutorialTitle,
102+ 'authorName' : tutorial.authorName,
62103 'articleText' : tutorial.tutorialText,
63- 'authorName' : tutorial.authorName
64104 };
65105 } else {
66106 throw Exception (
@@ -70,50 +110,72 @@ class BookmarksDatabaseService {
70110 }
71111
72112 Future <List <BookmarkedTutorial >> getBookmarks () async {
73- List <Map <String , dynamic >> bookmarksResults =
74- await _db.query (bookmarksTableName);
113+ await initialise ();
114+ final store = _store! ;
115+
116+ final data = await store.read ();
117+ final raw = (data['bookmarks' ] as List ? ) ?? < dynamic > [];
75118
76- List bookmarks = bookmarksResults
77- .map (
78- (tutorial) => BookmarkedTutorial .fromMap (tutorial),
79- )
119+ final normalized = < Map <String , dynamic >> [];
120+ for (var i = 0 ; i < raw.length; i++ ) {
121+ final entry = raw[i];
122+ if (entry is Map ) {
123+ normalized.add (Map <String , dynamic >.from (entry));
124+ }
125+ }
126+
127+ final bookmarks = normalized
128+ .map ((tutorial) => BookmarkedTutorial .fromMap (tutorial))
80129 .toList ();
81130
82- return List .from (bookmarks.reversed);
131+ return List < BookmarkedTutorial > .from (bookmarks.reversed);
83132 }
84133
85134 Future <bool > isBookmarked (dynamic tutorial) async {
86- List <Map <String , dynamic >> bookmarksResults = await _db.query (
87- bookmarksTableName,
88- where: 'articleId = ?' ,
89- whereArgs: [tutorial.id],
90- );
91- return bookmarksResults.isNotEmpty;
135+ await initialise ();
136+ final store = _store! ;
137+ final data = await store.read ();
138+ final raw = (data['bookmarks' ] as List ? ) ?? < dynamic > [];
139+ return raw.any ((e) => e is Map && e['articleId' ] == tutorial.id);
92140 }
93141
94142 Future addBookmark (dynamic tutorial) async {
95- try {
96- await _db.insert (
97- bookmarksTableName,
98- tutorialToMap (tutorial),
99- conflictAlgorithm: ConflictAlgorithm .replace,
100- );
143+ await initialise ();
144+ final store = _store! ;
145+ await store.updateAndWrite ((current) async {
146+ final raw = (current['bookmarks' ] as List ? ) ?? < dynamic > [];
147+ final list = raw
148+ .whereType <Map >()
149+ .map ((e) => Map <String , dynamic >.from (e))
150+ .toList ();
151+
152+ list.removeWhere ((e) => e['articleId' ] == tutorial.id);
153+ list.add (tutorialToMap (tutorial));
154+
101155 log ('Added bookmark: ${tutorial .id }' );
102- } catch (e) {
103- log ('Could not insert the bookmark: $e ' );
104- }
156+ return {
157+ ...current,
158+ 'bookmarks' : list,
159+ };
160+ });
105161 }
106162
107163 Future removeBookmark (dynamic tutorial) async {
108- try {
109- await _db.delete (
110- bookmarksTableName,
111- where: 'articleId = ?' ,
112- whereArgs: [tutorial.id],
113- );
164+ await initialise ();
165+ final store = _store! ;
166+ await store.updateAndWrite ((current) async {
167+ final raw = (current['bookmarks' ] as List ? ) ?? < dynamic > [];
168+ final list = raw
169+ .whereType <Map >()
170+ .map ((e) => Map <String , dynamic >.from (e))
171+ .toList ();
172+
173+ list.removeWhere ((e) => e['articleId' ] == tutorial.id);
114174 log ('Removed bookmark: ${tutorial .id }' );
115- } catch (e) {
116- log ('Could not remove the bookmark: $e ' );
117- }
175+ return {
176+ ...current,
177+ 'bookmarks' : list,
178+ };
179+ });
118180 }
119181}
0 commit comments