Update project structure and enhance functionality with new features and dependencies
This commit is contained in:
@@ -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) {}
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user