mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
initial commit
This commit is contained in:
487
lib/foundation/favorites.dart
Normal file
487
lib/foundation/favorites.dart
Normal file
@@ -0,0 +1,487 @@
|
||||
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) {
|
||||
if (_modifiedAfterLastCache) {
|
||||
_cacheFavoritedIds();
|
||||
}
|
||||
return _cachedFavoritedIds.containsKey(id);
|
||||
}
|
||||
|
||||
bool _modifiedAfterLastCache = true;
|
||||
|
||||
void _cacheFavoritedIds() {
|
||||
_modifiedAfterLastCache = false;
|
||||
_cachedFavoritedIds.clear();
|
||||
for (var folder in folderNames) {
|
||||
var res = _db.select("""
|
||||
select id from "$folder";
|
||||
""");
|
||||
for (var row in res) {
|
||||
_cachedFavoritedIds[row["id"]] = 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]);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user