193 lines
No EOL
6.1 KiB
Dart
193 lines
No EOL
6.1 KiB
Dart
import "package:shadcn_flutter/shadcn_flutter.dart";
|
|
|
|
import "../../../../src/project_store.dart";
|
|
import "../../../../src/session/session_types.dart";
|
|
import "../../../providers/projects_provider.dart";
|
|
import "../../../providers/session_provider.dart";
|
|
|
|
class ThreadsSection extends StatelessWidget {
|
|
const ThreadsSection({
|
|
required this.projectsProvider,
|
|
required this.sessionProvider,
|
|
required this.sessionsByProject,
|
|
required this.onOpenSession,
|
|
required this.onSelectProject,
|
|
required this.onDeleteSession,
|
|
});
|
|
|
|
final ProjectsProvider projectsProvider;
|
|
final SessionProvider sessionProvider;
|
|
final Map<String, List<SessionSummary>> sessionsByProject;
|
|
final ValueChanged<SessionSummary> onOpenSession;
|
|
final ValueChanged<ProjectRecord> onSelectProject;
|
|
final ValueChanged<SessionSummary> onDeleteSession;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Sort sessions by update time (newest first) within each project
|
|
final sortedSessionsByProject = <String, List<SessionSummary>>{};
|
|
sessionsByProject.forEach((workingDirectory, sessions) {
|
|
final sortedSessions = List<SessionSummary>.from(sessions)
|
|
..sort((a, b) => b.updated.compareTo(a.updated));
|
|
sortedSessionsByProject[workingDirectory] = sortedSessions;
|
|
});
|
|
|
|
return ListView(
|
|
padding: const EdgeInsets.fromLTRB(8, 0, 8, 12),
|
|
children: [
|
|
if (projectsProvider.projects.isEmpty)
|
|
const _SidebarHint(text: "No projects yet")
|
|
else
|
|
for (final project in projectsProvider.projects) ...[
|
|
// Project header
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: Button.ghost(
|
|
onPressed: () => onSelectProject(project),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 6),
|
|
child: Text(
|
|
project.name,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 12,
|
|
color: Theme.of(context).colorScheme.mutedForeground,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Project sessions
|
|
if (sortedSessionsByProject[project.workingDirectory]?.isEmpty ??
|
|
true)
|
|
const Padding(
|
|
padding: EdgeInsets.fromLTRB(8, 4, 8, 8),
|
|
child: _SidebarHint(text: "No threads yet"),
|
|
)
|
|
else
|
|
for (final session
|
|
in sortedSessionsByProject[project.workingDirectory]!)
|
|
_SidebarSessionTile(
|
|
session: session,
|
|
isSelected: sessionProvider.currentSessionId == session.id,
|
|
onTap: () => onOpenSession(session),
|
|
onDelete: () => onDeleteSession(session),
|
|
),
|
|
const Divider(height: 16),
|
|
],
|
|
// Handle sessions that don't belong to any current project
|
|
if (sortedSessionsByProject.keys.any(
|
|
(key) => !projectsProvider.projects.any(
|
|
(project) => project.workingDirectory == key,
|
|
),
|
|
)) ...[
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(8, 8, 8, 4),
|
|
child: Text(
|
|
"Sessions Without Projects",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 12,
|
|
color: Theme.of(context).colorScheme.mutedForeground,
|
|
),
|
|
),
|
|
),
|
|
for (final entry in sortedSessionsByProject.entries)
|
|
if (!projectsProvider.projects.any(
|
|
(project) => project.workingDirectory == entry.key,
|
|
) &&
|
|
entry.key.isNotEmpty)
|
|
for (final session in entry.value)
|
|
_SidebarSessionTile(
|
|
session: session,
|
|
isSelected: sessionProvider.currentSessionId == session.id,
|
|
onTap: () => onOpenSession(session),
|
|
onDelete: () => onDeleteSession(session),
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SidebarHint extends StatelessWidget {
|
|
const _SidebarHint({required this.text});
|
|
|
|
final String text;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
|
child: Text(text).textSmall.muted,
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SidebarSessionTile extends StatelessWidget {
|
|
const _SidebarSessionTile({
|
|
required this.session,
|
|
required this.isSelected,
|
|
required this.onTap,
|
|
required this.onDelete,
|
|
});
|
|
|
|
final SessionSummary session;
|
|
final bool isSelected;
|
|
final VoidCallback onTap;
|
|
final VoidCallback onDelete;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ContextMenu(
|
|
items: [
|
|
MenuButton(
|
|
onPressed: (context) {
|
|
onDelete();
|
|
},
|
|
child: const Text("Delete"),
|
|
),
|
|
],
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
child: Button(
|
|
style: isSelected ? ButtonStyle.secondary() : ButtonStyle.ghost(),
|
|
child: Text(
|
|
session.name,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(fontWeight: FontWeight.w400, fontSize: 13),
|
|
).textSmall,
|
|
trailing: Text(
|
|
_formatRelativeTime(session.updated),
|
|
style: TextStyle(fontWeight: FontWeight.w400, fontSize: 13),
|
|
).muted.textSmall,
|
|
onPressed: () {
|
|
onTap();
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
String _formatRelativeTime(DateTime timestamp) {
|
|
final difference = DateTime.now().toUtc().difference(timestamp.toUtc());
|
|
|
|
if (difference.inMinutes < 1) {
|
|
return "just now";
|
|
}
|
|
if (difference.inHours < 1) {
|
|
return "${difference.inMinutes}m";
|
|
}
|
|
if (difference.inDays < 1) {
|
|
return "${difference.inHours}h";
|
|
}
|
|
if (difference.inDays < 7) {
|
|
return "${difference.inDays}d";
|
|
}
|
|
|
|
final month = timestamp.month.toString().padLeft(2, "0");
|
|
final day = timestamp.day.toString().padLeft(2, "0");
|
|
return "${timestamp.year}-$month-$day";
|
|
} |