Compare commits

..

8 Commits

Author SHA1 Message Date
Mathias Beaulieu-Duncan
780ef9378f 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
2026-04-07 14:32:06 -04:00
Mathias Beaulieu-Duncan
0b72c679bc fix: update tooltip - hidden folders not visible in native dialogs
The Cmd+Shift+. shortcut does not work in NSOpenPanel dialogs used by file_picker.
Path input field is the recommended way to access hidden folders like ~/.claude.
Signed, notarized, stapled DMG
2026-04-07 14:23:59 -04:00
Mathias Beaulieu-Duncan
df23483278 ux: add hidden folder hint, better path input placeholder, dialog titles
- Add info tooltip: 'Press Cmd+Shift+. in dialogs to show hidden folders'
- Better path input hint: 'Path to scan (e.g. ~/.claude/projects)'
- Add dialog titles for Load and Browse pickers
- Signed, notarized, stapled DMG
2026-04-07 14:19:22 -04:00
Mathias Beaulieu-Duncan
e658c6c9cf fix: add files.user-selected.read-only entitlement for file_picker
The file_picker plugin requires com.apple.security.files.user-selected.read-only
to be present in the app's entitlements. It checks this at runtime via
SecTaskCopyValueForEntitlement and throws ENTITLEMENT_NOT_FOUND without it.
This was lost during a previous entitlements cleanup.

Signed, notarized, stapled DMG
2026-04-07 14:14:18 -04:00
Mathias Beaulieu-Duncan
ee06c97c06 fix: remove CocoaPods, migrate fully to Swift Package Manager
CocoaPods was conflicting with Swift PM and preventing file_picker
plugin from registering at runtime (MissingPluginException).
All macOS plugins are Swift Packages now — CocoaPods is not needed.

- pod deintegrate + remove Podfile, Podfile.lock, Pods/
- Remove CocoaPods includes from Flutter-Debug.xcconfig and Flutter-Release.xcconfig
- Signed, notarized, stapled DMG
2026-04-07 14:09:01 -04:00
Mathias Beaulieu-Duncan
f6b496dad7 fix: two-row header layout, flexible path input, button error handling
- Split header into two rows: breadcrumb + count on top, path/buttons below
- Path input is now flexible (fills available width) instead of fixed 320px
- Fixes Row overflow on narrow windows
- Load/Browse buttons now same height (36px) as path input
- Added try/catch with snackbar error feedback for Load and Browse
- Removed initialDirectory from file picker (could fail in non-sandbox)
- Browse also updates the path input field when a folder is selected
- Signed, notarized, stapled DMG
2026-04-07 14:01:04 -04:00
Mathias Beaulieu-Duncan
53ed5a6cd1 fix: disable sandbox for ~/.claude access, add path input field for hidden folders
- Remove App Sandbox from Release entitlements (developer tool needs
  filesystem access to ~/.claude/projects — same as VS Code, iTerm2)
- Explicitly set get-task-allow=false in entitlements for notarization
- Add path input field in header so users can type paths with hidden
  folders (e.g. ~/.claude/projects) — press Enter or click arrow to scan
- Field pre-populated with ~/.claude/projects on launch
- Signed, notarized, stapled DMG
2026-04-07 13:55:24 -04:00
Mathias Beaulieu-Duncan
5c693bf3d8 feat: Browse picks folder, Load picks single file, recursive .jsonl scan
- Browse button now opens a folder picker and recursively scans all
  subfolders for .jsonl files, displaying them in the session list
- New Load button for loading a single .jsonl file (old Browse behavior)
- Default path remains ~/.claude/projects on launch
- Signed, notarized, stapled DMG
2026-04-07 13:45:24 -04:00
11 changed files with 440 additions and 229 deletions

View File

