31 Commits

Author SHA1 Message Date
b8492b3adc remove permission_handler 2024-11-10 18:10:30 +08:00
nyne
0f37feb318 Merge pull request #25 from venera-app/dev
v1.0.4
2024-11-10 17:59:58 +08:00
6e2c5c6e07 update version number 2024-11-10 17:59:06 +08:00
64d8bcba9a [Android] Turn page by volume keys 2024-11-10 17:50:20 +08:00
160d0df935 fix setting new download path 2024-11-10 17:48:12 +08:00
6a60194ffb support setting new download path on android 2024-11-10 17:27:27 +08:00
93193bddc0 Merge branch 'refs/heads/master' into dev 2024-11-10 16:01:45 +08:00
aa415f201e quick favorite 2024-11-10 15:57:52 +08:00
4f4411fcc3 sync data using webdav 2024-11-10 10:38:46 +08:00
nyne
afd690ed07 Merge pull request #24 from boa-z/master
Experimental Support for Setting New Storage Path on iOS
2024-11-09 17:16:52 +08:00
nyne
a3936f64da Delete tg.yaml 2024-11-09 17:09:02 +08:00
boa-z
7bf8cf569f experimental support for set new storage path on iOS 2024-11-09 11:04:34 +08:00
boa-z
856ec23586 add copy storage path button 2024-11-08 23:32:18 +08:00
boa-z
d910b8a35d add multiSelect for local_comics_page 2024-11-07 23:30:01 +08:00
nyne
234bf218a9 Merge pull request #23 from venera-app/telegram
Create tg.yaml
2024-11-07 18:33:19 +08:00
nyne
0226477256 Create tg.yaml 2024-11-07 18:33:06 +08:00
nyne
42ded1221a Merge pull request #22 from venera-app/dev
v 1.0.3
2024-11-07 10:26:40 +08:00
a9a22ace14 update README.md 2024-11-07 10:20:50 +08:00
99bbea80dc update version code 2024-11-07 09:56:10 +08:00
26fa41f503 improve translation 2024-11-07 09:41:14 +08:00
082aa36316 improve reader; fix #21 2024-11-07 09:31:57 +08:00
5a14ea48c1 fix changing search target in search result page 2024-11-07 09:02:03 +08:00
5d43f5c556 fix favorites page 2024-11-07 08:59:49 +08:00
e51a58ba4f fix deleting files when canceling a task 2024-11-07 08:49:32 +08:00
5234de434a improve network log 2024-11-06 22:08:23 +08:00
22f2ac99ad fix http 2024-11-06 18:06:20 +08:00
b08b5d0abe update action 2024-11-06 17:43:36 +08:00
nyne
96c6323c07 Merge pull request #18 from venera-app/dev
v1.0.2-patch
2024-11-06 09:21:29 +08:00
ae80715db1 update windows build script 2024-11-06 08:57:06 +08:00
3d7f30af00 update .gitignore 2024-11-06 08:53:57 +08:00
f12cb55bbc update windows build script 2024-11-06 08:51:20 +08:00
47 changed files with 1221 additions and 165 deletions

View File

@@ -4,7 +4,7 @@ on:
workflow_dispatch: {}
jobs:
Build_MacOS:
runs-on: macos-13
runs-on: macos-15
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
@@ -12,7 +12,7 @@ jobs:
channel: "stable"
flutter-version-file: pubspec.yaml
architecture: x64
- run: sudo xcode-select --switch /Applications/Xcode_14.3.1.app
- run: sudo xcode-select --switch /Applications/Xcode_16.0.app
- run: flutter pub get
# Step 1: Decode and install the certificate
- name: Decode and install certificate
@@ -38,12 +38,12 @@ jobs:
# Step 4: Attach and upload artifacts (optional)
- name: Upload DMG
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: venera.dmg
path: dist/venera.dmg
Build_IOS:
runs-on: macos-13
runs-on: macos-15
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
@@ -51,7 +51,7 @@ jobs:
channel: "stable"
flutter-version-file: pubspec.yaml
architecture: x64
- run: sudo xcode-select --switch /Applications/Xcode_14.3.1.app
- run: sudo xcode-select --switch /Applications/Xcode_16.0.app
- run: flutter pub get
- run: flutter build ios --release --no-codesign
- run: |

2
.gitignore vendored
View File

@@ -42,4 +42,4 @@ app.*.map.json
/android/app/profile
/android/app/release
./add_translation.py
add_translation.py

View File

@@ -4,6 +4,7 @@
[![License](https://img.shields.io/github/license/venera-app/venera)](https://github.com/venera-app/venera/blob/master/LICENSE)
[![Download](https://img.shields.io/github/v/release/venera-app/venera)](https://github.com/venera-app/venera/releases)
[![stars](https://img.shields.io/github/stars/venera-app/venera)](https://github.com/venera-app/venera/stargazers)
[![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=flat&logo=telegram&logoColor=white)](https://t.me/+Ws-IpmUutzkxMjhl)
A comic reader that support reading local and network comics.

View File

@@ -1,31 +0,0 @@
import re
import json
path='./assets/translation.json'
with open(path, 'r',encoding='utf-8') as file:
translations=json.load(file)
while True:
line=input()
if line=="q":
break
words=line.split('-')
if len(words)!=3:
print("invalid entry:",line,"(len(words) != 3)"
continue
en=words[0]
cn=words[1]
tw=words[2]
translations["zh_CN"][en]=cn
translations["zh_TW"][en]=tw
with open(path, 'w',encoding='utf-8') as file:
json.dump(translations, file, indent=2,ensure_ascii=False)

View File

@@ -1,5 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:label="venera"
android:name="${applicationName}"

View File

@@ -3,8 +3,16 @@ package com.github.wgh136.venera
import android.app.Activity
import android.content.ContentResolver
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.view.KeyEvent
import android.Manifest
import android.os.Environment
import android.provider.Settings
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
@@ -23,6 +31,9 @@ class MainActivity : FlutterActivity() {
private lateinit var result: MethodChannel.Result
private val storageRequestCode = 0x10
private var storagePermissionRequest: ((Boolean) -> Unit)? = null
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == pickDirectoryCode) {
@@ -43,6 +54,11 @@ class MainActivity : FlutterActivity() {
result.error("Failed to Copy Files", e.toString(), null)
}
}.start()
} else if (requestCode == storageRequestCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
storagePermissionRequest?.invoke(Environment.isExternalStorageManager())
}
storagePermissionRequest = null
}
}
@@ -89,6 +105,13 @@ class MainActivity : FlutterActivity() {
listening = false
}
})
val storageChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "venera/storage")
storageChannel.setMethodCallHandler { _, res ->
requestStoragePermission {result ->
res.success(result)
}
}
}
private fun getProxy(): String {
@@ -145,6 +168,61 @@ class MainActivity : FlutterActivity() {
}
}
}
private fun requestStoragePermission(result: (Boolean) -> Unit) {
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
val readPermission = ContextCompat.checkSelfPermission(
this,
Manifest.permission.READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
val writePermission = ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
if (!readPermission || !writePermission) {
storagePermissionRequest = result
ActivityCompat.requestPermissions(
this,
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
),
storageRequestCode
)
} else {
result(true)
}
} else {
if (!Environment.isExternalStorageManager()) {
try {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
intent.addCategory("android.intent.category.DEFAULT")
intent.data = Uri.parse("package:" + context.packageName)
startActivityForResult(intent, storageRequestCode)
} catch (e: Exception) {
result(false)
}
} else {
result(true)
}
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if(requestCode == storageRequestCode) {
storagePermissionRequest?.invoke(grantResults.all {
it == PackageManager.PERMISSION_GRANTED
})
storagePermissionRequest = null
}
}
}
class VolumeListen{

View File

@@ -169,7 +169,27 @@
"minAppVersion @version is required": "需要最低App版本 @version",
"Remove": "移除",
"Long press to zoom": "长按缩放",
"Updates Available": "更新可用"
"Updates Available": "更新可用",
"Unselected": "未选择",
"Long press and drag to reorder.": "长按并拖动以重新排序。",
"Limit image width": "限制图片宽度",
"When using Continuous(Top to Bottom) mode": "当使用连续(从上到下)模式",
"Open link": "打开链接",
"Open comic": "打开漫画",
"Move To First": "移动到最前",
"Cancel": "取消",
"Paused": "已暂停",
"Pause": "暂停",
"Operation": "操作",
"Upload": "上传",
"Saved": "已保存",
"Sync Data": "同步数据",
"Syncing Data": "正在同步数据",
"Data Sync": "数据同步",
"Quick Favorite": "快速收藏",
"Long press on the favorite button to quickly add to this folder": "长按收藏按钮快速添加到这个文件夹",
"Added": "已添加",
"Turn page by volume keys": "使用音量键翻页"
},
"zh_TW": {
"Home": "首頁",
@@ -341,6 +361,26 @@
"minAppVersion @version is required": "需要最低App版本 @version",
"Remove": "移除",
"Long press to zoom": "長按縮放",
"Updates Available": "更新可用"
"Updates Available": "更新可用",
"Unselected": "未選擇",
"Long press and drag to reorder.": "長按並拖動以重新排序。",
"Limit image width": "限制圖片寬度",
"When using Continuous(Top to Bottom) mode": "當使用連續(從上到下)模式",
"Open link": "打開鏈接",
"Open comic": "打開漫畫",
"Move To First": "移動到最前",
"Cancel": "取消",
"Paused": "已暫停",
"Pause": "暫停",
"Operation": "操作",
"Upload": "上傳",
"Saved": "已保存",
"Sync Data": "同步數據",
"Syncing Data": "正在同步數據",
"Data Sync": "數據同步",
"Quick Favorite": "快速收藏",
"Long press on the favorite button to quickly add to this folder": "長按收藏按鈕快速添加到這個文件夾",
"Added": "已添加",
"Turn page by volume keys": "使用音量鍵翻頁"
}
}

