Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 0 additions & 25 deletions mobile-app/assets/sql/1_create_db_schema.sql

This file was deleted.

2 changes: 0 additions & 2 deletions mobile-app/assets/sql/2_delete_all_values.sql

This file was deleted.

13 changes: 0 additions & 13 deletions mobile-app/assets/sql/3_reset_episodes_schema.sql

This file was deleted.

2 changes: 0 additions & 2 deletions mobile-app/assets/sql/4_delete_all_values.sql

This file was deleted.

30 changes: 17 additions & 13 deletions mobile-app/integration_test/news/bookmark_test.dart
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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<Map<String, dynamic>> 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<dynamic> 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]);
});
}
2 changes: 0 additions & 2 deletions mobile-app/lib/app/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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),
Expand Down
2 changes: 0 additions & 2 deletions mobile-app/lib/app/app.locator.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 18 additions & 6 deletions mobile-app/lib/models/news/bookmarked_tutorial_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ class BookmarkedTutorial {
late String tutorialText;
late String authorName;

BookmarkedTutorial.fromMap(Map<String, dynamic> map) {
bookmarkId = map['bookmark_id'];
tutorialTitle = map['articleTitle'];
id = map['articleId'];
tutorialText = map['articleText'];
authorName = map['authorName'];
// Constructor for JSON serialization
BookmarkedTutorial.fromJson(Map<String, dynamic> json) {
bookmarkId = json['bookmarkId'] ?? 0;
tutorialTitle = json['tutorialTitle'] ?? '';
id = json['id'] ?? '';
tutorialText = json['tutorialText'] ?? '';
authorName = json['authorName'] ?? '';
}

BookmarkedTutorial({
Expand All @@ -20,4 +21,15 @@ class BookmarkedTutorial {
required this.tutorialText,
required this.authorName,
});

// Convert to JSON for file storage
Map<String, dynamic> toJson() {
return {
'bookmarkId': bookmarkId,
'tutorialTitle': tutorialTitle,
'id': id,
'tutorialText': tutorialText,
'authorName': authorName,
};
}
}
7 changes: 3 additions & 4 deletions mobile-app/lib/models/podcasts/episodes_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, dynamic> toJson() => {
Expand All @@ -51,7 +50,7 @@ class Episodes {
'description': description,
'publicationDate': publicationDate?.toIso8601String(),
'contentUrl': contentUrl,
'duration': duration.toString(),
'duration': duration?.toString(),
};

@override
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion mobile-app/lib/models/podcasts/podcasts_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
149 changes: 93 additions & 56 deletions mobile-app/lib/service/news/bookmark_service.dart
Original file line number Diff line number Diff line change
@@ -1,50 +1,84 @@
import 'dart:async';
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:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart';

const String bookmarksTableName = 'bookmarks';
const String articlesFileName = 'articles.json';

class BookmarksDatabaseService {
late Database _db;
List<BookmarkedTutorial> _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));

// 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 _loadBookmarksFromFile() async {
try {
if (_articlesFile == null || !await _articlesFile!.exists()) {
log('Articles file does not exist, starting with empty bookmarks');
_bookmarks = [];
return;
}

ByteData data = await rootBundle.load(
path.join(
'assets',
'database',
'bookmarked-article.db',
),
);
List<int> bytes = data.buffer.asUint8List(
data.offsetInBytes,
data.lengthInBytes,
);
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<dynamic> 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 = [];
}
}

_db = await openDatabase(dbPathTutorials, version: 1);
Future _saveBookmarksToFile() async {
try {
if (_articlesFile == null) {
log('Articles file not initialized');
return;
}

// Ensure the directory exists
await _articlesFile!.parent.create(recursive: true);

List<Map<String, dynamic>> 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<String, dynamic> tutorialToMap(dynamic tutorial) {
Expand All @@ -70,34 +104,34 @@ class BookmarksDatabaseService {
}

Future<List<BookmarkedTutorial>> getBookmarks() async {
List<Map<String, dynamic>> 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<bool> isBookmarked(dynamic tutorial) async {
List<Map<String, dynamic>> 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<String, dynamic> 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');
Expand All @@ -106,12 +140,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');
}
Expand Down
Loading
Loading