diff --git a/assets/init.js b/assets/init.js index 553f404..7a016e3 100644 --- a/assets/init.js +++ b/assets/init.js @@ -1357,4 +1357,30 @@ let APP = { method: 'getPlatform' }) } +} + +/** + * Set clipboard text + * @param text {string} + * @returns {Promise} + * + * @since 1.3.4 + */ +function setClipboard(text) { + return sendMessage({ + method: 'setClipboard', + text: text + }) +} + +/** + * Get clipboard text + * @returns {Promise} + * + * @since 1.3.4 + */ +function getClipboard() { + return sendMessage({ + method: 'getClipboard' + }) } \ No newline at end of file diff --git a/assets/translation.json b/assets/translation.json index 1aa4952..add7a5a 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -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" } } diff --git a/lib/components/appbar.dart b/lib/components/appbar.dart index 5be9e44..1dd2674 100644 --- a/lib/components/appbar.dart +++ b/lib/components/appbar.dart @@ -80,7 +80,7 @@ class _AppbarState extends State { 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, diff --git a/lib/components/menu.dart b/lib/components/menu.dart index 7293625..98a17bf 100644 --- a/lib/components/menu.dart +++ b/lib/components/menu.dart @@ -61,7 +61,7 @@ class _MenuRoute extends PopupRoute { 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, diff --git a/lib/components/window_frame.dart b/lib/components/window_frame.dart index 6525970..75c6f5b 100644 --- a/lib/components/window_frame.dart +++ b/lib/components/window_frame.dart @@ -82,7 +82,10 @@ class _WindowFrameState extends State { return; } } - windowManager.close(); + windowManager.close().then((_) { + // Make sure the app exits when the window is closed. + exit(0); + }); } @override @@ -147,9 +150,10 @@ class _WindowFrameState extends State { onPressed: debug, child: Text('Debug'), ), - if (!App.isMacOS) _WindowButtons( - onClose: _onClose, - ) + if (!App.isMacOS) + _WindowButtons( + onClose: _onClose, + ) ], ), ); @@ -559,31 +563,31 @@ class _VirtualWindowFrameState extends State } Widget _buildVirtualWindowFrame(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - color: Colors.transparent, - border: Border.all( - color: Theme.of(context).dividerColor, - width: (_isMaximized || _isFullScreen) ? 0 : 1, - ), - boxShadow: [ - if (!_isMaximized && !_isFullScreen) + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(_isMaximized ? 0 : 8), + color: Colors.transparent, + boxShadow: [ BoxShadow( - color: Colors.black.toOpacity(0.1), - offset: Offset(0.0, _isFocused ? 4 : 2), - blurRadius: 6, - ) - ], - ), - child: widget.child, - ); + color: Colors.black.toOpacity(_isFocused ? 0.4 : 0.2), + offset: Offset(0.0, 2), + blurRadius: 4, + ) + ], + ), + clipBehavior: Clip.antiAlias, + child: widget.child, + ); } @override Widget build(BuildContext context) { return DragToResizeArea( enableResizeEdges: (_isMaximized || _isFullScreen) ? [] : null, - child: _buildVirtualWindowFrame(context), + child: Padding( + padding: EdgeInsets.all(_isMaximized ? 0 : 4), + child: _buildVirtualWindowFrame(context), + ), ); } diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index 9c472b7..d48286e 100644 --- a/lib/foundation/app.dart +++ b/lib/foundation/app.dart @@ -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; diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index 6959be4..b4e4073 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -17,17 +17,18 @@ class Appdata with Init { bool _isSavingData = false; Future 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 = {}; - 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) { diff --git a/lib/foundation/comic_source/parser.dart b/lib/foundation/comic_source/parser.dart index 08d399a..e9ed69a 100644 --- a/lib/foundation/comic_source/parser.dart +++ b/lib/foundation/comic_source/parser.dart @@ -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")) { diff --git a/lib/foundation/js_engine.dart b/lib/foundation/js_engine.dart index 3772491..791215b 100644 --- a/lib/foundation/js_engine.dart +++ b/lib/foundation/js_engine.dart @@ -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; diff --git a/lib/main.dart b/lib/main.dart index 0ab3875..463a3b0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -34,13 +34,10 @@ void main(List args) { await windowManager.setBackgroundColor(Colors.transparent); } await windowManager.setMinimumSize(const Size(500, 600)); - if (!App.isLinux) { - // https://github.com/leanflutter/window_manager/issues/460 - var placement = await WindowPlacement.loadFromFile(); - await placement.applyToWindow(); - await windowManager.show(); - WindowPlacement.loop(); - } + var placement = await WindowPlacement.loadFromFile(); + await placement.applyToWindow(); + await windowManager.show(); + WindowPlacement.loop(); }); } }, (error, stack) { @@ -201,6 +198,7 @@ class _MyAppState extends State with WidgetsBindingObserver { 'dark' => ThemeMode.dark, _ => ThemeMode.system }, + color: Colors.transparent, localizationsDelegates: [ GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, @@ -248,6 +246,7 @@ class _MyAppState extends State with WidgetsBindingObserver { ); } return _SystemUiProvider(Material( + color: App.isLinux ? Colors.transparent : null, child: widget, )); } diff --git a/lib/pages/comic_details_page/comic_page.dart b/lib/pages/comic_details_page/comic_page.dart index 5447a31..99745a8 100644 --- a/lib/pages/comic_details_page/comic_page.dart +++ b/lib/pages/comic_details_page/comic_page.dart @@ -75,6 +75,8 @@ class _ComicPageState extends LoadingState bool isDownloaded = false; + bool showFAB = false; + @override void onReadEnd() { history ??= @@ -114,7 +116,15 @@ class _ComicPageState extends LoadingState 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 @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 + ), + ], + ), ); } diff --git a/lib/pages/local_comics_page.dart b/lib/pages/local_comics_page.dart index 2b45c63..e30c64a 100644 --- a/lib/pages/local_comics_page.dart +++ b/lib/pages/local_comics_page.dart @@ -306,7 +306,7 @@ class _LocalComicsPageState extends State { }); } 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(); } }, diff --git a/lib/pages/reader/comic_image.dart b/lib/pages/reader/comic_image.dart index 7f6d5e3..ff8a2da 100644 --- a/lib/pages/reader/comic_image.dart +++ b/lib/pages/reader/comic_image.dart @@ -24,6 +24,8 @@ class ComicImage extends StatefulWidget { Map? 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 state)? onInit; + + final void Function(State state)? onDispose; + static void clear() => _ComicImageState.clear(); @override @@ -87,6 +93,7 @@ class _ComicImageState extends State with WidgetsBindingObserver { super.initState(); WidgetsBinding.instance.addObserver(this); _scrollAwareContext = DisposableBuildContext>(this); + widget.onInit?.call(this); } @override @@ -97,6 +104,7 @@ class _ComicImageState extends State with WidgetsBindingObserver { _completerHandle?.dispose(); _scrollAwareContext.dispose(); _replaceImage(info: null); + widget.onDispose?.call(this); super.dispose(); } @@ -136,6 +144,15 @@ class _ComicImageState extends State 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; diff --git a/lib/pages/reader/gesture.dart b/lib/pages/reader/gesture.dart index 0b3928c..04ef114 100644 --- a/lib/pages/reader/gesture.dart +++ b/lib/pages/reader/gesture.dart @@ -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 { diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index 00018f1..e2693e4 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -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 = >{}; + + 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 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 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 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 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 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 _kTouchLikeDeviceTypes = { @@ -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 = >{}; + 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 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( diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index cb75c04..5321f10 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -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 getImageByOffset(Offset offset); } diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 41ef5e6..6f8007a 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -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( diff --git a/lib/pages/settings/explore_settings.dart b/lib/pages/settings/explore_settings.dart index da8e2bf..61b068c 100644 --- a/lib/pages/settings/explore_settings.dart +++ b/lib/pages/settings/explore_settings.dart @@ -25,8 +25,8 @@ class _ExploreSettingsState extends State { 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, diff --git a/lib/pages/settings/reader.dart b/lib/pages/settings/reader.dart index 97e19fd..fc282ed 100644 --- a/lib/pages/settings/reader.dart +++ b/lib/pages/settings/reader.dart @@ -179,6 +179,13 @@ class _ReaderSettingsState extends State { min: 1, max: 16, ).toSliver(), + _SwitchSetting( + title: "Show Page Number".tl, + settingKey: "showPageNumberInReader", + onChanged: () { + widget.onChanged?.call("showPageNumberInReader"); + }, + ).toSliver(), ], ); } diff --git a/lib/utils/clipboard_image.dart b/lib/utils/clipboard_image.dart new file mode 100644 index 0000000..4201cec --- /dev/null +++ b/lib/utils/clipboard_image.dart @@ -0,0 +1,25 @@ +import 'dart:io'; +import 'dart:ui'; + +import 'package:flutter/services.dart'; + +Future 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"); + } +} diff --git a/linux/my_application.cc b/linux/my_application.cc index 774c9c2..274826b 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -5,15 +5,45 @@ #include #endif +#include + #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); @@ -48,6 +78,12 @@ static void my_application_activate(GApplication* application) { } gtk_window_set_default_size(window, 1280, 720); + GdkVisual* visual; + gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE); + visual = gdk_screen_get_rgba_visual(screen); + if (visual != NULL && gdk_screen_is_composited(screen)) { + gtk_widget_set_visual(GTK_WIDGET(window), visual); + } gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); @@ -59,6 +95,15 @@ static void my_application_activate(GApplication* application) { 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)); } @@ -103,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); } diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index 906f330..d3e795e 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -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() { diff --git a/pubspec.lock b/pubspec.lock index 620d6c1..ecbe6fc 100644 --- a/pubspec.lock +++ b/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: diff --git a/pubspec.yaml b/pubspec.yaml index ae37b56..d7ee5af 100644 --- a/pubspec.yaml +++ b/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 diff --git a/windows/build_arm64.iss b/windows/build_arm64.iss new file mode 100644 index 0000000..db4fc70 --- /dev/null +++ b/windows/build_arm64.iss @@ -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 diff --git a/windows/build_arm64.py b/windows/build_arm64.py new file mode 100644 index 0000000..36c8c34 --- /dev/null +++ b/windows/build_arm64.py @@ -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) \ No newline at end of file diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index f22f2f4..f379ea8 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -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>& result) { + if(call.method_name() == "writeImageToClipboard"){ + flutter::EncodableMap arguments = std::get(*call.arguments()); + std::vector data = std::get>(arguments["data"]); + std::int32_t width = std::get(arguments["width"]); + std::int32_t height = std::get(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([&]() {