更改安卓端的文件访问方式,优化导入逻辑 (#64)

* Refactor import function & Allow import local comics without copying them to local path.

* android: use file_picker instead, support directory access for android 10

* Improve import logic

* Fix sql query.

* Add ability to remove invalid favorite items.

* Perform sort before choosing cover

* Revert changes of "use file_picker instead".

* Try catch on "check update"

* Added module 'flutter_saf'

* gitignore

* remove unsupported arch in build.gradle

* Use flutter_saf to handle android's directory and files, improve import logic.

* revert changes of 'requestLegacyExternalStorage'

* fix cbz import

* openDirectoryPlatform

* Remove double check on source folder

* use openFilePlatform

* remove unused import

* improve local comic's path handling

* bump version

* fix pubspec format

* return null when comic folder is empty
This commit is contained in:
pkuislm
2024-11-23 11:05:00 +08:00
committed by GitHub
parent c3474b1dff
commit a1474ca9c3
16 changed files with 369 additions and 253 deletions

View File

@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/image_provider/local_favorite_image.dart';
import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/log.dart';
import 'dart:io';
@@ -496,6 +497,22 @@ class LocalFavoritesManager with ChangeNotifier {
notifyListeners();
}
Future<int> removeInvalid() async {
int count = 0;
await Future.microtask(() {
var all = allComics();
for(var c in all) {
var comicSource = c.type.comicSource;
if ((c.type == ComicType.local && LocalManager().find(c.id, c.type) == null)
|| (c.type != ComicType.local && comicSource == null)) {
deleteComicWithId(c.folder, c.id, c.type);
count++;
}
}
});
return count;
}
Future<void> clearAll() async {
_db.dispose();
File("${App.dataPath}/local_favorite.db").deleteSync();

View File

@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:venera/network/images.dart';
import 'package:venera/utils/io.dart';
import 'base_image_provider.dart';
import 'cached_image.dart' as image_provider;
@@ -24,7 +25,7 @@ class CachedImageProvider
@override
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
if(url.startsWith("file://")) {
var file = File(url.substring(7));
var file = openFilePlatform(url.substring(7));
return file.readAsBytes();
}
await for (var progress in ImageDownloader.loadThumbnail(url, sourceKey, cid)) {

View File

@@ -71,12 +71,13 @@ class LocalComic with HistoryMixin implements Comic {
downloadedChapters = List.from(jsonDecode(row[8] as String)),
createdAt = DateTime.fromMillisecondsSinceEpoch(row[9] as int);
File get coverFile => File(FilePath.join(
LocalManager().path,
directory,
File get coverFile => openFilePlatform(FilePath.join(
baseDir,
cover,
));
String get baseDir => directory.contains("/") ? directory : FilePath.join(LocalManager().path, directory);
@override
String get description => "";
@@ -341,12 +342,12 @@ class LocalManager with ChangeNotifier {
throw "Invalid ep";
}
var comic = find(id, type) ?? (throw "Comic Not Found");
var directory = Directory(FilePath.join(path, comic.directory));
var directory = openDirectoryPlatform(comic.baseDir);
if (comic.chapters != null) {
var cid = ep is int
? comic.chapters!.keys.elementAt(ep - 1)
: (ep as String);
directory = Directory(FilePath.join(directory.path, cid));
directory = openDirectoryPlatform(FilePath.join(directory.path, cid));
}
var files = <File>[];
await for (var entity in directory.list()) {
@@ -392,10 +393,10 @@ class LocalManager with ChangeNotifier {
String id, ComicType type, String name) async {
var comic = find(id, type);
if (comic != null) {
return Directory(FilePath.join(path, comic.directory));
return openDirectoryPlatform(FilePath.join(path, comic.directory));
}
var dir = findValidDirectoryName(path, name);
return Directory(FilePath.join(path, dir)).create().then((value) => value);
return openDirectoryPlatform(FilePath.join(path, dir)).create().then((value) => value);
}
void completeTask(DownloadTask task) {
@@ -454,7 +455,7 @@ class LocalManager with ChangeNotifier {
void deleteComic(LocalComic c, [bool removeFileOnDisk = true]) {
if(removeFileOnDisk) {
var dir = Directory(FilePath.join(path, c.directory));
var dir = openDirectoryPlatform(FilePath.join(path, c.directory));
dir.deleteIgnoreError(recursive: true);
}
//Deleting a local comic means that it's nolonger available, thus both favorite and history should be deleted.

View File

@@ -1,3 +1,4 @@
import 'package:flutter_saf/flutter_saf.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/cache_manager.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
@@ -12,6 +13,7 @@ import 'package:venera/utils/translations.dart';
import 'foundation/appdata.dart';
Future<void> init() async {
await SAFTaskWorker().init();
await AppTranslation.init();
await appdata.init();
await App.init();

View File

@@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:sliver_tools/sliver_tools.dart';
import 'package:venera/components/components.dart';
@@ -497,6 +496,10 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
String? selectedFolder;
bool copyToLocalFolder = true;
bool cancelled = false;
@override
void dispose() {
loading = false;
@@ -530,22 +533,23 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
),
)
: Column(
key: key,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(width: 600),
...List.generate(importMethods.length, (index) {
return RadioListTile(
title: Text(importMethods[index]),
value: index,
groupValue: type,
onChanged: (value) {
setState(() {
type = value as int;
});
},
);
}),
key: key,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(width: 600),
...List.generate(importMethods.length, (index) {
return RadioListTile(
title: Text(importMethods[index]),
value: index,
groupValue: type,
onChanged: (value) {
setState(() {
type = value as int;
});
},
);
}),
if(type != 3)
ListTile(
title: Text("Add to favorites".tl),
trailing: Select(
@@ -559,10 +563,19 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
},
),
).paddingHorizontal(8),
const SizedBox(height: 8),
Text(info).paddingHorizontal(24),
],
),
CheckboxListTile(
enabled: true,
title: Text("Copy to app local path".tl),
value: copyToLocalFolder,
onChanged:(v) {
setState(() {
copyToLocalFolder = !copyToLocalFolder;
});
}).paddingHorizontal(8),
const SizedBox(height: 8),
Text(info).paddingHorizontal(24),
],
),
actions: [
Button.text(
child: Row(
@@ -620,18 +633,20 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
void selectAndImport() async {
height = key.currentContext!.size!.height;
setState(() {
loading = true;
});
var importer = ImportComic(selectedFolder: selectedFolder);
var result = false;
if (type == 2) {
result = await importer.cbz();
} else if (type == 3) {
result = await importer.ehViewer();
} else {
result = await importer.directory(type == 0);
}
var importer = ImportComic(
selectedFolder: selectedFolder,
copyToLocal: copyToLocalFolder);
var result = switch(type) {
0 => await importer.directory(true),
1 => await importer.directory(false),
2 => await importer.cbz(),
3 => await importer.ehViewer(),
int() => true,
};
if(result) {
context.pop();
} else {

View File

@@ -604,7 +604,7 @@ ImageProvider _createImageProvider(int page, BuildContext context) {
var reader = context.reader;
var imageKey = reader.images![page - 1];
if (imageKey.startsWith('file://')) {
return FileImage(File(imageKey.replaceFirst("file://", '')));
return FileImage(openFilePlatform(imageKey.replaceFirst("file://", '')));
} else {
return ReaderImageProvider(
imageKey,

View File

@@ -469,7 +469,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
ImageProvider image;
var imageKey = images[index];
if (imageKey.startsWith('file://')) {
image = FileImage(File(imageKey.replaceFirst("file://", '')));
image = FileImage(openFilePlatform(imageKey.replaceFirst("file://", '')));
} else {
image = ReaderImageProvider(
imageKey,
@@ -515,7 +515,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
}
}
if (imageKey.startsWith("file://")) {
return await File(imageKey.substring(7)).readAsBytes();
return await openFilePlatform(imageKey.substring(7)).readAsBytes();
} else {
return (await CacheManager().findCache(
"$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))!

View File

@@ -86,29 +86,33 @@ Future<bool> checkUpdate() async {
}
Future<void> checkUpdateUi([bool showMessageIfNoUpdate = true]) async {
var value = await checkUpdate();
if (value) {
showDialog(
context: App.rootContext,
builder: (context) {
return ContentDialog(
title: "New version available".tl,
content: Text(
"A new version is available. Do you want to update now?".tl),
actions: [
Button.text(
onPressed: () {
Navigator.pop(context);
launchUrlString(
"https://github.com/venera-app/venera/releases");
},
child: Text("Update".tl),
),
],
);
});
} else if (showMessageIfNoUpdate) {
App.rootContext.showMessage(message: "No new version available".tl);
try {
var value = await checkUpdate();
if (value) {
showDialog(
context: App.rootContext,
builder: (context) {
return ContentDialog(
title: "New version available".tl,
content: Text(
"A new version is available. Do you want to update now?".tl),
actions: [
Button.text(
onPressed: () {
Navigator.pop(context);
launchUrlString(
"https://github.com/venera-app/venera/releases");
},
child: Text("Update".tl),
),
],
);
});
} else if (showMessageIfNoUpdate) {
App.rootContext.showMessage(message: "No new version available".tl);
}
} catch (e, s) {
Log.error("Check Update", e.toString(), s);
}
}

View File

@@ -38,6 +38,16 @@ class _LocalFavoritesSettingsState extends State<LocalFavoritesSettings> {
for (var e in LocalFavoritesManager().folderNames) e: e
},
).toSliver(),
_CallbackSetting(
title: "Delete all unavailable local favorite items".tl,
callback: () async {
var controller = showLoadingDialog(context);
var count = await LocalFavoritesManager().removeInvalid();
controller.close();
context.showMessage(message: "Deleted @a favorite items".tlParams({'a': count}));
},
actionTitle: 'Delete'.tl,
).toSliver(),
],
);
}

View File

@@ -8,50 +8,40 @@ 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/ext.dart';
import 'package:venera/utils/translations.dart';
import 'cbz.dart';
import 'io.dart';
class ImportComic {
final String? selectedFolder;
final bool copyToLocal;
const ImportComic({this.selectedFolder});
const ImportComic({this.selectedFolder, this.copyToLocal = true});
Future<bool> cbz() async {
var file = await selectFile(ext: ['cbz']);
Map<String?, List<LocalComic>> imported = {};
if(file == null) {
return false;
}
var controller = showLoadingDialog(App.rootContext, allowCancel: false);
var isSuccessful = false;
try {
var comic = await CBZ.import(File(file.path));
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,
),
);
}
isSuccessful = true;
imported[selectedFolder] = [comic];
} catch (e, s) {
Log.error("Import Comic", e.toString(), s);
App.rootContext.showMessage(message: e.toString());
}
controller.close();
return isSuccessful;
return registerComics(imported, true);
}
Future<bool> ehViewer() async {
var dbFile = await selectFile(ext: ['db']);
final picker = DirectoryPicker();
final comicSrc = await picker.pickDirectory();
Map<String?, List<LocalComic>> imported = {};
if (dbFile == null || comicSrc == null) {
return false;
}
@@ -60,129 +50,91 @@ class ImportComic {
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);
var db = sql.sqlite3.open(dbFile.path);
Future<void> addTagComics(String destFolder, List<sql.Row> comics) async {
Future<List<LocalComic>> validateComics(List<sql.Row> comics) async {
List<LocalComic> imported = [];
for (var comic in comics) {
if (cancelled) {
return;
return imported;
}
var comicDir = Directory(
var comicDir = openDirectoryPlatform(
FilePath.join(comicSrc.path, comic['DIRNAME'] as String));
if (!(await comicDir.exists())) {
continue;
}
String titleJP =
comic['TITLE_JPN'] == null ? "" : comic['TITLE_JPN'] as String;
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");
int timeStamp = comic['TIME'] as int;
DateTime downloadTime = timeStamp != 0
? DateTime.fromMillisecondsSinceEpoch(timeStamp)
: DateTime.now();
var comicObj = await _checkSingleComic(comicDir,
title: title,
tags: [
//1 >> x
[
"MISC",
"DOUJINSHI",
"MANGA",
"ARTISTCG",
"GAMECG",
"IMAGE SET",
"COSPLAY",
"ASIAN PORN",
"NON-H",
"WESTERN",
][(log(comic['CATEGORY'] as int) / ln2).floor()]
],
createTime: downloadTime);
if (comicObj == null) {
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,
),
);
imported.add(comicObj);
}
return imported;
}
{
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 tags = <String>[""];
tags.addAll(db.select("""
SELECT * FROM DOWNLOAD_LABELS LB
ORDER BY LB.TIME DESC;
""").map((r) => r['LABEL'] as String).toList());
var folders = db.select("""
SELECT * FROM DOWNLOAD_LABELS;
""");
for (var folder in folders) {
for (var tag in tags) {
if (cancelled) {
break;
}
var label = folder["LABEL"] as String;
var folderName = '(EhViewer)$label';
if (!LocalFavoritesManager().existsFolder(folderName)) {
LocalFavoritesManager().createFolder(folderName);
}
var folderName =
tag == '' ? '(EhViewer)Default'.tl : '(EhViewer)$tag';
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
WHERE DL.LABEL ${tag == '' ? 'IS NULL' : '= \'$tag\''} AND DL.STATE = 3
ORDER BY DL.TIME DESC
""", [label]).toList();
await addTagComics(folderName, comicList);
""").toList();
var validComics = await validateComics(comicList);
imported[folderName] = validComics;
if (validComics.isNotEmpty &&
!LocalFavoritesManager().existsFolder(folderName)) {
LocalFavoritesManager().createFolder(folderName);
}
}
db.dispose();
//Android specific
var cache = FilePath.join(App.cachePath, dbFile.name);
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;
if(cancelled) return false;
return registerComics(imported, copyToLocal);
}
Future<bool> directory(bool single) async {
@@ -191,71 +143,43 @@ class ImportComic {
if (path == null) {
return false;
}
Map<Directory, LocalComic> comics = {};
if (single) {
var result = await _checkSingleComic(path);
if (result != null) {
comics[path] = result;
Map<String?, List<LocalComic>> imported = {selectedFolder: []};
try {
if (single) {
var result = await _checkSingleComic(path);
if (result != null) {
imported[selectedFolder]!.add(result);
} else {
App.rootContext.showMessage(message: "Invalid Comic".tl);
return false;
}
} 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;
await for (var entry in path.list()) {
if (entry is Directory) {
var result = await _checkSingleComic(entry);
if (result != null) {
imported[selectedFolder]!.add(result);
}
}
}
}
} catch (e, s) {
Log.error("Import Comic", e.toString(), s);
App.rootContext.showMessage(message: e.toString());
}
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;
return registerComics(imported, copyToLocal);
}
Future<LocalComic?> _checkSingleComic(Directory directory) async {
//Automatically search for cover image and chapters
Future<LocalComic?> _checkSingleComic(Directory directory,
{String? id,
String? title,
String? subtitle,
List<String>? tags,
DateTime? createTime})
async {
if (!(await directory.exists())) return null;
var name = directory.name;
var name = title ?? directory.name;
if (LocalManager().findByName(name) != null) {
Log.info("Import Comic", "Comic already exists: $name");
return null;
@@ -263,7 +187,8 @@ class ImportComic {
bool hasChapters = false;
var chapters = <String>[];
var coverPath = ''; // relative path to the cover image
for (var entry in directory.listSync()) {
var fileList = <String>[];
await for (var entry in directory.list()) {
if (entry is Directory) {
hasChapters = true;
chapters.add(entry.name);
@@ -275,20 +200,24 @@ class ImportComic {
}
}
} 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;
if (imageExtensions.contains(entry.extension)) {
fileList.add(entry.name);
}
}
}
if(fileList.isEmpty) {
return null;
}
fileList.sort();
coverPath = fileList.firstWhereOrNull((l) => l.startsWith('cover')) ?? fileList.first;
chapters.sort();
if (hasChapters && coverPath == '') {
// use the first image in the first chapter as the cover
var firstChapter = Directory('${directory.path}/${chapters.first}');
var firstChapter = openDirectoryPlatform('${directory.path}/${chapters.first}');
await for (var entry in firstChapter.list()) {
if (entry is File) {
coverPath = entry.name;
@@ -301,25 +230,26 @@ class ImportComic {
return null;
}
return LocalComic(
id: '0',
id: id ?? '0',
title: name,
subtitle: '',
tags: [],
directory: directory.name,
subtitle: subtitle ?? '',
tags: tags ?? [],
directory: directory.path,
chapters: hasChapters ? Map.fromIterables(chapters, chapters) : null,
cover: coverPath,
comicType: ComicType.local,
downloadedChapters: chapters,
createdAt: DateTime.now(),
createdAt: createTime ?? DateTime.now(),
);
}
static Future<void> _copyDirectories(Map<String, dynamic> data) async {
static Future<Map<String, String>> _copyDirectories(Map<String, dynamic> data) async {
var toBeCopied = data['toBeCopied'] as List<String>;
var destination = data['destination'] as String;
Map<String, String> result = {};
for (var dir in toBeCopied) {
var source = Directory(dir);
var dest = Directory("$destination/${source.name}");
var source = openDirectoryPlatform(dir);
var dest = openDirectoryPlatform("$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.
@@ -330,6 +260,95 @@ class ImportComic {
}
dest.createSync();
await copyDirectory(source, dest);
result[source.path] = dest.path;
}
return result;
}
Future<Map<String?, List<LocalComic>>> _copyComicsToLocalDir(
Map<String?, List<LocalComic>> comics) async {
var destPath = LocalManager().path;
Map<String?, List<LocalComic>> result = {};
for (var favoriteFolder in comics.keys) {
result[favoriteFolder] = comics[favoriteFolder]!
.where((c) => c.directory.startsWith(destPath))
.toList();
comics[favoriteFolder]!
.removeWhere((c) => c.directory.startsWith(destPath));
if (comics[favoriteFolder]!.isEmpty) {
continue;
}
try {
// copy the comics to the local directory
var pathMap = await compute<Map<String, dynamic>, Map<String, String>>(
_copyDirectories, {
'toBeCopied': comics[favoriteFolder]!.map((e) => e.directory).toList(),
'destination': destPath,
});
//Construct a new object since LocalComic.directory is a final String
for (var c in comics[favoriteFolder]!) {
result[favoriteFolder]!.add(
LocalComic(
id: c.id,
title: c.title,
subtitle: c.subtitle,
tags: c.tags,
directory: pathMap[c.directory]!,
chapters: c.chapters,
cover: c.cover,
comicType: c.comicType,
downloadedChapters: c.downloadedChapters,
createdAt: c.createdAt
)
);
}
} catch (e) {
App.rootContext.showMessage(message: "Failed to copy comics".tl);
Log.error("Import Comic", e.toString());
return result;
}
}
return result;
}
Future<bool> registerComics(Map<String?, List<LocalComic>> importedComics, bool copy) async {
try {
if (copy) {
importedComics = await _copyComicsToLocalDir(importedComics);
}
int importedCount = 0;
for (var folder in importedComics.keys) {
for (var comic in importedComics[folder]!) {
var id = LocalManager().findValidId(ComicType.local);
LocalManager().add(comic, id);
importedCount++;
if (folder != null) {
LocalFavoritesManager().addComic(
folder,
FavoriteItem(
id: id,
name: comic.title,
coverPath: comic.cover,
author: comic.subtitle,
type: comic.comicType,
tags: comic.tags,
favoriteTime: comic.createdAt
)
);
}
}
}
App.rootContext.showMessage(
message: "Imported @a comics".tlParams({
'a': importedCount,
}));
} catch(e) {
App.rootContext.showMessage(message: "Failed to register comics".tl);
Log.error("Import Comic", e.toString());
return false;
}
return true;
}
}

View File

@@ -4,6 +4,7 @@ import 'dart:isolate';
import 'package:flutter/services.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:flutter_saf/flutter_saf.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/utils/ext.dart';
import 'package:path/path.dart' as p;
@@ -80,7 +81,7 @@ extension DirectoryExtension on Directory {
int total = 0;
for (var f in listSync(recursive: true)) {
if (FileSystemEntity.typeSync(f.path) == FileSystemEntityType.file) {
total += await File(f.path).length();
total += await openFilePlatform(f.path).length();
}
}
return total;
@@ -92,7 +93,7 @@ extension DirectoryExtension on Directory {
}
File joinFile(String name) {
return File(FilePath.join(path, name));
return openFilePlatform(FilePath.join(path, name));
}
}
@@ -130,7 +131,7 @@ Future<void> copyDirectory(Directory source, Directory destination) async {
if (content is File) {
content.copySync(newPath);
} else if (content is Directory) {
Directory newDirectory = Directory(newPath);
Directory newDirectory = openDirectoryPlatform(newPath);
newDirectory.createSync();
copyDirectory(content.absolute, newDirectory.absolute);
}
@@ -146,11 +147,11 @@ Future<void> copyDirectoryIsolate(
String findValidDirectoryName(String path, String directory) {
var name = sanitizeFileName(directory);
var dir = Directory("$path/$name");
var dir = openDirectoryPlatform("$path/$name");
var i = 1;
while (dir.existsSync() && dir.listSync().isNotEmpty) {
name = sanitizeFileName("$directory($i)");
dir = Directory("$path/$name");
dir = openDirectoryPlatform("$path/$name");
i++;
}
return name;
@@ -180,14 +181,14 @@ class DirectoryPicker {
if (App.isWindows || App.isLinux) {
directory = await file_selector.getDirectoryPath();
} else if (App.isAndroid) {
directory = await _methodChannel.invokeMethod<String?>("getDirectoryPath");
directory = (await AndroidDirectory.pickDirectory())?.path;
} else {
// ios, macos
directory = await _methodChannel.invokeMethod<String?>("getDirectoryPath");
}
if (directory == null) return null;
_finalizer.attach(this, directory);
return Directory(directory);
return openDirectoryPlatform(directory);
} finally {
Future.delayed(const Duration(milliseconds: 100), () {
IO._isSelectingFiles = false;
@@ -310,6 +311,30 @@ Future<void> saveFile(
}
}
Directory openDirectoryPlatform(String path) {
if(App.isAndroid) {
var dir = AndroidDirectory.fromPathSync(path);
if(dir == null) {
return Directory(path);
}
return dir;
} else {
return Directory(path);
}
}
File openFilePlatform(String path) {
if(App.isAndroid) {
var f = AndroidFile.fromPathSync(path);
if(f == null) {
return File(path);
}
return f;
} else {
return File(path);
}
}
class Share {
static void shareFile({
required Uint8List data,