Convert network folder to local

This commit is contained in:
2024-11-16 16:51:56 +08:00
parent 7bc0aeb4af
commit 30a1c806cd
7 changed files with 308 additions and 9 deletions

View File

@@ -212,7 +212,10 @@
"Select an image on screen": "选择屏幕上的图片", "Select an image on screen": "选择屏幕上的图片",
"Added @count comics to download queue.": "已添加 @count 本漫画到下载队列", "Added @count comics to download queue.": "已添加 @count 本漫画到下载队列",
"Ignore Certificate Errors": "忽略证书错误", "Ignore Certificate Errors": "忽略证书错误",
"Authorization Required": "需要身份验证" "Authorization Required": "需要身份验证",
"Sync": "同步",
"The folder is Linked to @source": "文件夹已关联到 @source",
"Source Folder": "源收藏夹"
}, },
"zh_TW": { "zh_TW": {
"Home": "首頁", "Home": "首頁",
@@ -427,6 +430,9 @@
"Select an image on screen": "選擇屏幕上的圖片", "Select an image on screen": "選擇屏幕上的圖片",
"Added @count comics to download queue.": "已添加 @count 本漫畫到下載隊列", "Added @count comics to download queue.": "已添加 @count 本漫畫到下載隊列",
"Ignore Certificate Errors": "忽略證書錯誤", "Ignore Certificate Errors": "忽略證書錯誤",
"Authorization Required": "需要身份驗證" "Authorization Required": "需要身份驗證",
"Sync": "同步",
"The folder is Linked to @source": "文件夾已關聯到 @source",
"Source Folder": "源收藏夾"
} }
} }

View File

@@ -51,6 +51,10 @@ class Flyout extends StatefulWidget {
@override @override
State<Flyout> createState() => FlyoutState(); State<Flyout> createState() => FlyoutState();
static FlyoutState of(BuildContext context) {
return context.findAncestorStateOfType<FlyoutState>()!;
}
} }
class FlyoutState extends State<Flyout> { class FlyoutState extends State<Flyout> {

View File

@@ -346,6 +346,32 @@ class LocalFavoritesManager with ChangeNotifier {
return name; return name;
} }
void linkFolderToNetwork(String folder, String source, String networkFolder) {
_db.execute("""
insert or replace into folder_sync (folder_name, source_key, source_folder)
values (?, ?, ?);
""", [folder, source, networkFolder]);
}
bool isLinkedToNetworkFolder(String folder, String source, String networkFolder) {
var res = _db.select("""
select * from folder_sync
where folder_name == ? and source_key == ? and source_folder == ?;
""", [folder, source, networkFolder]);
return res.isNotEmpty;
}
(String?, String?) findLinked(String folder) {
var res = _db.select("""
select * from folder_sync
where folder_name == ?;
""", [folder]);
if (res.isEmpty) {
return (null, null);
}
return (res.first["source_key"], res.first["source_folder"]);
}
bool comicExists(String folder, String id, ComicType type) { bool comicExists(String folder, String id, ComicType type) {
var res = _db.select(""" var res = _db.select("""
select * from "$folder" select * from "$folder"
@@ -365,20 +391,19 @@ class LocalFavoritesManager with ChangeNotifier {
return FavoriteItem.fromRow(res.first); return FavoriteItem.fromRow(res.first);
} }
/// add comic to a folder /// add comic to a folder.
/// /// return true if success, false if already exists
/// This method will download cover to local, to avoid problems like changing url bool addComic(String folder, FavoriteItem comic, [int? order]) {
void addComic(String folder, FavoriteItem comic, [int? order]) async {
_modifiedAfterLastCache = true; _modifiedAfterLastCache = true;
if (!existsFolder(folder)) { if (!existsFolder(folder)) {
throw Exception("Folder does not exists"); throw Exception("Folder does not exists");
} }
var res = _db.select(""" var res = _db.select("""
select * from "$folder" select * from "$folder"
where id == '${comic.id}'; where id == ? and type == ?;
"""); """, [comic.id, comic.type.value]);
if (res.isNotEmpty) { if (res.isNotEmpty) {
return; return false;
} }
final params = [ final params = [
comic.id, comic.id,
@@ -406,6 +431,7 @@ class LocalFavoritesManager with ChangeNotifier {
""", [...params, minValue(folder) - 1]); """, [...params, minValue(folder) - 1]);
} }
notifyListeners(); notifyListeners();
return true;
} }
/// delete a folder /// delete a folder
@@ -414,6 +440,10 @@ class LocalFavoritesManager with ChangeNotifier {
_db.execute(""" _db.execute("""
drop table "$name"; drop table "$name";
"""); """);
_db.execute("""
delete from folder_order
where folder_name == ?;
""", [name]);
notifyListeners(); notifyListeners();
} }
@@ -461,6 +491,16 @@ class LocalFavoritesManager with ChangeNotifier {
ALTER TABLE "$before" ALTER TABLE "$before"
RENAME TO "$after"; RENAME TO "$after";
"""); """);
_db.execute("""
update folder_order
set folder_name = ?
where folder_name == ?;
""", [after, before]);
_db.execute("""
update folder_sync
set folder_name = ?
where folder_name == ?;
""", [after, before]);
notifyListeners(); notifyListeners();
} }

