mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
Compare commits
35 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bda299e1f8 | ||
![]() |
78ea129564 | ||
![]() |
f3b4598bb6 | ||
![]() |
7bc4c69a32 | ||
![]() |
a8e55e0151 | ||
![]() |
fddd959545 | ||
![]() |
ebf6846bf1 | ||
0f2d0bb9f9 | |||
48338e4ef7 | |||
8d8e345d82 | |||
![]() |
fcbf6a6277 | ||
d83d679eb9 | |||
d6087e5f59 | |||
37371bee6c | |||
45fe5f503a | |||
d440ed6424 | |||
d812332613 | |||
dee8d17b1e | |||
![]() |
c0d461ebd9 | ||
![]() |
45e2a1142a | ||
![]() |
533c2b2507 | ||
![]() |
29b7e0d646 | ||
b1870b65d6 | |||
1103076009 | |||
51739355c8 | |||
1b4f67b314 | |||
d9b23dadf0 | |||
ba8831caa6 | |||
2b1684b0fc | |||
cd3f09efae | |||
d05eaf8c7e | |||
03628f2afa | |||
![]() |
9dae28e366 | ||
![]() |
11e66328c4 | ||
![]() |
73d4e28ed0 |
29
.github/workflows/issue_check.yml
vendored
Normal file
29
.github/workflows/issue_check.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: Check Issue
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Check Issue
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
id: checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check Issue
|
||||||
|
id: check
|
||||||
|
uses: wgh136/gpt_issue_checker@v1.0.2
|
||||||
|
with:
|
||||||
|
api-url: ${{ secrets.API_URL }}
|
||||||
|
api-key: ${{ secrets.API_KEY }}
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
prompt: "You are a repository issue checker. The project is a comic app that supports view local or network comics using config files. To view a comic source, user must add a config file. User should not report any issue related to config file to the project repository because there is another repository for managing config files. You are given an issue content and you need to decide whether to close the issue. If you decide to close the issue, you should also provide a comment explaining why you are closing the issue. If you decide not to close the issue, you should provide a comment which is a summary of the issue. You should response with a JSON object with the following keys: should_close, should_comment, comment."
|
||||||
|
model: "gpt-4o"
|
9
.github/workflows/main.yml
vendored
9
.github/workflows/main.yml
vendored
@@ -26,6 +26,9 @@ jobs:
|
|||||||
echo "$CERTIFICATE" | base64 --decode > signing_certificate.p12
|
echo "$CERTIFICATE" | base64 --decode > signing_certificate.p12
|
||||||
security import signing_certificate.p12 -k ~/Library/Keychains/login.keychain -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
security import signing_certificate.p12 -k ~/Library/Keychains/login.keychain -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
||||||
|
|
||||||
|
- name: Check rust-toolchain.toml
|
||||||
|
run: rustup show
|
||||||
|
|
||||||
# Step 2: Build the Flutter macOS app
|
# Step 2: Build the Flutter macOS app
|
||||||
- name: Build Flutter macOS App
|
- name: Build Flutter macOS App
|
||||||
run: flutter build macos --release
|
run: flutter build macos --release
|
||||||
@@ -97,10 +100,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
distribution: 'oracle'
|
distribution: 'oracle'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Setup Rust
|
- name: Check rust-toolchain.toml
|
||||||
run: |
|
run: rustup show
|
||||||
rustup update
|
|
||||||
rustup default stable
|
|
||||||
- run: flutter pub get
|
- run: flutter pub get
|
||||||
- run: flutter build apk --release
|
- run: flutter build apk --release
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
|
@@ -1358,3 +1358,29 @@ let APP = {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set clipboard text
|
||||||
|
* @param text {string}
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*
|
||||||
|
* @since 1.3.4
|
||||||
|
*/
|
||||||
|
function setClipboard(text) {
|
||||||
|
return sendMessage({
|
||||||
|
method: 'setClipboard',
|
||||||
|
text: text
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get clipboard text
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*
|
||||||
|
* @since 1.3.4
|
||||||
|
*/
|
||||||
|
function getClipboard() {
|
||||||
|
return sendMessage({
|
||||||
|
method: 'getClipboard'
|
||||||
|
})
|
||||||
|
}
|
@@ -358,7 +358,7 @@
|
|||||||
"Once the operation is successful, app will automatically sync data with the server.": "操作成功后, APP将自动与服务器同步数据",
|
"Once the operation is successful, app will automatically sync data with the server.": "操作成功后, APP将自动与服务器同步数据",
|
||||||
"Cache cleared": "缓存已清除",
|
"Cache cleared": "缓存已清除",
|
||||||
"Disabled": "已禁用",
|
"Disabled": "已禁用",
|
||||||
"WebDAV Auto Sync": "WebDAV 自动同步",
|
"Auto Sync Data": "自动同步数据",
|
||||||
"Mark all as read": "全部标记为已读",
|
"Mark all as read": "全部标记为已读",
|
||||||
"Do you want to mark all as read?" : "您要全部标记为已读吗?",
|
"Do you want to mark all as read?" : "您要全部标记为已读吗?",
|
||||||
"Swipe down for previous chapter": "向下滑动查看上一章",
|
"Swipe down for previous chapter": "向下滑动查看上一章",
|
||||||
@@ -372,7 +372,13 @@
|
|||||||
"Refresh": "刷新",
|
"Refresh": "刷新",
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"zh_TW": {
|
"zh_TW": {
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
@@ -733,7 +739,7 @@
|
|||||||
"Once the operation is successful, app will automatically sync data with the server.": "操作成功後, APP將自動與伺服器同步資料",
|
"Once the operation is successful, app will automatically sync data with the server.": "操作成功後, APP將自動與伺服器同步資料",
|
||||||
"Cache cleared": "快取已清除",
|
"Cache cleared": "快取已清除",
|
||||||
"Disabled": "已停用",
|
"Disabled": "已停用",
|
||||||
"WebDAV Auto Sync": "WebDAV 自動同步",
|
"Auto Sync Data": "自動同步資料",
|
||||||
"Mark all as read": "全部標記為已讀",
|
"Mark all as read": "全部標記為已讀",
|
||||||
"Do you want to mark all as read?" : "您要全部標記為已讀嗎?",
|
"Do you want to mark all as read?" : "您要全部標記為已讀嗎?",
|
||||||
"Swipe down for previous chapter": "向下滑動查看上一章",
|
"Swipe down for previous chapter": "向下滑動查看上一章",
|
||||||
@@ -747,6 +753,12 @@
|
|||||||
"Refresh": "刷新",
|
"Refresh": "刷新",
|
||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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(
|
||||||
|
@@ -330,11 +330,10 @@ class _WebdavSettingState extends State<_WebdavSetting> {
|
|||||||
String url = "";
|
String url = "";
|
||||||
String user = "";
|
String user = "";
|
||||||
String pass = "";
|
String pass = "";
|
||||||
bool autoSync = false;
|
bool autoSync = true;
|
||||||
|
|
||||||
bool isTesting = false;
|
bool isTesting = false;
|
||||||
bool upload = true;
|
bool upload = true;
|
||||||
bool isEnabled = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -349,8 +348,7 @@ class _WebdavSettingState extends State<_WebdavSetting> {
|
|||||||
url = configs[0];
|
url = configs[0];
|
||||||
user = configs[1];
|
user = configs[1];
|
||||||
pass = configs[2];
|
pass = configs[2];
|
||||||
isEnabled = true;
|
autoSync = appdata.implicitData['webdavAutoSync'] ?? true;
|
||||||
autoSync = appdata.implicitData['webdavAutoSync'] ?? false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onAutoSyncChanged(bool value) {
|
void onAutoSyncChanged(bool value) {
|
||||||
@@ -368,16 +366,11 @@ class _WebdavSettingState extends State<_WebdavSetting> {
|
|||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 12),
|
|
||||||
SwitchListTile(
|
|
||||||
title: Text("WebDAV Auto Sync".tl),
|
|
||||||
value: autoSync,
|
|
||||||
onChanged: onAutoSyncChanged,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
TextField(
|
TextField(
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "URL",
|
labelText: "URL",
|
||||||
|
hintText: "A valid WebDav directory URL".tl,
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
controller: TextEditingController(text: url),
|
controller: TextEditingController(text: url),
|
||||||
@@ -402,6 +395,16 @@ class _WebdavSettingState extends State<_WebdavSetting> {
|
|||||||
onChanged: (value) => pass = value,
|
onChanged: (value) => pass = value,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(Icons.sync),
|
||||||
|
title: Text("Auto Sync Data".tl),
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
trailing: Switch(
|
||||||
|
value: autoSync,
|
||||||
|
onChanged: onAutoSyncChanged,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text("Operation".tl),
|
Text("Operation".tl),
|
||||||
@@ -428,21 +431,28 @@ class _WebdavSettingState extends State<_WebdavSetting> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Container(
|
AnimatedSize(
|
||||||
padding: const EdgeInsets.all(8),
|
duration: const Duration(milliseconds: 200),
|
||||||
decoration: BoxDecoration(
|
child: autoSync
|
||||||
color: Theme.of(context).colorScheme.primaryContainer,
|
? Container(
|
||||||
borderRadius: BorderRadius.circular(8),
|
padding: const EdgeInsets.all(8),
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
child: Row(
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
children: [
|
borderRadius: BorderRadius.circular(8),
|
||||||
const Icon(Icons.info_outline, size: 20),
|
),
|
||||||
const SizedBox(width: 8),
|
child: Row(
|
||||||
Expanded(
|
children: [
|
||||||
child: Text("Once the operation is successful, app will automatically sync data with the server.".tl),
|
const Icon(Icons.info_outline, size: 20),
|
||||||
),
|
const SizedBox(width: 8),
|
||||||
],
|
Expanded(
|
||||||
),
|
child: Text(
|
||||||
|
"Once the operation is successful, app will automatically sync data with the server."
|
||||||
|
.tl),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Center(
|
Center(
|
||||||
|
@@ -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
|
||||||
|
3
rust-toolchain.toml
Normal file
3
rust-toolchain.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "1.85.1"
|
||||||
|
targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "aarch64-linux-android", "armv7-linux-androideabi", "x86_64-linux-android"]
|
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