@ -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(

View File

@ -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
@ -100,15 +100,27 @@ class _HomeScreenState extends State<HomeScreen> {
bool _scanning = true;
String _searchQuery = '';
_Project? _selectedProject;
_Project? _customFolderProject;
final _pathController = TextEditingController();
final _pathFocusNode = FocusNode();
@override
void initState() {
super.initState();
final home = _getRealHome() ?? '';
_pathController.text = '$home/.claude/projects';
_scanProjects();
}
@override
void dispose() {
_pathController.dispose();
_pathFocusNode.dispose();
super.dispose();
}
/// 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;
@ -290,15 +302,130 @@ class _HomeScreenState extends State<HomeScreen> {
}
}
Future<void> _pickFile() async {
final home = _getRealHome() ?? '';
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['jsonl'],
initialDirectory: '$home/.claude/projects',
);
if (result != null && result.files.single.path != null) {
await _loadFile(result.files.single.path!);
/// Resolve a path string, expanding ~ to the home directory.
String _resolvePath(String input) {
final home = _getRealHome() ?? Platform.environment['HOME'] ?? '';
var path = input.trim();
if (path.startsWith('~/')) {
path = '$home${path.substring(1)}';
} else if (path == '~') {
path = home;
}
return path;
}
Future<void> _scanFromPathInput() async {
final resolved = _resolvePath(_pathController.text);
final dir = Directory(resolved);
if (await dir.exists()) {
await _scanFolder(resolved);
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Folder not found: $resolved'),
backgroundColor: AppColors.error,
),
);
}
}
}
Future<void> _pickFolder() async {
try {
final home = _getRealHome() ?? '';
final result = await NativePicker.getDirectoryPath(
initialDirectory: '$home/.claude/projects',
);
if (result != null) {
_pathController.text = result;
await _scanFolder(result);
}
} catch (e) {
debugPrint('[Browse] Error: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Could not open folder picker: $e'),
backgroundColor: AppColors.error,
),
);
}
}
}
Future<void> _scanFolder(String folderPath) async {
setState(() {
_scanning = true;
_selectedProject = null;
});
final sessions = <_SessionFile>[];
// Recursively scan for .jsonl files
final dir = Directory(folderPath);
if (await dir.exists()) {
await _scanFolderRecursive(dir, sessions);
}
sessions.sort((a, b) => b.stat.modified.compareTo(a.stat.modified));
if (mounted) {
setState(() {
_customFolderProject = _Project(
dirPath: folderPath,
rawDirName: folderPath.split('/').last,
displayName: folderPath.split('/').last,
fullPath: folderPath,
sessions: sessions,
lastModified: sessions.isNotEmpty ? sessions.first.stat.modified : DateTime.now(),
);
_selectedProject = _customFolderProject;
_scanning = false;
});
}
}
Future<void> _scanFolderRecursive(Directory dir, List<_SessionFile> sessions) async {
try {
await for (final entity in dir.list()) {
if (entity is File && entity.path.endsWith('.jsonl')) {
try {
final stat = entity.statSync();
final fileName = entity.path.split('/').last;
sessions.add(_SessionFile(
file: entity,
stat: stat,
sessionId: fileName.replaceAll('.jsonl', ''),
));
} catch (_) {}
} else if (entity is Directory) {
await _scanFolderRecursive(entity, sessions);
}
}
} catch (_) {}
}
Future<void> _loadSingleFile() async {
try {
final home = _getRealHome() ?? '';
final result = await NativePicker.pickFile(
allowedExtensions: ['jsonl'],
initialDirectory: '$home/.claude/projects',
);
if (result != null) {
await _loadFile(result);
}
} catch (e) {
debugPrint('[Load] Error: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Could not open file picker: $e'),
backgroundColor: AppColors.error,
),
);
}
}
}
@ -389,68 +516,161 @@ class _HomeScreenState extends State<HomeScreen> {
// Header with breadcrumb
Widget _buildHeader(SessionProvider provider) {
return Row(
return Column(
children: [
const Icon(Icons.terminal, size: 24, color: AppColors.assistant),
const SizedBox(width: 10),
// Breadcrumb
InkWell(
onTap: () => setState(() => _selectedProject = null),
borderRadius: BorderRadius.circular(4),
child: Text(
'Projects',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: _selectedProject != null
? AppColors.assistant
: AppColors.textPrimary,
decoration: _selectedProject != null
? TextDecoration.underline
: TextDecoration.none,
decorationColor: AppColors.assistant,
),
),
),
if (_selectedProject != null) ...[
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Icon(Icons.chevron_right, size: 18, color: AppColors.textMuted),
),
Flexible(
child: Text(
_selectedProject!.displayName,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
// Row 1: breadcrumb + count
Row(
children: [
const Icon(Icons.terminal, size: 24, color: AppColors.assistant),
const SizedBox(width: 10),
InkWell(
onTap: () => setState(() => _selectedProject = null),
borderRadius: BorderRadius.circular(4),
child: Text(
'Projects',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: _selectedProject != null
? AppColors.assistant
: AppColors.textPrimary,
decoration: _selectedProject != null
? TextDecoration.underline
: TextDecoration.none,
decorationColor: AppColors.assistant,
),
),
overflow: TextOverflow.ellipsis,
),
),
],
const Spacer(),
Text(
_selectedProject != null
? '${_selectedProject!.sessionCount} session${_selectedProject!.sessionCount == 1 ? '' : 's'}'
: '${_projects.length} projects',
style: const TextStyle(fontSize: 12, color: AppColors.textMuted),
if (_selectedProject != null) ...[
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Icon(Icons.chevron_right, size: 18, color: AppColors.textMuted),
),
Flexible(
child: Text(
_selectedProject!.displayName,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
),
overflow: TextOverflow.ellipsis,
),
),
],
const Spacer(),
Text(
_selectedProject != null
? '${_selectedProject!.sessionCount} session${_selectedProject!.sessionCount == 1 ? '' : 's'}'
: '${_projects.length} projects',
style: const TextStyle(fontSize: 12, color: AppColors.textMuted),
),
],
),
const SizedBox(width: 12),
ElevatedButton.icon(
onPressed: _pickFile,
icon: const Icon(Icons.folder_open, size: 14),
label: const Text('Browse'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.surfaceLight,
foregroundColor: AppColors.textPrimary,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
side: const BorderSide(color: AppColors.surfaceBorder),
const SizedBox(height: 12),
// Row 2: path input + Load + Browse
Row(
children: [
// Path input field flexible to fill available space
Expanded(
child: SizedBox(
height: 36,
child: TextField(
controller: _pathController,
focusNode: _pathFocusNode,
style: const TextStyle(
fontSize: 12,
color: AppColors.textPrimary,
fontFamily: 'JetBrains Mono',
),
decoration: InputDecoration(
hintText: 'Path to scan (e.g. ~/.claude/projects)',
hintStyle: const TextStyle(
fontSize: 12,
color: AppColors.textMuted,
fontFamily: 'JetBrains Mono',
),
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 8,
),
filled: true,
fillColor: AppColors.surface,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: const BorderSide(color: AppColors.surfaceBorder),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: const BorderSide(color: AppColors.surfaceBorder),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: const BorderSide(color: AppColors.assistant),
),
suffixIcon: IconButton(
icon: const Icon(Icons.arrow_forward, size: 14,
color: AppColors.textMuted),
tooltip: 'Scan this folder',
onPressed: _scanFromPathInput,
),
),
onSubmitted: (_) => _scanFromPathInput(),
),
),
),
textStyle: const TextStyle(fontSize: 12),
),
const SizedBox(width: 8),
SizedBox(
height: 36,
child: ElevatedButton.icon(
onPressed: _loadSingleFile,
icon: const Icon(Icons.insert_drive_file_outlined, size: 14),
label: const Text('Load'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.surfaceLight,
foregroundColor: AppColors.textPrimary,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 0),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
side: const BorderSide(color: AppColors.surfaceBorder),
),
textStyle: const TextStyle(fontSize: 12),
),
),
),
const SizedBox(width: 8),
SizedBox(
height: 36,
child: ElevatedButton.icon(
onPressed: _pickFolder,
icon: const Icon(Icons.folder_open, size: 14),
label: const Text('Browse'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.surfaceLight,
foregroundColor: AppColors.textPrimary,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 0),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
side: const BorderSide(color: AppColors.surfaceBorder),
),
textStyle: const TextStyle(fontSize: 12),
),
),
),
const SizedBox(width: 8),
Tooltip(
message: 'Type a path with hidden folders directly in the input field',
child: Padding(
padding: const EdgeInsets.only(left: 4),
child: Icon(Icons.info_outline, size: 14, color: AppColors.textMuted),
),
),
],
),
],
);

