Merge pull request #304 from venera-app/v1.3.4-dev

V1.3.4
This commit is contained in:
nyne
2025-03-28 19:23:50 +08:00
committed by GitHub
27 changed files with 739 additions and 186 deletions

View File

@@ -1358,3 +1358,29 @@ let APP = {
}) })
} }
} }
/**
* Set clipboard text
* @param text {string}
* @returns {Promise<void>}
*
* @since 1.3.4
*/
function setClipboard(text) {
return sendMessage({
method: 'setClipboard',
text: text
})
}
/**
* Get clipboard text
* @returns {Promise<string>}
*
* @since 1.3.4
*/
function getClipboard() {
return sendMessage({
method: 'getClipboard'
})
}

View File

@@ -373,6 +373,11 @@
"Paging": "分页", "Paging": "分页",
"Continuous": "连续", "Continuous": "连续",
"Display mode of comic list": "漫画列表的显示模式", "Display mode of comic list": "漫画列表的显示模式",
"Show Page Number": "显示页码",
"Jump to page": "跳转到页面",
"Page": "页面",
"Jump": "跳转",
"Copy Image": "复制图片",
"A valid WebDav directory URL": "有效的WebDav目录URL" "A valid WebDav directory URL": "有效的WebDav目录URL"
}, },
"zh_TW": { "zh_TW": {
@@ -749,6 +754,11 @@
"Paging": "分頁", "Paging": "分頁",
"Continuous": "連續", "Continuous": "連續",
"Display mode of comic list": "漫畫列表的顯示模式", "Display mode of comic list": "漫畫列表的顯示模式",
"Show Page Number": "顯示頁碼",
"Jump to page": "跳轉到頁面",
"Page": "頁面",
"Jump": "跳轉",
"Copy Image": "複製圖片",
"A valid WebDav directory URL": "有效的WebDav目錄URL" "A valid WebDav directory URL": "有效的WebDav目錄URL"
} }
} }

View File

@@ -80,7 +80,7 @@ class _AppbarState extends State<Appbar> {
var content = Container( var content = Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: widget.backgroundColor ?? color: widget.backgroundColor ??
context.colorScheme.surface.toOpacity(0.72), context.colorScheme.surface.toOpacity(0.86),
), ),
height: _kAppBarHeight + context.padding.top, height: _kAppBarHeight + context.padding.top,
child: Row( child: Row(
@@ -231,7 +231,7 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
child: BlurEffect( child: BlurEffect(
blur: 15, blur: 15,
child: Material( child: Material(
color: context.colorScheme.surface.toOpacity(0.72), color: context.colorScheme.surface.toOpacity(0.86),
elevation: 0, elevation: 0,
borderRadius: BorderRadius.circular(radius), borderRadius: BorderRadius.circular(radius),
child: body, child: body,

View File

@@ -61,7 +61,7 @@ class _MenuRoute<T> extends PopupRoute<T> {
child: BlurEffect( child: BlurEffect(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
child: Material( child: Material(
color: context.colorScheme.surface.toOpacity(0.78), color: context.colorScheme.surface.toOpacity(0.92),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
child: Container( child: Container(
width: width, width: width,

View File

@@ -82,7 +82,10 @@ class _WindowFrameState extends State<WindowFrame> {
return; return;
} }
} }
windowManager.close(); windowManager.close().then((_) {
// Make sure the app exits when the window is closed.
exit(0);
});
} }
@override @override
@@ -147,9 +150,10 @@ class _WindowFrameState extends State<WindowFrame> {
onPressed: debug, onPressed: debug,
child: Text('Debug'), child: Text('Debug'),
), ),
if (!App.isMacOS) _WindowButtons( if (!App.isMacOS)
onClose: _onClose, _WindowButtons(
) onClose: _onClose,
)
], ],
), ),
); );
@@ -559,31 +563,31 @@ class _VirtualWindowFrameState extends State<VirtualWindowFrame>
} }
Widget _buildVirtualWindowFrame(BuildContext context) { Widget _buildVirtualWindowFrame(BuildContext context) {
return DecoratedBox( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.transparent, borderRadius: BorderRadius.circular(_isMaximized ? 0 : 8),
border: Border.all( color: Colors.transparent,
color: Theme.of(context).dividerColor, boxShadow: <BoxShadow>[
width: (_isMaximized || _isFullScreen) ? 0 : 1,
),
boxShadow: <BoxShadow>[
if (!_isMaximized && !_isFullScreen)
BoxShadow( BoxShadow(
color: Colors.black.toOpacity(0.1), color: Colors.black.toOpacity(_isFocused ? 0.4 : 0.2),
offset: Offset(0.0, _isFocused ? 4 : 2), offset: Offset(0.0, 2),
blurRadius: 6, blurRadius: 4,
) )
], ],
), ),
child: widget.child, clipBehavior: Clip.antiAlias,
); child: widget.child,
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DragToResizeArea( return DragToResizeArea(
enableResizeEdges: (_isMaximized || _isFullScreen) ? [] : null, enableResizeEdges: (_isMaximized || _isFullScreen) ? [] : null,
child: _buildVirtualWindowFrame(context), child: Padding(
padding: EdgeInsets.all(_isMaximized ? 0 : 4),
child: _buildVirtualWindowFrame(context),
),
); );
} }

