diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b2c3459..4319b96 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,8 @@ + + + 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, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if(requestCode == storageRequestCode) { + storagePermissionRequest?.invoke(grantResults.all { + it == PackageManager.PERMISSION_GRANTED + }) + storagePermissionRequest = null + } + } } class VolumeListen{ diff --git a/assets/translation.json b/assets/translation.json index 92f5bb0..f213c0a 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -179,7 +179,17 @@ "Move To First": "移动到最前", "Cancel": "取消", "Paused": "已暂停", - "Pause": "暂停" + "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": "首頁", @@ -361,6 +371,16 @@ "Move To First": "移動到最前", "Cancel": "取消", "Paused": "已暫停", - "Pause": "暫停" + "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": "使用音量鍵翻頁" } } \ No newline at end of file diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index ef843d6..4e7bbd0 100644 --- a/lib/foundation/app.dart +++ b/lib/foundation/app.dart @@ -10,7 +10,7 @@ export "widget_utils.dart"; export "context.dart"; class _App { - final version = "1.0.3"; + final version = "1.0.4"; bool get isAndroid => Platform.isAndroid; diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index eae041e..dc93fb8 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -114,6 +114,10 @@ class _Settings with ChangeNotifier { 'enableLongPressToZoom': true, 'checkUpdateOnStart': true, 'limitImageWidth': true, + 'webdav': [], // empty means not configured + 'dataVersion': 0, + 'quickFavorite': null, + 'enableTurnPageByVolumeKey': true, }; operator [](String key) { diff --git a/lib/foundation/favorites.dart b/lib/foundation/favorites.dart index c50f4ed..4ed1cd1 100644 --- a/lib/foundation/favorites.dart +++ b/lib/foundation/favorites.dart @@ -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 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 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 search(String keyword) { @@ -521,6 +531,7 @@ class LocalFavoritesManager { set tags = ? where id == ?; """, [tags.join(","), id]); + notifyListeners(); } final _cachedFavoritedIds = {}; @@ -560,6 +571,7 @@ class LocalFavoritesManager { comic.id, comic.type.value ]); + notifyListeners(); } String folderToJson(String folder) { diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index a539644..8876b84 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -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); diff --git a/lib/foundation/log.dart b/lib/foundation/log.dart index efdda91..ffe051b 100644 --- a/lib/foundation/log.dart +++ b/lib/foundation/log.dart @@ -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(); diff --git a/lib/network/app_dio.dart b/lib/network/app_dio.dart index b190994..af21d6f 100644 --- a/lib/network/app_dio.dart +++ b/lib/network/app_dio.dart @@ -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() { @@ -211,7 +211,7 @@ class AppDio with DioMixin { class RHttpAdapter implements HttpClientAdapter { rhttp.ClientSettings settings; - RHttpAdapter(this.settings) { + RHttpAdapter([this.settings = const rhttp.ClientSettings()]) { settings = settings.copyWith( redirectSettings: const rhttp.RedirectSettings.limited(5), timeoutSettings: const rhttp.TimeoutSettings( @@ -232,12 +232,6 @@ class RHttpAdapter implements HttpClientAdapter { Stream? requestStream, Future? cancelFuture, ) async { - Log.info( - "Network", - "${options.method} ${options.uri}\n" - "Headers: ${options.headers}\n" - "Data: ${options.data}\n", - ); var res = await rhttp.Rhttp.request( method: switch (options.method) { 'GET' => rhttp.HttpMethod.get, diff --git a/lib/pages/comic_page.dart b/lib/pages/comic_page.dart index 3488b66..62b086b 100644 --- a/lib/pages/comic_page.dart +++ b/lib/pages/comic_page.dart @@ -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 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 = []; 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), diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index f4c5641..dc22b35 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -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(); diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 65b4a47..65a432e 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -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 with _ReaderLocation, _ReaderWindow { var focusNode = FocusNode(); + VolumeListener? volumeListener; + @override void initState() { page = widget.initialPage ?? 1; @@ -107,6 +110,9 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { updateHistory(); }); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + if(appdata.settings['enableTurnPageByVolumeKey']) { + handleVolumeEvent(); + } super.initState(); } @@ -115,6 +121,7 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { autoPageTurningTimer?.cancel(); focusNode.dispose(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + stopVolumeEvent(); super.dispose(); } @@ -152,6 +159,31 @@ class _ReaderState extends State 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 { diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 598e173..36ecea3 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -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(); }, ), diff --git a/lib/pages/settings/app.dart b/lib/pages/settings/app.dart index 1e0a1e4..c491f51 100644 --- a/lib/pages/settings/app.dart +++ b/lib/pages/settings/app.dart @@ -32,12 +32,28 @@ class _AppSettingsState extends State { title: "Set New Storage Path".tl, actionTitle: "Set".tl, callback: () async { - var result; + String? result; if (App.isAndroid) { - context.showMessage(message: "Not supported".tl); - return; - } - else if (App.isIOS) { + 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(); @@ -90,8 +106,7 @@ class _AppSettingsState extends State { appdata.settings['cacheSize'] = int.parse(value); appdata.saveData(); setState(() {}); - CacheManager() - .setLimitSize(appdata.settings['cacheSize']); + CacheManager().setLimitSize(appdata.settings['cacheSize']); return null; }, ); @@ -113,13 +128,12 @@ class _AppSettingsState extends State { 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); } @@ -128,6 +142,13 @@ class _AppSettingsState extends State { }, 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, @@ -283,3 +304,129 @@ class _LogsPageState extends State { 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().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( + groupValue: upload, + value: true, + onChanged: (value) { + setState(() { + upload = value!; + }); + }, + ), + Text("Upload".tl), + Radio( + 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), + ), + ); + } +} diff --git a/lib/pages/settings/local_favorites.dart b/lib/pages/settings/local_favorites.dart index ac056e5..9cbce52 100644 --- a/lib/pages/settings/local_favorites.dart +++ b/lib/pages/settings/local_favorites.dart @@ -24,12 +24,20 @@ class _LocalFavoritesSettingsState extends State { 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(), ], ); } diff --git a/lib/pages/settings/reader.dart b/lib/pages/settings/reader.dart index a597736..3635800 100644 --- a/lib/pages/settings/reader.dart +++ b/lib/pages/settings/reader.dart @@ -69,6 +69,14 @@ class _ReaderSettingsState extends State { widget.onChanged?.call('limitImageWidth'); }, ).toSliver(), + if(App.isAndroid) + _SwitchSetting( + title: 'Turn page by volume key'.tl, + settingKey: 'enableTurnPageByVolumeKey', + onChanged: () { + widget.onChanged?.call('enableTurnPageByVolumeKey'); + }, + ).toSliver(), ], ); } diff --git a/lib/pages/settings/setting_components.dart b/lib/pages/settings/setting_components.dart index 9a95270..ee14ddc 100644 --- a/lib/pages/settings/setting_components.dart +++ b/lib/pages/settings/setting_components.dart @@ -49,6 +49,7 @@ class SelectSetting extends StatelessWidget { required this.settingKey, required this.optionTranslation, this.onChanged, + this.help, }); final String title; @@ -59,6 +60,8 @@ class SelectSetting extends StatelessWidget { final VoidCallback? onChanged; + final String? help; + @override Widget build(BuildContext context) { return SizedBox( @@ -71,6 +74,7 @@ class SelectSetting extends StatelessWidget { settingKey: settingKey, optionTranslation: optionTranslation, onChanged: onChanged, + help: help, ); } else { return _EndSelectorSelectSetting( @@ -78,6 +82,7 @@ class SelectSetting extends StatelessWidget { settingKey: settingKey, optionTranslation: optionTranslation, onChanged: onChanged, + help: help, ); } }, @@ -92,6 +97,7 @@ class _DoubleLineSelectSettings extends StatefulWidget { required this.settingKey, required this.optionTranslation, this.onChanged, + this.help, }); final String title; @@ -102,6 +108,8 @@ class _DoubleLineSelectSettings extends StatefulWidget { final VoidCallback? onChanged; + final String? help; + @override State<_DoubleLineSelectSettings> createState() => _DoubleLineSelectSettingsState(); @@ -111,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; @@ -156,6 +192,7 @@ class _EndSelectorSelectSetting extends StatefulWidget { required this.settingKey, required this.optionTranslation, this.onChanged, + this.help, }); final String title; @@ -166,6 +203,8 @@ class _EndSelectorSelectSetting extends StatefulWidget { final VoidCallback? onChanged; + final String? help; + @override State<_EndSelectorSelectSetting> createState() => _EndSelectorSelectSettingState(); @@ -176,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); diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart index f574e4f..ee6337e 100644 --- a/lib/pages/settings/settings_page.dart +++ b/lib/pages/settings/settings_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; @@ -11,10 +12,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'; diff --git a/lib/utils/data.dart b/lib/utils/data.dart index 486aac8..ec55e9b 100644 --- a/lib/utils/data.dart +++ b/lib/utils/data.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:isolate'; import 'package:venera/foundation/app.dart'; @@ -35,7 +36,7 @@ Future exportAppData() async { return cacheFile; } -Future importAppData(File file) async { +Future 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 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()) { diff --git a/lib/utils/data_sync.dart b/lib/utils/data_sync.dart new file mode 100644 index 0000000..b277ae5 --- /dev/null +++ b/lib/utils/data_sync.dart @@ -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? _validateConfig() { + var config = appdata.settings['webdav']; + if (config is! List || (config.isNotEmpty && config.length != 3)) { + return null; + } + if (config.whereType().length != 3) { + return null; + } + return List.from(config); + } + + Future> 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> 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(); + } + } +} diff --git a/lib/utils/io.dart b/lib/utils/io.dart index c714c69..5431f18 100644 --- a/lib/utils/io.dart +++ b/lib/utils/io.dart @@ -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 copyDirectory(Directory source, Directory destination) async { } } +Future 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"); diff --git a/lib/utils/volume.dart b/lib/utils/volume.dart new file mode 100644 index 0000000..fb053ed --- /dev/null +++ b/lib/utils/volume.dart @@ -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(); + } +} \ No newline at end of file diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 37c71c0..e9281b4 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include #include #include @@ -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); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 819bf88..3b7ed15 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -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 diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 9bf393f..4b07183 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -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")) diff --git a/pubspec.lock b/pubspec.lock index 0c55483..4fd2dae 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: @@ -888,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: @@ -912,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: diff --git a/pubspec.yaml b/pubspec.yaml index 30c4f5e..2e81d13 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: venera description: "A comic app." publish_to: 'none' -version: 1.0.3+103 +version: 1.0.4+104 environment: sdk: '>=3.5.0 <4.0.0' @@ -59,6 +59,11 @@ 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 + permission_handler: ^11.3.1 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index c2fe9ba..11d2b2f 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -11,7 +11,8 @@ #include #include #include -#include +#include +#include #include #include #include @@ -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( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index d92ca11..e7ccd0e 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -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