mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
dbc2c27db0 | ||
fffb3dc973 | |||
0ca8a28639 | |||
6426ebaf16 | |||
316f61394d | |||
04ab75cf92 | |||
4828a57e1a | |||
d089163220 | |||
7b5c13200d | |||
0f6874f8d7 | |||
4af15b9139 | |||
9fe49217dc | |||
76c56964a5 | |||
e8afbca7b2 | |||
5843d7c919 | |||
![]() |
de98dfaa1b | ||
![]() |
30cbfb54ef | ||
c633021963 | |||
![]() |
4640831e69 | ||
af7a7c220e | |||
fd19f6bf7d | |||
96b4125613 |
24
.github/ISSUE_TEMPLATE/bug.yaml
vendored
24
.github/ISSUE_TEMPLATE/bug.yaml
vendored
@@ -8,9 +8,31 @@ body:
|
|||||||
value: |
|
value: |
|
||||||
Thank you for reporting a problem, please complete the title and fill in the following information.
|
Thank you for reporting a problem, please complete the title and fill in the following information.
|
||||||
|
|
||||||
|
感谢您的反馈,请填写完整标题并填写以下信息。
|
||||||
|
|
||||||
**Please do not report any issues related to config files.**
|
**Please do not report any issues related to config files.**
|
||||||
|
|
||||||
To report a bug related to the config file, please send it to the [config repository](https://github.com/venera-app/venera-configs).
|
**请不要报告与配置文件相关的任何问题。**
|
||||||
|
|
||||||
|
This project is a comic reader that allows users write their own config files. And there is no built-in comic source.
|
||||||
|
|
||||||
|
本项目是一个漫画阅读器,允许用户编写自己的配置文件,并且没有内置漫画源。
|
||||||
|
- type: dropdown
|
||||||
|
id: bugType
|
||||||
|
attributes:
|
||||||
|
label: Bug type
|
||||||
|
description: What type of bug are you reporting?
|
||||||
|
options:
|
||||||
|
- Crash
|
||||||
|
- UI
|
||||||
|
- Performance
|
||||||
|
- Security
|
||||||
|
- Reader
|
||||||
|
- JS Engine
|
||||||
|
- Comic Source
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: what-happened
|
id: what-happened
|
||||||
attributes:
|
attributes:
|
||||||
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
10
.github/ISSUE_TEMPLATE/enhancement.yaml
vendored
10
.github/ISSUE_TEMPLATE/enhancement.yaml
vendored
@@ -7,6 +7,16 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Welcome to make a feature request, please fill in the following information after completing the title.
|
Welcome to make a feature request, please fill in the following information after completing the title.
|
||||||
|
|
||||||
|
欢迎提出功能建议,请填写完整标题后填写以下信息。
|
||||||
|
|
||||||
|
**Please do not report any issues related to config files.**
|
||||||
|
|
||||||
|
**请不要报告与配置文件相关的任何问题。**
|
||||||
|
|
||||||
|
This project is a comic reader that allows users write their own config files. And there is no built-in comic source.
|
||||||
|
|
||||||
|
本项目是一个漫画阅读器,允许用户编写自己的配置文件,并且没有内置漫画源。
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: what-happened
|
id: what-happened
|
||||||
attributes:
|
attributes:
|
||||||
|
9
.github/ISSUE_TEMPLATE/other.yaml
vendored
9
.github/ISSUE_TEMPLATE/other.yaml
vendored
@@ -1,9 +0,0 @@
|
|||||||
name: other
|
|
||||||
description: Other contents
|
|
||||||
body:
|
|
||||||
- type: textarea
|
|
||||||
id: what-happened
|
|
||||||
attributes:
|
|
||||||
label: Content
|
|
||||||
validations:
|
|
||||||
required: true
|
|
@@ -362,7 +362,12 @@
|
|||||||
"Mark all as read": "全部标记为已读",
|
"Mark all as read": "全部标记为已读",
|
||||||
"Do you want to mark all as read?" : "您要全部标记为已读吗?",
|
"Do you want to mark all as read?" : "您要全部标记为已读吗?",
|
||||||
"Swipe down for previous chapter": "向下滑动查看上一章",
|
"Swipe down for previous chapter": "向下滑动查看上一章",
|
||||||
"Swipe up for next chapter": "向上滑动查看下一章"
|
"Swipe up for next chapter": "向上滑动查看下一章",
|
||||||
|
"Initial Page": "初始页面",
|
||||||
|
"Home Page": "主页",
|
||||||
|
"Favorites Page": "收藏页面",
|
||||||
|
"Explore Page": "探索页面",
|
||||||
|
"Categories Page": "分类页面"
|
||||||
},
|
},
|
||||||
"zh_TW": {
|
"zh_TW": {
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
@@ -727,6 +732,11 @@
|
|||||||
"Mark all as read": "全部標記為已讀",
|
"Mark all as read": "全部標記為已讀",
|
||||||
"Do you want to mark all as read?" : "您要全部標記為已讀嗎?",
|
"Do you want to mark all as read?" : "您要全部標記為已讀嗎?",
|
||||||
"Swipe down for previous chapter": "向下滑動查看上一章",
|
"Swipe down for previous chapter": "向下滑動查看上一章",
|
||||||
"Swipe up for next chapter": "向上滑動查看下一章"
|
"Swipe up for next chapter": "向上滑動查看下一章",
|
||||||
|
"Initial Page": "初始頁面",
|
||||||
|
"Home Page": "主頁",
|
||||||
|
"Favorites Page": "收藏頁面",
|
||||||
|
"Explore Page": "探索頁面",
|
||||||
|
"Categories Page": "分類頁面"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -125,11 +125,11 @@ class OverlayWidgetState extends State<OverlayWidget> {
|
|||||||
void showDialogMessage(BuildContext context, String title, String message) {
|
void showDialogMessage(BuildContext context, String title, String message) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => ContentDialog(
|
||||||
title: Text(title),
|
title: title,
|
||||||
content: Text(message),
|
content: Text(message).paddingHorizontal(16),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
FilledButton(
|
||||||
onPressed: context.pop,
|
onPressed: context.pop,
|
||||||
child: Text("OK".tl),
|
child: Text("OK".tl),
|
||||||
)
|
)
|
||||||
|
@@ -1,15 +1,13 @@
|
|||||||
part of 'components.dart';
|
part of 'components.dart';
|
||||||
|
|
||||||
class SideBarRoute<T> extends PopupRoute<T> {
|
class SideBarRoute<T> extends PopupRoute<T> {
|
||||||
SideBarRoute(this.title, this.widget,
|
SideBarRoute(this.widget,
|
||||||
{this.showBarrier = true,
|
{this.showBarrier = true,
|
||||||
this.useSurfaceTintColor = false,
|
this.useSurfaceTintColor = false,
|
||||||
required this.width,
|
required this.width,
|
||||||
this.addBottomPadding = true,
|
this.addBottomPadding = true,
|
||||||
this.addTopPadding = true});
|
this.addTopPadding = true});
|
||||||
|
|
||||||
final String? title;
|
|
||||||
|
|
||||||
final Widget widget;
|
final Widget widget;
|
||||||
|
|
||||||
final bool showBarrier;
|
final bool showBarrier;
|
||||||
@@ -36,11 +34,7 @@ class SideBarRoute<T> extends PopupRoute<T> {
|
|||||||
Animation<double> secondaryAnimation) {
|
Animation<double> secondaryAnimation) {
|
||||||
bool showSideBar = MediaQuery.of(context).size.width > width;
|
bool showSideBar = MediaQuery.of(context).size.width > width;
|
||||||
|
|
||||||
Widget body = SidebarBody(
|
Widget body = widget;
|
||||||
title: title,
|
|
||||||
widget: widget,
|
|
||||||
autoChangeTitleBarColor: !useSurfaceTintColor,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (addTopPadding) {
|
if (addTopPadding) {
|
||||||
body = Padding(
|
body = Padding(
|
||||||
@@ -129,97 +123,13 @@ class SideBarRoute<T> extends PopupRoute<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SidebarBody extends StatefulWidget {
|
|
||||||
const SidebarBody(
|
|
||||||
{required this.title,
|
|
||||||
required this.widget,
|
|
||||||
required this.autoChangeTitleBarColor,
|
|
||||||
super.key});
|
|
||||||
|
|
||||||
final String? title;
|
|
||||||
final Widget widget;
|
|
||||||
final bool autoChangeTitleBarColor;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<SidebarBody> createState() => _SidebarBodyState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SidebarBodyState extends State<SidebarBody> {
|
|
||||||
bool top = true;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
Widget body = Expanded(child: widget.widget);
|
|
||||||
|
|
||||||
if (widget.autoChangeTitleBarColor) {
|
|
||||||
body = NotificationListener<ScrollNotification>(
|
|
||||||
onNotification: (notifications) {
|
|
||||||
if (notifications.metrics.pixels ==
|
|
||||||
notifications.metrics.minScrollExtent &&
|
|
||||||
!top) {
|
|
||||||
setState(() {
|
|
||||||
top = true;
|
|
||||||
});
|
|
||||||
} else if (notifications.metrics.pixels !=
|
|
||||||
notifications.metrics.minScrollExtent &&
|
|
||||||
top) {
|
|
||||||
setState(() {
|
|
||||||
top = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
child: body,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
if (widget.title != null)
|
|
||||||
Container(
|
|
||||||
height: 60 + MediaQuery.of(context).padding.top,
|
|
||||||
color: top
|
|
||||||
? null
|
|
||||||
: Theme.of(context).colorScheme.surfaceTint.withAlpha(20),
|
|
||||||
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const SizedBox(
|
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
Tooltip(
|
|
||||||
message: "Back".tl,
|
|
||||||
child: IconButton(
|
|
||||||
iconSize: 25,
|
|
||||||
icon: const Icon(Icons.arrow_back),
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 10,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
widget.title!,
|
|
||||||
style: const TextStyle(fontSize: 22),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> showSideBar(BuildContext context, Widget widget,
|
Future<void> showSideBar(BuildContext context, Widget widget,
|
||||||
{String? title,
|
{bool showBarrier = true,
|
||||||
bool showBarrier = true,
|
|
||||||
bool useSurfaceTintColor = false,
|
bool useSurfaceTintColor = false,
|
||||||
double width = 500,
|
double width = 500,
|
||||||
bool addTopPadding = false}) {
|
bool addTopPadding = false}) {
|
||||||
return Navigator.of(context).push(
|
return Navigator.of(context).push(
|
||||||
SideBarRoute(
|
SideBarRoute(
|
||||||
title,
|
|
||||||
widget,
|
widget,
|
||||||
showBarrier: showBarrier,
|
showBarrier: showBarrier,
|
||||||
useSurfaceTintColor: useSurfaceTintColor,
|
useSurfaceTintColor: useSurfaceTintColor,
|
||||||
|
@@ -10,6 +10,34 @@ import 'package:window_manager/window_manager.dart';
|
|||||||
|
|
||||||
const _kTitleBarHeight = 36.0;
|
const _kTitleBarHeight = 36.0;
|
||||||
|
|
||||||
|
class WindowFrameController extends InheritedWidget {
|
||||||
|
/// Whether the window frame is hidden.
|
||||||
|
final bool isWindowFrameHidden;
|
||||||
|
|
||||||
|
/// Sets the visibility of the window frame.
|
||||||
|
final void Function(bool) setWindowFrame;
|
||||||
|
|
||||||
|
/// Adds a listener that will be called when close button is clicked.
|
||||||
|
/// The listener should return `true` to allow the window to be closed.
|
||||||
|
final void Function(WindowCloseListener listener) addCloseListener;
|
||||||
|
|
||||||
|
/// Removes a close listener.
|
||||||
|
final void Function(WindowCloseListener listener) removeCloseListener;
|
||||||
|
|
||||||
|
const WindowFrameController._create({
|
||||||
|
required this.isWindowFrameHidden,
|
||||||
|
required this.setWindowFrame,
|
||||||
|
required this.addCloseListener,
|
||||||
|
required this.removeCloseListener,
|
||||||
|
required super.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(covariant InheritedWidget oldWidget) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class WindowFrame extends StatefulWidget {
|
class WindowFrame extends StatefulWidget {
|
||||||
const WindowFrame(this.child, {super.key});
|
const WindowFrame(this.child, {super.key});
|
||||||
|
|
||||||
@@ -17,98 +45,145 @@ class WindowFrame extends StatefulWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
State<WindowFrame> createState() => _WindowFrameState();
|
State<WindowFrame> createState() => _WindowFrameState();
|
||||||
|
|
||||||
|
static WindowFrameController of(BuildContext context) {
|
||||||
|
return context.dependOnInheritedWidgetOfExactType<WindowFrameController>()!;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef WindowCloseListener = bool Function();
|
||||||
|
|
||||||
class _WindowFrameState extends State<WindowFrame> {
|
class _WindowFrameState extends State<WindowFrame> {
|
||||||
bool isHideWindowFrame = false;
|
bool isWindowFrameHidden = false;
|
||||||
bool useDarkTheme = false;
|
bool useDarkTheme = false;
|
||||||
|
var closeListeners = <WindowCloseListener>[];
|
||||||
|
|
||||||
|
/// Sets the visibility of the window frame.
|
||||||
|
void setWindowFrame(bool show) {
|
||||||
|
setState(() {
|
||||||
|
isWindowFrameHidden = !show;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a listener that will be called when close button is clicked.
|
||||||
|
/// The listener should return `true` to allow the window to be closed.
|
||||||
|
void addCloseListener(WindowCloseListener listener) {
|
||||||
|
closeListeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a close listener.
|
||||||
|
void removeCloseListener(WindowCloseListener listener) {
|
||||||
|
closeListeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onClose() {
|
||||||
|
for (var listener in closeListeners) {
|
||||||
|
if (!listener()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
windowManager.close();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (App.isMobile) return widget.child;
|
if (App.isMobile) return widget.child;
|
||||||
if (isHideWindowFrame) return widget.child;
|
|
||||||
|
|
||||||
var body = Stack(
|
Widget body = Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: MediaQuery(
|
child: MediaQuery(
|
||||||
data: MediaQuery.of(context).copyWith(
|
data: MediaQuery.of(context).copyWith(
|
||||||
padding: const EdgeInsets.only(top: _kTitleBarHeight)),
|
padding: isWindowFrameHidden
|
||||||
|
? null
|
||||||
|
: const EdgeInsets.only(top: _kTitleBarHeight),
|
||||||
|
),
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
if (!isWindowFrameHidden)
|
||||||
top: 0,
|
Positioned(
|
||||||
left: 0,
|
top: 0,
|
||||||
right: 0,
|
left: 0,
|
||||||
child: Material(
|
right: 0,
|
||||||
color: Colors.transparent,
|
child: Material(
|
||||||
child: Theme(
|
color: Colors.transparent,
|
||||||
data: Theme.of(context).copyWith(
|
child: Theme(
|
||||||
brightness: useDarkTheme ? Brightness.dark : null,
|
data: Theme.of(context).copyWith(
|
||||||
),
|
brightness: useDarkTheme ? Brightness.dark : null,
|
||||||
child: Builder(builder: (context) {
|
),
|
||||||
return SizedBox(
|
child: Builder(builder: (context) {
|
||||||
height: _kTitleBarHeight,
|
return SizedBox(
|
||||||
child: Row(
|
height: _kTitleBarHeight,
|
||||||
children: [
|
child: Row(
|
||||||
if (App.isMacOS)
|
children: [
|
||||||
const DragToMoveArea(
|
if (App.isMacOS)
|
||||||
child: SizedBox(
|
const DragToMoveArea(
|
||||||
height: double.infinity,
|
child: SizedBox(
|
||||||
width: 16,
|
height: double.infinity,
|
||||||
),
|
width: 16,
|
||||||
).paddingRight(52)
|
|
||||||
else
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: DragToMoveArea(
|
|
||||||
child: Text(
|
|
||||||
'Venera',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: (useDarkTheme ||
|
|
||||||
context.brightness == Brightness.dark)
|
|
||||||
? Colors.white
|
|
||||||
: Colors.black,
|
|
||||||
),
|
),
|
||||||
)
|
).paddingRight(52)
|
||||||
.toAlign(Alignment.centerLeft)
|
else
|
||||||
.paddingLeft(4 + (App.isMacOS ? 25 : 0)),
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: DragToMoveArea(
|
||||||
|
child: Text(
|
||||||
|
'Venera',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: (useDarkTheme ||
|
||||||
|
context.brightness == Brightness.dark)
|
||||||
|
? Colors.white
|
||||||
|
: Colors.black,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toAlign(Alignment.centerLeft)
|
||||||
|
.paddingLeft(4 + (App.isMacOS ? 25 : 0)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
if (kDebugMode)
|
||||||
if (kDebugMode)
|
const TextButton(
|
||||||
const TextButton(
|
onPressed: debug,
|
||||||
onPressed: debug,
|
child: Text('Debug'),
|
||||||
child: Text('Debug'),
|
),
|
||||||
),
|
if (!App.isMacOS) _WindowButtons(
|
||||||
if (!App.isMacOS) const WindowButtons()
|
onClose: _onClose,
|
||||||
],
|
)
|
||||||
),
|
],
|
||||||
);
|
),
|
||||||
}),
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (App.isLinux) {
|
if (App.isLinux) {
|
||||||
return VirtualWindowFrame(child: body);
|
body = VirtualWindowFrame(child: body);
|
||||||
} else {
|
|
||||||
return body;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return WindowFrameController._create(
|
||||||
|
isWindowFrameHidden: isWindowFrameHidden,
|
||||||
|
setWindowFrame: setWindowFrame,
|
||||||
|
addCloseListener: addCloseListener,
|
||||||
|
removeCloseListener: removeCloseListener,
|
||||||
|
child: body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WindowButtons extends StatefulWidget {
|
class _WindowButtons extends StatefulWidget {
|
||||||
const WindowButtons({super.key});
|
const _WindowButtons({required this.onClose});
|
||||||
|
|
||||||
|
final void Function() onClose;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<WindowButtons> createState() => _WindowButtonsState();
|
State<_WindowButtons> createState() => _WindowButtonsState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _WindowButtonsState extends State<WindowButtons> with WindowListener {
|
class _WindowButtonsState extends State<_WindowButtons> with WindowListener {
|
||||||
bool isMaximized = false;
|
bool isMaximized = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -197,9 +272,7 @@ class _WindowButtonsState extends State<WindowButtons> with WindowListener {
|
|||||||
color: !dark ? Colors.white : Colors.black,
|
color: !dark ? Colors.white : Colors.black,
|
||||||
),
|
),
|
||||||
hoverColor: Colors.red,
|
hoverColor: Colors.red,
|
||||||
onPressed: () {
|
onPressed: widget.onClose,
|
||||||
windowManager.close();
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -567,5 +640,5 @@ TransitionBuilder VirtualWindowFrameInit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void debug() {
|
void debug() {
|
||||||
ComicSource.reload();
|
ComicSourceManager().reload();
|
||||||
}
|
}
|
||||||
|
@@ -13,7 +13,7 @@ export "widget_utils.dart";
|
|||||||
export "context.dart";
|
export "context.dart";
|
||||||
|
|
||||||
class _App {
|
class _App {
|
||||||
final version = "1.3.1";
|
final version = "1.3.2";
|
||||||
|
|
||||||
bool get isAndroid => Platform.isAndroid;
|
bool get isAndroid => Platform.isAndroid;
|
||||||
|
|
||||||
@@ -77,10 +77,15 @@ class _App {
|
|||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
cachePath = (await getApplicationCacheDirectory()).path;
|
cachePath = (await getApplicationCacheDirectory()).path;
|
||||||
dataPath = (await getApplicationSupportDirectory()).path;
|
dataPath = (await getApplicationSupportDirectory()).path;
|
||||||
await data.init();
|
}
|
||||||
await history.init();
|
|
||||||
await favorites.init();
|
Future<void> initComponents() async {
|
||||||
await local.init();
|
await Future.wait([
|
||||||
|
data.init(),
|
||||||
|
history.init(),
|
||||||
|
favorites.init(),
|
||||||
|
local.init(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Function? _forceRebuildHandler;
|
Function? _forceRebuildHandler;
|
||||||
|
@@ -7,7 +7,9 @@ import 'package:venera/utils/data_sync.dart';
|
|||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
|
|
||||||
class Appdata {
|
class Appdata {
|
||||||
final Settings settings = Settings();
|
Appdata._create();
|
||||||
|
|
||||||
|
final Settings settings = Settings._create();
|
||||||
|
|
||||||
var searchHistory = <String>[];
|
var searchHistory = <String>[];
|
||||||
|
|
||||||
@@ -110,10 +112,10 @@ class Appdata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final appdata = Appdata();
|
final appdata = Appdata._create();
|
||||||
|
|
||||||
class Settings with ChangeNotifier {
|
class Settings with ChangeNotifier {
|
||||||
Settings();
|
Settings._create();
|
||||||
|
|
||||||
final _data = <String, dynamic>{
|
final _data = <String, dynamic>{
|
||||||
'comicDisplayMode': 'detailed', // detailed, brief
|
'comicDisplayMode': 'detailed', // detailed, brief
|
||||||
@@ -161,6 +163,7 @@ class Settings with ChangeNotifier {
|
|||||||
'comicSourceListUrl': "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@latest/index.json",
|
'comicSourceListUrl': "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@latest/index.json",
|
||||||
'preloadImageCount': 4,
|
'preloadImageCount': 4,
|
||||||
'followUpdatesFolder': null,
|
'followUpdatesFolder': null,
|
||||||
|
'initialPage': '0',
|
||||||
};
|
};
|
||||||
|
|
||||||
operator [](String key) {
|
operator [](String key) {
|
||||||
|
@@ -145,7 +145,7 @@ class RandomCategoryPartWithRuntimeData extends BaseCategoryPart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CategoryData getCategoryDataWithKey(String key) {
|
CategoryData getCategoryDataWithKey(String key) {
|
||||||
for (var source in ComicSource._sources) {
|
for (var source in ComicSource.all()) {
|
||||||
if (source.categoryData?.key == key) {
|
if (source.categoryData?.key == key) {
|
||||||
return source.categoryData!;
|
return source.categoryData!;
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,7 @@ import 'package:venera/foundation/history.dart';
|
|||||||
import 'package:venera/foundation/res.dart';
|
import 'package:venera/foundation/res.dart';
|
||||||
import 'package:venera/utils/data_sync.dart';
|
import 'package:venera/utils/data_sync.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
|
import 'package:venera/utils/init.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
@@ -27,81 +28,29 @@ part 'parser.dart';
|
|||||||
|
|
||||||
part 'models.dart';
|
part 'models.dart';
|
||||||
|
|
||||||
/// build comic list, [Res.subData] should be maxPage or null if there is no limit.
|
part 'types.dart';
|
||||||
typedef ComicListBuilder = Future<Res<List<Comic>>> Function(int page);
|
|
||||||
|
|
||||||
/// build comic list with next param, [Res.subData] should be next page param or null if there is no next page.
|
class ComicSourceManager with ChangeNotifier, Init {
|
||||||
typedef ComicListBuilderWithNext = Future<Res<List<Comic>>> Function(
|
final List<ComicSource> _sources = [];
|
||||||
String? next);
|
|
||||||
|
|
||||||
typedef LoginFunction = Future<Res<bool>> Function(String, String);
|
static ComicSourceManager? _instance;
|
||||||
|
|
||||||
typedef LoadComicFunc = Future<Res<ComicDetails>> Function(String id);
|
ComicSourceManager._create();
|
||||||
|
|
||||||
typedef LoadComicPagesFunc = Future<Res<List<String>>> Function(
|
factory ComicSourceManager() => _instance ??= ComicSourceManager._create();
|
||||||
String id, String? ep);
|
|
||||||
|
|
||||||
typedef CommentsLoader = Future<Res<List<Comment>>> Function(
|
List<ComicSource> all() => List.from(_sources);
|
||||||
String id, String? subId, int page, String? replyTo);
|
|
||||||
|
|
||||||
typedef SendCommentFunc = Future<Res<bool>> Function(
|
ComicSource? find(String key) =>
|
||||||
String id, String? subId, String content, String? replyTo);
|
|
||||||
|
|
||||||
typedef GetImageLoadingConfigFunc = Future<Map<String, dynamic>> Function(
|
|
||||||
String imageKey, String comicId, String epId)?;
|
|
||||||
typedef GetThumbnailLoadingConfigFunc = Map<String, dynamic> Function(
|
|
||||||
String imageKey)?;
|
|
||||||
|
|
||||||
typedef ComicThumbnailLoader = Future<Res<List<String>>> Function(
|
|
||||||
String comicId, String? next);
|
|
||||||
|
|
||||||
typedef LikeOrUnlikeComicFunc = Future<Res<bool>> Function(
|
|
||||||
String comicId, bool isLiking);
|
|
||||||
|
|
||||||
/// [isLiking] is true if the user is liking the comment, false if unliking.
|
|
||||||
/// return the new likes count or null.
|
|
||||||
typedef LikeCommentFunc = Future<Res<int?>> Function(
|
|
||||||
String comicId, String? subId, String commentId, bool isLiking);
|
|
||||||
|
|
||||||
/// [isUp] is true if the user is upvoting the comment, false if downvoting.
|
|
||||||
/// return the new vote count or null.
|
|
||||||
typedef VoteCommentFunc = Future<Res<int?>> Function(
|
|
||||||
String comicId, String? subId, String commentId, bool isUp, bool isCancel);
|
|
||||||
|
|
||||||
typedef HandleClickTagEvent = Map<String, String> Function(
|
|
||||||
String namespace, String tag);
|
|
||||||
|
|
||||||
/// [rating] is the rating value, 0-10. 1 represents 0.5 star.
|
|
||||||
typedef StarRatingFunc = Future<Res<bool>> Function(String comicId, int rating);
|
|
||||||
|
|
||||||
class ComicSource {
|
|
||||||
static final List<ComicSource> _sources = [];
|
|
||||||
|
|
||||||
static final List<Function> _listeners = [];
|
|
||||||
|
|
||||||
static void addListener(Function listener) {
|
|
||||||
_listeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void removeListener(Function listener) {
|
|
||||||
_listeners.remove(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void notifyListeners() {
|
|
||||||
for (var listener in _listeners) {
|
|
||||||
listener();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<ComicSource> all() => List.from(_sources);
|
|
||||||
|
|
||||||
static ComicSource? find(String key) =>
|
|
||||||
_sources.firstWhereOrNull((element) => element.key == key);
|
_sources.firstWhereOrNull((element) => element.key == key);
|
||||||
|
|
||||||
static ComicSource? fromIntKey(int key) =>
|
ComicSource? fromIntKey(int key) =>
|
||||||
_sources.firstWhereOrNull((element) => element.key.hashCode == key);
|
_sources.firstWhereOrNull((element) => element.key.hashCode == key);
|
||||||
|
|
||||||
static Future<void> init() async {
|
@override
|
||||||
|
@protected
|
||||||
|
Future<void> doInit() async {
|
||||||
|
await JsEngine().ensureInit();
|
||||||
final path = "${App.dataPath}/comic_source";
|
final path = "${App.dataPath}/comic_source";
|
||||||
if (!(await Directory(path).exists())) {
|
if (!(await Directory(path).exists())) {
|
||||||
Directory(path).create();
|
Directory(path).create();
|
||||||
@@ -120,26 +69,49 @@ class ComicSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future reload() async {
|
Future reload() async {
|
||||||
_sources.clear();
|
_sources.clear();
|
||||||
JsEngine().runCode("ComicSource.sources = {};");
|
JsEngine().runCode("ComicSource.sources = {};");
|
||||||
await init();
|
await doInit();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void add(ComicSource source) {
|
void add(ComicSource source) {
|
||||||
_sources.add(source);
|
_sources.add(source);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void remove(String key) {
|
void remove(String key) {
|
||||||
_sources.removeWhere((element) => element.key == key);
|
_sources.removeWhere((element) => element.key == key);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
static final availableUpdates = <String, String>{};
|
bool get isEmpty => _sources.isEmpty;
|
||||||
|
|
||||||
static bool get isEmpty => _sources.isEmpty;
|
/// Key is the source key, value is the version.
|
||||||
|
final _availableUpdates = <String, String>{};
|
||||||
|
|
||||||
|
void updateAvailableUpdates(Map<String, String> updates) {
|
||||||
|
_availableUpdates.addAll(updates);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> get availableUpdates => Map.from(_availableUpdates);
|
||||||
|
|
||||||
|
void notifyStateChange() {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ComicSource {
|
||||||
|
static List<ComicSource> all() => ComicSourceManager().all();
|
||||||
|
|
||||||
|
static ComicSource? find(String key) => ComicSourceManager().find(key);
|
||||||
|
|
||||||
|
static ComicSource? fromIntKey(int key) =>
|
||||||
|
ComicSourceManager().fromIntKey(key);
|
||||||
|
|
||||||
|
static bool get isEmpty => ComicSourceManager().isEmpty;
|
||||||
|
|
||||||
/// Name of this source.
|
/// Name of this source.
|
||||||
final String name;
|
final String name;
|
||||||
@@ -321,7 +293,7 @@ class AccountConfig {
|
|||||||
this.onLoginWithWebviewSuccess,
|
this.onLoginWithWebviewSuccess,
|
||||||
this.cookieFields,
|
this.cookieFields,
|
||||||
this.validateCookies,
|
this.validateCookies,
|
||||||
) : infoItems = const [];
|
) : infoItems = const [];
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountInfoItem {
|
class AccountInfoItem {
|
||||||
@@ -478,4 +450,4 @@ class ArchiveDownloader {
|
|||||||
final Future<Res<String>> Function(String cid, String aid) getDownloadUrl;
|
final Future<Res<String>> Function(String cid, String aid) getDownloadUrl;
|
||||||
|
|
||||||
const ArchiveDownloader(this.getArchives, this.getDownloadUrl);
|
const ArchiveDownloader(this.getArchives, this.getDownloadUrl);
|
||||||
}
|
}
|
||||||
|
@@ -336,8 +336,10 @@ class ComicChapters {
|
|||||||
}
|
}
|
||||||
if (chapters.isNotEmpty) {
|
if (chapters.isNotEmpty) {
|
||||||
return ComicChapters(chapters);
|
return ComicChapters(chapters);
|
||||||
} else {
|
} else if (groupedChapters.isNotEmpty) {
|
||||||
return ComicChapters.grouped(groupedChapters);
|
return ComicChapters.grouped(groupedChapters);
|
||||||
|
} else {
|
||||||
|
throw ArgumentError("Empty chapter list");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
48
lib/foundation/comic_source/types.dart
Normal file
48
lib/foundation/comic_source/types.dart
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
part of 'comic_source.dart';
|
||||||
|
|
||||||
|
/// build comic list, [Res.subData] should be maxPage or null if there is no limit.
|
||||||
|
typedef ComicListBuilder = Future<Res<List<Comic>>> Function(int page);
|
||||||
|
|
||||||
|
/// build comic list with next param, [Res.subData] should be next page param or null if there is no next page.
|
||||||
|
typedef ComicListBuilderWithNext = Future<Res<List<Comic>>> Function(
|
||||||
|
String? next);
|
||||||
|
|
||||||
|
typedef LoginFunction = Future<Res<bool>> Function(String, String);
|
||||||
|
|
||||||
|
typedef LoadComicFunc = Future<Res<ComicDetails>> Function(String id);
|
||||||
|
|
||||||
|
typedef LoadComicPagesFunc = Future<Res<List<String>>> Function(
|
||||||
|
String id, String? ep);
|
||||||
|
|
||||||
|
typedef CommentsLoader = Future<Res<List<Comment>>> Function(
|
||||||
|
String id, String? subId, int page, String? replyTo);
|
||||||
|
|
||||||
|
typedef SendCommentFunc = Future<Res<bool>> Function(
|
||||||
|
String id, String? subId, String content, String? replyTo);
|
||||||
|
|
||||||
|
typedef GetImageLoadingConfigFunc = Future<Map<String, dynamic>> Function(
|
||||||
|
String imageKey, String comicId, String epId)?;
|
||||||
|
typedef GetThumbnailLoadingConfigFunc = Map<String, dynamic> Function(
|
||||||
|
String imageKey)?;
|
||||||
|
|
||||||
|
typedef ComicThumbnailLoader = Future<Res<List<String>>> Function(
|
||||||
|
String comicId, String? next);
|
||||||
|
|
||||||
|
typedef LikeOrUnlikeComicFunc = Future<Res<bool>> Function(
|
||||||
|
String comicId, bool isLiking);
|
||||||
|
|
||||||
|
/// [isLiking] is true if the user is liking the comment, false if unliking.
|
||||||
|
/// return the new likes count or null.
|
||||||
|
typedef LikeCommentFunc = Future<Res<int?>> Function(
|
||||||
|
String comicId, String? subId, String commentId, bool isLiking);
|
||||||
|
|
||||||
|
/// [isUp] is true if the user is upvoting the comment, false if downvoting.
|
||||||
|
/// return the new vote count or null.
|
||||||
|
typedef VoteCommentFunc = Future<Res<int?>> Function(
|
||||||
|
String comicId, String? subId, String commentId, bool isUp, bool isCancel);
|
||||||
|
|
||||||
|
typedef HandleClickTagEvent = Map<String, String> Function(
|
||||||
|
String namespace, String tag);
|
||||||
|
|
||||||
|
/// [rating] is the rating value, 0-10. 1 represents 0.5 star.
|
||||||
|
typedef StarRatingFunc = Future<Res<bool>> Function(String comicId, int rating);
|
@@ -3,6 +3,7 @@ import 'dart:io';
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:dio/io.dart';
|
import 'package:dio/io.dart';
|
||||||
|
import 'package:flutter/foundation.dart' show protected;
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:html/parser.dart' as html;
|
import 'package:html/parser.dart' as html;
|
||||||
import 'package:html/dom.dart' as dom;
|
import 'package:html/dom.dart' as dom;
|
||||||
@@ -24,6 +25,7 @@ import 'package:venera/components/js_ui.dart';
|
|||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/network/app_dio.dart';
|
import 'package:venera/network/app_dio.dart';
|
||||||
import 'package:venera/network/cookie_jar.dart';
|
import 'package:venera/network/cookie_jar.dart';
|
||||||
|
import 'package:venera/utils/init.dart';
|
||||||
|
|
||||||
import 'comic_source/comic_source.dart';
|
import 'comic_source/comic_source.dart';
|
||||||
import 'consts.dart';
|
import 'consts.dart';
|
||||||
@@ -40,7 +42,7 @@ class JavaScriptRuntimeException implements Exception {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class JsEngine with _JSEngineApi, JsUiApi {
|
class JsEngine with _JSEngineApi, JsUiApi, Init {
|
||||||
factory JsEngine() => _cache ?? (_cache = JsEngine._create());
|
factory JsEngine() => _cache ?? (_cache = JsEngine._create());
|
||||||
|
|
||||||
static JsEngine? _cache;
|
static JsEngine? _cache;
|
||||||
@@ -64,7 +66,9 @@ class JsEngine with _JSEngineApi, JsUiApi {
|
|||||||
responseType: ResponseType.plain, validateStatus: (status) => true));
|
responseType: ResponseType.plain, validateStatus: (status) => true));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> init() async {
|
@override
|
||||||
|
@protected
|
||||||
|
Future<void> doInit() async {
|
||||||
if (!_closed) {
|
if (!_closed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -265,6 +265,7 @@ class LocalManager with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
_checkPathValidation();
|
_checkPathValidation();
|
||||||
_checkNoMedia();
|
_checkNoMedia();
|
||||||
|
await ComicSourceManager().ensureInit();
|
||||||
restoreDownloadingTasks();
|
restoreDownloadingTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -30,13 +30,15 @@ extension _FutureInit<T> on Future<T> {
|
|||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
await App.init().wait();
|
await App.init().wait();
|
||||||
SingleInstanceCookieJar("${App.dataPath}/cookie.db");
|
await SingleInstanceCookieJar.createInstance();
|
||||||
var futures = [
|
var futures = [
|
||||||
Rhttp.init(),
|
Rhttp.init(),
|
||||||
|
App.initComponents(),
|
||||||
SAFTaskWorker().init().wait(),
|
SAFTaskWorker().init().wait(),
|
||||||
AppTranslation.init().wait(),
|
AppTranslation.init().wait(),
|
||||||
TagsTranslation.readData().wait(),
|
TagsTranslation.readData().wait(),
|
||||||
JsEngine().init().then((_) => ComicSource.init()).wait(),
|
JsEngine().init().wait(),
|
||||||
|
ComicSourceManager().init().wait(),
|
||||||
];
|
];
|
||||||
await Future.wait(futures);
|
await Future.wait(futures);
|
||||||
CacheManager().setLimitSize(appdata.settings['cacheSize']);
|
CacheManager().setLimitSize(appdata.settings['cacheSize']);
|
||||||
|
@@ -152,6 +152,17 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
'sans-serif'
|
'sans-serif'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
if (App.isLinux) {
|
||||||
|
font = 'Noto Sans CJK';
|
||||||
|
fallback = [
|
||||||
|
'Segoe UI',
|
||||||
|
'Microsoft YaHei',
|
||||||
|
'PingFang SC',
|
||||||
|
'Noto Sans CJK',
|
||||||
|
'Arial',
|
||||||
|
'sans-serif'
|
||||||
|
];
|
||||||
|
}
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
colorScheme: SeedColorScheme.fromSeeds(
|
colorScheme: SeedColorScheme.fromSeeds(
|
||||||
primaryKey: primary,
|
primaryKey: primary,
|
||||||
|
@@ -257,18 +257,7 @@ class RHttpAdapter implements HttpClientAdapter {
|
|||||||
Future<void>? cancelFuture,
|
Future<void>? cancelFuture,
|
||||||
) async {
|
) async {
|
||||||
var res = await rhttp.Rhttp.request(
|
var res = await rhttp.Rhttp.request(
|
||||||
method: switch (options.method) {
|
method: rhttp.HttpMethod(options.method),
|
||||||
'GET' => rhttp.HttpMethod.get,
|
|
||||||
'POST' => rhttp.HttpMethod.post,
|
|
||||||
'PUT' => rhttp.HttpMethod.put,
|
|
||||||
'PATCH' => rhttp.HttpMethod.patch,
|
|
||||||
'DELETE' => rhttp.HttpMethod.delete,
|
|
||||||
'HEAD' => rhttp.HttpMethod.head,
|
|
||||||
'OPTIONS' => rhttp.HttpMethod.options,
|
|
||||||
'TRACE' => rhttp.HttpMethod.trace,
|
|
||||||
'CONNECT' => rhttp.HttpMethod.connect,
|
|
||||||
_ => throw ArgumentError('Unsupported method: ${options.method}'),
|
|
||||||
},
|
|
||||||
url: options.uri.toString(),
|
url: options.uri.toString(),
|
||||||
settings: settings,
|
settings: settings,
|
||||||
expectBody: rhttp.HttpExpectBody.stream,
|
expectBody: rhttp.HttpExpectBody.stream,
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:sqlite3/sqlite3.dart';
|
import 'package:sqlite3/sqlite3.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
@@ -200,6 +201,11 @@ class SingleInstanceCookieJar extends CookieJarSql {
|
|||||||
SingleInstanceCookieJar._create(super.path);
|
SingleInstanceCookieJar._create(super.path);
|
||||||
|
|
||||||
static SingleInstanceCookieJar? instance;
|
static SingleInstanceCookieJar? instance;
|
||||||
|
|
||||||
|
static Future<void> createInstance() async {
|
||||||
|
var dataPath = (await getApplicationSupportDirectory()).path;
|
||||||
|
instance = SingleInstanceCookieJar("$dataPath/cookie.db");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CookieManagerSql extends Interceptor {
|
class CookieManagerSql extends Interceptor {
|
||||||
|
@@ -2,6 +2,8 @@ import 'dart:async';
|
|||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart' show ChangeNotifier;
|
import 'package:flutter/widgets.dart' show ChangeNotifier;
|
||||||
|
import 'package:flutter_saf/flutter_saf.dart';
|
||||||
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.dart';
|
||||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||||
import 'package:venera/foundation/comic_type.dart';
|
import 'package:venera/foundation/comic_type.dart';
|
||||||
@@ -739,11 +741,12 @@ class ArchiveDownloadTask extends DownloadTask {
|
|||||||
path = dir.path;
|
path = dir.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
var resultFile = File(FilePath.join(path!, "archive.zip"));
|
var archiveFile =
|
||||||
|
File(FilePath.join(App.dataPath, "archive_downloading.zip"));
|
||||||
|
|
||||||
Log.info("Download", "Downloading $archiveUrl");
|
Log.info("Download", "Downloading $archiveUrl");
|
||||||
|
|
||||||
_downloader = FileDownloader(archiveUrl, resultFile.path);
|
_downloader = FileDownloader(archiveUrl, archiveFile.path);
|
||||||
|
|
||||||
bool isDownloaded = false;
|
bool isDownloaded = false;
|
||||||
|
|
||||||
@@ -772,22 +775,33 @@ class ArchiveDownloadTask extends DownloadTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await extractArchive(path!);
|
await _extractArchive(archiveFile.path, path!);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_setError("Failed to extract archive: $e");
|
_setError("Failed to extract archive: $e");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await resultFile.deleteIgnoreError();
|
await archiveFile.deleteIgnoreError();
|
||||||
|
|
||||||
LocalManager().completeTask(this);
|
LocalManager().completeTask(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> extractArchive(String path) async {
|
static Future<void> _extractArchive(String archive, String outDir) async {
|
||||||
var resultFile = FilePath.join(path, "archive.zip");
|
var out = Directory(outDir);
|
||||||
await Isolate.run(() {
|
if (out is AndroidDirectory) {
|
||||||
ZipFile.openAndExtract(resultFile, path);
|
// Saf directory can't be accessed by native code.
|
||||||
});
|
var cacheDir = FilePath.join(App.cachePath, "archive_downloading");
|
||||||
|
Directory(cacheDir).forceCreateSync();
|
||||||
|
await Isolate.run(() {
|
||||||
|
ZipFile.openAndExtract(archive, cacheDir);
|
||||||
|
});
|
||||||
|
await copyDirectoryIsolate(Directory(cacheDir), Directory(outDir));
|
||||||
|
await Directory(cacheDir).deleteIgnoreError(recursive: true);
|
||||||
|
} else {
|
||||||
|
await Isolate.run(() {
|
||||||
|
ZipFile.openAndExtract(archive, outDir);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@@ -651,10 +651,16 @@ class _CommentImage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class RichCommentContent extends StatefulWidget {
|
class RichCommentContent extends StatefulWidget {
|
||||||
const RichCommentContent({super.key, required this.text});
|
const RichCommentContent({
|
||||||
|
super.key,
|
||||||
|
required this.text,
|
||||||
|
this.showImages = true,
|
||||||
|
});
|
||||||
|
|
||||||
final String text;
|
final String text;
|
||||||
|
|
||||||
|
final bool showImages;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<RichCommentContent> createState() => _RichCommentContentState();
|
State<RichCommentContent> createState() => _RichCommentContentState();
|
||||||
}
|
}
|
||||||
@@ -808,7 +814,7 @@ class _RichCommentContentState extends State<RichCommentContent> {
|
|||||||
children: textSpan,
|
children: textSpan,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (images.isNotEmpty) {
|
if (images.isNotEmpty && widget.showImages) {
|
||||||
content = Column(
|
content = Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
@@ -138,7 +138,10 @@ class _CommentWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: RichCommentContent(text: comment.content).fixWidth(324),
|
child: RichCommentContent(
|
||||||
|
text: comment.content,
|
||||||
|
showImages: false,
|
||||||
|
).fixWidth(324),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
if (comment.time != null)
|
if (comment.time != null)
|
||||||
@@ -147,4 +150,4 @@ class _CommentWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -40,10 +40,11 @@ class ComicSourcePage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (shouldUpdate.isNotEmpty) {
|
if (shouldUpdate.isNotEmpty) {
|
||||||
|
var updates = <String, String>{};
|
||||||
for (var key in shouldUpdate) {
|
for (var key in shouldUpdate) {
|
||||||
ComicSource.availableUpdates[key] = versions[key]!;
|
updates[key] = versions[key]!;
|
||||||
}
|
}
|
||||||
ComicSource.notifyListeners();
|
ComicSourceManager().updateAvailableUpdates(updates);
|
||||||
}
|
}
|
||||||
return shouldUpdate.length;
|
return shouldUpdate.length;
|
||||||
}
|
}
|
||||||
@@ -73,13 +74,13 @@ class _BodyState extends State<_Body> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
ComicSource.addListener(updateUI);
|
ComicSourceManager().addListener(updateUI);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
ComicSource.removeListener(updateUI);
|
ComicSourceManager().removeListener(updateUI);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -115,7 +116,7 @@ class _BodyState extends State<_Body> {
|
|||||||
onConfirm: () {
|
onConfirm: () {
|
||||||
var file = File(source.filePath);
|
var file = File(source.filePath);
|
||||||
file.delete();
|
file.delete();
|
||||||
ComicSource.remove(source.key);
|
ComicSourceManager().remove(source.key);
|
||||||
_validatePages();
|
_validatePages();
|
||||||
App.forceRebuild();
|
App.forceRebuild();
|
||||||
},
|
},
|
||||||
@@ -136,7 +137,7 @@ class _BodyState extends State<_Body> {
|
|||||||
child: const Text("cancel")),
|
child: const Text("cancel")),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await ComicSource.reload();
|
await ComicSourceManager().reload();
|
||||||
App.forceRebuild();
|
App.forceRebuild();
|
||||||
},
|
},
|
||||||
child: const Text("continue")),
|
child: const Text("continue")),
|
||||||
@@ -150,7 +151,7 @@ class _BodyState extends State<_Body> {
|
|||||||
}
|
}
|
||||||
context.to(
|
context.to(
|
||||||
() => _EditFilePage(source.filePath, () async {
|
() => _EditFilePage(source.filePath, () async {
|
||||||
await ComicSource.reload();
|
await ComicSourceManager().reload();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -162,7 +163,7 @@ class _BodyState extends State<_Body> {
|
|||||||
App.rootContext.showMessage(message: "Invalid url config");
|
App.rootContext.showMessage(message: "Invalid url config");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ComicSource.remove(source.key);
|
ComicSourceManager().remove(source.key);
|
||||||
bool cancel = false;
|
bool cancel = false;
|
||||||
LoadingDialogController? controller;
|
LoadingDialogController? controller;
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
@@ -179,14 +180,14 @@ class _BodyState extends State<_Body> {
|
|||||||
controller?.close();
|
controller?.close();
|
||||||
await ComicSourceParser().parse(res.data!, source.filePath);
|
await ComicSourceParser().parse(res.data!, source.filePath);
|
||||||
await File(source.filePath).writeAsString(res.data!);
|
await File(source.filePath).writeAsString(res.data!);
|
||||||
if (ComicSource.availableUpdates.containsKey(source.key)) {
|
if (ComicSourceManager().availableUpdates.containsKey(source.key)) {
|
||||||
ComicSource.availableUpdates.remove(source.key);
|
ComicSourceManager().availableUpdates.remove(source.key);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (cancel) return;
|
if (cancel) return;
|
||||||
App.rootContext.showMessage(message: e.toString());
|
App.rootContext.showMessage(message: e.toString());
|
||||||
}
|
}
|
||||||
await ComicSource.reload();
|
await ComicSourceManager().reload();
|
||||||
App.forceRebuild();
|
App.forceRebuild();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +305,7 @@ class _BodyState extends State<_Body> {
|
|||||||
|
|
||||||
Future<void> addSource(String js, String fileName) async {
|
Future<void> addSource(String js, String fileName) async {
|
||||||
var comicSource = await ComicSourceParser().createAndParse(js, fileName);
|
var comicSource = await ComicSourceParser().createAndParse(js, fileName);
|
||||||
ComicSource.add(comicSource);
|
ComicSourceManager().add(comicSource);
|
||||||
_addAllPagesWithComicSource(comicSource);
|
_addAllPagesWithComicSource(comicSource);
|
||||||
appdata.saveData();
|
appdata.saveData();
|
||||||
App.forceRebuild();
|
App.forceRebuild();
|
||||||
@@ -563,7 +564,7 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void showUpdateDialog() async {
|
void showUpdateDialog() async {
|
||||||
var text = ComicSource.availableUpdates.entries.map((e) {
|
var text = ComicSourceManager().availableUpdates.entries.map((e) {
|
||||||
return "${ComicSource.find(e.key)!.name}: ${e.value}";
|
return "${ComicSource.find(e.key)!.name}: ${e.value}";
|
||||||
}).join("\n");
|
}).join("\n");
|
||||||
bool doUpdate = false;
|
bool doUpdate = false;
|
||||||
@@ -592,9 +593,9 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> {
|
|||||||
withProgress: true,
|
withProgress: true,
|
||||||
);
|
);
|
||||||
int current = 0;
|
int current = 0;
|
||||||
int total = ComicSource.availableUpdates.length;
|
int total = ComicSourceManager().availableUpdates.length;
|
||||||
try {
|
try {
|
||||||
var shouldUpdate = ComicSource.availableUpdates.keys.toList();
|
var shouldUpdate = ComicSourceManager().availableUpdates.keys.toList();
|
||||||
for (var key in shouldUpdate) {
|
for (var key in shouldUpdate) {
|
||||||
var source = ComicSource.find(key)!;
|
var source = ComicSource.find(key)!;
|
||||||
await _BodyState.update(source, false);
|
await _BodyState.update(source, false);
|
||||||
@@ -692,7 +693,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var newVersion = ComicSource.availableUpdates[source.key];
|
var newVersion = ComicSourceManager().availableUpdates[source.key];
|
||||||
bool hasUpdate =
|
bool hasUpdate =
|
||||||
newVersion != null && compareSemVer(newVersion, source.version);
|
newVersion != null && compareSemVer(newVersion, source.version);
|
||||||
|
|
||||||
@@ -960,7 +961,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
|
|||||||
source.data["account"] = null;
|
source.data["account"] = null;
|
||||||
source.account?.logout();
|
source.account?.logout();
|
||||||
source.saveData();
|
source.saveData();
|
||||||
ComicSource.notifyListeners();
|
ComicSourceManager().notifyStateChange();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
trailing: const Icon(Icons.logout),
|
trailing: const Icon(Icons.logout),
|
||||||
|
@@ -703,8 +703,9 @@ abstract class FollowUpdatesService {
|
|||||||
if (_isInitialized) return;
|
if (_isInitialized) return;
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
_check();
|
_check();
|
||||||
|
DataSync().addListener(updateFollowUpdatesUI);
|
||||||
// A short interval will not affect the performance since every comic has a check time.
|
// A short interval will not affect the performance since every comic has a check time.
|
||||||
Timer.periodic(const Duration(minutes: 5), (timer) {
|
Timer.periodic(const Duration(minutes: 10), (timer) {
|
||||||
_check();
|
_check();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -162,16 +162,50 @@ class _SyncDataWidgetState extends State<_SyncDataWidget>
|
|||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
if (DataSync().lastError != null)
|
||||||
|
InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
onTap: () {
|
||||||
|
showDialogMessage(
|
||||||
|
App.rootContext,
|
||||||
|
"Error".tl,
|
||||||
|
DataSync().lastError!,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.errorContainer,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
color: Colors.red,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text('Error'.tl, style: ts.s12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).paddingRight(4),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.cloud_upload_outlined),
|
icon: const Icon(Icons.cloud_upload_outlined),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
DataSync().uploadData();
|
DataSync().uploadData();
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.cloud_download_outlined),
|
icon: const Icon(Icons.cloud_download_outlined),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
DataSync().downloadData();
|
DataSync().downloadData();
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -538,7 +572,8 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
launchUrlString("https://github.com/venera-app/venera/blob/master/doc/import_comic.md");
|
launchUrlString(
|
||||||
|
"https://github.com/venera-app/venera/blob/master/doc/import_comic.md");
|
||||||
},
|
},
|
||||||
).fixWidth(90).paddingRight(8),
|
).fixWidth(90).paddingRight(8),
|
||||||
Button.filled(
|
Button.filled(
|
||||||
@@ -595,19 +630,19 @@ class _ComicSourceWidgetState extends State<_ComicSourceWidget> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
comicSources = ComicSource.all().map((e) => e.name).toList();
|
comicSources = ComicSource.all().map((e) => e.name).toList();
|
||||||
ComicSource.addListener(onComicSourceChange);
|
ComicSourceManager().addListener(onComicSourceChange);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
ComicSource.removeListener(onComicSourceChange);
|
ComicSourceManager().removeListener(onComicSourceChange);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
int get _availableUpdates {
|
int get _availableUpdates {
|
||||||
int c = 0;
|
int c = 0;
|
||||||
ComicSource.availableUpdates.forEach((key, version) {
|
ComicSourceManager().availableUpdates.forEach((key, version) {
|
||||||
var source = ComicSource.find(key);
|
var source = ComicSource.find(key);
|
||||||
if (source != null) {
|
if (source != null) {
|
||||||
if (compareSemVer(version, source.version)) {
|
if (compareSemVer(version, source.version)) {
|
||||||
@@ -697,14 +732,24 @@ class _ComicSourceWidgetState extends State<_ComicSourceWidget> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.update, color: context.colorScheme.primary, size: 20,),
|
Icon(
|
||||||
|
Icons.update,
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text("@c updates".tlParams({
|
Text(
|
||||||
'c': _availableUpdates,
|
"@c updates".tlParams({
|
||||||
}), style: ts.withColor(context.colorScheme.primary),),
|
'c': _availableUpdates,
|
||||||
|
}),
|
||||||
|
style: ts.withColor(context.colorScheme.primary),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
).toAlign(Alignment.centerLeft).paddingHorizontal(16).paddingBottom(8),
|
)
|
||||||
|
.toAlign(Alignment.centerLeft)
|
||||||
|
.paddingHorizontal(16)
|
||||||
|
.paddingBottom(8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -844,7 +889,8 @@ class _ImageFavoritesState extends State<ImageFavorites> {
|
|||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 8, vertical: 2),
|
horizontal: 8, vertical: 2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
color:
|
||||||
|
Theme.of(context).colorScheme.secondaryContainer,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:venera/foundation/appdata.dart';
|
||||||
import 'package:venera/pages/categories_page.dart';
|
import 'package:venera/pages/categories_page.dart';
|
||||||
import 'package:venera/pages/search_page.dart';
|
import 'package:venera/pages/search_page.dart';
|
||||||
import 'package:venera/pages/settings/settings_page.dart';
|
import 'package:venera/pages/settings/settings_page.dart';
|
||||||
@@ -39,6 +40,7 @@ class _MainPageState extends State<MainPage> {
|
|||||||
_observer = NaviObserver();
|
_observer = NaviObserver();
|
||||||
_navigatorKey = GlobalKey();
|
_navigatorKey = GlobalKey();
|
||||||
App.mainNavigatorKey = _navigatorKey;
|
App.mainNavigatorKey = _navigatorKey;
|
||||||
|
index = int.tryParse(appdata.settings['initialPage'].toString()) ?? 0;
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +62,7 @@ class _MainPageState extends State<MainPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return NaviPane(
|
return NaviPane(
|
||||||
|
initialPage: index,
|
||||||
observer: _observer,
|
observer: _observer,
|
||||||
navigatorKey: _navigatorKey!,
|
navigatorKey: _navigatorKey!,
|
||||||
paneItems: [
|
paneItems: [
|
||||||
|
@@ -15,6 +15,7 @@ import 'package:photo_view/photo_view_gallery.dart';
|
|||||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
import 'package:venera/components/components.dart';
|
import 'package:venera/components/components.dart';
|
||||||
import 'package:venera/components/custom_slider.dart';
|
import 'package:venera/components/custom_slider.dart';
|
||||||
|
import 'package:venera/components/window_frame.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.dart';
|
||||||
import 'package:venera/foundation/cache_manager.dart';
|
import 'package:venera/foundation/cache_manager.dart';
|
||||||
@@ -169,6 +170,7 @@ class _ReaderState extends State<Reader>
|
|||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
initImagesPerPage(widget.initialPage ?? 1);
|
initImagesPerPage(widget.initialPage ?? 1);
|
||||||
|
initReaderWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setImageCacheSize() async {
|
void setImageCacheSize() async {
|
||||||
@@ -191,6 +193,9 @@ class _ReaderState extends State<Reader>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
if (isFullscreen) {
|
||||||
|
fullscreen();
|
||||||
|
}
|
||||||
autoPageTurningTimer?.cancel();
|
autoPageTurningTimer?.cancel();
|
||||||
focusNode.dispose();
|
focusNode.dispose();
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
@@ -199,6 +204,7 @@ class _ReaderState extends State<Reader>
|
|||||||
DataSync().onDataChanged();
|
DataSync().onDataChanged();
|
||||||
});
|
});
|
||||||
PaintingBinding.instance.imageCache.maximumSizeBytes = 100 << 20;
|
PaintingBinding.instance.imageCache.maximumSizeBytes = 100 << 20;
|
||||||
|
disposeReaderWindow();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,6 +224,9 @@ class _ReaderState extends State<Reader>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onKeyEvent(KeyEvent event) {
|
void onKeyEvent(KeyEvent event) {
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.f12 && event is KeyUpEvent) {
|
||||||
|
fullscreen();
|
||||||
|
}
|
||||||
_imageViewController?.handleKeyEvent(event);
|
_imageViewController?.handleKeyEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,11 +438,8 @@ abstract mixin class _ReaderLocation {
|
|||||||
|
|
||||||
bool toPage(int page) {
|
bool toPage(int page) {
|
||||||
if (_validatePage(page)) {
|
if (_validatePage(page)) {
|
||||||
if (page == this.page) {
|
if (page == this.page && page != 1 && page != maxPage) {
|
||||||
if (!(chapter == 1 && page == 1) &&
|
return false;
|
||||||
!(chapter == maxChapter && page == maxPage)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.page = page;
|
this.page = page;
|
||||||
update();
|
update();
|
||||||
@@ -495,9 +501,38 @@ abstract mixin class _ReaderLocation {
|
|||||||
mixin class _ReaderWindow {
|
mixin class _ReaderWindow {
|
||||||
bool isFullscreen = false;
|
bool isFullscreen = false;
|
||||||
|
|
||||||
void fullscreen() {
|
late WindowFrameController windowFrame;
|
||||||
windowManager.setFullScreen(!isFullscreen);
|
|
||||||
|
bool _isInit = false;
|
||||||
|
|
||||||
|
void initReaderWindow() {
|
||||||
|
if (!App.isDesktop || _isInit) return;
|
||||||
|
windowFrame = WindowFrame.of(App.rootContext);
|
||||||
|
windowFrame.addCloseListener(onWindowClose);
|
||||||
|
_isInit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fullscreen() async {
|
||||||
|
if (!App.isDesktop) return;
|
||||||
|
await windowManager.hide();
|
||||||
|
await windowManager.setFullScreen(!isFullscreen);
|
||||||
|
await windowManager.show();
|
||||||
isFullscreen = !isFullscreen;
|
isFullscreen = !isFullscreen;
|
||||||
|
WindowFrame.of(App.rootContext).setWindowFrame(!isFullscreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool onWindowClose() {
|
||||||
|
if (Navigator.of(App.rootContext).canPop()) {
|
||||||
|
Navigator.of(App.rootContext).pop();
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void disposeReaderWindow() {
|
||||||
|
if (!App.isDesktop) return;
|
||||||
|
windowFrame.removeCloseListener(onWindowClose);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -80,6 +80,16 @@ class _ExploreSettingsState extends State<ExploreSettings> {
|
|||||||
'japanese': "Japanese",
|
'japanese': "Japanese",
|
||||||
},
|
},
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
|
SelectSetting(
|
||||||
|
title: "Initial Page".tl,
|
||||||
|
settingKey: "initialPage",
|
||||||
|
optionTranslation: {
|
||||||
|
'0': "Home Page".tl,
|
||||||
|
'1': "Favorites Page".tl,
|
||||||
|
'2': "Explore Page".tl,
|
||||||
|
'3': "Categories Page".tl,
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -177,7 +177,7 @@ abstract class CBZ {
|
|||||||
tags: metaData.tags,
|
tags: metaData.tags,
|
||||||
comicType: ComicType.local,
|
comicType: ComicType.local,
|
||||||
directory: dest.name,
|
directory: dest.name,
|
||||||
chapters: ComicChapters.fromJson(cpMap),
|
chapters: ComicChapters.fromJsonOrNull(cpMap),
|
||||||
downloadedChapters: cpMap?.keys.toList() ?? [],
|
downloadedChapters: cpMap?.keys.toList() ?? [],
|
||||||
cover: 'cover.${coverFile.extension}',
|
cover: 'cover.${coverFile.extension}',
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
|
@@ -103,7 +103,7 @@ Future<void> importAppData(File file, [bool checkVersion = false]) async {
|
|||||||
await file.copy(targetFile);
|
await file.copy(targetFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await ComicSource.reload();
|
await ComicSourceManager().reload();
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
cacheDir.deleteIgnoreError(recursive: true);
|
cacheDir.deleteIgnoreError(recursive: true);
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
import 'package:dio/io.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.dart';
|
||||||
@@ -19,7 +18,7 @@ class DataSync with ChangeNotifier {
|
|||||||
downloadData();
|
downloadData();
|
||||||
}
|
}
|
||||||
LocalFavoritesManager().addListener(onDataChanged);
|
LocalFavoritesManager().addListener(onDataChanged);
|
||||||
ComicSource.addListener(onDataChanged);
|
ComicSourceManager().addListener(onDataChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDataChanged() {
|
void onDataChanged() {
|
||||||
@@ -40,7 +39,11 @@ class DataSync with ChangeNotifier {
|
|||||||
|
|
||||||
bool get isUploading => _isUploading;
|
bool get isUploading => _isUploading;
|
||||||
|
|
||||||
bool haveWaitingTask = false;
|
bool _haveWaitingTask = false;
|
||||||
|
|
||||||
|
String? _lastError;
|
||||||
|
|
||||||
|
String? get lastError => _lastError;
|
||||||
|
|
||||||
bool get isEnabled {
|
bool get isEnabled {
|
||||||
var config = appdata.settings['webdav'];
|
var config = appdata.settings['webdav'];
|
||||||
@@ -64,17 +67,19 @@ class DataSync with ChangeNotifier {
|
|||||||
|
|
||||||
Future<Res<bool>> uploadData() async {
|
Future<Res<bool>> uploadData() async {
|
||||||
if (isDownloading) return const Res(true);
|
if (isDownloading) return const Res(true);
|
||||||
if (haveWaitingTask) return const Res(true);
|
if (_haveWaitingTask) return const Res(true);
|
||||||
while (isUploading) {
|
while (isUploading) {
|
||||||
haveWaitingTask = true;
|
_haveWaitingTask = true;
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
}
|
}
|
||||||
haveWaitingTask = false;
|
_haveWaitingTask = false;
|
||||||
_isUploading = true;
|
_isUploading = true;
|
||||||
|
_lastError = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
try {
|
try {
|
||||||
var config = _validateConfig();
|
var config = _validateConfig();
|
||||||
if (config == null) {
|
if (config == null) {
|
||||||
|
_lastError = 'Invalid WebDAV configuration';
|
||||||
return const Res.error('Invalid WebDAV configuration');
|
return const Res.error('Invalid WebDAV configuration');
|
||||||
}
|
}
|
||||||
if (config.isEmpty) {
|
if (config.isEmpty) {
|
||||||
@@ -84,27 +89,13 @@ class DataSync with ChangeNotifier {
|
|||||||
String user = config[1];
|
String user = config[1];
|
||||||
String pass = config[2];
|
String pass = config[2];
|
||||||
|
|
||||||
var proxy = await AppDio.getProxy();
|
|
||||||
|
|
||||||
var client = newClient(
|
var client = newClient(
|
||||||
url,
|
url,
|
||||||
user: user,
|
user: user,
|
||||||
password: pass,
|
password: pass,
|
||||||
adapter: IOHttpClientAdapter(
|
adapter: RHttpAdapter(),
|
||||||
createHttpClient: () {
|
|
||||||
return HttpClient()
|
|
||||||
..findProxy = (uri) => proxy == null ? "DIRECT" : "PROXY $proxy";
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
|
||||||
await client.ping();
|
|
||||||
} catch (e) {
|
|
||||||
Log.error("Upload Data", 'Failed to connect to WebDAV server');
|
|
||||||
return const Res.error('Failed to connect to WebDAV server');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
appdata.settings['dataVersion']++;
|
appdata.settings['dataVersion']++;
|
||||||
await appdata.saveData(false);
|
await appdata.saveData(false);
|
||||||
@@ -131,6 +122,7 @@ class DataSync with ChangeNotifier {
|
|||||||
return const Res(true);
|
return const Res(true);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("Upload Data", e, s);
|
Log.error("Upload Data", e, s);
|
||||||
|
_lastError = e.toString();
|
||||||
return Res.error(e.toString());
|
return Res.error(e.toString());
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -140,17 +132,19 @@ class DataSync with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Res<bool>> downloadData() async {
|
Future<Res<bool>> downloadData() async {
|
||||||
if (haveWaitingTask) return const Res(true);
|
if (_haveWaitingTask) return const Res(true);
|
||||||
while (isDownloading || isUploading) {
|
while (isDownloading || isUploading) {
|
||||||
haveWaitingTask = true;
|
_haveWaitingTask = true;
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
}
|
}
|
||||||
haveWaitingTask = false;
|
_haveWaitingTask = false;
|
||||||
_isDownloading = true;
|
_isDownloading = true;
|
||||||
|
_lastError = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
try {
|
try {
|
||||||
var config = _validateConfig();
|
var config = _validateConfig();
|
||||||
if (config == null) {
|
if (config == null) {
|
||||||
|
_lastError = 'Invalid WebDAV configuration';
|
||||||
return const Res.error('Invalid WebDAV configuration');
|
return const Res.error('Invalid WebDAV configuration');
|
||||||
}
|
}
|
||||||
if (config.isEmpty) {
|
if (config.isEmpty) {
|
||||||
@@ -160,27 +154,13 @@ class DataSync with ChangeNotifier {
|
|||||||
String user = config[1];
|
String user = config[1];
|
||||||
String pass = config[2];
|
String pass = config[2];
|
||||||
|
|
||||||
var proxy = await AppDio.getProxy();
|
|
||||||
|
|
||||||
var client = newClient(
|
var client = newClient(
|
||||||
url,
|
url,
|
||||||
user: user,
|
user: user,
|
||||||
password: pass,
|
password: pass,
|
||||||
adapter: IOHttpClientAdapter(
|
adapter: RHttpAdapter(),
|
||||||
createHttpClient: () {
|
|
||||||
return HttpClient()
|
|
||||||
..findProxy = (uri) => proxy == null ? "DIRECT" : "PROXY $proxy";
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
|
||||||
await client.ping();
|
|
||||||
} catch (e) {
|
|
||||||
Log.error("Data Sync", 'Failed to connect to WebDAV server');
|
|
||||||
return const Res.error('Failed to connect to WebDAV server');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var files = await client.readDir('/');
|
var files = await client.readDir('/');
|
||||||
files.sort((a, b) => b.name!.compareTo(a.name!));
|
files.sort((a, b) => b.name!.compareTo(a.name!));
|
||||||
@@ -206,6 +186,7 @@ class DataSync with ChangeNotifier {
|
|||||||
return const Res(true);
|
return const Res(true);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("Data Sync", e, s);
|
Log.error("Data Sync", e, s);
|
||||||
|
_lastError = e.toString();
|
||||||
return Res.error(e.toString());
|
return Res.error(e.toString());
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
40
lib/utils/init.dart
Normal file
40
lib/utils/init.dart
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
/// A mixin class that provides a way to ensure the class is initialized.
|
||||||
|
abstract mixin class Init {
|
||||||
|
bool _isInit = false;
|
||||||
|
|
||||||
|
final _initCompleter = <Completer<void>>[];
|
||||||
|
|
||||||
|
/// Ensure the class is initialized.
|
||||||
|
Future<void> ensureInit() async {
|
||||||
|
if (_isInit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var completer = Completer<void>();
|
||||||
|
_initCompleter.add(completer);
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _markInit() async {
|
||||||
|
_isInit = true;
|
||||||
|
for (var completer in _initCompleter) {
|
||||||
|
completer.complete();
|
||||||
|
}
|
||||||
|
_initCompleter.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Future<void> doInit();
|
||||||
|
|
||||||
|
/// Initialize the class.
|
||||||
|
Future<void> init() async {
|
||||||
|
if (_isInit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await doInit();
|
||||||
|
await _markInit();
|
||||||
|
}
|
||||||
|
}
|
@@ -40,6 +40,7 @@ extension FileSystemEntityExt on FileSystemEntity {
|
|||||||
return p.basename(path);
|
return p.basename(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete the file or directory and ignore errors.
|
||||||
Future<void> deleteIgnoreError({bool recursive = false}) async {
|
Future<void> deleteIgnoreError({bool recursive = false}) async {
|
||||||
try {
|
try {
|
||||||
await delete(recursive: recursive);
|
await delete(recursive: recursive);
|
||||||
@@ -48,12 +49,14 @@ extension FileSystemEntityExt on FileSystemEntity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete the file or directory if it exists.
|
||||||
Future<void> deleteIfExists({bool recursive = false}) async {
|
Future<void> deleteIfExists({bool recursive = false}) async {
|
||||||
if (existsSync()) {
|
if (existsSync()) {
|
||||||
await delete(recursive: recursive);
|
await delete(recursive: recursive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete the file or directory if it exists.
|
||||||
void deleteIfExistsSync({bool recursive = false}) {
|
void deleteIfExistsSync({bool recursive = false}) {
|
||||||
if (existsSync()) {
|
if (existsSync()) {
|
||||||
deleteSync(recursive: recursive);
|
deleteSync(recursive: recursive);
|
||||||
@@ -74,12 +77,14 @@ extension FileExtension on File {
|
|||||||
await newFile.writeAsBytes(await readAsBytes());
|
await newFile.writeAsBytes(await readAsBytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the base name of the file without the extension.
|
||||||
String get basenameWithoutExt {
|
String get basenameWithoutExt {
|
||||||
return p.basenameWithoutExtension(path);
|
return p.basenameWithoutExtension(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DirectoryExtension on Directory {
|
extension DirectoryExtension on Directory {
|
||||||
|
/// Calculate the size of the directory.
|
||||||
Future<int> get size async {
|
Future<int> get size async {
|
||||||
if (!existsSync()) return 0;
|
if (!existsSync()) return 0;
|
||||||
int total = 0;
|
int total = 0;
|
||||||
@@ -91,6 +96,7 @@ extension DirectoryExtension on Directory {
|
|||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Change the base name of the directory.
|
||||||
Directory renameX(String newName) {
|
Directory renameX(String newName) {
|
||||||
newName = sanitizeFileName(newName);
|
newName = sanitizeFileName(newName);
|
||||||
return renameSync(path.replaceLast(name, newName));
|
return renameSync(path.replaceLast(name, newName));
|
||||||
@@ -100,6 +106,7 @@ extension DirectoryExtension on Directory {
|
|||||||
return File(FilePath.join(path, name));
|
return File(FilePath.join(path, name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete the contents of the directory.
|
||||||
void deleteContentsSync({recursive = true}) {
|
void deleteContentsSync({recursive = true}) {
|
||||||
if (!existsSync()) return;
|
if (!existsSync()) return;
|
||||||
for (var f in listSync()) {
|
for (var f in listSync()) {
|
||||||
@@ -107,14 +114,24 @@ extension DirectoryExtension on Directory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete the contents of the directory.
|
||||||
Future<void> deleteContents({recursive = true}) async {
|
Future<void> deleteContents({recursive = true}) async {
|
||||||
if (!existsSync()) return;
|
if (!existsSync()) return;
|
||||||
for (var f in listSync()) {
|
for (var f in listSync()) {
|
||||||
await f.deleteIfExists(recursive: recursive);
|
await f.deleteIfExists(recursive: recursive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create the directory. If the directory already exists, delete it first.
|
||||||
|
void forceCreateSync() {
|
||||||
|
if (existsSync()) {
|
||||||
|
deleteSync(recursive: true);
|
||||||
|
}
|
||||||
|
createSync(recursive: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sanitize the file name. Remove invalid characters and trim the file name.
|
||||||
String sanitizeFileName(String fileName) {
|
String sanitizeFileName(String fileName) {
|
||||||
if (fileName.endsWith('.')) {
|
if (fileName.endsWith('.')) {
|
||||||
fileName = fileName.substring(0, fileName.length - 1);
|
fileName = fileName.substring(0, fileName.length - 1);
|
||||||
@@ -157,6 +174,8 @@ Future<void> copyDirectory(Directory source, Directory destination) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Copy the **contents** of the source directory to the destination directory.
|
||||||
|
/// This function is executed in an isolate to prevent the UI from freezing.
|
||||||
Future<void> copyDirectoryIsolate(
|
Future<void> copyDirectoryIsolate(
|
||||||
Directory source, Directory destination) async {
|
Directory source, Directory destination) async {
|
||||||
await Isolate.run(() => overrideIO(() => copyDirectory(source, destination)));
|
await Isolate.run(() => overrideIO(() => copyDirectory(source, destination)));
|
||||||
|
@@ -757,10 +757,11 @@ packages:
|
|||||||
rhttp:
|
rhttp:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: rhttp
|
path: rhttp
|
||||||
sha256: "3deabc6c3384b4efa252dfb4a5059acc6530117fdc1b10f5f67ff9768c9af75a"
|
ref: HEAD
|
||||||
url: "https://pub.dev"
|
resolved-ref: "18d430cc45fd4f0114885c5235090abf65106257"
|
||||||
source: hosted
|
url: "https://github.com/wgh136/rhttp"
|
||||||
|
source: git
|
||||||
version: "0.10.0"
|
version: "0.10.0"
|
||||||
screen_retriever:
|
screen_retriever:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
|
@@ -2,7 +2,7 @@ name: venera
|
|||||||
description: "A comic app."
|
description: "A comic app."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.3.1+131
|
version: 1.3.2+132
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.6.0 <4.0.0'
|
sdk: '>=3.6.0 <4.0.0'
|
||||||
@@ -57,7 +57,10 @@ 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.10.0
|
rhttp:
|
||||||
|
git:
|
||||||
|
url: https://github.com/wgh136/rhttp
|
||||||
|
path: rhttp
|
||||||
webdav_client:
|
webdav_client:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/wgh136/webdav_client
|
url: https://github.com/wgh136/webdav_client
|
||||||
|
Reference in New Issue
Block a user