Files
venera/lib/foundation/favorites.dart
2024-10-01 16:37:49 +08:00

488 lines
13 KiB
Dart

import 'package:sqlite3/sqlite3.dart';
import 'package:venera/foundation/appdata.dart';
import 'dart:io';
import 'app.dart';
import 'comic_type.dart';
String _getCurTime() {
return DateTime.now()
.toIso8601String()
.replaceFirst("T", " ")
.substring(0, 19);
}
class FavoriteItem {
String name;
String author;
ComicType type;
List<String> tags;
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;
}
}
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<void> 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
);
""");
}
Future<List<String>> find(String id, ComicType type) async {
var res = <String>[];
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<List<String>> findWithModel(FavoriteItem item) async {
var res = <String>[];
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<String> _getTablesWithDB() {
final tables = _db
.select("SELECT name FROM sqlite_master WHERE type='table';")
.map((element) => element["name"] as String)
.toList();
return tables;
}
List<String> _getFolderNamesWithDB() {
final folders = _getTablesWithDB();
folders.remove('folder_sync');
folders.remove('folder_order');
var folderToOrder = <String, int>{};
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<String, int> 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<String> 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<FavoriteItem> 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<FavoriteItemWithFolderInfo> allComics() {
var res = <FavoriteItemWithFolderInfo>[];
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("""
delete from folder_sync where folder_name == ?;
""", [name]);
_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<void> clearAll() async {
_db.dispose();
File("${App.dataPath}/local_favorite.db").deleteSync();
await init();
}
void reorder(List<FavoriteItem> 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<FavoriteItemWithFolderInfo> search(String keyword) {
var keywordList = keyword.split(" ");
keyword = keywordList.first;
var comics = <FavoriteItemWithFolderInfo>[];
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<String> tags) {
_db.execute("""
update "$folder"
set tags = ?
where id == ?;
""", [tags.join(","), id]);
}
final _cachedFavoritedIds = <String, bool>{};
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]);
}
}