import 'package:sqlite3/sqlite3.dart'; import 'package:venera/foundation/appdata.dart'; import 'dart:io'; import 'app.dart'; import 'comic_source/comic_source.dart'; import 'comic_type.dart'; String _getCurTime() { return DateTime.now() .toIso8601String() .replaceFirst("T", " ") .substring(0, 19); } class FavoriteItem implements Comic { String name; String author; ComicType type; @override List tags; @override String id; String coverPath; String time = _getCurTime(); FavoriteItem({ required this.id, required this.name, required this.coverPath, required this.author, required this.type, required this.tags, }); FavoriteItem.fromRow(Row row) : name = row["name"], author = row["author"], type = ComicType(row["type"]), tags = (row["tags"] as String).split(","), id = row["id"], coverPath = row["cover_path"], time = row["time"] { tags.remove(""); } @override bool operator ==(Object other) { return other is FavoriteItem && other.id == id && other.type == type; } @override int get hashCode => id.hashCode ^ type.hashCode; @override String toString() { var s = "FavoriteItem: $name $author $coverPath $hashCode $tags"; if (s.length > 100) { return s.substring(0, 100); } return s; } @override String get cover => coverPath; @override String get description { return "$time | ${type == ComicType.local ? 'local' : type.comicSource?.name ?? "Unknown"}"; } @override String? get favoriteId => null; @override String? get language => null; @override int? get maxPage => null; @override String get sourceKey => type == ComicType.local ? 'local' : type.comicSource?.key ?? "Unknown:${type.value}"; @override double? get stars => null; @override String? get subtitle => author; @override String get title => name; @override Map toJson() { throw UnimplementedError(); } } class FavoriteItemWithFolderInfo { FavoriteItem comic; String folder; FavoriteItemWithFolderInfo(this.comic, this.folder); @override bool operator ==(Object other) { return other is FavoriteItemWithFolderInfo && other.comic == comic && other.folder == folder; } @override int get hashCode => comic.hashCode ^ folder.hashCode; } class LocalFavoritesManager { factory LocalFavoritesManager() => cache ?? (cache = LocalFavoritesManager._create()); LocalFavoritesManager._create(); static LocalFavoritesManager? cache; late Database _db; Future init() async { _db = sqlite3.open("${App.dataPath}/local_favorite.db"); _db.execute(""" create table if not exists folder_order ( folder_name text primary key, order_value int ); """); } List find(String id, ComicType type) { var res = []; for (var folder in folderNames) { var rows = _db.select(""" select * from "$folder" where id == ? and type == ?; """, [id, type.value]); if (rows.isNotEmpty) { res.add(folder); } } return res; } Future> findWithModel(FavoriteItem item) async { var res = []; for (var folder in folderNames) { var rows = _db.select(""" select * from "$folder" where id == ? and type == ?; """, [item.id, item.type.value]); if (rows.isNotEmpty) { res.add(folder); } } return res; } List _getTablesWithDB() { final tables = _db .select("SELECT name FROM sqlite_master WHERE type='table';") .map((element) => element["name"] as String) .toList(); return tables; } List _getFolderNamesWithDB() { final folders = _getTablesWithDB(); folders.remove('folder_sync'); folders.remove('folder_order'); var folderToOrder = {}; for (var folder in folders) { var res = _db.select(""" select * from folder_order where folder_name == ?; """, [folder]); if (res.isNotEmpty) { folderToOrder[folder] = res.first["order_value"]; } else { folderToOrder[folder] = 0; } } folders.sort((a, b) { return folderToOrder[a]! - folderToOrder[b]!; }); return folders; } void updateOrder(Map order) { for (var folder in order.keys) { _db.execute(""" insert or replace into folder_order (folder_name, order_value) values (?, ?); """, [folder, order[folder]]); } } int count(String folderName) { return _db.select(""" select count(*) as c from "$folderName" """).first["c"]; } List get folderNames => _getFolderNamesWithDB(); int maxValue(String folder) { return _db.select(""" SELECT MAX(display_order) AS max_value FROM "$folder"; """).firstOrNull?["max_value"] ?? 0; } int minValue(String folder) { return _db.select(""" SELECT MIN(display_order) AS min_value FROM "$folder"; """).firstOrNull?["min_value"] ?? 0; } List getAllComics(String folder) { var rows = _db.select(""" select * from "$folder" ORDER BY display_order; """); return rows.map((element) => FavoriteItem.fromRow(element)).toList(); } void addTagTo(String folder, String id, String tag) { _db.execute(""" update "$folder" set tags = '$tag,' || tags where id == ? """, [id]); } List allComics() { var res = []; for (final folder in folderNames) { var comics = _db.select(""" select * from "$folder"; """); res.addAll(comics.map((element) => FavoriteItemWithFolderInfo(FavoriteItem.fromRow(element), folder))); } return res; } /// create a folder String createFolder(String name, [bool renameWhenInvalidName = false]) { if (name.isEmpty) { if (renameWhenInvalidName) { int i = 0; while (folderNames.contains(i.toString())) { i++; } name = i.toString(); } else { throw "name is empty!"; } } if (folderNames.contains(name)) { if (renameWhenInvalidName) { var prevName = name; int i = 0; while (folderNames.contains(i.toString())) { i++; } name = prevName + i.toString(); } else { throw Exception("Folder is existing"); } } _db.execute(""" create table "$name"( id text, name TEXT, author TEXT, type int, tags TEXT, cover_path TEXT, time TEXT, display_order int, primary key (id, type) ); """); return name; } bool comicExists(String folder, String id, ComicType type) { var res = _db.select(""" select * from "$folder" where id == ? and type == ?; """, [id, type.value]); return res.isNotEmpty; } FavoriteItem getComic(String folder, String id, ComicType type) { var res = _db.select(""" select * from "$folder" where id == ? and type == ?; """, [id, type.value]); if (res.isEmpty) { throw Exception("Comic not found"); } return FavoriteItem.fromRow(res.first); } /// add comic to a folder /// /// This method will download cover to local, to avoid problems like changing url void addComic(String folder, FavoriteItem comic, [int? order]) async { _modifiedAfterLastCache = true; if (!folderNames.contains(folder)) { throw Exception("Folder does not exists"); } var res = _db.select(""" select * from "$folder" where id == '${comic.id}'; """); if (res.isNotEmpty) { return; } final params = [ comic.id, comic.name, comic.author, comic.type.value, comic.tags.join(","), comic.coverPath, comic.time ]; if (order != null) { _db.execute(""" insert into "$folder" (id, name, author, type, tags, cover_path, time, display_order) values (?, ?, ?, ?, ?, ?, ?, ?); """, [...params, order]); } else if (appdata.settings['newFavoriteAddTo'] == "end") { _db.execute(""" insert into "$folder" (id, name, author, type, tags, cover_path, time, display_order) values (?, ?, ?, ?, ?, ?, ?, ?); """, [...params, maxValue(folder) + 1]); } else { _db.execute(""" insert into "$folder" (id, name, author, type, tags, cover_path, time, display_order) values (?, ?, ?, ?, ?, ?, ?, ?); """, [...params, minValue(folder) - 1]); } } /// delete a folder void deleteFolder(String name) { _modifiedAfterLastCache = true; _db.execute(""" drop table "$name"; """); } void deleteComic(String folder, FavoriteItem comic) { _modifiedAfterLastCache = true; deleteComicWithId(folder, comic.id, comic.type); } void deleteComicWithId(String folder, String id, ComicType type) { _modifiedAfterLastCache = true; _db.execute(""" delete from "$folder" where id == ? and type == ?; """, [id, type.value]); } Future clearAll() async { _db.dispose(); File("${App.dataPath}/local_favorite.db").deleteSync(); await init(); } void reorder(List newFolder, String folder) async { if (!folderNames.contains(folder)) { throw Exception("Failed to reorder: folder not found"); } deleteFolder(folder); createFolder(folder); for (int i = 0; i < newFolder.length; i++) { addComic(folder, newFolder[i], i); } } void rename(String before, String after) { if (folderNames.contains(after)) { throw "Name already exists!"; } if (after.contains('"')) { throw "Invalid name"; } _db.execute(""" ALTER TABLE "$before" RENAME TO "$after"; """); } void onReadEnd(String id, ComicType type) async { _modifiedAfterLastCache = true; for (final folder in folderNames) { var rows = _db.select(""" select * from "$folder" where id == ? and type == ?; """, [id, type.value]); if (rows.isNotEmpty) { var newTime = DateTime.now() .toIso8601String() .replaceFirst("T", " ") .substring(0, 19); String updateLocationSql = ""; if (appdata.settings['moveFavoriteAfterRead'] == "end") { int maxValue = _db.select(""" SELECT MAX(display_order) AS max_value FROM "$folder"; """).firstOrNull?["max_value"] ?? 0; updateLocationSql = "display_order = ${maxValue + 1},"; } else if (appdata.settings['moveFavoriteAfterRead'] == "start") { int minValue = _db.select(""" SELECT MIN(display_order) AS min_value FROM "$folder"; """).firstOrNull?["min_value"] ?? 0; updateLocationSql = "display_order = ${minValue - 1},"; } _db.execute(""" UPDATE "$folder" SET $updateLocationSql time = ? WHERE id == ?; """, [newTime, id]); } } } List search(String keyword) { var keywordList = keyword.split(" "); keyword = keywordList.first; var comics = []; for (var table in folderNames) { keyword = "%$keyword%"; var res = _db.select(""" SELECT * FROM "$table" WHERE name LIKE ? OR author LIKE ? OR tags LIKE ?; """, [keyword, keyword, keyword]); for (var comic in res) { comics.add( FavoriteItemWithFolderInfo(FavoriteItem.fromRow(comic), table)); } if (comics.length > 200) { break; } } bool test(FavoriteItemWithFolderInfo comic, String keyword) { if (comic.comic.name.contains(keyword)) { return true; } else if (comic.comic.author.contains(keyword)) { return true; } else if (comic.comic.tags.any((element) => element.contains(keyword))) { return true; } return false; } for (var i = 1; i < keywordList.length; i++) { comics = comics.where((element) => test(element, keywordList[i])).toList(); } return comics; } void editTags(String id, String folder, List tags) { _db.execute(""" update "$folder" set tags = ? where id == ?; """, [tags.join(","), id]); } final _cachedFavoritedIds = {}; bool isExist(String id, ComicType type) { if (_modifiedAfterLastCache) { _cacheFavoritedIds(); } return _cachedFavoritedIds.containsKey("$id@${type.value}"); } bool _modifiedAfterLastCache = true; void _cacheFavoritedIds() { _modifiedAfterLastCache = false; _cachedFavoritedIds.clear(); for (var folder in folderNames) { var rows = _db.select(""" select id, type from "$folder"; """); for (var row in rows) { _cachedFavoritedIds["${row["id"]}@${row["type"]}"] = true; } } } void updateInfo(String folder, FavoriteItem comic) { _db.execute(""" update "$folder" set name = ?, author = ?, cover_path = ?, tags = ? where id == ? and type == ?; """, [ comic.name, comic.author, comic.coverPath, comic.tags.join(","), comic.id, comic.type.value ]); } }