60 Commits

Author SHA1 Message Date
nyne
97768b4945 Merge pull request #317 from venera-app/v1.4.0-dev
V1.4.0
2025-04-05 22:06:21 +08:00
2481780ab3 fix issues reported by analyzer. 2025-04-05 22:03:54 +08:00
nyne
49481bfa6a Fix windows arm64 build script 2025-04-05 21:32:31 +08:00
211850d73e Improve comic source importing UI 2025-04-05 21:22:00 +08:00
fcf0334d55 Fix the issue that the downloaded chapters was not saved when download a comic without select chapters. Close #305 2025-04-05 20:58:06 +08:00
aa8eec5792 Improve UI. 2025-04-05 20:48:04 +08:00
6eb0060dd6 Add debug page. 2025-04-05 20:29:30 +08:00
c096f5a2d8 Add dynamic category part. 2025-04-05 20:11:05 +08:00
554b9f2a77 Fix search sources in search results page. 2025-04-05 19:31:41 +08:00
f87afbe397 Fix issues with empty chapter list. 2025-04-05 18:00:55 +08:00
6ff30f8ac3 Improve chapter display. 2025-04-05 17:48:49 +08:00
118941f239 Fix the mouse scrolling issue when multiple scroll lists are nested. 2025-04-05 17:45:29 +08:00
d91bca6913 [Comic Source] Improve data conversion 2025-04-05 17:18:53 +08:00
463ad5b5bc [Comic Source] New model PageJumpTarget. All page jump operations now use PageJumpTarget. 2025-04-04 22:47:43 +08:00
971fc1da92 Update version code. 2025-04-03 13:04:25 +08:00
37af7e266a Allow changing chapter by volume key. Close #250 2025-04-03 13:03:39 +08:00
276e23354d Smooth scroll for comments page. 2025-04-03 11:53:43 +08:00
3da00595b7 Add a setting for long press position. Close #287 2025-04-02 16:23:51 +08:00
nyne
d3c115ee0c fix log 2025-04-02 09:41:10 +08:00
dcc94c5b3d Fix crash on Android. 2025-04-01 21:07:29 +08:00
a116b5b615 Update AGP to 8.9.0 2025-04-01 20:36:24 +08:00
05fcb23a4d Limit download directory length. Close #311 2025-04-01 15:49:22 +08:00
daa6e8ce18 Show comic pages in details page. 2025-04-01 15:13:09 +08:00
8665994572 Write logs to file. 2025-04-01 14:57:11 +08:00
90441af989 Fix the issue where local comics page can not been opened when there is a comic with empty chapter list. Close #309 2025-03-31 16:10:14 +08:00
7631fab86b Prevent window from closing while uploading data 2025-03-31 15:46:41 +08:00
cd9b07bb3e Fix restoring window placement on linux 2025-03-31 12:26:32 +08:00
6c179ceb95 Add UA to WebDav requests. Close #308 2025-03-30 18:27:52 +08:00
ec48dbef57 Update linux icon 2025-03-30 18:23:43 +08:00
cd1cc1229e Remove native linux window decoration. 2025-03-30 15:42:43 +08:00
nyne
bda299e1f8 Merge pull request #304 from venera-app/v1.3.4-dev
V1.3.4
2025-03-28 19:23:50 +08:00
nyne
78ea129564 fix analyze error 2025-03-28 19:22:02 +08:00
nyne
f3b4598bb6 Fix the issue of not being able to read local comics. 2025-03-28 18:54:32 +08:00
nyne
7bc4c69a32 Add windows arm64 build script 2025-03-28 18:29:56 +08:00
nyne
a8e55e0151 Improve the long press to zoom feature. 2025-03-28 18:03:44 +08:00
nyne
fddd959545 fix windows arm64 build. 2025-03-28 18:02:36 +08:00
nyne
ebf6846bf1 fix windows arm64 build. 2025-03-28 16:18:23 +08:00
0f2d0bb9f9 Update version code. 2025-03-28 10:59:48 +08:00
48338e4ef7 Fix implicit data writing. Close #280 2025-03-28 10:58:47 +08:00
8d8e345d82 Fix invalid space when using Galley mode with multiple images on screen. Close #277 2025-03-27 23:00:06 +08:00
nyne
fcbf6a6277 Update issue checker 2025-03-27 21:28:07 +08:00
d83d679eb9 Implement writeImageToClipboard on macOS. 2025-03-27 19:40:51 +08:00
d6087e5f59 Implement writeImageToClipboard on Linux. 2025-03-27 14:52:05 +08:00
37371bee6c Merge remote-tracking branch 'origin/linux-window' into v1.3.4-dev
# Conflicts:
#	assets/translation.json
2025-03-27 13:13:18 +08:00
45fe5f503a Improve blur effect. 2025-03-27 13:11:20 +08:00
d440ed6424 Improve the long press to zoom feature.
Close #287
2025-03-27 13:04:19 +08:00
d812332613 Add image copy functionality.
Currently only supports Windows.
Close #260
2025-03-26 22:50:00 +08:00
dee8d17b1e Increase the range of comic tile size. Close #275 2025-03-26 19:55:56 +08:00
nyne
c0d461ebd9 Update prompt. 2025-03-26 18:41:27 +08:00
nyne
45e2a1142a Update model 2025-03-26 18:25:36 +08:00
nyne
533c2b2507 Update issue_check.yml 2025-03-26 18:15:52 +08:00
nyne
29b7e0d646 Add a workflow to check issues. 2025-03-26 17:47:59 +08:00
b1870b65d6 Translations for page selector. Close #286 2025-03-25 16:49:44 +08:00
1103076009 Improved page switching via keyboard. Close #293 2025-03-25 16:36:08 +08:00
51739355c8 Add clipboard methods to js engine. 2025-03-25 16:24:05 +08:00
1b4f67b314 The line starts with 'class' is considered as first line. 2025-03-25 16:18:43 +08:00
ba8831caa6 Add option to show page number in reader settings 2025-03-24 18:54:48 +08:00
2b1684b0fc Added a 'Back to Top' button. Close #276 2025-03-23 17:11:23 +08:00
cd3f09efae Make sure the app quits when the window is closed. 2025-03-23 16:48:07 +08:00
03628f2afa Improve gesture for continuous mode. 2025-03-22 11:11:20 +08:00
56 changed files with 1615 additions and 601 deletions

29
.github/workflows/issue_check.yml vendored Normal file
View 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"

View File

@@ -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'
} }

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View 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");
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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([&]() {