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
This commit is contained in:
parent
53ed5a6cd1
commit
f6b496dad7
@ -332,11 +332,24 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
}
|
||||
|
||||
Future<void> _pickFolder() async {
|
||||
final result = await FilePicker.platform.getDirectoryPath(
|
||||
dialogTitle: 'Select a folder containing Claude session files',
|
||||
);
|
||||
if (result != null) {
|
||||
await _scanFolder(result);
|
||||
try {
|
||||
final result = await FilePicker.platform.getDirectoryPath(
|
||||
dialogTitle: 'Select a folder containing Claude session files',
|
||||
);
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -393,14 +406,24 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
}
|
||||
|
||||
Future<void> _loadSingleFile() 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!);
|
||||
try {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['jsonl'],
|
||||
);
|
||||
if (result != null && result.files.single.path != null) {
|
||||
await _loadFile(result.files.single.path!);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[Load] Error: $e');
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Could not open file picker: $e'),
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -491,131 +514,153 @@ 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,
|
||||
// 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
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),
|
||||
),
|
||||
// Path input field
|
||||
SizedBox(
|
||||
width: 320,
|
||||
child: TextField(
|
||||
controller: _pathController,
|
||||
focusNode: _pathFocusNode,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textPrimary,
|
||||
fontFamily: 'JetBrains Mono',
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: '~/ .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,
|
||||
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: '~/.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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
onSubmitted: (_) => _scanFromPathInput(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
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: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
side: const BorderSide(color: AppColors.surfaceBorder),
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
textStyle: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
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: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
side: const BorderSide(color: AppColors.surfaceBorder),
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
textStyle: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user