View File

@@ -15,6 +15,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
C0086D072CDEFE6E004596D9 /* DirectoryPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0086D062CDEFE64004596D9 /* DirectoryPicker.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -59,6 +60,7 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C0086D062CDEFE64004596D9 /* DirectoryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectoryPicker.swift; sourceTree = "<group>"; };
C22B8A9F3177D4A68EB8F66B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -133,6 +135,7 @@
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
C0086D062CDEFE64004596D9 /* DirectoryPicker.swift */,
);
path = Runner;
sourceTree = "<group>";
@@ -144,7 +147,6 @@
730F73FE38E23FCF3E461640 /* Pods-Runner.release.xcconfig */,
29B89F848F26E839605E1D88 /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
@@ -336,6 +338,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C0086D072CDEFE6E004596D9 /* DirectoryPicker.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);

View File

@@ -1,12 +1,16 @@
import Flutter
import UIKit
import UniformTypeIdentifiers
import Foundation //
@main
@objc class AppDelegate: FlutterAppDelegate, UIDocumentPickerDelegate {
var flutterResult: FlutterResult?
var directoryPath: URL!
//
private var directoryPicker: DirectoryPicker?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
@@ -42,6 +46,9 @@ import UniformTypeIdentifiers
self.directoryPath?.stopAccessingSecurityScopedResource()
self.directoryPath = nil
result(nil)
} else if call.method == "selectDirectory" {
self.directoryPicker = DirectoryPicker()
self.directoryPicker?.selectDirectory(result: result)
} else {
result(FlutterMethodNotImplemented)
}

View File

@@ -0,0 +1,36 @@
import UIKit
import Flutter
class DirectoryPicker: NSObject, UIDocumentPickerDelegate {
private var result: FlutterResult?
//
func selectDirectory(result: @escaping FlutterResult) {
self.result = result
// UIDocumentPicker
let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.folder])
documentPicker.delegate = self
documentPicker.allowsMultipleSelection = false
//
if let rootViewController = UIApplication.shared.keyWindow?.rootViewController {
rootViewController.present(documentPicker, animated: true, completion: nil)
}
}
//
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
//
if let url = urls.first {
result?(url.path)
} else {
result?(nil)
}
}
//
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
result?(nil)
}
}

View File

@@ -46,6 +46,10 @@
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSPhotoLibraryUsageDescription</key>
<string>Choose images</string>
<string>Choose images</string>
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
</dict>
</plist>

View File

@@ -10,7 +10,7 @@ export "widget_utils.dart";
export "context.dart";
class _App {
final version = "1.0.2";
final version = "1.0.4";
bool get isAndroid => Platform.isAndroid;

View File

@@ -113,6 +113,11 @@ class _Settings with ChangeNotifier {
'downloadThreads': 5,
'enableLongPressToZoom': true,
'checkUpdateOnStart': true,
'limitImageWidth': true,
'webdav': [], // empty means not configured
'dataVersion': 0,
'quickFavorite': null,
'enableTurnPageByVolumeKey': true,
};
operator [](String key) {

View File

@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/image_provider/local_favorite_image.dart';
@@ -148,7 +149,7 @@ class FavoriteItemWithFolderInfo extends FavoriteItem {
);
}
class LocalFavoritesManager {
class LocalFavoritesManager with ChangeNotifier {
factory LocalFavoritesManager() =>
cache ?? (cache = LocalFavoritesManager._create());
@@ -233,6 +234,7 @@ class LocalFavoritesManager {
values (?, ?);
""", [folder, order[folder]]);
}
notifyListeners();
}
int count(String folderName) {
@@ -272,6 +274,7 @@ class LocalFavoritesManager {
set tags = '$tag,' || tags
where id == ?
""", [id]);
notifyListeners();
}
List<FavoriteItemWithFolderInfo> allComics() {
@@ -324,6 +327,7 @@ class LocalFavoritesManager {
primary key (id, type)
);
""");
notifyListeners();
return name;
}
@@ -386,6 +390,7 @@ class LocalFavoritesManager {
values (?, ?, ?, ?, ?, ?, ?, ?);
""", [...params, minValue(folder) - 1]);
}
notifyListeners();
}
/// delete a folder
@@ -394,6 +399,7 @@ class LocalFavoritesManager {
_db.execute("""
drop table "$name";
""");
notifyListeners();
}
void deleteComic(String folder, FavoriteItem comic) {
@@ -408,6 +414,7 @@ class LocalFavoritesManager {
delete from "$folder"
where id == ? and type == ?;
""", [id, type.value]);
notifyListeners();
}
Future<void> clearAll() async {
@@ -425,6 +432,7 @@ class LocalFavoritesManager {
for (int i = 0; i < newFolder.length; i++) {
addComic(folder, newFolder[i], i);
}
notifyListeners();
}
void rename(String before, String after) {
@@ -438,6 +446,7 @@ class LocalFavoritesManager {
ALTER TABLE "$before"
RENAME TO "$after";
""");
notifyListeners();
}
void onReadEnd(String id, ComicType type) async {
@@ -475,6 +484,7 @@ class LocalFavoritesManager {
""", [newTime, id]);
}
}
notifyListeners();
}
List<FavoriteItemWithFolderInfo> search(String keyword) {
@@ -521,6 +531,7 @@ class LocalFavoritesManager {
set tags = ?
where id == ?;
""", [tags.join(","), id]);
notifyListeners();
}
final _cachedFavoritedIds = <String, bool>{};
@@ -560,6 +571,7 @@ class LocalFavoritesManager {
comic.id,
comic.type.value
]);
notifyListeners();
}
String folderToJson(String folder) {

View File

@@ -5,6 +5,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/comic_type.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/network/download.dart';
import 'package:venera/pages/reader/reader.dart';
import 'package:venera/utils/ext.dart';
@@ -158,12 +159,13 @@ class LocalManager with ChangeNotifier {
return "Directory is not empty";
}
try {
await copyDirectory(
await copyDirectoryIsolate(
Directory(path),
newDir,
);
await File(FilePath.join(App.dataPath, 'local_path')).writeAsString(path);
} catch (e) {
await File(FilePath.join(App.dataPath, 'local_path')).writeAsString(newPath);
} catch (e, s) {
Log.error("IO", e, s);
return e.toString();
}
await Directory(path).deleteIgnoreError(recursive:true);

View File

@@ -82,11 +82,12 @@ class Log {
addLog(LogLevel.warning, title, content);
}
static error(String title, String content, [Object? stackTrace]) {
static error(String title, Object content, [Object? stackTrace]) {
var info = content.toString();
if(stackTrace != null) {
content += "\n${stackTrace.toString()}";
info += "\n${stackTrace.toString()}";
}
addLog(LogLevel.error, title, content);
addLog(LogLevel.error, title, info);
}
static void clear() => _logs.clear();

View File

@@ -108,11 +108,11 @@ class AppDio with DioMixin {
AppDio([BaseOptions? options]) {
this.options = options ?? BaseOptions();
interceptors.add(MyLogInterceptor());
httpClientAdapter = RHttpAdapter(const rhttp.ClientSettings());
interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
interceptors.add(NetworkCacheManager());
interceptors.add(CloudflareInterceptor());
interceptors.add(MyLogInterceptor());
}
static HttpClient createHttpClient() {
@@ -196,12 +196,6 @@ class AppDio with DioMixin {
: rhttp.ProxySettings.proxy(proxy!),
));
}
Log.info(
"Network",
"${options?.method ?? 'GET'} $path\n"
"Headers: ${options?.headers}\n"
"Data: $data\n",
);
return super.request(
path,
data: data,
@@ -217,15 +211,15 @@ class AppDio with DioMixin {
class RHttpAdapter implements HttpClientAdapter {
rhttp.ClientSettings settings;
RHttpAdapter(this.settings) {
settings.copyWith(
RHttpAdapter([this.settings = const rhttp.ClientSettings()]) {
settings = settings.copyWith(
redirectSettings: const rhttp.RedirectSettings.limited(5),
timeoutSettings: const rhttp.TimeoutSettings(
connectTimeout: Duration(seconds: 15),
keepAliveTimeout: Duration(seconds: 60),
keepAlivePing: Duration(seconds: 30),
),
httpVersionPref: rhttp.HttpVersionPref.http1_1,
throwOnStatusCode: false,
);
}
@@ -275,12 +269,7 @@ class RHttpAdapter implements HttpClientAdapter {
var data = res.body;
if(headers['content-encoding']?.contains('gzip') ?? false) {
// rhttp does not support gzip decoding
var buffer = <int>[];
await for (var chunk in data) {
buffer.addAll(chunk);
}
data = Stream.value(Uint8List.fromList(gzip.decode(buffer)));
buffer.clear();
data = gzip.decoder.bind(data).map((data) => Uint8List.fromList(data));
}
return ResponseBody(
data,

View File

@@ -90,7 +90,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
var local = LocalManager().find(id, comicType);
if (path != null) {
if (local == null) {
Directory(path!).deleteIgnoreError();
Directory(path!).deleteIgnoreError(recursive: true);
} else if (chapters != null) {
for (var c in chapters!) {
var dir = Directory(FilePath.join(path!, c));

View File

@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:venera/components/components.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/comic_type.dart';
import 'package:venera/foundation/consts.dart';
@@ -282,6 +283,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
isActive: isFavorite || isAddToLocalFav,
text: 'Favorite'.tl,
onPressed: openFavPanel,
onLongPressed: quickFavorite,
iconColor: context.useTextColor(Colors.purple),
),
if (comicSource.commentsLoader != null)
@@ -538,12 +540,22 @@ abstract mixin class _ComicPageActions {
bool isFavorite = false;
void openFavPanel() {
FavoriteItem _toFavoriteItem() {
var tags = <String>[];
for (var e in comic.tags.entries) {
tags.addAll(e.value.map((tag) => '${e.key}:$tag'));
}
return FavoriteItem(
id: comic.id,
name: comic.title,
coverPath: comic.cover,
author: comic.subTitle ?? comic.uploader ?? '',
type: comic.comicType,
tags: tags,
);
}
void openFavPanel() {
showSideBar(
App.rootContext,
_FavoritePanel(
@@ -555,18 +567,25 @@ abstract mixin class _ComicPageActions {
isAddToLocalFav = local ?? isAddToLocalFav;
update();
},
favoriteItem: FavoriteItem(
id: comic.id,
name: comic.title,
coverPath: comic.cover,
author: comic.subTitle ?? comic.uploader ?? '',
type: comic.comicType,
tags: tags,
),
favoriteItem: _toFavoriteItem(),
),
);
}
void quickFavorite() {
var folder = appdata.settings['quickFavorite'];
if(folder is! String) {
return;
}
LocalFavoritesManager().addComic(
folder,
_toFavoriteItem(),
);
isAddToLocalFav = true;
update();
App.rootContext.showMessage(message: "Added".tl);
}
void share() {
var text = comic.title;
if (comic.url != null) {
@@ -800,6 +819,7 @@ class _ActionButton extends StatelessWidget {
required this.icon,
required this.text,
required this.onPressed,
this.onLongPressed,
this.activeIcon,
this.isActive,
this.isLoading,
@@ -820,6 +840,8 @@ class _ActionButton extends StatelessWidget {
final Color? iconColor;
final void Function()? onLongPressed;
@override
Widget build(BuildContext context) {
return Container(
@@ -837,6 +859,7 @@ class _ActionButton extends StatelessWidget {
onPressed();
}
},
onLongPress: onLongPressed,
borderRadius: BorderRadius.circular(18),
child: IconTheme.merge(
data: IconThemeData(size: 20, color: iconColor),

View File

@@ -6,7 +6,6 @@ Future<void> newFolder() async {
context: App.rootContext,
builder: (context) {
var controller = TextEditingController();
var folders = LocalFavoritesManager().folderNames;
String? error;
return StatefulBuilder(builder: (context, setState) {

View File

@@ -152,7 +152,8 @@ class _FavoritesPageState extends State<FavoritesPage> {
} else {
var favoriteData = getFavoriteDataOrNull(folder!);
if (favoriteData == null) {
return const Center(child: Text("Unknown source"));
folder = null;
return buildBody();
} else {
return NetworkFavoritePage(favoriteData, key: Key(folder!));
}

View File

@@ -15,10 +15,8 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
late List<FavoriteItem> comics;
void updateComics() {
print(comics.length);
setState(() {
comics = LocalFavoritesManager().getAllComics(widget.folder);
print(comics.length);
});
}
@@ -107,7 +105,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
},
).then(
(value) {
setState(() {});
if(mounted) {
setState(() {});
}
},
);
}),
@@ -199,6 +199,7 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> {
var comicSource = e.type.comicSource;
return ComicTile(
key: Key(e.hashCode.toString()),
enableLongPressed: false,
comic: Comic(
e.name,
e.coverPath,

View File

@@ -1,5 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:sliver_tools/sliver_tools.dart';
import 'package:venera/components/components.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
@@ -17,6 +18,7 @@ import 'package:venera/pages/downloading_page.dart';
import 'package:venera/pages/history_page.dart';
import 'package:venera/pages/search_page.dart';
import 'package:venera/utils/cbz.dart';
import 'package:venera/utils/data_sync.dart';
import 'package:venera/utils/ext.dart';
import 'package:venera/utils/io.dart';
import 'package:venera/utils/translations.dart';
@@ -32,6 +34,7 @@ class HomePage extends StatelessWidget {
slivers: [
SliverPadding(padding: EdgeInsets.only(top: context.padding.top)),
const _SearchBar(),
const _SyncDataWidget(),
const _History(),
const _Local(),
const _ComicSourceWidget(),
@@ -77,6 +80,97 @@ class _SearchBar extends StatelessWidget {
}
}
class _SyncDataWidget extends StatefulWidget {
const _SyncDataWidget();
@override
State<_SyncDataWidget> createState() => _SyncDataWidgetState();
}
class _SyncDataWidgetState extends State<_SyncDataWidget> {
@override
void initState() {
super.initState();
DataSync().addListener(update);
}
void update() {
if(mounted) {
setState(() {});
}
}
@override
void dispose() {
super.dispose();
DataSync().removeListener(update);
}
@override
Widget build(BuildContext context) {
Widget child;
if(!DataSync().isEnabled) {
child = const SliverPadding(padding: EdgeInsets.zero);
} else if (DataSync().isUploading || DataSync().isDownloading) {
child = SliverToBoxAdapter(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.primary,
),
borderRadius: BorderRadius.circular(8),
),
child: ListTile(
leading: const Icon(Icons.sync),
title: Text('Syncing Data'.tl),
trailing: const CircularProgressIndicator(strokeWidth: 2)
.fixWidth(18)
.fixHeight(18),
),
),
);
} else {
child = SliverToBoxAdapter(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outlineVariant,
),
borderRadius: BorderRadius.circular(8),
),
child: ListTile(
leading: const Icon(Icons.sync),
title: Text('Sync Data'.tl),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.cloud_upload_outlined),
onPressed: () async {
DataSync().uploadData();
}
),
IconButton(
icon: const Icon(Icons.cloud_download_outlined),
onPressed: () async {
DataSync().downloadData();
}
),
],
),
),
),
);
}
return SliverAnimatedPaintExtent(
duration: const Duration(milliseconds: 200),
child: child,
);
}
}
class _History extends StatefulWidget {
const _History();
@@ -529,14 +623,16 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
await xFile!.saveTo(cache);
var comic = await CBZ.import(File(cache));
if (selectedFolder != null) {
LocalFavoritesManager().addComic(selectedFolder!, FavoriteItem(
id: comic.id,
name: comic.title,
coverPath: comic.cover,
author: comic.subtitle,
type: comic.comicType,
tags: comic.tags,
));
LocalFavoritesManager().addComic(
selectedFolder!,
FavoriteItem(
id: comic.id,
name: comic.title,
coverPath: comic.cover,
author: comic.subtitle,
type: comic.comicType,
tags: comic.tags,
));
}
await File(cache).deleteIgnoreError();
} catch (e, s) {
@@ -610,14 +706,16 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
for (var comic in comics.values) {
LocalManager().add(comic, LocalManager().findValidId(ComicType.local));
if (selectedFolder != null) {
LocalFavoritesManager().addComic(selectedFolder!, FavoriteItem(
id: comic.id,
name: comic.title,
coverPath: comic.cover,
author: comic.subtitle,
type: comic.comicType,
tags: comic.tags,
));
LocalFavoritesManager().addComic(
selectedFolder!,
FavoriteItem(
id: comic.id,
name: comic.title,
coverPath: comic.cover,
author: comic.subtitle,
type: comic.comicType,
tags: comic.tags,
));
}
}
context.pop();

View File

@@ -24,8 +24,12 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
bool searchMode = false;
bool multiSelectMode = false;
Map<LocalComic, bool> selectedComics = {};
void update() {
if(keyword.isEmpty) {
if (keyword.isEmpty) {
setState(() {
comics = LocalManager().getComics(sortType);
});
@@ -95,8 +99,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
actions: [
FilledButton(
onPressed: () {
appdata.implicitData["local_sort"] =
sortType.value;
appdata.implicitData["local_sort"] =sortType.value;
appdata.writeImplicitData();
Navigator.pop(context);
update();
@@ -115,7 +118,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
return Scaffold(
body: SmoothCustomScrollView(
slivers: [
if(!searchMode)
if (!searchMode && !multiSelectMode)
SliverAppbar(
title: Text("Local".tl),
actions: [
@@ -145,10 +148,38 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
showPopUpWidget(context, const DownloadingPage());
},
),
)
),
Tooltip(
message: multiSelectMode
? "Exit Multi-Select".tl
: "Multi-Select".tl,
child: IconButton(
icon: const Icon(Icons.checklist),
onPressed: () {
setState(() {
multiSelectMode = !multiSelectMode;
});
},
),
),
],
)
else
else if (multiSelectMode)
SliverAppbar(
title: Text("Selected ${selectedComics.length} comics"),
actions: [
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() {
multiSelectMode = false;
selectedComics.clear();
});
},
),
],
)
else if (searchMode)
SliverAppbar(
title: TextField(
autofocus: true,
@@ -176,16 +207,42 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
),
SliverGridComics(
comics: comics,
onTap: (c) {
(c as LocalComic).read();
},
onTap: multiSelectMode
? (c) {
setState(() {
if (selectedComics.containsKey(c as LocalComic)) {
selectedComics.remove(c as LocalComic);
} else {
selectedComics[c as LocalComic] = true;
}
});
}
: (c) {
(c as LocalComic).read();
},
menuBuilder: (c) {
return [
MenuEntry(
icon: Icons.delete,
text: "Delete".tl,
onClick: () {
LocalManager().deleteComic(c as LocalComic);
if (multiSelectMode) {
showConfirmDialog(
context: context,
title: "Delete".tl,
content: "Delete selected comics?".tl,
onConfirm: () {
for (var comic in selectedComics.keys) {
LocalManager().deleteComic(comic);
}
setState(() {
selectedComics.clear();
});
},
);
} else {
LocalManager().deleteComic(c as LocalComic);
}
}),
MenuEntry(
icon: Icons.outbox_outlined,
@@ -196,9 +253,20 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
allowCancel: false,
);
try {
var file = await CBZ.export(c as LocalComic);
await saveFile(filename: file.name, file: file);
await file.delete();
if (multiSelectMode) {
for (var comic in selectedComics.keys) {
var file = await CBZ.export(comic);
await saveFile(filename: file.name, file: file);
await file.delete();
}
setState(() {
selectedComics.clear();
});
} else {
var file = await CBZ.export(c as LocalComic);
await saveFile(filename: file.name, file: file);
await file.delete();
}
} catch (e) {
context.showMessage(message: e.toString());
}

View File

@@ -22,6 +22,8 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
_DragListener? dragListener;
int fingers = 0;
@override
void initState() {
_tapGestureRecognizer = TapGestureRecognizer()
@@ -38,6 +40,7 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
return Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (event) {
fingers++;
_lastTapPointer = event.pointer;
_lastTapMoveDistance = Offset.zero;
_tapGestureRecognizer.addPointer(event);
@@ -46,7 +49,7 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
_dragInProgress = false;
}
Future.delayed(_kLongPressMinTime, () {
if (_lastTapPointer == event.pointer) {
if (_lastTapPointer == event.pointer && fingers == 1) {
if(_lastTapMoveDistance!.distanceSquared < 20.0 * 20.0) {
onLongPressedDown(event.position);
_longPressInProgress = true;
@@ -67,6 +70,19 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
}
},
onPointerUp: (event) {
fingers--;
if (_longPressInProgress) {
onLongPressedUp(event.position);
}
if(_dragInProgress) {
dragListener?.onEnd?.call();
_dragInProgress = false;
}
_lastTapPointer = null;
_lastTapMoveDistance = null;
},
onPointerCancel: (event) {
fingers--;
if (_longPressInProgress) {
onLongPressedUp(event.position);
}

View File

@@ -471,18 +471,24 @@ class _ContinuousModeState extends State<_ContinuousMode>
},
child: widget,
);
var width = MediaQuery.of(context).size.width;
var height = MediaQuery.of(context).size.height;
if(appdata.settings['limitImageWidth'] && width / height > 0.7) {
width = height * 0.7;
}
return PhotoView.customChild(
backgroundDecoration: BoxDecoration(
color: context.colorScheme.surface,
),
childSize: Size(width, height),
minScale: 1.0,
maxScale: 2.5,
strictScale: true,
controller: photoViewController,
child: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
width: width,
height: height,
child: widget,
),
);

View File

@@ -23,6 +23,7 @@ import 'package:venera/pages/settings/settings_page.dart';
import 'package:venera/utils/file_type.dart';
import 'package:venera/utils/io.dart';
import 'package:venera/utils/translations.dart';
import 'package:venera/utils/volume.dart';
import 'package:window_manager/window_manager.dart';
part 'scaffold.dart';
@@ -97,6 +98,8 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
var focusNode = FocusNode();
VolumeListener? volumeListener;
@override
void initState() {
page = widget.initialPage ?? 1;
@@ -107,6 +110,9 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
updateHistory();
});
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
if(appdata.settings['enableTurnPageByVolumeKey']) {
handleVolumeEvent();
}
super.initState();
}
@@ -115,6 +121,7 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
autoPageTurningTimer?.cancel();
focusNode.dispose();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
stopVolumeEvent();
super.dispose();
}
@@ -152,6 +159,31 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
HistoryManager().addHistory(history!);
}
}
void handleVolumeEvent() {
if(!App.isAndroid) {
// Currently only support Android
return;
}
if(volumeListener != null) {
volumeListener?.cancel();
}
volumeListener = VolumeListener(
onDown: () {
toNextPage();
},
onUp: () {
toPrevPage();
},
)..listen();
}
void stopVolumeEvent() {
if(volumeListener != null) {
volumeListener?.cancel();
volumeListener = null;
}
}
}
abstract mixin class _ReaderLocation {

View File

@@ -470,6 +470,13 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
context.reader.mode = ReaderMode.fromKey(appdata.settings[key]);
App.rootContext.pop();
}
if (key == "enableTurnPageByVolumeKey") {
if(appdata.settings[key]) {
context.reader.handleVolumeEvent();
} else {
context.reader.stopVolumeEvent();
}
}
context.reader.update();
},
),

View File

@@ -369,6 +369,9 @@ class _SearchPageState extends State<SearchPage> {
),
trailing: const Icon(Icons.arrow_right),
onTap: () {
setState(() {
suggestions.clear();
});
handleAppLink(Uri.parse(controller.text));
},
);

View File

@@ -42,7 +42,7 @@ class _SearchResultPageState extends State<SearchResultPage> {
void search([String? text]) {
if (text != null) {
if(suggestionsController.entry != null) {
if (suggestionsController.entry != null) {
suggestionsController.remove();
}
setState(() {
@@ -135,20 +135,24 @@ class _SearchResultPageState extends State<SearchResultPage> {
onChanged: onChanged,
action: buildAction(),
),
loadPage: source!.searchPageData!.loadPage == null ? null : (i) {
return source.searchPageData!.loadPage!(
text,
i,
options,
);
},
loadNext: source.searchPageData!.loadNext == null ? null : (i) {
return source.searchPageData!.loadNext!(
text,
i,
options,
);
},
loadPage: source!.searchPageData!.loadPage == null
? null
: (i) {
return source.searchPageData!.loadPage!(
text,
i,
options,
);
},
loadNext: source.searchPageData!.loadNext == null
? null
: (i) {
return source.searchPageData!.loadNext!(
text,
i,
options,
);
},
);
}
@@ -424,6 +428,11 @@ class _SearchSettingsDialogState extends State<_SearchSettingsDialog> {
setState(() {
searchTarget = e.key;
options.clear();
final searchOptions = ComicSource.find(searchTarget)!
.searchPageData!
.searchOptions ??
<SearchOptions>[];
options = searchOptions.map((e) => e.defaultValue).toList();
onChanged();
});
},

View File

@@ -20,16 +20,44 @@ class _AppSettingsState extends State<AppSettings> {
ListTile(
title: Text("Storage Path for local comics".tl),
subtitle: Text(LocalManager().path, softWrap: false),
trailing: IconButton(
icon: const Icon(Icons.copy),
onPressed: () {
Clipboard.setData(ClipboardData(text: LocalManager().path));
context.showMessage(message: "Path copied to clipboard".tl);
},
),
).toSliver(),
_CallbackSetting(
title: "Set New Storage Path".tl,
actionTitle: "Set".tl,
callback: () async {
if (App.isMobile) {
context.showMessage(message: "Not supported".tl);
return;
String? result;
if (App.isAndroid) {
var channel = const MethodChannel("venera/storage");
var permission = await channel.invokeMethod('');
if(permission != true) {
context.showMessage(message: "Permission denied".tl);
return;
}
var path = await selectDirectory();
if(path != null) {
// check if the path is writable
var testFile = File(FilePath.join(path, "test"));
try {
await testFile.writeAsBytes([1]);
await testFile.delete();
} catch (e) {
context.showMessage(message: "Permission denied".tl);
return;
}
result = path;
}
} else if (App.isIOS) {
result = await selectDirectoryIOS();
} else {
result = await selectDirectory();
}
var result = await selectDirectory();
if (result == null) return;
var loadingDialog = showLoadingDialog(
App.rootContext,
@@ -78,8 +106,7 @@ class _AppSettingsState extends State<AppSettings> {
appdata.settings['cacheSize'] = int.parse(value);
appdata.saveData();
setState(() {});
CacheManager()
.setLimitSize(appdata.settings['cacheSize']);
CacheManager().setLimitSize(appdata.settings['cacheSize']);
return null;
},
);
@@ -101,13 +128,12 @@ class _AppSettingsState extends State<AppSettings> {
callback: () async {
var controller = showLoadingDialog(context);
var file = await selectFile(ext: ['venera']);
if(file != null) {
if (file != null) {
var cacheFile = File(FilePath.join(App.cachePath, "temp.venera"));
await file.saveTo(cacheFile.path);
try {
await importAppData(cacheFile);
}
catch(e, s) {
} catch (e, s) {
Log.error("Import data", e.toString(), s);
context.showMessage(message: "Failed to import data".tl);
}
@@ -116,6 +142,13 @@ class _AppSettingsState extends State<AppSettings> {
},
actionTitle: 'Import'.tl,
).toSliver(),
_CallbackSetting(
title: "Data Sync".tl,
callback: () async {
showPopUpWidget(context, const _WebdavSetting());
},
actionTitle: 'Set'.tl,
).toSliver(),
_SettingPartTitle(
title: "Log".tl,
icon: Icons.error_outline,
@@ -271,3 +304,129 @@ class _LogsPageState extends State<LogsPage> {
saveFile(data: utf8.encode(log), filename: 'log.txt');
}
}
class _WebdavSetting extends StatefulWidget {
const _WebdavSetting();
@override
State<_WebdavSetting> createState() => _WebdavSettingState();
}
class _WebdavSettingState extends State<_WebdavSetting> {
String url = "";
String user = "";
String pass = "";
bool isTesting = false;
bool upload = true;
@override
void initState() {
super.initState();
if (appdata.settings['webdav'] is! List) {
appdata.settings['webdav'] = [];
}
var configs = appdata.settings['webdav'] as List;
if (configs.whereType<String>().length != 3) {
return;
}
url = configs[0];
user = configs[1];
pass = configs[2];
}
@override
Widget build(BuildContext context) {
return PopUpWidgetScaffold(
title: "Webdav",
body: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 12),
TextField(
decoration: const InputDecoration(
labelText: "URL",
border: OutlineInputBorder(),
),
controller: TextEditingController(text: url),
onChanged: (value) => url = value,
),
const SizedBox(height: 12),
TextField(
decoration: InputDecoration(
labelText: "Username".tl,
border: const OutlineInputBorder(),
),
controller: TextEditingController(text: user),
onChanged: (value) => user = value,
),
const SizedBox(height: 12),
TextField(
decoration: InputDecoration(
labelText: "Password".tl,
border: const OutlineInputBorder(),
),
controller: TextEditingController(text: pass),
onChanged: (value) => pass = value,
),
const SizedBox(height: 12),
Row(
children: [
Text("Operation".tl),
Radio<bool>(
groupValue: upload,
value: true,
onChanged: (value) {
setState(() {
upload = value!;
});
},
),
Text("Upload".tl),
Radio<bool>(
groupValue: upload,
value: false,
onChanged: (value) {
setState(() {
upload = value!;
});
},
),
Text("Download".tl),
],
),
const SizedBox(height: 16),
Center(
child: Button.filled(
isLoading: isTesting,
onPressed: () async {
var oldConfig = appdata.settings['webdav'];
appdata.settings['webdav'] = [url, user, pass];
setState(() {
isTesting = true;
});
var testResult = upload
? await DataSync().uploadData()
: await DataSync().downloadData();
if (testResult.error) {
setState(() {
isTesting = false;
});
appdata.settings['webdav'] = oldConfig;
context.showMessage(message: testResult.errorMessage!);
return;
}
appdata.saveData();
context.showMessage(message: "Saved".tl);
App.rootPop();
},
child: Text("Continue".tl),
),
)
],
).paddingHorizontal(16),
),
);
}
}

View File

@@ -24,12 +24,20 @@ class _LocalFavoritesSettingsState extends State<LocalFavoritesSettings> {
SelectSetting(
title: "Move favorite after reading".tl,
settingKey: "moveFavoriteAfterRead",
optionTranslation: {
optionTranslation: const {
"none": "None",
"end": "End",
"start": "Start",
},
).toSliver(),
SelectSetting(
title: "Quick Favorite".tl,
settingKey: "quickFavorite",
help: "Long press on the favorite button to quickly add to this folder".tl,
optionTranslation: {
for (var e in LocalFavoritesManager().folderNames) e: e
},
).toSliver(),
],
);
}

View File

@@ -61,6 +61,22 @@ class _ReaderSettingsState extends State<ReaderSettings> {
widget.onChanged?.call('enableLongPressToZoom');
},
).toSliver(),
_SwitchSetting(
title: 'Limit image width'.tl,
subtitle: 'When using Continuous(Top to Bottom) mode'.tl,
settingKey: 'limitImageWidth',
onChanged: () {
widget.onChanged?.call('limitImageWidth');
},
).toSliver(),
if(App.isAndroid)
_SwitchSetting(
title: 'Turn page by volume key'.tl,
settingKey: 'enableTurnPageByVolumeKey',
onChanged: () {
widget.onChanged?.call('enableTurnPageByVolumeKey');
},
).toSliver(),
],
);
}

View File

@@ -5,6 +5,7 @@ class _SwitchSetting extends StatefulWidget {
required this.title,
required this.settingKey,
this.onChanged,
this.subtitle,
});
final String title;
@@ -13,6 +14,8 @@ class _SwitchSetting extends StatefulWidget {
final VoidCallback? onChanged;
final String? subtitle;
@override
State<_SwitchSetting> createState() => _SwitchSettingState();
}
@@ -24,6 +27,7 @@ class _SwitchSettingState extends State<_SwitchSetting> {
return ListTile(
title: Text(widget.title),
subtitle: widget.subtitle == null ? null : Text(widget.subtitle!),
trailing: Switch(
value: appdata.settings[widget.settingKey],
onChanged: (value) {
@@ -45,6 +49,7 @@ class SelectSetting extends StatelessWidget {
required this.settingKey,
required this.optionTranslation,
this.onChanged,
this.help,
});
final String title;
@@ -55,6 +60,8 @@ class SelectSetting extends StatelessWidget {
final VoidCallback? onChanged;
final String? help;
@override
Widget build(BuildContext context) {
return SizedBox(
@@ -67,6 +74,7 @@ class SelectSetting extends StatelessWidget {
settingKey: settingKey,
optionTranslation: optionTranslation,
onChanged: onChanged,
help: help,
);
} else {
return _EndSelectorSelectSetting(
@@ -74,6 +82,7 @@ class SelectSetting extends StatelessWidget {
settingKey: settingKey,
optionTranslation: optionTranslation,
onChanged: onChanged,
help: help,
);
}
},
@@ -88,6 +97,7 @@ class _DoubleLineSelectSettings extends StatefulWidget {
required this.settingKey,
required this.optionTranslation,
this.onChanged,
this.help,
});
final String title;
@@ -98,6 +108,8 @@ class _DoubleLineSelectSettings extends StatefulWidget {
final VoidCallback? onChanged;
final String? help;
@override
State<_DoubleLineSelectSettings> createState() =>
_DoubleLineSelectSettingsState();
@@ -107,9 +119,37 @@ class _DoubleLineSelectSettingsState extends State<_DoubleLineSelectSettings> {
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(widget.title),
subtitle:
Text(widget.optionTranslation[appdata.settings[widget.settingKey]]!),
title: Row(
children: [
Text(widget.title),
const SizedBox(width: 4),
if (widget.help != null)
Button.icon(
size: 18,
icon: const Icon(Icons.help_outline),
onPressed: () {
showDialog(
context: context,
builder: (context) {
return ContentDialog(
title: "Help".tl,
content: Text(widget.help!).paddingHorizontal(16).fixWidth(double.infinity),
actions: [
Button.filled(
onPressed: context.pop,
child: Text("OK".tl),
),
],
);
},
);
},
),
],
),
subtitle: Text(
widget.optionTranslation[appdata.settings[widget.settingKey]] ??
"None"),
trailing: const Icon(Icons.arrow_drop_down),
onTap: () {
var renderBox = context.findRenderObject() as RenderBox;
@@ -152,6 +192,7 @@ class _EndSelectorSelectSetting extends StatefulWidget {
required this.settingKey,
required this.optionTranslation,
this.onChanged,
this.help,
});
final String title;
@@ -162,6 +203,8 @@ class _EndSelectorSelectSetting extends StatefulWidget {
final VoidCallback? onChanged;
final String? help;
@override
State<_EndSelectorSelectSetting> createState() =>
_EndSelectorSelectSettingState();
@@ -172,10 +215,38 @@ class _EndSelectorSelectSettingState extends State<_EndSelectorSelectSetting> {
Widget build(BuildContext context) {
var options = widget.optionTranslation;
return ListTile(
title: Text(widget.title),
title: Row(
children: [
Text(widget.title),
const SizedBox(width: 4),
if (widget.help != null)
Button.icon(
size: 18,
icon: const Icon(Icons.help_outline),
onPressed: () {
showDialog(
context: context,
builder: (context) {
return ContentDialog(
title: "Help".tl,
content: Text(widget.help!).paddingHorizontal(16).fixWidth(double.infinity),
actions: [
Button.filled(
onPressed: context.pop,
child: Text("OK".tl),
),
],
);
},
);
},
),
],
),
trailing: Select(
current: options[appdata.settings[widget.settingKey]]!,
current: options[appdata.settings[widget.settingKey]],
values: options.values.toList(),
minWidth: 64,
onTap: (index) {
setState(() {
appdata.settings[widget.settingKey] = options.keys.elementAt(index);

View File

@@ -11,10 +11,12 @@ import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/cache_manager.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/consts.dart';
import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/network/app_dio.dart';
import 'package:venera/utils/data.dart';
import 'package:venera/utils/data_sync.dart';
import 'package:venera/utils/io.dart';
import 'package:venera/utils/translations.dart';
import 'package:yaml/yaml.dart';

View File

@@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:isolate';
import 'package:venera/foundation/app.dart';
@@ -35,7 +36,7 @@ Future<File> exportAppData() async {
return cacheFile;
}
Future<void> importAppData(File file) async {
Future<void> importAppData(File file, [bool checkVersion = false]) async {
var cacheDirPath = FilePath.join(App.cachePath, 'temp_data');
var cacheDir = Directory(cacheDirPath);
await Isolate.run(() {
@@ -44,14 +45,21 @@ Future<void> importAppData(File file) async {
var historyFile = cacheDir.joinFile("history.db");
var localFavoriteFile = cacheDir.joinFile("local_favorite.db");
var appdataFile = cacheDir.joinFile("appdata.json");
if(checkVersion && appdataFile.existsSync()) {
var data = jsonDecode(await appdataFile.readAsString());
var version = data["settings"]["dataVersion"];
if(version is int && version <= appdata.settings["dataVersion"]) {
return;
}
}
if(await historyFile.exists()) {
HistoryManager().close();
await historyFile.copy(FilePath.join(App.dataPath, "history.db"));
historyFile.copySync(FilePath.join(App.dataPath, "history.db"));
HistoryManager().init();
}
if(await localFavoriteFile.exists()) {
LocalFavoritesManager().close();
await localFavoriteFile.copy(FilePath.join(App.dataPath, "local_favorite.db"));
localFavoriteFile.copySync(FilePath.join(App.dataPath, "local_favorite.db"));
LocalFavoritesManager().init();
}
if(await appdataFile.exists()) {

205
lib/utils/data_sync.dart Normal file
View File

@@ -0,0 +1,205 @@
import 'package:dio/io.dart';
import 'package:flutter/foundation.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/history.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/foundation/res.dart';
import 'package:venera/network/app_dio.dart';
import 'package:venera/utils/data.dart';
import 'package:venera/utils/ext.dart';
import 'package:webdav_client/webdav_client.dart' hide File;
import 'io.dart';
class DataSync with ChangeNotifier {
DataSync._() {
if (isEnabled) {
downloadData();
}
HistoryManager().addListener(onDataChanged);
LocalFavoritesManager().addListener(onDataChanged);
ComicSource.addListener(onDataChanged);
}
void onDataChanged() {
if (isEnabled) {
uploadData();
}
}
static DataSync? instance;
factory DataSync() => instance ?? (instance = DataSync._());
bool isDownloading = false;
bool isUploading = false;
bool haveWaitingTask = false;
bool get isEnabled {
var config = appdata.settings['webdav'];
return config is List && config.isNotEmpty;
}
List<String>? _validateConfig() {
var config = appdata.settings['webdav'];
if (config is! List || (config.isNotEmpty && config.length != 3)) {
return null;
}
if (config.whereType<String>().length != 3) {
return null;
}
return List.from(config);
}
Future<Res<bool>> uploadData() async {
if (haveWaitingTask) return const Res(true);
while (isDownloading || isUploading) {
haveWaitingTask = true;
await Future.delayed(const Duration(milliseconds: 100));
}
haveWaitingTask = false;
isUploading = true;
notifyListeners();
try {
var config = _validateConfig();
if (config == null) {
return const Res.error('Invalid WebDAV configuration');
}
if (config.isEmpty) {
return const Res(true);
}
String url = config[0];
String user = config[1];
String pass = config[2];
var proxy = await AppDio.getProxy();
var client = newClient(
url,
user: user,
password: pass,
adapter: IOHttpClientAdapter(
createHttpClient: () {
return HttpClient()
..findProxy = (uri) => proxy == null ? "DIRECT" : "PROXY $proxy";
},
),
);
try {
await client.ping();
} catch (e) {
Log.error("Upload Data", 'Failed to connect to WebDAV server');
return const Res.error('Failed to connect to WebDAV server');
}
try {
appdata.settings['dataVersion']++;
await appdata.saveData();
var data = await exportAppData();
var time =
(DateTime.now().millisecondsSinceEpoch ~/ 86400000).toString();
var filename = time;
filename += '-';
filename += appdata.settings['dataVersion'].toString();
filename += '.venera';
var files = await client.readDir('/');
files = files.where((e) => e.name!.endsWith('.venera')).toList();
var old = files.firstWhereOrNull( (e) => e.name!.startsWith("$time-"));
if (old != null) {
await client.remove(old.name!);
}
if (files.length >= 10) {
files.sort((a, b) => a.name!.compareTo(b.name!));
await client.remove(files.first.name!);
}
await client.write(filename, await data.readAsBytes());
Log.info("Upload Data", "Data uploaded successfully");
return const Res(true);
} catch (e, s) {
Log.error("Upload Data", e, s);
return Res.error(e.toString());
}
} finally {
isUploading = false;
notifyListeners();
}
}
Future<Res<bool>> downloadData() async {
if (haveWaitingTask) return const Res(true);
while (isDownloading || isUploading) {
haveWaitingTask = true;
await Future.delayed(const Duration(milliseconds: 100));
}
haveWaitingTask = false;
isDownloading = true;
notifyListeners();
try {
var config = _validateConfig();
if (config == null) {
return const Res.error('Invalid WebDAV configuration');
}
if (config.isEmpty) {
return const Res(true);
}
String url = config[0];
String user = config[1];
String pass = config[2];
var proxy = await AppDio.getProxy();
var client = newClient(
url,
user: user,
password: pass,
adapter: IOHttpClientAdapter(
createHttpClient: () {
return HttpClient()
..findProxy = (uri) => proxy == null ? "DIRECT" : "PROXY $proxy";
},
),
);
try {
await client.ping();
} catch (e) {
Log.error("Data Sync", 'Failed to connect to WebDAV server');
return const Res.error('Failed to connect to WebDAV server');
}
try {
var files = await client.readDir('/');
files.sort((a, b) => b.name!.compareTo(a.name!));
var file = files.firstWhereOrNull((e) => e.name!.endsWith('.venera'));
var version =
file!.name!.split('-').elementAtOrNull(1)?.split('.').first;
if (version != null && int.tryParse(version) != null) {
var currentVersion = appdata.settings['dataVersion'];
if (currentVersion != null && int.parse(version) <= currentVersion) {
Log.info("Data Sync", 'No new data to download');
return const Res(true);
}
}
Log.info("Data Sync", "Downloading data from WebDAV server");
var localFile = File(FilePath.join(App.cachePath, file.name!));
await client.read2File(file.name!, localFile.path);
await importAppData(localFile, true);
await localFile.delete();
Log.info("Data Sync", "Data downloaded successfully");
return const Res(true);
} catch (e, s) {
Log.error("Data Sync", e, s);
return Res.error(e.toString());
}
} finally {
isDownloading = false;
notifyListeners();
}
}
}

View File

@@ -1,5 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:flutter/services.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
@@ -113,6 +114,12 @@ Future<void> copyDirectory(Directory source, Directory destination) async {
}
}
Future<void> copyDirectoryIsolate(Directory source, Directory destination) async {
await Isolate.run(() {
copyDirectory(source, destination);
});
}
String findValidDirectoryName(String path, String directory) {
var name = sanitizeFileName(directory);
var dir = Directory("$path/$name");
@@ -160,6 +167,22 @@ class DirectoryPicker {
}
}
class IOSDirectoryPicker {
static const MethodChannel _channel = MethodChannel("venera/method_channel");
// 调用 iOS 目录选择方法
static Future<String?> selectDirectory() async {
try {
final String? path = await _channel.invokeMethod('selectDirectory');
return path;
} catch (e) {
print("Error selecting directory: $e");
// 返回报错信息
return e.toString();
}
}
}
Future<file_selector.XFile?> selectFile({required List<String> ext}) async {
file_selector.XTypeGroup typeGroup = file_selector.XTypeGroup(
label: 'files',
@@ -181,6 +204,11 @@ Future<String?> selectDirectory() async {
return path;
}
// selectDirectoryIOS
Future<String?> selectDirectoryIOS() async {
return IOSDirectoryPicker.selectDirectory();
}
Future<void> saveFile(
{Uint8List? data, required String filename, File? file}) async {
if (data == null && file == null) {

31
lib/utils/volume.dart Normal file
View File

@@ -0,0 +1,31 @@
import 'dart:async';
import 'package:flutter/services.dart';
class VolumeListener {
static const channel = EventChannel('venera/volume');
void Function()? onUp;
void Function()? onDown;
VolumeListener({this.onUp, this.onDown});
StreamSubscription? stream;
void listen() {
stream = channel.receiveBroadcastStream().listen(onEvent);
}
void onEvent(event) {
if (event == 1) {
onUp!();
} else if (event == 2) {
onDown!();
}
}
void cancel() {
stream?.cancel();
}
}

View File

@@ -10,7 +10,7 @@
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_qjs/flutter_qjs_plugin.h>
#include <gtk/gtk_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h>
@@ -28,9 +28,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) gtk_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
gtk_plugin_register_with_registrar(gtk_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);

View File

@@ -7,7 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
flutter_qjs
gtk
screen_retriever
screen_retriever_linux
sqlite3_flutter_libs
url_launcher_linux
window_manager

View File

@@ -10,7 +10,7 @@ import desktop_webview_window
import file_selector_macos
import flutter_inappwebview_macos
import path_provider_foundation
import screen_retriever
import screen_retriever_macos
import share_plus
import sqlite3_flutter_libs
import url_launcher_macos
@@ -22,7 +22,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))

View File

@@ -345,10 +345,10 @@ packages:
dependency: "direct main"
description:
name: flutter_reorderable_grid_view
sha256: "40abcc5bff228ebff119326502e7357ee6399956b60b80b17385e9770b7458c0"
sha256: "93a2b9e279bf40b9333428a67e70e520ca1528554984eb6f6304538400897e64"
url: "https://pub.dev"
source: hosted
version: "5.0.1"
version: "5.3.2"
flutter_rust_bridge:
dependency: transitive
description:
@@ -593,6 +593,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
url: "https://pub.dev"
source: hosted
version: "11.3.1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1"
url: "https://pub.dev"
source: hosted
version: "12.0.13"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0
url: "https://pub.dev"
source: hosted
version: "9.4.5"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851
url: "https://pub.dev"
source: hosted
version: "0.1.3+2"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9
url: "https://pub.dev"
source: hosted
version: "4.2.3"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev"
source: hosted
version: "6.0.2"
photo_view:
dependency: "direct main"
description:
@@ -638,10 +694,42 @@ packages:
dependency: transitive
description:
name: screen_retriever
sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90"
sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c"
url: "https://pub.dev"
source: hosted
version: "0.1.9"
version: "0.2.0"
screen_retriever_linux:
dependency: transitive
description:
name: screen_retriever_linux
sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_macos:
dependency: transitive
description:
name: screen_retriever_macos
sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_platform_interface:
dependency: transitive
description:
name: screen_retriever_platform_interface
sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_windows:
dependency: transitive
description:
name: screen_retriever_windows
sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
scrollable_positioned_list:
dependency: "direct main"
description:
@@ -856,6 +944,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
webdav_client:
dependency: "direct main"
description:
path: "."
ref: "285f87f15bccd2d5d5ff443761348c6ee47b98d1"
resolved-ref: "285f87f15bccd2d5d5ff443761348c6ee47b98d1"
url: "https://github.com/wgh136/webdav_client"
source: git
version: "1.2.2"
win32:
dependency: transitive
description:
@@ -868,10 +965,10 @@ packages:
dependency: "direct main"
description:
name: window_manager
sha256: ab8b2a7f97543d3db2b506c9d875e637149d48ee0c6a5cb5f5fd6e0dac463792
sha256: "732896e1416297c63c9e3fb95aea72d0355f61390263982a47fd519169dc5059"
url: "https://pub.dev"
source: hosted
version: "0.4.2"
version: "0.4.3"
xdg_directories:
dependency: transitive
description:
@@ -880,6 +977,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.4"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.5.0"
yaml:
dependency: "direct main"
description:

View File

@@ -2,7 +2,7 @@ name: venera
description: "A comic app."
publish_to: 'none'
version: 1.0.2+102
version: 1.0.4+104
environment:
sdk: '>=3.5.0 <4.0.0'
@@ -15,7 +15,7 @@ dependencies:
flutter_localizations:
sdk: flutter
intl: any
window_manager: ^0.4.2
window_manager: ^0.4.3
sqlite3: any
sqlite3_flutter_libs: any
flutter_qjs:
@@ -39,7 +39,7 @@ dependencies:
url: https://github.com/venera-app/flutter.widgets
ref: 09e756b1f1b04e6298318d99ec20a787fb360f59
path: packages/scrollable_positioned_list
flutter_reorderable_grid_view: 5.0.1
flutter_reorderable_grid_view: 5.3.2
yaml: any
uuid: ^4.5.1
desktop_webview_window:
@@ -59,6 +59,10 @@ dependencies:
url: https://github.com/venera-app/lodepng_flutter
ref: d1c96cd6503103b3270dfe2f320d4a1c93780f53
rhttp: 0.9.1
webdav_client:
git:
url: https://github.com/wgh136/webdav_client
ref: 285f87f15bccd2d5d5ff443761348c6ee47b98d1
dev_dependencies:
flutter_test:

View File

@@ -52,9 +52,11 @@ Source: "{#RootPath}\build\windows\x64\runner\Release\WebView2Loader.dll"; DestD
Source: "{#RootPath}\build\windows\x64\runner\Release\share_plus_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\screen_retriever_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\screen_retriever_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\window_manager_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\zip_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\rhttp.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\lodepng_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files

View File

@@ -11,7 +11,8 @@
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
#include <flutter_qjs/flutter_qjs_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
@@ -28,8 +29,10 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
FlutterQjsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterQjsPlugin"));
ScreenRetrieverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar(

View File

@@ -8,7 +8,8 @@ list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
flutter_inappwebview_windows
flutter_qjs
screen_retriever
permission_handler_windows
screen_retriever_windows
share_plus
sqlite3_flutter_libs
url_launcher_windows