redesign welcome as full-screen overlay with recent projects
Welcome screen is now a full-window overlay that covers the IDE when no project is open. Two-column layout: START actions (open folder, clone from git, start Claude session) with keyboard shortcut hints, and RECENT projects list showing name, path, branch, and relative timestamp. Status line shows version, daemon connection state, and active theme. ProjectManager tracks up to 10 recent projects with path, branch name, and last-opened time, persisted as JSON in app.recentProjects. Boot chain: try last project → try cwd → show welcome. Clicking a recent project opens it directly into Claude. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:clide_app/kernel/src/events/bus.dart';
|
||||
@@ -6,6 +7,41 @@ import 'package:clide_app/kernel/src/log.dart';
|
||||
import 'package:clide_app/kernel/src/settings.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class RecentProject {
|
||||
const RecentProject({required this.path, required this.name, this.branch, required this.lastOpened});
|
||||
|
||||
final String path;
|
||||
final String name;
|
||||
final String? branch;
|
||||
final DateTime lastOpened;
|
||||
|
||||
Map<String, dynamic> toJson() => {'path': path, 'name': name, 'branch': branch, 'lastOpened': lastOpened.toIso8601String()};
|
||||
|
||||
factory RecentProject.fromJson(Map<String, dynamic> json) => RecentProject(
|
||||
path: json['path'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
branch: json['branch'] as String?,
|
||||
lastOpened: DateTime.tryParse(json['lastOpened'] as String? ?? '') ?? DateTime.now(),
|
||||
);
|
||||
|
||||
String get relativePath {
|
||||
final home = Platform.environment['HOME'] ?? '';
|
||||
if (home.isNotEmpty && path.startsWith(home)) return '~${path.substring(home.length)}';
|
||||
return path;
|
||||
}
|
||||
|
||||
String get timeAgo {
|
||||
final diff = DateTime.now().difference(lastOpened);
|
||||
if (diff.inMinutes < 1) return 'just now';
|
||||
if (diff.inMinutes < 60) return '${diff.inMinutes} min ago';
|
||||
if (diff.inHours < 24) return '${diff.inHours} hours ago';
|
||||
if (diff.inDays == 1) return 'yesterday';
|
||||
if (diff.inDays < 7) return '${diff.inDays} days ago';
|
||||
if (diff.inDays < 30) return '${(diff.inDays / 7).floor()} weeks ago';
|
||||
return '${(diff.inDays / 30).floor()} months ago';
|
||||
}
|
||||
}
|
||||
|
||||
class ProjectManager extends ChangeNotifier {
|
||||
ProjectManager({
|
||||
required Logger log,
|
||||
@@ -23,8 +59,23 @@ class ProjectManager extends ChangeNotifier {
|
||||
Directory? get current => _current;
|
||||
bool get isOpen => _current != null;
|
||||
|
||||
/// Open a project by path. Runs `git rev-parse --show-toplevel` to
|
||||
/// find the workspace root. Returns true on success.
|
||||
List<RecentProject> _recents = [];
|
||||
List<RecentProject> get recents => List.unmodifiable(_recents);
|
||||
|
||||
Future<void> loadRecents() async {
|
||||
final raw = _settings.get<String>('app.recentProjects');
|
||||
if (raw == null || raw.isEmpty) {
|
||||
_recents = [];
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final list = (jsonDecode(raw) as List).cast<Map<String, dynamic>>();
|
||||
_recents = list.map(RecentProject.fromJson).toList();
|
||||
} catch (_) {
|
||||
_recents = [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> open(String path) async {
|
||||
final root = await resolveWorkspace(path);
|
||||
if (root == null) {
|
||||
@@ -34,6 +85,14 @@ class ProjectManager extends ChangeNotifier {
|
||||
_current = Directory(root);
|
||||
await _settings.setProjectDir(_current);
|
||||
await _settings.set<String>('app.lastProject', root);
|
||||
|
||||
final branch = await _currentBranch(root);
|
||||
final name = root.split('/').last;
|
||||
_recents.removeWhere((r) => r.path == root);
|
||||
_recents.insert(0, RecentProject(path: root, name: name, branch: branch, lastOpened: DateTime.now()));
|
||||
if (_recents.length > 10) _recents = _recents.sublist(0, 10);
|
||||
await _settings.set<String>('app.recentProjects', jsonEncode(_recents.map((r) => r.toJson()).toList()));
|
||||
|
||||
_events.emit(ProjectOpened(path: root));
|
||||
notifyListeners();
|
||||
return true;
|
||||
@@ -55,16 +114,9 @@ class ProjectManager extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Walks up from [path] via `git rev-parse --show-toplevel`. Returns
|
||||
/// null if the path is outside a git repo or git isn't available.
|
||||
Future<String?> resolveWorkspace(String path) async {
|
||||
try {
|
||||
final r = await Process.run(
|
||||
'git',
|
||||
['rev-parse', '--show-toplevel'],
|
||||
workingDirectory: path,
|
||||
runInShell: false,
|
||||
);
|
||||
final r = await Process.run('git', ['rev-parse', '--show-toplevel'], workingDirectory: path, runInShell: false);
|
||||
if (r.exitCode != 0) return null;
|
||||
final out = (r.stdout as String).trim();
|
||||
return out.isEmpty ? null : out;
|
||||
@@ -73,4 +125,15 @@ class ProjectManager extends ChangeNotifier {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _currentBranch(String root) async {
|
||||
try {
|
||||
final r = await Process.run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], workingDirectory: root, runInShell: false);
|
||||
if (r.exitCode != 0) return null;
|
||||
final out = (r.stdout as String).trim();
|
||||
return out.isEmpty ? null : out;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user