Add new features and update configurations for improved functionality
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
],
|
||||
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user