mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
Compare commits
30 Commits
linux-wind
...
v1.3.4
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bda299e1f8 | ||
![]() |
78ea129564 | ||
![]() |
f3b4598bb6 | ||
![]() |
7bc4c69a32 | ||
![]() |
a8e55e0151 | ||
![]() |
fddd959545 | ||
![]() |
ebf6846bf1 | ||
0f2d0bb9f9 | |||
48338e4ef7 | |||
8d8e345d82 | |||
![]() |
fcbf6a6277 | ||
d83d679eb9 | |||
d6087e5f59 | |||
37371bee6c | |||
45fe5f503a | |||
d440ed6424 | |||
d812332613 | |||
dee8d17b1e | |||
![]() |
c0d461ebd9 | ||
![]() |
45e2a1142a | ||
![]() |
533c2b2507 | ||
![]() |
29b7e0d646 | ||
b1870b65d6 | |||
1103076009 | |||
51739355c8 | |||
1b4f67b314 | |||
ba8831caa6 | |||
2b1684b0fc | |||
cd3f09efae | |||
03628f2afa |
29
.github/workflows/issue_check.yml
vendored
Normal file
29
.github/workflows/issue_check.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Check Issue
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check Issue
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
id: checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check Issue
|
||||
id: check
|
||||
uses: wgh136/gpt_issue_checker@v1.0.2
|
||||
with:
|
||||
api-url: ${{ secrets.API_URL }}
|
||||
api-key: ${{ secrets.API_KEY }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prompt: "You are a repository issue checker. The project is a comic app that supports view local or network comics using config files. To view a comic source, user must add a config file. User should not report any issue related to config file to the project repository because there is another repository for managing config files. You are given an issue content and you need to decide whether to close the issue. If you decide to close the issue, you should also provide a comment explaining why you are closing the issue. If you decide not to close the issue, you should provide a comment which is a summary of the issue. You should response with a JSON object with the following keys: should_close, should_comment, comment."
|
||||
model: "gpt-4o"
|
@@ -1357,4 +1357,30 @@ let APP = {
|
||||
method: 'getPlatform'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set clipboard text
|
||||
* @param text {string}
|
||||
* @returns {Promise<void>}
|
||||
*
|
||||
* @since 1.3.4
|
||||
*/
|
||||
function setClipboard(text) {
|
||||
return sendMessage({
|
||||
method: 'setClipboard',
|
||||
text: text
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clipboard text
|
||||
* @returns {Promise<string>}
|
||||
*
|
||||
* @since 1.3.4
|
||||
*/
|
||||
function getClipboard() {
|
||||
return sendMessage({
|
||||
method: 'getClipboard'
|
||||
})
|
||||
}
|
@@ -373,6 +373,11 @@
|
||||
"Paging": "分页",
|
||||
"Continuous": "连续",
|
||||
"Display mode of comic list": "漫画列表的显示模式",
|
||||
"Show Page Number": "显示页码",
|
||||
"Jump to page": "跳转到页面",
|
||||
"Page": "页面",
|
||||
"Jump": "跳转",
|
||||
"Copy Image": "复制图片",
|
||||
"A valid WebDav directory URL": "有效的WebDav目录URL"
|
||||
},
|
||||
"zh_TW": {
|
||||
@@ -749,6 +754,11 @@
|
||||
"Paging": "分頁",
|
||||
"Continuous": "連續",
|
||||
"Display mode of comic list": "漫畫列表的顯示模式",
|
||||
"Show Page Number": "顯示頁碼",
|
||||
"Jump to page": "跳轉到頁面",
|
||||
"Page": "頁面",
|
||||
"Jump": "跳轉",
|
||||
"Copy Image": "複製圖片",
|
||||
"A valid WebDav directory URL": "有效的WebDav目錄URL"
|
||||
}
|
||||
}
|
||||
|
@@ -80,7 +80,7 @@ class _AppbarState extends State<Appbar> {
|
||||
var content = Container(
|
||||
decoration: BoxDecoration(
|
||||
color: widget.backgroundColor ??
|
||||
context.colorScheme.surface.toOpacity(0.72),
|
||||
context.colorScheme.surface.toOpacity(0.86),
|
||||
),
|
||||
height: _kAppBarHeight + context.padding.top,
|
||||
child: Row(
|
||||
@@ -231,7 +231,7 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
||||
child: BlurEffect(
|
||||
blur: 15,
|
||||
child: Material(
|
||||
color: context.colorScheme.surface.toOpacity(0.72),
|
||||
color: context.colorScheme.surface.toOpacity(0.86),
|
||||
elevation: 0,
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
child: body,
|
||||
|
@@ -61,7 +61,7 @@ class _MenuRoute<T> extends PopupRoute<T> {
|
||||
child: BlurEffect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Material(
|
||||
color: context.colorScheme.surface.toOpacity(0.78),
|
||||
color: context.colorScheme.surface.toOpacity(0.92),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
width: width,
|
||||
|
@@ -82,7 +82,10 @@ class _WindowFrameState extends State<WindowFrame> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
windowManager.close();
|
||||
windowManager.close().then((_) {
|
||||
// Make sure the app exits when the window is closed.
|
||||
exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
@@ -13,7 +13,7 @@ export "widget_utils.dart";
|
||||
export "context.dart";
|
||||
|
||||
class _App {
|
||||
final version = "1.3.3";
|
||||
final version = "1.3.4";
|
||||
|
||||
bool get isAndroid => Platform.isAndroid;
|
||||
|
||||
|
@@ -17,17 +17,18 @@ class Appdata with Init {
|
||||
bool _isSavingData = false;
|
||||
|
||||
Future<void> saveData([bool sync = true]) async {
|
||||
if (_isSavingData) {
|
||||
await Future.doWhile(() async {
|
||||
await Future.delayed(const Duration(milliseconds: 20));
|
||||
return _isSavingData;
|
||||
});
|
||||
while (_isSavingData) {
|
||||
await Future.delayed(const Duration(milliseconds: 20));
|
||||
}
|
||||
_isSavingData = true;
|
||||
var data = jsonEncode(toJson());
|
||||
var file = File(FilePath.join(App.dataPath, 'appdata.json'));
|
||||
await file.writeAsString(data);
|
||||
_isSavingData = false;
|
||||
try {
|
||||
var data = jsonEncode(toJson());
|
||||
var file = File(FilePath.join(App.dataPath, 'appdata.json'));
|
||||
await file.writeAsString(data);
|
||||
}
|
||||
finally {
|
||||
_isSavingData = false;
|
||||
}
|
||||
if (sync) {
|
||||
DataSync().uploadData();
|
||||
}
|
||||
@@ -85,9 +86,18 @@ class Appdata with Init {
|
||||
|
||||
var implicitData = <String, dynamic>{};
|
||||
|
||||
void writeImplicitData() {
|
||||
var file = File(FilePath.join(App.dataPath, 'implicitData.json'));
|
||||
file.writeAsString(jsonEncode(implicitData));
|
||||
void writeImplicitData() async {
|
||||
while (_isSavingData) {
|
||||
await Future.delayed(const Duration(milliseconds: 20));
|
||||
}
|
||||
_isSavingData = true;
|
||||
try {
|
||||
var file = File(FilePath.join(App.dataPath, 'implicitData.json'));
|
||||
await file.writeAsString(jsonEncode(implicitData));
|
||||
}
|
||||
finally {
|
||||
_isSavingData = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -109,7 +119,12 @@ class Appdata with Init {
|
||||
searchHistory = List.from(json['searchHistory']);
|
||||
var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json'));
|
||||
if (await implicitDataFile.exists()) {
|
||||
implicitData = jsonDecode(await implicitDataFile.readAsString());
|
||||
try {
|
||||
implicitData = jsonDecode(await implicitDataFile.readAsString());
|
||||
}
|
||||
catch(_) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,6 +183,7 @@ class Settings with ChangeNotifier {
|
||||
'followUpdatesFolder': null,
|
||||
'initialPage': '0',
|
||||
'comicListDisplayMode': 'paging', // paging, continuous
|
||||
'showPageNumberInReader': true,
|
||||
};
|
||||
|
||||
operator [](String key) {
|
||||
|
@@ -82,7 +82,7 @@ class ComicSourceParser {
|
||||
js = js.replaceAll("\r\n", "\n");
|
||||
var line1 = js
|
||||
.split('\n')
|
||||
.firstWhereOrNull((element) => element.removeAllBlank.isNotEmpty);
|
||||
.firstWhereOrNull((e) => e.trim().startsWith("class "));
|
||||
if (line1 == null ||
|
||||
!line1.startsWith("class ") ||
|
||||
!line1.contains("extends ComicSource")) {
|
||||
|
@@ -163,6 +163,13 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
|
||||
return "${App.locale.languageCode}_${App.locale.countryCode}";
|
||||
case "getPlatform":
|
||||
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;
|
||||
|
@@ -75,6 +75,8 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
|
||||
bool isDownloaded = false;
|
||||
|
||||
bool showFAB = false;
|
||||
|
||||
@override
|
||||
void onReadEnd() {
|
||||
history ??=
|
||||
@@ -114,7 +116,15 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
ComicDetails get comic => data!;
|
||||
|
||||
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) {
|
||||
setState(() {
|
||||
showAppbarTitle = true;
|
||||
@@ -133,19 +143,33 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
|
||||
@override
|
||||
Widget buildContent(BuildContext context, ComicDetails data) {
|
||||
return SmoothCustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
...buildTitle(),
|
||||
buildActions(),
|
||||
buildDescription(),
|
||||
buildInfo(),
|
||||
buildChapters(),
|
||||
buildComments(),
|
||||
buildThumbnails(),
|
||||
buildRecommend(),
|
||||
SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom)),
|
||||
],
|
||||
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,
|
||||
slivers: [
|
||||
...buildTitle(),
|
||||
buildActions(),
|
||||
buildDescription(),
|
||||
buildInfo(),
|
||||
buildChapters(),
|
||||
buildComments(),
|
||||
buildThumbnails(),
|
||||
buildRecommend(),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(bottom: context.padding.bottom + 80), // Add additional padding for FAB
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -306,7 +306,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
||||
});
|
||||
} else {
|
||||
// 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();
|
||||
}
|
||||
},
|
||||
|
@@ -24,6 +24,8 @@ class ComicImage extends StatefulWidget {
|
||||
Map<String, String>? headers,
|
||||
int? cacheWidth,
|
||||
int? cacheHeight,
|
||||
this.onInit,
|
||||
this.onDispose,
|
||||
}) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, image),
|
||||
assert(cacheWidth == null || cacheWidth > 0),
|
||||
assert(cacheHeight == null || cacheHeight > 0);
|
||||
@@ -60,6 +62,10 @@ class ComicImage extends StatefulWidget {
|
||||
|
||||
final bool isAntiAlias;
|
||||
|
||||
final void Function(State<ComicImage> state)? onInit;
|
||||
|
||||
final void Function(State<ComicImage> state)? onDispose;
|
||||
|
||||
static void clear() => _ComicImageState.clear();
|
||||
|
||||
@override
|
||||
@@ -87,6 +93,7 @@ class _ComicImageState extends State<ComicImage> with WidgetsBindingObserver {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_scrollAwareContext = DisposableBuildContext<State<ComicImage>>(this);
|
||||
widget.onInit?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -97,6 +104,7 @@ class _ComicImageState extends State<ComicImage> with WidgetsBindingObserver {
|
||||
_completerHandle?.dispose();
|
||||
_scrollAwareContext.dispose();
|
||||
_replaceImage(info: null);
|
||||
widget.onDispose?.call(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -136,6 +144,15 @@ class _ComicImageState extends State<ComicImage> with WidgetsBindingObserver {
|
||||
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() {
|
||||
_invertColors = MediaQuery.maybeInvertColorsOf(context) ??
|
||||
SemanticsBinding.instance.accessibilityFeatures.invertColors;
|
||||
|
@@ -281,6 +281,12 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet
|
||||
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
|
||||
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 {
|
||||
|
@@ -25,8 +25,8 @@ class _ReaderImagesState extends State<_ReaderImages> {
|
||||
if (inProgress) return;
|
||||
inProgress = true;
|
||||
if (reader.type == ComicType.local ||
|
||||
(LocalManager()
|
||||
.isDownloaded(reader.cid, reader.type, reader.chapter, reader.widget.chapters))) {
|
||||
(LocalManager().isDownloaded(
|
||||
reader.cid, reader.type, reader.chapter, reader.widget.chapters))) {
|
||||
try {
|
||||
var images = await LocalManager()
|
||||
.getImages(reader.cid, reader.type, reader.chapter);
|
||||
@@ -113,6 +113,12 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
|
||||
int get totalPages => (reader.images!.length / reader.imagesPerPage).ceil();
|
||||
|
||||
var imageStates = <State<ComicImage>>{};
|
||||
|
||||
bool isLongPressing = false;
|
||||
|
||||
int fingers = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
reader = context.reader;
|
||||
@@ -142,81 +148,103 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PhotoViewGallery.builder(
|
||||
backgroundDecoration: BoxDecoration(
|
||||
color: context.colorScheme.surface,
|
||||
),
|
||||
reverse: reader.mode == ReaderMode.galleryRightToLeft,
|
||||
scrollDirection: reader.mode == ReaderMode.galleryTopToBottom
|
||||
? Axis.vertical
|
||||
: Axis.horizontal,
|
||||
itemCount: totalPages + 2,
|
||||
builder: (BuildContext context, int index) {
|
||||
if (index == 0 || index == totalPages + 1) {
|
||||
return PhotoViewGalleryPageOptions.customChild(
|
||||
child: const SizedBox(),
|
||||
);
|
||||
} else {
|
||||
int pageIndex = index - 1;
|
||||
int startIndex = pageIndex * reader.imagesPerPage;
|
||||
int endIndex = math.min(
|
||||
startIndex + reader.imagesPerPage, reader.images!.length);
|
||||
List<String> pageImages =
|
||||
reader.images!.sublist(startIndex, endIndex);
|
||||
|
||||
cached[index] = true;
|
||||
cache(index);
|
||||
|
||||
photoViewControllers[index] ??= PhotoViewController();
|
||||
|
||||
if (reader.imagesPerPage == 1) {
|
||||
return PhotoViewGalleryPageOptions(
|
||||
filterQuality: FilterQuality.medium,
|
||||
controller: photoViewControllers[index],
|
||||
imageProvider:
|
||||
_createImageProviderFromKey(pageImages[0], context),
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, error, s, retry) {
|
||||
return NetworkError(message: error.toString(), retry: retry);
|
||||
},
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
return PhotoViewGalleryPageOptions.customChild(
|
||||
controller: photoViewControllers[index],
|
||||
minScale: PhotoViewComputedScale.contained * 1.0,
|
||||
maxScale: PhotoViewComputedScale.covered * 10.0,
|
||||
child: buildPageImages(pageImages),
|
||||
);
|
||||
}
|
||||
},
|
||||
pageController: controller,
|
||||
loadingBuilder: (context, event) => Center(
|
||||
child: SizedBox(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: CircularProgressIndicator(
|
||||
backgroundColor: context.colorScheme.surfaceContainerHigh,
|
||||
value: event == null || event.expectedTotalBytes == null
|
||||
? null
|
||||
: event.cumulativeBytesLoaded / event.expectedTotalBytes!,
|
||||
child: PhotoViewGallery.builder(
|
||||
backgroundDecoration: BoxDecoration(
|
||||
color: context.colorScheme.surface,
|
||||
),
|
||||
reverse: reader.mode == ReaderMode.galleryRightToLeft,
|
||||
scrollDirection: reader.mode == ReaderMode.galleryTopToBottom
|
||||
? Axis.vertical
|
||||
: Axis.horizontal,
|
||||
itemCount: totalPages + 2,
|
||||
builder: (BuildContext context, int index) {
|
||||
if (index == 0 || index == totalPages + 1) {
|
||||
return PhotoViewGalleryPageOptions.customChild(
|
||||
child: const SizedBox(),
|
||||
);
|
||||
} else {
|
||||
int pageIndex = index - 1;
|
||||
int startIndex = pageIndex * reader.imagesPerPage;
|
||||
int endIndex = math.min(
|
||||
startIndex + reader.imagesPerPage, reader.images!.length);
|
||||
List<String> pageImages =
|
||||
reader.images!.sublist(startIndex, endIndex);
|
||||
|
||||
cached[index] = true;
|
||||
cache(index);
|
||||
|
||||
photoViewControllers[index] ??= PhotoViewController();
|
||||
|
||||
if (reader.imagesPerPage == 1) {
|
||||
return PhotoViewGalleryPageOptions(
|
||||
filterQuality: FilterQuality.medium,
|
||||
controller: photoViewControllers[index],
|
||||
imageProvider:
|
||||
_createImageProviderFromKey(pageImages[0], context),
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, error, s, retry) {
|
||||
return NetworkError(message: error.toString(), retry: retry);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return PhotoViewGalleryPageOptions.customChild(
|
||||
controller: photoViewControllers[index],
|
||||
minScale: PhotoViewComputedScale.contained * 1.0,
|
||||
maxScale: PhotoViewComputedScale.covered * 10.0,
|
||||
child: buildPageImages(pageImages),
|
||||
);
|
||||
}
|
||||
},
|
||||
pageController: controller,
|
||||
loadingBuilder: (context, event) => Center(
|
||||
child: SizedBox(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: CircularProgressIndicator(
|
||||
backgroundColor: context.colorScheme.surfaceContainerHigh,
|
||||
value: event == null || event.expectedTotalBytes == null
|
||||
? null
|
||||
: event.cumulativeBytesLoaded / event.expectedTotalBytes!,
|
||||
),
|
||||
),
|
||||
),
|
||||
onPageChanged: (i) {
|
||||
if (i == 0) {
|
||||
if (reader.isFirstChapterOfGroup || !reader.toPrevChapter()) {
|
||||
reader.toPage(1);
|
||||
}
|
||||
} else if (i == totalPages + 1) {
|
||||
if (reader.isLastChapterOfGroup || !reader.toNextChapter()) {
|
||||
reader.toPage(totalPages);
|
||||
}
|
||||
} else {
|
||||
reader.setPage(i);
|
||||
context.readerScaffold.update();
|
||||
}
|
||||
},
|
||||
),
|
||||
onPageChanged: (i) {
|
||||
if (i == 0) {
|
||||
if (reader.isFirstChapterOfGroup || !reader.toPrevChapter()) {
|
||||
reader.toPage(1);
|
||||
}
|
||||
} else if (i == totalPages + 1) {
|
||||
if (reader.isLastChapterOfGroup || !reader.toNextChapter()) {
|
||||
reader.toPage(totalPages);
|
||||
}
|
||||
} else {
|
||||
reader.setPage(i);
|
||||
context.readerScaffold.update();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -226,20 +254,54 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
: Axis.horizontal;
|
||||
|
||||
bool reverse = reader.mode == ReaderMode.galleryRightToLeft;
|
||||
|
||||
List<Widget> imageWidgets = images.map((imageKey) {
|
||||
ImageProvider imageProvider =
|
||||
_createImageProviderFromKey(imageKey, context);
|
||||
return Expanded(
|
||||
child: ComicImage(
|
||||
image: imageProvider,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
if (reverse) {
|
||||
imageWidgets = imageWidgets.reversed.toList();
|
||||
images = images.reversed.toList();
|
||||
}
|
||||
|
||||
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 =
|
||||
_createImageProviderFromKey(imageKey, context);
|
||||
return Expanded(
|
||||
child: ComicImage(
|
||||
image: imageProvider,
|
||||
fit: BoxFit.contain,
|
||||
onInit: (state) => imageStates.add(state),
|
||||
onDispose: (state) => imageStates.remove(state),
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return axis == Axis.vertical
|
||||
@@ -276,7 +338,7 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
|
||||
@override
|
||||
void handleLongPressDown(Offset location) {
|
||||
if (!appdata.settings['enableLongPressToZoom']) {
|
||||
if (!appdata.settings['enableLongPressToZoom'] || fingers != 1) {
|
||||
return;
|
||||
}
|
||||
var photoViewController = photoViewControllers[reader.page]!;
|
||||
@@ -286,18 +348,22 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
target,
|
||||
Offset(size.width / 2 - location.dx, size.height / 2 - location.dy),
|
||||
);
|
||||
isLongPressing = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void handleLongPressUp(Offset location) {
|
||||
if (!appdata.settings['enableLongPressToZoom']) {
|
||||
if (!appdata.settings['enableLongPressToZoom'] || !isLongPressing) {
|
||||
return;
|
||||
}
|
||||
var photoViewController = photoViewControllers[reader.page]!;
|
||||
double target = photoViewController.getInitialScale!.call()!;
|
||||
photoViewController.animateScale?.call(target);
|
||||
isLongPressing = false;
|
||||
}
|
||||
|
||||
Timer? keyRepeatTimer;
|
||||
|
||||
@override
|
||||
void handleKeyEvent(KeyEvent event) {
|
||||
bool? forward;
|
||||
@@ -320,7 +386,11 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
||||
forward = false;
|
||||
}
|
||||
if (event is KeyDownEvent || event is KeyRepeatEvent) {
|
||||
if (event is KeyDownEvent) {
|
||||
if (keyRepeatTimer != null) {
|
||||
keyRepeatTimer!.cancel();
|
||||
keyRepeatTimer = null;
|
||||
}
|
||||
if (forward == true) {
|
||||
controller.nextPage(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
@@ -333,12 +403,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
|
||||
bool handleOnTap(Offset location) {
|
||||
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>{
|
||||
@@ -383,6 +500,8 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
/// To handle the tap event, we need to know if the user was scrolling before the delay.
|
||||
bool delayedIsScrolling = false;
|
||||
|
||||
var imageStates = <State<ComicImage>>{};
|
||||
|
||||
void delayedSetIsScrolling(bool value) {
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 300),
|
||||
@@ -395,6 +514,9 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
bool jumpToNextChapter = false;
|
||||
bool jumpToPrevChapter = false;
|
||||
|
||||
bool isZoomedIn = false;
|
||||
bool isLongPressing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
reader = context.reader;
|
||||
@@ -485,6 +607,16 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
}
|
||||
}
|
||||
|
||||
bool onScaleUpdate([double? scale]) {
|
||||
var isZoomedIn = (scale ?? photoViewController.scale) != 1.0;
|
||||
if (isZoomedIn != this.isZoomedIn) {
|
||||
setState(() {
|
||||
this.isZoomedIn = isZoomedIn;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget widget = ScrollablePositionedList.builder(
|
||||
@@ -506,7 +638,9 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
reverse: reader.mode == ReaderMode.continuousRightToLeft,
|
||||
physics: isCTRLPressed || _isMouseScrolling || disableScroll
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: const BouncingScrollPhysics(),
|
||||
: isZoomedIn
|
||||
? const ClampingScrollPhysics()
|
||||
: const BouncingScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0 || index == reader.maxPage + 1) {
|
||||
return const SizedBox();
|
||||
@@ -529,6 +663,8 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.contain,
|
||||
onInit: (state) => imageStates.add(state),
|
||||
onDispose: (state) => imageStates.remove(state),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -593,18 +729,23 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
if (photoViewController.scale == 1 || fingers != 1) {
|
||||
return;
|
||||
}
|
||||
if (scrollController.offset !=
|
||||
scrollController.position.maxScrollExtent &&
|
||||
scrollController.offset !=
|
||||
scrollController.position.minScrollExtent) {
|
||||
Offset offset;
|
||||
var sp = scrollController.position;
|
||||
if (sp.pixels < sp.minScrollExtent || sp.pixels > sp.maxScrollExtent) {
|
||||
offset = Offset(value.dx, value.dy);
|
||||
} else {
|
||||
if (reader.mode == ReaderMode.continuousTopToBottom) {
|
||||
value = Offset(value.dx, 0);
|
||||
offset = Offset(value.dx, 0);
|
||||
} else {
|
||||
value = Offset(0, value.dy);
|
||||
offset = Offset(0, value.dy);
|
||||
}
|
||||
}
|
||||
if (isLongPressing) {
|
||||
offset += value;
|
||||
}
|
||||
photoViewController.updateMultiple(
|
||||
position: photoViewController.position + value);
|
||||
position: photoViewController.position + offset,
|
||||
);
|
||||
},
|
||||
onPointerSignal: onPointerSignal,
|
||||
child: widget,
|
||||
@@ -676,6 +817,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
maxScale: 2.5,
|
||||
strictScale: true,
|
||||
controller: photoViewController,
|
||||
onScaleUpdate: onScaleUpdate,
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
@@ -731,6 +873,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
target,
|
||||
Offset(size.width / 2 - location.dx, size.height / 2 - location.dy),
|
||||
);
|
||||
onScaleUpdate(target);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -739,11 +882,12 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
return;
|
||||
}
|
||||
double target = photoViewController.getInitialScale!.call()! * 1.75;
|
||||
var size = MediaQuery.of(context).size;
|
||||
photoViewController.animateScale?.call(
|
||||
target,
|
||||
Offset(size.width / 2 - location.dx, size.height / 2 - location.dy),
|
||||
Offset(0, 0),
|
||||
);
|
||||
onScaleUpdate(target);
|
||||
isLongPressing = true;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -753,6 +897,8 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
}
|
||||
double target = photoViewController.getInitialScale!.call()!;
|
||||
photoViewController.animateScale?.call(target);
|
||||
onScaleUpdate(target);
|
||||
isLongPressing = false;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -818,6 +964,24 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
}
|
||||
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(
|
||||
|
@@ -30,6 +30,7 @@ import 'package:venera/foundation/local.dart';
|
||||
import 'package:venera/foundation/log.dart';
|
||||
import 'package:venera/foundation/res.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/ext.dart';
|
||||
import 'package:venera/utils/file_type.dart';
|
||||
@@ -577,4 +578,6 @@ abstract interface class _ImageViewController {
|
||||
|
||||
/// Returns true if the event is handled.
|
||||
bool handleOnTap(Offset location);
|
||||
|
||||
Future<Uint8List?> getImageByOffset(Offset offset);
|
||||
}
|
||||
|
@@ -127,7 +127,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
Positioned.fill(
|
||||
child: widget.child,
|
||||
),
|
||||
buildPageInfoText(),
|
||||
if (appdata.settings['showPageNumberInReader'] == true)
|
||||
buildPageInfoText(),
|
||||
buildStatusInfo(),
|
||||
AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
@@ -161,7 +162,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(top: context.padding.top),
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surface.toOpacity(0.82),
|
||||
color: context.colorScheme.surface.toOpacity(0.92),
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Colors.grey.toOpacity(0.5),
|
||||
@@ -475,7 +476,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
return BlurEffect(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surface.toOpacity(0.82),
|
||||
color: context.colorScheme.surface.toOpacity(0.92),
|
||||
border: isOpen
|
||||
? Border(
|
||||
top: BorderSide(
|
||||
|
@@ -25,8 +25,8 @@ class _ExploreSettingsState extends State<ExploreSettings> {
|
||||
title: "Size of comic tile".tl,
|
||||
settingsIndex: "comicTileScale",
|
||||
interval: 0.05,
|
||||
min: 0.75,
|
||||
max: 1.25,
|
||||
min: 0.5,
|
||||
max: 1.5,
|
||||
).toSliver(),
|
||||
_PopupWindowSetting(
|
||||
title: "Explore Pages".tl,
|
||||
|
@@ -179,6 +179,13 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
min: 1,
|
||||
max: 16,
|
||||
).toSliver(),
|
||||
_SwitchSetting(
|
||||
title: "Show Page Number".tl,
|
||||
settingKey: "showPageNumberInReader",
|
||||
onChanged: () {
|
||||
widget.onChanged?.call("showPageNumberInReader");
|
||||
},
|
||||
).toSliver(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
25
lib/utils/clipboard_image.dart
Normal file
25
lib/utils/clipboard_image.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
Future<void> writeImageToClipboard(Uint8List imageBytes) async {
|
||||
const channel = MethodChannel("venera/clipboard");
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
var image = await instantiateImageCodec(imageBytes);
|
||||
var frame = await image.getNextFrame();
|
||||
var data = await frame.image.toByteData(format: ImageByteFormat.rawRgba);
|
||||
await channel.invokeMethod("writeImageToClipboard", {
|
||||
"width": frame.image.width,
|
||||
"height": frame.image.height,
|
||||
"data": Uint8List.view(data!.buffer)
|
||||
});
|
||||
image.dispose();
|
||||
} else if (Platform.isMacOS) {
|
||||
await channel.invokeMethod("writeImageToClipboard", {
|
||||
"data": imageBytes,
|
||||
});
|
||||
} else {
|
||||
throw UnsupportedError("Clipboard image is not supported on this platform");
|
||||
}
|
||||
}
|
@@ -5,15 +5,45 @@
|
||||
#include <gdk/gdkx.h>
|
||||
#endif
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
struct _MyApplication {
|
||||
GtkApplication parent_instance;
|
||||
char** dart_entrypoint_arguments;
|
||||
FlMethodChannel* clipboard_channel;
|
||||
};
|
||||
|
||||
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.
|
||||
static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
@@ -64,6 +94,14 @@ static void my_application_activate(GApplication* application) {
|
||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(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_grab_focus(GTK_WIDGET(view));
|
||||
@@ -110,6 +148,7 @@ static void my_application_shutdown(GApplication* application) {
|
||||
static void my_application_dispose(GObject* object) {
|
||||
MyApplication* self = MY_APPLICATION(object);
|
||||
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||
g_clear_object(&self->clipboard_channel);
|
||||
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
|
@@ -38,6 +38,31 @@ class AppDelegate: FlutterAppDelegate {
|
||||
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() {
|
||||
|
41
pubspec.lock
41
pubspec.lock
@@ -45,10 +45,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.12.0"
|
||||
version: "2.13.0"
|
||||
battery_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -149,8 +149,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "packages/desktop_webview_window"
|
||||
ref: HEAD
|
||||
resolved-ref: b8f7e94c576acf4ca3dce5b9f8fb8076e5eaca5e
|
||||
ref: "7801fc582ecf5a7351632887891ecf309a7b2583"
|
||||
resolved-ref: "7801fc582ecf5a7351632887891ecf309a7b2583"
|
||||
url: "https://github.com/wgh136/flutter_desktop_webview"
|
||||
source: git
|
||||
version: "0.2.4"
|
||||
@@ -182,10 +182,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -516,10 +516,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
version: "0.20.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -540,10 +540,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.8"
|
||||
version: "10.0.9"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -580,10 +580,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_android
|
||||
sha256: "6763aaf8965f21822624cb2fd3c03d2a8b3791037b5efb0fe4b13e110f5afc92"
|
||||
sha256: "0abe4e72f55c785b28900de52a2522c86baba0988838b5dc22241b072ecccd74"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.46"
|
||||
version: "1.0.48"
|
||||
local_auth_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -725,8 +725,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: d71faf3c75e059d013b0ce9ddaf5ecc1680e2eb6
|
||||
resolved-ref: d71faf3c75e059d013b0ce9ddaf5ecc1680e2eb6
|
||||
ref: a1255d1b5945aad4b7323303ec2ecdf0c90ffc4c
|
||||
resolved-ref: a1255d1b5945aad4b7323303ec2ecdf0c90ffc4c
|
||||
url: "https://github.com/wgh136/photo_view"
|
||||
source: git
|
||||
version: "0.14.0"
|
||||
@@ -757,10 +757,11 @@ packages:
|
||||
rhttp:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: rhttp
|
||||
sha256: "037e9b59a68bb4ba664db1cbb4601e878cf5a2fc1cb3d0a9c58e3776609dec4d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: rhttp
|
||||
ref: e7dca15ca543b5df49f3ada06016e874b79bce36
|
||||
resolved-ref: e7dca15ca543b5df49f3ada06016e874b79bce36
|
||||
url: "https://github.com/wgh136/rhttp"
|
||||
source: git
|
||||
version: "0.11.0"
|
||||
screen_retriever:
|
||||
dependency: transitive
|
||||
@@ -1028,10 +1029,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.3.1"
|
||||
version: "15.0.0"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
11
pubspec.yaml
11
pubspec.yaml
@@ -2,7 +2,7 @@ name: venera
|
||||
description: "A comic app."
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.3.3+133
|
||||
version: 1.3.4+134
|
||||
|
||||
environment:
|
||||
sdk: '>=3.6.0 <4.0.0'
|
||||
@@ -29,7 +29,7 @@ dependencies:
|
||||
photo_view:
|
||||
git:
|
||||
url: https://github.com/wgh136/photo_view
|
||||
ref: d71faf3c75e059d013b0ce9ddaf5ecc1680e2eb6
|
||||
ref: a1255d1b5945aad4b7323303ec2ecdf0c90ffc4c
|
||||
mime: ^2.0.0
|
||||
share_plus: ^10.1.4
|
||||
scrollable_positioned_list:
|
||||
@@ -43,6 +43,7 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/wgh136/flutter_desktop_webview
|
||||
path: packages/desktop_webview_window
|
||||
ref: 7801fc582ecf5a7351632887891ecf309a7b2583
|
||||
flutter_inappwebview:
|
||||
git:
|
||||
url: https://github.com/pichillilorenzo/flutter_inappwebview
|
||||
@@ -57,7 +58,11 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/venera-app/lodepng_flutter
|
||||
ref: ac7d05dde32e8d728102a9ff66e6b55f05d94ba1
|
||||
rhttp: ^0.11.0
|
||||
rhttp:
|
||||
git:
|
||||
url: https://github.com/wgh136/rhttp
|
||||
ref: e7dca15ca543b5df49f3ada06016e874b79bce36
|
||||
path: rhttp
|
||||
webdav_client:
|
||||
git:
|
||||
url: https://github.com/wgh136/webdav_client
|
||||
|
73
windows/build_arm64.iss
Normal file
73
windows/build_arm64.iss
Normal file
@@ -0,0 +1,73 @@
|
||||
; Script generated by the Inno Setup Script Wizard.
|
||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||
|
||||
#define MyAppName "Venera"
|
||||
#define MyAppVersion "1.3.4"
|
||||
#define MyAppPublisher "nyne"
|
||||
#define MyAppURL "https://github.com/venera-app/venera"
|
||||
#define MyAppExeName "venera.exe"
|
||||
#define RootPath "D:\code\venera"
|
||||
|
||||
[Setup]
|
||||
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
|
||||
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||
AppId={{1A39CB64-0A5B-478E-9590-978614C804A8}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#MyAppVersion}
|
||||
;AppVerName={#MyAppName} {#MyAppVersion}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
AppPublisherURL={#MyAppURL}
|
||||
AppSupportURL={#MyAppURL}
|
||||
AppUpdatesURL={#MyAppURL}
|
||||
DefaultDirName={autopf}\{#MyAppName}
|
||||
DisableProgramGroupPage=yes
|
||||
; Uncomment the following line to run in non administrative install mode (install for current user only.)
|
||||
;PrivilegesRequired=lowest
|
||||
PrivilegesRequiredOverridesAllowed=dialog
|
||||
OutputDir={#RootPath}\build\windows
|
||||
OutputBaseFilename=Venera-{#MyAppVersion}-windows-arm64-installer
|
||||
SetupIconFile={#RootPath}\windows\runner\resources\app_icon.ico
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
ArchitecturesInstallIn64BitMode=arm64
|
||||
ArchitecturesAllowed=arm64
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
Name: "chinesesimplified"; MessagesFile: "{#RootPath}\windows\ChineseSimplified.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||
|
||||
[Files]
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\flutter_inappwebview_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\file_selector_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\app_links_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\sqlite3.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\sqlite3_flutter_libs_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\flutter_qjs_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\desktop_webview_window_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\WebView2Loader.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\share_plus_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\battery_plus_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\screen_retriever_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\window_manager_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\local_auth_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\zip_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\rhttp.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\lodepng_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\dynamic_color_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\flutter_7zip.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[Icons]
|
||||
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall
|
43
windows/build_arm64.py
Normal file
43
windows/build_arm64.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import platform
|
||||
import subprocess
|
||||
import os
|
||||
import httpx
|
||||
|
||||
file = open('pubspec.yaml', 'r')
|
||||
content = file.read()
|
||||
file.close()
|
||||
|
||||
subprocess.run(["flutter", "build", "windows"], shell=True)
|
||||
|
||||
if os.path.exists("build/app-windows.zip"):
|
||||
os.remove("build/app-windows.zip")
|
||||
|
||||
version = str.split(str.split(content, 'version: ')[1], '+')[0]
|
||||
|
||||
subprocess.run(["tar", "-a", "-c", "-f", f"build/windows/Venera-{version}-windows-arm64.zip", "-C", "build/windows/x64/runner/Release", "*"]
|
||||
, shell=True)
|
||||
|
||||
issPath = "windows/build_arm64.iss"
|
||||
|
||||
issContent = ""
|
||||
file = open(issPath, 'r')
|
||||
issContent = file.read()
|
||||
newContent = issContent
|
||||
newContent = newContent.replace("{{version}}", version)
|
||||
newContent = newContent.replace("{{root_path}}", os.getcwd())
|
||||
file.close()
|
||||
file = open(issPath, 'w')
|
||||
file.write(newContent)
|
||||
file.close()
|
||||
|
||||
if not os.path.exists("windows/ChineseSimplified.isl"):
|
||||
# download ChineseSimplified.isl
|
||||
url = "https://cdn.jsdelivr.net/gh/kira-96/Inno-Setup-Chinese-Simplified-Translation@latest/ChineseSimplified.isl"
|
||||
response = httpx.get(url)
|
||||
with open('windows/ChineseSimplified.isl', 'wb') as file:
|
||||
file.write(response.content)
|
||||
|
||||
subprocess.run(["iscc", issPath], shell=True)
|
||||
|
||||
with open(issPath, 'w') as file:
|
||||
file.write(issContent)
|
@@ -102,6 +102,47 @@ bool FlutterWindow::OnCreate() {
|
||||
|
||||
channel2.SetStreamHandler(std::move(eventHandler));
|
||||
|
||||
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());
|
||||
|
||||
flutter_controller_->engine()->SetNextFrameCallback([&]() {
|
||||
|
Reference in New Issue
Block a user