mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
@@ -1357,4 +1357,30 @@ let APP = {
|
|||||||
method: 'getPlatform'
|
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": "分页",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
|
@@ -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,
|
||||||
|
@@ -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),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
|
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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")) {
|
||||||
|
@@ -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;
|
||||||
|
@@ -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,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -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;
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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(
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
@@ -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(
|
||||||
|
@@ -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,
|
||||||
|
@@ -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(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
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>
|
#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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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() {
|
||||||
|
41
pubspec.lock
41
pubspec.lock
@@ -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:
|
||||||
|
11
pubspec.yaml
11
pubspec.yaml
@@ -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
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));
|
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([&]() {
|
||||||
|
Reference in New Issue
Block a user