View File

@@ -13,7 +13,7 @@ export "widget_utils.dart";
export "context.dart"; export "context.dart";
class _App { class _App {
final version = "1.3.3"; final version = "1.3.4";
bool get isAndroid => Platform.isAndroid; bool get isAndroid => Platform.isAndroid;

View File

@@ -17,17 +17,18 @@ class Appdata with Init {
bool _isSavingData = false; bool _isSavingData = false;
Future<void> saveData([bool sync = true]) async { Future<void> saveData([bool sync = true]) async {
if (_isSavingData) { while (_isSavingData) {
await Future.doWhile(() async { await Future.delayed(const Duration(milliseconds: 20));
await Future.delayed(const Duration(milliseconds: 20));
return _isSavingData;
});
} }
_isSavingData = true; _isSavingData = true;
var data = jsonEncode(toJson()); try {
var file = File(FilePath.join(App.dataPath, 'appdata.json')); var data = jsonEncode(toJson());
await file.writeAsString(data); var file = File(FilePath.join(App.dataPath, 'appdata.json'));
_isSavingData = false; await file.writeAsString(data);
}
finally {
_isSavingData = false;
}
if (sync) { if (sync) {
DataSync().uploadData(); DataSync().uploadData();
} }
@@ -85,9 +86,18 @@ class Appdata with Init {
var implicitData = <String, dynamic>{}; var implicitData = <String, dynamic>{};
void writeImplicitData() { void writeImplicitData() async {
var file = File(FilePath.join(App.dataPath, 'implicitData.json')); while (_isSavingData) {
file.writeAsString(jsonEncode(implicitData)); 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 @override
@@ -109,7 +119,12 @@ class Appdata with Init {
searchHistory = List.from(json['searchHistory']); searchHistory = List.from(json['searchHistory']);
var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json')); var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json'));
if (await implicitDataFile.exists()) { if (await implicitDataFile.exists()) {
implicitData = jsonDecode(await implicitDataFile.readAsString()); try {
implicitData = jsonDecode(await implicitDataFile.readAsString());
}
catch(_) {
// ignore
}
} }
} }
} }
@@ -168,6 +183,7 @@ class Settings with ChangeNotifier {
'followUpdatesFolder': null, 'followUpdatesFolder': null,
'initialPage': '0', 'initialPage': '0',
'comicListDisplayMode': 'paging', // paging, continuous 'comicListDisplayMode': 'paging', // paging, continuous
'showPageNumberInReader': true,
}; };
operator [](String key) { operator [](String key) {

View File

@@ -82,7 +82,7 @@ class ComicSourceParser {
js = js.replaceAll("\r\n", "\n"); js = js.replaceAll("\r\n", "\n");
var line1 = js var line1 = js
.split('\n') .split('\n')
.firstWhereOrNull((element) => element.removeAllBlank.isNotEmpty); .firstWhereOrNull((e) => e.trim().startsWith("class "));
if (line1 == null || if (line1 == null ||
!line1.startsWith("class ") || !line1.startsWith("class ") ||
!line1.contains("extends ComicSource")) { !line1.contains("extends ComicSource")) {

View File

@@ -163,6 +163,13 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
return "${App.locale.languageCode}_${App.locale.countryCode}"; return "${App.locale.languageCode}_${App.locale.countryCode}";
case "getPlatform": case "getPlatform":
return Platform.operatingSystem; return Platform.operatingSystem;
case "setClipboard":
return Clipboard.setData(ClipboardData(text: message["text"]));
case "getClipboard":
return Future.sync(() async {
var res = await Clipboard.getData(Clipboard.kTextPlain);
return res?.text;
});
} }
} }
return null; return null;

View File

@@ -34,13 +34,10 @@ void main(List<String> args) {
await windowManager.setBackgroundColor(Colors.transparent); await windowManager.setBackgroundColor(Colors.transparent);
} }
await windowManager.setMinimumSize(const Size(500, 600)); await windowManager.setMinimumSize(const Size(500, 600));
if (!App.isLinux) { var placement = await WindowPlacement.loadFromFile();
// https://github.com/leanflutter/window_manager/issues/460 await placement.applyToWindow();
var placement = await WindowPlacement.loadFromFile(); await windowManager.show();
await placement.applyToWindow(); WindowPlacement.loop();
await windowManager.show();
WindowPlacement.loop();
}
}); });
} }
}, (error, stack) { }, (error, stack) {
@@ -201,6 +198,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
'dark' => ThemeMode.dark, 'dark' => ThemeMode.dark,
_ => ThemeMode.system _ => ThemeMode.system
}, },
color: Colors.transparent,
localizationsDelegates: [ localizationsDelegates: [
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
@@ -248,6 +246,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
); );
} }
return _SystemUiProvider(Material( return _SystemUiProvider(Material(
color: App.isLinux ? Colors.transparent : null,
child: widget, child: widget,
)); ));
} }

View File

