diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 28ca16d..1126638 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -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 = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C0086D062CDEFE64004596D9 /* DirectoryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectoryPicker.swift; sourceTree = ""; }; 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 = ""; }; /* End PBXFileReference section */ @@ -133,6 +135,7 @@ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + C0086D062CDEFE64004596D9 /* DirectoryPicker.swift */, ); path = Runner; sourceTree = ""; @@ -144,7 +147,6 @@ 730F73FE38E23FCF3E461640 /* Pods-Runner.release.xcconfig */, 29B89F848F26E839605E1D88 /* Pods-Runner.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -336,6 +338,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + C0086D072CDEFE6E004596D9 /* DirectoryPicker.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index ee6fbe4..407320b 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -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) } diff --git a/ios/Runner/DirectoryPicker.swift b/ios/Runner/DirectoryPicker.swift new file mode 100644 index 0000000..c3d44a3 --- /dev/null +++ b/ios/Runner/DirectoryPicker.swift @@ -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) + } +} diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 02592e6..93a0af2 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -46,6 +46,10 @@ UIApplicationSupportsIndirectInputEvents NSPhotoLibraryUsageDescription - Choose images + Choose images + UIFileSharingEnabled + + LSSupportsOpeningDocumentsInPlace + diff --git a/lib/pages/local_comics_page.dart b/lib/pages/local_comics_page.dart index eee469b..29e4d38 100644 --- a/lib/pages/local_comics_page.dart +++ b/lib/pages/local_comics_page.dart @@ -24,8 +24,12 @@ class _LocalComicsPageState extends State { bool searchMode = false; + bool multiSelectMode = false; + + Map selectedComics = {}; + void update() { - if(keyword.isEmpty) { + if (keyword.isEmpty) { setState(() { comics = LocalManager().getComics(sortType); }); @@ -95,8 +99,7 @@ class _LocalComicsPageState extends State { 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 { return Scaffold( body: SmoothCustomScrollView( slivers: [ - if(!searchMode) + if (!searchMode && !multiSelectMode) SliverAppbar( title: Text("Local".tl), actions: [ @@ -145,10 +148,38 @@ class _LocalComicsPageState extends State { 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 { ), 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 { 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()); } diff --git a/lib/pages/settings/app.dart b/lib/pages/settings/app.dart index 8583135..fae96fd 100644 --- a/lib/pages/settings/app.dart +++ b/lib/pages/settings/app.dart @@ -20,16 +20,28 @@ class _AppSettingsState extends State { 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) { + var result; + if (App.isAndroid) { context.showMessage(message: "Not supported".tl); return; } - var result = await selectDirectory(); + else if (App.isIOS) { + result = await selectDirectoryIOS(); + } else { + result = await selectDirectory(); + } if (result == null) return; var loadingDialog = showLoadingDialog( App.rootContext, diff --git a/lib/utils/io.dart b/lib/utils/io.dart index a76353d..c714c69 100644 --- a/lib/utils/io.dart +++ b/lib/utils/io.dart @@ -160,6 +160,22 @@ class DirectoryPicker { } } +class IOSDirectoryPicker { + static const MethodChannel _channel = MethodChannel("venera/method_channel"); + + // 调用 iOS 目录选择方法 + static Future selectDirectory() async { + try { + final String? path = await _channel.invokeMethod('selectDirectory'); + return path; + } catch (e) { + print("Error selecting directory: $e"); + // 返回报错信息 + return e.toString(); + } + } +} + Future selectFile({required List ext}) async { file_selector.XTypeGroup typeGroup = file_selector.XTypeGroup( label: 'files', @@ -181,6 +197,11 @@ Future selectDirectory() async { return path; } +// selectDirectoryIOS +Future selectDirectoryIOS() async { + return IOSDirectoryPicker.selectDirectory(); +} + Future saveFile( {Uint8List? data, required String filename, File? file}) async { if (data == null && file == null) {