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:
@@ -22,8 +22,8 @@ class ContentBlock {
|
||||
class TextBlock extends ContentBlock {
|
||||
final String text;
|
||||
|
||||
TextBlock({required this.text, required Map<String, dynamic> raw})
|
||||
: super(type: 'text', raw: raw);
|
||||
TextBlock({required this.text, required super.raw})
|
||||
: super(type: 'text');
|
||||
|
||||
factory TextBlock.fromJson(Map<String, dynamic> json) {
|
||||
return TextBlock(
|
||||
@@ -40,8 +40,8 @@ class ThinkingBlock extends ContentBlock {
|
||||
ThinkingBlock({
|
||||
required this.thinking,
|
||||
this.signature,
|
||||
required Map<String, dynamic> raw,
|
||||
}) : super(type: 'thinking', raw: raw);
|
||||
required super.raw,
|
||||
}) : super(type: 'thinking');
|
||||
|
||||
factory ThinkingBlock.fromJson(Map<String, dynamic> json) {
|
||||
return ThinkingBlock(
|
||||
@@ -63,8 +63,8 @@ class ToolUseBlock extends ContentBlock {
|
||||
required this.name,
|
||||
required this.input,
|
||||
this.linkedResult,
|
||||
required Map<String, dynamic> raw,
|
||||
}) : super(type: 'tool_use', raw: raw);
|
||||
required super.raw,
|
||||
}) : super(type: 'tool_use');
|
||||
|
||||
factory ToolUseBlock.fromJson(Map<String, dynamic> json) {
|
||||
return ToolUseBlock(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'dart:io';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../providers/session_provider.dart';
|
||||
import '../../services/native_picker.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
|
||||
// ─── Data models ─────────────────────────────────────────────
|
||||
@@ -120,7 +120,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
}
|
||||
|
||||
/// Returns the real user home directory, even inside App Sandbox.
|
||||
/// In sandbox, HOME points to ~/Library/Containers/<bundleid>/Data.
|
||||
/// In sandbox, HOME points to `~/Library/Containers/bundleid/Data`.
|
||||
String? _getRealHome() {
|
||||
final home = Platform.environment['HOME'];
|
||||
if (home == null) return null;
|
||||
@@ -333,8 +333,9 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
|
||||
Future<void> _pickFolder() async {
|
||||
try {
|
||||
final result = await FilePicker.platform.getDirectoryPath(
|
||||
dialogTitle: 'Select a folder containing Claude session files',
|
||||
final home = _getRealHome() ?? '';
|
||||
final result = await NativePicker.getDirectoryPath(
|
||||
initialDirectory: '$home/.claude/projects',
|
||||
);
|
||||
if (result != null) {
|
||||
_pathController.text = result;
|
||||
@@ -407,13 +408,13 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
|
||||
Future<void> _loadSingleFile() async {
|
||||
try {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
final home = _getRealHome() ?? '';
|
||||
final result = await NativePicker.pickFile(
|
||||
allowedExtensions: ['jsonl'],
|
||||
dialogTitle: 'Select a Claude session file (.jsonl)',
|
||||
initialDirectory: '$home/.claude/projects',
|
||||
);
|
||||
if (result != null && result.files.single.path != null) {
|
||||
await _loadFile(result.files.single.path!);
|
||||
if (result != null) {
|
||||
await _loadFile(result);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[Load] Error: $e');
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Native file picker that shows hidden files/folders on macOS.
|
||||
/// Uses a custom method channel to NSOpenPanel with showsHiddenFiles = true,
|
||||
/// which the file_picker package hardcodes to false.
|
||||
class NativePicker {
|
||||
static const _channel = MethodChannel('com.svrnty.native_picker');
|
||||
|
||||
/// Opens a folder picker that shows hidden directories.
|
||||
/// Returns the selected folder path, or null if cancelled.
|
||||
static Future<String?> getDirectoryPath({String? initialDirectory}) async {
|
||||
try {
|
||||
return _channel.invokeMethod<String?>('getDirectoryPath', {
|
||||
'initialDirectory': initialDirectory,
|
||||
});
|
||||
} on PlatformException catch (e) {
|
||||
throw Exception('Native picker error: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a file picker that shows hidden files, filtered to specific extensions.
|
||||
/// Returns the selected file path, or null if cancelled.
|
||||
static Future<String?> pickFile({
|
||||
List<String> allowedExtensions = const [],
|
||||
String? initialDirectory,
|
||||
}) async {
|
||||
try {
|
||||
final result = await _channel.invokeMethod<List?>('pickFiles', {
|
||||
'allowMultiple': false,
|
||||
'allowedExtensions': allowedExtensions,
|
||||
'initialDirectory': initialDirectory,
|
||||
});
|
||||
if (result != null && result.isNotEmpty) {
|
||||
return result.first as String;
|
||||
}
|
||||
return null;
|
||||
} on PlatformException catch (e) {
|
||||
throw Exception('Native picker error: ${e.message}');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user