Merge branch 'refs/heads/master' into dev

This commit is contained in:
2024-11-10 16:01:45 +08:00
7 changed files with 168 additions and 17 deletions

View File

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

View File

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

@@ -47,5 +47,9 @@
<true/> <true/>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>Choose images</string> <string>Choose images</string>
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
</dict> </dict>
</plist> </plist>

View File

@@ -24,6 +24,10 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
bool searchMode = false; bool searchMode = false;
bool multiSelectMode = false;
Map<LocalComic, bool> selectedComics = {};
void update() { void update() {
if (keyword.isEmpty) { if (keyword.isEmpty) {
setState(() { setState(() {
@@ -95,8 +99,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
actions: [ actions: [
FilledButton( FilledButton(
onPressed: () { onPressed: () {
appdata.implicitData["local_sort"] = appdata.implicitData["local_sort"] =sortType.value;
sortType.value;
appdata.writeImplicitData(); appdata.writeImplicitData();
Navigator.pop(context); Navigator.pop(context);
update(); update();
@@ -115,7 +118,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
return Scaffold( return Scaffold(
body: SmoothCustomScrollView( body: SmoothCustomScrollView(
slivers: [ slivers: [
if(!searchMode) if (!searchMode && !multiSelectMode)
SliverAppbar( SliverAppbar(
title: Text("Local".tl), title: Text("Local".tl),
actions: [ actions: [
@@ -145,10 +148,38 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
showPopUpWidget(context, const DownloadingPage()); 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( SliverAppbar(
title: TextField( title: TextField(
autofocus: true, autofocus: true,
@@ -176,7 +207,17 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
), ),
SliverGridComics( SliverGridComics(
comics: comics, comics: comics,
onTap: (c) { 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(); (c as LocalComic).read();
}, },
menuBuilder: (c) { menuBuilder: (c) {
@@ -185,7 +226,23 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
icon: Icons.delete, icon: Icons.delete,
text: "Delete".tl, text: "Delete".tl,
onClick: () { onClick: () {
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); LocalManager().deleteComic(c as LocalComic);
}
}), }),
MenuEntry( MenuEntry(
icon: Icons.outbox_outlined, icon: Icons.outbox_outlined,
@@ -196,9 +253,20 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
allowCancel: false, allowCancel: false,
); );
try { try {
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); var file = await CBZ.export(c as LocalComic);
await saveFile(filename: file.name, file: file); await saveFile(filename: file.name, file: file);
await file.delete(); await file.delete();
}
} catch (e) { } catch (e) {
context.showMessage(message: e.toString()); context.showMessage(message: e.toString());
} }

View File

@@ -20,16 +20,28 @@ class _AppSettingsState extends State<AppSettings> {
ListTile( ListTile(
title: Text("Storage Path for local comics".tl), title: Text("Storage Path for local comics".tl),
subtitle: Text(LocalManager().path, softWrap: false), 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(), ).toSliver(),
_CallbackSetting( _CallbackSetting(
title: "Set New Storage Path".tl, title: "Set New Storage Path".tl,
actionTitle: "Set".tl, actionTitle: "Set".tl,
callback: () async { callback: () async {
if (App.isMobile) { var result;
if (App.isAndroid) {
context.showMessage(message: "Not supported".tl); context.showMessage(message: "Not supported".tl);
return; return;
} }
var result = await selectDirectory(); else if (App.isIOS) {
result = await selectDirectoryIOS();
} else {
result = await selectDirectory();
}
if (result == null) return; if (result == null) return;
var loadingDialog = showLoadingDialog( var loadingDialog = showLoadingDialog(
App.rootContext, App.rootContext,

View File

@@ -160,6 +160,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 { Future<file_selector.XFile?> selectFile({required List<String> ext}) async {
file_selector.XTypeGroup typeGroup = file_selector.XTypeGroup( file_selector.XTypeGroup typeGroup = file_selector.XTypeGroup(
label: 'files', label: 'files',
@@ -181,6 +197,11 @@ Future<String?> selectDirectory() async {
return path; return path;
} }
// selectDirectoryIOS
Future<String?> selectDirectoryIOS() async {
return IOSDirectoryPicker.selectDirectory();
}
Future<void> saveFile( Future<void> saveFile(
{Uint8List? data, required String filename, File? file}) async { {Uint8List? data, required String filename, File? file}) async {
if (data == null && file == null) { if (data == null && file == null) {