mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
Feat/saf (#81)
* [Android] Use SAF to change local path * Use IOOverrides to replace openDirectoryPlatform and openFilePlatform * fix io
This commit is contained in:
@@ -25,7 +25,6 @@ import 'package:venera/network/cloudflare.dart';
|
||||
import 'package:venera/pages/comic_page.dart';
|
||||
import 'package:venera/pages/favorites/favorites_page.dart';
|
||||
import 'package:venera/utils/ext.dart';
|
||||
import 'package:venera/utils/io.dart';
|
||||
import 'package:venera/utils/tags_translation.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import 'dart:async' show Future, StreamController, scheduleMicrotask;
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:ui' as ui show Codec;
|
||||
import 'dart:ui';
|
||||
@@ -108,7 +107,7 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
|
||||
}
|
||||
}
|
||||
|
||||
static final _cache = LinkedHashMap<String, Uint8List>();
|
||||
static final _cache = <String, Uint8List>{};
|
||||
|
||||
static var _cacheSize = 0;
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import 'dart:async' show Future, StreamController;
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:venera/network/images.dart';
|
||||
@@ -25,7 +24,7 @@ class CachedImageProvider
|
||||
@override
|
||||
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
|
||||
if(url.startsWith("file://")) {
|
||||
var file = openFilePlatform(url.substring(7));
|
||||
var file = File(url.substring(7));
|
||||
return file.readAsBytes();
|
||||
}
|
||||
await for (var progress in ImageDownloader.loadThumbnail(url, sourceKey, cid)) {
|
||||
|
@@ -71,7 +71,7 @@ class LocalComic with HistoryMixin implements Comic {
|
||||
downloadedChapters = List.from(jsonDecode(row[8] as String)),
|
||||
createdAt = DateTime.fromMillisecondsSinceEpoch(row[9] as int);
|
||||
|
||||
File get coverFile => openFilePlatform(FilePath.join(
|
||||
File get coverFile => File(FilePath.join(
|
||||
baseDir,
|
||||
cover,
|
||||
));
|
||||
@@ -151,6 +151,8 @@ class LocalManager with ChangeNotifier {
|
||||
/// path to the directory where all the comics are stored
|
||||
late String path;
|
||||
|
||||
Directory get directory => Directory(path);
|
||||
|
||||
// return error message if failed
|
||||
Future<String?> setNewPath(String newPath) async {
|
||||
var newDir = Directory(newPath);
|
||||
@@ -162,7 +164,7 @@ class LocalManager with ChangeNotifier {
|
||||
}
|
||||
try {
|
||||
await copyDirectoryIsolate(
|
||||
Directory(path),
|
||||
directory,
|
||||
newDir,
|
||||
);
|
||||
await File(FilePath.join(App.dataPath, 'local_path')).writeAsString(newPath);
|
||||
@@ -170,7 +172,7 @@ class LocalManager with ChangeNotifier {
|
||||
Log.error("IO", e, s);
|
||||
return e.toString();
|
||||
}
|
||||
await Directory(path).deleteIgnoreError(recursive:true);
|
||||
await directory.deleteContents(recursive: true);
|
||||
path = newPath;
|
||||
return null;
|
||||
}
|
||||
@@ -217,15 +219,15 @@ class LocalManager with ChangeNotifier {
|
||||
''');
|
||||
if (File(FilePath.join(App.dataPath, 'local_path')).existsSync()) {
|
||||
path = File(FilePath.join(App.dataPath, 'local_path')).readAsStringSync();
|
||||
if (!Directory(path).existsSync()) {
|
||||
if (!directory.existsSync()) {
|
||||
path = await findDefaultPath();
|
||||
}
|
||||
} else {
|
||||
path = await findDefaultPath();
|
||||
}
|
||||
try {
|
||||
if (!Directory(path).existsSync()) {
|
||||
await Directory(path).create();
|
||||
if (!directory.existsSync()) {
|
||||
await directory.create();
|
||||
}
|
||||
}
|
||||
catch(e, s) {
|
||||
@@ -354,12 +356,12 @@ class LocalManager with ChangeNotifier {
|
||||
throw "Invalid ep";
|
||||
}
|
||||
var comic = find(id, type) ?? (throw "Comic Not Found");
|
||||
var directory = openDirectoryPlatform(comic.baseDir);
|
||||
var directory = Directory(comic.baseDir);
|
||||
if (comic.chapters != null) {
|
||||
var cid = ep is int
|
||||
? comic.chapters!.keys.elementAt(ep - 1)
|
||||
: (ep as String);
|
||||
directory = openDirectoryPlatform(FilePath.join(directory.path, cid));
|
||||
directory = Directory(FilePath.join(directory.path, cid));
|
||||
}
|
||||
var files = <File>[];
|
||||
await for (var entity in directory.list()) {
|
||||
@@ -406,10 +408,10 @@ class LocalManager with ChangeNotifier {
|
||||
String id, ComicType type, String name) async {
|
||||
var comic = find(id, type);
|
||||
if (comic != null) {
|
||||
return openDirectoryPlatform(FilePath.join(path, comic.directory));
|
||||
return Directory(FilePath.join(path, comic.directory));
|
||||
}
|
||||
var dir = findValidDirectoryName(path, name);
|
||||
return openDirectoryPlatform(FilePath.join(path, dir)).create().then((value) => value);
|
||||
return Directory(FilePath.join(path, dir)).create().then((value) => value);
|
||||
}
|
||||
|
||||
void completeTask(DownloadTask task) {
|
||||
@@ -468,7 +470,7 @@ class LocalManager with ChangeNotifier {
|
||||
|
||||
void deleteComic(LocalComic c, [bool removeFileOnDisk = true]) {
|
||||
if(removeFileOnDisk) {
|
||||
var dir = openDirectoryPlatform(FilePath.join(path, c.directory));
|
||||
var dir = Directory(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.
|
||||
|
@@ -20,6 +20,7 @@ void main(List<String> args) {
|
||||
if (runWebViewTitleBarWidget(args)) {
|
||||
return;
|
||||
}
|
||||
overrideIO(() {
|
||||
runZonedGuarded(() async {
|
||||
await Rhttp.init();
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -55,6 +56,7 @@ void main(List<String> args) {
|
||||
}, (error, stack) {
|
||||
Log.error("Unhandled Exception", "$error\n$stack");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
|
@@ -235,21 +235,22 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
||||
}
|
||||
|
||||
if (path == null) {
|
||||
try {
|
||||
var dir = await LocalManager().findValidDirectory(
|
||||
comicId,
|
||||
comicType,
|
||||
comic!.title,
|
||||
);
|
||||
if (!(await dir.exists())) {
|
||||
try {
|
||||
await dir.create();
|
||||
} catch (e) {
|
||||
}
|
||||
path = dir.path;
|
||||
} catch (e, s) {
|
||||
Log.error("Download", e.toString(), s);
|
||||
_setError("Error: $e");
|
||||
return;
|
||||
}
|
||||
}
|
||||
path = dir.path;
|
||||
}
|
||||
|
||||
await LocalManager().saveCurrentDownloadingTasks();
|
||||
|
||||
@@ -266,11 +267,13 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
||||
throw "Failed to download cover";
|
||||
}
|
||||
var fileType = detectFileType(data);
|
||||
var file = File(FilePath.join(path!, "cover${fileType.ext}"));
|
||||
var file =
|
||||
File(FilePath.join(path!, "cover${fileType.ext}"));
|
||||
file.writeAsBytesSync(data);
|
||||
return "file://${file.path}";
|
||||
});
|
||||
if (res.error) {
|
||||
Log.error("Download", res.errorMessage!);
|
||||
_setError("Error: ${res.errorMessage}");
|
||||
return;
|
||||
} else {
|
||||
@@ -294,6 +297,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
||||
return;
|
||||
}
|
||||
if (res.error) {
|
||||
Log.error("Download", res.errorMessage!);
|
||||
_setError("Error: ${res.errorMessage}");
|
||||
return;
|
||||
} else {
|
||||
@@ -323,6 +327,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
||||
return;
|
||||
}
|
||||
if (res.error) {
|
||||
Log.error("Download", res.errorMessage!);
|
||||
_setError("Error: ${res.errorMessage}");
|
||||
return;
|
||||
} else {
|
||||
@@ -347,6 +352,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
||||
return;
|
||||
}
|
||||
if (task.error != null) {
|
||||
Log.error("Download", task.error.toString());
|
||||
_setError("Error: ${task.error}");
|
||||
return;
|
||||
}
|
||||
@@ -375,7 +381,6 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
||||
_message = message;
|
||||
notifyListeners();
|
||||
stopRecorder();
|
||||
Log.error("Download", message);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -448,7 +453,8 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
||||
}).toList(),
|
||||
directory: Directory(path!).name,
|
||||
chapters: comic!.chapters,
|
||||
cover: File(_cover!.split("file://").last).uri.pathSegments.last,
|
||||
cover:
|
||||
File(_cover!.split("file://").last).name,
|
||||
comicType: ComicType(source.key.hashCode),
|
||||
downloadedChapters: chapters ?? [],
|
||||
createdAt: DateTime.now(),
|
||||
@@ -726,8 +732,7 @@ class ArchiveDownloadTask extends DownloadTask {
|
||||
isDownloaded = status.isFinished;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
catch(e) {
|
||||
} catch (e) {
|
||||
_setError("Error: $e");
|
||||
return;
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart';
|
||||
import 'package:venera/components/components.dart';
|
||||
@@ -11,9 +10,7 @@ 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/favorites.dart';
|
||||
import 'package:venera/foundation/local.dart';
|
||||
import 'package:venera/foundation/res.dart';
|
||||
import 'package:venera/network/download.dart';
|
||||
import 'package:venera/pages/comic_page.dart';
|
||||
import 'package:venera/utils/io.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
|
@@ -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(openFilePlatform(imageKey.replaceFirst("file://", '')));
|
||||
return FileImage(File(imageKey.replaceFirst("file://", '')));
|
||||
} else {
|
||||
return ReaderImageProvider(
|
||||
imageKey,
|
||||
|
@@ -469,7 +469,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
ImageProvider image;
|
||||
var imageKey = images[index];
|
||||
if (imageKey.startsWith('file://')) {
|
||||
image = FileImage(openFilePlatform(imageKey.replaceFirst("file://", '')));
|
||||
image = FileImage(File(imageKey.replaceFirst("file://", '')));
|
||||
} else {
|
||||
image = ReaderImageProvider(
|
||||
imageKey,
|
||||
@@ -515,7 +515,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
}
|
||||
}
|
||||
if (imageKey.startsWith("file://")) {
|
||||
return await openFilePlatform(imageKey.substring(7)).readAsBytes();
|
||||
return await File(imageKey.substring(7)).readAsBytes();
|
||||
} else {
|
||||
return (await CacheManager().findCache(
|
||||
"$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))!
|
||||
|
@@ -34,25 +34,8 @@ class _AppSettingsState extends State<AppSettings> {
|
||||
callback: () async {
|
||||
String? result;
|
||||
if (App.isAndroid) {
|
||||
var channel = const MethodChannel("venera/storage");
|
||||
var permission = await channel.invokeMethod('');
|
||||
if (permission != true) {
|
||||
context.showMessage(message: "Permission denied".tl);
|
||||
return;
|
||||
}
|
||||
var path = await selectDirectory();
|
||||
if (path != null) {
|
||||
// check if the path is writable
|
||||
var testFile = File(FilePath.join(path, "test"));
|
||||
try {
|
||||
await testFile.writeAsBytes([1]);
|
||||
await testFile.delete();
|
||||
} catch (e) {
|
||||
context.showMessage(message: "Permission denied".tl);
|
||||
return;
|
||||
}
|
||||
result = path;
|
||||
}
|
||||
var picker = DirectoryPicker();
|
||||
result = (await picker.pickDirectory())?.path;
|
||||
} else if (App.isIOS) {
|
||||
result = await selectDirectoryIOS();
|
||||
} else {
|
||||
|
@@ -187,7 +187,7 @@ abstract class CBZ {
|
||||
}
|
||||
int i = 1;
|
||||
for (var image in allImages) {
|
||||
var src = openFilePlatform(image);
|
||||
var src = File(image);
|
||||
var width = allImages.length.toString().length;
|
||||
var dstName =
|
||||
'${i.toString().padLeft(width, '0')}.${image.split('.').last}';
|
||||
|
@@ -60,7 +60,7 @@ class ImportComic {
|
||||
if (cancelled) {
|
||||
return imported;
|
||||
}
|
||||
var comicDir = openDirectoryPlatform(
|
||||
var comicDir = Directory(
|
||||
FilePath.join(comicSrc.path, comic['DIRNAME'] as String));
|
||||
String titleJP =
|
||||
comic['TITLE_JPN'] == null ? "" : comic['TITLE_JPN'] as String;
|
||||
@@ -217,7 +217,7 @@ class ImportComic {
|
||||
chapters.sort();
|
||||
if (hasChapters && coverPath == '') {
|
||||
// use the first image in the first chapter as the cover
|
||||
var firstChapter = openDirectoryPlatform('${directory.path}/${chapters.first}');
|
||||
var firstChapter = Directory('${directory.path}/${chapters.first}');
|
||||
await for (var entry in firstChapter.list()) {
|
||||
if (entry is File) {
|
||||
coverPath = entry.name;
|
||||
@@ -248,8 +248,8 @@ class ImportComic {
|
||||
var destination = data['destination'] as String;
|
||||
Map<String, String> result = {};
|
||||
for (var dir in toBeCopied) {
|
||||
var source = openDirectoryPlatform(dir);
|
||||
var dest = openDirectoryPlatform("$destination/${source.name}");
|
||||
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.
|
||||
|
@@ -81,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 openFilePlatform(f.path).length();
|
||||
total += await File(f.path).length();
|
||||
}
|
||||
}
|
||||
return total;
|
||||
@@ -93,7 +93,21 @@ extension DirectoryExtension on Directory {
|
||||
}
|
||||
|
||||
File joinFile(String name) {
|
||||
return openFilePlatform(FilePath.join(path, name));
|
||||
return File(FilePath.join(path, name));
|
||||
}
|
||||
|
||||
void deleteContentsSync({recursive = true}) {
|
||||
if (!existsSync()) return;
|
||||
for (var f in listSync()) {
|
||||
f.deleteIfExistsSync(recursive: recursive);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteContents({recursive = true}) async {
|
||||
if (!existsSync()) return;
|
||||
for (var f in listSync()) {
|
||||
await f.deleteIfExists(recursive: recursive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,14 +138,15 @@ String sanitizeFileName(String fileName) {
|
||||
Future<void> copyDirectory(Directory source, Directory destination) async {
|
||||
List<FileSystemEntity> contents = source.listSync();
|
||||
for (FileSystemEntity content in contents) {
|
||||
String newPath = destination.path +
|
||||
Platform.pathSeparator +
|
||||
content.path.split(Platform.pathSeparator).last;
|
||||
String newPath = FilePath.join(destination.path, content.name);
|
||||
|
||||
if (content is File) {
|
||||
content.copySync(newPath);
|
||||
var resultFile = File(newPath);
|
||||
resultFile.createSync();
|
||||
var data = content.readAsBytesSync();
|
||||
resultFile.writeAsBytesSync(data);
|
||||
} else if (content is Directory) {
|
||||
Directory newDirectory = openDirectoryPlatform(newPath);
|
||||
Directory newDirectory = Directory(newPath);
|
||||
newDirectory.createSync();
|
||||
copyDirectory(content.absolute, newDirectory.absolute);
|
||||
}
|
||||
@@ -140,18 +155,18 @@ Future<void> copyDirectory(Directory source, Directory destination) async {
|
||||
|
||||
Future<void> copyDirectoryIsolate(
|
||||
Directory source, Directory destination) async {
|
||||
await Isolate.run(() {
|
||||
copyDirectory(source, destination);
|
||||
await Isolate.run(() async {
|
||||
await copyDirectory(source, destination);
|
||||
});
|
||||
}
|
||||
|
||||
String findValidDirectoryName(String path, String directory) {
|
||||
var name = sanitizeFileName(directory);
|
||||
var dir = openDirectoryPlatform("$path/$name");
|
||||
var dir = Directory("$path/$name");
|
||||
var i = 1;
|
||||
while (dir.existsSync() && dir.listSync().isNotEmpty) {
|
||||
name = sanitizeFileName("$directory($i)");
|
||||
dir = openDirectoryPlatform("$path/$name");
|
||||
dir = Directory("$path/$name");
|
||||
i++;
|
||||
}
|
||||
return name;
|
||||
@@ -184,11 +199,12 @@ class DirectoryPicker {
|
||||
directory = (await AndroidDirectory.pickDirectory())?.path;
|
||||
} else {
|
||||
// ios, macos
|
||||
directory = await _methodChannel.invokeMethod<String?>("getDirectoryPath");
|
||||
directory =
|
||||
await _methodChannel.invokeMethod<String?>("getDirectoryPath");
|
||||
}
|
||||
if (directory == null) return null;
|
||||
_finalizer.attach(this, directory);
|
||||
return openDirectoryPlatform(directory);
|
||||
return Directory(directory);
|
||||
} finally {
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
IO._isSelectingFiles = false;
|
||||
@@ -311,31 +327,42 @@ Future<void> saveFile(
|
||||
}
|
||||
}
|
||||
|
||||
Directory openDirectoryPlatform(String path) {
|
||||
if(App.isAndroid) {
|
||||
class _IOOverrides extends IOOverrides {
|
||||
@override
|
||||
Directory createDirectory(String path) {
|
||||
if (App.isAndroid) {
|
||||
var dir = AndroidDirectory.fromPathSync(path);
|
||||
if(dir == null) {
|
||||
return Directory(path);
|
||||
if (dir == null) {
|
||||
return super.createDirectory(path);
|
||||
}
|
||||
return dir;
|
||||
} else {
|
||||
return Directory(path);
|
||||
return super.createDirectory(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File openFilePlatform(String path) {
|
||||
if(path.startsWith("file://")) {
|
||||
@override
|
||||
File createFile(String path) {
|
||||
if (path.startsWith("file://")) {
|
||||
path = path.substring(7);
|
||||
}
|
||||
if(App.isAndroid) {
|
||||
if (App.isAndroid) {
|
||||
var f = AndroidFile.fromPathSync(path);
|
||||
if(f == null) {
|
||||
return File(path);
|
||||
if (f == null) {
|
||||
return super.createFile(path);
|
||||
}
|
||||
return f;
|
||||
} else {
|
||||
return File(path);
|
||||
return super.createFile(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void overrideIO(void Function() f) {
|
||||
IOOverrides.runWithIOOverrides(
|
||||
f,
|
||||
_IOOverrides(),
|
||||
);
|
||||
}
|
||||
|
||||
class Share {
|
||||
|
@@ -393,8 +393,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "829a566b738a26ea98e523807f49838e21308543"
|
||||
resolved-ref: "829a566b738a26ea98e523807f49838e21308543"
|
||||
ref: dd5242918da0ea9a0a50b0f87ade7a2def65453d
|
||||
resolved-ref: dd5242918da0ea9a0a50b0f87ade7a2def65453d
|
||||
url: "https://github.com/pkuislm/flutter_saf.git"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
|
Reference in New Issue
Block a user