Update project structure and enhance functionality with new features and dependencies

This commit is contained in:
ImBenji
2026-04-14 03:31:29 +01:00
parent 0b6b604c56
commit 3588783001
63 changed files with 10565 additions and 789 deletions
+205
View File
@@ -0,0 +1,205 @@
import "package:flutter/widgets.dart";
// Renders text via TextPainter directly, bypassing any theme/font overrides
// from shadcn or other inherited themes. Use this when you need a specific
// font (e.g. google fonts) and the theme keeps clobbering it.
class AnaText extends StatefulWidget {
const AnaText(
this.text, {
required this.style,
this.textAlign = TextAlign.left,
this.maxLines,
super.key,
});
final String text;
final TextStyle style;
final TextAlign textAlign;
final int? maxLines;
@override
State<AnaText> createState() => _AnaTextState();
}
class _AnaTextState extends State<AnaText> {
int _fontVersion = 0;
@override
void initState() {
super.initState();
PaintingBinding.instance.systemFonts.addListener(_onFontChange);
}
void _onFontChange() {
setState(() => _fontVersion++);
}
@override
void dispose() {
PaintingBinding.instance.systemFonts.removeListener(_onFontChange);
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _AnaTextPainter(
text: widget.text,
style: widget.style,
textAlign: widget.textAlign,
maxLines: widget.maxLines,
textDirection: Directionality.of(context),
fontVersion: _fontVersion,
),
child: _AnaTextSizer(
text: widget.text,
style: widget.style,
maxLines: widget.maxLines,
textDirection: Directionality.of(context),
fontVersion: _fontVersion,
),
);
}
}
class _AnaTextPainter extends CustomPainter {
_AnaTextPainter({
required this.text,
required this.style,
required this.textAlign,
required this.textDirection,
required this.fontVersion,
this.maxLines,
});
final String text;
final TextStyle style;
final TextAlign textAlign;
final TextDirection textDirection;
final int? maxLines;
final int fontVersion;
@override
void paint(Canvas canvas, Size size) {
final tp = TextPainter(
text: TextSpan(text: text, style: style),
textAlign: textAlign,
textDirection: textDirection,
maxLines: maxLines,
)..layout(maxWidth: size.width);
final dy = (size.height - tp.height) / 2;
tp.paint(canvas, Offset(0, dy.clamp(0.0, double.infinity)));
}
@override
bool shouldRepaint(_AnaTextPainter old) =>
old.text != text ||
old.style != style ||
old.textAlign != textAlign ||
old.maxLines != maxLines ||
old.fontVersion != fontVersion;
}
// Invisible child that reports the natural text size back to the layout system
// so CustomPaint gets constrained correctly.
class _AnaTextSizer extends LeafRenderObjectWidget {
const _AnaTextSizer({
required this.text,
required this.style,
required this.textDirection,
required this.fontVersion,
this.maxLines,
});
final String text;
final TextStyle style;
final TextDirection textDirection;
final int? maxLines;
final int fontVersion;
@override
RenderObject createRenderObject(BuildContext context) => _AnaTextSizerBox(
text: text,
style: style,
textDirection: textDirection,
maxLines: maxLines,
fontVersion: fontVersion,
);
@override
void updateRenderObject(BuildContext context, _AnaTextSizerBox renderObject) {
renderObject
..text = text
..style = style
..textDirection = textDirection
..maxLines = maxLines
..fontVersion = fontVersion;
}
}
class _AnaTextSizerBox extends RenderBox {
_AnaTextSizerBox({
required String text,
required TextStyle style,
required TextDirection textDirection,
required int fontVersion,
int? maxLines,
}) : _text = text,
_style = style,
_textDirection = textDirection,
_maxLines = maxLines,
_fontVersion = fontVersion;
String _text;
TextStyle _style;
TextDirection _textDirection;
int? _maxLines;
int _fontVersion;
set text(String v) { if (_text == v) return; _text = v; markNeedsLayout(); }
set style(TextStyle v) { if (_style == v) return; _style = v; markNeedsLayout(); }
set textDirection(TextDirection v) {
if (_textDirection == v) return;
_textDirection = v;
markNeedsLayout();
}
set maxLines(int? v) { if (_maxLines == v) return; _maxLines = v; markNeedsLayout(); }
set fontVersion(int v) { if (_fontVersion == v) return; _fontVersion = v; markNeedsLayout(); }
TextPainter _buildPainter({double maxWidth = double.infinity}) {
return TextPainter(
text: TextSpan(text: _text, style: _style),
textDirection: _textDirection,
maxLines: _maxLines,
)..layout(maxWidth: maxWidth);
}
@override
double computeMinIntrinsicWidth(double height) => _buildPainter().width;
@override
double computeMaxIntrinsicWidth(double height) => _buildPainter().width;
@override
double computeMinIntrinsicHeight(double width) =>
_buildPainter(maxWidth: width).height;
@override
double computeMaxIntrinsicHeight(double width) =>
_buildPainter(maxWidth: width).height;
@override
void performLayout() {
final tp = _buildPainter();
size = constraints.constrain(Size(tp.width, tp.height));
}
@override
void paint(PaintingContext context, Offset offset) {}
}
+118
View File
@@ -0,0 +1,118 @@
import "dart:io";
import "package:flutter/foundation.dart";
import "package:google_fonts/google_fonts.dart";
import "package:provider/provider.dart";
import "package:shadcn_flutter/shadcn_flutter.dart";
import "ana_text.dart";
import "../../../src/project_store.dart";
import "../../providers/home_coordinator.dart";
import "../../providers/projects_provider.dart";
class AppHeader extends StatelessWidget {
const AppHeader({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final coordinator = context.read<HomeCoordinator>();
final selectedProject = context.watch<ProjectsProvider>().selectedProject;
final isWindows = !kIsWeb && defaultTargetPlatform == TargetPlatform.windows;
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: theme.colorScheme.background,
border: Border(
bottom: BorderSide(color: theme.colorScheme.border, width: 1),
),
),
child: IntrinsicHeight(
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 12, top: 4, bottom: 4),
child: _AgencyMenuBar(coordinator: coordinator),
),
const Spacer(),
_ProjectNameBox(project: selectedProject),
if (isWindows)
const Gap(6)
else
const Gap(64),
],
),
),
);
}
}
class _AgencyMenuBar extends StatelessWidget {
final HomeCoordinator coordinator;
const _AgencyMenuBar({required this.coordinator});
@override
Widget build(BuildContext context) {
return Menubar(
border: false,
children: [
MenuButton(
subMenu: [
MenuButton(
leading: const Icon(LucideIcons.squarePen).iconSmall,
onPressed: (_) => coordinator.createNewChat(),
child: const Text("New Chat"),
),
MenuButton(
leading: const Icon(LucideIcons.folderPlus).iconSmall,
onPressed: (_) => coordinator.pickProjectDirectory(),
child: const Text("New Project"),
),
],
child: const Text("File"),
),
],
);
}
}
class _ProjectNameBox extends StatelessWidget {
final ProjectRecord? project;
const _ProjectNameBox({required this.project});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final label = project?.name ?? "No project";
final textStyle = GoogleFonts.ibmPlexMono(
fontSize: 11,
height: 1,
fontWeight: FontWeight.w600,
color: theme.colorScheme.mutedForeground,
);
return Container(
color: theme.colorScheme.border,
constraints: const BoxConstraints(minWidth: 100),
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 10),
child: AnaText(label, style: textStyle),
);
}
}
+9 -7
View File
@@ -36,21 +36,23 @@ class _GhostButtonState extends State<AgcGhostButton> {
bg = colorScheme.accent.withOpacity(0.5);
}
final active = widget.onPressed != null;
return MouseRegion(
cursor: widget.onPressed != null ? SystemMouseCursors.click : MouseCursor.defer,
onEnter: (_) => setState(() => _hovering = true),
cursor: active ? SystemMouseCursors.click : MouseCursor.defer,
onEnter: (_) { if (active) setState(() => _hovering = true); },
onExit: (_) => setState(() {
_hovering = false;
_pressing = false;
}),
child: GestureDetector(
onTapDown: (_) => setState(() => _pressing = true),
onTapUp: (_) {
onTapDown: active ? (_) => setState(() => _pressing = true) : null,
onTapUp: active ? (_) {
setState(() => _pressing = false);
if (widget.onPressed != null) widget.onPressed!();
},
onTapCancel: () => setState(() => _pressing = false),
widget.onPressed!();
} : null,
onTapCancel: active ? () => setState(() => _pressing = false) : null,
child: AnimatedContainer(
duration: Duration(milliseconds: 80),
+69 -24
View File
@@ -1,10 +1,12 @@
import "package:flutter/widgets.dart" hide Tooltip;
import "package:google_fonts/google_fonts.dart";
import "package:shadcn_flutter/shadcn_flutter.dart" hide Row, Expanded;
import "package:provider/provider.dart";
import "../../providers/chat_provider.dart";
import "../../providers/cost_provider.dart";
import "../../providers/settings_provider.dart";
import "package:provider/provider.dart";
import "ana_text.dart";
@@ -27,7 +29,7 @@ class FooterBar extends StatelessWidget {
final theme = Theme.of(context);
final mutedFg = theme.colorScheme.mutedForeground;
final borderColor = theme.colorScheme.border;
final bg = theme.colorScheme.muted.scaleAlpha(0.3);
final bg = theme.colorScheme.background;
final costProvider = context.watch<CostProvider>();
final settingsProvider = context.watch<SettingsProvider>();
@@ -39,10 +41,11 @@ class FooterBar extends StatelessWidget {
final inputToks = costProvider.getTotalInputTokens();
final outputToks = costProvider.getTotalOutputTokens();
final isLoading = chatProvider.isLoading;
final isCompacting = chatProvider.isCompacting;
final contextTokens = chatProvider.contextTokens;
final warningState = chatProvider.tokenWarningState;
final textStyle = TextStyle(
fontFamily: "monospace",
final textStyle = GoogleFonts.ibmPlexMono(
fontSize: 11,
height: 1,
fontWeight: FontWeight.w600,
@@ -55,61 +58,103 @@ class FooterBar extends StatelessWidget {
);
Widget copyrightBlock() {
return Text(
return AnaText(
"© 2026 IMBENJI.NET LTD - The Agency",
style: textStyle,
);
}
Widget statusBlock() {
final label = isCompacting
? "compacting..."
: isLoading
? "running..."
: "idle";
final labelColor = isCompacting
? theme.colorScheme.primary.withAlpha(180)
: isLoading
? theme.colorScheme.primary
: mutedFg;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
isLoading ? "running..." : "idle",
style: textStyle.copyWith(
color: isLoading
? theme.colorScheme.primary
: mutedFg,
),
),
AnaText(label, style: textStyle.copyWith(color: labelColor)),
divider(),
Text(model.split("/").last, style: textStyle),
AnaText(model.split("/").last, style: textStyle),
],
);
}
Color? _contextWarningColor() {
if (warningState == null) return null;
if (warningState.isAboveErrorThreshold) return const Color(0xFFEF4444); // red
if (warningState.isAboveWarningThreshold) return const Color(0xFFF59E0B); // amber
return null;
}
Widget statsBlock() {
final warnColor = _contextWarningColor();
return Row(
mainAxisSize: MainAxisSize.min,
children: [
// compact button — shown when we're near the threshold
if (warningState != null && warningState.isAboveWarningThreshold && !isLoading && !isCompacting) ...[
GestureDetector(
onTap: () => chatProvider.runCompact(),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: AnaText(
"compact",
style: textStyle.copyWith(
color: warnColor ?? mutedFg,
decoration: TextDecoration.underline,
decorationColor: warnColor ?? mutedFg,
),
),
),
),
divider(),
],
if (contextTokens > 0) ...[
Text(_fmtTokens(contextTokens), style: textStyle),
Text(" tokens", style: textStyle),
Tooltip(
tooltip: (_) => TooltipContainer(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: AnaText(
warningState != null
? "${warningState.percentLeft}% context remaining"
: _fmtTokens(contextTokens),
style: GoogleFonts.ibmPlexMono(fontSize: 11, height: 1.2),
),
),
child: AnaText(
_fmtTokens(contextTokens),
style: textStyle.copyWith(color: warnColor ?? mutedFg),
),
),
AnaText(" tokens", style: textStyle.copyWith(color: warnColor ?? mutedFg)),
divider(),
],
Tooltip(
tooltip: (_) => TooltipContainer(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: Text(
child: AnaText(
"In: $inputToks\nOut: $outputToks",
style: const TextStyle(
fontFamily: "monospace",
fontSize: 11,
height: 1.2,
),
style: GoogleFonts.ibmPlexMono(fontSize: 11, height: 1.2),
),
),
child: Text(cost, style: textStyle),
child: AnaText(cost, style: textStyle),
),
],
);
}