View File

@@ -288,3 +288,178 @@ Future<void> sortFolders() async {
LocalFavoritesManager().updateOrder(folders); LocalFavoritesManager().updateOrder(folders);
} }
Future<void> importNetworkFolder(
String source,
String? folder,
String? folderID,
) async {
var comicSource = ComicSource.find(source);
if (comicSource == null) {
return;
}
if(folder != null && folder.isEmpty) {
folder = null;
}
var resultName = folder ?? comicSource.name;
var exists = LocalFavoritesManager().existsFolder(resultName);
if (exists) {
if (!LocalFavoritesManager()
.isLinkedToNetworkFolder(resultName, source, folderID ?? "")) {
App.rootContext.showMessage(message: "Folder already exists".tl);
return;
}
}
if(!exists) {
LocalFavoritesManager().createFolder(resultName);
LocalFavoritesManager().linkFolderToNetwork(
resultName,
source,
folderID ?? "",
);
}
var current = 0;
var isFinished = false;
String? next;
Future<void> fetchNext() async {
var retry = 3;
while (true) {
try {
if (comicSource.favoriteData?.loadComic != null) {
next ??= '1';
var page = int.parse(next!);
var res = await comicSource.favoriteData!.loadComic!(page, folderID);
var count = 0;
for (var c in res.data) {
var result = LocalFavoritesManager().addComic(
resultName,
FavoriteItem(
id: c.id,
name: c.title,
coverPath: c.cover,
type: ComicType(source.hashCode),
author: c.subtitle ?? '',
tags: c.tags ?? [],
),
);
if (result) {
count++;
}
}
current += count;
if (res.data.isEmpty || res.subData == page) {
isFinished = true;
next = null;
} else {
next = (page + 1).toString();
}
} else if (comicSource.favoriteData?.loadNext != null) {
var res = await comicSource.favoriteData!.loadNext!(next, folderID);
var count = 0;
for (var c in res.data) {
var result = LocalFavoritesManager().addComic(
resultName,
FavoriteItem(
id: c.id,
name: c.title,
coverPath: c.cover,
type: ComicType(source.hashCode),
author: c.subtitle ?? '',
tags: c.tags ?? [],
),
);
if (result) {
count++;
}
}
current += count;
if (res.data.isEmpty || res.subData == null) {
isFinished = true;
next = null;
} else {
next = res.subData;
}
} else {
throw "Unsupported source";
}
return;
} catch (e) {
retry--;
if (retry == 0) {
rethrow;
}
continue;
}
}
}
bool isCanceled = false;
String? errorMsg;
bool isErrored() => errorMsg != null;
void Function()? updateDialog;
showDialog(
context: App.rootContext,
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
updateDialog = () => setState(() {});
return ContentDialog(
title: isFinished
? "Finished".tl
: isErrored()
? "Error".tl
: "Importing".tl,
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
LinearProgressIndicator(
value: isFinished ? 1 : null,
),
const SizedBox(height: 4),
Text("Imported @c comics".tlParams({
"c": current,
})),
const SizedBox(height: 4),
if (isErrored()) Text("Error: $errorMsg"),
],
).paddingHorizontal(16),
actions: [
Button.filled(
color: (isFinished || isErrored())
? null
: context.colorScheme.error,
onPressed: () {
isCanceled = true;
context.pop();
},
child: (isFinished || isErrored())
? Text("OK".tl)
: Text("Cancel".tl),
),
],
);
},
);
},
).then((_) {
isCanceled = true;
});
while (!isFinished && !isCanceled) {
try {
await fetchNext();
updateDialog?.call();
} catch (e) {
errorMsg = e.toString();
updateDialog?.call();
break;
}
}
}

