mirror of
https://github.com/venera-app/venera.git
synced 2025-09-28 08:17:25 +00:00
Compare commits
60 Commits
linux-wind
...
v1.4.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
97768b4945 | ||
2481780ab3 | |||
![]() |
49481bfa6a | ||
211850d73e | |||
fcf0334d55 | |||
aa8eec5792 | |||
6eb0060dd6 | |||
c096f5a2d8 | |||
554b9f2a77 | |||
f87afbe397 | |||
6ff30f8ac3 | |||
118941f239 | |||
d91bca6913 | |||
463ad5b5bc | |||
971fc1da92 | |||
37af7e266a | |||
276e23354d | |||
3da00595b7 | |||
![]() |
d3c115ee0c | ||
dcc94c5b3d | |||
a116b5b615 | |||
05fcb23a4d | |||
daa6e8ce18 | |||
8665994572 | |||
90441af989 | |||
7631fab86b | |||
cd9b07bb3e | |||
6c179ceb95 | |||
ec48dbef57 | |||
cd1cc1229e | |||
![]() |
bda299e1f8 | ||
![]() |
78ea129564 | ||
![]() |
f3b4598bb6 | ||
![]() |
7bc4c69a32 | ||
![]() |
a8e55e0151 | ||
![]() |
fddd959545 | ||
![]() |
ebf6846bf1 | ||
0f2d0bb9f9 | |||
48338e4ef7 | |||
8d8e345d82 | |||
![]() |
fcbf6a6277 | ||
d83d679eb9 | |||
d6087e5f59 | |||
37371bee6c | |||
45fe5f503a | |||
d440ed6424 | |||
d812332613 | |||
dee8d17b1e | |||
![]() |
c0d461ebd9 | ||
![]() |
45e2a1142a | ||
![]() |
533c2b2507 | ||
![]() |
29b7e0d646 | ||
b1870b65d6 | |||
1103076009 | |||
51739355c8 | |||
1b4f67b314 | |||
ba8831caa6 | |||
2b1684b0fc | |||
cd3f09efae | |||
03628f2afa |
29
.github/workflows/issue_check.yml
vendored
Normal file
29
.github/workflows/issue_check.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: Check Issue
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Check Issue
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
id: checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check Issue
|
||||||
|
id: check
|
||||||
|
uses: wgh136/gpt_issue_checker@v1.0.2
|
||||||
|
with:
|
||||||
|
api-url: ${{ secrets.API_URL }}
|
||||||
|
api-key: ${{ secrets.API_KEY }}
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
prompt: "You are a repository issue checker. The project is a comic app that supports view local or network comics using config files. To view a comic source, user must add a config file. User should not report any issue related to config file to the project repository because there is another repository for managing config files. You are given an issue content and you need to decide whether to close the issue. If you decide to close the issue, you should also provide a comment explaining why you are closing the issue. If you decide not to close the issue, you should provide a comment which is a summary of the issue. You should response with a JSON object with the following keys: should_close, should_comment, comment."
|
||||||
|
model: "gpt-4o"
|
@@ -67,7 +67,6 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
|
||||||
applicationId = "com.github.wgh136.venera"
|
applicationId = "com.github.wgh136.venera"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||||
@@ -125,6 +124,6 @@ flutter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "androidx.activity:activity-ktx:1.9.2"
|
implementation "androidx.activity:activity-ktx:1.10.1"
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
}
|
}
|
||||||
|
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||||
|
@@ -18,7 +18,7 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version '8.3.2' apply false
|
id "com.android.application" version '8.9.0' apply false
|
||||||
id "org.jetbrains.kotlin.android" version "1.8.10" apply false
|
id "org.jetbrains.kotlin.android" version "1.8.10" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1358,3 +1358,29 @@ let APP = {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set clipboard text
|
||||||
|
* @param text {string}
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*
|
||||||
|
* @since 1.3.4
|
||||||
|
*/
|
||||||
|
function setClipboard(text) {
|
||||||
|
return sendMessage({
|
||||||
|
method: 'setClipboard',
|
||||||
|
text: text
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get clipboard text
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*
|
||||||
|
* @since 1.3.4
|
||||||
|
*/
|
||||||
|
function getClipboard() {
|
||||||
|
return sendMessage({
|
||||||
|
method: 'getClipboard'
|
||||||
|
})
|
||||||
|
}
|
@@ -373,7 +373,21 @@
|
|||||||
"Paging": "分页",
|
"Paging": "分页",
|
||||||
"Continuous": "连续",
|
"Continuous": "连续",
|
||||||
"Display mode of comic list": "漫画列表的显示模式",
|
"Display mode of comic list": "漫画列表的显示模式",
|
||||||
"A valid WebDav directory URL": "有效的WebDav目录URL"
|
"Show Page Number": "显示页码",
|
||||||
|
"Jump to page": "跳转到页面",
|
||||||
|
"Page": "页面",
|
||||||
|
"Jump": "跳转",
|
||||||
|
"Copy Image": "复制图片",
|
||||||
|
"A valid WebDav directory URL": "有效的WebDav目录URL",
|
||||||
|
"Shut Down": "关闭",
|
||||||
|
"Uploading data...": "正在上传数据...",
|
||||||
|
"Pages": "页数",
|
||||||
|
"Long press zoom position": "长按缩放位置",
|
||||||
|
"Press position": "按压位置",
|
||||||
|
"Screen center": "屏幕中心",
|
||||||
|
"Suggestions": "建议",
|
||||||
|
"Do not report any issues related to sources to App repo.": "请不要向App仓库报告任何与源相关的问题",
|
||||||
|
"Click the setting icon to change the source list url.": "点击设置图标更改源列表URL"
|
||||||
},
|
},
|
||||||
"zh_TW": {
|
"zh_TW": {
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
@@ -749,6 +763,20 @@
|
|||||||
"Paging": "分頁",
|
"Paging": "分頁",
|
||||||
"Continuous": "連續",
|
"Continuous": "連續",
|
||||||
"Display mode of comic list": "漫畫列表的顯示模式",
|
"Display mode of comic list": "漫畫列表的顯示模式",
|
||||||
"A valid WebDav directory URL": "有效的WebDav目錄URL"
|
"Show Page Number": "顯示頁碼",
|
||||||
|
"Jump to page": "跳轉到頁面",
|
||||||
|
"Page": "頁面",
|
||||||
|
"Jump": "跳轉",
|
||||||
|
"Copy Image": "複製圖片",
|
||||||
|
"A valid WebDav directory URL": "有效的WebDav目錄URL",
|
||||||
|
"Shut Down": "關閉",
|
||||||
|
"Uploading data...": "正在上傳數據...",
|
||||||
|
"Pages": "頁數",
|
||||||
|
"Long press zoom position": "長按縮放位置",
|
||||||
|
"Press position": "按壓位置",
|
||||||
|
"Screen center": "螢幕中心",
|
||||||
|
"Suggestions": "建議",
|
||||||
|
"Do not report any issues related to sources to App repo.": "請不要向App倉庫報告任何與源相關的問題",
|
||||||
|
"Click the setting icon to change the source list url.": "點擊設定圖示更改源列表URL"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BIN
debian/gui/venera.png
vendored
BIN
debian/gui/venera.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 64 KiB |
@@ -80,7 +80,7 @@ class _AppbarState extends State<Appbar> {
|
|||||||
var content = Container(
|
var content = Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: widget.backgroundColor ??
|
color: widget.backgroundColor ??
|
||||||
context.colorScheme.surface.toOpacity(0.72),
|
context.colorScheme.surface.toOpacity(0.86),
|
||||||
),
|
),
|
||||||
height: _kAppBarHeight + context.padding.top,
|
height: _kAppBarHeight + context.padding.top,
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -231,7 +231,7 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
|||||||
child: BlurEffect(
|
child: BlurEffect(
|
||||||
blur: 15,
|
blur: 15,
|
||||||
child: Material(
|
child: Material(
|
||||||
color: context.colorScheme.surface.toOpacity(0.72),
|
color: context.colorScheme.surface.toOpacity(0.86),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
borderRadius: BorderRadius.circular(radius),
|
borderRadius: BorderRadius.circular(radius),
|
||||||
child: body,
|
child: body,
|
||||||
|
@@ -334,7 +334,12 @@ class ComicTile extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var children = <Widget>[];
|
var children = <Widget>[];
|
||||||
for (var line in text.split('\n')) {
|
var lines = text.split('\n');
|
||||||
|
lines.removeWhere((e) => e.trim().isEmpty);
|
||||||
|
if (lines.length > 3) {
|
||||||
|
lines = lines.sublist(0, 3);
|
||||||
|
}
|
||||||
|
for (var line in lines) {
|
||||||
children.add(Container(
|
children.add(Container(
|
||||||
margin: const EdgeInsets.fromLTRB(2, 0, 2, 2),
|
margin: const EdgeInsets.fromLTRB(2, 0, 2, 2),
|
||||||
padding: constraints.maxWidth < 80
|
padding: constraints.maxWidth < 80
|
||||||
|
@@ -163,3 +163,29 @@ class SliverLazyToBoxAdapter extends StatelessWidget {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SliverAnimatedVisibility extends StatelessWidget {
|
||||||
|
const SliverAnimatedVisibility({
|
||||||
|
super.key,
|
||||||
|
required this.visible,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool visible;
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var child = visible ? this.child : const SizedBox.shrink();
|
||||||
|
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: AnimatedSize(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -61,7 +61,7 @@ class _MenuRoute<T> extends PopupRoute<T> {
|
|||||||
child: BlurEffect(
|
child: BlurEffect(
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
child: Material(
|
child: Material(
|
||||||
color: context.colorScheme.surface.toOpacity(0.78),
|
color: context.colorScheme.surface.toOpacity(0.92),
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: width,
|
width: width,
|
||||||
|
@@ -51,10 +51,32 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
|
|||||||
|
|
||||||
static bool _isMouseScroll = App.isDesktop;
|
static bool _isMouseScroll = App.isDesktop;
|
||||||
|
|
||||||
|
late int id;
|
||||||
|
|
||||||
|
static int _id = 0;
|
||||||
|
|
||||||
|
var activeChildren = <int>{};
|
||||||
|
|
||||||
|
ScrollState? parent;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_controller = widget.controller ?? ScrollController();
|
_controller = widget.controller ?? ScrollController();
|
||||||
super.initState();
|
super.initState();
|
||||||
|
id = _id;
|
||||||
|
_id++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
parent = ScrollState.maybeOf(context);
|
||||||
|
super.didChangeDependencies();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
parent?.onChildInactive(id);
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -66,8 +88,7 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
|
|||||||
const BouncingScrollPhysics(),
|
const BouncingScrollPhysics(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Listener(
|
var child = Listener(
|
||||||
behavior: HitTestBehavior.translucent,
|
|
||||||
onPointerDown: (event) {
|
onPointerDown: (event) {
|
||||||
_futurePosition = null;
|
_futurePosition = null;
|
||||||
if (_isMouseScroll) {
|
if (_isMouseScroll) {
|
||||||
@@ -77,6 +98,9 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPointerSignal: (pointerSignal) {
|
onPointerSignal: (pointerSignal) {
|
||||||
|
if (activeChildren.isNotEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (pointerSignal is PointerScrollEvent) {
|
if (pointerSignal is PointerScrollEvent) {
|
||||||
if (HardwareKeyboard.instance.isShiftPressed) {
|
if (HardwareKeyboard.instance.isShiftPressed) {
|
||||||
return;
|
return;
|
||||||
@@ -113,8 +137,14 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: ScrollControllerProvider._(
|
child: ScrollState._(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
|
onChildActive: (id) {
|
||||||
|
activeChildren.add(id);
|
||||||
|
},
|
||||||
|
onChildInactive: (id) {
|
||||||
|
activeChildren.remove(id);
|
||||||
|
},
|
||||||
child: widget.builder(
|
child: widget.builder(
|
||||||
context,
|
context,
|
||||||
_controller,
|
_controller,
|
||||||
@@ -124,25 +154,49 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (parent != null) {
|
||||||
|
return MouseRegion(
|
||||||
|
onEnter: (_) {
|
||||||
|
parent!.onChildActive(id);
|
||||||
|
},
|
||||||
|
onExit: (_) {
|
||||||
|
parent!.onChildInactive(id);
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return child;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScrollControllerProvider extends InheritedWidget {
|
class ScrollState extends InheritedWidget {
|
||||||
const ScrollControllerProvider._({
|
const ScrollState._({
|
||||||
required this.controller,
|
required this.controller,
|
||||||
required super.child,
|
required super.child,
|
||||||
|
required this.onChildActive,
|
||||||
|
required this.onChildInactive,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ScrollController controller;
|
final ScrollController controller;
|
||||||
|
|
||||||
static ScrollController of(BuildContext context) {
|
final void Function(int id) onChildActive;
|
||||||
final ScrollControllerProvider? provider =
|
|
||||||
context.dependOnInheritedWidgetOfExactType<ScrollControllerProvider>();
|
final void Function(int id) onChildInactive;
|
||||||
return provider!.controller;
|
|
||||||
|
static ScrollState of(BuildContext context) {
|
||||||
|
final ScrollState? provider =
|
||||||
|
context.dependOnInheritedWidgetOfExactType<ScrollState>();
|
||||||
|
return provider!;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ScrollState? maybeOf(BuildContext context) {
|
||||||
|
return context.dependOnInheritedWidgetOfExactType<ScrollState>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool updateShouldNotify(ScrollControllerProvider oldWidget) {
|
bool updateShouldNotify(ScrollState oldWidget) {
|
||||||
return oldWidget.controller != controller;
|
return oldWidget.controller != controller;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -82,7 +82,7 @@ class _WindowFrameState extends State<WindowFrame> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
windowManager.close();
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -567,7 +567,6 @@ class _VirtualWindowFrameState extends State<VirtualWindowFrame>
|
|||||||
boxShadow: <BoxShadow>[
|
boxShadow: <BoxShadow>[
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.toOpacity(_isFocused ? 0.4 : 0.2),
|
color: Colors.black.toOpacity(_isFocused ? 0.4 : 0.2),
|
||||||
offset: Offset(0.0, 2),
|
|
||||||
blurRadius: 4,
|
blurRadius: 4,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@@ -13,7 +13,7 @@ export "widget_utils.dart";
|
|||||||
export "context.dart";
|
export "context.dart";
|
||||||
|
|
||||||
class _App {
|
class _App {
|
||||||
final version = "1.3.3";
|
final version = "1.4.0";
|
||||||
|
|
||||||
bool get isAndroid => Platform.isAndroid;
|
bool get isAndroid => Platform.isAndroid;
|
||||||
|
|
||||||
@@ -47,6 +47,7 @@ class _App {
|
|||||||
|
|
||||||
late String dataPath;
|
late String dataPath;
|
||||||
late String cachePath;
|
late String cachePath;
|
||||||
|
String? externalStoragePath;
|
||||||
|
|
||||||
final rootNavigatorKey = GlobalKey<NavigatorState>();
|
final rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
@@ -77,6 +78,9 @@ 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;
|
||||||
|
if (isAndroid) {
|
||||||
|
externalStoragePath = (await getExternalStorageDirectory())!.path;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initComponents() async {
|
Future<void> initComponents() async {
|
||||||
|
@@ -17,17 +17,18 @@ class Appdata with Init {
|
|||||||
bool _isSavingData = false;
|
bool _isSavingData = false;
|
||||||
|
|
||||||
Future<void> saveData([bool sync = true]) async {
|
Future<void> saveData([bool sync = true]) async {
|
||||||
if (_isSavingData) {
|
while (_isSavingData) {
|
||||||
await Future.doWhile(() async {
|
|
||||||
await Future.delayed(const Duration(milliseconds: 20));
|
await Future.delayed(const Duration(milliseconds: 20));
|
||||||
return _isSavingData;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
_isSavingData = true;
|
_isSavingData = true;
|
||||||
|
try {
|
||||||
var data = jsonEncode(toJson());
|
var data = jsonEncode(toJson());
|
||||||
var file = File(FilePath.join(App.dataPath, 'appdata.json'));
|
var file = File(FilePath.join(App.dataPath, 'appdata.json'));
|
||||||
await file.writeAsString(data);
|
await file.writeAsString(data);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
_isSavingData = false;
|
_isSavingData = false;
|
||||||
|
}
|
||||||
if (sync) {
|
if (sync) {
|
||||||
DataSync().uploadData();
|
DataSync().uploadData();
|
||||||
}
|
}
|
||||||
@@ -85,9 +86,18 @@ class Appdata with Init {
|
|||||||
|
|
||||||
var implicitData = <String, dynamic>{};
|
var implicitData = <String, dynamic>{};
|
||||||
|
|
||||||
void writeImplicitData() {
|
void writeImplicitData() async {
|
||||||
|
while (_isSavingData) {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 20));
|
||||||
|
}
|
||||||
|
_isSavingData = true;
|
||||||
|
try {
|
||||||
var file = File(FilePath.join(App.dataPath, 'implicitData.json'));
|
var file = File(FilePath.join(App.dataPath, 'implicitData.json'));
|
||||||
file.writeAsString(jsonEncode(implicitData));
|
await file.writeAsString(jsonEncode(implicitData));
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_isSavingData = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -109,8 +119,13 @@ class Appdata with Init {
|
|||||||
searchHistory = List.from(json['searchHistory']);
|
searchHistory = List.from(json['searchHistory']);
|
||||||
var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json'));
|
var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json'));
|
||||||
if (await implicitDataFile.exists()) {
|
if (await implicitDataFile.exists()) {
|
||||||
|
try {
|
||||||
implicitData = jsonDecode(await implicitDataFile.readAsString());
|
implicitData = jsonDecode(await implicitDataFile.readAsString());
|
||||||
}
|
}
|
||||||
|
catch(_) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +161,7 @@ class Settings with ChangeNotifier {
|
|||||||
'cacheSize': 2048, // in MB
|
'cacheSize': 2048, // in MB
|
||||||
'downloadThreads': 5,
|
'downloadThreads': 5,
|
||||||
'enableLongPressToZoom': true,
|
'enableLongPressToZoom': true,
|
||||||
|
'longPressZoomPosition': "press", // press, center
|
||||||
'checkUpdateOnStart': false,
|
'checkUpdateOnStart': false,
|
||||||
'limitImageWidth': true,
|
'limitImageWidth': true,
|
||||||
'webdav': [], // empty means not configured
|
'webdav': [], // empty means not configured
|
||||||
@@ -168,6 +184,7 @@ class Settings with ChangeNotifier {
|
|||||||
'followUpdatesFolder': null,
|
'followUpdatesFolder': null,
|
||||||
'initialPage': '0',
|
'initialPage': '0',
|
||||||
'comicListDisplayMode': 'paging', // paging, continuous
|
'comicListDisplayMode': 'paging', // paging, continuous
|
||||||
|
'showPageNumberInReader': true,
|
||||||
};
|
};
|
||||||
|
|
||||||
operator [](String key) {
|
operator [](String key) {
|
||||||
|
@@ -34,24 +34,28 @@ class CategoryButtonData {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CategoryItem {
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
final PageJumpTarget target;
|
||||||
|
|
||||||
|
const CategoryItem(this.label, this.target);
|
||||||
|
}
|
||||||
|
|
||||||
abstract class BaseCategoryPart {
|
abstract class BaseCategoryPart {
|
||||||
String get title;
|
String get title;
|
||||||
|
|
||||||
List<String> get categories;
|
List<CategoryItem> get categories;
|
||||||
|
|
||||||
List<String>? get categoryParams => null;
|
|
||||||
|
|
||||||
bool get enableRandom;
|
bool get enableRandom;
|
||||||
|
|
||||||
String get categoryType;
|
|
||||||
|
|
||||||
/// Data class for building a part of category page.
|
/// Data class for building a part of category page.
|
||||||
const BaseCategoryPart();
|
const BaseCategoryPart();
|
||||||
}
|
}
|
||||||
|
|
||||||
class FixedCategoryPart extends BaseCategoryPart {
|
class FixedCategoryPart extends BaseCategoryPart {
|
||||||
@override
|
@override
|
||||||
final List<String> categories;
|
final List<CategoryItem> categories;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get enableRandom => false;
|
bool get enableRandom => false;
|
||||||
@@ -59,19 +63,12 @@ class FixedCategoryPart extends BaseCategoryPart {
|
|||||||
@override
|
@override
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
@override
|
|
||||||
final String categoryType;
|
|
||||||
|
|
||||||
@override
|
|
||||||
final List<String>? categoryParams;
|
|
||||||
|
|
||||||
/// A [BaseCategoryPart] that show fixed tags on category page.
|
/// A [BaseCategoryPart] that show fixed tags on category page.
|
||||||
const FixedCategoryPart(this.title, this.categories, this.categoryType,
|
const FixedCategoryPart(this.title, this.categories);
|
||||||
[this.categoryParams]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class RandomCategoryPart extends BaseCategoryPart {
|
class RandomCategoryPart extends BaseCategoryPart {
|
||||||
final List<String> tags;
|
final List<CategoryItem> all;
|
||||||
|
|
||||||
final int randomNumber;
|
final int randomNumber;
|
||||||
|
|
||||||
@@ -81,67 +78,59 @@ class RandomCategoryPart extends BaseCategoryPart {
|
|||||||
@override
|
@override
|
||||||
bool get enableRandom => true;
|
bool get enableRandom => true;
|
||||||
|
|
||||||
@override
|
List<CategoryItem> _categories() {
|
||||||
final String categoryType;
|
if (randomNumber >= all.length) {
|
||||||
|
return all;
|
||||||
List<String> _categories() {
|
|
||||||
if (randomNumber >= tags.length) {
|
|
||||||
return tags;
|
|
||||||
}
|
}
|
||||||
var start = math.Random().nextInt(tags.length - randomNumber);
|
var start = math.Random().nextInt(all.length - randomNumber);
|
||||||
return tags.sublist(start, start + randomNumber);
|
return all.sublist(start, start + randomNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<String> get categories => _categories();
|
List<CategoryItem> get categories => _categories();
|
||||||
|
|
||||||
/// A [BaseCategoryPart] that show random tags on category page.
|
/// A [BaseCategoryPart] that show a part of random tags on category page.
|
||||||
const RandomCategoryPart(
|
const RandomCategoryPart(
|
||||||
this.title, this.tags, this.randomNumber, this.categoryType);
|
this.title,
|
||||||
|
this.all,
|
||||||
|
this.randomNumber,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class RandomCategoryPartWithRuntimeData extends BaseCategoryPart {
|
class DynamicCategoryPart extends BaseCategoryPart {
|
||||||
final Iterable<String> Function() loadTags;
|
final JSAutoFreeFunction loader;
|
||||||
|
|
||||||
final int randomNumber;
|
final String sourceKey;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final String title;
|
List<CategoryItem> get categories {
|
||||||
|
var data = loader([]);
|
||||||
@override
|
if (data is! List) {
|
||||||
bool get enableRandom => true;
|
throw "DynamicCategoryPart loader must return a List";
|
||||||
|
|
||||||
@override
|
|
||||||
final String categoryType;
|
|
||||||
|
|
||||||
static final random = math.Random();
|
|
||||||
|
|
||||||
List<String> _categories() {
|
|
||||||
var tags = loadTags();
|
|
||||||
if (randomNumber >= tags.length) {
|
|
||||||
return tags.toList();
|
|
||||||
}
|
}
|
||||||
final start = random.nextInt(tags.length - randomNumber);
|
var res = <CategoryItem>[];
|
||||||
var res = List.filled(randomNumber, '');
|
for (var item in data) {
|
||||||
int index = -1;
|
if (item is! Map) {
|
||||||
for (var s in tags) {
|
throw "DynamicCategoryPart loader must return a List of Map";
|
||||||
index++;
|
|
||||||
if (start > index) {
|
|
||||||
continue;
|
|
||||||
} else if (index == start + randomNumber) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
res[index - start] = s;
|
var label = item['label'];
|
||||||
|
var target = PageJumpTarget.parse(sourceKey, item['target']);
|
||||||
|
if (label is! String) {
|
||||||
|
throw "Category label must be a String";
|
||||||
|
}
|
||||||
|
res.add(CategoryItem(label, target));
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<String> get categories => _categories();
|
bool get enableRandom => false;
|
||||||
|
|
||||||
/// A [BaseCategoryPart] that show random tags on category page.
|
@override
|
||||||
RandomCategoryPartWithRuntimeData(
|
final String title;
|
||||||
this.title, this.loadTags, this.randomNumber, this.categoryType);
|
|
||||||
|
/// A [BaseCategoryPart] that show dynamic tags on category page.
|
||||||
|
const DynamicCategoryPart(this.title, this.loader, this.sourceKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
CategoryData getCategoryDataWithKey(String key) {
|
CategoryData getCategoryDataWithKey(String key) {
|
||||||
|
@@ -11,6 +11,8 @@ import 'package:venera/foundation/app.dart';
|
|||||||
import 'package:venera/foundation/comic_type.dart';
|
import 'package:venera/foundation/comic_type.dart';
|
||||||
import 'package:venera/foundation/history.dart';
|
import 'package:venera/foundation/history.dart';
|
||||||
import 'package:venera/foundation/res.dart';
|
import 'package:venera/foundation/res.dart';
|
||||||
|
import 'package:venera/pages/category_comics_page.dart';
|
||||||
|
import 'package:venera/pages/search_result_page.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/init.dart';
|
||||||
@@ -349,7 +351,7 @@ class ExplorePagePart {
|
|||||||
/// - category:categoryName
|
/// - category:categoryName
|
||||||
///
|
///
|
||||||
/// End with `@`+`param` if the category has a parameter.
|
/// End with `@`+`param` if the category has a parameter.
|
||||||
final String? viewMore;
|
final PageJumpTarget? viewMore;
|
||||||
|
|
||||||
const ExplorePagePart(this.title, this.comics, this.viewMore);
|
const ExplorePagePart(this.title, this.comics, this.viewMore);
|
||||||
}
|
}
|
||||||
|
@@ -169,7 +169,9 @@ class ComicDetails with HistoryMixin {
|
|||||||
static Map<String, List<String>> _generateMap(Map<dynamic, dynamic> map) {
|
static Map<String, List<String>> _generateMap(Map<dynamic, dynamic> map) {
|
||||||
var res = <String, List<String>>{};
|
var res = <String, List<String>>{};
|
||||||
map.forEach((key, value) {
|
map.forEach((key, value) {
|
||||||
|
if (value is List) {
|
||||||
res[key] = List<String>.from(value);
|
res[key] = List<String>.from(value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -342,7 +344,8 @@ class ComicChapters {
|
|||||||
} else if (groupedChapters.isNotEmpty) {
|
} else if (groupedChapters.isNotEmpty) {
|
||||||
return ComicChapters.grouped(groupedChapters);
|
return ComicChapters.grouped(groupedChapters);
|
||||||
} else {
|
} else {
|
||||||
throw ArgumentError("Empty chapter list");
|
// return a empty list.
|
||||||
|
return ComicChapters(chapters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,3 +432,110 @@ class ComicChapters {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PageJumpTarget {
|
||||||
|
final String sourceKey;
|
||||||
|
|
||||||
|
final String page;
|
||||||
|
|
||||||
|
final Map<String, dynamic>? attributes;
|
||||||
|
|
||||||
|
const PageJumpTarget(this.sourceKey, this.page, this.attributes);
|
||||||
|
|
||||||
|
static PageJumpTarget parse(String sourceKey, dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
if (value['page'] != null) {
|
||||||
|
return PageJumpTarget(
|
||||||
|
sourceKey,
|
||||||
|
value["page"] ?? "search",
|
||||||
|
value["attributes"],
|
||||||
|
);
|
||||||
|
} else if (value["action"] != null) {
|
||||||
|
// old version `onClickTag`
|
||||||
|
var page = value["action"];
|
||||||
|
if (page == "search") {
|
||||||
|
return PageJumpTarget(
|
||||||
|
sourceKey,
|
||||||
|
"search",
|
||||||
|
{
|
||||||
|
"text": value["keyword"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (page == "category") {
|
||||||
|
return PageJumpTarget(
|
||||||
|
sourceKey,
|
||||||
|
"category",
|
||||||
|
{
|
||||||
|
"category": value["keyword"],
|
||||||
|
"param": value["param"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return PageJumpTarget(sourceKey, page, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (value is String) {
|
||||||
|
// old version string encoding. search: `search:keyword`, category: `category:keyword` or `category:keyword@param`
|
||||||
|
var segments = value.split(":");
|
||||||
|
var page = segments[0];
|
||||||
|
if (page == "search") {
|
||||||
|
return PageJumpTarget(
|
||||||
|
sourceKey,
|
||||||
|
"search",
|
||||||
|
{
|
||||||
|
"text": segments[1],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (page == "category") {
|
||||||
|
var c = segments[1];
|
||||||
|
if (c.contains('@')) {
|
||||||
|
var parts = c.split('@');
|
||||||
|
return PageJumpTarget(
|
||||||
|
sourceKey,
|
||||||
|
"category",
|
||||||
|
{
|
||||||
|
"category": parts[0],
|
||||||
|
"param": parts[1],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return PageJumpTarget(
|
||||||
|
sourceKey,
|
||||||
|
"category",
|
||||||
|
{
|
||||||
|
"category": c,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return PageJumpTarget(sourceKey, page, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return PageJumpTarget(sourceKey, "Invalid Data", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void jump(BuildContext context) {
|
||||||
|
if (page == "search") {
|
||||||
|
context.to(
|
||||||
|
() => SearchResultPage(
|
||||||
|
text: attributes?["text"] ?? attributes?["keyword"] ?? "",
|
||||||
|
sourceKey: sourceKey,
|
||||||
|
options: List.from(attributes?["options"] ?? []),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (page == "category") {
|
||||||
|
var key = ComicSource.find(sourceKey)!.categoryData!.key;
|
||||||
|
context.to(
|
||||||
|
() => CategoryComicsPage(
|
||||||
|
categoryKey: key,
|
||||||
|
category: attributes?["category"] ??
|
||||||
|
(throw ArgumentError("Category name is required")),
|
||||||
|
options: List.from(attributes?["options"] ?? []),
|
||||||
|
param: attributes?["param"],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Log.error("Page Jump", "Unknown page: $page");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -80,9 +80,8 @@ class ComicSourceParser {
|
|||||||
|
|
||||||
Future<ComicSource> parse(String js, String filePath) async {
|
Future<ComicSource> parse(String js, String filePath) async {
|
||||||
js = js.replaceAll("\r\n", "\n");
|
js = js.replaceAll("\r\n", "\n");
|
||||||
var line1 = js
|
var line1 =
|
||||||
.split('\n')
|
js.split('\n').firstWhereOrNull((e) => e.trim().startsWith("class "));
|
||||||
.firstWhereOrNull((element) => element.removeAllBlank.isNotEmpty);
|
|
||||||
if (line1 == null ||
|
if (line1 == null ||
|
||||||
!line1.startsWith("class ") ||
|
!line1.startsWith("class ") ||
|
||||||
!line1.contains("extends ComicSource")) {
|
!line1.contains("extends ComicSource")) {
|
||||||
@@ -336,7 +335,7 @@ class ComicSourceParser {
|
|||||||
(e['comics'] as List).map((e) {
|
(e['comics'] as List).map((e) {
|
||||||
return Comic.fromJson(e, _key!);
|
return Comic.fromJson(e, _key!);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
e['viewMore'],
|
PageJumpTarget.parse(_key!, e['viewMore']),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -404,6 +403,43 @@ class ComicSourceParser {
|
|||||||
var categoryParts = <BaseCategoryPart>[];
|
var categoryParts = <BaseCategoryPart>[];
|
||||||
|
|
||||||
for (var c in doc["parts"]) {
|
for (var c in doc["parts"]) {
|
||||||
|
if (c["categories"] != null && c["categories"] is! List) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List? categories = c["categories"];
|
||||||
|
if (categories == null || categories[0] is Map) {
|
||||||
|
// new format
|
||||||
|
final String name = c["name"];
|
||||||
|
final String type = c["type"];
|
||||||
|
final cs = categories
|
||||||
|
?.map(
|
||||||
|
(e) => CategoryItem(
|
||||||
|
e['label'],
|
||||||
|
PageJumpTarget.parse(_key!, e['target']),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
if (type != "dynamic" && (cs == null || cs.isEmpty)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (type == "fixed") {
|
||||||
|
categoryParts.add(FixedCategoryPart(name, cs!));
|
||||||
|
} else if (type == "random") {
|
||||||
|
categoryParts
|
||||||
|
.add(RandomCategoryPart(name, cs!, c["randomNumber"] ?? 1));
|
||||||
|
} else if (type == "dynamic" && categories == null) {
|
||||||
|
var loader = c["loader"];
|
||||||
|
if (loader is! JSInvokable) {
|
||||||
|
throw "DynamicCategoryPart loader must be a function";
|
||||||
|
}
|
||||||
|
categoryParts.add(DynamicCategoryPart(
|
||||||
|
name,
|
||||||
|
JSAutoFreeFunction(loader),
|
||||||
|
_key!,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// old format
|
||||||
final String name = c["name"];
|
final String name = c["name"];
|
||||||
final String type = c["type"];
|
final String type = c["type"];
|
||||||
final List<String> tags = List.from(c["categories"]);
|
final List<String> tags = List.from(c["categories"]);
|
||||||
@@ -413,12 +449,45 @@ class ComicSourceParser {
|
|||||||
if (groupParam != null) {
|
if (groupParam != null) {
|
||||||
categoryParams = List.filled(tags.length, groupParam);
|
categoryParams = List.filled(tags.length, groupParam);
|
||||||
}
|
}
|
||||||
|
var cs = <CategoryItem>[];
|
||||||
|
for (int i = 0; i < tags.length; i++) {
|
||||||
|
PageJumpTarget target;
|
||||||
|
if (itemType == 'category') {
|
||||||
|
target = PageJumpTarget(
|
||||||
|
_key!,
|
||||||
|
'category',
|
||||||
|
{
|
||||||
|
"category": tags[i],
|
||||||
|
"param": categoryParams?.elementAtOrNull(i),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (itemType == 'search') {
|
||||||
|
target = PageJumpTarget(
|
||||||
|
_key!,
|
||||||
|
'search',
|
||||||
|
{
|
||||||
|
"keyword": tags[i],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (itemType == 'search_with_namespace') {
|
||||||
|
target = PageJumpTarget(
|
||||||
|
_key!,
|
||||||
|
'search',
|
||||||
|
{
|
||||||
|
"keyword": "$name:$tags[i]",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
target = PageJumpTarget(_key!, itemType, null);
|
||||||
|
}
|
||||||
|
cs.add(CategoryItem(tags[i], target));
|
||||||
|
}
|
||||||
if (type == "fixed") {
|
if (type == "fixed") {
|
||||||
categoryParts
|
categoryParts.add(FixedCategoryPart(name, cs));
|
||||||
.add(FixedCategoryPart(name, tags, itemType, categoryParams));
|
|
||||||
} else if (type == "random") {
|
} else if (type == "random") {
|
||||||
categoryParts.add(
|
categoryParts
|
||||||
RandomCategoryPart(name, tags, c["randomNumber"] ?? 1, itemType));
|
.add(RandomCategoryPart(name, cs, c["randomNumber"] ?? 1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,7 +689,8 @@ class ComicSourceParser {
|
|||||||
|
|
||||||
final bool multiFolder = _getValue("favorites.multiFolder");
|
final bool multiFolder = _getValue("favorites.multiFolder");
|
||||||
final bool? isOldToNewSort = _getValue("favorites.isOldToNewSort");
|
final bool? isOldToNewSort = _getValue("favorites.isOldToNewSort");
|
||||||
final bool? singleFolderForSingleComic = _getValue("favorites.singleFolderForSingleComic");
|
final bool? singleFolderForSingleComic =
|
||||||
|
_getValue("favorites.singleFolderForSingleComic");
|
||||||
|
|
||||||
Future<Res<T>> retryZone<T>(Future<Res<T>> Function() func) async {
|
Future<Res<T>> retryZone<T>(Future<Res<T>> Function() func) async {
|
||||||
if (!ComicSource.find(_key!)!.isLogged) {
|
if (!ComicSource.find(_key!)!.isLogged) {
|
||||||
@@ -978,9 +1048,12 @@ class ComicSourceParser {
|
|||||||
var res = JsEngine().runCode("""
|
var res = JsEngine().runCode("""
|
||||||
ComicSource.sources.$_key.comic.onClickTag(${jsonEncode(namespace)}, ${jsonEncode(tag)})
|
ComicSource.sources.$_key.comic.onClickTag(${jsonEncode(namespace)}, ${jsonEncode(tag)})
|
||||||
""");
|
""");
|
||||||
var r = Map<String, String?>.from(res);
|
if (res is! Map) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var r = Map<String, dynamic>.from(res);
|
||||||
r.removeWhere((key, value) => value == null);
|
r.removeWhere((key, value) => value == null);
|
||||||
return Map.from(r);
|
return PageJumpTarget.parse(_key!, r);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -41,7 +41,7 @@ typedef LikeCommentFunc = Future<Res<int?>> Function(
|
|||||||
typedef VoteCommentFunc = Future<Res<int?>> Function(
|
typedef VoteCommentFunc = Future<Res<int?>> Function(
|
||||||
String comicId, String? subId, String commentId, bool isUp, bool isCancel);
|
String comicId, String? subId, String commentId, bool isUp, bool isCancel);
|
||||||
|
|
||||||
typedef HandleClickTagEvent = Map<String, String> Function(
|
typedef HandleClickTagEvent = PageJumpTarget? Function(
|
||||||
String namespace, String tag);
|
String namespace, String tag);
|
||||||
|
|
||||||
/// [rating] is the rating value, 0-10. 1 represents 0.5 star.
|
/// [rating] is the rating value, 0-10. 1 represents 0.5 star.
|
||||||
|
@@ -163,6 +163,13 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
|
|||||||
return "${App.locale.languageCode}_${App.locale.countryCode}";
|
return "${App.locale.languageCode}_${App.locale.countryCode}";
|
||||||
case "getPlatform":
|
case "getPlatform":
|
||||||
return Platform.operatingSystem;
|
return Platform.operatingSystem;
|
||||||
|
case "setClipboard":
|
||||||
|
return Clipboard.setData(ClipboardData(text: message["text"]));
|
||||||
|
case "getClipboard":
|
||||||
|
return Future.sync(() async {
|
||||||
|
var res = await Clipboard.getData(Clipboard.kTextPlain);
|
||||||
|
return res?.text;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@@ -461,6 +461,10 @@ class LocalManager with ChangeNotifier {
|
|||||||
if (comic != null) {
|
if (comic != null) {
|
||||||
return Directory(FilePath.join(path, comic.directory));
|
return Directory(FilePath.join(path, comic.directory));
|
||||||
}
|
}
|
||||||
|
const comicDirectoryMaxLength = 128;
|
||||||
|
if (name.length > comicDirectoryMaxLength) {
|
||||||
|
name = name.substring(0, comicDirectoryMaxLength);
|
||||||
|
}
|
||||||
var dir = findValidDirectoryName(path, name);
|
var dir = findValidDirectoryName(path, name);
|
||||||
return Directory(FilePath.join(path, dir)).create().then((value) => value);
|
return Directory(FilePath.join(path, dir)).create().then((value) => value);
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
|
import 'package:venera/utils/io.dart';
|
||||||
|
|
||||||
class LogItem {
|
class LogItem {
|
||||||
final LogLevel level;
|
final LogLevel level;
|
||||||
@@ -28,9 +28,6 @@ class Log {
|
|||||||
|
|
||||||
static bool ignoreLimitation = false;
|
static bool ignoreLimitation = false;
|
||||||
|
|
||||||
/// only for debug
|
|
||||||
static const String? logFile = null;
|
|
||||||
|
|
||||||
static void printWarning(String text) {
|
static void printWarning(String text) {
|
||||||
debugPrint('\x1B[33m$text\x1B[0m');
|
debugPrint('\x1B[33m$text\x1B[0m');
|
||||||
}
|
}
|
||||||
@@ -39,7 +36,20 @@ class Log {
|
|||||||
debugPrint('\x1B[31m$text\x1B[0m');
|
debugPrint('\x1B[31m$text\x1B[0m');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static IOSink? _file;
|
||||||
|
|
||||||
static void addLog(LogLevel level, String title, String content) {
|
static void addLog(LogLevel level, String title, String content) {
|
||||||
|
if (_file == null) {
|
||||||
|
Directory dir;
|
||||||
|
if (App.isAndroid) {
|
||||||
|
dir = Directory(App.externalStoragePath!);
|
||||||
|
} else {
|
||||||
|
dir = Directory(App.dataPath);
|
||||||
|
}
|
||||||
|
var file = dir.joinFile("logs.txt");
|
||||||
|
_file = file.openWrite();
|
||||||
|
}
|
||||||
|
|
||||||
if (!ignoreLimitation && content.length > maxLogLength) {
|
if (!ignoreLimitation && content.length > maxLogLength) {
|
||||||
content = "${content.substring(0, maxLogLength)}...";
|
content = "${content.substring(0, maxLogLength)}...";
|
||||||
}
|
}
|
||||||
@@ -62,8 +72,8 @@ class Log {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_logs.add(newLog);
|
_logs.add(newLog);
|
||||||
if(logFile != null) {
|
if(_file != null) {
|
||||||
File(logFile!).writeAsString(newLog.toString(), mode: FileMode.append);
|
_file!.write(newLog.toString());
|
||||||
}
|
}
|
||||||
if (_logs.length > maxLogNumber) {
|
if (_logs.length > maxLogNumber) {
|
||||||
var res = _logs.remove(
|
var res = _logs.remove(
|
||||||
|
@@ -35,8 +35,14 @@ void main(List<String> args) {
|
|||||||
}
|
}
|
||||||
await windowManager.setMinimumSize(const Size(500, 600));
|
await windowManager.setMinimumSize(const Size(500, 600));
|
||||||
var placement = await WindowPlacement.loadFromFile();
|
var placement = await WindowPlacement.loadFromFile();
|
||||||
|
if (App.isLinux) {
|
||||||
|
await windowManager.show();
|
||||||
|
await placement.applyToWindow();
|
||||||
|
} else {
|
||||||
await placement.applyToWindow();
|
await placement.applyToWindow();
|
||||||
await windowManager.show();
|
await windowManager.show();
|
||||||
|
}
|
||||||
|
|
||||||
WindowPlacement.loop();
|
WindowPlacement.loop();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -482,7 +482,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
chapters: comic!.chapters,
|
chapters: comic!.chapters,
|
||||||
cover: File(_cover!.split("file://").last).name,
|
cover: File(_cover!.split("file://").last).name,
|
||||||
comicType: ComicType(source.key.hashCode),
|
comicType: ComicType(source.key.hashCode),
|
||||||
downloadedChapters: chapters ?? [],
|
downloadedChapters: chapters ?? comic?.chapters?.ids.toList() ?? [],
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -4,12 +4,10 @@ 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/pages/ranking_page.dart';
|
import 'package:venera/pages/ranking_page.dart';
|
||||||
import 'package:venera/pages/search_result_page.dart';
|
|
||||||
import 'package:venera/pages/settings/settings_page.dart';
|
import 'package:venera/pages/settings/settings_page.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
import 'category_comics_page.dart';
|
|
||||||
import 'comic_source_page.dart';
|
import 'comic_source_page.dart';
|
||||||
|
|
||||||
class CategoriesPage extends StatefulWidget {
|
class CategoriesPage extends StatefulWidget {
|
||||||
@@ -147,43 +145,6 @@ class _CategoryPage extends StatelessWidget {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleClick(
|
|
||||||
String tag,
|
|
||||||
String? param,
|
|
||||||
String type,
|
|
||||||
String namespace,
|
|
||||||
String categoryKey,
|
|
||||||
) {
|
|
||||||
if (type == 'search') {
|
|
||||||
App.mainNavigatorKey?.currentContext?.to(
|
|
||||||
() => SearchResultPage(
|
|
||||||
text: tag,
|
|
||||||
options: const [],
|
|
||||||
sourceKey: findComicSourceKey(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if (type == "search_with_namespace") {
|
|
||||||
if (tag.contains(" ")) {
|
|
||||||
tag = '"$tag"';
|
|
||||||
}
|
|
||||||
App.mainNavigatorKey?.currentContext?.to(
|
|
||||||
() => SearchResultPage(
|
|
||||||
text: "$namespace:$tag",
|
|
||||||
options: const [],
|
|
||||||
sourceKey: findComicSourceKey(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if (type == "category") {
|
|
||||||
App.mainNavigatorKey!.currentContext!.to(
|
|
||||||
() => CategoryComicsPage(
|
|
||||||
category: tag,
|
|
||||||
categoryKey: categoryKey,
|
|
||||||
param: param,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var children = <Widget>[];
|
var children = <Widget>[];
|
||||||
@@ -194,11 +155,11 @@ class _CategoryPage extends StatelessWidget {
|
|||||||
child: Wrap(
|
child: Wrap(
|
||||||
children: [
|
children: [
|
||||||
if (data.enableRankingPage)
|
if (data.enableRankingPage)
|
||||||
buildTag("Ranking".tl, (p0, p1) {
|
buildTag("Ranking".tl, () {
|
||||||
context.to(() => RankingPage(categoryKey: data.key));
|
context.to(() => RankingPage(categoryKey: data.key));
|
||||||
}),
|
}),
|
||||||
for (var buttonData in data.buttons)
|
for (var buttonData in data.buttons)
|
||||||
buildTag(buttonData.label.tl, (p0, p1) => buttonData.onTap())
|
buildTag(buttonData.label.tl, buttonData.onTap)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
@@ -212,36 +173,14 @@ class _CategoryPage extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
buildTitleWithRefresh(part.title, () => updater(() {})),
|
buildTitleWithRefresh(part.title, () => updater(() {})),
|
||||||
buildTagsWithParams(
|
buildTags(part.categories)
|
||||||
part.categories,
|
|
||||||
part.categoryParams,
|
|
||||||
part.title,
|
|
||||||
(key, param) => handleClick(
|
|
||||||
key,
|
|
||||||
param,
|
|
||||||
part.categoryType,
|
|
||||||
part.title,
|
|
||||||
category,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
children.add(buildTitle(part.title));
|
children.add(buildTitle(part.title));
|
||||||
children.add(
|
children.add(
|
||||||
buildTagsWithParams(
|
buildTags(part.categories),
|
||||||
part.categories,
|
|
||||||
part.categoryParams,
|
|
||||||
part.title,
|
|
||||||
(tag, param) => handleClick(
|
|
||||||
tag,
|
|
||||||
param,
|
|
||||||
part.categoryType,
|
|
||||||
part.title,
|
|
||||||
data.key,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -280,30 +219,28 @@ class _CategoryPage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildTagsWithParams(
|
Widget buildTags(
|
||||||
List<String> tags,
|
List<CategoryItem> categories,
|
||||||
List<String>? params,
|
|
||||||
String? namespace,
|
|
||||||
ClickTagCallback onClick,
|
|
||||||
) {
|
) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(10, 0, 10, 16),
|
padding: const EdgeInsets.fromLTRB(10, 0, 10, 16),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
children: List<Widget>.generate(
|
children: List<Widget>.generate(
|
||||||
tags.length,
|
categories.length,
|
||||||
(index) => buildTag(
|
(index) => buildCategory(categories[index]),
|
||||||
tags[index],
|
|
||||||
onClick,
|
|
||||||
namespace,
|
|
||||||
params?.elementAtOrNull(index),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildTag(String tag, ClickTagCallback onClick,
|
Widget buildCategory(CategoryItem c) {
|
||||||
[String? namespace, String? param]) {
|
return buildTag(c.label, () {
|
||||||
|
var context = App.mainNavigatorKey!.currentContext!;
|
||||||
|
c.target.jump(context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildTag(String label, VoidCallback onClick) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(8, 6, 8, 6),
|
padding: const EdgeInsets.fromLTRB(8, 6, 8, 6),
|
||||||
child: Builder(
|
child: Builder(
|
||||||
@@ -313,10 +250,10 @@ class _CategoryPage extends StatelessWidget {
|
|||||||
color: context.colorScheme.primaryContainer.toOpacity(0.72),
|
color: context.colorScheme.primaryContainer.toOpacity(0.72),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
onTap: () => onClick(tag, param),
|
onTap: onClick,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||||
child: Text(tag),
|
child: Text(label),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@@ -9,6 +9,7 @@ class CategoryComicsPage extends StatefulWidget {
|
|||||||
required this.category,
|
required this.category,
|
||||||
this.param,
|
this.param,
|
||||||
required this.categoryKey,
|
required this.categoryKey,
|
||||||
|
this.options,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -18,6 +19,8 @@ class CategoryComicsPage extends StatefulWidget {
|
|||||||
|
|
||||||
final String categoryKey;
|
final String categoryKey;
|
||||||
|
|
||||||
|
final List<String>? options;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CategoryComicsPage> createState() => _CategoryComicsPageState();
|
State<CategoryComicsPage> createState() => _CategoryComicsPageState();
|
||||||
}
|
}
|
||||||
@@ -31,6 +34,9 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
|||||||
void findData() {
|
void findData() {
|
||||||
for (final source in ComicSource.all()) {
|
for (final source in ComicSource.all()) {
|
||||||
if (source.categoryData?.key == widget.categoryKey) {
|
if (source.categoryData?.key == widget.categoryKey) {
|
||||||
|
if (source.categoryComicsData == null) {
|
||||||
|
throw "The comic source ${source.name} does not support category comics";
|
||||||
|
}
|
||||||
data = source.categoryComicsData!;
|
data = source.categoryComicsData!;
|
||||||
options = data.options.where((element) {
|
options = data.options.where((element) {
|
||||||
if (element.notShowWhen.contains(widget.category)) {
|
if (element.notShowWhen.contains(widget.category)) {
|
||||||
@@ -40,7 +46,16 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}).toList();
|
}).toList();
|
||||||
optionsValue = options.map((e) => e.options.keys.first).toList();
|
var defaultOptionsValue =
|
||||||
|
options.map((e) => e.options.keys.first).toList();
|
||||||
|
if (optionsValue.length != options.length) {
|
||||||
|
var newOptionsValue = List<String>.filled(options.length, "");
|
||||||
|
for (var i = 0; i < options.length; i++) {
|
||||||
|
newOptionsValue[i] =
|
||||||
|
optionsValue.elementAtOrNull(i) ?? defaultOptionsValue[i];
|
||||||
|
}
|
||||||
|
optionsValue = newOptionsValue;
|
||||||
|
}
|
||||||
sourceKey = source.key;
|
sourceKey = source.key;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -50,6 +65,11 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
if (widget.options != null) {
|
||||||
|
optionsValue = widget.options!;
|
||||||
|
} else {
|
||||||
|
optionsValue = [];
|
||||||
|
}
|
||||||
findData();
|
findData();
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
@@ -294,27 +294,9 @@ abstract mixin class _ComicPageActions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onTapTag(String tag, String namespace) {
|
void onTapTag(String tag, String namespace) {
|
||||||
var config = comicSource.handleClickTagEvent?.call(namespace, tag) ??
|
var target = comicSource.handleClickTagEvent?.call(namespace, tag);
|
||||||
{
|
|
||||||
'action': 'search',
|
|
||||||
'keyword': tag,
|
|
||||||
};
|
|
||||||
var context = App.mainNavigatorKey!.currentContext!;
|
var context = App.mainNavigatorKey!.currentContext!;
|
||||||
if (config['action'] == 'search') {
|
target?.jump(context);
|
||||||
context.to(() => SearchResultPage(
|
|
||||||
text: config['keyword'] ?? '',
|
|
||||||
sourceKey: comicSource.key,
|
|
||||||
options: const [],
|
|
||||||
));
|
|
||||||
} else if (config['action'] == 'category') {
|
|
||||||
context.to(
|
|
||||||
() => CategoryComicsPage(
|
|
||||||
category: config['keyword'] ?? '',
|
|
||||||
categoryKey: comicSource.categoryData!.key,
|
|
||||||
param: config['param'],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void showMoreActions() {
|
void showMoreActions() {
|
||||||
|
@@ -105,7 +105,7 @@ class _NormalComicChaptersState extends State<_NormalComicChapters> {
|
|||||||
var value = chapters[key]!;
|
var value = chapters[key]!;
|
||||||
bool visited = (history?.readEpisode ?? {}).contains(i + 1);
|
bool visited = (history?.readEpisode ?? {}).contains(i + 1);
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(6, 4, 6, 4),
|
padding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
|
||||||
child: Material(
|
child: Material(
|
||||||
color: context.colorScheme.surfaceContainer,
|
color: context.colorScheme.surfaceContainer,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
@@ -113,7 +113,7 @@ class _NormalComicChaptersState extends State<_NormalComicChapters> {
|
|||||||
onTap: () => state.read(i + 1),
|
onTap: () => state.read(i + 1),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
value,
|
value,
|
||||||
@@ -134,7 +134,7 @@ class _NormalComicChaptersState extends State<_NormalComicChapters> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
gridDelegate: const SliverGridDelegateWithFixedHeight(
|
gridDelegate: const SliverGridDelegateWithFixedHeight(
|
||||||
maxCrossAxisExtent: 200,
|
maxCrossAxisExtent: 250,
|
||||||
itemHeight: 48,
|
itemHeight: 48,
|
||||||
),
|
),
|
||||||
).sliverPadding(const EdgeInsets.symmetric(horizontal: 8)),
|
).sliverPadding(const EdgeInsets.symmetric(horizontal: 8)),
|
||||||
@@ -300,15 +300,15 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters>
|
|||||||
history!.readEpisode.contains(rawIndex);
|
history!.readEpisode.contains(rawIndex);
|
||||||
}
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(6, 4, 6, 4),
|
padding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
|
||||||
child: Material(
|
child: Material(
|
||||||
color: context.colorScheme.surfaceContainer,
|
color: context.colorScheme.surfaceContainerLow,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => state.read(chapterIndex + 1),
|
onTap: () => state.read(chapterIndex + 1),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
value,
|
value,
|
||||||
@@ -329,7 +329,7 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters>
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
gridDelegate: const SliverGridDelegateWithFixedHeight(
|
gridDelegate: const SliverGridDelegateWithFixedHeight(
|
||||||
maxCrossAxisExtent: 200,
|
maxCrossAxisExtent: 250,
|
||||||
itemHeight: 48,
|
itemHeight: 48,
|
||||||
),
|
),
|
||||||
).sliverPadding(const EdgeInsets.symmetric(horizontal: 8)),
|
).sliverPadding(const EdgeInsets.symmetric(horizontal: 8)),
|
||||||
|
@@ -17,10 +17,8 @@ import 'package:venera/foundation/image_provider/cached_image.dart';
|
|||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
import 'package:venera/foundation/res.dart';
|
import 'package:venera/foundation/res.dart';
|
||||||
import 'package:venera/network/download.dart';
|
import 'package:venera/network/download.dart';
|
||||||
import 'package:venera/pages/category_comics_page.dart';
|
|
||||||
import 'package:venera/pages/favorites/favorites_page.dart';
|
import 'package:venera/pages/favorites/favorites_page.dart';
|
||||||
import 'package:venera/pages/reader/reader.dart';
|
import 'package:venera/pages/reader/reader.dart';
|
||||||
import 'package:venera/pages/search_result_page.dart';
|
|
||||||
import 'package:venera/utils/app_links.dart';
|
import 'package:venera/utils/app_links.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
@@ -75,6 +73,8 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
|
|
||||||
bool isDownloaded = false;
|
bool isDownloaded = false;
|
||||||
|
|
||||||
|
bool showFAB = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onReadEnd() {
|
void onReadEnd() {
|
||||||
history ??=
|
history ??=
|
||||||
@@ -114,7 +114,15 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
ComicDetails get comic => data!;
|
ComicDetails get comic => data!;
|
||||||
|
|
||||||
void onScroll() {
|
void onScroll() {
|
||||||
if (scrollController.offset > 100) {
|
var offset = scrollController.position.pixels -
|
||||||
|
scrollController.position.minScrollExtent;
|
||||||
|
var showFAB = offset > 0;
|
||||||
|
if (showFAB != this.showFAB) {
|
||||||
|
setState(() {
|
||||||
|
this.showFAB = showFAB;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (offset > 100) {
|
||||||
if (!showAppbarTitle) {
|
if (!showAppbarTitle) {
|
||||||
setState(() {
|
setState(() {
|
||||||
showAppbarTitle = true;
|
showAppbarTitle = true;
|
||||||
@@ -133,7 +141,18 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildContent(BuildContext context, ComicDetails data) {
|
Widget buildContent(BuildContext context, ComicDetails data) {
|
||||||
return SmoothCustomScrollView(
|
return Scaffold(
|
||||||
|
floatingActionButton: showFAB
|
||||||
|
? FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
scrollController.animateTo(0,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.ease);
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.arrow_upward),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
body: SmoothCustomScrollView(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
...buildTitle(),
|
...buildTitle(),
|
||||||
@@ -144,8 +163,11 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
buildComments(),
|
buildComments(),
|
||||||
buildThumbnails(),
|
buildThumbnails(),
|
||||||
buildRecommend(),
|
buildRecommend(),
|
||||||
SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom)),
|
SliverPadding(
|
||||||
|
padding: EdgeInsets.only(bottom: context.padding.bottom + 80), // Add additional padding for FAB
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,7 +409,9 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
var group = history!.group;
|
var group = history!.group;
|
||||||
String text;
|
String text;
|
||||||
if (haveChapter) {
|
if (haveChapter) {
|
||||||
var epName = group == null
|
var epName = "E$ep";
|
||||||
|
try {
|
||||||
|
epName = group == null
|
||||||
? comic.chapters!.titles.elementAt(
|
? comic.chapters!.titles.elementAt(
|
||||||
math.min(ep - 1, comic.chapters!.length - 1),
|
math.min(ep - 1, comic.chapters!.length - 1),
|
||||||
)
|
)
|
||||||
@@ -395,6 +419,10 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
.getGroupByIndex(group - 1)
|
.getGroupByIndex(group - 1)
|
||||||
.values
|
.values
|
||||||
.elementAt(ep - 1);
|
.elementAt(ep - 1);
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
text = "${"Last Reading".tl}: $epName P$page";
|
text = "${"Last Reading".tl}: $epName P$page";
|
||||||
} else {
|
} else {
|
||||||
text = "${"Last Reading".tl}: P$page";
|
text = "${"Last Reading".tl}: P$page";
|
||||||
@@ -437,7 +465,8 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
if (comic.tags.isEmpty &&
|
if (comic.tags.isEmpty &&
|
||||||
comic.uploader == null &&
|
comic.uploader == null &&
|
||||||
comic.uploadTime == null &&
|
comic.uploadTime == null &&
|
||||||
comic.uploadTime == null) {
|
comic.uploadTime == null &&
|
||||||
|
comic.maxPage == null) {
|
||||||
return const SliverPadding(padding: EdgeInsets.zero);
|
return const SliverPadding(padding: EdgeInsets.zero);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -601,6 +630,13 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
buildTag(text: formatTime(comic.updateTime!)),
|
buildTag(text: formatTime(comic.updateTime!)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if (comic.maxPage != null)
|
||||||
|
buildWrap(
|
||||||
|
children: [
|
||||||
|
buildTag(text: 'Pages'.tl, isTitle: true),
|
||||||
|
buildTag(text: comic.maxPage.toString()),
|
||||||
|
],
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
],
|
],
|
||||||
|
@@ -99,7 +99,11 @@ class _CommentsPageState extends State<CommentsPage> {
|
|||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: SmoothScrollProvider(
|
||||||
|
builder: (context, controller, physics) {
|
||||||
|
return ListView.builder(
|
||||||
|
controller: controller,
|
||||||
|
physics: physics,
|
||||||
primary: false,
|
primary: false,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
itemCount: _comments!.length + 2,
|
itemCount: _comments!.length + 2,
|
||||||
@@ -156,6 +160,8 @@ class _CommentsPageState extends State<CommentsPage> {
|
|||||||
showAvatar: showAvatar,
|
showAvatar: showAvatar,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
buildBottom(context)
|
buildBottom(context)
|
||||||
|
@@ -374,8 +374,35 @@ class _ComicSourceListState extends State<_ComicSourceList> {
|
|||||||
} else {
|
} else {
|
||||||
var currentKey = ComicSource.all().map((e) => e.key).toList();
|
var currentKey = ComicSource.all().map((e) => e.key).toList();
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: json!.length,
|
itemCount: json!.length + 1,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
if (index == 0) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
color: context.colorScheme.primaryContainer,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.info_outline),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("Do not report any issues related to sources to App repo.".tl),
|
||||||
|
Text("Click the setting icon to change the source list url.".tl),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
index--;
|
||||||
|
|
||||||
var key = json![index]["key"];
|
var key = json![index]["key"];
|
||||||
var action = currentKey.contains(key)
|
var action = currentKey.contains(key)
|
||||||
? const Icon(Icons.check, size: 20).paddingRight(8)
|
? const Icon(Icons.check, size: 20).paddingRight(8)
|
||||||
@@ -403,9 +430,14 @@ class _ComicSourceListState extends State<_ComicSourceList> {
|
|||||||
},
|
},
|
||||||
).fixHeight(32);
|
).fixHeight(32);
|
||||||
|
|
||||||
|
var description = json![index]["version"];
|
||||||
|
if (json![index]["description"] != null) {
|
||||||
|
description = "$description\n${json![index]["description"]}";
|
||||||
|
}
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(json![index]["name"]),
|
title: Text(json![index]["name"]),
|
||||||
subtitle: Text(json![index]["version"]),
|
subtitle: Text(description),
|
||||||
trailing: action,
|
trailing: action,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -461,6 +493,7 @@ void _addAllPagesWithComicSource(ComicSource source) {
|
|||||||
var explorePages = appdata.settings['explore_pages'];
|
var explorePages = appdata.settings['explore_pages'];
|
||||||
var categoryPages = appdata.settings['categories'];
|
var categoryPages = appdata.settings['categories'];
|
||||||
var networkFavorites = appdata.settings['favorites'];
|
var networkFavorites = appdata.settings['favorites'];
|
||||||
|
var searchPages = appdata.settings['searchSources'];
|
||||||
|
|
||||||
if (source.explorePages.isNotEmpty) {
|
if (source.explorePages.isNotEmpty) {
|
||||||
for (var page in source.explorePages) {
|
for (var page in source.explorePages) {
|
||||||
@@ -477,10 +510,15 @@ void _addAllPagesWithComicSource(ComicSource source) {
|
|||||||
!networkFavorites.contains(source.favoriteData!.key)) {
|
!networkFavorites.contains(source.favoriteData!.key)) {
|
||||||
networkFavorites.add(source.favoriteData!.key);
|
networkFavorites.add(source.favoriteData!.key);
|
||||||
}
|
}
|
||||||
|
if (source.searchPageData != null &&
|
||||||
|
!searchPages.contains(source.key)) {
|
||||||
|
searchPages.add(source.key);
|
||||||
|
}
|
||||||
|
|
||||||
appdata.settings['explore_pages'] = explorePages.toSet().toList();
|
appdata.settings['explore_pages'] = explorePages.toSet().toList();
|
||||||
appdata.settings['categories'] = categoryPages.toSet().toList();
|
appdata.settings['categories'] = categoryPages.toSet().toList();
|
||||||
appdata.settings['favorites'] = networkFavorites.toSet().toList();
|
appdata.settings['favorites'] = networkFavorites.toSet().toList();
|
||||||
|
appdata.settings['searchSources'] = searchPages.toSet().toList();
|
||||||
|
|
||||||
appdata.saveData();
|
appdata.saveData();
|
||||||
}
|
}
|
||||||
|
@@ -6,13 +6,10 @@ import 'package:venera/foundation/comic_source/comic_source.dart';
|
|||||||
import 'package:venera/foundation/global_state.dart';
|
import 'package:venera/foundation/global_state.dart';
|
||||||
import 'package:venera/foundation/res.dart';
|
import 'package:venera/foundation/res.dart';
|
||||||
import 'package:venera/pages/comic_source_page.dart';
|
import 'package:venera/pages/comic_source_page.dart';
|
||||||
import 'package:venera/pages/search_result_page.dart';
|
|
||||||
import 'package:venera/pages/settings/settings_page.dart';
|
import 'package:venera/pages/settings/settings_page.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
import 'category_comics_page.dart';
|
|
||||||
|
|
||||||
class ExplorePage extends StatefulWidget {
|
class ExplorePage extends StatefulWidget {
|
||||||
const ExplorePage({super.key});
|
const ExplorePage({super.key});
|
||||||
|
|
||||||
@@ -445,30 +442,7 @@ Iterable<Widget> _buildExplorePagePart(
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
var context = App.mainNavigatorKey!.currentContext!;
|
var context = App.mainNavigatorKey!.currentContext!;
|
||||||
if (part.viewMore!.startsWith("search:")) {
|
part.viewMore!.jump(context);
|
||||||
context.to(
|
|
||||||
() => SearchResultPage(
|
|
||||||
text: part.viewMore!.replaceFirst("search:", ""),
|
|
||||||
options: const [],
|
|
||||||
sourceKey: sourceKey,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if (part.viewMore!.startsWith("category:")) {
|
|
||||||
var cp = part.viewMore!.replaceFirst("category:", "");
|
|
||||||
var c = cp.split('@').first;
|
|
||||||
String? p = cp.split('@').last;
|
|
||||||
if (p == c) {
|
|
||||||
p = null;
|
|
||||||
}
|
|
||||||
context.to(
|
|
||||||
() => CategoryComicsPage(
|
|
||||||
category: c,
|
|
||||||
categoryKey:
|
|
||||||
ComicSource.find(sourceKey)!.categoryData!.key,
|
|
||||||
param: p,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
child: Text("View more".tl),
|
child: Text("View more".tl),
|
||||||
)
|
)
|
||||||
|
@@ -52,7 +52,7 @@ class _SearchBar extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 52,
|
height: App.isMobile ? 52 : 46,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
child: Material(
|
child: Material(
|
||||||
@@ -942,7 +942,7 @@ class _ImageFavoritesState extends State<ImageFavorites> {
|
|||||||
displayType = type;
|
displayType = type;
|
||||||
});
|
});
|
||||||
await Future.delayed(const Duration(milliseconds: 20));
|
await Future.delayed(const Duration(milliseconds: 20));
|
||||||
var scrollController = ScrollControllerProvider.of(context);
|
var scrollController = ScrollState.of(context).controller;
|
||||||
scrollController.animateTo(
|
scrollController.animateTo(
|
||||||
scrollController.position.maxScrollExtent,
|
scrollController.position.maxScrollExtent,
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
|
@@ -306,7 +306,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// prevent dirty data
|
// prevent dirty data
|
||||||
var comic = LocalManager().find(c.id, ComicType(c.sourceKey.hashCode))!;
|
var comic = LocalManager().find(c.id, ComicType.fromKey(c.sourceKey))!;
|
||||||
comic.read();
|
comic.read();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -24,6 +24,8 @@ class ComicImage extends StatefulWidget {
|
|||||||
Map<String, String>? headers,
|
Map<String, String>? headers,
|
||||||
int? cacheWidth,
|
int? cacheWidth,
|
||||||
int? cacheHeight,
|
int? cacheHeight,
|
||||||
|
this.onInit,
|
||||||
|
this.onDispose,
|
||||||
}) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, image),
|
}) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, image),
|
||||||
assert(cacheWidth == null || cacheWidth > 0),
|
assert(cacheWidth == null || cacheWidth > 0),
|
||||||
assert(cacheHeight == null || cacheHeight > 0);
|
assert(cacheHeight == null || cacheHeight > 0);
|
||||||
@@ -60,6 +62,10 @@ class ComicImage extends StatefulWidget {
|
|||||||
|
|
||||||
final bool isAntiAlias;
|
final bool isAntiAlias;
|
||||||
|
|
||||||
|
final void Function(State<ComicImage> state)? onInit;
|
||||||
|
|
||||||
|
final void Function(State<ComicImage> state)? onDispose;
|
||||||
|
|
||||||
static void clear() => _ComicImageState.clear();
|
static void clear() => _ComicImageState.clear();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -87,6 +93,7 @@ class _ComicImageState extends State<ComicImage> with WidgetsBindingObserver {
|
|||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
_scrollAwareContext = DisposableBuildContext<State<ComicImage>>(this);
|
_scrollAwareContext = DisposableBuildContext<State<ComicImage>>(this);
|
||||||
|
widget.onInit?.call(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -97,6 +104,7 @@ class _ComicImageState extends State<ComicImage> with WidgetsBindingObserver {
|
|||||||
_completerHandle?.dispose();
|
_completerHandle?.dispose();
|
||||||
_scrollAwareContext.dispose();
|
_scrollAwareContext.dispose();
|
||||||
_replaceImage(info: null);
|
_replaceImage(info: null);
|
||||||
|
widget.onDispose?.call(this);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +144,15 @@ class _ComicImageState extends State<ComicImage> with WidgetsBindingObserver {
|
|||||||
super.reassemble();
|
super.reassemble();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool containsPoint(Offset point) {
|
||||||
|
if (!mounted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var renderBox = context.findRenderObject() as RenderBox;
|
||||||
|
var localPoint = renderBox.globalToLocal(point);
|
||||||
|
return renderBox.paintBounds.contains(localPoint);
|
||||||
|
}
|
||||||
|
|
||||||
void _updateInvertColors() {
|
void _updateInvertColors() {
|
||||||
_invertColors = MediaQuery.maybeInvertColorsOf(context) ??
|
_invertColors = MediaQuery.maybeInvertColorsOf(context) ??
|
||||||
SemanticsBinding.instance.accessibilityFeatures.invertColors;
|
SemanticsBinding.instance.accessibilityFeatures.invertColors;
|
||||||
|
@@ -281,6 +281,12 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet
|
|||||||
context.pop();
|
context.pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (App.isDesktop && !reader.isLoading)
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.copy,
|
||||||
|
text: "Copy Image".tl,
|
||||||
|
onClick: () => copyImage(location),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -303,6 +309,16 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Object? get key => "reader_gesture";
|
Object? get key => "reader_gesture";
|
||||||
|
|
||||||
|
void copyImage(Offset location) async {
|
||||||
|
var controller = reader._imageViewController;
|
||||||
|
var image = await controller!.getImageByOffset(location);
|
||||||
|
if (image != null) {
|
||||||
|
writeImageToClipboard(image);
|
||||||
|
} else {
|
||||||
|
context.showMessage(message: "No Image");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DragListener {
|
class _DragListener {
|
||||||
|
@@ -25,8 +25,8 @@ class _ReaderImagesState extends State<_ReaderImages> {
|
|||||||
if (inProgress) return;
|
if (inProgress) return;
|
||||||
inProgress = true;
|
inProgress = true;
|
||||||
if (reader.type == ComicType.local ||
|
if (reader.type == ComicType.local ||
|
||||||
(LocalManager()
|
(LocalManager().isDownloaded(
|
||||||
.isDownloaded(reader.cid, reader.type, reader.chapter, reader.widget.chapters))) {
|
reader.cid, reader.type, reader.chapter, reader.widget.chapters))) {
|
||||||
try {
|
try {
|
||||||
var images = await LocalManager()
|
var images = await LocalManager()
|
||||||
.getImages(reader.cid, reader.type, reader.chapter);
|
.getImages(reader.cid, reader.type, reader.chapter);
|
||||||
@@ -43,9 +43,10 @@ class _ReaderImagesState extends State<_ReaderImages> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
var cp = reader.widget.chapters?.ids.elementAtOrNull(reader.chapter - 1);
|
||||||
var res = await reader.type.comicSource!.loadComicPages!(
|
var res = await reader.type.comicSource!.loadComicPages!(
|
||||||
reader.widget.cid,
|
reader.widget.cid,
|
||||||
reader.widget.chapters?.ids.elementAt(reader.chapter - 1),
|
cp,
|
||||||
);
|
);
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -113,6 +114,12 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
|
|
||||||
int get totalPages => (reader.images!.length / reader.imagesPerPage).ceil();
|
int get totalPages => (reader.images!.length / reader.imagesPerPage).ceil();
|
||||||
|
|
||||||
|
var imageStates = <State<ComicImage>>{};
|
||||||
|
|
||||||
|
bool isLongPressing = false;
|
||||||
|
|
||||||
|
int fingers = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
reader = context.reader;
|
reader = context.reader;
|
||||||
@@ -142,7 +149,28 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PhotoViewGallery.builder(
|
return Listener(
|
||||||
|
onPointerDown: (event) {
|
||||||
|
fingers++;
|
||||||
|
},
|
||||||
|
onPointerUp: (event) {
|
||||||
|
fingers--;
|
||||||
|
},
|
||||||
|
onPointerCancel: (event) {
|
||||||
|
fingers--;
|
||||||
|
},
|
||||||
|
onPointerMove: (event) {
|
||||||
|
if (isLongPressing) {
|
||||||
|
var controller = photoViewControllers[reader.page]!;
|
||||||
|
Offset value = event.delta;
|
||||||
|
if (isLongPressing) {
|
||||||
|
controller.updateMultiple(
|
||||||
|
position: controller.position + value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: PhotoViewGallery.builder(
|
||||||
backgroundDecoration: BoxDecoration(
|
backgroundDecoration: BoxDecoration(
|
||||||
color: context.colorScheme.surface,
|
color: context.colorScheme.surface,
|
||||||
),
|
),
|
||||||
@@ -217,6 +245,7 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
context.readerScaffold.update();
|
context.readerScaffold.update();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,20 +255,54 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
: Axis.horizontal;
|
: Axis.horizontal;
|
||||||
|
|
||||||
bool reverse = reader.mode == ReaderMode.galleryRightToLeft;
|
bool reverse = reader.mode == ReaderMode.galleryRightToLeft;
|
||||||
|
if (reverse) {
|
||||||
|
images = images.reversed.toList();
|
||||||
|
}
|
||||||
|
|
||||||
List<Widget> imageWidgets = images.map((imageKey) {
|
List<Widget> imageWidgets;
|
||||||
|
|
||||||
|
if (images.length == 2) {
|
||||||
|
imageWidgets = [
|
||||||
|
Expanded(
|
||||||
|
child: ComicImage(
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
image: _createImageProviderFromKey(images[0], context),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
alignment: axis == Axis.vertical
|
||||||
|
? Alignment.bottomCenter
|
||||||
|
: Alignment.centerRight,
|
||||||
|
onInit: (state) => imageStates.add(state),
|
||||||
|
onDispose: (state) => imageStates.remove(state),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ComicImage(
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
image: _createImageProviderFromKey(images[1], context),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
alignment: axis == Axis.vertical
|
||||||
|
? Alignment.topCenter
|
||||||
|
: Alignment.centerLeft,
|
||||||
|
onInit: (state) => imageStates.add(state),
|
||||||
|
onDispose: (state) => imageStates.remove(state),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
imageWidgets = images.map((imageKey) {
|
||||||
ImageProvider imageProvider =
|
ImageProvider imageProvider =
|
||||||
_createImageProviderFromKey(imageKey, context);
|
_createImageProviderFromKey(imageKey, context);
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: ComicImage(
|
child: ComicImage(
|
||||||
image: imageProvider,
|
image: imageProvider,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
|
onInit: (state) => imageStates.add(state),
|
||||||
|
onDispose: (state) => imageStates.remove(state),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
if (reverse) {
|
|
||||||
imageWidgets = imageWidgets.reversed.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return axis == Axis.vertical
|
return axis == Axis.vertical
|
||||||
@@ -276,28 +339,41 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void handleLongPressDown(Offset location) {
|
void handleLongPressDown(Offset location) {
|
||||||
if (!appdata.settings['enableLongPressToZoom']) {
|
if (!appdata.settings['enableLongPressToZoom'] || fingers != 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var photoViewController = photoViewControllers[reader.page]!;
|
var photoViewController = photoViewControllers[reader.page]!;
|
||||||
double target = photoViewController.getInitialScale!.call()! * 1.75;
|
double target = photoViewController.getInitialScale!.call()! * 1.75;
|
||||||
var size = MediaQuery.of(context).size;
|
var size = reader.size;
|
||||||
|
Offset zoomPosition;
|
||||||
|
if (appdata.settings['longPressZoomPosition'] != 'center') {
|
||||||
|
zoomPosition = Offset(
|
||||||
|
size.width / 2 - location.dx,
|
||||||
|
size.height / 2 - location.dy,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
zoomPosition = Offset(0, 0);
|
||||||
|
}
|
||||||
photoViewController.animateScale?.call(
|
photoViewController.animateScale?.call(
|
||||||
target,
|
target,
|
||||||
Offset(size.width / 2 - location.dx, size.height / 2 - location.dy),
|
zoomPosition,
|
||||||
);
|
);
|
||||||
|
isLongPressing = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void handleLongPressUp(Offset location) {
|
void handleLongPressUp(Offset location) {
|
||||||
if (!appdata.settings['enableLongPressToZoom']) {
|
if (!appdata.settings['enableLongPressToZoom'] || !isLongPressing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var photoViewController = photoViewControllers[reader.page]!;
|
var photoViewController = photoViewControllers[reader.page]!;
|
||||||
double target = photoViewController.getInitialScale!.call()!;
|
double target = photoViewController.getInitialScale!.call()!;
|
||||||
photoViewController.animateScale?.call(target);
|
photoViewController.animateScale?.call(target);
|
||||||
|
isLongPressing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer? keyRepeatTimer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void handleKeyEvent(KeyEvent event) {
|
void handleKeyEvent(KeyEvent event) {
|
||||||
bool? forward;
|
bool? forward;
|
||||||
@@ -320,7 +396,11 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
||||||
forward = false;
|
forward = false;
|
||||||
}
|
}
|
||||||
if (event is KeyDownEvent || event is KeyRepeatEvent) {
|
if (event is KeyDownEvent) {
|
||||||
|
if (keyRepeatTimer != null) {
|
||||||
|
keyRepeatTimer!.cancel();
|
||||||
|
keyRepeatTimer = null;
|
||||||
|
}
|
||||||
if (forward == true) {
|
if (forward == true) {
|
||||||
controller.nextPage(
|
controller.nextPage(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
@@ -333,12 +413,59 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (event is KeyRepeatEvent && keyRepeatTimer == null) {
|
||||||
|
keyRepeatTimer = Timer.periodic(
|
||||||
|
const Duration(milliseconds: 100),
|
||||||
|
(timer) {
|
||||||
|
if (!mounted) {
|
||||||
|
timer.cancel();
|
||||||
|
return;
|
||||||
|
} else if (forward == true) {
|
||||||
|
controller.nextPage(
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
curve: Curves.ease,
|
||||||
|
);
|
||||||
|
} else if (forward == false) {
|
||||||
|
controller.previousPage(
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
curve: Curves.ease,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (event is KeyUpEvent && keyRepeatTimer != null) {
|
||||||
|
keyRepeatTimer!.cancel();
|
||||||
|
keyRepeatTimer = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool handleOnTap(Offset location) {
|
bool handleOnTap(Offset location) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Uint8List?> getImageByOffset(Offset offset) async {
|
||||||
|
String? imageKey;
|
||||||
|
if (reader.imagesPerPage == 1) {
|
||||||
|
imageKey = reader.images![reader.page - 1];
|
||||||
|
} else {
|
||||||
|
for (var imageState in imageStates) {
|
||||||
|
if ((imageState as _ComicImageState).containsPoint(offset)) {
|
||||||
|
imageKey = (imageState.widget.image as ReaderImageProvider).imageKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (imageKey == null) return null;
|
||||||
|
if (imageKey.startsWith("file://")) {
|
||||||
|
return await File(imageKey.substring(7)).readAsBytes();
|
||||||
|
} else {
|
||||||
|
return (await CacheManager().findCache(
|
||||||
|
"$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))!
|
||||||
|
.readAsBytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
|
const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
|
||||||
@@ -383,6 +510,8 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
/// To handle the tap event, we need to know if the user was scrolling before the delay.
|
/// To handle the tap event, we need to know if the user was scrolling before the delay.
|
||||||
bool delayedIsScrolling = false;
|
bool delayedIsScrolling = false;
|
||||||
|
|
||||||
|
var imageStates = <State<ComicImage>>{};
|
||||||
|
|
||||||
void delayedSetIsScrolling(bool value) {
|
void delayedSetIsScrolling(bool value) {
|
||||||
Future.delayed(
|
Future.delayed(
|
||||||
const Duration(milliseconds: 300),
|
const Duration(milliseconds: 300),
|
||||||
@@ -395,6 +524,9 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
bool jumpToNextChapter = false;
|
bool jumpToNextChapter = false;
|
||||||
bool jumpToPrevChapter = false;
|
bool jumpToPrevChapter = false;
|
||||||
|
|
||||||
|
bool isZoomedIn = false;
|
||||||
|
bool isLongPressing = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
reader = context.reader;
|
reader = context.reader;
|
||||||
@@ -485,6 +617,23 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool onScaleUpdate([double? scale]) {
|
||||||
|
if (prepareToNextChapter || prepareToPrevChapter) {
|
||||||
|
setState(() {
|
||||||
|
prepareToPrevChapter = false;
|
||||||
|
prepareToNextChapter = false;
|
||||||
|
});
|
||||||
|
context.readerScaffold.setFloatingButton(0);
|
||||||
|
}
|
||||||
|
var isZoomedIn = (scale ?? photoViewController.scale) != 1.0;
|
||||||
|
if (isZoomedIn != this.isZoomedIn) {
|
||||||
|
setState(() {
|
||||||
|
this.isZoomedIn = isZoomedIn;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget widget = ScrollablePositionedList.builder(
|
Widget widget = ScrollablePositionedList.builder(
|
||||||
@@ -506,6 +655,8 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
reverse: reader.mode == ReaderMode.continuousRightToLeft,
|
reverse: reader.mode == ReaderMode.continuousRightToLeft,
|
||||||
physics: isCTRLPressed || _isMouseScrolling || disableScroll
|
physics: isCTRLPressed || _isMouseScrolling || disableScroll
|
||||||
? const NeverScrollableScrollPhysics()
|
? const NeverScrollableScrollPhysics()
|
||||||
|
: isZoomedIn
|
||||||
|
? const ClampingScrollPhysics()
|
||||||
: const BouncingScrollPhysics(),
|
: const BouncingScrollPhysics(),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index == 0 || index == reader.maxPage + 1) {
|
if (index == 0 || index == reader.maxPage + 1) {
|
||||||
@@ -529,6 +680,8 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
|
onInit: (state) => imageStates.add(state),
|
||||||
|
onDispose: (state) => imageStates.remove(state),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -593,18 +746,24 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
if (photoViewController.scale == 1 || fingers != 1) {
|
if (photoViewController.scale == 1 || fingers != 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (scrollController.offset !=
|
Offset offset;
|
||||||
scrollController.position.maxScrollExtent &&
|
var sp = scrollController.position;
|
||||||
scrollController.offset !=
|
if (sp.pixels <= sp.minScrollExtent ||
|
||||||
scrollController.position.minScrollExtent) {
|
sp.pixels >= sp.maxScrollExtent) {
|
||||||
if (reader.mode == ReaderMode.continuousTopToBottom) {
|
offset = Offset(value.dx, value.dy);
|
||||||
value = Offset(value.dx, 0);
|
|
||||||
} else {
|
} else {
|
||||||
value = Offset(0, value.dy);
|
if (reader.mode == ReaderMode.continuousTopToBottom) {
|
||||||
|
offset = Offset(value.dx, 0);
|
||||||
|
} else {
|
||||||
|
offset = Offset(0, value.dy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isLongPressing) {
|
||||||
|
offset += value;
|
||||||
|
}
|
||||||
photoViewController.updateMultiple(
|
photoViewController.updateMultiple(
|
||||||
position: photoViewController.position + value);
|
position: photoViewController.position + offset,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onPointerSignal: onPointerSignal,
|
onPointerSignal: onPointerSignal,
|
||||||
child: widget,
|
child: widget,
|
||||||
@@ -618,7 +777,10 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
delayedSetIsScrolling(false);
|
delayedSetIsScrolling(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notification is ScrollUpdateNotification) {
|
var scale = photoViewController.scale ?? 1.0;
|
||||||
|
|
||||||
|
if (notification is ScrollUpdateNotification &&
|
||||||
|
(scale - 1).abs() < 0.05) {
|
||||||
if (!scrollController.hasClients) return false;
|
if (!scrollController.hasClients) return false;
|
||||||
if (scrollController.position.pixels <=
|
if (scrollController.position.pixels <=
|
||||||
scrollController.position.minScrollExtent &&
|
scrollController.position.minScrollExtent &&
|
||||||
@@ -659,8 +821,8 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
},
|
},
|
||||||
child: widget,
|
child: widget,
|
||||||
);
|
);
|
||||||
var width = MediaQuery.of(context).size.width;
|
var width = reader.size.width;
|
||||||
var height = MediaQuery.of(context).size.height;
|
var height = reader.size.height;
|
||||||
if (appdata.settings['limitImageWidth'] &&
|
if (appdata.settings['limitImageWidth'] &&
|
||||||
width / height > 0.7 &&
|
width / height > 0.7 &&
|
||||||
reader.mode == ReaderMode.continuousTopToBottom) {
|
reader.mode == ReaderMode.continuousTopToBottom) {
|
||||||
@@ -676,6 +838,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
maxScale: 2.5,
|
maxScale: 2.5,
|
||||||
strictScale: true,
|
strictScale: true,
|
||||||
controller: photoViewController,
|
controller: photoViewController,
|
||||||
|
onScaleUpdate: onScaleUpdate,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
@@ -731,6 +894,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
target,
|
target,
|
||||||
Offset(size.width / 2 - location.dx, size.height / 2 - location.dy),
|
Offset(size.width / 2 - location.dx, size.height / 2 - location.dy),
|
||||||
);
|
);
|
||||||
|
onScaleUpdate(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -739,11 +903,22 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
double target = photoViewController.getInitialScale!.call()! * 1.75;
|
double target = photoViewController.getInitialScale!.call()! * 1.75;
|
||||||
var size = MediaQuery.of(context).size;
|
var size = reader.size;
|
||||||
|
Offset zoomPosition;
|
||||||
|
if (appdata.settings['longPressZoomPosition'] != 'center') {
|
||||||
|
zoomPosition = Offset(
|
||||||
|
size.width / 2 - location.dx,
|
||||||
|
size.height / 2 - location.dy,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
zoomPosition = Offset(0, 0);
|
||||||
|
}
|
||||||
photoViewController.animateScale?.call(
|
photoViewController.animateScale?.call(
|
||||||
target,
|
target,
|
||||||
Offset(size.width / 2 - location.dx, size.height / 2 - location.dy),
|
zoomPosition,
|
||||||
);
|
);
|
||||||
|
onScaleUpdate(target);
|
||||||
|
isLongPressing = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -753,6 +928,8 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
}
|
}
|
||||||
double target = photoViewController.getInitialScale!.call()!;
|
double target = photoViewController.getInitialScale!.call()!;
|
||||||
photoViewController.animateScale?.call(target);
|
photoViewController.animateScale?.call(target);
|
||||||
|
onScaleUpdate(target);
|
||||||
|
isLongPressing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -818,6 +995,24 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Uint8List?> getImageByOffset(Offset offset) async {
|
||||||
|
String? imageKey;
|
||||||
|
for (var imageState in imageStates) {
|
||||||
|
if ((imageState as _ComicImageState).containsPoint(offset)) {
|
||||||
|
imageKey = (imageState.widget.image as ReaderImageProvider).imageKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (imageKey == null) return null;
|
||||||
|
if (imageKey.startsWith("file://")) {
|
||||||
|
return await File(imageKey.substring(7)).readAsBytes();
|
||||||
|
} else {
|
||||||
|
return (await CacheManager().findCache(
|
||||||
|
"$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))!
|
||||||
|
.readAsBytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageProvider _createImageProviderFromKey(
|
ImageProvider _createImageProviderFromKey(
|
||||||
|
@@ -30,6 +30,7 @@ import 'package:venera/foundation/local.dart';
|
|||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
import 'package:venera/foundation/res.dart';
|
import 'package:venera/foundation/res.dart';
|
||||||
import 'package:venera/pages/settings/settings_page.dart';
|
import 'package:venera/pages/settings/settings_page.dart';
|
||||||
|
import 'package:venera/utils/clipboard_image.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/file_type.dart';
|
import 'package:venera/utils/file_type.dart';
|
||||||
@@ -308,6 +309,13 @@ class _ReaderState extends State<Reader>
|
|||||||
}
|
}
|
||||||
return chapter == maxChapter;
|
return chapter == maxChapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the size of the reader.
|
||||||
|
/// The size is not always the same as the size of the screen.
|
||||||
|
Size get size {
|
||||||
|
var renderBox = context.findRenderObject() as RenderBox;
|
||||||
|
return renderBox.size;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract mixin class _ImagePerPageHandler {
|
abstract mixin class _ImagePerPageHandler {
|
||||||
@@ -362,8 +370,24 @@ abstract mixin class _VolumeListener {
|
|||||||
|
|
||||||
bool toPrevPage();
|
bool toPrevPage();
|
||||||
|
|
||||||
|
bool toNextChapter();
|
||||||
|
|
||||||
|
bool toPrevChapter();
|
||||||
|
|
||||||
VolumeListener? volumeListener;
|
VolumeListener? volumeListener;
|
||||||
|
|
||||||
|
void onDown() {
|
||||||
|
if (!toNextPage()) {
|
||||||
|
toNextChapter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onUp() {
|
||||||
|
if (!toPrevPage()) {
|
||||||
|
toPrevChapter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void handleVolumeEvent() {
|
void handleVolumeEvent() {
|
||||||
if (!App.isAndroid) {
|
if (!App.isAndroid) {
|
||||||
// Currently only support Android
|
// Currently only support Android
|
||||||
@@ -373,8 +397,8 @@ abstract mixin class _VolumeListener {
|
|||||||
volumeListener?.cancel();
|
volumeListener?.cancel();
|
||||||
}
|
}
|
||||||
volumeListener = VolumeListener(
|
volumeListener = VolumeListener(
|
||||||
onDown: toNextPage,
|
onDown: onDown,
|
||||||
onUp: toPrevPage,
|
onUp: onUp,
|
||||||
)..listen();
|
)..listen();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,4 +601,6 @@ abstract interface class _ImageViewController {
|
|||||||
|
|
||||||
/// Returns true if the event is handled.
|
/// Returns true if the event is handled.
|
||||||
bool handleOnTap(Offset location);
|
bool handleOnTap(Offset location);
|
||||||
|
|
||||||
|
Future<Uint8List?> getImageByOffset(Offset offset);
|
||||||
}
|
}
|
||||||
|
@@ -127,6 +127,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
),
|
),
|
||||||
|
if (appdata.settings['showPageNumberInReader'] == true)
|
||||||
buildPageInfoText(),
|
buildPageInfoText(),
|
||||||
buildStatusInfo(),
|
buildStatusInfo(),
|
||||||
AnimatedPositioned(
|
AnimatedPositioned(
|
||||||
@@ -161,7 +162,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.only(top: context.padding.top),
|
padding: EdgeInsets.only(top: context.padding.top),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.colorScheme.surface.toOpacity(0.82),
|
color: context.colorScheme.surface.toOpacity(0.92),
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
color: Colors.grey.toOpacity(0.5),
|
color: Colors.grey.toOpacity(0.5),
|
||||||
@@ -475,7 +476,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
return BlurEffect(
|
return BlurEffect(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.colorScheme.surface.toOpacity(0.82),
|
color: context.colorScheme.surface.toOpacity(0.92),
|
||||||
border: isOpen
|
border: isOpen
|
||||||
? Border(
|
? Border(
|
||||||
top: BorderSide(
|
top: BorderSide(
|
||||||
|
@@ -441,6 +441,11 @@ class _SearchSettingsDialogState extends State<_SearchSettingsDialog> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
var sources = ComicSource.all();
|
||||||
|
var enabled = appdata.settings['searchSources'] as List;
|
||||||
|
sources.removeWhere((e) {
|
||||||
|
return !enabled.contains(e.key);
|
||||||
|
});
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: "Settings".tl,
|
title: "Settings".tl,
|
||||||
content: Column(
|
content: Column(
|
||||||
@@ -452,7 +457,7 @@ class _SearchSettingsDialogState extends State<_SearchSettingsDialog> {
|
|||||||
Wrap(
|
Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
children: ComicSource.all().map((e) {
|
children: sources.map((e) {
|
||||||
return OptionChip(
|
return OptionChip(
|
||||||
text: e.name.tl,
|
text: e.name.tl,
|
||||||
isSelected: searchTarget == e.key,
|
isSelected: searchTarget == e.key,
|
||||||
|
@@ -140,17 +140,6 @@ class _AppSettingsState extends State<AppSettings> {
|
|||||||
},
|
},
|
||||||
actionTitle: 'Set'.tl,
|
actionTitle: 'Set'.tl,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
_SettingPartTitle(
|
|
||||||
title: "Log".tl,
|
|
||||||
icon: Icons.error_outline,
|
|
||||||
),
|
|
||||||
_CallbackSetting(
|
|
||||||
title: "Open Log".tl,
|
|
||||||
callback: () {
|
|
||||||
context.to(() => const LogsPage());
|
|
||||||
},
|
|
||||||
actionTitle: 'Open'.tl,
|
|
||||||
).toSliver(),
|
|
||||||
_SettingPartTitle(
|
_SettingPartTitle(
|
||||||
title: "User".tl,
|
title: "User".tl,
|
||||||
icon: Icons.person_outline,
|
icon: Icons.person_outline,
|
||||||
|
95
lib/pages/settings/debug.dart
Normal file
95
lib/pages/settings/debug.dart
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
part of 'settings_page.dart';
|
||||||
|
|
||||||
|
class DebugPage extends StatefulWidget {
|
||||||
|
const DebugPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DebugPage> createState() => DebugPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class DebugPageState extends State<DebugPage> {
|
||||||
|
final controller = TextEditingController();
|
||||||
|
|
||||||
|
var result = "";
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SmoothCustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppbar(title: Text("Debug".tl)),
|
||||||
|
_CallbackSetting(
|
||||||
|
title: "Reload Configs",
|
||||||
|
actionTitle: "Reload",
|
||||||
|
callback: () {
|
||||||
|
ComicSourceManager().reload();
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
|
_CallbackSetting(
|
||||||
|
title: "Open Log".tl,
|
||||||
|
callback: () {
|
||||||
|
context.to(() => const LogsPage());
|
||||||
|
},
|
||||||
|
actionTitle: 'Open'.tl,
|
||||||
|
).toSliver(),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
"JS Evaluator",
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
).toAlign(Alignment.centerLeft).paddingLeft(16),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 200,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||||
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
|
maxLines: null,
|
||||||
|
expands: true,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
textAlignVertical: TextAlignVertical.top,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
contentPadding: const EdgeInsets.all(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
try {
|
||||||
|
var res = JsEngine().runCode(controller.text);
|
||||||
|
setState(() {
|
||||||
|
result = res.toString();
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
result = e.toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text("Run"),
|
||||||
|
).toAlign(Alignment.centerRight).paddingRight(16),
|
||||||
|
const Text(
|
||||||
|
"Result",
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
).toAlign(Alignment.centerLeft).paddingLeft(16),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 200,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: context.colorScheme.outline),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Text(result).paddingAll(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -25,8 +25,8 @@ class _ExploreSettingsState extends State<ExploreSettings> {
|
|||||||
title: "Size of comic tile".tl,
|
title: "Size of comic tile".tl,
|
||||||
settingsIndex: "comicTileScale",
|
settingsIndex: "comicTileScale",
|
||||||
interval: 0.05,
|
interval: 0.05,
|
||||||
min: 0.75,
|
min: 0.5,
|
||||||
max: 1.25,
|
max: 1.5,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
_PopupWindowSetting(
|
_PopupWindowSetting(
|
||||||
title: "Explore Pages".tl,
|
title: "Explore Pages".tl,
|
||||||
|
@@ -48,6 +48,7 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
"continuousTopToBottom": "Continuous (Top to Bottom)".tl,
|
"continuousTopToBottom": "Continuous (Top to Bottom)".tl,
|
||||||
},
|
},
|
||||||
onChanged: () {
|
onChanged: () {
|
||||||
|
setState(() {});
|
||||||
var readerMode = appdata.settings['readerMode'];
|
var readerMode = appdata.settings['readerMode'];
|
||||||
if (readerMode?.toLowerCase().startsWith('continuous') ?? false) {
|
if (readerMode?.toLowerCase().startsWith('continuous') ?? false) {
|
||||||
appdata.settings['readerScreenPicNumberForLandscape'] = 1;
|
appdata.settings['readerScreenPicNumberForLandscape'] = 1;
|
||||||
@@ -68,22 +69,12 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
widget.onChanged?.call("autoPageTurningInterval");
|
widget.onChanged?.call("autoPageTurningInterval");
|
||||||
},
|
},
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
SliverToBoxAdapter(
|
SliverAnimatedVisibility(
|
||||||
child: AbsorbPointer(
|
visible: appdata.settings['readerMode']!.startsWith('gallery'),
|
||||||
absorbing: (appdata.settings['readerMode']
|
|
||||||
?.toLowerCase()
|
|
||||||
.startsWith('continuous') ??
|
|
||||||
false),
|
|
||||||
child: AnimatedOpacity(
|
|
||||||
opacity: (appdata.settings['readerMode']
|
|
||||||
?.toLowerCase()
|
|
||||||
.startsWith('continuous') ??
|
|
||||||
false)
|
|
||||||
? 0.5
|
|
||||||
: 1.0,
|
|
||||||
duration: Duration(milliseconds: 300),
|
|
||||||
child: _SliderSetting(
|
child: _SliderSetting(
|
||||||
title: "The number of pic in screen for landscape (Only Gallery Mode)".tl,
|
title:
|
||||||
|
"The number of pic in screen for landscape (Only Gallery Mode)"
|
||||||
|
.tl,
|
||||||
settingsIndex: "readerScreenPicNumberForLandscape",
|
settingsIndex: "readerScreenPicNumberForLandscape",
|
||||||
interval: 1,
|
interval: 1,
|
||||||
min: 1,
|
min: 1,
|
||||||
@@ -93,24 +84,12 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
SliverAnimatedVisibility(
|
||||||
),
|
visible: appdata.settings['readerMode']!.startsWith('gallery'),
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: AbsorbPointer(
|
|
||||||
absorbing: (appdata.settings['readerMode']
|
|
||||||
?.toLowerCase()
|
|
||||||
.startsWith('continuous') ??
|
|
||||||
false),
|
|
||||||
child: AnimatedOpacity(
|
|
||||||
opacity: (appdata.settings['readerMode']
|
|
||||||
?.toLowerCase()
|
|
||||||
.startsWith('continuous') ??
|
|
||||||
false)
|
|
||||||
? 0.5
|
|
||||||
: 1.0,
|
|
||||||
duration: Duration(milliseconds: 300),
|
|
||||||
child: _SliderSetting(
|
child: _SliderSetting(
|
||||||
title: "The number of pic in screen for portrait (Only Gallery Mode)".tl,
|
title:
|
||||||
|
"The number of pic in screen for portrait (Only Gallery Mode)"
|
||||||
|
.tl,
|
||||||
settingsIndex: "readerScreenPicNumberForPortrait",
|
settingsIndex: "readerScreenPicNumberForPortrait",
|
||||||
interval: 1,
|
interval: 1,
|
||||||
min: 1,
|
min: 1,
|
||||||
@@ -120,15 +99,25 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
_SwitchSetting(
|
_SwitchSetting(
|
||||||
title: 'Long press to zoom'.tl,
|
title: 'Long press to zoom'.tl,
|
||||||
settingKey: 'enableLongPressToZoom',
|
settingKey: 'enableLongPressToZoom',
|
||||||
onChanged: () {
|
onChanged: () {
|
||||||
|
setState(() {});
|
||||||
widget.onChanged?.call('enableLongPressToZoom');
|
widget.onChanged?.call('enableLongPressToZoom');
|
||||||
},
|
},
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
|
SliverAnimatedVisibility(
|
||||||
|
visible: appdata.settings['enableLongPressToZoom'] == true,
|
||||||
|
child: SelectSetting(
|
||||||
|
title: "Long press zoom position".tl,
|
||||||
|
settingKey: "longPressZoomPosition",
|
||||||
|
optionTranslation: {
|
||||||
|
"press": "Press position".tl,
|
||||||
|
"center": "Screen center".tl,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
_SwitchSetting(
|
_SwitchSetting(
|
||||||
title: 'Limit image width'.tl,
|
title: 'Limit image width'.tl,
|
||||||
subtitle: 'When using Continuous(Top to Bottom) mode'.tl,
|
subtitle: 'When using Continuous(Top to Bottom) mode'.tl,
|
||||||
@@ -179,6 +168,13 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
min: 1,
|
min: 1,
|
||||||
max: 16,
|
max: 16,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
|
_SwitchSetting(
|
||||||
|
title: "Show Page Number".tl,
|
||||||
|
settingKey: "showPageNumberInReader",
|
||||||
|
onChanged: () {
|
||||||
|
widget.onChanged?.call("showPageNumberInReader");
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -30,6 +30,7 @@ part 'local_favorites.dart';
|
|||||||
part 'app.dart';
|
part 'app.dart';
|
||||||
part 'about.dart';
|
part 'about.dart';
|
||||||
part 'network.dart';
|
part 'network.dart';
|
||||||
|
part 'debug.dart';
|
||||||
|
|
||||||
class SettingsPage extends StatefulWidget {
|
class SettingsPage extends StatefulWidget {
|
||||||
const SettingsPage({this.initialPage = -1, super.key});
|
const SettingsPage({this.initialPage = -1, super.key});
|
||||||
@@ -55,6 +56,7 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
|
|||||||
"APP",
|
"APP",
|
||||||
"Network",
|
"Network",
|
||||||
"About",
|
"About",
|
||||||
|
"Debug"
|
||||||
];
|
];
|
||||||
|
|
||||||
final icons = <IconData>[
|
final icons = <IconData>[
|
||||||
@@ -64,7 +66,8 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
|
|||||||
Icons.collections_bookmark_rounded,
|
Icons.collections_bookmark_rounded,
|
||||||
Icons.apps,
|
Icons.apps,
|
||||||
Icons.public,
|
Icons.public,
|
||||||
Icons.info
|
Icons.info,
|
||||||
|
Icons.bug_report,
|
||||||
];
|
];
|
||||||
|
|
||||||
double offset = 0;
|
double offset = 0;
|
||||||
@@ -246,6 +249,9 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void handlePointerDown(PointerDownEvent event) {
|
void handlePointerDown(PointerDownEvent event) {
|
||||||
|
if (!App.isIOS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.position.dx < 20) {
|
if (event.position.dx < 20) {
|
||||||
gestureRecognizer.addPointer(event);
|
gestureRecognizer.addPointer(event);
|
||||||
}
|
}
|
||||||
@@ -350,6 +356,7 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
|
|||||||
4 => const AppSettings(),
|
4 => const AppSettings(),
|
||||||
5 => const NetworkSettings(),
|
5 => const NetworkSettings(),
|
||||||
6 => const AboutSettings(),
|
6 => const AboutSettings(),
|
||||||
|
7 => const DebugPage(),
|
||||||
_ => throw UnimplementedError()
|
_ => throw UnimplementedError()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
25
lib/utils/clipboard_image.dart
Normal file
25
lib/utils/clipboard_image.dart
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
Future<void> writeImageToClipboard(Uint8List imageBytes) async {
|
||||||
|
const channel = MethodChannel("venera/clipboard");
|
||||||
|
if (Platform.isWindows || Platform.isLinux) {
|
||||||
|
var image = await instantiateImageCodec(imageBytes);
|
||||||
|
var frame = await image.getNextFrame();
|
||||||
|
var data = await frame.image.toByteData(format: ImageByteFormat.rawRgba);
|
||||||
|
await channel.invokeMethod("writeImageToClipboard", {
|
||||||
|
"width": frame.image.width,
|
||||||
|
"height": frame.image.height,
|
||||||
|
"data": Uint8List.view(data!.buffer)
|
||||||
|
});
|
||||||
|
image.dispose();
|
||||||
|
} else if (Platform.isMacOS) {
|
||||||
|
await channel.invokeMethod("writeImageToClipboard", {
|
||||||
|
"data": imageBytes,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw UnsupportedError("Clipboard image is not supported on this platform");
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,6 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:venera/components/components.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/comic_source/comic_source.dart';
|
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||||
@@ -10,6 +12,7 @@ import 'package:venera/utils/data.dart';
|
|||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
import 'package:webdav_client/webdav_client.dart' hide File;
|
import 'package:webdav_client/webdav_client.dart' hide File;
|
||||||
import 'package:rhttp/rhttp.dart' as rhttp;
|
import 'package:rhttp/rhttp.dart' as rhttp;
|
||||||
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
import 'io.dart';
|
import 'io.dart';
|
||||||
|
|
||||||
@@ -20,6 +23,10 @@ class DataSync with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
LocalFavoritesManager().addListener(onDataChanged);
|
LocalFavoritesManager().addListener(onDataChanged);
|
||||||
ComicSourceManager().addListener(onDataChanged);
|
ComicSourceManager().addListener(onDataChanged);
|
||||||
|
Future.delayed(const Duration(seconds: 1), () {
|
||||||
|
var controller = WindowFrame.of(App.rootContext);
|
||||||
|
controller.addCloseListener(_handleWindowClose);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDataChanged() {
|
void onDataChanged() {
|
||||||
@@ -28,6 +35,28 @@ class DataSync with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _handleWindowClose() {
|
||||||
|
if (_isUploading) {
|
||||||
|
_showWindowCloseDialog();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showWindowCloseDialog() async {
|
||||||
|
showLoadingDialog(
|
||||||
|
App.rootContext,
|
||||||
|
cancelButtonText: "Shut Down".tl,
|
||||||
|
onCancel: () => exit(0),
|
||||||
|
barrierDismissible: false,
|
||||||
|
message: "Uploading data...".tl,
|
||||||
|
);
|
||||||
|
while (_isUploading) {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 50));
|
||||||
|
}
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
static DataSync? instance;
|
static DataSync? instance;
|
||||||
|
|
||||||
factory DataSync() => instance ?? (instance = DataSync._());
|
factory DataSync() => instance ?? (instance = DataSync._());
|
||||||
@@ -100,6 +129,7 @@ class DataSync with ChangeNotifier {
|
|||||||
rhttp.ClientSettings(
|
rhttp.ClientSettings(
|
||||||
proxySettings:
|
proxySettings:
|
||||||
proxy == null ? null : rhttp.ProxySettings.proxy(proxy),
|
proxy == null ? null : rhttp.ProxySettings.proxy(proxy),
|
||||||
|
userAgent: "venera v${App.version}",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -172,6 +202,7 @@ class DataSync with ChangeNotifier {
|
|||||||
rhttp.ClientSettings(
|
rhttp.ClientSettings(
|
||||||
proxySettings:
|
proxySettings:
|
||||||
proxy == null ? null : rhttp.ProxySettings.proxy(proxy),
|
proxy == null ? null : rhttp.ProxySettings.proxy(proxy),
|
||||||
|
userAgent: "venera v${App.version}",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
|
|
||||||
@@ -132,25 +131,28 @@ extension DirectoryExtension on Directory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sanitize the file name. Remove invalid characters and trim the file name.
|
/// Sanitize the file name. Remove invalid characters and trim the file name.
|
||||||
String sanitizeFileName(String fileName) {
|
String sanitizeFileName(String fileName, {String? dir, int? maxLength}) {
|
||||||
if (fileName.endsWith('.')) {
|
if (fileName.endsWith('.')) {
|
||||||
fileName = fileName.substring(0, fileName.length - 1);
|
fileName = fileName.substring(0, fileName.length - 1);
|
||||||
}
|
}
|
||||||
const maxLength = 255;
|
var maxLength = 255;
|
||||||
|
if (dir != null) {
|
||||||
|
if (!dir.endsWith('/') && !dir.endsWith('\\')) {
|
||||||
|
dir = "$dir/";
|
||||||
|
}
|
||||||
|
maxLength -= dir.length;
|
||||||
|
}
|
||||||
final invalidChars = RegExp(r'[<>:"/\\|?*]');
|
final invalidChars = RegExp(r'[<>:"/\\|?*]');
|
||||||
final sanitizedFileName = fileName.replaceAll(invalidChars, ' ');
|
final sanitizedFileName = fileName.replaceAll(invalidChars, ' ');
|
||||||
var trimmedFileName = sanitizedFileName.trim();
|
var trimmedFileName = sanitizedFileName.trim();
|
||||||
if (trimmedFileName.isEmpty) {
|
if (trimmedFileName.isEmpty) {
|
||||||
throw Exception('Invalid File Name: Empty length.');
|
throw Exception('Invalid File Name: Empty length.');
|
||||||
}
|
}
|
||||||
while (true) {
|
if (maxLength <= 0) {
|
||||||
final bytes = utf8.encode(trimmedFileName);
|
throw Exception('Invalid File Name: Max length is less than 0.');
|
||||||
if (bytes.length > maxLength) {
|
|
||||||
trimmedFileName =
|
|
||||||
trimmedFileName.substring(0, trimmedFileName.length - 1);
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
if (trimmedFileName.length > maxLength) {
|
||||||
|
trimmedFileName = trimmedFileName.substring(0, maxLength);
|
||||||
}
|
}
|
||||||
return trimmedFileName;
|
return trimmedFileName;
|
||||||
}
|
}
|
||||||
|
@@ -5,15 +5,45 @@
|
|||||||
#include <gdk/gdkx.h>
|
#include <gdk/gdkx.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
#include "flutter/generated_plugin_registrant.h"
|
#include "flutter/generated_plugin_registrant.h"
|
||||||
|
|
||||||
struct _MyApplication {
|
struct _MyApplication {
|
||||||
GtkApplication parent_instance;
|
GtkApplication parent_instance;
|
||||||
char** dart_entrypoint_arguments;
|
char** dart_entrypoint_arguments;
|
||||||
|
FlMethodChannel* clipboard_channel;
|
||||||
};
|
};
|
||||||
|
|
||||||
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||||
|
|
||||||
|
static void handle_clipboard_call(FlMethodChannel* channel, FlMethodCall* call, gpointer user_data) {
|
||||||
|
if (strcmp(fl_method_call_get_name(call), "writeImageToClipboard") == 0) {
|
||||||
|
const auto args = fl_method_call_get_args(call);
|
||||||
|
const auto width = fl_value_get_int(fl_value_get_map_value(args, 0));
|
||||||
|
const auto height = fl_value_get_int(fl_value_get_map_value(args, 1));
|
||||||
|
const auto data = fl_value_get_uint8_list(fl_value_get_map_value(args, 2));
|
||||||
|
|
||||||
|
std::cout << width << " " << height << " " << data[0] << " " << data[1] << std::endl;
|
||||||
|
|
||||||
|
GBytes* bytes = g_bytes_new(data, width * height * 4);
|
||||||
|
|
||||||
|
GdkDisplay* display = gdk_display_get_default();
|
||||||
|
GtkClipboard* clipboard = gtk_clipboard_get_default(display);
|
||||||
|
GdkPixbuf* pixbuf = gdk_pixbuf_new_from_bytes(
|
||||||
|
bytes,
|
||||||
|
GDK_COLORSPACE_RGB,
|
||||||
|
true,
|
||||||
|
8,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
width * 4
|
||||||
|
);
|
||||||
|
gtk_clipboard_set_image(clipboard, pixbuf);
|
||||||
|
fl_method_call_respond_success(call, fl_value_new_string("Ok"), nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Implements GApplication::activate.
|
// Implements GApplication::activate.
|
||||||
static void my_application_activate(GApplication* application) {
|
static void my_application_activate(GApplication* application) {
|
||||||
MyApplication* self = MY_APPLICATION(application);
|
MyApplication* self = MY_APPLICATION(application);
|
||||||
@@ -50,6 +80,7 @@ static void my_application_activate(GApplication* application) {
|
|||||||
gtk_window_set_default_size(window, 1280, 720);
|
gtk_window_set_default_size(window, 1280, 720);
|
||||||
GdkVisual* visual;
|
GdkVisual* visual;
|
||||||
gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE);
|
gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE);
|
||||||
|
gtk_window_set_decorated(window, FALSE);
|
||||||
visual = gdk_screen_get_rgba_visual(screen);
|
visual = gdk_screen_get_rgba_visual(screen);
|
||||||
if (visual != NULL && gdk_screen_is_composited(screen)) {
|
if (visual != NULL && gdk_screen_is_composited(screen)) {
|
||||||
gtk_widget_set_visual(GTK_WIDGET(window), visual);
|
gtk_widget_set_visual(GTK_WIDGET(window), visual);
|
||||||
@@ -64,6 +95,14 @@ static void my_application_activate(GApplication* application) {
|
|||||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||||
|
|
||||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||||
|
|
||||||
|
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
|
||||||
|
self->clipboard_channel = fl_method_channel_new(
|
||||||
|
fl_engine_get_binary_messenger(fl_view_get_engine(view)),
|
||||||
|
"venera/clipboard", FL_METHOD_CODEC(codec));
|
||||||
|
fl_method_channel_set_method_call_handler(
|
||||||
|
self->clipboard_channel, handle_clipboard_call, self, nullptr);
|
||||||
|
|
||||||
gtk_widget_hide(GTK_WIDGET(window));
|
gtk_widget_hide(GTK_WIDGET(window));
|
||||||
|
|
||||||
gtk_widget_grab_focus(GTK_WIDGET(view));
|
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||||
@@ -110,6 +149,7 @@ static void my_application_shutdown(GApplication* application) {
|
|||||||
static void my_application_dispose(GObject* object) {
|
static void my_application_dispose(GObject* object) {
|
||||||
MyApplication* self = MY_APPLICATION(object);
|
MyApplication* self = MY_APPLICATION(object);
|
||||||
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||||
|
g_clear_object(&self->clipboard_channel);
|
||||||
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -38,6 +38,31 @@ class AppDelegate: FlutterAppDelegate {
|
|||||||
result(FlutterMethodNotImplemented)
|
result(FlutterMethodNotImplemented)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let clipboardChannel = FlutterMethodChannel(name: "venera/clipboard", binaryMessenger: controller.engine.binaryMessenger)
|
||||||
|
|
||||||
|
clipboardChannel.setMethodCallHandler { (call, result) in
|
||||||
|
switch call.method {
|
||||||
|
case "writeImageToClipboard":
|
||||||
|
guard let arguments = call.arguments as? [String: Any],
|
||||||
|
let data = arguments["data"] as? FlutterStandardTypedData else {
|
||||||
|
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Invalid arguments", details: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let image = NSImage(data: data.data) else {
|
||||||
|
result(FlutterError(code: "INVALID_IMAGE", message: "Could not create image from data", details: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let pasteboard = NSPasteboard.general
|
||||||
|
pasteboard.clearContents()
|
||||||
|
pasteboard.writeObjects([image])
|
||||||
|
result(true)
|
||||||
|
default:
|
||||||
|
result(FlutterMethodNotImplemented)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDirectoryPath() {
|
func getDirectoryPath() {
|
||||||
|
45
pubspec.lock
45
pubspec.lock
@@ -45,10 +45,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.12.0"
|
version: "2.13.0"
|
||||||
battery_plus:
|
battery_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -149,8 +149,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "packages/desktop_webview_window"
|
path: "packages/desktop_webview_window"
|
||||||
ref: HEAD
|
ref: "7801fc582ecf5a7351632887891ecf309a7b2583"
|
||||||
resolved-ref: b8f7e94c576acf4ca3dce5b9f8fb8076e5eaca5e
|
resolved-ref: "7801fc582ecf5a7351632887891ecf309a7b2583"
|
||||||
url: "https://github.com/wgh136/flutter_desktop_webview"
|
url: "https://github.com/wgh136/flutter_desktop_webview"
|
||||||
source: git
|
source: git
|
||||||
version: "0.2.4"
|
version: "0.2.4"
|
||||||
@@ -182,10 +182,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: fake_async
|
name: fake_async
|
||||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.2"
|
version: "1.3.3"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -433,8 +433,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "690a03a954f1603e0149cfd479c8961b88f21336"
|
ref: fe182cdf40e5fa6230f451bc1d643b860f610d13
|
||||||
resolved-ref: "690a03a954f1603e0149cfd479c8961b88f21336"
|
resolved-ref: fe182cdf40e5fa6230f451bc1d643b860f610d13
|
||||||
url: "https://github.com/venera-app/flutter_saf"
|
url: "https://github.com/venera-app/flutter_saf"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
@@ -516,10 +516,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.19.0"
|
version: "0.20.2"
|
||||||
io:
|
io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -540,10 +540,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.8"
|
version: "10.0.9"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -580,10 +580,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: local_auth_android
|
name: local_auth_android
|
||||||
sha256: "6763aaf8965f21822624cb2fd3c03d2a8b3791037b5efb0fe4b13e110f5afc92"
|
sha256: "0abe4e72f55c785b28900de52a2522c86baba0988838b5dc22241b072ecccd74"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.46"
|
version: "1.0.48"
|
||||||
local_auth_darwin:
|
local_auth_darwin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -725,8 +725,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: d71faf3c75e059d013b0ce9ddaf5ecc1680e2eb6
|
ref: a1255d1b5945aad4b7323303ec2ecdf0c90ffc4c
|
||||||
resolved-ref: d71faf3c75e059d013b0ce9ddaf5ecc1680e2eb6
|
resolved-ref: a1255d1b5945aad4b7323303ec2ecdf0c90ffc4c
|
||||||
url: "https://github.com/wgh136/photo_view"
|
url: "https://github.com/wgh136/photo_view"
|
||||||
source: git
|
source: git
|
||||||
version: "0.14.0"
|
version: "0.14.0"
|
||||||
@@ -757,10 +757,11 @@ packages:
|
|||||||
rhttp:
|
rhttp:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: rhttp
|
path: rhttp
|
||||||
sha256: "037e9b59a68bb4ba664db1cbb4601e878cf5a2fc1cb3d0a9c58e3776609dec4d"
|
ref: e7dca15ca543b5df49f3ada06016e874b79bce36
|
||||||
url: "https://pub.dev"
|
resolved-ref: e7dca15ca543b5df49f3ada06016e874b79bce36
|
||||||
source: hosted
|
url: "https://github.com/wgh136/rhttp"
|
||||||
|
source: git
|
||||||
version: "0.11.0"
|
version: "0.11.0"
|
||||||
screen_retriever:
|
screen_retriever:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
@@ -1028,10 +1029,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.3.1"
|
version: "15.0.0"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
13
pubspec.yaml
13
pubspec.yaml
@@ -2,7 +2,7 @@ name: venera
|
|||||||
description: "A comic app."
|
description: "A comic app."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.3.3+133
|
version: 1.4.0+140
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.6.0 <4.0.0'
|
sdk: '>=3.6.0 <4.0.0'
|
||||||
@@ -29,7 +29,7 @@ dependencies:
|
|||||||
photo_view:
|
photo_view:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/wgh136/photo_view
|
url: https://github.com/wgh136/photo_view
|
||||||
ref: d71faf3c75e059d013b0ce9ddaf5ecc1680e2eb6
|
ref: a1255d1b5945aad4b7323303ec2ecdf0c90ffc4c
|
||||||
mime: ^2.0.0
|
mime: ^2.0.0
|
||||||
share_plus: ^10.1.4
|
share_plus: ^10.1.4
|
||||||
scrollable_positioned_list:
|
scrollable_positioned_list:
|
||||||
@@ -43,6 +43,7 @@ dependencies:
|
|||||||
git:
|
git:
|
||||||
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
|
||||||
|
ref: 7801fc582ecf5a7351632887891ecf309a7b2583
|
||||||
flutter_inappwebview:
|
flutter_inappwebview:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/pichillilorenzo/flutter_inappwebview
|
url: https://github.com/pichillilorenzo/flutter_inappwebview
|
||||||
@@ -57,7 +58,11 @@ dependencies:
|
|||||||
git:
|
git:
|
||||||
url: https://github.com/venera-app/lodepng_flutter
|
url: https://github.com/venera-app/lodepng_flutter
|
||||||
ref: ac7d05dde32e8d728102a9ff66e6b55f05d94ba1
|
ref: ac7d05dde32e8d728102a9ff66e6b55f05d94ba1
|
||||||
rhttp: ^0.11.0
|
rhttp:
|
||||||
|
git:
|
||||||
|
url: https://github.com/wgh136/rhttp
|
||||||
|
ref: e7dca15ca543b5df49f3ada06016e874b79bce36
|
||||||
|
path: rhttp
|
||||||
webdav_client:
|
webdav_client:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/wgh136/webdav_client
|
url: https://github.com/wgh136/webdav_client
|
||||||
@@ -67,7 +72,7 @@ dependencies:
|
|||||||
flutter_saf:
|
flutter_saf:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/venera-app/flutter_saf
|
url: https://github.com/venera-app/flutter_saf
|
||||||
ref: 690a03a954f1603e0149cfd479c8961b88f21336
|
ref: fe182cdf40e5fa6230f451bc1d643b860f610d13
|
||||||
dynamic_color: ^1.7.0
|
dynamic_color: ^1.7.0
|
||||||
shimmer_animation: ^2.1.0
|
shimmer_animation: ^2.1.0
|
||||||
flutter_memory_info: ^0.0.1
|
flutter_memory_info: ^0.0.1
|
||||||
|
73
windows/build_arm64.iss
Normal file
73
windows/build_arm64.iss
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
; Script generated by the Inno Setup Script Wizard.
|
||||||
|
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||||
|
|
||||||
|
#define MyAppName "Venera"
|
||||||
|
#define MyAppVersion "{{version}}"
|
||||||
|
#define MyAppPublisher "nyne"
|
||||||
|
#define MyAppURL "https://github.com/venera-app/venera"
|
||||||
|
#define MyAppExeName "venera.exe"
|
||||||
|
#define RootPath "{{root_path}}"
|
||||||
|
|
||||||
|
[Setup]
|
||||||
|
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
|
||||||
|
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||||
|
AppId={{1A39CB64-0A5B-478E-9590-978614C804A8}
|
||||||
|
AppName={#MyAppName}
|
||||||
|
AppVersion={#MyAppVersion}
|
||||||
|
;AppVerName={#MyAppName} {#MyAppVersion}
|
||||||
|
AppPublisher={#MyAppPublisher}
|
||||||
|
AppPublisherURL={#MyAppURL}
|
||||||
|
AppSupportURL={#MyAppURL}
|
||||||
|
AppUpdatesURL={#MyAppURL}
|
||||||
|
DefaultDirName={autopf}\{#MyAppName}
|
||||||
|
DisableProgramGroupPage=yes
|
||||||
|
; Uncomment the following line to run in non administrative install mode (install for current user only.)
|
||||||
|
;PrivilegesRequired=lowest
|
||||||
|
PrivilegesRequiredOverridesAllowed=dialog
|
||||||
|
OutputDir={#RootPath}\build\windows
|
||||||
|
OutputBaseFilename=Venera-{#MyAppVersion}-windows-arm64-installer
|
||||||
|
SetupIconFile={#RootPath}\windows\runner\resources\app_icon.ico
|
||||||
|
Compression=lzma
|
||||||
|
SolidCompression=yes
|
||||||
|
WizardStyle=modern
|
||||||
|
ArchitecturesInstallIn64BitMode=arm64
|
||||||
|
ArchitecturesAllowed=arm64
|
||||||
|
|
||||||
|
[Languages]
|
||||||
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
|
Name: "chinesesimplified"; MessagesFile: "{#RootPath}\windows\ChineseSimplified.isl"
|
||||||
|
|
||||||
|
[Tasks]
|
||||||
|
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||||
|
|
||||||
|
[Files]
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\flutter_inappwebview_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\file_selector_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\app_links_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\sqlite3.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\sqlite3_flutter_libs_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\flutter_qjs_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\desktop_webview_window_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\WebView2Loader.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\share_plus_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\battery_plus_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\screen_retriever_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\window_manager_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\local_auth_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\zip_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\rhttp.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\lodepng_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\dynamic_color_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\flutter_7zip.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "{#RootPath}\build\windows\arm64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
|
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||||
|
|
||||||
|
[Icons]
|
||||||
|
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||||
|
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||||
|
|
||||||
|
[Run]
|
||||||
|
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall
|
43
windows/build_arm64.py
Normal file
43
windows/build_arm64.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
file = open('pubspec.yaml', 'r')
|
||||||
|
content = file.read()
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
subprocess.run(["flutter", "build", "windows"], shell=True)
|
||||||
|
|
||||||
|
if os.path.exists("build/app-windows.zip"):
|
||||||
|
os.remove("build/app-windows.zip")
|
||||||
|
|
||||||
|
version = str.split(str.split(content, 'version: ')[1], '+')[0]
|
||||||
|
|
||||||
|
subprocess.run(["tar", "-a", "-c", "-f", f"build/windows/Venera-{version}-windows-arm64.zip", "-C", "build/windows/x64/runner/Release", "*"]
|
||||||
|
, shell=True)
|
||||||
|
|
||||||
|
issPath = "windows/build_arm64.iss"
|
||||||
|
|
||||||
|
issContent = ""
|
||||||
|
file = open(issPath, 'r')
|
||||||
|
issContent = file.read()
|
||||||
|
newContent = issContent
|
||||||
|
newContent = newContent.replace("{{version}}", version)
|
||||||
|
newContent = newContent.replace("{{root_path}}", os.getcwd())
|
||||||
|
file.close()
|
||||||
|
file = open(issPath, 'w')
|
||||||
|
file.write(newContent)
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
if not os.path.exists("windows/ChineseSimplified.isl"):
|
||||||
|
# download ChineseSimplified.isl
|
||||||
|
url = "https://cdn.jsdelivr.net/gh/kira-96/Inno-Setup-Chinese-Simplified-Translation@latest/ChineseSimplified.isl"
|
||||||
|
response = httpx.get(url)
|
||||||
|
with open('windows/ChineseSimplified.isl', 'wb') as file:
|
||||||
|
file.write(response.content)
|
||||||
|
|
||||||
|
subprocess.run(["iscc", issPath], shell=True)
|
||||||
|
|
||||||
|
with open(issPath, 'w') as file:
|
||||||
|
file.write(issContent)
|
@@ -102,6 +102,47 @@ bool FlutterWindow::OnCreate() {
|
|||||||
|
|
||||||
channel2.SetStreamHandler(std::move(eventHandler));
|
channel2.SetStreamHandler(std::move(eventHandler));
|
||||||
|
|
||||||
|
const flutter::MethodChannel<> channel3(
|
||||||
|
flutter_controller_->engine()->messenger(), "venera/clipboard",
|
||||||
|
&flutter::StandardMethodCodec::GetInstance()
|
||||||
|
);
|
||||||
|
channel3.SetMethodCallHandler(
|
||||||
|
[](const flutter::MethodCall<>& call,const std::unique_ptr<flutter::MethodResult<>>& result) {
|
||||||
|
if(call.method_name() == "writeImageToClipboard"){
|
||||||
|
flutter::EncodableMap arguments = std::get<flutter::EncodableMap>(*call.arguments());
|
||||||
|
std::vector<uint8_t> data = std::get<std::vector<uint8_t>>(arguments["data"]);
|
||||||
|
std::int32_t width = std::get<std::int32_t>(arguments["width"]);
|
||||||
|
std::int32_t height = std::get<std::int32_t>(arguments["height"]);
|
||||||
|
|
||||||
|
// convert rgba to bgra
|
||||||
|
for (int i = 0; i < data.size()/4; i++) {
|
||||||
|
uint8_t temp = data[i * 4];
|
||||||
|
data[i * 4] = data[i * 4 + 2];
|
||||||
|
data[i * 4 + 2] = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto bitmap = CreateBitmap((int)width, (int)height, 1, 32, data.data());
|
||||||
|
|
||||||
|
if (!bitmap) {
|
||||||
|
result->Error("0", "Invalid Image Data");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OpenClipboard(NULL))
|
||||||
|
{
|
||||||
|
EmptyClipboard();
|
||||||
|
SetClipboardData(CF_BITMAP, bitmap);
|
||||||
|
CloseClipboard();
|
||||||
|
result->Success();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result->Error("Failed to open clipboard");
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteObject(bitmap);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
||||||
|
|
||||||
flutter_controller_->engine()->SetNextFrameCallback([&]() {
|
flutter_controller_->engine()->SetNextFrameCallback([&]() {
|
||||||
|
Reference in New Issue
Block a user