@@ -75,6 +75,8 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
bool isDownloaded = false; bool isDownloaded = false;
bool showFAB = false;
@override @override
void onReadEnd() { void onReadEnd() {
history ??= history ??=
@@ -114,7 +116,15 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
ComicDetails get comic => data!; ComicDetails get comic => data!;
void onScroll() { void onScroll() {
if (scrollController.offset > 100) { var offset = scrollController.position.pixels -
scrollController.position.minScrollExtent;
var showFAB = offset > 0;
if (showFAB != this.showFAB) {
setState(() {
this.showFAB = showFAB;
});
}
if (offset > 100) {
if (!showAppbarTitle) { if (!showAppbarTitle) {
setState(() { setState(() {
showAppbarTitle = true; showAppbarTitle = true;
@@ -133,19 +143,33 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
@override @override
Widget buildContent(BuildContext context, ComicDetails data) { Widget buildContent(BuildContext context, ComicDetails data) {
return SmoothCustomScrollView( return Scaffold(
controller: scrollController, floatingActionButton: showFAB
slivers: [ ? FloatingActionButton(
...buildTitle(), onPressed: () {
buildActions(), scrollController.animateTo(0,
buildDescription(), duration: const Duration(milliseconds: 200),
buildInfo(), curve: Curves.ease);
buildChapters(), },
buildComments(), child: const Icon(Icons.arrow_upward),
buildThumbnails(), )
buildRecommend(), : null,
SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom)), 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
),
],
),
); );
} }

View File

@@ -306,7 +306,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
}); });
} else { } else {
// prevent dirty data // prevent dirty data
var comic = LocalManager().find(c.id, ComicType(c.sourceKey.hashCode))!; var comic = LocalManager().find(c.id, ComicType.fromKey(c.sourceKey))!;
comic.read(); comic.read();
} }
}, },

View File

