From 64a53bb7e7f6efbbbec5ae1e394cb6634c262758 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 Aug 2025 07:24:19 +0000 Subject: [PATCH 01/14] Initial plan From 6fa1b68676895d285741a2fdff3e011fdbee549a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 Aug 2025 07:32:37 +0000 Subject: [PATCH 02/14] Replace sqflite with JSON file storage for bookmarks Co-authored-by: Sembauke <46919888+Sembauke@users.noreply.github.com> --- mobile-app/lib/app/app.dart | 2 - .../news/bookmarked_tutorial_model.dart | 21 ++ .../lib/service/news/bookmark_service.dart | 181 +++++++++++++----- mobile-app/pubspec.yaml | 7 +- .../services/news/bookmark_service_test.dart | 142 ++++++++++++-- 5 files changed, 277 insertions(+), 76 deletions(-) diff --git a/mobile-app/lib/app/app.dart b/mobile-app/lib/app/app.dart index 61c59d867..e48ccd570 100644 --- a/mobile-app/lib/app/app.dart +++ b/mobile-app/lib/app/app.dart @@ -37,7 +37,6 @@ import 'package:freecodecamp/ui/views/profile/profile_view.dart'; import 'package:freecodecamp/ui/views/settings/delete-account/delete_account_view.dart'; import 'package:freecodecamp/ui/views/settings/settings_view.dart'; -import 'package:sqflite_migration_service/sqflite_migration_service.dart'; import 'package:stacked/stacked_annotations.dart'; import 'package:stacked_services/stacked_services.dart'; @@ -68,7 +67,6 @@ import 'package:stacked_services/stacked_services.dart'; LazySingleton(classType: NavigationService), LazySingleton(classType: DialogService), LazySingleton(classType: SnackbarService), - LazySingleton(classType: DatabaseMigrationService), LazySingleton(classType: PodcastsDatabaseService), LazySingleton(classType: NotificationService), LazySingleton(classType: DailyChallengeNotificationService), diff --git a/mobile-app/lib/models/news/bookmarked_tutorial_model.dart b/mobile-app/lib/models/news/bookmarked_tutorial_model.dart index f0c311140..44a9b0717 100644 --- a/mobile-app/lib/models/news/bookmarked_tutorial_model.dart +++ b/mobile-app/lib/models/news/bookmarked_tutorial_model.dart @@ -5,6 +5,7 @@ class BookmarkedTutorial { late String tutorialText; late String authorName; + // Legacy constructor for database migration BookmarkedTutorial.fromMap(Map map) { bookmarkId = map['bookmark_id']; tutorialTitle = map['articleTitle']; @@ -13,6 +14,15 @@ class BookmarkedTutorial { authorName = map['authorName']; } + // New constructor for JSON serialization + BookmarkedTutorial.fromJson(Map json) { + bookmarkId = json['bookmarkId'] ?? 0; + tutorialTitle = json['tutorialTitle'] ?? ''; + id = json['id'] ?? ''; + tutorialText = json['tutorialText'] ?? ''; + authorName = json['authorName'] ?? ''; + } + BookmarkedTutorial({ required this.bookmarkId, required this.tutorialTitle, @@ -20,4 +30,15 @@ class BookmarkedTutorial { required this.tutorialText, required this.authorName, }); + + // Convert to JSON for file storage + Map toJson() { + return { + 'bookmarkId': bookmarkId, + 'tutorialTitle': tutorialTitle, + 'id': id, + 'tutorialText': tutorialText, + 'authorName': authorName, + }; + } } diff --git a/mobile-app/lib/service/news/bookmark_service.dart b/mobile-app/lib/service/news/bookmark_service.dart index 398c7437e..4bf5279dc 100644 --- a/mobile-app/lib/service/news/bookmark_service.dart +++ b/mobile-app/lib/service/news/bookmark_service.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:developer'; import 'dart:io'; @@ -6,45 +7,116 @@ import 'package:flutter/services.dart'; import 'package:freecodecamp/models/news/bookmarked_tutorial_model.dart'; import 'package:freecodecamp/models/news/tutorial_model.dart'; import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; import 'package:sqflite/sqflite.dart'; const String bookmarksTableName = 'bookmarks'; +const String articlesFileName = 'articles.json'; class BookmarksDatabaseService { - late Database _db; + List _bookmarks = []; + File? _articlesFile; + int _nextBookmarkId = 1; Future initialise() async { - String dbPath = await getDatabasesPath(); - String dbPathTutorials = path.join(dbPath, 'bookmarked-article.db'); - bool dbExists = await databaseExists(dbPathTutorials); - - if (!dbExists) { - // Making new copy from assets - log('copying database from assets'); - try { - await Directory( - path.dirname(dbPathTutorials), - ).create(recursive: true); - } catch (error) { - log(error.toString()); + try { + final directory = await getApplicationDocumentsDirectory(); + _articlesFile = File(path.join(directory.path, articlesFileName)); + + // Try to migrate from old SQLite database first + await _migrateFromSqliteIfExists(); + + // Load existing bookmarks from JSON file + await _loadBookmarksFromFile(); + + log('Bookmark service initialized with ${_bookmarks.length} bookmarks'); + } catch (e) { + log('Error initializing bookmark service: $e'); + _bookmarks = []; + } + } + + Future _migrateFromSqliteIfExists() async { + try { + String dbPath = await getDatabasesPath(); + String dbPathTutorials = path.join(dbPath, 'bookmarked-article.db'); + bool dbExists = await databaseExists(dbPathTutorials); + + if (dbExists && _bookmarks.isEmpty) { + log('Migrating bookmarks from SQLite database'); + Database db = await openDatabase(dbPathTutorials, version: 1); + + List> results = await db.query(bookmarksTableName); + + for (Map row in results) { + BookmarkedTutorial bookmark = BookmarkedTutorial.fromMap(row); + _bookmarks.add(bookmark); + if (bookmark.bookmarkId >= _nextBookmarkId) { + _nextBookmarkId = bookmark.bookmarkId + 1; + } + } + + await db.close(); + + // Save migrated data to JSON file + await _saveBookmarksToFile(); + + log('Successfully migrated ${_bookmarks.length} bookmarks from SQLite'); } + } catch (e) { + log('Error during SQLite migration: $e'); + } + } - ByteData data = await rootBundle.load( - path.join( - 'assets', - 'database', - 'bookmarked-article.db', - ), - ); - List bytes = data.buffer.asUint8List( - data.offsetInBytes, - data.lengthInBytes, - ); + Future _loadBookmarksFromFile() async { + try { + if (_articlesFile == null || !await _articlesFile!.exists()) { + log('Articles file does not exist, starting with empty bookmarks'); + _bookmarks = []; + return; + } + + String contents = await _articlesFile!.readAsString(); + if (contents.trim().isEmpty) { + log('Articles file is empty, starting with empty bookmarks'); + _bookmarks = []; + return; + } - await File(dbPathTutorials).writeAsBytes(bytes, flush: true); + List jsonList = json.decode(contents); + _bookmarks = jsonList.map((json) => BookmarkedTutorial.fromJson(json)).toList(); + + // Update next bookmark ID + if (_bookmarks.isNotEmpty) { + int maxId = _bookmarks.map((b) => b.bookmarkId).reduce((a, b) => a > b ? a : b); + _nextBookmarkId = maxId + 1; + } + + log('Loaded ${_bookmarks.length} bookmarks from file'); + } catch (e) { + log('Error loading bookmarks from file: $e'); + _bookmarks = []; } + } + + Future _saveBookmarksToFile() async { + try { + if (_articlesFile == null) { + log('Articles file not initialized'); + return; + } - _db = await openDatabase(dbPathTutorials, version: 1); + // Ensure the directory exists + await _articlesFile!.parent.create(recursive: true); + + List> jsonList = _bookmarks.map((b) => b.toJson()).toList(); + String jsonString = json.encode(jsonList); + + await _articlesFile!.writeAsString(jsonString); + log('Saved ${_bookmarks.length} bookmarks to file'); + } catch (e) { + log('Error saving bookmarks to file: $e'); + } } Map tutorialToMap(dynamic tutorial) { @@ -70,34 +142,34 @@ class BookmarksDatabaseService { } Future> getBookmarks() async { - List> bookmarksResults = - await _db.query(bookmarksTableName); - - List bookmarks = bookmarksResults - .map( - (tutorial) => BookmarkedTutorial.fromMap(tutorial), - ) - .toList(); - - return List.from(bookmarks.reversed); + return List.from(_bookmarks.reversed); } Future isBookmarked(dynamic tutorial) async { - List> bookmarksResults = await _db.query( - bookmarksTableName, - where: 'articleId = ?', - whereArgs: [tutorial.id], - ); - return bookmarksResults.isNotEmpty; + return _bookmarks.any((bookmark) => bookmark.id == tutorial.id); } Future addBookmark(dynamic tutorial) async { try { - await _db.insert( - bookmarksTableName, - tutorialToMap(tutorial), - conflictAlgorithm: ConflictAlgorithm.replace, + // Check if already bookmarked + if (await isBookmarked(tutorial)) { + log('Bookmark already exists: ${tutorial.id}'); + return; + } + + Map tutorialMap = tutorialToMap(tutorial); + + BookmarkedTutorial bookmark = BookmarkedTutorial( + bookmarkId: _nextBookmarkId++, + tutorialTitle: tutorialMap['articleTitle'], + id: tutorialMap['articleId'], + tutorialText: tutorialMap['articleText'] ?? '', + authorName: tutorialMap['authorName'], ); + + _bookmarks.add(bookmark); + await _saveBookmarksToFile(); + log('Added bookmark: ${tutorial.id}'); } catch (e) { log('Could not insert the bookmark: $e'); @@ -106,12 +178,15 @@ class BookmarksDatabaseService { Future removeBookmark(dynamic tutorial) async { try { - await _db.delete( - bookmarksTableName, - where: 'articleId = ?', - whereArgs: [tutorial.id], - ); - log('Removed bookmark: ${tutorial.id}'); + int initialLength = _bookmarks.length; + _bookmarks.removeWhere((bookmark) => bookmark.id == tutorial.id); + + if (_bookmarks.length < initialLength) { + await _saveBookmarksToFile(); + log('Removed bookmark: ${tutorial.id}'); + } else { + log('Bookmark not found for removal: ${tutorial.id}'); + } } catch (e) { log('Could not remove the bookmark: $e'); } diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index ab552b540..98b7b6b72 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -46,10 +46,7 @@ dependencies: quick_actions: 1.1.0 share_plus: 10.1.4 # NOTE: v11 has breaking changes shared_preferences: 2.5.3 # TODO: update deprecated code - sqflite: 2.4.2 - # TODO: Replace with sqflite methods as below package isn't actively maintained - sqflite_migration_service: 2.0.0-nullsafety.1 - stacked: 3.4.3 # NOTE: 3.4.4+ is blocked by sqflite_migration_service + stacked: 3.4.4 # Unlocked since sqflite_migration_service is removed stacked_services: 1.6.0 timezone: 0.10.1 ua_client_hints: 1.4.1 @@ -70,7 +67,7 @@ dev_dependencies: integration_test: sdk: flutter mockito: 5.4.5 # NOTE: 5.4.6+ is blocked by stacked_generator - sqflite_common_ffi: 2.3.5 + path_provider_platform_interface: 2.1.2 stacked_generator: 1.6.1 flutter: diff --git a/mobile-app/test/services/news/bookmark_service_test.dart b/mobile-app/test/services/news/bookmark_service_test.dart index abcc2d25e..feb9a90a7 100644 --- a/mobile-app/test/services/news/bookmark_service_test.dart +++ b/mobile-app/test/services/news/bookmark_service_test.dart @@ -6,7 +6,7 @@ import 'package:freecodecamp/models/news/bookmarked_tutorial_model.dart'; import 'package:freecodecamp/models/news/tutorial_model.dart'; import 'package:freecodecamp/service/news/bookmark_service.dart'; import 'package:path/path.dart' as path; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import '../../helpers/test_helpers.dart'; @@ -37,24 +37,36 @@ final BookmarkedTutorial testBookmarkTutorial = BookmarkedTutorial( void main() { TestWidgetsFlutterBinding.ensureInitialized(); - sqfliteFfiInit(); - databaseFactory = databaseFactoryFfi; + group('News Bookmark Service Test -', () { - setUp(() => registerServices()); - tearDown(() => locator.reset()); - tearDownAll(() async { - String dbPath = await getDatabasesPath(); - String dbPathTutorials = path.join(dbPath, 'bookmarked-article.db'); - File(dbPathTutorials).deleteSync(); + late Directory tempDir; + + setUp(() async { + registerServices(); + // Create a temporary directory for testing + tempDir = await Directory.systemTemp.createTemp('bookmark_test_'); + + // Mock path_provider to use our temp directory + PathProviderPlatform.instance = FakePathProviderPlatform(tempDir.path); + }); + + tearDown(() async { + locator.reset(); + // Clean up temporary directory + if (await tempDir.exists()) { + await tempDir.delete(recursive: true); + } }); test('init service', () async { final service = locator(); await service.initialise(); - String dbPath = await getDatabasesPath(); - String dbPathTutorials = path.join(dbPath, 'bookmarked-article.db'); expect(service, isA()); - expect(await databaseExists(dbPathTutorials), true); + + // Check that articles.json file is created in temp directory + String articlesPath = path.join(tempDir.path, 'articles.json'); + // File might not exist yet if no bookmarks are added + expect(await Directory(tempDir.path).exists(), true); }); test('get all bookmarks', () async { @@ -73,7 +85,7 @@ void main() { expect(bookmarks, isA>()); expect(bookmarks.length, 1); expect(bookmarks[0].authorName, testBookmarkTutorial.authorName); - expect(bookmarks[0].bookmarkId, testBookmarkTutorial.bookmarkId); + expect(bookmarks[0].bookmarkId, 1); // First bookmark should have ID 1 expect(bookmarks[0].id, testBookmarkTutorial.id); expect(bookmarks[0].tutorialText, testBookmarkTutorial.tutorialText); expect(bookmarks[0].tutorialTitle, testBookmarkTutorial.tutorialTitle); @@ -82,16 +94,114 @@ void main() { test('check if bookmark exists', () async { final service = locator(); await service.initialise(); - expect(await service.isBookmarked(testBookmarkTutorial), true); + + // Initially should not be bookmarked + expect(await service.isBookmarked(testTutorial), false); + + // Add bookmark + await service.addBookmark(testTutorial); + + // Now should be bookmarked + expect(await service.isBookmarked(testTutorial), true); }); test('delete bookmark', () async { final service = locator(); await service.initialise(); - await service.removeBookmark(testBookmarkTutorial); - final bookmarks = await service.getBookmarks(); + + // Add bookmark first + await service.addBookmark(testTutorial); + var bookmarks = await service.getBookmarks(); + expect(bookmarks.length, 1); + + // Remove bookmark + await service.removeBookmark(testTutorial); + bookmarks = await service.getBookmarks(); expect(bookmarks, isA>()); expect(bookmarks.length, 0); }); + + test('prevent duplicate bookmarks', () async { + final service = locator(); + await service.initialise(); + + // Add same bookmark twice + await service.addBookmark(testTutorial); + await service.addBookmark(testTutorial); + + final bookmarks = await service.getBookmarks(); + expect(bookmarks.length, 1); // Should only have one bookmark + }); + + test('persistence between service instances', () async { + // First service instance + final service1 = locator(); + await service1.initialise(); + await service1.addBookmark(testTutorial); + + var bookmarks = await service1.getBookmarks(); + expect(bookmarks.length, 1); + + // Reset and create new service instance + locator.reset(); + registerServices(); + + final service2 = locator(); + await service2.initialise(); + + // Should load the previously saved bookmark + bookmarks = await service2.getBookmarks(); + expect(bookmarks.length, 1); + expect(bookmarks[0].id, testTutorial.id); + }); }); } + +// Mock PathProviderPlatform for testing +class FakePathProviderPlatform extends PathProviderPlatform { + final String tempPath; + + FakePathProviderPlatform(this.tempPath); + + @override + Future getApplicationDocumentsPath() async { + return tempPath; + } + + @override + Future getTemporaryPath() async { + return tempPath; + } + + @override + Future getApplicationSupportPath() async { + return tempPath; + } + + @override + Future getLibraryPath() async { + return tempPath; + } + + @override + Future getExternalStoragePath() async { + return tempPath; + } + + @override + Future?> getExternalCachePaths() async { + return [tempPath]; + } + + @override + Future?> getExternalStoragePaths({ + StorageDirectory? type, + }) async { + return [tempPath]; + } + + @override + Future getDownloadsPath() async { + return tempPath; + } +} From 00f4839676132680f800f28fc46264d0c5b9dea0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 Aug 2025 07:34:56 +0000 Subject: [PATCH 03/14] Complete migration from sqflite to JSON storage Co-authored-by: Sembauke <46919888+Sembauke@users.noreply.github.com> --- .../integration_test/news/bookmark_test.dart | 30 +++++++++++-------- mobile-app/lib/app/app.locator.dart | 2 -- mobile-app/pubspec.yaml | 1 - 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/mobile-app/integration_test/news/bookmark_test.dart b/mobile-app/integration_test/news/bookmark_test.dart index 238022a10..b58a59343 100644 --- a/mobile-app/integration_test/news/bookmark_test.dart +++ b/mobile-app/integration_test/news/bookmark_test.dart @@ -1,10 +1,13 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:freecodecamp/main.dart' as app; import 'package:freecodecamp/ui/views/news/news-tutorial/news_tutorial_view.dart'; import 'package:integration_test/integration_test.dart'; import 'package:path/path.dart' as path; -import 'package:sqflite/sqflite.dart'; +import 'package:path_provider/path_provider.dart'; void main() { final binding = IntegrationTestWidgetsFlutterBinding(); @@ -89,17 +92,18 @@ void main() { author.split('Written by ')[1], ); - // Check database if record exists - final db = await openDatabase( - path.join(await getDatabasesPath(), 'bookmarked-article.db')); - final List> result = await db.query( - 'bookmarks', - where: 'articleId = ?', - whereArgs: [firstTutorialKey.value], - ); - expect(result.length, 1); - expect(result[0]['articleId'], firstTutorialKey.value); - expect(result[0]['articleTitle'], title.data); - expect(result[0]['authorName'], author.split('Written by ')[1]); + // Check JSON file if record exists + final directory = await getApplicationDocumentsDirectory(); + final articlesFile = File(path.join(directory.path, 'articles.json')); + + expect(await articlesFile.exists(), true); + + final contents = await articlesFile.readAsString(); + final List bookmarks = json.decode(contents); + + expect(bookmarks.length, 1); + expect(bookmarks[0]['id'], firstTutorialKey.value); + expect(bookmarks[0]['tutorialTitle'], title.data); + expect(bookmarks[0]['authorName'], author.split('Written by ')[1]); }); } diff --git a/mobile-app/lib/app/app.locator.dart b/mobile-app/lib/app/app.locator.dart index 8829a2d8e..203878340 100644 --- a/mobile-app/lib/app/app.locator.dart +++ b/mobile-app/lib/app/app.locator.dart @@ -6,7 +6,6 @@ // ignore_for_file: public_member_api_docs, implementation_imports, depend_on_referenced_packages -import 'package:sqflite_migration_service/src/database_migration_service.dart'; import 'package:stacked_services/src/dialog/dialog_service.dart'; import 'package:stacked_services/src/navigation/navigation_service.dart'; import 'package:stacked_services/src/snackbar/snackbar_service.dart'; @@ -45,7 +44,6 @@ Future setupLocator({ locator.registerLazySingleton(() => NavigationService()); locator.registerLazySingleton(() => DialogService()); locator.registerLazySingleton(() => SnackbarService()); - locator.registerLazySingleton(() => DatabaseMigrationService()); locator.registerLazySingleton(() => PodcastsDatabaseService()); locator.registerLazySingleton(() => NotificationService()); locator.registerLazySingleton(() => DailyChallengeNotificationService()); diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index 98b7b6b72..ed4021f08 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -86,7 +86,6 @@ flutter: - assets/sql/ - assets/test_data/news_post.json - assets/test_data/news_feed.json - - assets/database/bookmarked-article.db - assets/learn/ - assets/test_runner/dist/ - assets/test_runner/babel/ From ef49411e28d54b0ca97b1999a111da24b773a193 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 Aug 2025 08:11:46 +0000 Subject: [PATCH 04/14] Remove SQLite migration code and clean up bookmark service Co-authored-by: Sembauke <46919888+Sembauke@users.noreply.github.com> --- .../news/bookmarked_tutorial_model.dart | 11 +----- .../lib/service/news/bookmark_service.dart | 38 ------------------- mobile-app/pubspec.yaml | 1 - 3 files changed, 1 insertion(+), 49 deletions(-) diff --git a/mobile-app/lib/models/news/bookmarked_tutorial_model.dart b/mobile-app/lib/models/news/bookmarked_tutorial_model.dart index 44a9b0717..15df76eac 100644 --- a/mobile-app/lib/models/news/bookmarked_tutorial_model.dart +++ b/mobile-app/lib/models/news/bookmarked_tutorial_model.dart @@ -5,16 +5,7 @@ class BookmarkedTutorial { late String tutorialText; late String authorName; - // Legacy constructor for database migration - BookmarkedTutorial.fromMap(Map map) { - bookmarkId = map['bookmark_id']; - tutorialTitle = map['articleTitle']; - id = map['articleId']; - tutorialText = map['articleText']; - authorName = map['authorName']; - } - - // New constructor for JSON serialization + // Constructor for JSON serialization BookmarkedTutorial.fromJson(Map json) { bookmarkId = json['bookmarkId'] ?? 0; tutorialTitle = json['tutorialTitle'] ?? ''; diff --git a/mobile-app/lib/service/news/bookmark_service.dart b/mobile-app/lib/service/news/bookmark_service.dart index 4bf5279dc..3c50fc290 100644 --- a/mobile-app/lib/service/news/bookmark_service.dart +++ b/mobile-app/lib/service/news/bookmark_service.dart @@ -3,14 +3,11 @@ import 'dart:convert'; import 'dart:developer'; import 'dart:io'; -import 'package:flutter/services.dart'; import 'package:freecodecamp/models/news/bookmarked_tutorial_model.dart'; import 'package:freecodecamp/models/news/tutorial_model.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; -import 'package:sqflite/sqflite.dart'; -const String bookmarksTableName = 'bookmarks'; const String articlesFileName = 'articles.json'; class BookmarksDatabaseService { @@ -23,9 +20,6 @@ class BookmarksDatabaseService { final directory = await getApplicationDocumentsDirectory(); _articlesFile = File(path.join(directory.path, articlesFileName)); - // Try to migrate from old SQLite database first - await _migrateFromSqliteIfExists(); - // Load existing bookmarks from JSON file await _loadBookmarksFromFile(); @@ -36,38 +30,6 @@ class BookmarksDatabaseService { } } - Future _migrateFromSqliteIfExists() async { - try { - String dbPath = await getDatabasesPath(); - String dbPathTutorials = path.join(dbPath, 'bookmarked-article.db'); - bool dbExists = await databaseExists(dbPathTutorials); - - if (dbExists && _bookmarks.isEmpty) { - log('Migrating bookmarks from SQLite database'); - Database db = await openDatabase(dbPathTutorials, version: 1); - - List> results = await db.query(bookmarksTableName); - - for (Map row in results) { - BookmarkedTutorial bookmark = BookmarkedTutorial.fromMap(row); - _bookmarks.add(bookmark); - if (bookmark.bookmarkId >= _nextBookmarkId) { - _nextBookmarkId = bookmark.bookmarkId + 1; - } - } - - await db.close(); - - // Save migrated data to JSON file - await _saveBookmarksToFile(); - - log('Successfully migrated ${_bookmarks.length} bookmarks from SQLite'); - } - } catch (e) { - log('Error during SQLite migration: $e'); - } - } - Future _loadBookmarksFromFile() async { try { if (_articlesFile == null || !await _articlesFile!.exists()) { diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index ed4021f08..40c9db770 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -83,7 +83,6 @@ flutter: assets: - .env - assets/images/ - - assets/sql/ - assets/test_data/news_post.json - assets/test_data/news_feed.json - assets/learn/ From 1a82274c54a3312076cab617e30b6d631d6219c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 Aug 2025 08:13:02 +0000 Subject: [PATCH 05/14] Restore sqflite dependency needed by podcast service Co-authored-by: Sembauke <46919888+Sembauke@users.noreply.github.com> --- mobile-app/pubspec.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index 40c9db770..b7bf482fd 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -46,6 +46,7 @@ dependencies: quick_actions: 1.1.0 share_plus: 10.1.4 # NOTE: v11 has breaking changes shared_preferences: 2.5.3 # TODO: update deprecated code + sqflite: 2.4.2 stacked: 3.4.4 # Unlocked since sqflite_migration_service is removed stacked_services: 1.6.0 timezone: 0.10.1 @@ -68,6 +69,7 @@ dev_dependencies: sdk: flutter mockito: 5.4.5 # NOTE: 5.4.6+ is blocked by stacked_generator path_provider_platform_interface: 2.1.2 + sqflite_common_ffi: 2.3.5 stacked_generator: 1.6.1 flutter: From 68fb83186819c166e5ba9fde31a47ec13db7f63f Mon Sep 17 00:00:00 2001 From: sembauke Date: Tue, 26 Aug 2025 10:19:31 +0200 Subject: [PATCH 06/14] fix: downgrade --- mobile-app/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index b7bf482fd..335b117b2 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -47,7 +47,7 @@ dependencies: share_plus: 10.1.4 # NOTE: v11 has breaking changes shared_preferences: 2.5.3 # TODO: update deprecated code sqflite: 2.4.2 - stacked: 3.4.4 # Unlocked since sqflite_migration_service is removed + stacked: 3.4.3 stacked_services: 1.6.0 timezone: 0.10.1 ua_client_hints: 1.4.1 From f6d195cc446dafbf8bdee13959f62f3561caf436 Mon Sep 17 00:00:00 2001 From: sembauke Date: Tue, 26 Aug 2025 10:46:53 +0200 Subject: [PATCH 07/14] fix: downgrade dependencies in pubspec.yaml and pubspec.lock --- mobile-app/pubspec.lock | 79 +++++++++++++++-------------------------- mobile-app/pubspec.yaml | 5 ++- 2 files changed, 31 insertions(+), 53 deletions(-) diff --git a/mobile-app/pubspec.lock b/mobile-app/pubspec.lock index 3c60b0d08..0549aa39c 100644 --- a/mobile-app/pubspec.lock +++ b/mobile-app/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "76.0.0" + version: "67.0.0" _flutterfire_internals: dependency: transitive description: @@ -17,11 +17,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.54" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" algolia_client_core: dependency: transitive description: @@ -74,10 +69,10 @@ packages: dependency: transitive description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "6.4.1" ansicolor: dependency: transitive description: @@ -170,10 +165,10 @@ packages: dependency: transitive description: name: build - sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.1" build_config: dependency: transitive description: @@ -194,26 +189,26 @@ packages: dependency: transitive description: name: build_resolvers - sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.15" + version: "2.4.13" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "7.3.2" built_collection: dependency: transitive description: @@ -338,10 +333,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.8" + version: "2.3.6" dbus: dependency: transitive description: @@ -1093,26 +1088,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.1" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -1145,14 +1140,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - macros: - dependency: transitive - description: - name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" - url: "https://pub.dev" - source: hosted - version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -1189,10 +1176,10 @@ packages: dependency: "direct dev" description: name: mockito - sha256: f99d8d072e249f719a5531735d146d8cf04c580d93920b04de75bef6dfb2daf6 + sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" url: "https://pub.dev" source: hosted - version: "5.4.5" + version: "5.4.4" nested: dependency: transitive description: @@ -1549,10 +1536,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "2.0.1" sky_engine: dependency: transitive description: flutter @@ -1630,14 +1617,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.2" - sqflite_migration_service: - dependency: "direct main" - description: - name: sqflite_migration_service - sha256: "40d2d074ebd54fde5e476ef20eec8c6c8285b4bf89d3c9dd880dfcc3489b1355" - url: "https://pub.dev" - source: hosted - version: "2.0.0-nullsafety.1" sqflite_platform_interface: dependency: transitive description: @@ -1746,10 +1725,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.6" timezone: dependency: "direct main" description: @@ -1898,10 +1877,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" version: dependency: transitive description: @@ -2055,5 +2034,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=3.7.0 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.27.0" diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index 335b117b2..1d966381c 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -56,7 +56,7 @@ dependencies: web_socket_channel: 3.0.3 youtube_player_iframe: 5.2.1 dev_dependencies: - build_runner: 2.4.15 + build_runner: 2.4.13 dependency_validator: 3.2.3 flutter_driver: sdk: flutter @@ -67,8 +67,7 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - mockito: 5.4.5 # NOTE: 5.4.6+ is blocked by stacked_generator - path_provider_platform_interface: 2.1.2 + mockito: 5.4.4 sqflite_common_ffi: 2.3.5 stacked_generator: 1.6.1 From da2b868e80bd13d612dfd56bca0f2acbbe02a36e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 Aug 2025 08:52:44 +0000 Subject: [PATCH 08/14] Restore sqflite_migration_service for podcast functionality Co-authored-by: Sembauke <46919888+Sembauke@users.noreply.github.com> --- mobile-app/lib/app/app.locator.dart | 2 ++ mobile-app/pubspec.yaml | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/mobile-app/lib/app/app.locator.dart b/mobile-app/lib/app/app.locator.dart index 203878340..4d69f40f2 100644 --- a/mobile-app/lib/app/app.locator.dart +++ b/mobile-app/lib/app/app.locator.dart @@ -10,6 +10,7 @@ import 'package:stacked_services/src/dialog/dialog_service.dart'; import 'package:stacked_services/src/navigation/navigation_service.dart'; import 'package:stacked_services/src/snackbar/snackbar_service.dart'; import 'package:stacked_shared/stacked_shared.dart'; +import 'package:sqflite_migration_service/sqflite_migration_service.dart'; import '../service/audio/audio_service.dart'; import '../service/authentication/authentication_service.dart'; @@ -44,6 +45,7 @@ Future setupLocator({ locator.registerLazySingleton(() => NavigationService()); locator.registerLazySingleton(() => DialogService()); locator.registerLazySingleton(() => SnackbarService()); + locator.registerLazySingleton(() => DatabaseMigrationService()); locator.registerLazySingleton(() => PodcastsDatabaseService()); locator.registerLazySingleton(() => NotificationService()); locator.registerLazySingleton(() => DailyChallengeNotificationService()); diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index 1d966381c..ee54dc134 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -47,7 +47,9 @@ dependencies: share_plus: 10.1.4 # NOTE: v11 has breaking changes shared_preferences: 2.5.3 # TODO: update deprecated code sqflite: 2.4.2 - stacked: 3.4.3 + # TODO: Replace with sqflite methods as below package isn't actively maintained + sqflite_migration_service: 2.0.0-nullsafety.1 + stacked: 3.4.3 # NOTE: 3.4.4+ is blocked by sqflite_migration_service stacked_services: 1.6.0 timezone: 0.10.1 ua_client_hints: 1.4.1 @@ -84,6 +86,7 @@ flutter: assets: - .env - assets/images/ + - assets/sql/ - assets/test_data/news_post.json - assets/test_data/news_feed.json - assets/learn/ From e410bfb96523b93c37f66ecca8caf917b8a818a3 Mon Sep 17 00:00:00 2001 From: sembauke Date: Tue, 26 Aug 2025 15:02:47 +0200 Subject: [PATCH 09/14] fix: downgrade build_runner to version 2.4.11 and add sqflite_migration_service dependency --- mobile-app/pubspec.lock | 16 ++++++++++++---- mobile-app/pubspec.yaml | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/mobile-app/pubspec.lock b/mobile-app/pubspec.lock index 0549aa39c..ce2e3eaac 100644 --- a/mobile-app/pubspec.lock +++ b/mobile-app/pubspec.lock @@ -197,10 +197,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.11" build_runner_core: dependency: transitive description: @@ -1168,10 +1168,10 @@ packages: dependency: transitive description: name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "1.0.6" mockito: dependency: "direct dev" description: @@ -1617,6 +1617,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.2" + sqflite_migration_service: + dependency: "direct main" + description: + name: sqflite_migration_service + sha256: "40d2d074ebd54fde5e476ef20eec8c6c8285b4bf89d3c9dd880dfcc3489b1355" + url: "https://pub.dev" + source: hosted + version: "2.0.0-nullsafety.1" sqflite_platform_interface: dependency: transitive description: diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index ee54dc134..0ea603cf8 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -58,7 +58,7 @@ dependencies: web_socket_channel: 3.0.3 youtube_player_iframe: 5.2.1 dev_dependencies: - build_runner: 2.4.13 + build_runner: 2.4.11 dependency_validator: 3.2.3 flutter_driver: sdk: flutter From 1a5496b89e9468ce6638a7191553440ca4d4b70c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 Aug 2025 13:16:43 +0000 Subject: [PATCH 10/14] Convert podcast service from SQLite to JSON storage Co-authored-by: Sembauke <46919888+Sembauke@users.noreply.github.com> --- mobile-app/assets/sql/1_create_db_schema.sql | 25 -- mobile-app/assets/sql/2_delete_all_values.sql | 2 - .../assets/sql/3_reset_episodes_schema.sql | 13 - mobile-app/assets/sql/4_delete_all_values.sql | 2 - mobile-app/lib/app/app.locator.dart | 2 - .../lib/models/podcasts/episodes_model.dart | 7 +- .../lib/models/podcasts/podcasts_model.dart | 2 +- .../lib/service/podcast/podcasts_service.dart | 225 ++++++++++++------ mobile-app/pubspec.yaml | 7 +- .../podcast/podcast_service_test.dart | 143 +++++++++++ 10 files changed, 299 insertions(+), 129 deletions(-) delete mode 100644 mobile-app/assets/sql/1_create_db_schema.sql delete mode 100644 mobile-app/assets/sql/2_delete_all_values.sql delete mode 100644 mobile-app/assets/sql/3_reset_episodes_schema.sql delete mode 100644 mobile-app/assets/sql/4_delete_all_values.sql create mode 100644 mobile-app/test/services/podcast/podcast_service_test.dart diff --git a/mobile-app/assets/sql/1_create_db_schema.sql b/mobile-app/assets/sql/1_create_db_schema.sql deleted file mode 100644 index f55be542e..000000000 --- a/mobile-app/assets/sql/1_create_db_schema.sql +++ /dev/null @@ -1,25 +0,0 @@ -CREATE TABLE podcasts( - id TEXT PRIMARY KEY, - url TEXT, - link TEXT, - title TEXT, - description TEXT, - image TEXT, - copyright TEXT, - numEps INTEGER -); - -CREATE TABLE episodes( - guid TEXT, - podcastId TEXT, - title TEXT, - description TEXT, - link TEXT, - publicationDate INTEGER, - contentUrl TEXT, - imageUrl TEXT, - duration INTEGER, - downloaded INTEGER, - FOREIGN KEY(podcastId) REFERENCES podcasts(id), - PRIMARY KEY (guid, podcastId) -); \ No newline at end of file diff --git a/mobile-app/assets/sql/2_delete_all_values.sql b/mobile-app/assets/sql/2_delete_all_values.sql deleted file mode 100644 index 776067a56..000000000 --- a/mobile-app/assets/sql/2_delete_all_values.sql +++ /dev/null @@ -1,2 +0,0 @@ -DELETE FROM podcasts; -DELETE FROM episodes; \ No newline at end of file diff --git a/mobile-app/assets/sql/3_reset_episodes_schema.sql b/mobile-app/assets/sql/3_reset_episodes_schema.sql deleted file mode 100644 index d061a01c9..000000000 --- a/mobile-app/assets/sql/3_reset_episodes_schema.sql +++ /dev/null @@ -1,13 +0,0 @@ -DROP TABLE episodes; - -CREATE TABLE episodes( - id TEXT, - podcastId TEXT, - title TEXT, - description TEXT, - publicationDate TEXT, - contentUrl TEXT, - duration TEXT, - FOREIGN KEY(podcastId) REFERENCES podcasts(id), - PRIMARY KEY (id, podcastId) -); \ No newline at end of file diff --git a/mobile-app/assets/sql/4_delete_all_values.sql b/mobile-app/assets/sql/4_delete_all_values.sql deleted file mode 100644 index afaa141c4..000000000 --- a/mobile-app/assets/sql/4_delete_all_values.sql +++ /dev/null @@ -1,2 +0,0 @@ -DELETE FROM podcasts; -DELETE FROM episodes; diff --git a/mobile-app/lib/app/app.locator.dart b/mobile-app/lib/app/app.locator.dart index 4d69f40f2..203878340 100644 --- a/mobile-app/lib/app/app.locator.dart +++ b/mobile-app/lib/app/app.locator.dart @@ -10,7 +10,6 @@ import 'package:stacked_services/src/dialog/dialog_service.dart'; import 'package:stacked_services/src/navigation/navigation_service.dart'; import 'package:stacked_services/src/snackbar/snackbar_service.dart'; import 'package:stacked_shared/stacked_shared.dart'; -import 'package:sqflite_migration_service/sqflite_migration_service.dart'; import '../service/audio/audio_service.dart'; import '../service/authentication/authentication_service.dart'; @@ -45,7 +44,6 @@ Future setupLocator({ locator.registerLazySingleton(() => NavigationService()); locator.registerLazySingleton(() => DialogService()); locator.registerLazySingleton(() => SnackbarService()); - locator.registerLazySingleton(() => DatabaseMigrationService()); locator.registerLazySingleton(() => PodcastsDatabaseService()); locator.registerLazySingleton(() => NotificationService()); locator.registerLazySingleton(() => DailyChallengeNotificationService()); diff --git a/mobile-app/lib/models/podcasts/episodes_model.dart b/mobile-app/lib/models/podcasts/episodes_model.dart index 34e078ea0..078d5c570 100644 --- a/mobile-app/lib/models/podcasts/episodes_model.dart +++ b/mobile-app/lib/models/podcasts/episodes_model.dart @@ -40,8 +40,7 @@ class Episodes { ? null : DateTime.parse(json['publicationDate']), contentUrl: json['contentUrl'] as String?, - duration: - json['duration'] == null ? null : parseDuration(json['duration']), + duration: json['duration'] == null ? null : parseDuration(json['duration']), ); Map toJson() => { @@ -51,7 +50,7 @@ class Episodes { 'description': description, 'publicationDate': publicationDate?.toIso8601String(), 'contentUrl': contentUrl, - 'duration': duration.toString(), + 'duration': duration?.toString(), }; @override @@ -60,7 +59,7 @@ class Episodes { id: $id, podcastId: $podcastId, title: $title, - description: ${description!.substring(0, 100)}, + description: ${description != null && description!.length > 100 ? description!.substring(0, 100) : description}, publicationDate: $publicationDate, contentUrl: $contentUrl, duration: $duration, diff --git a/mobile-app/lib/models/podcasts/podcasts_model.dart b/mobile-app/lib/models/podcasts/podcasts_model.dart index c92a4ff00..70cd62240 100644 --- a/mobile-app/lib/models/podcasts/podcasts_model.dart +++ b/mobile-app/lib/models/podcasts/podcasts_model.dart @@ -59,7 +59,7 @@ class Podcasts { url: $url, link: $link, title: $title, - description: ${description!.substring(0, 100)}, + description: ${description != null && description!.length > 100 ? description!.substring(0, 100) : description}, image: $image, copyright: $copyright numEps: $numEps diff --git a/mobile-app/lib/service/podcast/podcasts_service.dart b/mobile-app/lib/service/podcast/podcasts_service.dart index cf787608e..e150ebebc 100644 --- a/mobile-app/lib/service/podcast/podcasts_service.dart +++ b/mobile-app/lib/service/podcast/podcasts_service.dart @@ -1,54 +1,143 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:developer'; +import 'dart:io'; -import 'package:freecodecamp/app/app.locator.dart'; import 'package:freecodecamp/models/podcasts/episodes_model.dart'; import 'package:freecodecamp/models/podcasts/podcasts_model.dart'; -import 'package:sqflite/sqflite.dart'; -import 'package:sqflite_migration_service/sqflite_migration_service.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; -const String podcastsTableName = 'podcasts'; -const String episodesTableName = 'episodes'; +const String podcastsFileName = 'podcasts.json'; +const String episodesFileName = 'episodes.json'; class PodcastsDatabaseService { - final _migrationService = locator(); - late Database _db; + List _podcasts = []; + List _episodes = []; + File? _podcastsFile; + File? _episodesFile; Future initialise() async { - _db = await openDatabase('podcasts.db', version: 1); - log(_db.path); - // Uncomment below line to reset migrations - // _migrationService.resetVersion(); - - await _migrationService.runMigration( - _db, - migrationFiles: [ - '1_create_db_schema.sql', - '2_delete_all_values.sql', - '3_reset_episodes_schema.sql', - '4_delete_all_values.sql', - ], - verbose: true, - ); - log('FINISHED LOADING MIGRATIONS'); + try { + final directory = await getApplicationDocumentsDirectory(); + _podcastsFile = File(path.join(directory.path, podcastsFileName)); + _episodesFile = File(path.join(directory.path, episodesFileName)); + + await _loadPodcastsFromFile(); + await _loadEpisodesFromFile(); + + log('Podcast service initialized with ${_podcasts.length} podcasts and ${_episodes.length} episodes'); + } catch (e) { + log('Error initializing podcast service: $e'); + _podcasts = []; + _episodes = []; + } + } + + Future _loadPodcastsFromFile() async { + try { + if (_podcastsFile == null || !await _podcastsFile!.exists()) { + log('Podcasts file does not exist, starting with empty podcasts'); + _podcasts = []; + return; + } + + String contents = await _podcastsFile!.readAsString(); + if (contents.trim().isEmpty) { + log('Podcasts file is empty, starting with empty podcasts'); + _podcasts = []; + return; + } + + List jsonList = json.decode(contents); + _podcasts = jsonList.map((json) => Podcasts.fromDBJson(json)).toList(); + + log('Loaded ${_podcasts.length} podcasts from file'); + } catch (e) { + log('Error loading podcasts from file: $e'); + _podcasts = []; + } + } + + Future _loadEpisodesFromFile() async { + try { + if (_episodesFile == null || !await _episodesFile!.exists()) { + log('Episodes file does not exist, starting with empty episodes'); + _episodes = []; + return; + } + + String contents = await _episodesFile!.readAsString(); + if (contents.trim().isEmpty) { + log('Episodes file is empty, starting with empty episodes'); + _episodes = []; + return; + } + + List jsonList = json.decode(contents); + _episodes = jsonList.map((json) => Episodes.fromDBJson(json)).toList(); + + log('Loaded ${_episodes.length} episodes from file'); + } catch (e) { + log('Error loading episodes from file: $e'); + _episodes = []; + } + } + + Future _savePodcastsToFile() async { + try { + if (_podcastsFile == null) { + log('Podcasts file not initialized'); + return; + } + + await _podcastsFile!.parent.create(recursive: true); + + List> jsonList = _podcasts.map((p) => p.toJson()).toList(); + String jsonString = json.encode(jsonList); + + await _podcastsFile!.writeAsString(jsonString); + log('Saved ${_podcasts.length} podcasts to file'); + } catch (e) { + log('Error saving podcasts to file: $e'); + } + } + + Future _saveEpisodesToFile() async { + try { + if (_episodesFile == null) { + log('Episodes file not initialized'); + return; + } + + await _episodesFile!.parent.create(recursive: true); + + List> jsonList = _episodes.map((e) => e.toJson()).toList(); + String jsonString = json.encode(jsonList); + + await _episodesFile!.writeAsString(jsonString); + log('Saved ${_episodes.length} episodes to file'); + } catch (e) { + log('Error saving episodes to file: $e'); + } } // PODCAST QUERIES Future> getPodcasts() async { - List> podcastsResults = - await _db.query(podcastsTableName); - return podcastsResults - .map((podcast) => Podcasts.fromDBJson(podcast)) - .toList(); + return List.from(_podcasts); } Future addPodcast(Podcasts podcast) async { try { - await _db.insert( - podcastsTableName, - podcast.toJson(), - conflictAlgorithm: ConflictAlgorithm.replace, - ); + // Check if podcast already exists + bool exists = _podcasts.any((p) => p.id == podcast.id); + if (exists) { + log('Podcast already exists: ${podcast.title}'); + return; + } + + _podcasts.add(podcast); + await _savePodcastsToFile(); log('Added Podcast: ${podcast.title}'); } catch (e) { log('Could not insert the podcast: $e'); @@ -57,20 +146,15 @@ class PodcastsDatabaseService { Future removePodcast(Podcasts podcast) async { try { - final res = await _db.rawQuery( - 'SELECT COUNT(*) FROM episodes GROUP BY podcastId HAVING podcastId = ?', - [podcast.id], - ); - final count = Sqflite.firstIntValue(res); - if (count == 0 || count == null) { - await _db.delete( - podcastsTableName, - where: 'id = ?', - whereArgs: [podcast.id], - ); + // Check if podcast has episodes + int episodeCount = _episodes.where((e) => e.podcastId == podcast.id).length; + + if (episodeCount == 0) { + _podcasts.removeWhere((p) => p.id == podcast.id); + await _savePodcastsToFile(); log('Removed Podcast: ${podcast.title}'); } else { - log('Did not remove podcast: ${podcast.title} because it has $count episodes'); + log('Did not remove podcast: ${podcast.title} because it has $episodeCount episodes'); } } catch (e) { log('Could not remove the podcast: $e'); @@ -79,30 +163,31 @@ class PodcastsDatabaseService { // EPISODE QUERIES Future> getEpisodes(Podcasts podcast) async { - List> epsResults = await _db.query( - episodesTableName, - where: 'podcastId = ?', - whereArgs: [podcast.id], - ); - return epsResults.map((episode) => Episodes.fromDBJson(episode)).toList(); + return _episodes.where((e) => e.podcastId == podcast.id).toList(); } - Future getEpisode(String podcastId, String guid) async { - List> epResult = await _db.query( - episodesTableName, - where: 'podcastId = ? AND guid = ?', - whereArgs: [podcastId, guid], - ); - return Episodes.fromDBJson(epResult.first); + Future getEpisode(String podcastId, String guid) async { + try { + return _episodes.firstWhere( + (e) => e.podcastId == podcastId && e.id == guid, + ); + } catch (e) { + log('Episode not found: podcastId=$podcastId, guid=$guid'); + return null; + } } Future addEpisode(Episodes episode) async { try { - await _db.insert( - episodesTableName, - episode.toJson(), - conflictAlgorithm: ConflictAlgorithm.replace, - ); + // Check if episode already exists + bool exists = _episodes.any((e) => e.podcastId == episode.podcastId && e.id == episode.id); + if (exists) { + log('Episode already exists: ${episode.title}'); + return; + } + + _episodes.add(episode); + await _saveEpisodesToFile(); log('Added Episode: ${episode.title}'); } catch (e) { log('Could not insert the episode: $e'); @@ -111,11 +196,8 @@ class PodcastsDatabaseService { Future removeEpisode(Episodes episode) async { try { - await _db.delete( - episodesTableName, - where: 'podcastId = ? AND id = ?', - whereArgs: [episode.podcastId, episode.id], - ); + _episodes.removeWhere((e) => e.podcastId == episode.podcastId && e.id == episode.id); + await _saveEpisodesToFile(); log('Removed Episode: ${episode.title}'); } catch (e) { log('Could not remove the episode: $e'); @@ -123,11 +205,6 @@ class PodcastsDatabaseService { } Future episodeExists(Episodes episode) async { - List> epResult = await _db.query( - episodesTableName, - where: 'podcastId = ? AND id = ?', - whereArgs: [episode.podcastId, episode.id], - ); - return epResult.isNotEmpty; + return _episodes.any((e) => e.podcastId == episode.podcastId && e.id == episode.id); } } diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index 0ea603cf8..348477ce1 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -46,10 +46,7 @@ dependencies: quick_actions: 1.1.0 share_plus: 10.1.4 # NOTE: v11 has breaking changes shared_preferences: 2.5.3 # TODO: update deprecated code - sqflite: 2.4.2 - # TODO: Replace with sqflite methods as below package isn't actively maintained - sqflite_migration_service: 2.0.0-nullsafety.1 - stacked: 3.4.3 # NOTE: 3.4.4+ is blocked by sqflite_migration_service + stacked: 3.4.3 stacked_services: 1.6.0 timezone: 0.10.1 ua_client_hints: 1.4.1 @@ -70,7 +67,6 @@ dev_dependencies: integration_test: sdk: flutter mockito: 5.4.4 - sqflite_common_ffi: 2.3.5 stacked_generator: 1.6.1 flutter: @@ -86,7 +82,6 @@ flutter: assets: - .env - assets/images/ - - assets/sql/ - assets/test_data/news_post.json - assets/test_data/news_feed.json - assets/learn/ diff --git a/mobile-app/test/services/podcast/podcast_service_test.dart b/mobile-app/test/services/podcast/podcast_service_test.dart new file mode 100644 index 000000000..aafaaa3b1 --- /dev/null +++ b/mobile-app/test/services/podcast/podcast_service_test.dart @@ -0,0 +1,143 @@ +import 'dart:io'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:freecodecamp/models/podcasts/episodes_model.dart'; +import 'package:freecodecamp/models/podcasts/podcasts_model.dart'; +import 'package:freecodecamp/service/podcast/podcasts_service.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class MockPathProviderPlatform extends Fake + with MockPlatformInterfaceMixin + implements PathProviderPlatform { + @override + Future getApplicationDocumentsPath() async { + return Directory.systemTemp.createTempSync().path; + } +} + +void main() { + group('PodcastsDatabaseService Tests', () { + late PodcastsDatabaseService service; + late MockPathProviderPlatform mockPathProvider; + + setUp(() async { + mockPathProvider = MockPathProviderPlatform(); + PathProviderPlatform.instance = mockPathProvider; + service = PodcastsDatabaseService(); + await service.initialise(); + }); + + test('should initialize with empty lists', () async { + final podcasts = await service.getPodcasts(); + expect(podcasts, isEmpty); + }); + + test('should add and retrieve podcasts', () async { + final podcast = Podcasts( + id: 'test-id', + url: 'https://example.com/feed.xml', + title: 'Test Podcast', + description: 'A test podcast', + ); + + await service.addPodcast(podcast); + final podcasts = await service.getPodcasts(); + + expect(podcasts.length, 1); + expect(podcasts.first.id, 'test-id'); + expect(podcasts.first.title, 'Test Podcast'); + }); + + test('should not add duplicate podcasts', () async { + final podcast = Podcasts( + id: 'test-id', + url: 'https://example.com/feed.xml', + title: 'Test Podcast', + ); + + await service.addPodcast(podcast); + await service.addPodcast(podcast); + final podcasts = await service.getPodcasts(); + + expect(podcasts.length, 1); + }); + + test('should add and retrieve episodes', () async { + final podcast = Podcasts( + id: 'podcast-id', + url: 'https://example.com/feed.xml', + title: 'Test Podcast', + ); + + final episode = Episodes( + id: 'episode-id', + podcastId: 'podcast-id', + title: 'Test Episode', + description: 'A test episode', + ); + + await service.addPodcast(podcast); + await service.addEpisode(episode); + + final episodes = await service.getEpisodes(podcast); + expect(episodes.length, 1); + expect(episodes.first.id, 'episode-id'); + expect(episodes.first.title, 'Test Episode'); + }); + + test('should check if episode exists', () async { + final episode = Episodes( + id: 'episode-id', + podcastId: 'podcast-id', + title: 'Test Episode', + ); + + expect(await service.episodeExists(episode), false); + + await service.addEpisode(episode); + expect(await service.episodeExists(episode), true); + }); + + test('should remove episodes', () async { + final episode = Episodes( + id: 'episode-id', + podcastId: 'podcast-id', + title: 'Test Episode', + ); + + await service.addEpisode(episode); + expect(await service.episodeExists(episode), true); + + await service.removeEpisode(episode); + expect(await service.episodeExists(episode), false); + }); + + test('should remove podcast only if no episodes exist', () async { + final podcast = Podcasts( + id: 'podcast-id', + url: 'https://example.com/feed.xml', + title: 'Test Podcast', + ); + + final episode = Episodes( + id: 'episode-id', + podcastId: 'podcast-id', + title: 'Test Episode', + ); + + await service.addPodcast(podcast); + await service.addEpisode(episode); + + // Should not remove podcast because it has episodes + await service.removePodcast(podcast); + final podcasts = await service.getPodcasts(); + expect(podcasts.length, 1); + + // Remove episode first + await service.removeEpisode(episode); + await service.removePodcast(podcast); + final podcastsAfterRemoval = await service.getPodcasts(); + expect(podcastsAfterRemoval.length, 0); + }); + }); +} \ No newline at end of file From 49b1bf2a1224e4fabb59f7d9d14dddd8f5c20942 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 Aug 2025 13:19:02 +0000 Subject: [PATCH 11/14] Finalize podcast service tests and complete SQLite removal Co-authored-by: Sembauke <46919888+Sembauke@users.noreply.github.com> --- .../podcast/podcast_service_test.dart | 100 +++++++++++++++--- 1 file changed, 87 insertions(+), 13 deletions(-) diff --git a/mobile-app/test/services/podcast/podcast_service_test.dart b/mobile-app/test/services/podcast/podcast_service_test.dart index aafaaa3b1..02909aa28 100644 --- a/mobile-app/test/services/podcast/podcast_service_test.dart +++ b/mobile-app/test/services/podcast/podcast_service_test.dart @@ -4,29 +4,32 @@ import 'package:freecodecamp/models/podcasts/episodes_model.dart'; import 'package:freecodecamp/models/podcasts/podcasts_model.dart'; import 'package:freecodecamp/service/podcast/podcasts_service.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -class MockPathProviderPlatform extends Fake - with MockPlatformInterfaceMixin - implements PathProviderPlatform { - @override - Future getApplicationDocumentsPath() async { - return Directory.systemTemp.createTempSync().path; - } -} void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + group('PodcastsDatabaseService Tests', () { late PodcastsDatabaseService service; - late MockPathProviderPlatform mockPathProvider; + late Directory tempDir; setUp(() async { - mockPathProvider = MockPathProviderPlatform(); - PathProviderPlatform.instance = mockPathProvider; + // Create a temporary directory for testing + tempDir = await Directory.systemTemp.createTemp('podcast_test_'); + + // Mock path_provider to use our temp directory + PathProviderPlatform.instance = FakePathProviderPlatform(tempDir.path); + service = PodcastsDatabaseService(); await service.initialise(); }); + tearDown(() async { + // Clean up temporary directory + if (await tempDir.exists()) { + await tempDir.delete(recursive: true); + } + }); + test('should initialize with empty lists', () async { final podcasts = await service.getPodcasts(); expect(podcasts, isEmpty); @@ -139,5 +142,76 @@ void main() { final podcastsAfterRemoval = await service.getPodcasts(); expect(podcastsAfterRemoval.length, 0); }); + + test('should persist data between service instances', () async { + final podcast = Podcasts( + id: 'test-id', + url: 'https://example.com/feed.xml', + title: 'Test Podcast', + ); + + // Add podcast with first service instance + await service.addPodcast(podcast); + var podcasts = await service.getPodcasts(); + expect(podcasts.length, 1); + + // Create new service instance + final newService = PodcastsDatabaseService(); + await newService.initialise(); + + // Should load the previously saved podcast + podcasts = await newService.getPodcasts(); + expect(podcasts.length, 1); + expect(podcasts.first.id, 'test-id'); + }); }); +} + +// Mock PathProviderPlatform for testing +class FakePathProviderPlatform extends PathProviderPlatform { + final String tempPath; + + FakePathProviderPlatform(this.tempPath); + + @override + Future getApplicationDocumentsPath() async { + return tempPath; + } + + @override + Future getTemporaryPath() async { + return tempPath; + } + + @override + Future getApplicationSupportPath() async { + return tempPath; + } + + @override + Future getLibraryPath() async { + return tempPath; + } + + @override + Future getExternalStoragePath() async { + return tempPath; + } + + @override + Future?> getExternalCachePaths() async { + return [tempPath]; + } + + @override + Future?> getExternalStoragePaths({ + StorageDirectory? type, + }) async { + return [tempPath]; + } + + @override + Future getDownloadsPath() async { + return tempPath; + } } \ No newline at end of file From a119596fc00036637aa49301a70b438c36b896a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 Aug 2025 13:27:21 +0000 Subject: [PATCH 12/14] Add path_provider_platform_interface as dev dependency for test mocking Co-authored-by: Sembauke <46919888+Sembauke@users.noreply.github.com> --- mobile-app/pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index 348477ce1..3da20b7c2 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -67,6 +67,7 @@ dev_dependencies: integration_test: sdk: flutter mockito: 5.4.4 + path_provider_platform_interface: 2.1.2 stacked_generator: 1.6.1 flutter: From 179cac2a03b43842b1525179e2671b21f7bbd770 Mon Sep 17 00:00:00 2001 From: sembauke Date: Tue, 26 Aug 2025 15:30:23 +0200 Subject: [PATCH 13/14] fix: update dependency types in pubspec.lock for path_provider_platform_interface and sqflite --- mobile-app/pubspec.lock | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/mobile-app/pubspec.lock b/mobile-app/pubspec.lock index ce2e3eaac..5a16be78d 100644 --- a/mobile-app/pubspec.lock +++ b/mobile-app/pubspec.lock @@ -1285,7 +1285,7 @@ packages: source: hosted version: "2.2.1" path_provider_platform_interface: - dependency: transitive + dependency: "direct dev" description: name: path_provider_platform_interface sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" @@ -1578,7 +1578,7 @@ packages: source: hosted version: "7.0.0" sqflite: - dependency: "direct main" + dependency: transitive description: name: sqflite sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 @@ -1601,14 +1601,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.5.5" - sqflite_common_ffi: - dependency: "direct dev" - description: - name: sqflite_common_ffi - sha256: "1f3ef3888d3bfbb47785cc1dda0dc7dd7ebd8c1955d32a9e8e9dae1e38d1c4c1" - url: "https://pub.dev" - source: hosted - version: "2.3.5" sqflite_darwin: dependency: transitive description: @@ -1617,14 +1609,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.2" - sqflite_migration_service: - dependency: "direct main" - description: - name: sqflite_migration_service - sha256: "40d2d074ebd54fde5e476ef20eec8c6c8285b4bf89d3c9dd880dfcc3489b1355" - url: "https://pub.dev" - source: hosted - version: "2.0.0-nullsafety.1" sqflite_platform_interface: dependency: transitive description: @@ -1633,14 +1617,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" - sqlite3: - dependency: transitive - description: - name: sqlite3 - sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e" - url: "https://pub.dev" - source: hosted - version: "2.7.5" stack_trace: dependency: transitive description: From 885c53699c50277cd6ce69650d97d73b6fb98ee0 Mon Sep 17 00:00:00 2001 From: sembauke Date: Tue, 26 Aug 2025 15:32:47 +0200 Subject: [PATCH 14/14] fix: remove unused variable --- .../services/news/bookmark_service_test.dart | 53 +++++++++---------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/mobile-app/test/services/news/bookmark_service_test.dart b/mobile-app/test/services/news/bookmark_service_test.dart index feb9a90a7..ad6a5b49a 100644 --- a/mobile-app/test/services/news/bookmark_service_test.dart +++ b/mobile-app/test/services/news/bookmark_service_test.dart @@ -5,7 +5,6 @@ import 'package:freecodecamp/app/app.locator.dart'; import 'package:freecodecamp/models/news/bookmarked_tutorial_model.dart'; import 'package:freecodecamp/models/news/tutorial_model.dart'; import 'package:freecodecamp/service/news/bookmark_service.dart'; -import 'package:path/path.dart' as path; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import '../../helpers/test_helpers.dart'; @@ -37,19 +36,19 @@ final BookmarkedTutorial testBookmarkTutorial = BookmarkedTutorial( void main() { TestWidgetsFlutterBinding.ensureInitialized(); - + group('News Bookmark Service Test -', () { late Directory tempDir; - + setUp(() async { registerServices(); // Create a temporary directory for testing tempDir = await Directory.systemTemp.createTemp('bookmark_test_'); - + // Mock path_provider to use our temp directory PathProviderPlatform.instance = FakePathProviderPlatform(tempDir.path); }); - + tearDown(() async { locator.reset(); // Clean up temporary directory @@ -62,9 +61,7 @@ void main() { final service = locator(); await service.initialise(); expect(service, isA()); - - // Check that articles.json file is created in temp directory - String articlesPath = path.join(tempDir.path, 'articles.json'); + // File might not exist yet if no bookmarks are added expect(await Directory(tempDir.path).exists(), true); }); @@ -94,13 +91,13 @@ void main() { test('check if bookmark exists', () async { final service = locator(); await service.initialise(); - + // Initially should not be bookmarked expect(await service.isBookmarked(testTutorial), false); - + // Add bookmark await service.addBookmark(testTutorial); - + // Now should be bookmarked expect(await service.isBookmarked(testTutorial), true); }); @@ -108,12 +105,12 @@ void main() { test('delete bookmark', () async { final service = locator(); await service.initialise(); - + // Add bookmark first await service.addBookmark(testTutorial); var bookmarks = await service.getBookmarks(); expect(bookmarks.length, 1); - + // Remove bookmark await service.removeBookmark(testTutorial); bookmarks = await service.getBookmarks(); @@ -124,11 +121,11 @@ void main() { test('prevent duplicate bookmarks', () async { final service = locator(); await service.initialise(); - + // Add same bookmark twice await service.addBookmark(testTutorial); await service.addBookmark(testTutorial); - + final bookmarks = await service.getBookmarks(); expect(bookmarks.length, 1); // Should only have one bookmark }); @@ -138,17 +135,17 @@ void main() { final service1 = locator(); await service1.initialise(); await service1.addBookmark(testTutorial); - + var bookmarks = await service1.getBookmarks(); expect(bookmarks.length, 1); - + // Reset and create new service instance locator.reset(); registerServices(); - + final service2 = locator(); await service2.initialise(); - + // Should load the previously saved bookmark bookmarks = await service2.getBookmarks(); expect(bookmarks.length, 1); @@ -160,46 +157,46 @@ void main() { // Mock PathProviderPlatform for testing class FakePathProviderPlatform extends PathProviderPlatform { final String tempPath; - + FakePathProviderPlatform(this.tempPath); - + @override Future getApplicationDocumentsPath() async { return tempPath; } - + @override Future getTemporaryPath() async { return tempPath; } - + @override Future getApplicationSupportPath() async { return tempPath; } - + @override Future getLibraryPath() async { return tempPath; } - + @override Future getExternalStoragePath() async { return tempPath; } - + @override Future?> getExternalCachePaths() async { return [tempPath]; } - + @override Future?> getExternalStoragePaths({ StorageDirectory? type, }) async { return [tempPath]; } - + @override Future getDownloadsPath() async { return tempPath;