View File

@@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart'; import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart';
import 'package:venera/components/components.dart'; import 'package:venera/components/components.dart';

View File

@@ -14,6 +14,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
late List<FavoriteItem> comics; late List<FavoriteItem> comics;
String? networkSource;
String? networkFolder;
void updateComics() { void updateComics() {
setState(() { setState(() {
comics = LocalFavoritesManager().getAllComics(widget.folder); comics = LocalFavoritesManager().getAllComics(widget.folder);
@@ -24,6 +27,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
void initState() { void initState() {
favPage = context.findAncestorStateOfType<_FavoritesPageState>()!; favPage = context.findAncestorStateOfType<_FavoritesPageState>()!;
comics = LocalFavoritesManager().getAllComics(widget.folder); comics = LocalFavoritesManager().getAllComics(widget.folder);
var (a, b) = LocalFavoritesManager().findLinked(widget.folder);
networkSource = a;
networkFolder = b;
super.initState(); super.initState();
} }
@@ -49,6 +55,51 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
child: Text(favPage.folder ?? "Unselected".tl), child: Text(favPage.folder ?? "Unselected".tl),
), ),
actions: [ actions: [
if (networkSource != null)
Tooltip(
message: "Sync".tl,
child: Flyout(
flyoutBuilder: (context) {
var sourceName = ComicSource.find(networkSource!)?.name ??
networkSource!;
var text = "The folder is Linked to @source".tlParams({
"source": sourceName,
});
if(networkFolder != null && networkFolder!.isNotEmpty) {
text += "\n${"Source Folder".tl}: $networkFolder";
}
return FlyoutContent(
title: "Sync".tl,
content: Text(text),
actions: [
Button.filled(
child: Text("Update".tl),
onPressed: () {
context.pop();
importNetworkFolder(
networkSource!,
widget.folder,
networkFolder!,
).then(
(value) {
updateComics();
},
);
},
),
],
);
},
child: Builder(builder: (context) {
return IconButton(
icon: const Icon(Icons.sync),
onPressed: () {
Flyout.of(context).show();
},
);
}),
),
),
MenuButton( MenuButton(
entries: [ entries: [
MenuEntry( MenuEntry(

View File

@@ -108,6 +108,17 @@ class _NormalFavoritePageState extends State<_NormalFavoritePage> {
onTap: context.width < _kTwoPanelChangeWidth ? showFolders : null, onTap: context.width < _kTwoPanelChangeWidth ? showFolders : null,
child: Text(widget.data.title), child: Text(widget.data.title),
), ),
actions: [
MenuButton(entries: [
MenuEntry(
icon: Icons.sync,
text: "Convert to local".tl,
onClick: () {
importNetworkFolder(widget.data.key, null, null);
},
)
]),
],
), ),
errorLeading: Appbar( errorLeading: Appbar(
leading: Tooltip( leading: Tooltip(
@@ -533,6 +544,17 @@ class _FavoriteFolder extends StatelessWidget {
key: comicListKey, key: comicListKey,
leadingSliver: SliverAppbar( leadingSliver: SliverAppbar(
title: Text(title), title: Text(title),
actions: [
MenuButton(entries: [
MenuEntry(
icon: Icons.sync,
text: "Convert to local".tl,
onClick: () {
importNetworkFolder(data.key, title, folderID);
},
)
]),
],
), ),
errorLeading: Appbar( errorLeading: Appbar(
title: Text(title), title: Text(title),