Merge pull request #119 from venera-app/dev

v1.1.3
This commit is contained in:
nyne
2024-12-26 19:31:21 +08:00
committed by GitHub
23 changed files with 472 additions and 291 deletions

View File

@@ -1,5 +1,4 @@
[Desktop Entry] [Desktop Entry]
Version={{Version}}
Name=Venera Name=Venera
GenericName=Venera GenericName=Venera
Comment=venera Comment=venera

View File

@@ -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,7 +403,12 @@ 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(
constraints: BoxConstraints(
maxHeight: math.min(400, context.height - 136),
),
child: SingleChildScrollView(
child: Wrap(
runSpacing: 8, runSpacing: 8,
spacing: 8, spacing: 8,
children: [ children: [
@@ -402,7 +427,9 @@ class ComicTile extends StatelessWidget {
}, },
), ),
], ],
),
).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,8 +924,10 @@ class ComicListState extends State<ComicList> {
} }
void storeState() { void storeState() {
if(enablePageStorage) {
PageStorage.of(context).writeState(context, state); PageStorage.of(context).writeState(context, state);
} }
}
void refresh() { void refresh() {
_data.clear(); _data.clear();
@@ -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!,

View File

@@ -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,

View File

@@ -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;

View File

@@ -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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);

View File

@@ -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,7 +113,8 @@ 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 ??
History.fromModel(
model: this, model: this,
ep: 0, ep: 0,
page: 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';
@@ -358,9 +386,8 @@ class LocalManager with ChangeNotifier {
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>[];
@@ -451,6 +478,7 @@ 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()) {
try {
var tasks = jsonDecode(file.readAsStringSync()); var tasks = jsonDecode(file.readAsStringSync());
for (var e in tasks) { for (var e in tasks) {
var task = DownloadTask.fromJson(e); var task = DownloadTask.fromJson(e);
@@ -458,6 +486,10 @@ class LocalManager with ChangeNotifier {
downloadingTasks.add(task); downloadingTasks.add(task);
} }
} }
} catch (e) {
file.delete();
Log.error("LocalManager", "Failed to restore downloading tasks: $e");
}
} }
} }

View File

@@ -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']);
} }

View File

@@ -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()),

View File

@@ -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)),

View File

@@ -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"),

View File

@@ -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]);

View File

@@ -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,

View File

@@ -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: [

View File

@@ -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,13 +196,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
buildMultiSelectMenu(), buildMultiSelectMenu(),
]; ];
var body = Scaffold( List<Widget> normalActions = [
body: SmoothCustomScrollView(
slivers: [
if (!searchMode && !multiSelectMode)
SliverAppbar(
title: Text("Local".tl),
actions: [
Tooltip( Tooltip(
message: "Search".tl, message: "Search".tl,
child: IconButton( child: IconButton(
@@ -216,25 +224,35 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
}, },
), ),
), ),
], ];
)
else if (multiSelectMode) var body = Scaffold(
body: SmoothCustomScrollView(
slivers: [
if (!searchMode)
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: () {
if (multiSelectMode) {
setState(() { setState(() {
multiSelectMode = false; multiSelectMode = false;
selectedComics.clear(); 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(
@@ -302,77 +320,9 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
}); });
} }
}); });
}),
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,
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( ...exportActions(c as LocalComic),
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();
}
},
)
];
}
} }

View File

@@ -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'),
), ),

View File

@@ -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) {

View File

@@ -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,
); );
} }

View File

@@ -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!);
} }
} }

View File

@@ -456,9 +456,11 @@ 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;
if (images.length > 1) {
await showPopUpWidget( await showPopUpWidget(
context, context,
PopUpWidgetScaffold( PopUpWidgetScaffold(
@@ -508,6 +510,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
), ),
), ),
); );
} else {
selected = images.first;
}
if (selected == null) { if (selected == null) {
return null; return null;
} else { } else {

View File

@@ -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');

View File

@@ -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 =

View File

@@ -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:

View File

@@ -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: