mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
fix & improve importing comic
This commit is contained in:
@@ -8,7 +8,6 @@ import android.content.pm.PackageManager
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.DocumentsContract
|
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import androidx.activity.result.ActivityResultCallback
|
import androidx.activity.result.ActivityResultCallback
|
||||||
@@ -96,11 +95,7 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
if (pickedDirectoryUri == null)
|
if (pickedDirectoryUri == null)
|
||||||
res.success(null)
|
res.success(null)
|
||||||
else
|
else
|
||||||
try {
|
onPickedDirectory(pickedDirectoryUri, res)
|
||||||
res.success(onPickedDirectory(pickedDirectoryUri))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
res.error("Failed to Copy Files", e.toString(), null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,8 +129,9 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val selectFileChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "venera/select_file")
|
val selectFileChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "venera/select_file")
|
||||||
selectFileChannel.setMethodCallHandler { _, res ->
|
selectFileChannel.setMethodCallHandler { req, res ->
|
||||||
openFile(res)
|
val mimeType = req.arguments<String>()
|
||||||
|
openFile(res, mimeType!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,26 +162,40 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
return super.onKeyDown(keyCode, event)
|
return super.onKeyDown(keyCode, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// copy the directory to tmp directory, return copied directory
|
/// Ensure that the directory is accessible by dart:io
|
||||||
private fun onPickedDirectory(uri: Uri): String {
|
private fun onPickedDirectory(uri: Uri, result: MethodChannel.Result) {
|
||||||
if (!hasStoragePermission()) {
|
if (hasStoragePermission()) {
|
||||||
// dart:io cannot access the directory without permission.
|
var plain = uri.toString()
|
||||||
// so we need to copy the directory to cache directory
|
if(plain.contains("%3A")) {
|
||||||
val contentResolver = contentResolver
|
plain = Uri.decode(plain)
|
||||||
var tmp = cacheDir
|
}
|
||||||
tmp = File(tmp, "getDirectoryPathTemp")
|
val externalStoragePrefix = "content://com.android.externalstorage.documents/tree/primary:";
|
||||||
tmp.mkdir()
|
if(plain.startsWith(externalStoragePrefix)) {
|
||||||
Thread {
|
val path = plain.substring(externalStoragePrefix.length)
|
||||||
copyDirectory(contentResolver, uri, tmp)
|
result.success(Environment.getExternalStorageDirectory().absolutePath + "/" + path)
|
||||||
}.start()
|
}
|
||||||
|
// The uri cannot be parsed to plain path, use copy method
|
||||||
return tmp.absolutePath
|
|
||||||
} else {
|
|
||||||
val docId = DocumentsContract.getTreeDocumentId(uri)
|
|
||||||
val split: Array<String?> = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
|
||||||
return if ((split.size >= 2) && (split[1] != null)) split[1]!!
|
|
||||||
else File.separator
|
|
||||||
}
|
}
|
||||||
|
// dart:io cannot access the directory without permission.
|
||||||
|
// so we need to copy the directory to cache directory
|
||||||
|
val contentResolver = contentResolver
|
||||||
|
var tmp = cacheDir
|
||||||
|
var dirName = DocumentFile.fromTreeUri(this, uri)?.name
|
||||||
|
tmp = File(tmp, dirName!!)
|
||||||
|
if(tmp.exists()) {
|
||||||
|
tmp.deleteRecursively()
|
||||||
|
}
|
||||||
|
tmp.mkdir()
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
copyDirectory(contentResolver, uri, tmp)
|
||||||
|
result.success(tmp.absolutePath)
|
||||||
|
}
|
||||||
|
catch (e: Exception) {
|
||||||
|
result.error("copy error", e.message, null)
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copyDirectory(resolver: ContentResolver, srcUri: Uri, destDir: File) {
|
private fun copyDirectory(resolver: ContentResolver, srcUri: Uri, destDir: File) {
|
||||||
@@ -197,11 +207,12 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
copyDirectory(resolver, file.uri, newDir)
|
copyDirectory(resolver, file.uri, newDir)
|
||||||
} else {
|
} else {
|
||||||
val newFile = File(destDir, file.name!!)
|
val newFile = File(destDir, file.name!!)
|
||||||
val inputStream = resolver.openInputStream(file.uri) ?: return
|
resolver.openInputStream(file.uri)?.use { input ->
|
||||||
val outputStream = FileOutputStream(newFile)
|
FileOutputStream(newFile).use { output ->
|
||||||
inputStream.copyTo(outputStream)
|
input.copyTo(output, bufferSize = DEFAULT_BUFFER_SIZE)
|
||||||
inputStream.close()
|
output.flush()
|
||||||
outputStream.close()
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -277,10 +288,10 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openFile(result: MethodChannel.Result) {
|
private fun openFile(result: MethodChannel.Result, mimeType: String) {
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
intent.type = "*/*"
|
intent.type = mimeType
|
||||||
startContractForResult(ActivityResultContracts.StartActivityForResult(), intent){ activityResult ->
|
startContractForResult(ActivityResultContracts.StartActivityForResult(), intent){ activityResult ->
|
||||||
if (activityResult.resultCode != Activity.RESULT_OK) {
|
if (activityResult.resultCode != Activity.RESULT_OK) {
|
||||||
result.success(null)
|
result.success(null)
|
||||||
@@ -312,20 +323,9 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// copy file to cache directory
|
// use copy method
|
||||||
val cacheDir = cacheDir
|
val filePath = FileUtils.getPathFromCopyOfFileFromUri(this, uri)
|
||||||
val newFile = File(cacheDir, fileName)
|
result.success(filePath)
|
||||||
val inputStream = contentResolver.openInputStream(uri)
|
|
||||||
if (inputStream == null) {
|
|
||||||
result.success(null)
|
|
||||||
return@startContractForResult
|
|
||||||
}
|
|
||||||
val outputStream = FileOutputStream(newFile)
|
|
||||||
inputStream.copyTo(outputStream)
|
|
||||||
inputStream.close()
|
|
||||||
outputStream.close()
|
|
||||||
// send file path to flutter
|
|
||||||
result.success(newFile.absolutePath)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -231,7 +231,8 @@
|
|||||||
"Please check your settings": "请检查您的设置",
|
"Please check your settings": "请检查您的设置",
|
||||||
"No Category Pages": "没有分类页面",
|
"No Category Pages": "没有分类页面",
|
||||||
"Chapter @ep": "第 @ep 章",
|
"Chapter @ep": "第 @ep 章",
|
||||||
"Page @page": "第 @page 页"
|
"Page @page": "第 @page 页",
|
||||||
|
"Also remove files on disk": "同时删除磁盘上的文件"
|
||||||
},
|
},
|
||||||
"zh_TW": {
|
"zh_TW": {
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
@@ -465,6 +466,7 @@
|
|||||||
"Please check your settings": "請檢查您的設定",
|
"Please check your settings": "請檢查您的設定",
|
||||||
"No Category Pages": "沒有分類頁面",
|
"No Category Pages": "沒有分類頁面",
|
||||||
"Chapter @ep": "第 @ep 章",
|
"Chapter @ep": "第 @ep 章",
|
||||||
"Page @page": "第 @page 頁"
|
"Page @page": "第 @page 頁",
|
||||||
|
"Also remove files on disk": "同時刪除磁盤上的文件"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -453,7 +453,6 @@ class LocalManager with ChangeNotifier {
|
|||||||
if(HistoryManager().findSync(c.id, c.comicType) != null) {
|
if(HistoryManager().findSync(c.id, c.comicType) != null) {
|
||||||
HistoryManager().remove(c.id, c.comicType);
|
HistoryManager().remove(c.id, c.comicType);
|
||||||
}
|
}
|
||||||
assert(c.comicType == ComicType.local);
|
|
||||||
var folders = LocalFavoritesManager().find(c.id, c.comicType);
|
var folders = LocalFavoritesManager().find(c.id, c.comicType);
|
||||||
for (var f in folders) {
|
for (var f in folders) {
|
||||||
LocalFavoritesManager().deleteComicWithId(f, c.id, c.comicType);
|
LocalFavoritesManager().deleteComicWithId(f, c.id, c.comicType);
|
||||||
|
@@ -10,6 +10,7 @@ import 'package:venera/pages/comic_source_page.dart';
|
|||||||
import 'package:venera/pages/main_page.dart';
|
import 'package:venera/pages/main_page.dart';
|
||||||
import 'package:venera/pages/settings/settings_page.dart';
|
import 'package:venera/pages/settings/settings_page.dart';
|
||||||
import 'package:venera/utils/app_links.dart';
|
import 'package:venera/utils/app_links.dart';
|
||||||
|
import 'package:venera/utils/io.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
import 'components/components.dart';
|
import 'components/components.dart';
|
||||||
import 'components/window_frame.dart';
|
import 'components/window_frame.dart';
|
||||||
@@ -81,7 +82,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
if(!App.isMobile) {
|
if (!App.isMobile) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state == AppLifecycleState.inactive && hideContentOverlay == null) {
|
if (state == AppLifecycleState.inactive && hideContentOverlay == null) {
|
||||||
@@ -104,7 +105,8 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
if (state == AppLifecycleState.hidden &&
|
if (state == AppLifecycleState.hidden &&
|
||||||
appdata.settings['authorizationRequired'] &&
|
appdata.settings['authorizationRequired'] &&
|
||||||
!isAuthPageActive) {
|
!isAuthPageActive &&
|
||||||
|
!IO.isSelectingFiles) {
|
||||||
isAuthPageActive = true;
|
isAuthPageActive = true;
|
||||||
App.rootContext.to(
|
App.rootContext.to(
|
||||||
() => AuthPage(
|
() => AuthPage(
|
||||||
|
@@ -4,26 +4,21 @@ import 'package:sliver_tools/sliver_tools.dart';
|
|||||||
import 'package:venera/components/components.dart';
|
import 'package:venera/components/components.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||||
import 'package:venera/foundation/comic_type.dart';
|
|
||||||
import 'package:venera/foundation/consts.dart';
|
import 'package:venera/foundation/consts.dart';
|
||||||
import 'package:venera/foundation/favorites.dart';
|
import 'package:venera/foundation/favorites.dart';
|
||||||
import 'package:venera/foundation/history.dart';
|
import 'package:venera/foundation/history.dart';
|
||||||
import 'package:venera/foundation/image_provider/cached_image.dart';
|
import 'package:venera/foundation/image_provider/cached_image.dart';
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
|
||||||
import 'package:venera/pages/accounts_page.dart';
|
import 'package:venera/pages/accounts_page.dart';
|
||||||
import 'package:venera/pages/comic_page.dart';
|
import 'package:venera/pages/comic_page.dart';
|
||||||
import 'package:venera/pages/comic_source_page.dart';
|
import 'package:venera/pages/comic_source_page.dart';
|
||||||
import 'package:venera/pages/downloading_page.dart';
|
import 'package:venera/pages/downloading_page.dart';
|
||||||
import 'package:venera/pages/history_page.dart';
|
import 'package:venera/pages/history_page.dart';
|
||||||
import 'package:venera/pages/search_page.dart';
|
import 'package:venera/pages/search_page.dart';
|
||||||
import 'package:venera/utils/cbz.dart';
|
|
||||||
import 'package:venera/utils/data_sync.dart';
|
import 'package:venera/utils/data_sync.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/import_comic.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
import 'package:sqlite3/sqlite3.dart' as sql;
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'local_comics_page.dart';
|
import 'local_comics_page.dart';
|
||||||
|
|
||||||
@@ -624,323 +619,26 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void selectAndImport() async {
|
void selectAndImport() async {
|
||||||
if (type == 2) {
|
|
||||||
var xFile = await selectFile(ext: ['cbz']);
|
|
||||||
var controller = showLoadingDialog(context, allowCancel: false);
|
|
||||||
try {
|
|
||||||
var cache = FilePath.join(App.cachePath, xFile?.name ?? 'temp.cbz');
|
|
||||||
await xFile!.saveTo(cache);
|
|
||||||
var comic = await CBZ.import(File(cache));
|
|
||||||
if (selectedFolder != null) {
|
|
||||||
LocalFavoritesManager().addComic(
|
|
||||||
selectedFolder!,
|
|
||||||
FavoriteItem(
|
|
||||||
id: comic.id,
|
|
||||||
name: comic.title,
|
|
||||||
coverPath: comic.cover,
|
|
||||||
author: comic.subtitle,
|
|
||||||
type: comic.comicType,
|
|
||||||
tags: comic.tags,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
await File(cache).deleteIgnoreError();
|
|
||||||
} catch (e, s) {
|
|
||||||
Log.error("Import Comic", e.toString(), s);
|
|
||||||
context.showMessage(message: e.toString());
|
|
||||||
}
|
|
||||||
controller.close();
|
|
||||||
return;
|
|
||||||
} else if (type == 3) {
|
|
||||||
var dbFile = await selectFile(ext: ['db']);
|
|
||||||
final picker = DirectoryPicker();
|
|
||||||
final comicSrc = await picker.pickDirectory();
|
|
||||||
if (dbFile == null || comicSrc == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool cancelled = false;
|
|
||||||
var controller = showLoadingDialog(context, onCancel: () { cancelled = true; });
|
|
||||||
|
|
||||||
try {
|
|
||||||
var cache = FilePath.join(App.cachePath, dbFile.name);
|
|
||||||
await dbFile.saveTo(cache);
|
|
||||||
var db = sql.sqlite3.open(cache);
|
|
||||||
|
|
||||||
Future<void> addTagComics(String destFolder, List<sql.Row> comics) async {
|
|
||||||
for(var comic in comics) {
|
|
||||||
if(cancelled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var comicDir = Directory(FilePath.join(comicSrc.path, comic['DIRNAME'] as String));
|
|
||||||
if(!(await comicDir.exists())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String titleJP = comic['TITLE_JPN'] == null ? "" : comic['TITLE_JPN'] as String;
|
|
||||||
String title = titleJP == "" ? comic['TITLE'] as String : titleJP;
|
|
||||||
if (LocalManager().findByName(title) != null) {
|
|
||||||
Log.info("Import Comic", "Comic already exists: $title");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
String coverURL = await comicDir.joinFile(".thumb").exists() ?
|
|
||||||
comicDir.joinFile(".thumb").path :
|
|
||||||
(comic['THUMB'] as String).replaceAll('s.exhentai.org', 'ehgt.org');
|
|
||||||
int downloadedTimeStamp = comic['TIME'] as int;
|
|
||||||
DateTime downloadedTime =
|
|
||||||
downloadedTimeStamp != 0 ?
|
|
||||||
DateTime.fromMillisecondsSinceEpoch(downloadedTimeStamp) : DateTime.now();
|
|
||||||
var comicObj = LocalComic(
|
|
||||||
id: LocalManager().findValidId(ComicType.local),
|
|
||||||
title: title,
|
|
||||||
subtitle: '',
|
|
||||||
tags: [
|
|
||||||
//1 >> x
|
|
||||||
[
|
|
||||||
"MISC",
|
|
||||||
"DOUJINSHI",
|
|
||||||
"MANGA",
|
|
||||||
"ARTISTCG",
|
|
||||||
"GAMECG",
|
|
||||||
"IMAGE SET",
|
|
||||||
"COSPLAY",
|
|
||||||
"ASIAN PORN",
|
|
||||||
"NON-H",
|
|
||||||
"WESTERN",
|
|
||||||
][(log(comic['CATEGORY'] as int) / ln2).floor()]
|
|
||||||
],
|
|
||||||
directory: comicDir.path,
|
|
||||||
chapters: null,
|
|
||||||
cover: coverURL,
|
|
||||||
comicType: ComicType.local,
|
|
||||||
downloadedChapters: [],
|
|
||||||
createdAt: downloadedTime,
|
|
||||||
);
|
|
||||||
LocalManager().add(comicObj, comicObj.id);
|
|
||||||
LocalFavoritesManager().addComic(
|
|
||||||
destFolder,
|
|
||||||
FavoriteItem(
|
|
||||||
id: comicObj.id,
|
|
||||||
name: comicObj.title,
|
|
||||||
coverPath: comicObj.cover,
|
|
||||||
author: comicObj.subtitle,
|
|
||||||
type: comicObj.comicType,
|
|
||||||
tags: comicObj.tags,
|
|
||||||
favoriteTime: downloadedTime
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//default folder
|
|
||||||
{
|
|
||||||
var defaultFolderName = '(EhViewer)Default'.tl;
|
|
||||||
if(!LocalFavoritesManager().existsFolder(defaultFolderName)) {
|
|
||||||
LocalFavoritesManager().createFolder(defaultFolderName);
|
|
||||||
}
|
|
||||||
var comicList = db.select("""
|
|
||||||
SELECT *
|
|
||||||
FROM DOWNLOAD_DIRNAME DN
|
|
||||||
LEFT JOIN DOWNLOADS DL
|
|
||||||
ON DL.GID = DN.GID
|
|
||||||
WHERE DL.LABEL IS NULL AND DL.STATE = 3
|
|
||||||
ORDER BY DL.TIME DESC
|
|
||||||
""").toList();
|
|
||||||
await addTagComics(defaultFolderName, comicList);
|
|
||||||
}
|
|
||||||
|
|
||||||
var folders = db.select("""
|
|
||||||
SELECT * FROM DOWNLOAD_LABELS;
|
|
||||||
""");
|
|
||||||
|
|
||||||
for (var folder in folders) {
|
|
||||||
if(cancelled) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
var label = folder["LABEL"] as String;
|
|
||||||
var folderName = '(EhViewer)$label';
|
|
||||||
if(!LocalFavoritesManager().existsFolder(folderName)) {
|
|
||||||
LocalFavoritesManager().createFolder(folderName);
|
|
||||||
}
|
|
||||||
var comicList = db.select("""
|
|
||||||
SELECT *
|
|
||||||
FROM DOWNLOAD_DIRNAME DN
|
|
||||||
LEFT JOIN DOWNLOADS DL
|
|
||||||
ON DL.GID = DN.GID
|
|
||||||
WHERE DL.LABEL = ? AND DL.STATE = 3
|
|
||||||
ORDER BY DL.TIME DESC
|
|
||||||
""", [label]).toList();
|
|
||||||
await addTagComics(folderName, comicList);
|
|
||||||
}
|
|
||||||
db.dispose();
|
|
||||||
await File(cache).deleteIgnoreError();
|
|
||||||
} catch (e, s) {
|
|
||||||
Log.error("Import Comic", e.toString(), s);
|
|
||||||
context.showMessage(message: e.toString());
|
|
||||||
}
|
|
||||||
controller.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
height = key.currentContext!.size!.height;
|
height = key.currentContext!.size!.height;
|
||||||
setState(() {
|
setState(() {
|
||||||
loading = true;
|
loading = true;
|
||||||
});
|
});
|
||||||
final picker = DirectoryPicker();
|
var importer = ImportComic(selectedFolder: selectedFolder);
|
||||||
final path = await picker.pickDirectory();
|
var result = false;
|
||||||
if (!loading) {
|
if (type == 2) {
|
||||||
picker.dispose();
|
result = await importer.cbz();
|
||||||
return;
|
} else if (type == 3) {
|
||||||
|
result = await importer.ehViewer();
|
||||||
|
} else {
|
||||||
|
result = await importer.directory(type == 0);
|
||||||
}
|
}
|
||||||
if (path == null) {
|
if(result) {
|
||||||
|
context.pop();
|
||||||
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
loading = false;
|
loading = false;
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
Map<Directory, LocalComic> comics = {};
|
|
||||||
if (type == 0) {
|
|
||||||
var result = await checkSingleComic(path);
|
|
||||||
if (result != null) {
|
|
||||||
comics[path] = result;
|
|
||||||
} else {
|
|
||||||
context.showMessage(message: "Invalid Comic".tl);
|
|
||||||
setState(() {
|
|
||||||
loading = false;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await for (var entry in path.list()) {
|
|
||||||
if (entry is Directory) {
|
|
||||||
var result = await checkSingleComic(entry);
|
|
||||||
if (result != null) {
|
|
||||||
comics[entry] = result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bool shouldCopy = true;
|
|
||||||
for (var comic in comics.keys) {
|
|
||||||
if (comic.parent.path == LocalManager().path) {
|
|
||||||
shouldCopy = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (shouldCopy && comics.isNotEmpty) {
|
|
||||||
try {
|
|
||||||
// copy the comics to the local directory
|
|
||||||
await compute<Map<String, dynamic>, void>(_copyDirectories, {
|
|
||||||
'toBeCopied': comics.keys.map((e) => e.path).toList(),
|
|
||||||
'destination': LocalManager().path,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
context.showMessage(message: "Failed to import comics".tl);
|
|
||||||
Log.error("Import Comic", e.toString());
|
|
||||||
setState(() {
|
|
||||||
loading = false;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (var comic in comics.values) {
|
|
||||||
LocalManager().add(comic, LocalManager().findValidId(ComicType.local));
|
|
||||||
if (selectedFolder != null) {
|
|
||||||
LocalFavoritesManager().addComic(
|
|
||||||
selectedFolder!,
|
|
||||||
FavoriteItem(
|
|
||||||
id: comic.id,
|
|
||||||
name: comic.title,
|
|
||||||
coverPath: comic.cover,
|
|
||||||
author: comic.subtitle,
|
|
||||||
type: comic.comicType,
|
|
||||||
tags: comic.tags,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
context.pop();
|
|
||||||
context.showMessage(
|
|
||||||
message: "Imported @a comics".tlParams({
|
|
||||||
'a': comics.length,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
static _copyDirectories(Map<String, dynamic> data) {
|
|
||||||
var toBeCopied = data['toBeCopied'] as List<String>;
|
|
||||||
var destination = data['destination'] as String;
|
|
||||||
for (var dir in toBeCopied) {
|
|
||||||
var source = Directory(dir);
|
|
||||||
var dest = Directory("$destination/${source.name}");
|
|
||||||
if (dest.existsSync()) {
|
|
||||||
// The destination directory already exists, and it is not managed by the app.
|
|
||||||
// Rename the old directory to avoid conflicts.
|
|
||||||
Log.info("Import Comic",
|
|
||||||
"Directory already exists: ${source.name}\nRenaming the old directory.");
|
|
||||||
dest.rename(
|
|
||||||
findValidDirectoryName(dest.parent.path, "${dest.path}_old"));
|
|
||||||
}
|
|
||||||
dest.createSync();
|
|
||||||
copyDirectory(source, dest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<LocalComic?> checkSingleComic(Directory directory) async {
|
|
||||||
if (!(await directory.exists())) return null;
|
|
||||||
var name = directory.name;
|
|
||||||
if (LocalManager().findByName(name) != null) {
|
|
||||||
Log.info("Import Comic", "Comic already exists: $name");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
bool hasChapters = false;
|
|
||||||
var chapters = <String>[];
|
|
||||||
var coverPath = ''; // relative path to the cover image
|
|
||||||
await for (var entry in directory.list()) {
|
|
||||||
if (entry is Directory) {
|
|
||||||
hasChapters = true;
|
|
||||||
chapters.add(entry.name);
|
|
||||||
await for (var file in entry.list()) {
|
|
||||||
if (file is Directory) {
|
|
||||||
Log.info("Import Comic",
|
|
||||||
"Invalid Chapter: ${entry.name}\nA directory is found in the chapter directory.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (entry is File) {
|
|
||||||
if (entry.name.startsWith('cover')) {
|
|
||||||
coverPath = entry.name;
|
|
||||||
}
|
|
||||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'jpe'];
|
|
||||||
if (!coverPath.startsWith('cover') &&
|
|
||||||
imageExtensions.contains(entry.extension)) {
|
|
||||||
coverPath = entry.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chapters.sort();
|
|
||||||
if (hasChapters && coverPath == '') {
|
|
||||||
// use the first image in the first chapter as the cover
|
|
||||||
var firstChapter = Directory('${directory.path}/${chapters.first}');
|
|
||||||
await for (var entry in firstChapter.list()) {
|
|
||||||
if (entry is File) {
|
|
||||||
coverPath = entry.name;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (coverPath == '') {
|
|
||||||
Log.info("Import Comic", "Invalid Comic: $name\nNo cover image found.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return LocalComic(
|
|
||||||
id: '0',
|
|
||||||
title: name,
|
|
||||||
subtitle: '',
|
|
||||||
tags: [],
|
|
||||||
directory: directory.name,
|
|
||||||
chapters: hasChapters ? Map.fromIterables(chapters, chapters) : null,
|
|
||||||
cover: coverPath,
|
|
||||||
comicType: ComicType.local,
|
|
||||||
downloadedChapters: chapters,
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -298,24 +298,16 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
return StatefulBuilder(builder: (context, state) {
|
return StatefulBuilder(builder: (context, state) {
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: "Delete".tl,
|
title: "Delete".tl,
|
||||||
content: Column(
|
content: CheckboxListTile(
|
||||||
children: [
|
title:
|
||||||
Text("Delete selected comics?".tl)
|
Text("Also remove files on disk".tl),
|
||||||
.paddingVertical(8),
|
value: removeComicFile,
|
||||||
Transform.scale(
|
onChanged: (v) {
|
||||||
scale: 0.9,
|
state(() {
|
||||||
child: CheckboxListTile(
|
removeComicFile = !removeComicFile;
|
||||||
title: Text(
|
});
|
||||||
"Also remove files on disk".tl),
|
},
|
||||||
value: removeComicFile,
|
),
|
||||||
onChanged: (v) {
|
|
||||||
state(() {
|
|
||||||
removeComicFile =
|
|
||||||
!removeComicFile;
|
|
||||||
});
|
|
||||||
})),
|
|
||||||
],
|
|
||||||
).paddingHorizontal(16).paddingVertical(8),
|
|
||||||
actions: [
|
actions: [
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -379,12 +371,12 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: !multiSelectMode && !searchMode,
|
canPop: !multiSelectMode && !searchMode,
|
||||||
onPopInvokedWithResult: (didPop, result) {
|
onPopInvokedWithResult: (didPop, result) {
|
||||||
if(multiSelectMode) {
|
if (multiSelectMode) {
|
||||||
setState(() {
|
setState(() {
|
||||||
multiSelectMode = false;
|
multiSelectMode = false;
|
||||||
selectedComics.clear();
|
selectedComics.clear();
|
||||||
});
|
});
|
||||||
} else if(searchMode) {
|
} else if (searchMode) {
|
||||||
setState(() {
|
setState(() {
|
||||||
searchMode = false;
|
searchMode = false;
|
||||||
keyword = "";
|
keyword = "";
|
||||||
|
@@ -86,6 +86,9 @@ abstract class CBZ {
|
|||||||
var ext = e.path.split('.').last;
|
var ext = e.path.split('.').last;
|
||||||
return !['jpg', 'jpeg', 'png', 'webp', 'gif', 'jpe'].contains(ext);
|
return !['jpg', 'jpeg', 'png', 'webp', 'gif', 'jpe'].contains(ext);
|
||||||
});
|
});
|
||||||
|
if(files.isEmpty) {
|
||||||
|
throw Exception('No images found in the archive');
|
||||||
|
}
|
||||||
files.sort((a, b) => a.path.compareTo(b.path));
|
files.sort((a, b) => a.path.compareTo(b.path));
|
||||||
var coverFile = files.firstWhereOrNull(
|
var coverFile = files.firstWhereOrNull(
|
||||||
(element) =>
|
(element) =>
|
||||||
|
@@ -10,6 +10,9 @@ class FileType {
|
|||||||
if(ext.startsWith('.')) {
|
if(ext.startsWith('.')) {
|
||||||
ext = ext.substring(1);
|
ext = ext.substring(1);
|
||||||
}
|
}
|
||||||
|
if(ext == 'cbz') {
|
||||||
|
return const FileType('.cbz', 'application/octet-stream');
|
||||||
|
}
|
||||||
var mime = lookupMimeType('no-file.$ext');
|
var mime = lookupMimeType('no-file.$ext');
|
||||||
return FileType(".$ext", mime ?? 'application/octet-stream');
|
return FileType(".$ext", mime ?? 'application/octet-stream');
|
||||||
}
|
}
|
||||||
|
338
lib/utils/import_comic.dart
Normal file
338
lib/utils/import_comic.dart
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:venera/components/components.dart';
|
||||||
|
import 'package:venera/foundation/app.dart';
|
||||||
|
import 'package:venera/foundation/comic_type.dart';
|
||||||
|
import 'package:venera/foundation/favorites.dart';
|
||||||
|
import 'package:venera/foundation/local.dart';
|
||||||
|
import 'package:venera/foundation/log.dart';
|
||||||
|
import 'package:sqlite3/sqlite3.dart' as sql;
|
||||||
|
import 'package:venera/utils/translations.dart';
|
||||||
|
import 'cbz.dart';
|
||||||
|
import 'io.dart';
|
||||||
|
|
||||||
|
class ImportComic {
|
||||||
|
final String? selectedFolder;
|
||||||
|
|
||||||
|
const ImportComic({this.selectedFolder});
|
||||||
|
|
||||||
|
Future<bool> cbz() async {
|
||||||
|
var xFile = await selectFile(ext: ['cbz']);
|
||||||
|
if(xFile == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var controller = showLoadingDialog(App.rootContext, allowCancel: false);
|
||||||
|
var isSuccessful = false;
|
||||||
|
try {
|
||||||
|
var cache = FilePath.join(App.cachePath, xFile.name);
|
||||||
|
await xFile.saveTo(cache);
|
||||||
|
var comic = await CBZ.import(File(cache));
|
||||||
|
if (selectedFolder != null) {
|
||||||
|
LocalFavoritesManager().addComic(
|
||||||
|
selectedFolder!,
|
||||||
|
FavoriteItem(
|
||||||
|
id: comic.id,
|
||||||
|
name: comic.title,
|
||||||
|
coverPath: comic.cover,
|
||||||
|
author: comic.subtitle,
|
||||||
|
type: comic.comicType,
|
||||||
|
tags: comic.tags,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await File(cache).deleteIgnoreError();
|
||||||
|
isSuccessful = true;
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Import Comic", e.toString(), s);
|
||||||
|
App.rootContext.showMessage(message: e.toString());
|
||||||
|
}
|
||||||
|
controller.close();
|
||||||
|
return isSuccessful;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> ehViewer() async {
|
||||||
|
var dbFile = await selectFile(ext: ['db']);
|
||||||
|
final picker = DirectoryPicker();
|
||||||
|
final comicSrc = await picker.pickDirectory();
|
||||||
|
if (dbFile == null || comicSrc == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool cancelled = false;
|
||||||
|
var controller = showLoadingDialog(App.rootContext, onCancel: () {
|
||||||
|
cancelled = true;
|
||||||
|
});
|
||||||
|
bool isSuccessful = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var cache = FilePath.join(App.cachePath, dbFile.name);
|
||||||
|
await dbFile.saveTo(cache);
|
||||||
|
var db = sql.sqlite3.open(cache);
|
||||||
|
|
||||||
|
Future<void> addTagComics(String destFolder, List<sql.Row> comics) async {
|
||||||
|
for (var comic in comics) {
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var comicDir = Directory(
|
||||||
|
FilePath.join(comicSrc.path, comic['DIRNAME'] as String));
|
||||||
|
if (!(await comicDir.exists())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String titleJP =
|
||||||
|
comic['TITLE_JPN'] == null ? "" : comic['TITLE_JPN'] as String;
|
||||||
|
String title = titleJP == "" ? comic['TITLE'] as String : titleJP;
|
||||||
|
if (LocalManager().findByName(title) != null) {
|
||||||
|
Log.info("Import Comic", "Comic already exists: $title");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String coverURL = await comicDir.joinFile(".thumb").exists()
|
||||||
|
? comicDir.joinFile(".thumb").path
|
||||||
|
: (comic['THUMB'] as String)
|
||||||
|
.replaceAll('s.exhentai.org', 'ehgt.org');
|
||||||
|
int downloadedTimeStamp = comic['TIME'] as int;
|
||||||
|
DateTime downloadedTime = downloadedTimeStamp != 0
|
||||||
|
? DateTime.fromMillisecondsSinceEpoch(downloadedTimeStamp)
|
||||||
|
: DateTime.now();
|
||||||
|
var comicObj = LocalComic(
|
||||||
|
id: LocalManager().findValidId(ComicType.local),
|
||||||
|
title: title,
|
||||||
|
subtitle: '',
|
||||||
|
tags: [
|
||||||
|
//1 >> x
|
||||||
|
[
|
||||||
|
"MISC",
|
||||||
|
"DOUJINSHI",
|
||||||
|
"MANGA",
|
||||||
|
"ARTISTCG",
|
||||||
|
"GAMECG",
|
||||||
|
"IMAGE SET",
|
||||||
|
"COSPLAY",
|
||||||
|
"ASIAN PORN",
|
||||||
|
"NON-H",
|
||||||
|
"WESTERN",
|
||||||
|
][(log(comic['CATEGORY'] as int) / ln2).floor()]
|
||||||
|
],
|
||||||
|
directory: comicDir.path,
|
||||||
|
chapters: null,
|
||||||
|
cover: coverURL,
|
||||||
|
comicType: ComicType.local,
|
||||||
|
downloadedChapters: [],
|
||||||
|
createdAt: downloadedTime,
|
||||||
|
);
|
||||||
|
LocalManager().add(comicObj, comicObj.id);
|
||||||
|
LocalFavoritesManager().addComic(
|
||||||
|
destFolder,
|
||||||
|
FavoriteItem(
|
||||||
|
id: comicObj.id,
|
||||||
|
name: comicObj.title,
|
||||||
|
coverPath: comicObj.cover,
|
||||||
|
author: comicObj.subtitle,
|
||||||
|
type: comicObj.comicType,
|
||||||
|
tags: comicObj.tags,
|
||||||
|
favoriteTime: downloadedTime,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var defaultFolderName = '(EhViewer)Default';
|
||||||
|
if (!LocalFavoritesManager().existsFolder(defaultFolderName)) {
|
||||||
|
LocalFavoritesManager().createFolder(defaultFolderName);
|
||||||
|
}
|
||||||
|
var comicList = db.select("""
|
||||||
|
SELECT *
|
||||||
|
FROM DOWNLOAD_DIRNAME DN
|
||||||
|
LEFT JOIN DOWNLOADS DL
|
||||||
|
ON DL.GID = DN.GID
|
||||||
|
WHERE DL.LABEL IS NULL AND DL.STATE = 3
|
||||||
|
ORDER BY DL.TIME DESC
|
||||||
|
""").toList();
|
||||||
|
await addTagComics(defaultFolderName, comicList);
|
||||||
|
}
|
||||||
|
|
||||||
|
var folders = db.select("""
|
||||||
|
SELECT * FROM DOWNLOAD_LABELS;
|
||||||
|
""");
|
||||||
|
|
||||||
|
for (var folder in folders) {
|
||||||
|
if (cancelled) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var label = folder["LABEL"] as String;
|
||||||
|
var folderName = '(EhViewer)$label';
|
||||||
|
if (!LocalFavoritesManager().existsFolder(folderName)) {
|
||||||
|
LocalFavoritesManager().createFolder(folderName);
|
||||||
|
}
|
||||||
|
var comicList = db.select("""
|
||||||
|
SELECT *
|
||||||
|
FROM DOWNLOAD_DIRNAME DN
|
||||||
|
LEFT JOIN DOWNLOADS DL
|
||||||
|
ON DL.GID = DN.GID
|
||||||
|
WHERE DL.LABEL = ? AND DL.STATE = 3
|
||||||
|
ORDER BY DL.TIME DESC
|
||||||
|
""", [label]).toList();
|
||||||
|
await addTagComics(folderName, comicList);
|
||||||
|
}
|
||||||
|
db.dispose();
|
||||||
|
await File(cache).deleteIgnoreError();
|
||||||
|
isSuccessful = true;
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Import Comic", e.toString(), s);
|
||||||
|
App.rootContext.showMessage(message: e.toString());
|
||||||
|
}
|
||||||
|
controller.close();
|
||||||
|
return isSuccessful;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> directory(bool single) async {
|
||||||
|
final picker = DirectoryPicker();
|
||||||
|
final path = await picker.pickDirectory();
|
||||||
|
if (path == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Map<Directory, LocalComic> comics = {};
|
||||||
|
if (single) {
|
||||||
|
var result = await _checkSingleComic(path);
|
||||||
|
if (result != null) {
|
||||||
|
comics[path] = result;
|
||||||
|
} else {
|
||||||
|
App.rootContext.showMessage(message: "Invalid Comic".tl);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await for (var entry in path.list()) {
|
||||||
|
if (entry is Directory) {
|
||||||
|
var result = await _checkSingleComic(entry);
|
||||||
|
if (result != null) {
|
||||||
|
comics[entry] = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool shouldCopy = true;
|
||||||
|
for (var comic in comics.keys) {
|
||||||
|
if (comic.parent.path == LocalManager().path) {
|
||||||
|
shouldCopy = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shouldCopy && comics.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
// copy the comics to the local directory
|
||||||
|
await compute<Map<String, dynamic>, void>(_copyDirectories, {
|
||||||
|
'toBeCopied': comics.keys.map((e) => e.path).toList(),
|
||||||
|
'destination': LocalManager().path,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
App.rootContext.showMessage(message: "Failed to import comics".tl);
|
||||||
|
Log.error("Import Comic", e.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var comic in comics.values) {
|
||||||
|
LocalManager().add(comic, LocalManager().findValidId(ComicType.local));
|
||||||
|
if (selectedFolder != null) {
|
||||||
|
LocalFavoritesManager().addComic(
|
||||||
|
selectedFolder!,
|
||||||
|
FavoriteItem(
|
||||||
|
id: comic.id,
|
||||||
|
name: comic.title,
|
||||||
|
coverPath: comic.cover,
|
||||||
|
author: comic.subtitle,
|
||||||
|
type: comic.comicType,
|
||||||
|
tags: comic.tags,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
App.rootContext.showMessage(
|
||||||
|
message: "Imported @a comics".tlParams({
|
||||||
|
'a': comics.length,
|
||||||
|
}));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<LocalComic?> _checkSingleComic(Directory directory) async {
|
||||||
|
if (!(await directory.exists())) return null;
|
||||||
|
var name = directory.name;
|
||||||
|
if (LocalManager().findByName(name) != null) {
|
||||||
|
Log.info("Import Comic", "Comic already exists: $name");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
bool hasChapters = false;
|
||||||
|
var chapters = <String>[];
|
||||||
|
var coverPath = ''; // relative path to the cover image
|
||||||
|
for (var entry in directory.listSync()) {
|
||||||
|
if (entry is Directory) {
|
||||||
|
hasChapters = true;
|
||||||
|
chapters.add(entry.name);
|
||||||
|
await for (var file in entry.list()) {
|
||||||
|
if (file is Directory) {
|
||||||
|
Log.info("Import Comic",
|
||||||
|
"Invalid Chapter: ${entry.name}\nA directory is found in the chapter directory.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (entry is File) {
|
||||||
|
if (entry.name.startsWith('cover')) {
|
||||||
|
coverPath = entry.name;
|
||||||
|
}
|
||||||
|
const imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'jpe'];
|
||||||
|
if (!coverPath.startsWith('cover') &&
|
||||||
|
imageExtensions.contains(entry.extension)) {
|
||||||
|
coverPath = entry.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chapters.sort();
|
||||||
|
if (hasChapters && coverPath == '') {
|
||||||
|
// use the first image in the first chapter as the cover
|
||||||
|
var firstChapter = Directory('${directory.path}/${chapters.first}');
|
||||||
|
await for (var entry in firstChapter.list()) {
|
||||||
|
if (entry is File) {
|
||||||
|
coverPath = entry.name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (coverPath == '') {
|
||||||
|
Log.info("Import Comic", "Invalid Comic: $name\nNo cover image found.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return LocalComic(
|
||||||
|
id: '0',
|
||||||
|
title: name,
|
||||||
|
subtitle: '',
|
||||||
|
tags: [],
|
||||||
|
directory: directory.name,
|
||||||
|
chapters: hasChapters ? Map.fromIterables(chapters, chapters) : null,
|
||||||
|
cover: coverPath,
|
||||||
|
comicType: ComicType.local,
|
||||||
|
downloadedChapters: chapters,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static _copyDirectories(Map<String, dynamic> data) {
|
||||||
|
var toBeCopied = data['toBeCopied'] as List<String>;
|
||||||
|
var destination = data['destination'] as String;
|
||||||
|
for (var dir in toBeCopied) {
|
||||||
|
var source = Directory(dir);
|
||||||
|
var dest = Directory("$destination/${source.name}");
|
||||||
|
if (dest.existsSync()) {
|
||||||
|
// The destination directory already exists, and it is not managed by the app.
|
||||||
|
// Rename the old directory to avoid conflicts.
|
||||||
|
Log.info("Import Comic",
|
||||||
|
"Directory already exists: ${source.name}\nRenaming the old directory.");
|
||||||
|
dest.rename(
|
||||||
|
findValidDirectoryName(dest.parent.path, "${dest.path}_old"));
|
||||||
|
}
|
||||||
|
dest.createSync();
|
||||||
|
copyDirectory(source, dest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -14,6 +14,21 @@ import 'package:venera/utils/file_type.dart';
|
|||||||
export 'dart:io';
|
export 'dart:io';
|
||||||
export 'dart:typed_data';
|
export 'dart:typed_data';
|
||||||
|
|
||||||
|
class IO {
|
||||||
|
/// A global flag used to indicate whether the app is selecting files.
|
||||||
|
///
|
||||||
|
/// Select file and other similar file operations will launch external programs,
|
||||||
|
/// causing the app to lose focus. AppLifecycleState will be set to paused.
|
||||||
|
static bool get isSelectingFiles => _isSelectingFiles;
|
||||||
|
|
||||||
|
static bool _isSelectingFiles = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A finalizer that can be used to dispose resources related to file operations.
|
||||||
|
final _finalizer = Finalizer<Function>((e) {
|
||||||
|
e();
|
||||||
|
});
|
||||||
|
|
||||||
class FilePath {
|
class FilePath {
|
||||||
const FilePath._();
|
const FilePath._();
|
||||||
|
|
||||||
@@ -147,24 +162,35 @@ String findValidDirectoryName(String path, String directory) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DirectoryPicker {
|
class DirectoryPicker {
|
||||||
|
DirectoryPicker() {
|
||||||
|
_finalizer.attach(this, dispose);
|
||||||
|
}
|
||||||
|
|
||||||
String? _directory;
|
String? _directory;
|
||||||
|
|
||||||
final _methodChannel = const MethodChannel("venera/method_channel");
|
final _methodChannel = const MethodChannel("venera/method_channel");
|
||||||
|
|
||||||
Future<Directory?> pickDirectory() async {
|
Future<Directory?> pickDirectory() async {
|
||||||
if (App.isWindows || App.isLinux) {
|
IO._isSelectingFiles = true;
|
||||||
var d = await file_selector.getDirectoryPath();
|
try {
|
||||||
_directory = d;
|
if (App.isWindows || App.isLinux) {
|
||||||
return d == null ? null : Directory(d);
|
var d = await file_selector.getDirectoryPath();
|
||||||
} else if (App.isAndroid) {
|
_directory = d;
|
||||||
var d = await _methodChannel.invokeMethod<String?>("getDirectoryPath");
|
return d == null ? null : Directory(d);
|
||||||
_directory = d;
|
} else if (App.isAndroid) {
|
||||||
return d == null ? null : Directory(d);
|
var d = await _methodChannel.invokeMethod<String?>("getDirectoryPath");
|
||||||
} else {
|
_directory = d;
|
||||||
// ios, macos
|
return d == null ? null : Directory(d);
|
||||||
var d = await _methodChannel.invokeMethod<String?>("getDirectoryPath");
|
} else {
|
||||||
_directory = d;
|
// ios, macos
|
||||||
return d == null ? null : Directory(d);
|
var d = await _methodChannel.invokeMethod<String?>("getDirectoryPath");
|
||||||
|
_directory = d;
|
||||||
|
return d == null ? null : Directory(d);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
|
IO._isSelectingFiles = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,11 +198,15 @@ class DirectoryPicker {
|
|||||||
if (_directory == null) {
|
if (_directory == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (App.isAndroid && _directory != null) {
|
if (App.isAndroid &&
|
||||||
return Directory(_directory!).deleteIgnoreError(recursive: true);
|
_directory != null &&
|
||||||
|
_directory!.startsWith(App.cachePath)) {
|
||||||
|
await Directory(_directory!).deleteIgnoreError(recursive: true);
|
||||||
|
_directory = null;
|
||||||
}
|
}
|
||||||
if (App.isIOS || App.isMacOS) {
|
if (App.isIOS || App.isMacOS) {
|
||||||
await _methodChannel.invokeMethod("stopAccessingSecurityScopedResource");
|
await _methodChannel.invokeMethod("stopAccessingSecurityScopedResource");
|
||||||
|
_directory = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,53 +216,73 @@ class IOSDirectoryPicker {
|
|||||||
|
|
||||||
// 调用 iOS 目录选择方法
|
// 调用 iOS 目录选择方法
|
||||||
static Future<String?> selectDirectory() async {
|
static Future<String?> selectDirectory() async {
|
||||||
|
IO._isSelectingFiles = true;
|
||||||
try {
|
try {
|
||||||
final String? path = await _channel.invokeMethod('selectDirectory');
|
final String? path = await _channel.invokeMethod('selectDirectory');
|
||||||
return path;
|
return path;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 返回报错信息
|
// 返回报错信息
|
||||||
return e.toString();
|
return e.toString();
|
||||||
|
} finally {
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
|
IO._isSelectingFiles = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<file_selector.XFile?> selectFile({required List<String> ext}) async {
|
Future<file_selector.XFile?> selectFile({required List<String> ext}) async {
|
||||||
var extensions = App.isMacOS || App.isIOS ? null : ext;
|
IO._isSelectingFiles = true;
|
||||||
if (App.isAndroid) {
|
try {
|
||||||
for (var e in ext) {
|
var extensions = App.isMacOS || App.isIOS ? null : ext;
|
||||||
var fileType = FileType.fromExtension(e);
|
file_selector.XTypeGroup typeGroup = file_selector.XTypeGroup(
|
||||||
if (fileType.mime == "application/octet-stream") {
|
label: 'files',
|
||||||
extensions = null;
|
extensions: extensions,
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
file_selector.XTypeGroup typeGroup = file_selector.XTypeGroup(
|
|
||||||
label: 'files',
|
|
||||||
extensions: extensions,
|
|
||||||
);
|
|
||||||
file_selector.XFile? file;
|
|
||||||
if (extensions == null && App.isAndroid) {
|
|
||||||
const selectFileChannel = MethodChannel("venera/select_file");
|
|
||||||
var filePath = await selectFileChannel.invokeMethod("selectFile");
|
|
||||||
if (filePath == null) return null;
|
|
||||||
file = file_selector.XFile(filePath);
|
|
||||||
} else {
|
|
||||||
file = await file_selector.openFile(
|
|
||||||
acceptedTypeGroups: <file_selector.XTypeGroup>[typeGroup],
|
|
||||||
);
|
);
|
||||||
if (file == null) return null;
|
file_selector.XFile? file;
|
||||||
|
if (App.isAndroid) {
|
||||||
|
const selectFileChannel = MethodChannel("venera/select_file");
|
||||||
|
String mimeType = "*/*";
|
||||||
|
if(ext.length == 1) {
|
||||||
|
mimeType = FileType.fromExtension(ext[0]).mime;
|
||||||
|
if(mimeType == "application/octet-stream") {
|
||||||
|
mimeType = "*/*";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var filePath = await selectFileChannel.invokeMethod(
|
||||||
|
"selectFile",
|
||||||
|
mimeType,
|
||||||
|
);
|
||||||
|
if (filePath == null) return null;
|
||||||
|
file = _AndroidFileSelectResult(filePath);
|
||||||
|
} else {
|
||||||
|
file = await file_selector.openFile(
|
||||||
|
acceptedTypeGroups: <file_selector.XTypeGroup>[typeGroup],
|
||||||
|
);
|
||||||
|
if (file == null) return null;
|
||||||
|
}
|
||||||
|
if (!ext.contains(file.path.split(".").last)) {
|
||||||
|
App.rootContext.showMessage(message: "Invalid file type");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
} finally {
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
|
IO._isSelectingFiles = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (!ext.contains(file.path.split(".").last)) {
|
|
||||||
App.rootContext.showMessage(message: "Invalid file type");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return file;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> selectDirectory() async {
|
Future<String?> selectDirectory() async {
|
||||||
var path = await file_selector.getDirectoryPath();
|
IO._isSelectingFiles = true;
|
||||||
return path;
|
try {
|
||||||
|
var path = await file_selector.getDirectoryPath();
|
||||||
|
return path;
|
||||||
|
} finally {
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
|
IO._isSelectingFiles = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// selectDirectoryIOS
|
// selectDirectoryIOS
|
||||||
@@ -245,25 +295,32 @@ Future<void> saveFile(
|
|||||||
if (data == null && file == null) {
|
if (data == null && file == null) {
|
||||||
throw Exception("data and file cannot be null at the same time");
|
throw Exception("data and file cannot be null at the same time");
|
||||||
}
|
}
|
||||||
if (data != null) {
|
IO._isSelectingFiles = true;
|
||||||
var cache = FilePath.join(App.cachePath, filename);
|
try {
|
||||||
if (File(cache).existsSync()) {
|
if (data != null) {
|
||||||
File(cache).deleteSync();
|
var cache = FilePath.join(App.cachePath, filename);
|
||||||
|
if (File(cache).existsSync()) {
|
||||||
|
File(cache).deleteSync();
|
||||||
|
}
|
||||||
|
await File(cache).writeAsBytes(data);
|
||||||
|
file = File(cache);
|
||||||
}
|
}
|
||||||
await File(cache).writeAsBytes(data);
|
if (App.isMobile) {
|
||||||
file = File(cache);
|
final params = SaveFileDialogParams(sourceFilePath: file!.path);
|
||||||
}
|
await FlutterFileDialog.saveFile(params: params);
|
||||||
if (App.isMobile) {
|
} else {
|
||||||
final params = SaveFileDialogParams(sourceFilePath: file!.path);
|
final result = await file_selector.getSaveLocation(
|
||||||
await FlutterFileDialog.saveFile(params: params);
|
suggestedName: filename,
|
||||||
} else {
|
);
|
||||||
final result = await file_selector.getSaveLocation(
|
if (result != null) {
|
||||||
suggestedName: filename,
|
var xFile = file_selector.XFile(file!.path);
|
||||||
);
|
await xFile.saveTo(result.path);
|
||||||
if (result != null) {
|
}
|
||||||
var xFile = file_selector.XFile(file!.path);
|
|
||||||
await xFile.saveTo(result.path);
|
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
|
IO._isSelectingFiles = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,3 +359,16 @@ String bytesToReadableString(int bytes) {
|
|||||||
return "${(bytes / 1024 / 1024 / 1024).toStringAsFixed(2)} GB";
|
return "${(bytes / 1024 / 1024 / 1024).toStringAsFixed(2)} GB";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AndroidFileSelectResult extends s.XFile {
|
||||||
|
_AndroidFileSelectResult(super.path) {
|
||||||
|
_finalizer.attach(this, dispose);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
print("dispose $path");
|
||||||
|
if (path.startsWith(App.cachePath)) {
|
||||||
|
File(path).deleteIgnoreError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user