feat: custom native file picker showing hidden files/folders

Replaced file_picker's Load/Browse with a custom NativePickerRegistrar
Swift plugin that opens NSOpenPanel with showsHiddenFiles = true.
The file_picker package hardcodes this to false, making hidden folders
like ~/.claude invisible in its dialogs.

Changes:
- New NativePickerRegistrar.swift: custom NSOpenPanel with hidden files
- New NativePicker Dart service using method channel
- Browse: only shows folders (canChooseFiles=false), hidden visible
- Load: only shows .jsonl files, hidden folders visible
- Registered via AppDelegate.applicationDidFinishLaunching
- Removed file_picker dependency from home_screen imports
- Fixed all info-level lint issues (super params, null-aware, doc comment)
- Signed, notarized, stapled DMG
This commit is contained in:
Mathias Beaulieu-Duncan
2026-04-07 14:32:06 -04:00
parent 0b72c679bc
commit 780ef9378f
6 changed files with 154 additions and 15 deletions
+4
View File
@@ -24,6 +24,7 @@
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
AABB001B1B1B1B1B1B1B /* NativePickerRegistrar.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABB001A1A1A1A1A1A1A /* NativePickerRegistrar.swift */; };
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC10FF2044A3C60003C045 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10FE2044A3C60003C045 /* PrivacyInfo.xcprivacy */; };
@@ -68,6 +69,7 @@
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* Claude Session Analysis.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Claude Session Analysis.app"; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
AABB001A1A1A1A1A1A1A /* NativePickerRegistrar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePickerRegistrar.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
@@ -168,6 +170,7 @@
isa = PBXGroup;
children = (
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
AABB001A1A1A1A1A1A1A /* NativePickerRegistrar.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
33CC10FE2044A3C60003C045 /* PrivacyInfo.xcprivacy */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
@@ -352,6 +355,7 @@
files = (
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
AABB001B1B1B1B1B1B1B /* NativePickerRegistrar.swift in Sources */,
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
+9
View File
@@ -10,4 +10,13 @@ class AppDelegate: FlutterAppDelegate {
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
override func applicationDidFinishLaunching(_ notification: Notification) {
// Register our custom native picker that shows hidden files
// This is called before Flutter engine starts
if let controller = mainFlutterWindow?.contentViewController as? FlutterViewController {
NativePickerRegistrar.register(with: controller.registrar(forPlugin: "NativePickerRegistrar"))
}
super.applicationDidFinishLaunching(notification)
}
}
+84
View File
@@ -0,0 +1,84 @@
import Cocoa
import FlutterMacOS
import UniformTypeIdentifiers
/// Registers a method channel for native file picking with hidden files visible.
/// Called from Dart via `ensureInitialized`.
class NativePickerRegistrar: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
name: "com.svrnty.native_picker",
binaryMessenger: registrar.messenger)
let instance = NativePickerRegistrar()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getDirectoryPath":
let args = call.arguments as? [String: Any] ?? [:]
let dialog = NSOpenPanel()
if let initial = args["initialDirectory"] as? String, !initial.isEmpty {
dialog.directoryURL = URL(fileURLWithPath: initial)
}
dialog.showsHiddenFiles = true
dialog.canChooseDirectories = true
dialog.canChooseFiles = false
dialog.allowsMultipleSelection = false
dialog.treatsFilePackagesAsDirectories = true
dialog.message = "Select a folder containing session files"
guard let window = NSApp.keyWindow else {
result(FlutterError(code: "NO_WINDOW", message: "No key window found", details: nil))
return
}
dialog.beginSheetModal(for: window) { response in
if response == .OK, let url = dialog.url {
result(url.path)
} else {
result(nil)
}
}
case "pickFiles":
let args = call.arguments as? [String: Any] ?? [:]
let dialog = NSOpenPanel()
if let initial = args["initialDirectory"] as? String, !initial.isEmpty {
dialog.directoryURL = URL(fileURLWithPath: initial)
}
dialog.showsHiddenFiles = true
dialog.canChooseDirectories = false
dialog.canChooseFiles = true
dialog.allowsMultipleSelection = false
if let extensions = args["allowedExtensions"] as? [String], !extensions.isEmpty {
if #available(macOS 11.0, *) {
let contentTypes = extensions.compactMap { UTType(filenameExtension: $0) }
dialog.allowedContentTypes = contentTypes
} else {
dialog.allowedFileTypes = extensions
}
}
dialog.message = "Select a session file"
guard let window = NSApp.keyWindow else {
result(FlutterError(code: "NO_WINDOW", message: "No key window found", details: nil))
return
}
dialog.beginSheetModal(for: window) { response in
if response == .OK, let url = dialog.url {
result([url.path])
} else {
result(nil)
}
}
default:
result(FlutterMethodNotImplemented)
}
}
}