mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
1
debian/gui/venera.desktop
vendored
1
debian/gui/venera.desktop
vendored
@@ -1,5 +1,4 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Version={{Version}}
|
|
||||||
Name=Venera
|
Name=Venera
|
||||||
GenericName=Venera
|
GenericName=Venera
|
||||||
Comment=venera
|
Comment=venera
|
||||||
|
@@ -342,21 +342,39 @@ class ComicTile extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<String> _splitText(String text) {
|
List<String> _splitText(String text) {
|
||||||
// split text by space, comma. text in brackets will be kept together.
|
// split text by comma, brackets
|
||||||
var words = <String>[];
|
var words = <String>[];
|
||||||
var buffer = StringBuffer();
|
var buffer = StringBuffer();
|
||||||
var inBracket = false;
|
var inBracket = false;
|
||||||
|
String? prevBracket;
|
||||||
for (var i = 0; i < text.length; i++) {
|
for (var i = 0; i < text.length; i++) {
|
||||||
var c = text[i];
|
var c = text[i];
|
||||||
if (c == '[' || c == '(') {
|
if (c == '[' || c == '(') {
|
||||||
inBracket = true;
|
|
||||||
} else if (c == ']' || c == ')') {
|
|
||||||
inBracket = false;
|
|
||||||
} else if (c == ' ' || c == ',') {
|
|
||||||
if (inBracket) {
|
if (inBracket) {
|
||||||
buffer.write(c);
|
buffer.write(c);
|
||||||
} else {
|
} else {
|
||||||
words.add(buffer.toString());
|
if (buffer.isNotEmpty) {
|
||||||
|
words.add(buffer.toString().trim());
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
inBracket = true;
|
||||||
|
prevBracket = c;
|
||||||
|
}
|
||||||
|
} else if (c == ']' || c == ')') {
|
||||||
|
if (prevBracket == '[' && c == ']' || prevBracket == '(' && c == ')') {
|
||||||
|
if (buffer.isNotEmpty) {
|
||||||
|
words.add(buffer.toString().trim());
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
inBracket = false;
|
||||||
|
} else {
|
||||||
|
buffer.write(c);
|
||||||
|
}
|
||||||
|
} else if (c == ',') {
|
||||||
|
if (inBracket) {
|
||||||
|
buffer.write(c);
|
||||||
|
} else {
|
||||||
|
words.add(buffer.toString().trim());
|
||||||
buffer.clear();
|
buffer.clear();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -364,8 +382,10 @@ class ComicTile extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (buffer.isNotEmpty) {
|
if (buffer.isNotEmpty) {
|
||||||
words.add(buffer.toString());
|
words.add(buffer.toString().trim());
|
||||||
}
|
}
|
||||||
|
words.removeWhere((element) => element == "");
|
||||||
|
words = words.toSet().toList();
|
||||||
return words;
|
return words;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,26 +403,33 @@ class ComicTile extends StatelessWidget {
|
|||||||
return StatefulBuilder(builder: (context, setState) {
|
return StatefulBuilder(builder: (context, setState) {
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: 'Block'.tl,
|
title: 'Block'.tl,
|
||||||
content: Wrap(
|
content: ConstrainedBox(
|
||||||
runSpacing: 8,
|
constraints: BoxConstraints(
|
||||||
spacing: 8,
|
maxHeight: math.min(400, context.height - 136),
|
||||||
children: [
|
),
|
||||||
for (var word in all)
|
child: SingleChildScrollView(
|
||||||
OptionChip(
|
child: Wrap(
|
||||||
text: word,
|
runSpacing: 8,
|
||||||
isSelected: words.contains(word),
|
spacing: 8,
|
||||||
onTap: () {
|
children: [
|
||||||
setState(() {
|
for (var word in all)
|
||||||
if (!words.contains(word)) {
|
OptionChip(
|
||||||
words.add(word);
|
text: word,
|
||||||
} else {
|
isSelected: words.contains(word),
|
||||||
words.remove(word);
|
onTap: () {
|
||||||
}
|
setState(() {
|
||||||
});
|
if (!words.contains(word)) {
|
||||||
},
|
words.add(word);
|
||||||
),
|
} else {
|
||||||
],
|
words.remove(word);
|
||||||
).paddingHorizontal(16),
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).paddingHorizontal(16),
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
Button.filled(
|
Button.filled(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -833,6 +860,7 @@ class ComicList extends StatefulWidget {
|
|||||||
this.menuBuilder,
|
this.menuBuilder,
|
||||||
this.controller,
|
this.controller,
|
||||||
this.refreshHandlerCallback,
|
this.refreshHandlerCallback,
|
||||||
|
this.enablePageStorage = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Future<Res<List<Comic>>> Function(int page)? loadPage;
|
final Future<Res<List<Comic>>> Function(int page)? loadPage;
|
||||||
@@ -851,6 +879,8 @@ class ComicList extends StatefulWidget {
|
|||||||
|
|
||||||
final void Function(VoidCallback c)? refreshHandlerCallback;
|
final void Function(VoidCallback c)? refreshHandlerCallback;
|
||||||
|
|
||||||
|
final bool enablePageStorage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ComicList> createState() => ComicListState();
|
State<ComicList> createState() => ComicListState();
|
||||||
}
|
}
|
||||||
@@ -868,6 +898,8 @@ class ComicListState extends State<ComicList> {
|
|||||||
|
|
||||||
String? _nextUrl;
|
String? _nextUrl;
|
||||||
|
|
||||||
|
late bool enablePageStorage = widget.enablePageStorage;
|
||||||
|
|
||||||
Map<String, dynamic> get state => {
|
Map<String, dynamic> get state => {
|
||||||
'maxPage': _maxPage,
|
'maxPage': _maxPage,
|
||||||
'data': _data,
|
'data': _data,
|
||||||
@@ -878,7 +910,7 @@ class ComicListState extends State<ComicList> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
void restoreState(Map<String, dynamic>? state) {
|
void restoreState(Map<String, dynamic>? state) {
|
||||||
if (state == null) {
|
if (state == null || !enablePageStorage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_maxPage = state['maxPage'];
|
_maxPage = state['maxPage'];
|
||||||
@@ -892,7 +924,9 @@ class ComicListState extends State<ComicList> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void storeState() {
|
void storeState() {
|
||||||
PageStorage.of(context).writeState(context, state);
|
if(enablePageStorage) {
|
||||||
|
PageStorage.of(context).writeState(context, state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void refresh() {
|
void refresh() {
|
||||||
@@ -1122,7 +1156,7 @@ class ComicListState extends State<ComicList> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return SmoothCustomScrollView(
|
return SmoothCustomScrollView(
|
||||||
key: const PageStorageKey('scroll'),
|
key: enablePageStorage ? PageStorageKey('scroll$_page') : null,
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
slivers: [
|
slivers: [
|
||||||
if (widget.leadingSliver != null) widget.leadingSliver!,
|
if (widget.leadingSliver != null) widget.leadingSliver!,
|
||||||
|
@@ -290,6 +290,7 @@ class ContentDialog extends StatelessWidget {
|
|||||||
: const EdgeInsets.symmetric(horizontal: 16),
|
: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
shadowColor: context.colorScheme.shadow,
|
shadowColor: context.colorScheme.shadow,
|
||||||
|
backgroundColor: context.colorScheme.surface,
|
||||||
child: AnimatedSize(
|
child: AnimatedSize(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
|
@@ -10,7 +10,7 @@ export "widget_utils.dart";
|
|||||||
export "context.dart";
|
export "context.dart";
|
||||||
|
|
||||||
class _App {
|
class _App {
|
||||||
final version = "1.1.2";
|
final version = "1.1.3";
|
||||||
|
|
||||||
bool get isAndroid => Platform.isAndroid;
|
bool get isAndroid => Platform.isAndroid;
|
||||||
|
|
||||||
|
@@ -201,8 +201,6 @@ class HistoryManager with ChangeNotifier {
|
|||||||
|
|
||||||
Map<String, bool>? _cachedHistory;
|
Map<String, bool>? _cachedHistory;
|
||||||
|
|
||||||
static const _kMaxHistoryLength = 200;
|
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
_db = sqlite3.open("${App.dataPath}/history.db");
|
_db = sqlite3.open("${App.dataPath}/history.db");
|
||||||
|
|
||||||
@@ -228,12 +226,6 @@ class HistoryManager with ChangeNotifier {
|
|||||||
///
|
///
|
||||||
/// This function would be called when user start reading.
|
/// This function would be called when user start reading.
|
||||||
Future<void> addHistory(History newItem) async {
|
Future<void> addHistory(History newItem) async {
|
||||||
while(count() >= _kMaxHistoryLength) {
|
|
||||||
_db.execute("""
|
|
||||||
delete from history
|
|
||||||
where time == (select min(time) from history);
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
_db.execute("""
|
_db.execute("""
|
||||||
insert or replace into history (id, title, subtitle, cover, time, type, ep, page, readEpisode, max_page)
|
insert or replace into history (id, title, subtitle, cover, time, type, ep, page, readEpisode, max_page)
|
||||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||||
|
@@ -76,15 +76,16 @@ class LocalComic with HistoryMixin implements Comic {
|
|||||||
cover,
|
cover,
|
||||||
));
|
));
|
||||||
|
|
||||||
String get baseDir => (directory.contains('/') || directory.contains('\\')) ? directory : FilePath.join(LocalManager().path, directory);
|
String get baseDir => (directory.contains('/') || directory.contains('\\'))
|
||||||
|
? directory
|
||||||
|
: FilePath.join(LocalManager().path, directory);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get description => "";
|
String get description => "";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sourceKey => comicType == ComicType.local
|
String get sourceKey =>
|
||||||
? "local"
|
comicType == ComicType.local ? "local" : comicType.sourceKey;
|
||||||
: comicType.sourceKey;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
@@ -112,11 +113,12 @@ class LocalComic with HistoryMixin implements Comic {
|
|||||||
chapters: chapters,
|
chapters: chapters,
|
||||||
initialChapter: history?.ep,
|
initialChapter: history?.ep,
|
||||||
initialPage: history?.page,
|
initialPage: history?.page,
|
||||||
history: history ?? History.fromModel(
|
history: history ??
|
||||||
model: this,
|
History.fromModel(
|
||||||
ep: 0,
|
model: this,
|
||||||
page: 0,
|
ep: 0,
|
||||||
),
|
page: 0,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -153,6 +155,15 @@ class LocalManager with ChangeNotifier {
|
|||||||
|
|
||||||
Directory get directory => Directory(path);
|
Directory get directory => Directory(path);
|
||||||
|
|
||||||
|
void _checkNoMedia() {
|
||||||
|
if (App.isAndroid) {
|
||||||
|
var file = File(FilePath.join(path, '.nomedia'));
|
||||||
|
if (!file.existsSync()) {
|
||||||
|
file.createSync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// return error message if failed
|
// return error message if failed
|
||||||
Future<String?> setNewPath(String newPath) async {
|
Future<String?> setNewPath(String newPath) async {
|
||||||
var newDir = Directory(newPath);
|
var newDir = Directory(newPath);
|
||||||
@@ -167,13 +178,15 @@ class LocalManager with ChangeNotifier {
|
|||||||
directory,
|
directory,
|
||||||
newDir,
|
newDir,
|
||||||
);
|
);
|
||||||
await File(FilePath.join(App.dataPath, 'local_path')).writeAsString(newPath);
|
await File(FilePath.join(App.dataPath, 'local_path'))
|
||||||
|
.writeAsString(newPath);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("IO", e, s);
|
Log.error("IO", e, s);
|
||||||
return e.toString();
|
return e.toString();
|
||||||
}
|
}
|
||||||
await directory.deleteContents(recursive: true);
|
await directory.deleteContents(recursive: true);
|
||||||
path = newPath;
|
path = newPath;
|
||||||
|
_checkNoMedia();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +200,8 @@ class LocalManager with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
} else if (App.isIOS) {
|
} else if (App.isIOS) {
|
||||||
var oldPath = FilePath.join(App.dataPath, 'local');
|
var oldPath = FilePath.join(App.dataPath, 'local');
|
||||||
if (Directory(oldPath).existsSync() && Directory(oldPath).listSync().isNotEmpty) {
|
if (Directory(oldPath).existsSync() &&
|
||||||
|
Directory(oldPath).listSync().isNotEmpty) {
|
||||||
return oldPath;
|
return oldPath;
|
||||||
} else {
|
} else {
|
||||||
var directory = await getApplicationDocumentsDirectory();
|
var directory = await getApplicationDocumentsDirectory();
|
||||||
@@ -198,6 +212,18 @@ class LocalManager with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _checkPathValidation() async {
|
||||||
|
var testFile = File(FilePath.join(path, 'venera_test'));
|
||||||
|
try {
|
||||||
|
testFile.createSync();
|
||||||
|
testFile.deleteSync();
|
||||||
|
} catch (e) {
|
||||||
|
Log.error("IO",
|
||||||
|
"Failed to create test file in local path: $e\nUsing default path instead.");
|
||||||
|
path = await findDefaultPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
_db = sqlite3.open(
|
_db = sqlite3.open(
|
||||||
'${App.dataPath}/local.db',
|
'${App.dataPath}/local.db',
|
||||||
@@ -229,10 +255,11 @@ class LocalManager with ChangeNotifier {
|
|||||||
if (!directory.existsSync()) {
|
if (!directory.existsSync()) {
|
||||||
await directory.create();
|
await directory.create();
|
||||||
}
|
}
|
||||||
}
|
} catch (e, s) {
|
||||||
catch(e, s) {
|
|
||||||
Log.error("IO", "Failed to create local folder: $e", s);
|
Log.error("IO", "Failed to create local folder: $e", s);
|
||||||
}
|
}
|
||||||
|
_checkPathValidation();
|
||||||
|
_checkNoMedia();
|
||||||
restoreDownloadingTasks();
|
restoreDownloadingTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +269,8 @@ class LocalManager with ChangeNotifier {
|
|||||||
SELECT id FROM comics WHERE comic_type = ?
|
SELECT id FROM comics WHERE comic_type = ?
|
||||||
ORDER BY CAST(id AS INTEGER) DESC
|
ORDER BY CAST(id AS INTEGER) DESC
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
''', [type.value],
|
''',
|
||||||
|
[type.value],
|
||||||
);
|
);
|
||||||
if (res.isEmpty) {
|
if (res.isEmpty) {
|
||||||
return '1';
|
return '1';
|
||||||
@@ -352,15 +380,14 @@ class LocalManager with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>> getImages(String id, ComicType type, Object ep) async {
|
Future<List<String>> getImages(String id, ComicType type, Object ep) async {
|
||||||
if(ep is! String && ep is! int) {
|
if (ep is! String && ep is! int) {
|
||||||
throw "Invalid ep";
|
throw "Invalid ep";
|
||||||
}
|
}
|
||||||
var comic = find(id, type) ?? (throw "Comic Not Found");
|
var comic = find(id, type) ?? (throw "Comic Not Found");
|
||||||
var directory = Directory(comic.baseDir);
|
var directory = Directory(comic.baseDir);
|
||||||
if (comic.chapters != null) {
|
if (comic.chapters != null) {
|
||||||
var cid = ep is int
|
var cid =
|
||||||
? comic.chapters!.keys.elementAt(ep - 1)
|
ep is int ? comic.chapters!.keys.elementAt(ep - 1) : (ep as String);
|
||||||
: (ep as String);
|
|
||||||
directory = Directory(FilePath.join(directory.path, cid));
|
directory = Directory(FilePath.join(directory.path, cid));
|
||||||
}
|
}
|
||||||
var files = <File>[];
|
var files = <File>[];
|
||||||
@@ -372,7 +399,7 @@ class LocalManager with ChangeNotifier {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
//Hidden file in some file system
|
//Hidden file in some file system
|
||||||
if(entity.name.startsWith('.')) {
|
if (entity.name.startsWith('.')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
files.add(entity);
|
files.add(entity);
|
||||||
@@ -394,7 +421,7 @@ class LocalManager with ChangeNotifier {
|
|||||||
if (comic == null) return false;
|
if (comic == null) return false;
|
||||||
if (comic.chapters == null || ep == null) return true;
|
if (comic.chapters == null || ep == null) return true;
|
||||||
return comic.downloadedChapters
|
return comic.downloadedChapters
|
||||||
.contains(comic.chapters!.keys.elementAt(ep-1));
|
.contains(comic.chapters!.keys.elementAt(ep - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
List<DownloadTask> downloadingTasks = [];
|
List<DownloadTask> downloadingTasks = [];
|
||||||
@@ -451,12 +478,17 @@ class LocalManager with ChangeNotifier {
|
|||||||
void restoreDownloadingTasks() {
|
void restoreDownloadingTasks() {
|
||||||
var file = File(FilePath.join(App.dataPath, 'downloading_tasks.json'));
|
var file = File(FilePath.join(App.dataPath, 'downloading_tasks.json'));
|
||||||
if (file.existsSync()) {
|
if (file.existsSync()) {
|
||||||
var tasks = jsonDecode(file.readAsStringSync());
|
try {
|
||||||
for (var e in tasks) {
|
var tasks = jsonDecode(file.readAsStringSync());
|
||||||
var task = DownloadTask.fromJson(e);
|
for (var e in tasks) {
|
||||||
if (task != null) {
|
var task = DownloadTask.fromJson(e);
|
||||||
downloadingTasks.add(task);
|
if (task != null) {
|
||||||
|
downloadingTasks.add(task);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
file.delete();
|
||||||
|
Log.error("LocalManager", "Failed to restore downloading tasks: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -469,13 +501,13 @@ class LocalManager with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void deleteComic(LocalComic c, [bool removeFileOnDisk = true]) {
|
void deleteComic(LocalComic c, [bool removeFileOnDisk = true]) {
|
||||||
if(removeFileOnDisk) {
|
if (removeFileOnDisk) {
|
||||||
var dir = Directory(FilePath.join(path, c.directory));
|
var dir = Directory(FilePath.join(path, c.directory));
|
||||||
dir.deleteIgnoreError(recursive: true);
|
dir.deleteIgnoreError(recursive: true);
|
||||||
}
|
}
|
||||||
// Deleting a local comic means that it's nolonger available, thus both favorite and history should be deleted.
|
// Deleting a local comic means that it's nolonger available, thus both favorite and history should be deleted.
|
||||||
if(c.comicType == ComicType.local) {
|
if (c.comicType == ComicType.local) {
|
||||||
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);
|
||||||
}
|
}
|
||||||
var folders = LocalFavoritesManager().find(c.id, c.comicType);
|
var folders = LocalFavoritesManager().find(c.id, c.comicType);
|
||||||
|
@@ -6,23 +6,37 @@ import 'package:venera/foundation/favorites.dart';
|
|||||||
import 'package:venera/foundation/history.dart';
|
import 'package:venera/foundation/history.dart';
|
||||||
import 'package:venera/foundation/js_engine.dart';
|
import 'package:venera/foundation/js_engine.dart';
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
|
import 'package:venera/foundation/log.dart';
|
||||||
import 'package:venera/network/cookie_jar.dart';
|
import 'package:venera/network/cookie_jar.dart';
|
||||||
import 'package:venera/utils/tags_translation.dart';
|
import 'package:venera/utils/tags_translation.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
import 'foundation/appdata.dart';
|
import 'foundation/appdata.dart';
|
||||||
|
|
||||||
|
extension FutureInit<T> on Future<T> {
|
||||||
|
/// Prevent unhandled exception
|
||||||
|
///
|
||||||
|
/// A unhandled exception occurred in init() will cause the app to crash.
|
||||||
|
Future<void> wait() async {
|
||||||
|
try {
|
||||||
|
await this;
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("init", "$e\n$s");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
await SAFTaskWorker().init();
|
await SAFTaskWorker().init().wait();
|
||||||
await AppTranslation.init();
|
await AppTranslation.init().wait();
|
||||||
await appdata.init();
|
await appdata.init().wait();
|
||||||
await App.init();
|
await App.init().wait();
|
||||||
await HistoryManager().init();
|
await HistoryManager().init().wait();
|
||||||
await TagsTranslation.readData();
|
await TagsTranslation.readData().wait();
|
||||||
await LocalFavoritesManager().init();
|
await LocalFavoritesManager().init().wait();
|
||||||
SingleInstanceCookieJar("${App.dataPath}/cookie.db");
|
SingleInstanceCookieJar("${App.dataPath}/cookie.db");
|
||||||
await JsEngine().init();
|
await JsEngine().init().wait();
|
||||||
await ComicSource.init();
|
await ComicSource.init().wait();
|
||||||
await LocalManager().init();
|
await LocalManager().init().wait();
|
||||||
CacheManager().setLimitSize(appdata.settings['cacheSize']);
|
CacheManager().setLimitSize(appdata.settings['cacheSize']);
|
||||||
}
|
}
|
@@ -156,30 +156,40 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
home = const MainPage();
|
home = const MainPage();
|
||||||
}
|
}
|
||||||
return DynamicColorBuilder(builder: (light, dark) {
|
return DynamicColorBuilder(builder: (light, dark) {
|
||||||
if (appdata.settings['color'] != 'system' || light == null || dark == null) {
|
if (appdata.settings['color'] != 'system' ||
|
||||||
|
light == null ||
|
||||||
|
dark == null) {
|
||||||
var color = translateColorSetting();
|
var color = translateColorSetting();
|
||||||
light = ColorScheme.fromSeed(
|
light = ColorScheme.fromSeed(
|
||||||
seedColor: color,
|
seedColor: color,
|
||||||
|
surface: Colors.white,
|
||||||
);
|
);
|
||||||
dark = ColorScheme.fromSeed(
|
dark = ColorScheme.fromSeed(
|
||||||
seedColor: color,
|
seedColor: color,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
|
surface: Colors.black,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
light = ColorScheme.fromSeed(
|
||||||
|
seedColor: light.primary,
|
||||||
|
surface: Colors.white,
|
||||||
|
);
|
||||||
|
dark = ColorScheme.fromSeed(
|
||||||
|
seedColor: dark.primary,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
surface: Colors.black,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
home: home,
|
home: home,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
colorScheme: light.copyWith(
|
colorScheme: light,
|
||||||
surface: Colors.white,
|
|
||||||
),
|
|
||||||
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
|
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
|
||||||
),
|
),
|
||||||
navigatorKey: App.rootNavigatorKey,
|
navigatorKey: App.rootNavigatorKey,
|
||||||
darkTheme: ThemeData(
|
darkTheme: ThemeData(
|
||||||
colorScheme: dark.copyWith(
|
colorScheme: dark,
|
||||||
surface: Colors.black,
|
|
||||||
),
|
|
||||||
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
|
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
|
||||||
),
|
),
|
||||||
themeMode: switch (appdata.settings['theme_mode']) {
|
themeMode: switch (appdata.settings['theme_mode']) {
|
||||||
@@ -211,8 +221,8 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
],
|
],
|
||||||
builder: (context, widget) {
|
builder: (context, widget) {
|
||||||
ErrorWidget.builder = (details) {
|
ErrorWidget.builder = (details) {
|
||||||
Log.error(
|
Log.error("Unhandled Exception",
|
||||||
"Unhandled Exception", "${details.exception}\n${details.stack}");
|
"${details.exception}\n${details.stack}");
|
||||||
return Material(
|
return Material(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(details.exception.toString()),
|
child: Text(details.exception.toString()),
|
||||||
|
@@ -163,6 +163,9 @@ class _BodyState extends State<_Body> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
current = item.value['options']
|
||||||
|
.firstWhere((e) => e['value'] == current)['text'] ?? current;
|
||||||
}
|
}
|
||||||
yield ListTile(
|
yield ListTile(
|
||||||
title: Text((item.value['title'] as String).ts(source.key)),
|
title: Text((item.value['title'] as String).ts(source.key)),
|
||||||
|
@@ -295,6 +295,7 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage>
|
|||||||
);
|
);
|
||||||
} else if (data.loadPage != null || data.loadNext != null) {
|
} else if (data.loadPage != null || data.loadNext != null) {
|
||||||
return ComicList(
|
return ComicList(
|
||||||
|
enablePageStorage: true,
|
||||||
loadPage: data.loadPage,
|
loadPage: data.loadPage,
|
||||||
loadNext: data.loadNext,
|
loadNext: data.loadNext,
|
||||||
key: const PageStorageKey("comic_list"),
|
key: const PageStorageKey("comic_list"),
|
||||||
|
@@ -146,6 +146,18 @@ Future<List<FavoriteItem>> updateComicsInfo(String folder) async {
|
|||||||
|
|
||||||
var newInfo = (await comicSource.loadComicInfo!(c.id)).data;
|
var newInfo = (await comicSource.loadComicInfo!(c.id)).data;
|
||||||
|
|
||||||
|
var newTags = <String>[];
|
||||||
|
for(var entry in newInfo.tags.entries) {
|
||||||
|
const shouldIgnore = ['author', 'artist', 'time'];
|
||||||
|
var namespace = entry.key;
|
||||||
|
if (shouldIgnore.contains(namespace.toLowerCase())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for(var tag in entry.value) {
|
||||||
|
newTags.add("$namespace:$tag");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
comics[index] = FavoriteItem(
|
comics[index] = FavoriteItem(
|
||||||
id: c.id,
|
id: c.id,
|
||||||
name: newInfo.title,
|
name: newInfo.title,
|
||||||
@@ -154,7 +166,7 @@ Future<List<FavoriteItem>> updateComicsInfo(String folder) async {
|
|||||||
newInfo.tags['author']?.firstOrNull ??
|
newInfo.tags['author']?.firstOrNull ??
|
||||||
c.author,
|
c.author,
|
||||||
type: c.type,
|
type: c.type,
|
||||||
tags: c.tags,
|
tags: newTags,
|
||||||
);
|
);
|
||||||
|
|
||||||
LocalFavoritesManager().updateInfo(folder, comics[index]);
|
LocalFavoritesManager().updateInfo(folder, comics[index]);
|
||||||
|
@@ -102,10 +102,13 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scrollController = ScrollController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var body = Scaffold(
|
Widget body = SmoothCustomScrollView(
|
||||||
body: SmoothCustomScrollView(slivers: [
|
controller: scrollController,
|
||||||
|
slivers: [
|
||||||
if (!searchMode && !multiSelectMode)
|
if (!searchMode && !multiSelectMode)
|
||||||
SliverAppbar(
|
SliverAppbar(
|
||||||
style: context.width < changePoint
|
style: context.width < changePoint
|
||||||
@@ -387,6 +390,19 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (appdata.settings["onClickFavorite"] == "viewDetail")
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.menu_book_outlined,
|
||||||
|
text: "Read".tl,
|
||||||
|
onClick: () {
|
||||||
|
App.mainNavigatorKey?.currentContext?.to(
|
||||||
|
() => ReaderWithLoading(
|
||||||
|
id: c.id,
|
||||||
|
sourceKey: c.sourceKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
onTap: (c) {
|
onTap: (c) {
|
||||||
@@ -447,7 +463,17 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]),
|
],
|
||||||
|
);
|
||||||
|
body = Scrollbar(
|
||||||
|
controller: scrollController,
|
||||||
|
thickness: App.isDesktop ? 8 : 12,
|
||||||
|
radius: const Radius.circular(8),
|
||||||
|
interactive: true,
|
||||||
|
child: ScrollConfiguration(
|
||||||
|
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||||
|
child: body,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: !multiSelectMode && !searchMode,
|
canPop: !multiSelectMode && !searchMode,
|
||||||
|
@@ -166,6 +166,7 @@ class _NormalFavoritePageState extends State<_NormalFavoritePage> {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
enablePageStorage: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -548,6 +549,7 @@ class _FavoriteFolder extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ComicList(
|
return ComicList(
|
||||||
key: comicListKey,
|
key: comicListKey,
|
||||||
|
enablePageStorage: true,
|
||||||
leadingSliver: SliverAppbar(
|
leadingSliver: SliverAppbar(
|
||||||
title: Text(title),
|
title: Text(title),
|
||||||
actions: [
|
actions: [
|
||||||
|
@@ -4,6 +4,7 @@ import 'package:venera/foundation/app.dart';
|
|||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.dart';
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
|
import 'package:venera/pages/comic_page.dart';
|
||||||
import 'package:venera/pages/downloading_page.dart';
|
import 'package:venera/pages/downloading_page.dart';
|
||||||
import 'package:venera/pages/favorites/favorites_page.dart';
|
import 'package:venera/pages/favorites/favorites_page.dart';
|
||||||
import 'package:venera/utils/cbz.dart';
|
import 'package:venera/utils/cbz.dart';
|
||||||
@@ -140,6 +141,19 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
addFavorite(selectedComics.keys.toList());
|
addFavorite(selectedComics.keys.toList());
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (selectedComics.length == 1)
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.chrome_reader_mode_outlined,
|
||||||
|
text: "View Detail".tl,
|
||||||
|
onClick: () {
|
||||||
|
context.to(() => ComicPage(
|
||||||
|
id: selectedComics.keys.first.id,
|
||||||
|
sourceKey: selectedComics.keys.first.sourceKey,
|
||||||
|
));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (selectedComics.length == 1)
|
||||||
|
...exportActions(selectedComics.keys.first),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,59 +196,63 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
buildMultiSelectMenu(),
|
buildMultiSelectMenu(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
List<Widget> normalActions = [
|
||||||
|
Tooltip(
|
||||||
|
message: "Search".tl,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.search),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
searchMode = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tooltip(
|
||||||
|
message: "Sort".tl,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.sort),
|
||||||
|
onPressed: sort,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tooltip(
|
||||||
|
message: "Downloading".tl,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.download),
|
||||||
|
onPressed: () {
|
||||||
|
showPopUpWidget(context, const DownloadingPage());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
var body = Scaffold(
|
var body = Scaffold(
|
||||||
body: SmoothCustomScrollView(
|
body: SmoothCustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
if (!searchMode && !multiSelectMode)
|
if (!searchMode)
|
||||||
SliverAppbar(
|
|
||||||
title: Text("Local".tl),
|
|
||||||
actions: [
|
|
||||||
Tooltip(
|
|
||||||
message: "Search".tl,
|
|
||||||
child: IconButton(
|
|
||||||
icon: const Icon(Icons.search),
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
searchMode = true;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Tooltip(
|
|
||||||
message: "Sort".tl,
|
|
||||||
child: IconButton(
|
|
||||||
icon: const Icon(Icons.sort),
|
|
||||||
onPressed: sort,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Tooltip(
|
|
||||||
message: "Downloading".tl,
|
|
||||||
child: IconButton(
|
|
||||||
icon: const Icon(Icons.download),
|
|
||||||
onPressed: () {
|
|
||||||
showPopUpWidget(context, const DownloadingPage());
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
else if (multiSelectMode)
|
|
||||||
SliverAppbar(
|
SliverAppbar(
|
||||||
leading: Tooltip(
|
leading: Tooltip(
|
||||||
message: "Cancel".tl,
|
message: multiSelectMode ? "Cancel".tl : "Back".tl,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: const Icon(Icons.close),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
if (multiSelectMode) {
|
||||||
multiSelectMode = false;
|
setState(() {
|
||||||
selectedComics.clear();
|
multiSelectMode = false;
|
||||||
});
|
selectedComics.clear();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
icon: multiSelectMode
|
||||||
|
? const Icon(Icons.close)
|
||||||
|
: const Icon(Icons.arrow_back),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: multiSelectMode
|
||||||
"Selected @c comics".tlParams({"c": selectedComics.length})),
|
? Text(selectedComics.length.toString())
|
||||||
actions: selectActions,
|
: Text("Local".tl),
|
||||||
|
actions: multiSelectMode ? selectActions : normalActions,
|
||||||
)
|
)
|
||||||
else if (searchMode)
|
else if (searchMode)
|
||||||
SliverAppbar(
|
SliverAppbar(
|
||||||
@@ -273,14 +291,14 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onTap: (c) {
|
onTap: (c) {
|
||||||
if(multiSelectMode) {
|
if (multiSelectMode) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (selectedComics.containsKey(c as LocalComic)) {
|
if (selectedComics.containsKey(c as LocalComic)) {
|
||||||
selectedComics.remove(c);
|
selectedComics.remove(c);
|
||||||
} else {
|
} else {
|
||||||
selectedComics[c] = true;
|
selectedComics[c] = true;
|
||||||
}
|
}
|
||||||
if(selectedComics.isEmpty) {
|
if (selectedComics.isEmpty) {
|
||||||
multiSelectMode = false;
|
multiSelectMode = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -291,88 +309,20 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
menuBuilder: (c) {
|
menuBuilder: (c) {
|
||||||
return [
|
return [
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
icon: Icons.delete,
|
icon: Icons.delete,
|
||||||
text: "Delete".tl,
|
text: "Delete".tl,
|
||||||
onClick: () {
|
onClick: () {
|
||||||
deleteComics([c as LocalComic]).then((value) {
|
deleteComics([c as LocalComic]).then((value) {
|
||||||
if (value && multiSelectMode) {
|
if (value && multiSelectMode) {
|
||||||
setState(() {
|
setState(() {
|
||||||
multiSelectMode = false;
|
multiSelectMode = false;
|
||||||
selectedComics.clear();
|
selectedComics.clear();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
MenuEntry(
|
|
||||||
icon: Icons.outbox_outlined,
|
|
||||||
text: "Export as cbz".tl,
|
|
||||||
onClick: () async {
|
|
||||||
var controller = showLoadingDialog(
|
|
||||||
context,
|
|
||||||
allowCancel: false,
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
var file = await CBZ.export(c as LocalComic);
|
|
||||||
await saveFile(filename: file.name, file: file);
|
|
||||||
await file.delete();
|
|
||||||
} catch (e) {
|
|
||||||
context.showMessage(message: e.toString());
|
|
||||||
}
|
}
|
||||||
controller.close();
|
});
|
||||||
}),
|
},
|
||||||
MenuEntry(
|
),
|
||||||
icon: Icons.picture_as_pdf_outlined,
|
...exportActions(c as LocalComic),
|
||||||
text: "Export as pdf".tl,
|
|
||||||
onClick: () async {
|
|
||||||
var cache = FilePath.join(App.cachePath, 'temp.pdf');
|
|
||||||
var controller = showLoadingDialog(
|
|
||||||
context,
|
|
||||||
allowCancel: false,
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await createPdfFromComicIsolate(
|
|
||||||
comic: c as LocalComic,
|
|
||||||
savePath: cache,
|
|
||||||
);
|
|
||||||
await saveFile(
|
|
||||||
file: File(cache),
|
|
||||||
filename: "${c.title}.pdf",
|
|
||||||
);
|
|
||||||
} catch (e, s) {
|
|
||||||
Log.error("PDF Export", e, s);
|
|
||||||
context.showMessage(message: e.toString());
|
|
||||||
} finally {
|
|
||||||
controller.close();
|
|
||||||
File(cache).deleteIgnoreError();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
MenuEntry(
|
|
||||||
icon: Icons.import_contacts_outlined,
|
|
||||||
text: "Export as epub".tl,
|
|
||||||
onClick: () async {
|
|
||||||
var controller = showLoadingDialog(
|
|
||||||
context,
|
|
||||||
allowCancel: false,
|
|
||||||
);
|
|
||||||
File? file;
|
|
||||||
try {
|
|
||||||
file = await createEpubWithLocalComic(
|
|
||||||
c as LocalComic,
|
|
||||||
);
|
|
||||||
await saveFile(
|
|
||||||
file: file,
|
|
||||||
filename: "${c.title}.epub",
|
|
||||||
);
|
|
||||||
} catch (e, s) {
|
|
||||||
Log.error("EPUB Export", e, s);
|
|
||||||
context.showMessage(message: e.toString());
|
|
||||||
} finally {
|
|
||||||
controller.close();
|
|
||||||
file?.deleteIgnoreError();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -439,4 +389,79 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
);
|
);
|
||||||
return isDeleted;
|
return isDeleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<MenuEntry> exportActions(LocalComic c) {
|
||||||
|
return [
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.outbox_outlined,
|
||||||
|
text: "Export as cbz".tl,
|
||||||
|
onClick: () async {
|
||||||
|
var controller = showLoadingDialog(
|
||||||
|
context,
|
||||||
|
allowCancel: false,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
var file = await CBZ.export(c);
|
||||||
|
await saveFile(filename: file.name, file: file);
|
||||||
|
await file.delete();
|
||||||
|
} catch (e) {
|
||||||
|
context.showMessage(message: e.toString());
|
||||||
|
}
|
||||||
|
controller.close();
|
||||||
|
}),
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.picture_as_pdf_outlined,
|
||||||
|
text: "Export as pdf".tl,
|
||||||
|
onClick: () async {
|
||||||
|
var cache = FilePath.join(App.cachePath, 'temp.pdf');
|
||||||
|
var controller = showLoadingDialog(
|
||||||
|
context,
|
||||||
|
allowCancel: false,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await createPdfFromComicIsolate(
|
||||||
|
comic: c,
|
||||||
|
savePath: cache,
|
||||||
|
);
|
||||||
|
await saveFile(
|
||||||
|
file: File(cache),
|
||||||
|
filename: "${c.title}.pdf",
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("PDF Export", e, s);
|
||||||
|
context.showMessage(message: e.toString());
|
||||||
|
} finally {
|
||||||
|
controller.close();
|
||||||
|
File(cache).deleteIgnoreError();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.import_contacts_outlined,
|
||||||
|
text: "Export as epub".tl,
|
||||||
|
onClick: () async {
|
||||||
|
var controller = showLoadingDialog(
|
||||||
|
context,
|
||||||
|
allowCancel: false,
|
||||||
|
);
|
||||||
|
File? file;
|
||||||
|
try {
|
||||||
|
file = await createEpubWithLocalComic(
|
||||||
|
c,
|
||||||
|
);
|
||||||
|
await saveFile(
|
||||||
|
file: file,
|
||||||
|
filename: "${c.title}.epub",
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("EPUB Export", e, s);
|
||||||
|
context.showMessage(message: e.toString());
|
||||||
|
} finally {
|
||||||
|
controller.close();
|
||||||
|
file?.deleteIgnoreError();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -62,9 +62,7 @@ class _MainPageState extends State<MainPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final _pages = [
|
final _pages = [
|
||||||
const HomePage(
|
const HomePage(),
|
||||||
key: PageStorageKey('home'),
|
|
||||||
),
|
|
||||||
const FavoritesPage(
|
const FavoritesPage(
|
||||||
key: PageStorageKey('favorites'),
|
key: PageStorageKey('favorites'),
|
||||||
),
|
),
|
||||||
|
@@ -356,6 +356,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
var isCTRLPressed = false;
|
var isCTRLPressed = false;
|
||||||
static var _isMouseScrolling = false;
|
static var _isMouseScrolling = false;
|
||||||
var fingers = 0;
|
var fingers = 0;
|
||||||
|
bool disableScroll = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -426,7 +427,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
? Axis.vertical
|
? Axis.vertical
|
||||||
: Axis.horizontal,
|
: Axis.horizontal,
|
||||||
reverse: reader.mode == ReaderMode.continuousRightToLeft,
|
reverse: reader.mode == ReaderMode.continuousRightToLeft,
|
||||||
physics: isCTRLPressed || _isMouseScrolling
|
physics: isCTRLPressed || _isMouseScrolling || disableScroll
|
||||||
? const NeverScrollableScrollPhysics()
|
? const NeverScrollableScrollPhysics()
|
||||||
: const ClampingScrollPhysics(),
|
: const ClampingScrollPhysics(),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
@@ -460,6 +461,11 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
widget = Listener(
|
widget = Listener(
|
||||||
onPointerDown: (event) {
|
onPointerDown: (event) {
|
||||||
fingers++;
|
fingers++;
|
||||||
|
if(fingers > 1 && !disableScroll) {
|
||||||
|
setState(() {
|
||||||
|
disableScroll = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
futurePosition = null;
|
futurePosition = null;
|
||||||
if (_isMouseScrolling) {
|
if (_isMouseScrolling) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -469,6 +475,11 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
},
|
},
|
||||||
onPointerUp: (event) {
|
onPointerUp: (event) {
|
||||||
fingers--;
|
fingers--;
|
||||||
|
if(fingers <= 1 && disableScroll) {
|
||||||
|
setState(() {
|
||||||
|
disableScroll = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onPointerPanZoomUpdate: (event) {
|
onPointerPanZoomUpdate: (event) {
|
||||||
if (event.scale == 1.0) {
|
if (event.scale == 1.0) {
|
||||||
|
@@ -25,6 +25,8 @@ class _ReaderWithLoadingState
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
chapters: data.chapters,
|
chapters: data.chapters,
|
||||||
history: data.history,
|
history: data.history,
|
||||||
|
initialChapter: data.history.ep,
|
||||||
|
initialPage: data.history.page,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -212,7 +212,11 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
|
|||||||
if(history != null) {
|
if(history != null) {
|
||||||
history!.page = page;
|
history!.page = page;
|
||||||
history!.ep = chapter;
|
history!.ep = chapter;
|
||||||
|
if (maxPage > 1) {
|
||||||
|
history!.maxPage = maxPage;
|
||||||
|
}
|
||||||
history!.readEpisode.add(chapter);
|
history!.readEpisode.add(chapter);
|
||||||
|
history!.time = DateTime.now();
|
||||||
HistoryManager().addHistory(history!);
|
HistoryManager().addHistory(history!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -456,58 +456,63 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
var imagesOnScreen =
|
var imagesOnScreen =
|
||||||
continuesState.itemPositionsListener.itemPositions.value;
|
continuesState.itemPositionsListener.itemPositions.value;
|
||||||
var images = imagesOnScreen
|
var images = imagesOnScreen
|
||||||
.map((e) => context.reader.images![e.index - 1])
|
.map((e) => context.reader.images!.elementAtOrNull(e.index - 1))
|
||||||
|
.whereType<String>()
|
||||||
.toList();
|
.toList();
|
||||||
String? selected;
|
String? selected;
|
||||||
await showPopUpWidget(
|
if (images.length > 1) {
|
||||||
context,
|
await showPopUpWidget(
|
||||||
PopUpWidgetScaffold(
|
context,
|
||||||
title: "Select an image on screen".tl,
|
PopUpWidgetScaffold(
|
||||||
body: GridView.builder(
|
title: "Select an image on screen".tl,
|
||||||
itemCount: images.length,
|
body: GridView.builder(
|
||||||
itemBuilder: (context, index) {
|
itemCount: images.length,
|
||||||
ImageProvider image;
|
itemBuilder: (context, index) {
|
||||||
var imageKey = images[index];
|
ImageProvider image;
|
||||||
if (imageKey.startsWith('file://')) {
|
var imageKey = images[index];
|
||||||
image = FileImage(File(imageKey.replaceFirst("file://", '')));
|
if (imageKey.startsWith('file://')) {
|
||||||
} else {
|
image = FileImage(File(imageKey.replaceFirst("file://", '')));
|
||||||
image = ReaderImageProvider(
|
} else {
|
||||||
imageKey,
|
image = ReaderImageProvider(
|
||||||
reader.type.comicSource!.key,
|
imageKey,
|
||||||
reader.cid,
|
reader.type.comicSource!.key,
|
||||||
reader.eid,
|
reader.cid,
|
||||||
);
|
reader.eid,
|
||||||
}
|
);
|
||||||
return InkWell(
|
}
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
return InkWell(
|
||||||
onTap: () {
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
selected = images[index];
|
onTap: () {
|
||||||
App.rootContext.pop();
|
selected = images[index];
|
||||||
},
|
App.rootContext.pop();
|
||||||
child: Container(
|
},
|
||||||
decoration: BoxDecoration(
|
child: Container(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
color: Theme.of(context).colorScheme.outline,
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
width: double.infinity,
|
|
||||||
height: double.infinity,
|
|
||||||
child: Image(
|
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
image: image,
|
child: Image(
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
image: image,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
).padding(const EdgeInsets.all(8));
|
||||||
).padding(const EdgeInsets.all(8));
|
},
|
||||||
},
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
maxCrossAxisExtent: 200,
|
||||||
maxCrossAxisExtent: 200,
|
childAspectRatio: 0.7,
|
||||||
childAspectRatio: 0.7,
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
} else {
|
||||||
|
selected = images.first;
|
||||||
|
}
|
||||||
if (selected == null) {
|
if (selected == null) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
|
@@ -39,7 +39,7 @@ class ImportComic {
|
|||||||
|
|
||||||
Future<bool> multipleCbz() async {
|
Future<bool> multipleCbz() async {
|
||||||
var picker = DirectoryPicker();
|
var picker = DirectoryPicker();
|
||||||
var dir = await picker.pickDirectory();
|
var dir = await picker.pickDirectory(directAccess: true);
|
||||||
if (dir != null) {
|
if (dir != null) {
|
||||||
var files = (await dir.list().toList()).whereType<File>().toList();
|
var files = (await dir.list().toList()).whereType<File>().toList();
|
||||||
files.removeWhere((e) => e.extension != 'cbz' && e.extension != 'zip');
|
files.removeWhere((e) => e.extension != 'cbz' && e.extension != 'zip');
|
||||||
|
@@ -197,7 +197,7 @@ class DirectoryPicker {
|
|||||||
|
|
||||||
static const _methodChannel = MethodChannel("venera/method_channel");
|
static const _methodChannel = MethodChannel("venera/method_channel");
|
||||||
|
|
||||||
Future<Directory?> pickDirectory() async {
|
Future<Directory?> pickDirectory({bool directAccess = false}) async {
|
||||||
IO._isSelectingFiles = true;
|
IO._isSelectingFiles = true;
|
||||||
try {
|
try {
|
||||||
String? directory;
|
String? directory;
|
||||||
@@ -205,6 +205,16 @@ class DirectoryPicker {
|
|||||||
directory = await file_selector.getDirectoryPath();
|
directory = await file_selector.getDirectoryPath();
|
||||||
} else if (App.isAndroid) {
|
} else if (App.isAndroid) {
|
||||||
directory = (await AndroidDirectory.pickDirectory())?.path;
|
directory = (await AndroidDirectory.pickDirectory())?.path;
|
||||||
|
if (directory != null && directAccess) {
|
||||||
|
// Native library does not have access to the directory. Copy it to cache.
|
||||||
|
var cache = FilePath.join(App.cachePath, "selected_directory");
|
||||||
|
if (Directory(cache).existsSync()) {
|
||||||
|
Directory(cache).deleteSync(recursive: true);
|
||||||
|
}
|
||||||
|
Directory(cache).createSync();
|
||||||
|
await copyDirectoryIsolate(Directory(directory), Directory(cache));
|
||||||
|
directory = cache;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// ios, macos
|
// ios, macos
|
||||||
directory =
|
directory =
|
||||||
|
32
pubspec.lock
32
pubspec.lock
@@ -5,10 +5,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: app_links
|
name: app_links
|
||||||
sha256: ad1a6d598e7e39b46a34f746f9a8b011ee147e4c275d407fa457e7a62f84dd99
|
sha256: "433df2e61b10519407475d7f69e470789d23d593f28224c38ba1068597be7950"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.2"
|
version: "6.3.3"
|
||||||
app_links_linux:
|
app_links_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -69,10 +69,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: battery_plus
|
name: battery_plus
|
||||||
sha256: "220c8f1961efb01d6870493b5ac5a80afaeaffc8757f7a11ed3025a8570d29e7"
|
sha256: a0409fe7d21905987eb1348ad57c634f913166f14f0c8936b73d3f5940fac551
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.0"
|
version: "6.2.1"
|
||||||
battery_plus_platform_interface:
|
battery_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -425,10 +425,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_rust_bridge
|
name: flutter_rust_bridge
|
||||||
sha256: fb9d3c9395eae3c71d4fe3ec343b9f30636c9988150c8bb33b60047549b34e3d
|
sha256: "35c257fc7f98e34c1314d6c145e5ed54e7c94e8a9f469947e31c9298177d546f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.0"
|
version: "2.7.0"
|
||||||
flutter_saf:
|
flutter_saf:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -447,10 +447,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: flutter_to_arch
|
name: flutter_to_arch
|
||||||
sha256: "656cffc182b05af38aa96a1115931620b8865c4b0cfe00813b26fcff0875f2ab"
|
sha256: b68b2757a89a517ae2141cbc672acdd1f69721dd686cacad03876b6f436ff040
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.1"
|
||||||
flutter_to_debian:
|
flutter_to_debian:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -798,10 +798,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: rhttp
|
name: rhttp
|
||||||
sha256: "581d57b5b6056d31489af94db8653a1c11d7b59050cbbc41ece4279e50414de5"
|
sha256: "8212cbc816cc3e761eecb8d4dbbaa1eca95de715428320a198a4e7a89acdcd2e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.6"
|
version: "0.9.8"
|
||||||
screen_retriever:
|
screen_retriever:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -855,18 +855,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: share_plus
|
name: share_plus
|
||||||
sha256: "9c9bafd4060728d7cdb2464c341743adbd79d327cb067ec7afb64583540b47c8"
|
sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.1.2"
|
version: "10.1.3"
|
||||||
share_plus_platform_interface:
|
share_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: share_plus_platform_interface
|
name: share_plus_platform_interface
|
||||||
sha256: c57c0bbfec7142e3a0f55633be504b796af72e60e3c791b44d5a017b985f7a48
|
sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.1"
|
version: "5.0.2"
|
||||||
shimmer:
|
shimmer:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -916,10 +916,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: sqlite3_flutter_libs
|
name: sqlite3_flutter_libs
|
||||||
sha256: "636b0fe8a2de894e5455572f6cbbc458f4ffecfe9f860b79439e27041ea4f0b9"
|
sha256: "73016db8419f019e807b7a5e5fbf2a7bd45c165fed403b8e7681230f3a102785"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.27"
|
version: "0.5.28"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
14
pubspec.yaml
14
pubspec.yaml
@@ -2,7 +2,7 @@ name: venera
|
|||||||
description: "A comic app."
|
description: "A comic app."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.1.2+112
|
version: 1.1.3+113
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.6.0 <4.0.0'
|
sdk: '>=3.6.0 <4.0.0'
|
||||||
@@ -17,7 +17,7 @@ dependencies:
|
|||||||
intl: ^0.19.0
|
intl: ^0.19.0
|
||||||
window_manager: ^0.4.3
|
window_manager: ^0.4.3
|
||||||
sqlite3: ^2.4.7
|
sqlite3: ^2.4.7
|
||||||
sqlite3_flutter_libs: any
|
sqlite3_flutter_libs: ^0.5.28
|
||||||
flutter_qjs:
|
flutter_qjs:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/wgh136/flutter_qjs
|
url: https://github.com/wgh136/flutter_qjs
|
||||||
@@ -33,7 +33,7 @@ dependencies:
|
|||||||
url: https://github.com/wgh136/photo_view
|
url: https://github.com/wgh136/photo_view
|
||||||
ref: 94724a0b
|
ref: 94724a0b
|
||||||
mime: ^2.0.0
|
mime: ^2.0.0
|
||||||
share_plus: ^10.0.2
|
share_plus: ^10.1.3
|
||||||
scrollable_positioned_list:
|
scrollable_positioned_list:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/venera-app/flutter.widgets
|
url: https://github.com/venera-app/flutter.widgets
|
||||||
@@ -47,7 +47,7 @@ dependencies:
|
|||||||
url: https://github.com/wgh136/flutter_desktop_webview
|
url: https://github.com/wgh136/flutter_desktop_webview
|
||||||
path: packages/desktop_webview_window
|
path: packages/desktop_webview_window
|
||||||
flutter_inappwebview: ^6.1.5
|
flutter_inappwebview: ^6.1.5
|
||||||
app_links: ^6.3.2
|
app_links: ^6.3.3
|
||||||
sliver_tools: ^0.2.12
|
sliver_tools: ^0.2.12
|
||||||
flutter_file_dialog: ^3.0.2
|
flutter_file_dialog: ^3.0.2
|
||||||
file_selector: ^1.0.3
|
file_selector: ^1.0.3
|
||||||
@@ -56,12 +56,12 @@ dependencies:
|
|||||||
git:
|
git:
|
||||||
url: https://github.com/venera-app/lodepng_flutter
|
url: https://github.com/venera-app/lodepng_flutter
|
||||||
ref: 9a784b193af5d55b2a35e58fa390bda3e4f35d00
|
ref: 9a784b193af5d55b2a35e58fa390bda3e4f35d00
|
||||||
rhttp: 0.9.6
|
rhttp: 0.9.8
|
||||||
webdav_client:
|
webdav_client:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/wgh136/webdav_client
|
url: https://github.com/wgh136/webdav_client
|
||||||
ref: 285f87f15bccd2d5d5ff443761348c6ee47b98d1
|
ref: 285f87f15bccd2d5d5ff443761348c6ee47b98d1
|
||||||
battery_plus: ^6.2.0
|
battery_plus: ^6.2.1
|
||||||
local_auth: ^2.3.0
|
local_auth: ^2.3.0
|
||||||
flutter_saf:
|
flutter_saf:
|
||||||
git:
|
git:
|
||||||
@@ -76,7 +76,7 @@ dev_dependencies:
|
|||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^5.0.0
|
flutter_lints: ^5.0.0
|
||||||
flutter_to_arch: ^1.0.0
|
flutter_to_arch: ^1.0.1
|
||||||
flutter_to_debian:
|
flutter_to_debian:
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
|
Reference in New Issue
Block a user