mirror of
https://github.com/venera-app/venera.git
synced 2025-09-26 23:47:23 +00:00
@@ -1357,4 +1357,30 @@ let APP = {
|
||||
method: 'getPlatform'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set clipboard text
|
||||
* @param text {string}
|
||||
* @returns {Promise<void>}
|
||||
*
|
||||
* @since 1.3.4
|
||||
*/
|
||||
function setClipboard(text) {
|
||||
return sendMessage({
|
||||
method: 'setClipboard',
|
||||
text: text
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clipboard text
|
||||
* @returns {Promise<string>}
|
||||
*
|
||||
* @since 1.3.4
|
||||
*/
|
||||
function getClipboard() {
|
||||
return sendMessage({
|
||||
method: 'getClipboard'
|
||||
})
|
||||
}
|
@@ -373,6 +373,11 @@
|
||||
"Paging": "分页",
|
||||
"Continuous": "连续",
|
||||
"Display mode of comic list": "漫画列表的显示模式",
|
||||
"Show Page Number": "显示页码",
|
||||
"Jump to page": "跳转到页面",
|
||||
"Page": "页面",
|
||||
"Jump": "跳转",
|
||||
"Copy Image": "复制图片",
|
||||
"A valid WebDav directory URL": "有效的WebDav目录URL"
|
||||
},
|
||||
"zh_TW": {
|
||||
@@ -749,6 +754,11 @@
|
||||
"Paging": "分頁",
|
||||
"Continuous": "連續",
|
||||
"Display mode of comic list": "漫畫列表的顯示模式",
|
||||
"Show Page Number": "顯示頁碼",
|
||||
"Jump to page": "跳轉到頁面",
|
||||
"Page": "頁面",
|
||||
"Jump": "跳轉",
|
||||
"Copy Image": "複製圖片",
|
||||
"A valid WebDav directory URL": "有效的WebDav目錄URL"
|
||||
}
|
||||
}
|
||||
|
@@ -80,7 +80,7 @@ class _AppbarState extends State<Appbar> {
|
||||
var content = Container(
|
||||
decoration: BoxDecoration(
|
||||
color: widget.backgroundColor ??
|
||||
context.colorScheme.surface.toOpacity(0.72),
|
||||
context.colorScheme.surface.toOpacity(0.86),
|
||||
),
|
||||
height: _kAppBarHeight + context.padding.top,
|
||||
child: Row(
|
||||
@@ -231,7 +231,7 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
||||
child: BlurEffect(
|
||||
blur: 15,
|
||||
child: Material(
|
||||
color: context.colorScheme.surface.toOpacity(0.72),
|
||||
color: context.colorScheme.surface.toOpacity(0.86),
|
||||
elevation: 0,
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
child: body,
|
||||
|
@@ -61,7 +61,7 @@ class _MenuRoute<T> extends PopupRoute<T> {
|
||||
child: BlurEffect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Material(
|
||||
color: context.colorScheme.surface.toOpacity(0.78),
|
||||
color: context.colorScheme.surface.toOpacity(0.92),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
width: width,
|
||||
|
@@ -82,7 +82,10 @@ class _WindowFrameState extends State<WindowFrame> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
windowManager.close();
|
||||
windowManager.close().then((_) {
|
||||
// Make sure the app exits when the window is closed.
|
||||
exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -147,9 +150,10 @@ class _WindowFrameState extends State<WindowFrame> {
|
||||
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<VirtualWindowFrame>
|
||||
}
|
||||
|
||||
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: <BoxShadow>[
|
||||
if (!_isMaximized && !_isFullScreen)
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(_isMaximized ? 0 : 8),
|
||||
color: Colors.transparent,
|
||||
boxShadow: <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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -13,7 +13,7 @@ export "widget_utils.dart";
|
||||
export "context.dart";
|
||||
|
||||
class _App {
|
||||
final version = "1.3.3";
|
||||
final version = "1.3.4";
|
||||
|
||||
bool get isAndroid => Platform.isAndroid;
|
||||
|
||||
|
@@ -17,17 +17,18 @@ class Appdata with Init {
|
||||
bool _isSavingData = false;
|
||||
|
||||
Future<void> saveData([bool sync = true]) async {
|
||||
if (_isSavingData) {
|
||||
await Future.doWhile(() async {
|
||||
await Future.delayed(const Duration(milliseconds: 20));
|
||||
return _isSavingData;
|
||||
});
|
||||
while (_isSavingData) {
|
||||
await Future.delayed(const Duration(milliseconds: 20));
|
||||
}
|
||||
_isSavingData = true;
|
||||
var data = jsonEncode(toJson());
|
||||
var file = File(FilePath.join(App.dataPath, 'appdata.json'));
|
||||
await file.writeAsString(data);
|
||||
_isSavingData = false;
|
||||
try {
|
||||
var data = jsonEncode(toJson());
|
||||
var file = File(FilePath.join(App.dataPath, 'appdata.json'));
|
||||
await file.writeAsString(data);
|
||||
}
|
||||
finally {
|
||||
_isSavingData = false;
|
||||
}
|
||||
if (sync) {
|
||||
DataSync().uploadData();
|
||||
}
|
||||
@@ -85,9 +86,18 @@ class Appdata with Init {
|
||||
|
||||
var implicitData = <String, dynamic>{};
|
||||
|
||||
void writeImplicitData() {
|
||||
var file = File(FilePath.join(App.dataPath, 'implicitData.json'));
|
||||
file.writeAsString(jsonEncode(implicitData));
|
||||
void writeImplicitData() async {
|
||||
while (_isSavingData) {
|
||||
await Future.delayed(const Duration(milliseconds: 20));
|
||||
}
|
||||
_isSavingData = true;
|
||||
try {
|
||||
var file = File(FilePath.join(App.dataPath, 'implicitData.json'));
|
||||
await file.writeAsString(jsonEncode(implicitData));
|
||||
}
|
||||
finally {
|
||||
_isSavingData = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -109,7 +119,12 @@ class Appdata with Init {
|
||||
searchHistory = List.from(json['searchHistory']);
|
||||
var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json'));
|
||||
if (await implicitDataFile.exists()) {
|
||||
implicitData = jsonDecode(await implicitDataFile.readAsString());
|
||||
try {
|
||||
implicitData = jsonDecode(await implicitDataFile.readAsString());
|
||||
}
|
||||
catch(_) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,6 +183,7 @@ class Settings with ChangeNotifier {
|
||||
'followUpdatesFolder': null,
|
||||
'initialPage': '0',
|
||||
'comicListDisplayMode': 'paging', // paging, continuous
|
||||
'showPageNumberInReader': true,
|
||||
};
|
||||
|
||||
operator [](String key) {
|
||||
|
@@ -82,7 +82,7 @@ class ComicSourceParser {
|
||||
js = js.replaceAll("\r\n", "\n");
|
||||
var line1 = js
|
||||
.split('\n')
|
||||
.firstWhereOrNull((element) => element.removeAllBlank.isNotEmpty);
|
||||
.firstWhereOrNull((e) => e.trim().startsWith("class "));
|
||||
if (line1 == null ||
|
||||
!line1.startsWith("class ") ||
|
||||
!line1.contains("extends ComicSource")) {
|
||||
|
@@ -163,6 +163,13 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
|
||||
return "${App.locale.languageCode}_${App.locale.countryCode}";
|
||||
case "getPlatform":
|
||||
return Platform.operatingSystem;
|
||||
case "setClipboard":
|
||||
return Clipboard.setData(ClipboardData(text: message["text"]));
|
||||
case "getClipboard":
|
||||
return Future.sync(() async {
|
||||
var res = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
return res?.text;
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
@@ -34,13 +34,10 @@ void main(List<String> 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<MyApp> with WidgetsBindingObserver {
|
||||
'dark' => ThemeMode.dark,
|
||||
_ => ThemeMode.system
|
||||
},
|
||||
color: Colors.transparent,
|
||||
localizationsDelegates: [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
@@ -248,6 +246,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||
);
|
||||
}
|
||||
return _SystemUiProvider(Material(
|
||||
color: App.isLinux ? Colors.transparent : null,
|
||||
child: widget,
|
||||
));
|
||||
}
|
||||
|
@@ -75,6 +75,8 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
|
||||
bool isDownloaded = false;
|
||||
|
||||
bool showFAB = false;
|
||||
|
||||
@override
|
||||
void onReadEnd() {
|
||||
history ??=
|
||||
@@ -114,7 +116,15 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
ComicDetails get comic => data!;
|
||||
|
||||
void onScroll() {
|
||||
if (scrollController.offset > 100) {
|
||||
var offset = scrollController.position.pixels -
|
||||
scrollController.position.minScrollExtent;
|
||||
var showFAB = offset > 0;
|
||||
if (showFAB != this.showFAB) {
|
||||
setState(() {
|
||||
this.showFAB = showFAB;
|
||||
});
|
||||
}
|
||||
if (offset > 100) {
|
||||
if (!showAppbarTitle) {
|
||||
setState(() {
|
||||
showAppbarTitle = true;
|
||||
@@ -133,19 +143,33 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
|
||||
@override
|
||||
Widget buildContent(BuildContext context, ComicDetails data) {
|
||||
return SmoothCustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
...buildTitle(),
|
||||
buildActions(),
|
||||
buildDescription(),
|
||||
buildInfo(),
|
||||
buildChapters(),
|
||||
buildComments(),
|
||||
buildThumbnails(),
|
||||
buildRecommend(),
|
||||
SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom)),
|
||||
],
|
||||
return Scaffold(
|
||||
floatingActionButton: showFAB
|
||||
? FloatingActionButton(
|
||||
onPressed: () {
|
||||
scrollController.animateTo(0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.ease);
|
||||
},
|
||||
child: const Icon(Icons.arrow_upward),
|
||||
)
|
||||
: null,
|
||||
body: SmoothCustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
...buildTitle(),
|
||||
buildActions(),
|
||||
buildDescription(),
|
||||
buildInfo(),
|
||||
buildChapters(),
|
||||
buildComments(),
|
||||
buildThumbnails(),
|
||||
buildRecommend(),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(bottom: context.padding.bottom + 80), // Add additional padding for FAB
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -306,7 +306,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
||||
});
|
||||
} else {
|
||||
// prevent dirty data
|
||||
var comic = LocalManager().find(c.id, ComicType(c.sourceKey.hashCode))!;
|
||||
var comic = LocalManager().find(c.id, ComicType.fromKey(c.sourceKey))!;
|
||||
comic.read();
|
||||
}
|
||||
},
|
||||
|
@@ -24,6 +24,8 @@ class ComicImage extends StatefulWidget {
|
||||
Map<String, String>? headers,
|
||||
int? cacheWidth,
|
||||
int? cacheHeight,
|
||||
this.onInit,
|
||||
this.onDispose,
|
||||
}) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, image),
|
||||
assert(cacheWidth == null || cacheWidth > 0),
|
||||
assert(cacheHeight == null || cacheHeight > 0);
|
||||
@@ -60,6 +62,10 @@ class ComicImage extends StatefulWidget {
|
||||
|
||||
final bool isAntiAlias;
|
||||
|
||||
final void Function(State<ComicImage> state)? onInit;
|
||||
|
||||
final void Function(State<ComicImage> state)? onDispose;
|
||||
|
||||
static void clear() => _ComicImageState.clear();
|
||||
|
||||
@override
|
||||
@@ -87,6 +93,7 @@ class _ComicImageState extends State<ComicImage> with WidgetsBindingObserver {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_scrollAwareContext = DisposableBuildContext<State<ComicImage>>(this);
|
||||
widget.onInit?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -97,6 +104,7 @@ class _ComicImageState extends State<ComicImage> with WidgetsBindingObserver {
|
||||
_completerHandle?.dispose();
|
||||
_scrollAwareContext.dispose();
|
||||
_replaceImage(info: null);
|
||||
widget.onDispose?.call(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -136,6 +144,15 @@ class _ComicImageState extends State<ComicImage> with WidgetsBindingObserver {
|
||||
super.reassemble();
|
||||
}
|
||||
|
||||
bool containsPoint(Offset point) {
|
||||
if (!mounted) {
|
||||
return false;
|
||||
}
|
||||
var renderBox = context.findRenderObject() as RenderBox;
|
||||
var localPoint = renderBox.globalToLocal(point);
|
||||
return renderBox.paintBounds.contains(localPoint);
|
||||
}
|
||||
|
||||
void _updateInvertColors() {
|
||||
_invertColors = MediaQuery.maybeInvertColorsOf(context) ??
|
||||
SemanticsBinding.instance.accessibilityFeatures.invertColors;
|
||||
|
@@ -281,6 +281,12 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
if (App.isDesktop && !reader.isLoading)
|
||||
MenuEntry(
|
||||
icon: Icons.copy,
|
||||
text: "Copy Image".tl,
|
||||
onClick: () => copyImage(location),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -303,6 +309,16 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet
|
||||
|
||||
@override
|
||||
Object? get key => "reader_gesture";
|
||||
|
||||
void copyImage(Offset location) async {
|
||||
var controller = reader._imageViewController;
|
||||
var image = await controller!.getImageByOffset(location);
|
||||
if (image != null) {
|
||||
writeImageToClipboard(image);
|
||||
} else {
|
||||
context.showMessage(message: "No Image");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _DragListener {
|
||||
|
@@ -25,8 +25,8 @@ class _ReaderImagesState extends State<_ReaderImages> {
|
||||
if (inProgress) return;
|
||||
inProgress = true;
|
||||
if (reader.type == ComicType.local ||
|
||||
(LocalManager()
|
||||
.isDownloaded(reader.cid, reader.type, reader.chapter, reader.widget.chapters))) {
|
||||
(LocalManager().isDownloaded(
|
||||
reader.cid, reader.type, reader.chapter, reader.widget.chapters))) {
|
||||
try {
|
||||
var images = await LocalManager()
|
||||
.getImages(reader.cid, reader.type, reader.chapter);
|
||||
@@ -113,6 +113,12 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
|
||||
int get totalPages => (reader.images!.length / reader.imagesPerPage).ceil();
|
||||
|
||||
var imageStates = <State<ComicImage>>{};
|
||||
|
||||
bool isLongPressing = false;
|
||||
|
||||
int fingers = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
reader = context.reader;
|
||||
@@ -142,81 +148,103 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PhotoViewGallery.builder(
|
||||
backgroundDecoration: BoxDecoration(
|
||||
color: context.colorScheme.surface,
|
||||
),
|
||||
reverse: reader.mode == ReaderMode.galleryRightToLeft,
|
||||
scrollDirection: reader.mode == ReaderMode.galleryTopToBottom
|
||||
? Axis.vertical
|
||||
: Axis.horizontal,
|
||||
itemCount: totalPages + 2,
|
||||
builder: (BuildContext context, int index) {
|
||||
if (index == 0 || index == totalPages + 1) {
|
||||
return PhotoViewGalleryPageOptions.customChild(
|
||||
child: const SizedBox(),
|
||||
);
|
||||
} else {
|
||||
int pageIndex = index - 1;
|
||||
int startIndex = pageIndex * reader.imagesPerPage;
|
||||
int endIndex = math.min(
|
||||
startIndex + reader.imagesPerPage, reader.images!.length);
|
||||
List<String> pageImages =
|
||||
reader.images!.sublist(startIndex, endIndex);
|
||||
|
||||
cached[index] = true;
|
||||
cache(index);
|
||||
|
||||
photoViewControllers[index] ??= PhotoViewController();
|
||||
|
||||
if (reader.imagesPerPage == 1) {
|
||||
return PhotoViewGalleryPageOptions(
|
||||
filterQuality: FilterQuality.medium,
|
||||
controller: photoViewControllers[index],
|
||||
imageProvider:
|
||||
_createImageProviderFromKey(pageImages[0], context),
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, error, s, retry) {
|
||||
return NetworkError(message: error.toString(), retry: retry);
|
||||
},
|
||||
return Listener(
|
||||
onPointerDown: (event) {
|
||||
fingers++;
|
||||
},
|
||||
onPointerUp: (event) {
|
||||
fingers--;
|
||||
},
|
||||
onPointerCancel: (event) {
|
||||
fingers--;
|
||||
},
|
||||
onPointerMove: (event) {
|
||||
if (isLongPressing) {
|
||||
var controller = photoViewControllers[reader.page]!;
|
||||
Offset value = event.delta;
|
||||
if (isLongPressing) {
|
||||
controller.updateMultiple(
|
||||
position: controller.position + value,
|
||||
);
|
||||
}
|
||||
|
||||
return PhotoViewGalleryPageOptions.customChild(
|
||||
controller: photoViewControllers[index],
|
||||
minScale: PhotoViewComputedScale.contained * 1.0,
|
||||
maxScale: PhotoViewComputedScale.covered * 10.0,
|
||||
child: buildPageImages(pageImages),
|
||||
);
|
||||
}
|
||||
},
|
||||
pageController: controller,
|
||||
loadingBuilder: (context, event) => Center(
|
||||
child: SizedBox(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: CircularProgressIndicator(
|
||||
backgroundColor: context.colorScheme.surfaceContainerHigh,
|
||||
value: event == null || event.expectedTotalBytes == null
|
||||
? null
|
||||
: event.cumulativeBytesLoaded / event.expectedTotalBytes!,
|
||||
child: PhotoViewGallery.builder(
|
||||
backgroundDecoration: BoxDecoration(
|
||||
color: context.colorScheme.surface,
|
||||
),
|
||||
reverse: reader.mode == ReaderMode.galleryRightToLeft,
|
||||
scrollDirection: reader.mode == ReaderMode.galleryTopToBottom
|
||||
? Axis.vertical
|
||||
: Axis.horizontal,
|
||||
itemCount: totalPages + 2,
|
||||
builder: (BuildContext context, int index) {
|
||||
if (index == 0 || index == totalPages + 1) {
|
||||
return PhotoViewGalleryPageOptions.customChild(
|
||||
child: const SizedBox(),
|
||||
);
|
||||
} else {
|
||||
int pageIndex = index - 1;
|
||||
int startIndex = pageIndex * reader.imagesPerPage;
|
||||
int endIndex = math.min(
|
||||
startIndex + reader.imagesPerPage, reader.images!.length);
|
||||
List<String> pageImages =
|
||||
reader.images!.sublist(startIndex, endIndex);
|
||||
|
||||
cached[index] = true;
|
||||
cache(index);
|
||||
|
||||
photoViewControllers[index] ??= PhotoViewController();
|
||||
|
||||
if (reader.imagesPerPage == 1) {
|
||||
return PhotoViewGalleryPageOptions(
|
||||
filterQuality: FilterQuality.medium,
|
||||
controller: photoViewControllers[index],
|
||||
imageProvider:
|
||||
_createImageProviderFromKey(pageImages[0], context),
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, error, s, retry) {
|
||||
return NetworkError(message: error.toString(), retry: retry);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return PhotoViewGalleryPageOptions.customChild(
|
||||
controller: photoViewControllers[index],
|
||||
minScale: PhotoViewComputedScale.contained * 1.0,
|
||||
maxScale: PhotoViewComputedScale.covered * 10.0,
|
||||
child: buildPageImages(pageImages),
|
||||
);
|
||||
}
|
||||
},
|
||||
pageController: controller,
|
||||
loadingBuilder: (context, event) => Center(
|
||||
child: SizedBox(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: CircularProgressIndicator(
|
||||
backgroundColor: context.colorScheme.surfaceContainerHigh,
|
||||
value: event == null || event.expectedTotalBytes == null
|
||||
? null
|
||||
: event.cumulativeBytesLoaded / event.expectedTotalBytes!,
|
||||
),
|
||||
),
|
||||
),
|
||||
onPageChanged: (i) {
|
||||
if (i == 0) {
|
||||
if (reader.isFirstChapterOfGroup || !reader.toPrevChapter()) {
|
||||
reader.toPage(1);
|
||||
}
|
||||
} else if (i == totalPages + 1) {
|
||||
if (reader.isLastChapterOfGroup || !reader.toNextChapter()) {
|
||||
reader.toPage(totalPages);
|
||||
}
|
||||
} else {
|
||||
reader.setPage(i);
|
||||
context.readerScaffold.update();
|
||||
}
|
||||
},
|
||||
),
|
||||
onPageChanged: (i) {
|
||||
if (i == 0) {
|
||||
if (reader.isFirstChapterOfGroup || !reader.toPrevChapter()) {
|
||||
reader.toPage(1);
|
||||
}
|
||||
} else if (i == totalPages + 1) {
|
||||
if (reader.isLastChapterOfGroup || !reader.toNextChapter()) {
|
||||
reader.toPage(totalPages);
|
||||
}
|
||||
} else {
|
||||
reader.setPage(i);
|
||||
context.readerScaffold.update();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -226,20 +254,54 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
: Axis.horizontal;
|
||||
|
||||
bool reverse = reader.mode == ReaderMode.galleryRightToLeft;
|
||||
|
||||
List<Widget> imageWidgets = images.map((imageKey) {
|
||||
ImageProvider imageProvider =
|
||||
_createImageProviderFromKey(imageKey, context);
|
||||
return Expanded(
|
||||
child: ComicImage(
|
||||
image: imageProvider,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
if (reverse) {
|
||||
imageWidgets = imageWidgets.reversed.toList();
|
||||
images = images.reversed.toList();
|
||||
}
|
||||
|
||||
List<Widget> imageWidgets;
|
||||
|
||||
if (images.length == 2) {
|
||||
imageWidgets = [
|
||||
Expanded(
|
||||
child: ComicImage(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
image: _createImageProviderFromKey(images[0], context),
|
||||
fit: BoxFit.contain,
|
||||
alignment: axis == Axis.vertical
|
||||
? Alignment.bottomCenter
|
||||
: Alignment.centerRight,
|
||||
onInit: (state) => imageStates.add(state),
|
||||
onDispose: (state) => imageStates.remove(state),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ComicImage(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
image: _createImageProviderFromKey(images[1], context),
|
||||
fit: BoxFit.contain,
|
||||
alignment: axis == Axis.vertical
|
||||
? Alignment.topCenter
|
||||
: Alignment.centerLeft,
|
||||
onInit: (state) => imageStates.add(state),
|
||||
onDispose: (state) => imageStates.remove(state),
|
||||
),
|
||||
)
|
||||
];
|
||||
} else {
|
||||
imageWidgets = images.map((imageKey) {
|
||||
ImageProvider imageProvider =
|
||||
_createImageProviderFromKey(imageKey, context);
|
||||
return Expanded(
|
||||
child: ComicImage(
|
||||
image: imageProvider,
|
||||
fit: BoxFit.contain,
|
||||
onInit: (state) => imageStates.add(state),
|
||||
onDispose: (state) => imageStates.remove(state),
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return axis == Axis.vertical
|
||||
@@ -276,7 +338,7 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
|
||||
@override
|
||||
void handleLongPressDown(Offset location) {
|
||||
if (!appdata.settings['enableLongPressToZoom']) {
|
||||
if (!appdata.settings['enableLongPressToZoom'] || fingers != 1) {
|
||||
return;
|
||||
}
|
||||
var photoViewController = photoViewControllers[reader.page]!;
|
||||
@@ -286,18 +348,22 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
target,
|
||||
Offset(size.width / 2 - location.dx, size.height / 2 - location.dy),
|
||||
);
|
||||
isLongPressing = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void handleLongPressUp(Offset location) {
|
||||
if (!appdata.settings['enableLongPressToZoom']) {
|
||||
if (!appdata.settings['enableLongPressToZoom'] || !isLongPressing) {
|
||||
return;
|
||||
}
|
||||
var photoViewController = photoViewControllers[reader.page]!;
|
||||
double target = photoViewController.getInitialScale!.call()!;
|
||||
photoViewController.animateScale?.call(target);
|
||||
isLongPressing = false;
|
||||
}
|
||||
|
||||
Timer? keyRepeatTimer;
|
||||
|
||||
@override
|
||||
void handleKeyEvent(KeyEvent event) {
|
||||
bool? forward;
|
||||
@@ -320,7 +386,11 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
||||
forward = false;
|
||||
}
|
||||
if (event is KeyDownEvent || event is KeyRepeatEvent) {
|
||||
if (event is KeyDownEvent) {
|
||||
if (keyRepeatTimer != null) {
|
||||
keyRepeatTimer!.cancel();
|
||||
keyRepeatTimer = null;
|
||||
}
|
||||
if (forward == true) {
|
||||
controller.nextPage(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
@@ -333,12 +403,59 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (event is KeyRepeatEvent && keyRepeatTimer == null) {
|
||||
keyRepeatTimer = Timer.periodic(
|
||||
const Duration(milliseconds: 100),
|
||||
(timer) {
|
||||
if (!mounted) {
|
||||
timer.cancel();
|
||||
return;
|
||||
} else if (forward == true) {
|
||||
controller.nextPage(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
curve: Curves.ease,
|
||||
);
|
||||
} else if (forward == false) {
|
||||
controller.previousPage(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
curve: Curves.ease,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
if (event is KeyUpEvent && keyRepeatTimer != null) {
|
||||
keyRepeatTimer!.cancel();
|
||||
keyRepeatTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool handleOnTap(Offset location) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List?> getImageByOffset(Offset offset) async {
|
||||
String? imageKey;
|
||||
if (reader.imagesPerPage == 1) {
|
||||
imageKey = reader.images![reader.page - 1];
|
||||
} else {
|
||||
for (var imageState in imageStates) {
|
||||
if ((imageState as _ComicImageState).containsPoint(offset)) {
|
||||
imageKey = (imageState.widget.image as ReaderImageProvider).imageKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (imageKey == null) return null;
|
||||
if (imageKey.startsWith("file://")) {
|
||||
return await File(imageKey.substring(7)).readAsBytes();
|
||||
} else {
|
||||
return (await CacheManager().findCache(
|
||||
"$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))!
|
||||
.readAsBytes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
|
||||
@@ -383,6 +500,8 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
/// To handle the tap event, we need to know if the user was scrolling before the delay.
|
||||
bool delayedIsScrolling = false;
|
||||
|
||||
var imageStates = <State<ComicImage>>{};
|
||||
|
||||
void delayedSetIsScrolling(bool value) {
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 300),
|
||||
@@ -395,6 +514,9 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
bool jumpToNextChapter = false;
|
||||
bool jumpToPrevChapter = false;
|
||||
|
||||
bool isZoomedIn = false;
|
||||
bool isLongPressing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
reader = context.reader;
|
||||
@@ -485,6 +607,16 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
}
|
||||
}
|
||||
|
||||
bool onScaleUpdate([double? scale]) {
|
||||
var isZoomedIn = (scale ?? photoViewController.scale) != 1.0;
|
||||
if (isZoomedIn != this.isZoomedIn) {
|
||||
setState(() {
|
||||
this.isZoomedIn = isZoomedIn;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget widget = ScrollablePositionedList.builder(
|
||||
@@ -506,7 +638,9 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
reverse: reader.mode == ReaderMode.continuousRightToLeft,
|
||||
physics: isCTRLPressed || _isMouseScrolling || disableScroll
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: const BouncingScrollPhysics(),
|
||||
: isZoomedIn
|
||||
? const ClampingScrollPhysics()
|
||||
: const BouncingScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0 || index == reader.maxPage + 1) {
|
||||
return const SizedBox();
|
||||
@@ -529,6 +663,8 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.contain,
|
||||
onInit: (state) => imageStates.add(state),
|
||||
onDispose: (state) => imageStates.remove(state),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -593,18 +729,23 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
if (photoViewController.scale == 1 || fingers != 1) {
|
||||
return;
|
||||
}
|
||||
if (scrollController.offset !=
|
||||
scrollController.position.maxScrollExtent &&
|
||||
scrollController.offset !=
|
||||
scrollController.position.minScrollExtent) {
|
||||
Offset offset;
|
||||
var sp = scrollController.position;
|
||||
if (sp.pixels < sp.minScrollExtent || sp.pixels > sp.maxScrollExtent) {
|
||||
offset = Offset(value.dx, value.dy);
|
||||
} else {
|
||||
if (reader.mode == ReaderMode.continuousTopToBottom) {
|
||||
value = Offset(value.dx, 0);
|
||||
offset = Offset(value.dx, 0);
|
||||
} else {
|
||||
value = Offset(0, value.dy);
|
||||
offset = Offset(0, value.dy);
|
||||
}
|
||||
}
|
||||
if (isLongPressing) {
|
||||
offset += value;
|
||||
}
|
||||
photoViewController.updateMultiple(
|
||||
position: photoViewController.position + value);
|
||||
position: photoViewController.position + offset,
|
||||
);
|
||||
},
|
||||
onPointerSignal: onPointerSignal,
|
||||
child: widget,
|
||||
@@ -676,6 +817,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
maxScale: 2.5,
|
||||
strictScale: true,
|
||||
controller: photoViewController,
|
||||
onScaleUpdate: onScaleUpdate,
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
@@ -731,6 +873,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
target,
|
||||
Offset(size.width / 2 - location.dx, size.height / 2 - location.dy),
|
||||
);
|
||||
onScaleUpdate(target);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -739,11 +882,12 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
return;
|
||||
}
|
||||
double target = photoViewController.getInitialScale!.call()! * 1.75;
|
||||
var size = MediaQuery.of(context).size;
|
||||
photoViewController.animateScale?.call(
|
||||
target,
|
||||
Offset(size.width / 2 - location.dx, size.height / 2 - location.dy),
|
||||
Offset(0, 0),
|
||||
);
|
||||
onScaleUpdate(target);
|
||||
isLongPressing = true;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -753,6 +897,8 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
}
|
||||
double target = photoViewController.getInitialScale!.call()!;
|
||||
photoViewController.animateScale?.call(target);
|
||||
onScaleUpdate(target);
|
||||
isLongPressing = false;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -818,6 +964,24 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List?> getImageByOffset(Offset offset) async {
|
||||
String? imageKey;
|
||||
for (var imageState in imageStates) {
|
||||
if ((imageState as _ComicImageState).containsPoint(offset)) {
|
||||
imageKey = (imageState.widget.image as ReaderImageProvider).imageKey;
|
||||
}
|
||||
}
|
||||
if (imageKey == null) return null;
|
||||
if (imageKey.startsWith("file://")) {
|
||||
return await File(imageKey.substring(7)).readAsBytes();
|
||||
} else {
|
||||
return (await CacheManager().findCache(
|
||||
"$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))!
|
||||
.readAsBytes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImageProvider _createImageProviderFromKey(
|
||||
|
@@ -30,6 +30,7 @@ import 'package:venera/foundation/local.dart';
|
||||
import 'package:venera/foundation/log.dart';
|
||||
import 'package:venera/foundation/res.dart';
|
||||
import 'package:venera/pages/settings/settings_page.dart';
|
||||
import 'package:venera/utils/clipboard_image.dart';
|
||||
import 'package:venera/utils/data_sync.dart';
|
||||
import 'package:venera/utils/ext.dart';
|
||||
import 'package:venera/utils/file_type.dart';
|
||||
@@ -577,4 +578,6 @@ abstract interface class _ImageViewController {
|
||||
|
||||
/// Returns true if the event is handled.
|
||||
bool handleOnTap(Offset location);
|
||||
|
||||
Future<Uint8List?> getImageByOffset(Offset offset);
|
||||
}
|
||||
|
@@ -127,7 +127,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
Positioned.fill(
|
||||
child: widget.child,
|
||||
),
|
||||
buildPageInfoText(),
|
||||
if (appdata.settings['showPageNumberInReader'] == true)
|
||||
buildPageInfoText(),
|
||||
buildStatusInfo(),
|
||||
AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
@@ -161,7 +162,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(top: context.padding.top),
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surface.toOpacity(0.82),
|
||||
color: context.colorScheme.surface.toOpacity(0.92),
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Colors.grey.toOpacity(0.5),
|
||||
@@ -475,7 +476,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
return BlurEffect(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surface.toOpacity(0.82),
|
||||
color: context.colorScheme.surface.toOpacity(0.92),
|
||||
border: isOpen
|
||||
? Border(
|
||||
top: BorderSide(
|
||||
|
@@ -25,8 +25,8 @@ class _ExploreSettingsState extends State<ExploreSettings> {
|
||||
title: "Size of comic tile".tl,
|
||||
settingsIndex: "comicTileScale",
|
||||
interval: 0.05,
|
||||
min: 0.75,
|
||||
max: 1.25,
|
||||
min: 0.5,
|
||||
max: 1.5,
|
||||
).toSliver(),
|
||||
_PopupWindowSetting(
|
||||
title: "Explore Pages".tl,
|
||||
|
@@ -179,6 +179,13 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
min: 1,
|
||||
max: 16,
|
||||
).toSliver(),
|
||||
_SwitchSetting(
|
||||
title: "Show Page Number".tl,
|
||||
settingKey: "showPageNumberInReader",
|
||||
onChanged: () {
|
||||
widget.onChanged?.call("showPageNumberInReader");
|
||||
},
|
||||
).toSliver(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
25
lib/utils/clipboard_image.dart
Normal file
25
lib/utils/clipboard_image.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
Future<void> writeImageToClipboard(Uint8List imageBytes) async {
|
||||
const channel = MethodChannel("venera/clipboard");
|
||||
if (Platform.isWindows || Platform.isLinux) {
|
||||
var image = await instantiateImageCodec(imageBytes);
|
||||
var frame = await image.getNextFrame();
|
||||
var data = await frame.image.toByteData(format: ImageByteFormat.rawRgba);
|
||||
await channel.invokeMethod("writeImageToClipboard", {
|
||||
"width": frame.image.width,
|
||||
"height": frame.image.height,
|
||||
"data": Uint8List.view(data!.buffer)
|
||||
});
|
||||
image.dispose();
|
||||
} else if (Platform.isMacOS) {
|
||||
await channel.invokeMethod("writeImageToClipboard", {
|
||||
"data": imageBytes,
|
||||
});
|
||||
} else {
|
||||
throw UnsupportedError("Clipboard image is not supported on this platform");
|
||||
}
|
||||
}
|
@@ -5,15 +5,45 @@
|
||||
#include <gdk/gdkx.h>
|
||||
#endif
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
struct _MyApplication {
|
||||
GtkApplication parent_instance;
|
||||
char** dart_entrypoint_arguments;
|
||||
FlMethodChannel* clipboard_channel;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||
|
||||
static void handle_clipboard_call(FlMethodChannel* channel, FlMethodCall* call, gpointer user_data) {
|
||||
if (strcmp(fl_method_call_get_name(call), "writeImageToClipboard") == 0) {
|
||||
const auto args = fl_method_call_get_args(call);
|
||||
const auto width = fl_value_get_int(fl_value_get_map_value(args, 0));
|
||||
const auto height = fl_value_get_int(fl_value_get_map_value(args, 1));
|
||||
const auto data = fl_value_get_uint8_list(fl_value_get_map_value(args, 2));
|
||||
|
||||
std::cout << width << " " << height << " " << data[0] << " " << data[1] << std::endl;
|
||||
|
||||
GBytes* bytes = g_bytes_new(data, width * height * 4);
|
||||
|
||||
GdkDisplay* display = gdk_display_get_default();
|
||||
GtkClipboard* clipboard = gtk_clipboard_get_default(display);
|
||||
GdkPixbuf* pixbuf = gdk_pixbuf_new_from_bytes(
|
||||
bytes,
|
||||
GDK_COLORSPACE_RGB,
|
||||
true,
|
||||
8,
|
||||
width,
|
||||
height,
|
||||
width * 4
|
||||
);
|
||||
gtk_clipboard_set_image(clipboard, pixbuf);
|
||||
fl_method_call_respond_success(call, fl_value_new_string("Ok"), nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
// Implements GApplication::activate.
|
||||
static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -38,6 +38,31 @@ class AppDelegate: FlutterAppDelegate {
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
let clipboardChannel = FlutterMethodChannel(name: "venera/clipboard", binaryMessenger: controller.engine.binaryMessenger)
|
||||
|
||||
clipboardChannel.setMethodCallHandler { (call, result) in
|
||||
switch call.method {
|
||||
case "writeImageToClipboard":
|
||||
guard let arguments = call.arguments as? [String: Any],
|
||||
let data = arguments["data"] as? FlutterStandardTypedData else {
|
||||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Invalid arguments", details: nil))
|
||||
return
|
||||
}
|
||||
|
||||
guard let image = NSImage(data: data.data) else {
|
||||
result(FlutterError(code: "INVALID_IMAGE", message: "Could not create image from data", details: nil))
|
||||
return
|
||||
}
|
||||
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.clearContents()
|
||||
pasteboard.writeObjects([image])
|
||||
result(true)
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getDirectoryPath() {
|
||||
|
41
pubspec.lock
41
pubspec.lock
@@ -45,10 +45,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.12.0"
|
||||
version: "2.13.0"
|
||||
battery_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -149,8 +149,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "packages/desktop_webview_window"
|
||||
ref: HEAD
|
||||
resolved-ref: b8f7e94c576acf4ca3dce5b9f8fb8076e5eaca5e
|
||||
ref: "7801fc582ecf5a7351632887891ecf309a7b2583"
|
||||
resolved-ref: "7801fc582ecf5a7351632887891ecf309a7b2583"
|
||||
url: "https://github.com/wgh136/flutter_desktop_webview"
|
||||
source: git
|
||||
version: "0.2.4"
|
||||
@@ -182,10 +182,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -516,10 +516,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
version: "0.20.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -540,10 +540,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.8"
|
||||
version: "10.0.9"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -580,10 +580,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_android
|
||||
sha256: "6763aaf8965f21822624cb2fd3c03d2a8b3791037b5efb0fe4b13e110f5afc92"
|
||||
sha256: "0abe4e72f55c785b28900de52a2522c86baba0988838b5dc22241b072ecccd74"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.46"
|
||||
version: "1.0.48"
|
||||
local_auth_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -725,8 +725,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: d71faf3c75e059d013b0ce9ddaf5ecc1680e2eb6
|
||||
resolved-ref: d71faf3c75e059d013b0ce9ddaf5ecc1680e2eb6
|
||||
ref: a1255d1b5945aad4b7323303ec2ecdf0c90ffc4c
|
||||
resolved-ref: a1255d1b5945aad4b7323303ec2ecdf0c90ffc4c
|
||||
url: "https://github.com/wgh136/photo_view"
|
||||
source: git
|
||||
version: "0.14.0"
|
||||
@@ -757,10 +757,11 @@ packages:
|
||||
rhttp:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: rhttp
|
||||
sha256: "037e9b59a68bb4ba664db1cbb4601e878cf5a2fc1cb3d0a9c58e3776609dec4d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: rhttp
|
||||
ref: e7dca15ca543b5df49f3ada06016e874b79bce36
|
||||
resolved-ref: e7dca15ca543b5df49f3ada06016e874b79bce36
|
||||
url: "https://github.com/wgh136/rhttp"
|
||||
source: git
|
||||
version: "0.11.0"
|
||||
screen_retriever:
|
||||
dependency: transitive
|
||||
@@ -1028,10 +1029,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.3.1"
|
||||
version: "15.0.0"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
11
pubspec.yaml
11
pubspec.yaml
@@ -2,7 +2,7 @@ name: venera
|
||||
description: "A comic app."
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.3.3+133
|
||||
version: 1.3.4+134
|
||||
|
||||
environment:
|
||||
sdk: '>=3.6.0 <4.0.0'
|
||||
@@ -29,7 +29,7 @@ dependencies:
|
||||
photo_view:
|
||||
git:
|
||||
url: https://github.com/wgh136/photo_view
|
||||
ref: d71faf3c75e059d013b0ce9ddaf5ecc1680e2eb6
|
||||
ref: a1255d1b5945aad4b7323303ec2ecdf0c90ffc4c
|
||||
mime: ^2.0.0
|
||||
share_plus: ^10.1.4
|
||||
scrollable_positioned_list:
|
||||
@@ -43,6 +43,7 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/wgh136/flutter_desktop_webview
|
||||
path: packages/desktop_webview_window
|
||||
ref: 7801fc582ecf5a7351632887891ecf309a7b2583
|
||||
flutter_inappwebview:
|
||||
git:
|
||||
url: https://github.com/pichillilorenzo/flutter_inappwebview
|
||||
@@ -57,7 +58,11 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/venera-app/lodepng_flutter
|
||||
ref: ac7d05dde32e8d728102a9ff66e6b55f05d94ba1
|
||||
rhttp: ^0.11.0
|
||||
rhttp:
|
||||
git:
|
||||
url: https://github.com/wgh136/rhttp
|
||||
ref: e7dca15ca543b5df49f3ada06016e874b79bce36
|
||||
path: rhttp
|
||||
webdav_client:
|
||||
git:
|
||||
url: https://github.com/wgh136/webdav_client
|
||||
|
73
windows/build_arm64.iss
Normal file
73
windows/build_arm64.iss
Normal file
@@ -0,0 +1,73 @@
|
||||
; Script generated by the Inno Setup Script Wizard.
|
||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||
|
||||
#define MyAppName "Venera"
|
||||
#define MyAppVersion "1.3.4"
|
||||
#define MyAppPublisher "nyne"
|
||||
#define MyAppURL "https://github.com/venera-app/venera"
|
||||
#define MyAppExeName "venera.exe"
|
||||
#define RootPath "D:\code\venera"
|
||||
|
||||
[Setup]
|
||||
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
|
||||
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||
AppId={{1A39CB64-0A5B-478E-9590-978614C804A8}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#MyAppVersion}
|
||||
;AppVerName={#MyAppName} {#MyAppVersion}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
AppPublisherURL={#MyAppURL}
|
||||
AppSupportURL={#MyAppURL}
|
||||
AppUpdatesURL={#MyAppURL}
|
||||
DefaultDirName={autopf}\{#MyAppName}
|
||||
DisableProgramGroupPage=yes
|
||||
; Uncomment the following line to run in non administrative install mode (install for current user only.)
|
||||
;PrivilegesRequired=lowest
|
||||
PrivilegesRequiredOverridesAllowed=dialog
|
||||
OutputDir={#RootPath}\build\windows
|
||||
OutputBaseFilename=Venera-{#MyAppVersion}-windows-arm64-installer
|
||||
SetupIconFile={#RootPath}\windows\runner\resources\app_icon.ico
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
ArchitecturesInstallIn64BitMode=arm64
|
||||
ArchitecturesAllowed=arm64
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
Name: "chinesesimplified"; MessagesFile: "{#RootPath}\windows\ChineseSimplified.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||
|
||||
[Files]
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\flutter_inappwebview_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\file_selector_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\app_links_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\sqlite3.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\sqlite3_flutter_libs_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\flutter_qjs_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\desktop_webview_window_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\WebView2Loader.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\share_plus_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\battery_plus_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\screen_retriever_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\window_manager_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\local_auth_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\zip_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\rhttp.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\lodepng_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\dynamic_color_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\flutter_7zip.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\arm64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[Icons]
|
||||
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall
|
43
windows/build_arm64.py
Normal file
43
windows/build_arm64.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import platform
|
||||
import subprocess
|
||||
import os
|
||||
import httpx
|
||||
|
||||
file = open('pubspec.yaml', 'r')
|
||||
content = file.read()
|
||||
file.close()
|
||||
|
||||
subprocess.run(["flutter", "build", "windows"], shell=True)
|
||||
|
||||
if os.path.exists("build/app-windows.zip"):
|
||||
os.remove("build/app-windows.zip")
|
||||
|
||||
version = str.split(str.split(content, 'version: ')[1], '+')[0]
|
||||
|
||||
subprocess.run(["tar", "-a", "-c", "-f", f"build/windows/Venera-{version}-windows-arm64.zip", "-C", "build/windows/x64/runner/Release", "*"]
|
||||
, shell=True)
|
||||
|
||||
issPath = "windows/build_arm64.iss"
|
||||
|
||||
issContent = ""
|
||||
file = open(issPath, 'r')
|
||||
issContent = file.read()
|
||||
newContent = issContent
|
||||
newContent = newContent.replace("{{version}}", version)
|
||||
newContent = newContent.replace("{{root_path}}", os.getcwd())
|
||||
file.close()
|
||||
file = open(issPath, 'w')
|
||||
file.write(newContent)
|
||||
file.close()
|
||||
|
||||
if not os.path.exists("windows/ChineseSimplified.isl"):
|
||||
# download ChineseSimplified.isl
|
||||
url = "https://cdn.jsdelivr.net/gh/kira-96/Inno-Setup-Chinese-Simplified-Translation@latest/ChineseSimplified.isl"
|
||||
response = httpx.get(url)
|
||||
with open('windows/ChineseSimplified.isl', 'wb') as file:
|
||||
file.write(response.content)
|
||||
|
||||
subprocess.run(["iscc", issPath], shell=True)
|
||||
|
||||
with open(issPath, 'w') as file:
|
||||
file.write(issContent)
|
@@ -102,6 +102,47 @@ bool FlutterWindow::OnCreate() {
|
||||
|
||||
channel2.SetStreamHandler(std::move(eventHandler));
|
||||
|
||||
const flutter::MethodChannel<> channel3(
|
||||
flutter_controller_->engine()->messenger(), "venera/clipboard",
|
||||
&flutter::StandardMethodCodec::GetInstance()
|
||||
);
|
||||
channel3.SetMethodCallHandler(
|
||||
[](const flutter::MethodCall<>& call,const std::unique_ptr<flutter::MethodResult<>>& result) {
|
||||
if(call.method_name() == "writeImageToClipboard"){
|
||||
flutter::EncodableMap arguments = std::get<flutter::EncodableMap>(*call.arguments());
|
||||
std::vector<uint8_t> data = std::get<std::vector<uint8_t>>(arguments["data"]);
|
||||
std::int32_t width = std::get<std::int32_t>(arguments["width"]);
|
||||
std::int32_t height = std::get<std::int32_t>(arguments["height"]);
|
||||
|
||||
// convert rgba to bgra
|
||||
for (int i = 0; i < data.size()/4; i++) {
|
||||
uint8_t temp = data[i * 4];
|
||||
data[i * 4] = data[i * 4 + 2];
|
||||
data[i * 4 + 2] = temp;
|
||||
}
|
||||
|
||||
auto bitmap = CreateBitmap((int)width, (int)height, 1, 32, data.data());
|
||||
|
||||
if (!bitmap) {
|
||||
result->Error("0", "Invalid Image Data");
|
||||
return;
|
||||
}
|
||||
|
||||
if (OpenClipboard(NULL))
|
||||
{
|
||||
EmptyClipboard();
|
||||
SetClipboardData(CF_BITMAP, bitmap);
|
||||
CloseClipboard();
|
||||
result->Success();
|
||||
}
|
||||
else {
|
||||
result->Error("Failed to open clipboard");
|
||||
}
|
||||
|
||||
DeleteObject(bitmap);
|
||||
}
|
||||
});
|
||||
|
||||
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
||||
|
||||
flutter_controller_->engine()->SetNextFrameCallback([&]() {
|
||||
|
Reference in New Issue
Block a user