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 {
|
Future<void> _pickFolder() async {
|
||||||
final result = await FilePicker.platform.getDirectoryPath(
|
try {
|
||||||
dialogTitle: 'Select a folder containing Claude session files',
|
final result = await FilePicker.platform.getDirectoryPath(
|
||||||
);
|
dialogTitle: 'Select a folder containing Claude session files',
|
||||||
if (result != null) {
|
);
|
||||||
await _scanFolder(result);
|
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 {
|
Future<void> _loadSingleFile() async {
|
||||||
final home = _getRealHome() ?? '';
|
try {
|
||||||
final result = await FilePicker.platform.pickFiles(
|
final result = await FilePicker.platform.pickFiles(
|
||||||
type: FileType.custom,
|
type: FileType.custom,
|
||||||
allowedExtensions: ['jsonl'],
|
allowedExtensions: ['jsonl'],
|
||||||
initialDirectory: '$home/.claude/projects',
|
);
|
||||||
);
|
if (result != null && result.files.single.path != null) {
|
||||||
if (result != null && result.files.single.path != null) {
|
await _loadFile(result.files.single.path!);
|
||||||
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 ──────────────────────────────
|
// ─── Header with breadcrumb ──────────────────────────────
|
||||||
|
|
||||||
Widget _buildHeader(SessionProvider provider) {
|
Widget _buildHeader(SessionProvider provider) {
|
||||||
return Row(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.terminal, size: 24, color: AppColors.assistant),
|
// Row 1: breadcrumb + count
|
||||||
const SizedBox(width: 10),
|
Row(
|
||||||
// Breadcrumb
|
children: [
|
||||||
InkWell(
|
const Icon(Icons.terminal, size: 24, color: AppColors.assistant),
|
||||||
onTap: () => setState(() => _selectedProject = null),
|
const SizedBox(width: 10),
|
||||||
borderRadius: BorderRadius.circular(4),
|
InkWell(
|
||||||
child: Text(
|
onTap: () => setState(() => _selectedProject = null),
|
||||||
'Projects',
|
borderRadius: BorderRadius.circular(4),
|
||||||
style: TextStyle(
|
child: Text(
|
||||||
fontSize: 18,
|
'Projects',
|
||||||
fontWeight: FontWeight.bold,
|
style: TextStyle(
|
||||||
color: _selectedProject != null
|
fontSize: 18,
|
||||||
? AppColors.assistant
|
fontWeight: FontWeight.bold,
|
||||||
: AppColors.textPrimary,
|
color: _selectedProject != null
|
||||||
decoration: _selectedProject != null
|
? AppColors.assistant
|
||||||
? TextDecoration.underline
|
: AppColors.textPrimary,
|
||||||
: TextDecoration.none,
|
decoration: _selectedProject != null
|
||||||
decorationColor: AppColors.assistant,
|
? 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 SizedBox(height: 12),
|
||||||
const Padding(
|
// Row 2: path input + Load + Browse
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
Row(
|
||||||
child: Icon(Icons.chevron_right, size: 18, color: AppColors.textMuted),
|
children: [
|
||||||
),
|
// Path input field — flexible to fill available space
|
||||||
Flexible(
|
Expanded(
|
||||||
child: Text(
|
child: SizedBox(
|
||||||
_selectedProject!.displayName,
|
height: 36,
|
||||||
style: const TextStyle(
|
child: TextField(
|
||||||
fontSize: 18,
|
controller: _pathController,
|
||||||
fontWeight: FontWeight.bold,
|
focusNode: _pathFocusNode,
|
||||||
color: AppColors.textPrimary,
|
style: const TextStyle(
|
||||||
),
|
fontSize: 12,
|
||||||
overflow: TextOverflow.ellipsis,
|
color: AppColors.textPrimary,
|
||||||
),
|
fontFamily: 'JetBrains Mono',
|
||||||
),
|
),
|
||||||
],
|
decoration: InputDecoration(
|
||||||
const Spacer(),
|
hintText: '~/.claude/projects',
|
||||||
Text(
|
hintStyle: const TextStyle(
|
||||||
_selectedProject != null
|
fontSize: 12,
|
||||||
? '${_selectedProject!.sessionCount} session${_selectedProject!.sessionCount == 1 ? '' : 's'}'
|
color: AppColors.textMuted,
|
||||||
: '${_projects.length} projects',
|
fontFamily: 'JetBrains Mono',
|
||||||
style: const TextStyle(fontSize: 12, color: AppColors.textMuted),
|
),
|
||||||
),
|
isDense: true,
|
||||||
// Path input field
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
SizedBox(
|
horizontal: 10,
|
||||||
width: 320,
|
vertical: 8,
|
||||||
child: TextField(
|
),
|
||||||
controller: _pathController,
|
filled: true,
|
||||||
focusNode: _pathFocusNode,
|
fillColor: AppColors.surface,
|
||||||
style: const TextStyle(
|
border: OutlineInputBorder(
|
||||||
fontSize: 12,
|
borderRadius: BorderRadius.circular(6),
|
||||||
color: AppColors.textPrimary,
|
borderSide: const BorderSide(color: AppColors.surfaceBorder),
|
||||||
fontFamily: 'JetBrains Mono',
|
),
|
||||||
),
|
enabledBorder: OutlineInputBorder(
|
||||||
decoration: InputDecoration(
|
borderRadius: BorderRadius.circular(6),
|
||||||
hintText: '~/ .claude/projects',
|
borderSide: const BorderSide(color: AppColors.surfaceBorder),
|
||||||
hintStyle: const TextStyle(
|
),
|
||||||
fontSize: 12,
|
focusedBorder: OutlineInputBorder(
|
||||||
color: AppColors.textMuted,
|
borderRadius: BorderRadius.circular(6),
|
||||||
fontFamily: 'JetBrains Mono',
|
borderSide: const BorderSide(color: AppColors.assistant),
|
||||||
),
|
),
|
||||||
isDense: true,
|
suffixIcon: IconButton(
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
icon: const Icon(Icons.arrow_forward, size: 14,
|
||||||
horizontal: 10,
|
color: AppColors.textMuted),
|
||||||
vertical: 8,
|
tooltip: 'Scan this folder',
|
||||||
),
|
onPressed: _scanFromPathInput,
|
||||||
filled: true,
|
),
|
||||||
fillColor: AppColors.surface,
|
),
|
||||||
border: OutlineInputBorder(
|
onSubmitted: (_) => _scanFromPathInput(),
|
||||||
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(),
|
const SizedBox(width: 8),
|
||||||
),
|
SizedBox(
|
||||||
),
|
height: 36,
|
||||||
const SizedBox(width: 8),
|
child: ElevatedButton.icon(
|
||||||
ElevatedButton.icon(
|
onPressed: _loadSingleFile,
|
||||||
onPressed: _loadSingleFile,
|
icon: const Icon(Icons.insert_drive_file_outlined, size: 14),
|
||||||
icon: const Icon(Icons.insert_drive_file_outlined, size: 14),
|
label: const Text('Load'),
|
||||||
label: const Text('Load'),
|
style: ElevatedButton.styleFrom(
|
||||||
style: ElevatedButton.styleFrom(
|
backgroundColor: AppColors.surfaceLight,
|
||||||
backgroundColor: AppColors.surfaceLight,
|
foregroundColor: AppColors.textPrimary,
|
||||||
foregroundColor: AppColors.textPrimary,
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 0),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
minimumSize: Size.zero,
|
||||||
shape: RoundedRectangleBorder(
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
borderRadius: BorderRadius.circular(6),
|
shape: RoundedRectangleBorder(
|
||||||
side: const BorderSide(color: AppColors.surfaceBorder),
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
side: const BorderSide(color: AppColors.surfaceBorder),
|
||||||
|
),
|
||||||
|
textStyle: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
textStyle: const TextStyle(fontSize: 12),
|
const SizedBox(width: 8),
|
||||||
),
|
SizedBox(
|
||||||
),
|
height: 36,
|
||||||
const SizedBox(width: 8),
|
child: ElevatedButton.icon(
|
||||||
ElevatedButton.icon(
|
onPressed: _pickFolder,
|
||||||
onPressed: _pickFolder,
|
icon: const Icon(Icons.folder_open, size: 14),
|
||||||
icon: const Icon(Icons.folder_open, size: 14),
|
label: const Text('Browse'),
|
||||||
label: const Text('Browse'),
|
style: ElevatedButton.styleFrom(
|
||||||
style: ElevatedButton.styleFrom(
|
backgroundColor: AppColors.surfaceLight,
|
||||||
backgroundColor: AppColors.surfaceLight,
|
foregroundColor: AppColors.textPrimary,
|
||||||
foregroundColor: AppColors.textPrimary,
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 0),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
minimumSize: Size.zero,
|
||||||
shape: RoundedRectangleBorder(
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
borderRadius: BorderRadius.circular(6),
|
shape: RoundedRectangleBorder(
|
||||||
side: const BorderSide(color: AppColors.surfaceBorder),
|
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