Add new features and update configurations for improved functionality

This commit is contained in:
ImBenji
2026-04-11 12:34:00 +01:00
parent fa4415553d
commit 0b6b604c56
125 changed files with 14119 additions and 1664 deletions
+31
View File
@@ -0,0 +1,31 @@
import "package:shadcn_flutter/shadcn_flutter.dart";
class AppHeader extends StatelessWidget {
const AppHeader({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"THE AGENCY",
style: TextStyle(
fontSize: 32,
height: 1,
fontWeight: FontWeight.w900,
),
),
Text(
"by IMBENJI.NET LTD",
style: TextStyle(
fontSize: 12,
height: 1,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.mutedForeground,
),
),
],
);
}
}
+32
View File
@@ -0,0 +1,32 @@
import "package:shadcn_flutter/shadcn_flutter.dart";
class AppLogo extends StatelessWidget {
const AppLogo({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
"THE AGENCY",
style: TextStyle(
fontSize: 32,
height: 1,
fontWeight: FontWeight.w900,
),
),
Text(
"by IMBENJI.NET LTD",
style: TextStyle(
fontSize: 12,
height: 1,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.mutedForeground,
),
),
],
);
}
}
+112
View File
@@ -0,0 +1,112 @@
import "package:shadcn_flutter/shadcn_flutter.dart";
import '../../utils/format_relative_time.dart';
class ProjectButton extends StatefulWidget {
final String label;
final VoidCallback? onPressed;
final DateTime? lastMessage;
final bool collapsed;
ProjectButton({
required this.label,
this.onPressed,
this.lastMessage,
this.collapsed = false,
});
@override
State<ProjectButton> createState() => ProjectButtonState();
}
class ProjectButtonState extends State<ProjectButton> with TickerProviderStateMixin {
bool _isHovering = false;
late AnimationController _chevronController;
@override
void initState() {
super.initState();
_chevronController = AnimationController(
duration: Duration(milliseconds: 100),
vsync: this,
);
if (!widget.collapsed) {
_chevronController.forward();
}
}
@override
void didUpdateWidget(ProjectButton oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.collapsed != widget.collapsed) {
if (widget.collapsed) {
_chevronController.reverse();
} else {
_chevronController.forward();
}
}
}
@override
void dispose() {
_chevronController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
ColorScheme colorScheme = Theme.of(context).colorScheme;
return SizedBox(
width: double.infinity,
child: Button(
style: ButtonStyle.ghost().copyWith(
padding: (context, state, edgeInsets) {
return EdgeInsets.only(
top: 8,
left: 12,
bottom: 8,
right: 12
);
}
),
disableFocusOutline: true,
onPressed: () {
if (widget.onPressed != null) {
widget.onPressed!();
}
},
onHover: (isHovering) {
setState(() {
_isHovering = isHovering;
});
},
leading: !_isHovering ? Icon(
!widget.collapsed ? LucideIcons.folderOpen : LucideIcons.folderClosed
).iconSmall : RotationTransition(
turns: Tween(begin: 0.0, end: 0.25).animate(_chevronController),
child: Icon(
LucideIcons.chevronRight,
color: colorScheme.mutedForeground,
).iconSmall,
),
trailingGap: 32,
trailing: widget.lastMessage != null ?
Text(
formatRelativeTime(widget.lastMessage!)
).muted : null,
child: Text(
widget.label,
style: TextStyle(
color: colorScheme.mutedForeground
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
).small,
),
);
}
}
@@ -0,0 +1,98 @@
import "package:shadcn_flutter/shadcn_flutter.dart";
import 'project_button.dart';
class ProjectSection extends StatefulWidget {
final String projectLabel;
final List<Widget> children;
final VoidCallback? onHeaderPressed;
ProjectSection({
required this.projectLabel,
this.children = const [],
this.onHeaderPressed,
});
@override
State<ProjectSection> createState() => ProjectSectionState();
}
class ProjectSectionState extends State<ProjectSection> with TickerProviderStateMixin {
bool _isCollapsed = true;
late AnimationController _sizeController;
late AnimationController _fadeController;
@override
void initState() {
super.initState();
_sizeController = AnimationController(
duration: Duration(milliseconds: 150),
vsync: this,
);
_fadeController = AnimationController(
duration: Duration(milliseconds: 250),
vsync: this,
);
if (!_isCollapsed) {
_sizeController.forward();
_fadeController.forward();
}
}
@override
void dispose() {
_sizeController.dispose();
_fadeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
ProjectButton(
label: widget.projectLabel,
collapsed: _isCollapsed,
onPressed: () {
setState(() {
_isCollapsed = !_isCollapsed;
if (_isCollapsed) {
_fadeController.reverse();
_sizeController.reverse();
} else {
_fadeController.forward();
_sizeController.forward();
}
});
widget.onHeaderPressed?.call();
},
),
Gap(2),
ClipRect(
child: Align(
alignment: Alignment.topCenter,
child: FadeTransition(
opacity: _fadeController,
child: SizeTransition(
sizeFactor: _sizeController,
child: Column(
spacing: 2,
children: [
...widget.children
],
),
),
),
),
)
],
);
}
}
+175
View File
@@ -0,0 +1,175 @@
import "package:provider/provider.dart";
import "package:shadcn_flutter/shadcn_flutter.dart";
import '../../../src/session/session_types.dart';
import '../../providers/chat_provider.dart';
import '../../providers/home_coordinator.dart';
import '../../providers/projects_provider.dart';
import '../../providers/session_provider.dart';
import 'app_logo.dart';
import 'project_section.dart';
import 'thread_button.dart';
class Sidebar extends StatelessWidget {
const Sidebar({super.key});
@override
Widget build(BuildContext context) {
final projectsProvider = context.watch<ProjectsProvider>();
final sessionProvider = context.watch<SessionProvider>();
final chatProvider = context.watch<ChatProvider>();
final coordinator = context.read<HomeCoordinator>();
return Container(
width: 300,
color: Theme.of(context).colorScheme.input.scaleAlpha(0.3),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 100,
alignment: Alignment.centerLeft,
padding: EdgeInsets.symmetric(horizontal: 16),
child: AppLogo(),
),
Divider(),
Gap(16),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: _fixedSection(context, coordinator, chatProvider),
),
Gap(16),
Divider(),
Gap(16),
Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: _projectsSection(context, projectsProvider, sessionProvider, coordinator),
),
),
],
),
);
}
Widget _fixedSection(BuildContext context, HomeCoordinator coordinator, ChatProvider chatProvider) {
return Column(
children: [
SizedBox(
width: double.infinity,
child: Button.ghost(
style: ButtonStyle.ghost().copyWith(
padding: (context, state, edgeInsets) {
return EdgeInsets.only(
top: 8,
left: 8,
bottom: 8,
right: 10
);
}
),
onPressed: chatProvider.isLoading ? null : coordinator.createNewChat,
disableFocusOutline: true,
leading: Icon(LucideIcons.squarePen).iconSmall,
alignment: Alignment.centerLeft,
child: Text("New Chat"),
),
),
SizedBox(
width: double.infinity,
child: Button.ghost(
style: ButtonStyle.ghost().copyWith(
padding: (context, state, edgeInsets) {
return EdgeInsets.only(
top: 8,
left: 8,
bottom: 8,
right: 10
);
}
),
onPressed: coordinator.pickProjectDirectory,
disableFocusOutline: true,
leading: Icon(LucideIcons.folderPlus).iconSmall,
alignment: Alignment.centerLeft,
child: Text("New Project"),
),
),
],
);
}
Widget _projectsSection(BuildContext context, ProjectsProvider projectsProvider, SessionProvider sessionProvider, HomeCoordinator coordinator) {
if (projectsProvider.projects.isEmpty) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Text("No projects yet").textSmall.muted,
);
}
// group sessions by working directory
final sessionsByProject = <String, List<SessionSummary>>{};
for (final session in sessionProvider.sessions) {
final dir = session.workingDirectory ?? '';
sessionsByProject.putIfAbsent(dir, () => []).add(session);
}
// sort sessions within each project newest first
final sorted = <String, List<SessionSummary>>{};
sessionsByProject.forEach((dir, sessions) {
sorted[dir] = List<SessionSummary>.from(sessions)
..sort((a, b) => b.updated.compareTo(a.updated));
});
return ListView(
padding: EdgeInsets.zero,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text("Projects").textMuted,
),
Gap(8),
for (final project in projectsProvider.projects) ...[
ProjectSection(
projectLabel: project.name,
children: [
if (sorted[project.workingDirectory]?.isEmpty ?? true)
ThreadButton(
label: "No threads yet",
muted: true,
)
else
for (final session in sorted[project.workingDirectory]!)
ThreadButton(
label: session.name,
lastMessage: session.updated,
selected: sessionProvider.currentSessionId == session.id,
onPressed: () => coordinator.openSession(session),
onDelete: () => coordinator.deleteSession(session),
),
],
),
Gap(2),
],
],
);
}
}
+81
View File
@@ -0,0 +1,81 @@
import "package:shadcn_flutter/shadcn_flutter.dart";
import '../../utils/format_relative_time.dart';
class ThreadButton extends StatelessWidget {
final String label;
final IconData? icon;
final VoidCallback? onPressed;
final VoidCallback? onDelete;
final DateTime? lastMessage;
final bool selected;
final bool muted;
ThreadButton({
required this.label,
this.icon,
this.onPressed,
this.onDelete,
this.lastMessage,
this.selected = false,
this.muted = false,
});
@override
Widget build(BuildContext context) {
ButtonStyle style = selected ? ButtonStyle.secondary() : ButtonStyle.ghost();
ColorScheme colorScheme = Theme.of(context).colorScheme;
final button = SizedBox(
width: double.infinity,
child: Button(
style: style.copyWith(
padding: (context, state, edgeInsets) {
return EdgeInsets.only(
top: 8,
left: 12,
bottom: 8,
right: 12
);
}
),
disableFocusOutline: true,
onPressed: onPressed ?? () {},
enabled: onPressed != null,
leading: Icon(
icon,
color: icon == null ? Colors.transparent : (muted ? colorScheme.mutedForeground : null),
).iconSmall,
trailingGap: 32,
trailing: lastMessage != null ?
Text(
formatRelativeTime(lastMessage!)
).muted.small.light : null,
child: Text(
label,
style: TextStyle(
color: (muted ? colorScheme.mutedForeground : null)
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
).small.light,
),
);
if (onDelete == null) return button;
return ContextMenu(
items: [
MenuButton(
onPressed: (_) => onDelete!(),
child: const Text("Delete"),
),
],
child: button,
);
}
}