@@ -24,6 +24,8 @@ class ComicImage extends StatefulWidget {
Map<String, String>? headers, Map<String, String>? headers,
int? cacheWidth, int? cacheWidth,
int? cacheHeight, int? cacheHeight,
this.onInit,
this.onDispose,
}) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, image), }) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, image),
assert(cacheWidth == null || cacheWidth > 0), assert(cacheWidth == null || cacheWidth > 0),
assert(cacheHeight == null || cacheHeight > 0); assert(cacheHeight == null || cacheHeight > 0);
@@ -60,6 +62,10 @@ class ComicImage extends StatefulWidget {
final bool isAntiAlias; final bool isAntiAlias;
final void Function(State<ComicImage> state)? onInit;
final void Function(State<ComicImage> state)? onDispose;
static void clear() => _ComicImageState.clear(); static void clear() => _ComicImageState.clear();
@override @override
@@ -87,6 +93,7 @@ class _ComicImageState extends State<ComicImage> with WidgetsBindingObserver {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
_scrollAwareContext = DisposableBuildContext<State<ComicImage>>(this); _scrollAwareContext = DisposableBuildContext<State<ComicImage>>(this);
widget.onInit?.call(this);
} }
@override @override
@@ -97,6 +104,7 @@ class _ComicImageState extends State<ComicImage> with WidgetsBindingObserver {
_completerHandle?.dispose(); _completerHandle?.dispose();
_scrollAwareContext.dispose(); _scrollAwareContext.dispose();
_replaceImage(info: null); _replaceImage(info: null);
widget.onDispose?.call(this);
super.dispose(); super.dispose();
} }
@@ -136,6 +144,15 @@ class _ComicImageState extends State<ComicImage> with WidgetsBindingObserver {
super.reassemble(); super.reassemble();
} }
bool containsPoint(Offset point) {
if (!mounted) {
return false;
}
var renderBox = context.findRenderObject() as RenderBox;
var localPoint = renderBox.globalToLocal(point);
return renderBox.paintBounds.contains(localPoint);
}
void _updateInvertColors() { void _updateInvertColors() {
_invertColors = MediaQuery.maybeInvertColorsOf(context) ?? _invertColors = MediaQuery.maybeInvertColorsOf(context) ??
SemanticsBinding.instance.accessibilityFeatures.invertColors; SemanticsBinding.instance.accessibilityFeatures.invertColors;

View File

@@ -281,6 +281,12 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet
context.pop(); context.pop();
}, },
), ),
if (App.isDesktop && !reader.isLoading)
MenuEntry(
icon: Icons.copy,
text: "Copy Image".tl,
onClick: () => copyImage(location),
),
], ],
); );
} }
@@ -303,6 +309,16 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet
@override @override
Object? get key => "reader_gesture"; Object? get key => "reader_gesture";
void copyImage(Offset location) async {
var controller = reader._imageViewController;
var image = await controller!.getImageByOffset(location);
if (image != null) {
writeImageToClipboard(image);
} else {
context.showMessage(message: "No Image");
}
}
} }
class _DragListener { class _DragListener {

View File

@@ -25,8 +25,8 @@ class _ReaderImagesState extends State<_ReaderImages> {
if (inProgress) return; if (inProgress) return;
inProgress = true; inProgress = true;
if (reader.type == ComicType.local || if (reader.type == ComicType.local ||
(LocalManager() (LocalManager().isDownloaded(
.isDownloaded(reader.cid, reader.type, reader.chapter, reader.widget.chapters))) { reader.cid, reader.type, reader.chapter, reader.widget.chapters))) {
try { try {
var images = await LocalManager() var images = await LocalManager()
.getImages(reader.cid, reader.type, reader.chapter); .getImages(reader.cid, reader.type, reader.chapter);
@@ -113,6 +113,12 @@ class _GalleryModeState extends State<_GalleryMode>
int get totalPages => (reader.images!.length / reader.imagesPerPage).ceil(); int get totalPages => (reader.images!.length / reader.imagesPerPage).ceil();
var imageStates = <State<ComicImage>>{};
bool isLongPressing = false;
int fingers = 0;
@override @override
void initState() { void initState() {
reader = context.reader; reader = context.reader;
@@ -142,81 +148,103 @@ class _GalleryModeState extends State<_GalleryMode>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PhotoViewGallery.builder( return Listener(
backgroundDecoration: BoxDecoration( onPointerDown: (event) {
color: context.colorScheme.surface, fingers++;
), },
reverse: reader.mode == ReaderMode.galleryRightToLeft, onPointerUp: (event) {
scrollDirection: reader.mode == ReaderMode.galleryTopToBottom fingers--;
? Axis.vertical },
: Axis.horizontal, onPointerCancel: (event) {
itemCount: totalPages + 2, fingers--;
builder: (BuildContext context, int index) { },
if (index == 0 || index == totalPages + 1) { onPointerMove: (event) {
return PhotoViewGalleryPageOptions.customChild( if (isLongPressing) {
child: const SizedBox(), var controller = photoViewControllers[reader.page]!;
); Offset value = event.delta;
} else { if (isLongPressing) {
int pageIndex = index - 1; controller.updateMultiple(
int startIndex = pageIndex * reader.imagesPerPage; position: controller.position + value,
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, child: PhotoViewGallery.builder(
loadingBuilder: (context, event) => Center( backgroundDecoration: BoxDecoration(
child: SizedBox( color: context.colorScheme.surface,
width: 20.0, ),
height: 20.0, reverse: reader.mode == ReaderMode.galleryRightToLeft,
child: CircularProgressIndicator( scrollDirection: reader.mode == ReaderMode.galleryTopToBottom
backgroundColor: context.colorScheme.surfaceContainerHigh, ? Axis.vertical
value: event == null || event.expectedTotalBytes == null : Axis.horizontal,
? null itemCount: totalPages + 2,
: event.cumulativeBytesLoaded / event.expectedTotalBytes!, 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; : Axis.horizontal;
bool reverse = reader.mode == ReaderMode.galleryRightToLeft; 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) { 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 return axis == Axis.vertical
@@ -276,7 +338,7 @@ class _GalleryModeState extends State<_GalleryMode>
@override @override
void handleLongPressDown(Offset location) { void handleLongPressDown(Offset location) {
if (!appdata.settings['enableLongPressToZoom']) { if (!appdata.settings['enableLongPressToZoom'] || fingers != 1) {
return; return;
} }
var photoViewController = photoViewControllers[reader.page]!; var photoViewController = photoViewControllers[reader.page]!;
@@ -286,18 +348,22 @@ class _GalleryModeState extends State<_GalleryMode>
target, target,
Offset(size.width / 2 - location.dx, size.height / 2 - location.dy), Offset(size.width / 2 - location.dx, size.height / 2 - location.dy),
); );
isLongPressing = true;
} }
@override @override
void handleLongPressUp(Offset location) { void handleLongPressUp(Offset location) {
if (!appdata.settings['enableLongPressToZoom']) { if (!appdata.settings['enableLongPressToZoom'] || !isLongPressing) {
return; return;
} }
var photoViewController = photoViewControllers[reader.page]!; var photoViewController = photoViewControllers[reader.page]!;
double target = photoViewController.getInitialScale!.call()!; double target = photoViewController.getInitialScale!.call()!;
photoViewController.animateScale?.call(target); photoViewController.animateScale?.call(target);
isLongPressing = false;
} }
Timer? keyRepeatTimer;
@override @override
void handleKeyEvent(KeyEvent event) { void handleKeyEvent(KeyEvent event) {
bool? forward; bool? forward;
@@ -320,7 +386,11 @@ class _GalleryModeState extends State<_GalleryMode>
event.logicalKey == LogicalKeyboardKey.arrowRight) { event.logicalKey == LogicalKeyboardKey.arrowRight) {
forward = false; forward = false;
} }
if (event is KeyDownEvent || event is KeyRepeatEvent) { if (event is KeyDownEvent) {
if (keyRepeatTimer != null) {
keyRepeatTimer!.cancel();
keyRepeatTimer = null;
}
if (forward == true) { if (forward == true) {
controller.nextPage( controller.nextPage(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
@@ -333,12 +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 @override
bool handleOnTap(Offset location) { bool handleOnTap(Offset location) {
return false; return false;
} }
@override
Future<Uint8List?> getImageByOffset(Offset offset) async {
String? imageKey;
if (reader.imagesPerPage == 1) {
imageKey = reader.images![reader.page - 1];
} else {
for (var imageState in imageStates) {
if ((imageState as _ComicImageState).containsPoint(offset)) {
imageKey = (imageState.widget.image as ReaderImageProvider).imageKey;
}
}
}
if (imageKey == null) return null;
if (imageKey.startsWith("file://")) {
return await File(imageKey.substring(7)).readAsBytes();
} else {
return (await CacheManager().findCache(
"$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))!
.readAsBytes();
}
}
} }
const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{ const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
@@ -383,6 +500,8 @@ class _ContinuousModeState extends State<_ContinuousMode>
/// To handle the tap event, we need to know if the user was scrolling before the delay. /// To handle the tap event, we need to know if the user was scrolling before the delay.
bool delayedIsScrolling = false; bool delayedIsScrolling = false;
var imageStates = <State<ComicImage>>{};
void delayedSetIsScrolling(bool value) { void delayedSetIsScrolling(bool value) {
Future.delayed( Future.delayed(
const Duration(milliseconds: 300), const Duration(milliseconds: 300),
@@ -395,6 +514,9 @@ class _ContinuousModeState extends State<_ContinuousMode>
bool jumpToNextChapter = false; bool jumpToNextChapter = false;
bool jumpToPrevChapter = false; bool jumpToPrevChapter = false;
bool isZoomedIn = false;
bool isLongPressing = false;
@override @override
void initState() { void initState() {
reader = context.reader; reader = context.reader;
@@ -485,6 +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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget widget = ScrollablePositionedList.builder( Widget widget = ScrollablePositionedList.builder(
@@ -506,7 +638,9 @@ class _ContinuousModeState extends State<_ContinuousMode>
reverse: reader.mode == ReaderMode.continuousRightToLeft, reverse: reader.mode == ReaderMode.continuousRightToLeft,
physics: isCTRLPressed || _isMouseScrolling || disableScroll physics: isCTRLPressed || _isMouseScrolling || disableScroll
? const NeverScrollableScrollPhysics() ? const NeverScrollableScrollPhysics()
: const BouncingScrollPhysics(), : isZoomedIn
? const ClampingScrollPhysics()
: const BouncingScrollPhysics(),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == 0 || index == reader.maxPage + 1) { if (index == 0 || index == reader.maxPage + 1) {
return const SizedBox(); return const SizedBox();
@@ -529,6 +663,8 @@ class _ContinuousModeState extends State<_ContinuousMode>
width: width, width: width,
height: height, height: height,
fit: BoxFit.contain, fit: BoxFit.contain,
onInit: (state) => imageStates.add(state),
onDispose: (state) => imageStates.remove(state),
), ),
); );
}, },
@@ -593,18 +729,23 @@ class _ContinuousModeState extends State<_ContinuousMode>
if (photoViewController.scale == 1 || fingers != 1) { if (photoViewController.scale == 1 || fingers != 1) {
return; return;
} }
if (scrollController.offset != Offset offset;
scrollController.position.maxScrollExtent && var sp = scrollController.position;
scrollController.offset != if (sp.pixels < sp.minScrollExtent || sp.pixels > sp.maxScrollExtent) {
scrollController.position.minScrollExtent) { offset = Offset(value.dx, value.dy);
} else {
if (reader.mode == ReaderMode.continuousTopToBottom) { if (reader.mode == ReaderMode.continuousTopToBottom) {
value = Offset(value.dx, 0); offset = Offset(value.dx, 0);
} else { } else {
value = Offset(0, value.dy); offset = Offset(0, value.dy);
} }
} }
if (isLongPressing) {
offset += value;
}
photoViewController.updateMultiple( photoViewController.updateMultiple(
position: photoViewController.position + value); position: photoViewController.position + offset,
);
}, },
onPointerSignal: onPointerSignal, onPointerSignal: onPointerSignal,
child: widget, child: widget,
@@ -676,6 +817,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
maxScale: 2.5, maxScale: 2.5,
strictScale: true, strictScale: true,
controller: photoViewController, controller: photoViewController,
onScaleUpdate: onScaleUpdate,
child: SizedBox( child: SizedBox(
width: width, width: width,
height: height, height: height,
@@ -731,6 +873,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
target, target,
Offset(size.width / 2 - location.dx, size.height / 2 - location.dy), Offset(size.width / 2 - location.dx, size.height / 2 - location.dy),
); );
onScaleUpdate(target);
} }
@override @override
@@ -739,11 +882,12 @@ class _ContinuousModeState extends State<_ContinuousMode>
return; return;
} }
double target = photoViewController.getInitialScale!.call()! * 1.75; double target = photoViewController.getInitialScale!.call()! * 1.75;
var size = MediaQuery.of(context).size;
photoViewController.animateScale?.call( photoViewController.animateScale?.call(
target, target,
Offset(size.width / 2 - location.dx, size.height / 2 - location.dy), Offset(0, 0),
); );
onScaleUpdate(target);
isLongPressing = true;
} }
@override @override
@@ -753,6 +897,8 @@ class _ContinuousModeState extends State<_ContinuousMode>
} }
double target = photoViewController.getInitialScale!.call()!; double target = photoViewController.getInitialScale!.call()!;
photoViewController.animateScale?.call(target); photoViewController.animateScale?.call(target);
onScaleUpdate(target);
isLongPressing = false;
} }
@override @override
@@ -818,6 +964,24 @@ class _ContinuousModeState extends State<_ContinuousMode>
} }
return false; return false;
} }
@override
Future<Uint8List?> getImageByOffset(Offset offset) async {
String? imageKey;
for (var imageState in imageStates) {
if ((imageState as _ComicImageState).containsPoint(offset)) {
imageKey = (imageState.widget.image as ReaderImageProvider).imageKey;
}
}
if (imageKey == null) return null;
if (imageKey.startsWith("file://")) {
return await File(imageKey.substring(7)).readAsBytes();
} else {
return (await CacheManager().findCache(
"$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))!
.readAsBytes();
}
}
} }
ImageProvider _createImageProviderFromKey( ImageProvider _createImageProviderFromKey(

View File

@@ -30,6 +30,7 @@ import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/log.dart'; import 'package:venera/foundation/log.dart';
import 'package:venera/foundation/res.dart'; import 'package:venera/foundation/res.dart';
import 'package:venera/pages/settings/settings_page.dart'; import 'package:venera/pages/settings/settings_page.dart';
import 'package:venera/utils/clipboard_image.dart';
import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/data_sync.dart';
import 'package:venera/utils/ext.dart'; import 'package:venera/utils/ext.dart';
import 'package:venera/utils/file_type.dart'; import 'package:venera/utils/file_type.dart';
@@ -577,4 +578,6 @@ abstract interface class _ImageViewController {
/// Returns true if the event is handled. /// Returns true if the event is handled.
bool handleOnTap(Offset location); bool handleOnTap(Offset location);
Future<Uint8List?> getImageByOffset(Offset offset);
} }

View File

@@ -127,7 +127,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
Positioned.fill( Positioned.fill(
child: widget.child, child: widget.child,
), ),
buildPageInfoText(), if (appdata.settings['showPageNumberInReader'] == true)
buildPageInfoText(),
buildStatusInfo(), buildStatusInfo(),
AnimatedPositioned( AnimatedPositioned(
duration: const Duration(milliseconds: 180), duration: const Duration(milliseconds: 180),
@@ -161,7 +162,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
child: Container( child: Container(
padding: EdgeInsets.only(top: context.padding.top), padding: EdgeInsets.only(top: context.padding.top),
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.colorScheme.surface.toOpacity(0.82), color: context.colorScheme.surface.toOpacity(0.92),
border: Border( border: Border(
bottom: BorderSide( bottom: BorderSide(
color: Colors.grey.toOpacity(0.5), color: Colors.grey.toOpacity(0.5),
@@ -475,7 +476,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
return BlurEffect( return BlurEffect(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.colorScheme.surface.toOpacity(0.82), color: context.colorScheme.surface.toOpacity(0.92),
border: isOpen border: isOpen
? Border( ? Border(
top: BorderSide( top: BorderSide(

View File

@@ -25,8 +25,8 @@ class _ExploreSettingsState extends State<ExploreSettings> {
title: "Size of comic tile".tl, title: "Size of comic tile".tl,
settingsIndex: "comicTileScale", settingsIndex: "comicTileScale",
interval: 0.05, interval: 0.05,
min: 0.75, min: 0.5,
max: 1.25, max: 1.5,
).toSliver(), ).toSliver(),
_PopupWindowSetting( _PopupWindowSetting(
title: "Explore Pages".tl, title: "Explore Pages".tl,

View File

@@ -179,6 +179,13 @@ class _ReaderSettingsState extends State<ReaderSettings> {
min: 1, min: 1,
max: 16, max: 16,
).toSliver(), ).toSliver(),
_SwitchSetting(
title: "Show Page Number".tl,
settingKey: "showPageNumberInReader",
onChanged: () {
widget.onChanged?.call("showPageNumberInReader");
},
).toSliver(),
], ],
); );
} }

View File

@@ -0,0 +1,25 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/services.dart';
Future<void> writeImageToClipboard(Uint8List imageBytes) async {
const channel = MethodChannel("venera/clipboard");
if (Platform.isWindows || Platform.isLinux) {
var image = await instantiateImageCodec(imageBytes);
var frame = await image.getNextFrame();
var data = await frame.image.toByteData(format: ImageByteFormat.rawRgba);
await channel.invokeMethod("writeImageToClipboard", {
"width": frame.image.width,
"height": frame.image.height,
"data": Uint8List.view(data!.buffer)
});
image.dispose();
} else if (Platform.isMacOS) {
await channel.invokeMethod("writeImageToClipboard", {
"data": imageBytes,
});
} else {
throw UnsupportedError("Clipboard image is not supported on this platform");
}
}

View File

@@ -5,15 +5,45 @@
#include <gdk/gdkx.h> #include <gdk/gdkx.h>
#endif #endif
#include <iostream>
#include "flutter/generated_plugin_registrant.h" #include "flutter/generated_plugin_registrant.h"
struct _MyApplication { struct _MyApplication {
GtkApplication parent_instance; GtkApplication parent_instance;
char** dart_entrypoint_arguments; char** dart_entrypoint_arguments;
FlMethodChannel* clipboard_channel;
}; };
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
static void handle_clipboard_call(FlMethodChannel* channel, FlMethodCall* call, gpointer user_data) {
if (strcmp(fl_method_call_get_name(call), "writeImageToClipboard") == 0) {
const auto args = fl_method_call_get_args(call);
const auto width = fl_value_get_int(fl_value_get_map_value(args, 0));
const auto height = fl_value_get_int(fl_value_get_map_value(args, 1));
const auto data = fl_value_get_uint8_list(fl_value_get_map_value(args, 2));
std::cout << width << " " << height << " " << data[0] << " " << data[1] << std::endl;
GBytes* bytes = g_bytes_new(data, width * height * 4);
GdkDisplay* display = gdk_display_get_default();
GtkClipboard* clipboard = gtk_clipboard_get_default(display);
GdkPixbuf* pixbuf = gdk_pixbuf_new_from_bytes(
bytes,
GDK_COLORSPACE_RGB,
true,
8,
width,
height,
width * 4
);
gtk_clipboard_set_image(clipboard, pixbuf);
fl_method_call_respond_success(call, fl_value_new_string("Ok"), nullptr);
}
}
// Implements GApplication::activate. // Implements GApplication::activate.
static void my_application_activate(GApplication* application) { static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application); MyApplication* self = MY_APPLICATION(application);
@@ -48,6 +78,12 @@ static void my_application_activate(GApplication* application) {
} }
gtk_window_set_default_size(window, 1280, 720); gtk_window_set_default_size(window, 1280, 720);
GdkVisual* visual;
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)); gtk_widget_show(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new(); 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)); 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)); 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) { static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object); MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
g_clear_object(&self->clipboard_channel);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object); G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
} }

View File

@@ -38,6 +38,31 @@ class AppDelegate: FlutterAppDelegate {
result(FlutterMethodNotImplemented) result(FlutterMethodNotImplemented)
} }
} }
let clipboardChannel = FlutterMethodChannel(name: "venera/clipboard", binaryMessenger: controller.engine.binaryMessenger)
clipboardChannel.setMethodCallHandler { (call, result) in
switch call.method {
case "writeImageToClipboard":
guard let arguments = call.arguments as? [String: Any],
let data = arguments["data"] as? FlutterStandardTypedData else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Invalid arguments", details: nil))
return
}
guard let image = NSImage(data: data.data) else {
result(FlutterError(code: "INVALID_IMAGE", message: "Could not create image from data", details: nil))
return
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.writeObjects([image])
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
} }
func getDirectoryPath() { func getDirectoryPath() {

View File

@@ -45,10 +45,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: async name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.12.0" version: "2.13.0"
battery_plus: battery_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -149,8 +149,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "packages/desktop_webview_window" path: "packages/desktop_webview_window"
ref: HEAD ref: "7801fc582ecf5a7351632887891ecf309a7b2583"
resolved-ref: b8f7e94c576acf4ca3dce5b9f8fb8076e5eaca5e resolved-ref: "7801fc582ecf5a7351632887891ecf309a7b2583"
url: "https://github.com/wgh136/flutter_desktop_webview" url: "https://github.com/wgh136/flutter_desktop_webview"
source: git source: git
version: "0.2.4" version: "0.2.4"
@@ -182,10 +182,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: fake_async name: fake_async
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" version: "1.3.3"
ffi: ffi:
dependency: transitive dependency: transitive
description: description:
@@ -516,10 +516,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: intl name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.19.0" version: "0.20.2"
io: io:
dependency: transitive dependency: transitive
description: description:
@@ -540,10 +540,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.8" version: "10.0.9"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
@@ -580,10 +580,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: local_auth_android name: local_auth_android
sha256: "6763aaf8965f21822624cb2fd3c03d2a8b3791037b5efb0fe4b13e110f5afc92" sha256: "0abe4e72f55c785b28900de52a2522c86baba0988838b5dc22241b072ecccd74"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.46" version: "1.0.48"
local_auth_darwin: local_auth_darwin:
dependency: transitive dependency: transitive
description: description:
@@ -725,8 +725,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: d71faf3c75e059d013b0ce9ddaf5ecc1680e2eb6 ref: a1255d1b5945aad4b7323303ec2ecdf0c90ffc4c
resolved-ref: d71faf3c75e059d013b0ce9ddaf5ecc1680e2eb6 resolved-ref: a1255d1b5945aad4b7323303ec2ecdf0c90ffc4c
url: "https://github.com/wgh136/photo_view" url: "https://github.com/wgh136/photo_view"
source: git source: git
version: "0.14.0" version: "0.14.0"
@@ -757,10 +757,11 @@ packages:
rhttp: rhttp:
dependency: "direct main" dependency: "direct main"
description: description:
name: rhttp path: rhttp
sha256: "037e9b59a68bb4ba664db1cbb4601e878cf5a2fc1cb3d0a9c58e3776609dec4d" ref: e7dca15ca543b5df49f3ada06016e874b79bce36
url: "https://pub.dev" resolved-ref: e7dca15ca543b5df49f3ada06016e874b79bce36
source: hosted url: "https://github.com/wgh136/rhttp"
source: git
version: "0.11.0" version: "0.11.0"
screen_retriever: screen_retriever:
dependency: transitive dependency: transitive
@@ -1028,10 +1029,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.3.1" version: "15.0.0"
web: web:
dependency: transitive dependency: transitive
description: description:

View File

@@ -2,7 +2,7 @@ name: venera
description: "A comic app." description: "A comic app."
publish_to: 'none' publish_to: 'none'
version: 1.3.3+133 version: 1.3.4+134
environment: environment:
sdk: '>=3.6.0 <4.0.0' sdk: '>=3.6.0 <4.0.0'
@@ -29,7 +29,7 @@ dependencies:
photo_view: photo_view:
git: git:
url: https://github.com/wgh136/photo_view url: https://github.com/wgh136/photo_view
ref: d71faf3c75e059d013b0ce9ddaf5ecc1680e2eb6 ref: a1255d1b5945aad4b7323303ec2ecdf0c90ffc4c
mime: ^2.0.0 mime: ^2.0.0
share_plus: ^10.1.4 share_plus: ^10.1.4
scrollable_positioned_list: scrollable_positioned_list:
@@ -43,6 +43,7 @@ dependencies:
git: git:
url: https://github.com/wgh136/flutter_desktop_webview url: https://github.com/wgh136/flutter_desktop_webview
path: packages/desktop_webview_window path: packages/desktop_webview_window
ref: 7801fc582ecf5a7351632887891ecf309a7b2583
flutter_inappwebview: flutter_inappwebview:
git: git:
url: https://github.com/pichillilorenzo/flutter_inappwebview url: https://github.com/pichillilorenzo/flutter_inappwebview
@@ -57,7 +58,11 @@ dependencies:
git: git:
url: https://github.com/venera-app/lodepng_flutter url: https://github.com/venera-app/lodepng_flutter
ref: ac7d05dde32e8d728102a9ff66e6b55f05d94ba1 ref: ac7d05dde32e8d728102a9ff66e6b55f05d94ba1
rhttp: ^0.11.0 rhttp:
git:
url: https://github.com/wgh136/rhttp
ref: e7dca15ca543b5df49f3ada06016e874b79bce36
path: rhttp
webdav_client: webdav_client:
git: git:
url: https://github.com/wgh136/webdav_client url: https://github.com/wgh136/webdav_client

73
windows/build_arm64.iss Normal file
View File

@@ -0,0 +1,73 @@
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "Venera"
#define MyAppVersion "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
View File

@@ -0,0 +1,43 @@
import platform
import subprocess
import os
import httpx
file = open('pubspec.yaml', 'r')
content = file.read()
file.close()
subprocess.run(["flutter", "build", "windows"], shell=True)
if os.path.exists("build/app-windows.zip"):
os.remove("build/app-windows.zip")
version = str.split(str.split(content, 'version: ')[1], '+')[0]
subprocess.run(["tar", "-a", "-c", "-f", f"build/windows/Venera-{version}-windows-arm64.zip", "-C", "build/windows/x64/runner/Release", "*"]
, shell=True)
issPath = "windows/build_arm64.iss"
issContent = ""
file = open(issPath, 'r')
issContent = file.read()
newContent = issContent
newContent = newContent.replace("{{version}}", version)
newContent = newContent.replace("{{root_path}}", os.getcwd())
file.close()
file = open(issPath, 'w')
file.write(newContent)
file.close()
if not os.path.exists("windows/ChineseSimplified.isl"):
# download ChineseSimplified.isl
url = "https://cdn.jsdelivr.net/gh/kira-96/Inno-Setup-Chinese-Simplified-Translation@latest/ChineseSimplified.isl"
response = httpx.get(url)
with open('windows/ChineseSimplified.isl', 'wb') as file:
file.write(response.content)
subprocess.run(["iscc", issPath], shell=True)
with open(issPath, 'w') as file:
file.write(issContent)

View File

@@ -102,6 +102,47 @@ bool FlutterWindow::OnCreate() {
channel2.SetStreamHandler(std::move(eventHandler)); channel2.SetStreamHandler(std::move(eventHandler));
const flutter::MethodChannel<> channel3(
flutter_controller_->engine()->messenger(), "venera/clipboard",
&flutter::StandardMethodCodec::GetInstance()
);
channel3.SetMethodCallHandler(
[](const flutter::MethodCall<>& call,const std::unique_ptr<flutter::MethodResult<>>& result) {
if(call.method_name() == "writeImageToClipboard"){
flutter::EncodableMap arguments = std::get<flutter::EncodableMap>(*call.arguments());
std::vector<uint8_t> data = std::get<std::vector<uint8_t>>(arguments["data"]);
std::int32_t width = std::get<std::int32_t>(arguments["width"]);
std::int32_t height = std::get<std::int32_t>(arguments["height"]);
// convert rgba to bgra
for (int i = 0; i < data.size()/4; i++) {
uint8_t temp = data[i * 4];
data[i * 4] = data[i * 4 + 2];
data[i * 4 + 2] = temp;
}
auto bitmap = CreateBitmap((int)width, (int)height, 1, 32, data.data());
if (!bitmap) {
result->Error("0", "Invalid Image Data");
return;
}
if (OpenClipboard(NULL))
{
EmptyClipboard();
SetClipboardData(CF_BITMAP, bitmap);
CloseClipboard();
result->Success();
}
else {
result->Error("Failed to open clipboard");
}
DeleteObject(bitmap);
}
});
SetChildContent(flutter_controller_->view()->GetNativeWindow()); SetChildContent(flutter_controller_->view()->GetNativeWindow());
flutter_controller_->engine()->SetNextFrameCallback([&]() { flutter_controller_->engine()->SetNextFrameCallback([&]() {