mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
Convert network folder to local
This commit is contained in:
@@ -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": "源收藏夾"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -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> {
|
||||||
|
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -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';
|
||||||
|
@@ -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(
|
||||||
|
@@ -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),
|
||||||
|
Reference in New Issue
Block a user