View File

@ -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}');
}
}
}

View File

@ -1,2 +1 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@ -1,2 +1 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@ -1,42 +0,0 @@
platform :osx, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_macos_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(target)
end
end

View File

@ -1,16 +0,0 @@
PODS:
- FlutterMacOS (1.0.0)
DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral`)
EXTERNAL SOURCES:
FlutterMacOS:
:path: Flutter/ephemeral
SPEC CHECKSUMS:
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
PODFILE CHECKSUM: 89c84cf5c2351c1e554c6dea18d31a879fc3a19e
COCOAPODS: 1.16.2

View File

@ -21,16 +21,15 @@
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
130C39CAC9CC7AA143C489DF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C8D4F4D6A14DBC06CABB730 /* Pods_Runner.framework */; };
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 */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
79F3DEC2140214566E19F388 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6CB8C54BF040E1E6BF05BCBD /* Pods_RunnerTests.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -64,34 +63,27 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
153C2DDE069AD579210ED2C4 /* 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>"; };
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
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>"; };
33CC10FE2044A3C60003C045 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
33CC10FE2044A3C60003C045 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
4BD835AFFC7FFFD2AC416CAE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
4C8D4F4D6A14DBC06CABB730 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
4EB19377A730AE2C095D7A54 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
6CB8C54BF040E1E6BF05BCBD /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
881127F55C6D6CFE2534841B /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
A5398467BE6DA3EC050E790F /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
C0A0ADC8E8ED2668AF72715E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -99,7 +91,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
79F3DEC2140214566E19F388 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -108,7 +99,6 @@
buildActionMask = 2147483647;
files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
130C39CAC9CC7AA143C489DF /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -141,8 +131,6 @@
33CEB47122A05771004F2AC0 /* Flutter */,
331C80D6294CF71000263BE5 /* RunnerTests */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
8208943E7AB9C5C3402F88ED /* Pods */,
);
sourceTree = "<group>";
};
@ -182,6 +170,7 @@
isa = PBXGroup;
children = (
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
AABB001A1A1A1A1A1A1A /* NativePickerRegistrar.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
33CC10FE2044A3C60003C045 /* PrivacyInfo.xcprivacy */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
@ -192,29 +181,6 @@
path = Runner;
sourceTree = "<group>";
};
8208943E7AB9C5C3402F88ED /* Pods */ = {
isa = PBXGroup;
children = (
153C2DDE069AD579210ED2C4 /* Pods-Runner.debug.xcconfig */,
C0A0ADC8E8ED2668AF72715E /* Pods-Runner.release.xcconfig */,
4BD835AFFC7FFFD2AC416CAE /* Pods-Runner.profile.xcconfig */,
881127F55C6D6CFE2534841B /* Pods-RunnerTests.debug.xcconfig */,
4EB19377A730AE2C095D7A54 /* Pods-RunnerTests.release.xcconfig */,
A5398467BE6DA3EC050E790F /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
4C8D4F4D6A14DBC06CABB730 /* Pods_Runner.framework */,
6CB8C54BF040E1E6BF05BCBD /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -222,7 +188,6 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
2D5E86AC9DD4210FFD8C9DE8 /* [CP] Check Pods Manifest.lock */,
331C80D1294CF70F00263BE5 /* Sources */,
331C80D2294CF70F00263BE5 /* Frameworks */,
331C80D3294CF70F00263BE5 /* Resources */,
@ -241,7 +206,6 @@
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
DDC6FD45F59CD5AA33826D72 /* [CP] Check Pods Manifest.lock */,
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
@ -336,28 +300,6 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
2D5E86AC9DD4210FFD8C9DE8 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@ -396,28 +338,6 @@
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
DDC6FD45F59CD5AA33826D72 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@ -435,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;
@ -469,7 +390,6 @@
/* Begin XCBuildConfiguration section */
331C80DB294CF71000263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 881127F55C6D6CFE2534841B /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
@ -484,7 +404,6 @@
};
331C80DC294CF71000263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 4EB19377A730AE2C095D7A54 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
@ -499,7 +418,6 @@
};
331C80DD294CF71000263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A5398467BE6DA3EC050E790F /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
@ -719,18 +637,19 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
CODE_SIGN_IDENTITY = "Developer ID Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
"DEVELOPMENT_TEAM[sdk=macosx*]" = LD76P8L42W;
ENABLE_HARDENED_RUNTIME = YES;
OTHER_CODE_SIGN_FLAGS = "--timestamp --options runtime";
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
OTHER_CODE_SIGN_FLAGS = "--timestamp --options runtime";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};

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)
}
}

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)
}
}
}

View File

@ -2,11 +2,9 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.files.home-directory.read-only</key>
<true/>
<key>com.apple.security.get-task-allow</key>
<false/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>