2025-08-10 01:20:45 +05:30
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import '../theme/theme_extensions.dart';
|
|
|
|
|
import 'package:flutter/cupertino.dart';
|
|
|
|
|
import 'dart:io' show Platform;
|
2025-10-03 00:12:25 +05:30
|
|
|
import '../theme/color_tokens.dart';
|
2025-10-18 13:58:15 +05:30
|
|
|
import '../theme/tweakcn_themes.dart';
|
2025-08-10 01:20:45 +05:30
|
|
|
|
|
|
|
|
/// Centralized service for consistent brand identity throughout the app
|
|
|
|
|
/// Uses the hub icon as the primary brand element
|
|
|
|
|
class BrandService {
|
|
|
|
|
BrandService._();
|
|
|
|
|
|
2025-08-16 15:51:27 +05:30
|
|
|
/// Primary brand icon - the hub icon (consistent across platforms)
|
|
|
|
|
static IconData get primaryIcon => Icons.hub;
|
2025-08-10 01:20:45 +05:30
|
|
|
|
|
|
|
|
/// Alternative brand icons for different contexts
|
2025-08-16 15:51:27 +05:30
|
|
|
static IconData get primaryIconOutlined => Icons.hub_outlined;
|
2025-08-10 01:20:45 +05:30
|
|
|
static IconData get connectivityIcon =>
|
2025-08-16 15:51:27 +05:30
|
|
|
Platform.isIOS ? CupertinoIcons.wifi : Icons.wifi;
|
2025-08-10 01:20:45 +05:30
|
|
|
static IconData get networkIcon =>
|
2025-08-16 15:51:27 +05:30
|
|
|
Platform.isIOS ? CupertinoIcons.globe : Icons.public;
|
2025-08-10 01:20:45 +05:30
|
|
|
|
|
|
|
|
/// Brand colors - these should be accessed through context.conduitTheme in UI components
|
2025-10-02 01:58:12 +05:30
|
|
|
static Color primaryBrandColor({
|
|
|
|
|
BuildContext? context,
|
|
|
|
|
Brightness? brightness,
|
|
|
|
|
}) {
|
|
|
|
|
final palette = _resolvePalette(context);
|
|
|
|
|
final resolvedBrightness = brightness ?? _resolveBrightness(context);
|
2025-10-18 13:58:15 +05:30
|
|
|
return palette.variantFor(resolvedBrightness).primary;
|
2025-10-02 01:58:12 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static Color secondaryBrandColor({
|
|
|
|
|
BuildContext? context,
|
|
|
|
|
Brightness? brightness,
|
|
|
|
|
}) {
|
|
|
|
|
final palette = _resolvePalette(context);
|
|
|
|
|
final resolvedBrightness = brightness ?? _resolveBrightness(context);
|
2025-10-18 13:58:15 +05:30
|
|
|
return palette.variantFor(resolvedBrightness).secondary;
|
2025-10-02 01:58:12 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static Color accentBrandColor({
|
|
|
|
|
BuildContext? context,
|
|
|
|
|
Brightness? brightness,
|
|
|
|
|
}) {
|
|
|
|
|
final palette = _resolvePalette(context);
|
|
|
|
|
final resolvedBrightness = brightness ?? _resolveBrightness(context);
|
2025-10-18 13:58:15 +05:30
|
|
|
return palette.variantFor(resolvedBrightness).accent;
|
2025-10-02 01:58:12 +05:30
|
|
|
}
|
2025-08-10 01:20:45 +05:30
|
|
|
|
|
|
|
|
/// Creates a branded icon with consistent styling
|
|
|
|
|
static Widget createBrandIcon({
|
|
|
|
|
double size = 24,
|
|
|
|
|
Color? color,
|
|
|
|
|
IconData? icon,
|
|
|
|
|
bool useGradient = false,
|
|
|
|
|
bool addShadow = false,
|
2025-10-02 01:58:12 +05:30
|
|
|
BuildContext? context,
|
2025-08-10 01:20:45 +05:30
|
|
|
}) {
|
|
|
|
|
final iconData = icon ?? primaryIcon;
|
2025-10-02 01:58:12 +05:30
|
|
|
final resolvedColor = color ?? primaryBrandColor(context: context);
|
2025-08-10 01:20:45 +05:30
|
|
|
|
|
|
|
|
Widget iconWidget = Icon(
|
|
|
|
|
iconData,
|
|
|
|
|
size: size,
|
2025-10-02 01:58:12 +05:30
|
|
|
color: useGradient ? null : resolvedColor,
|
2025-08-10 01:20:45 +05:30
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (useGradient) {
|
|
|
|
|
iconWidget = ShaderMask(
|
|
|
|
|
blendMode: BlendMode.srcIn,
|
|
|
|
|
shaderCallback: (bounds) => LinearGradient(
|
2025-10-02 01:58:12 +05:30
|
|
|
colors: [
|
|
|
|
|
primaryBrandColor(context: context),
|
|
|
|
|
secondaryBrandColor(context: context),
|
|
|
|
|
],
|
2025-08-10 01:20:45 +05:30
|
|
|
).createShader(bounds),
|
|
|
|
|
child: Icon(iconData, size: size),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (addShadow) {
|
|
|
|
|
iconWidget = Container(
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
boxShadow: [
|
|
|
|
|
BoxShadow(
|
2025-10-02 01:58:12 +05:30
|
|
|
color: primaryBrandColor(context: context).withValues(alpha: 0.3),
|
2025-08-10 01:20:45 +05:30
|
|
|
blurRadius: size * 0.3,
|
|
|
|
|
offset: Offset(0, size * 0.1),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
child: iconWidget,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return iconWidget;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Creates a branded avatar with the hub icon
|
|
|
|
|
static Widget createBrandAvatar({
|
|
|
|
|
double size = 40,
|
|
|
|
|
Color? backgroundColor,
|
|
|
|
|
Color? iconColor,
|
|
|
|
|
bool useGradient = true,
|
|
|
|
|
String? fallbackText,
|
|
|
|
|
BuildContext? context,
|
|
|
|
|
}) {
|
2025-10-02 01:58:12 +05:30
|
|
|
final bgColor = backgroundColor ?? primaryBrandColor(context: context);
|
2025-10-03 00:12:25 +05:30
|
|
|
final tokens = _resolveTokens(context);
|
2025-08-10 01:20:45 +05:30
|
|
|
final iColor =
|
2025-10-03 00:12:25 +05:30
|
|
|
iconColor ??
|
|
|
|
|
(context?.conduitTheme.textInverse ?? tokens.neutralTone00);
|
2025-08-10 01:20:45 +05:30
|
|
|
|
|
|
|
|
return Container(
|
|
|
|
|
width: size,
|
|
|
|
|
height: size,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
gradient: useGradient
|
|
|
|
|
? LinearGradient(
|
|
|
|
|
begin: Alignment.topLeft,
|
|
|
|
|
end: Alignment.bottomRight,
|
2025-10-02 01:58:12 +05:30
|
|
|
colors: [
|
|
|
|
|
primaryBrandColor(context: context),
|
|
|
|
|
secondaryBrandColor(context: context),
|
|
|
|
|
],
|
2025-08-10 01:20:45 +05:30
|
|
|
)
|
|
|
|
|
: null,
|
|
|
|
|
color: useGradient ? null : bgColor,
|
|
|
|
|
borderRadius: BorderRadius.circular(size / 2),
|
|
|
|
|
boxShadow: [
|
|
|
|
|
BoxShadow(
|
2025-10-02 01:58:12 +05:30
|
|
|
color: primaryBrandColor(context: context).withValues(alpha: 0.3),
|
2025-08-10 01:20:45 +05:30
|
|
|
blurRadius: size * 0.2,
|
|
|
|
|
offset: Offset(0, size * 0.1),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
child: fallbackText != null && fallbackText.isNotEmpty
|
|
|
|
|
? Center(
|
|
|
|
|
child: Text(
|
|
|
|
|
fallbackText.toUpperCase(),
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
color: iColor,
|
|
|
|
|
fontSize: size * 0.4,
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
: Icon(primaryIcon, size: size * 0.5, color: iColor),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Creates a branded loading indicator
|
|
|
|
|
static Widget createBrandLoadingIndicator({
|
|
|
|
|
double size = 24,
|
|
|
|
|
double strokeWidth = 2,
|
|
|
|
|
Color? color,
|
2025-10-02 01:58:12 +05:30
|
|
|
BuildContext? context,
|
2025-08-10 01:20:45 +05:30
|
|
|
}) {
|
|
|
|
|
return SizedBox(
|
|
|
|
|
width: size,
|
|
|
|
|
height: size,
|
|
|
|
|
child: CircularProgressIndicator(
|
|
|
|
|
strokeWidth: strokeWidth,
|
2025-10-02 01:58:12 +05:30
|
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
|
|
|
color ?? primaryBrandColor(context: context),
|
|
|
|
|
),
|
2025-08-10 01:20:45 +05:30
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Creates a branded empty state icon
|
|
|
|
|
static Widget createBrandEmptyStateIcon({
|
|
|
|
|
double size = 80,
|
|
|
|
|
Color? color,
|
|
|
|
|
bool showBackground = true,
|
|
|
|
|
BuildContext? context,
|
|
|
|
|
}) {
|
2025-10-03 00:12:25 +05:30
|
|
|
final tokens = _resolveTokens(context);
|
2025-08-10 01:20:45 +05:30
|
|
|
final iconColor =
|
2025-10-03 00:12:25 +05:30
|
|
|
color ?? (context?.conduitTheme.iconSecondary ?? tokens.neutralTone80);
|
2025-08-10 01:20:45 +05:30
|
|
|
|
|
|
|
|
if (!showBackground) {
|
|
|
|
|
return createBrandIcon(
|
|
|
|
|
size: size,
|
|
|
|
|
color: iconColor,
|
|
|
|
|
icon: primaryIconOutlined,
|
2025-10-02 01:58:12 +05:30
|
|
|
context: context,
|
2025-08-10 01:20:45 +05:30
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Container(
|
|
|
|
|
width: size,
|
|
|
|
|
height: size,
|
|
|
|
|
decoration: BoxDecoration(
|
2025-10-03 00:12:25 +05:30
|
|
|
color: context?.conduitTheme.surfaceBackground ?? tokens.neutralTone10,
|
2025-08-10 01:20:45 +05:30
|
|
|
borderRadius: BorderRadius.circular(size / 2),
|
|
|
|
|
border: Border.all(
|
2025-10-03 00:12:25 +05:30
|
|
|
color: context?.conduitTheme.dividerColor ?? tokens.neutralTone40,
|
2025-08-10 01:20:45 +05:30
|
|
|
width: 2,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
child: createBrandIcon(
|
|
|
|
|
size: size * 0.5,
|
|
|
|
|
color: iconColor,
|
|
|
|
|
icon: primaryIconOutlined,
|
2025-10-02 01:58:12 +05:30
|
|
|
context: context,
|
2025-08-10 01:20:45 +05:30
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Creates a branded button with hub icon
|
|
|
|
|
static Widget createBrandButton({
|
|
|
|
|
required String text,
|
|
|
|
|
required VoidCallback? onPressed,
|
|
|
|
|
bool isLoading = false,
|
|
|
|
|
IconData? icon,
|
|
|
|
|
double? width,
|
|
|
|
|
bool isSecondary = false,
|
|
|
|
|
BuildContext? context,
|
|
|
|
|
}) {
|
2025-10-02 01:58:12 +05:30
|
|
|
final theme = context?.conduitTheme;
|
2025-10-03 00:12:25 +05:30
|
|
|
final tokens = _resolveTokens(context);
|
2025-08-10 01:20:45 +05:30
|
|
|
return SizedBox(
|
|
|
|
|
width: width,
|
|
|
|
|
height: 48,
|
|
|
|
|
child: ElevatedButton.icon(
|
|
|
|
|
onPressed: isLoading ? null : onPressed,
|
|
|
|
|
icon: isLoading
|
2025-10-02 01:58:12 +05:30
|
|
|
? createBrandLoadingIndicator(size: IconSize.sm, context: context)
|
2025-08-10 01:20:45 +05:30
|
|
|
: createBrandIcon(
|
|
|
|
|
size: IconSize.md,
|
|
|
|
|
icon: icon ?? primaryIcon,
|
2025-10-03 00:12:25 +05:30
|
|
|
color: theme?.textInverse ?? tokens.neutralTone00,
|
2025-10-02 01:58:12 +05:30
|
|
|
context: context,
|
2025-08-10 01:20:45 +05:30
|
|
|
),
|
|
|
|
|
label: Text(text),
|
|
|
|
|
style: ElevatedButton.styleFrom(
|
|
|
|
|
backgroundColor: isSecondary
|
2025-10-03 00:12:25 +05:30
|
|
|
? (theme?.buttonSecondary ?? tokens.neutralTone20)
|
2025-10-02 01:58:12 +05:30
|
|
|
: (theme?.buttonPrimary ?? primaryBrandColor(context: context)),
|
2025-10-03 00:12:25 +05:30
|
|
|
foregroundColor: theme?.buttonPrimaryText ?? tokens.brandOn60,
|
|
|
|
|
disabledBackgroundColor:
|
|
|
|
|
theme?.buttonDisabled ?? tokens.neutralTone40,
|
2025-08-10 01:20:45 +05:30
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
|
|
|
),
|
|
|
|
|
elevation: Elevation.none,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Brand-specific semantic labels for accessibility
|
|
|
|
|
static String get brandName => 'Conduit';
|
|
|
|
|
static String get brandDescription => 'Your AI Conversation Hub';
|
|
|
|
|
static String get connectionLabel => 'Hub Connection';
|
|
|
|
|
static String get networkLabel => 'Network Hub';
|
|
|
|
|
|
|
|
|
|
/// Creates branded AppBar with consistent styling
|
|
|
|
|
static PreferredSizeWidget createBrandAppBar({
|
|
|
|
|
required String title,
|
|
|
|
|
List<Widget>? actions,
|
|
|
|
|
Widget? leading,
|
|
|
|
|
bool centerTitle = true,
|
|
|
|
|
double elevation = 0,
|
|
|
|
|
BuildContext? context,
|
|
|
|
|
}) {
|
|
|
|
|
return AppBar(
|
|
|
|
|
title: Text(
|
|
|
|
|
title,
|
2025-09-24 12:00:49 +05:30
|
|
|
style: context != null
|
2025-08-16 15:51:27 +05:30
|
|
|
? context.conduitTheme.headingSmall?.copyWith(
|
|
|
|
|
color: context.conduitTheme.textPrimary,
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
)
|
|
|
|
|
: TextStyle(
|
|
|
|
|
fontSize: AppTypography.headlineSmall,
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
),
|
2025-08-10 01:20:45 +05:30
|
|
|
),
|
|
|
|
|
centerTitle: centerTitle,
|
|
|
|
|
elevation: elevation,
|
|
|
|
|
backgroundColor: context?.conduitTheme.surfaceBackground,
|
|
|
|
|
surfaceTintColor: Colors.transparent,
|
|
|
|
|
shadowColor: Colors.transparent,
|
|
|
|
|
leading: leading,
|
|
|
|
|
actions: actions,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Creates a branded splash screen logo
|
|
|
|
|
static Widget createSplashLogo({
|
|
|
|
|
double size = 140,
|
|
|
|
|
bool animate = true,
|
|
|
|
|
BuildContext? context,
|
|
|
|
|
}) {
|
2025-10-02 01:58:12 +05:30
|
|
|
final theme = context?.conduitTheme;
|
2025-10-03 00:12:25 +05:30
|
|
|
final tokens = _resolveTokens(context);
|
2025-10-02 01:58:12 +05:30
|
|
|
final baseColor =
|
|
|
|
|
theme?.buttonPrimary ??
|
|
|
|
|
primaryBrandColor(context: context, brightness: Brightness.dark);
|
|
|
|
|
final accentColor =
|
|
|
|
|
theme?.buttonPrimary.withValues(alpha: 0.8) ??
|
|
|
|
|
secondaryBrandColor(context: context, brightness: Brightness.dark);
|
|
|
|
|
|
2025-08-10 01:20:45 +05:30
|
|
|
return Container(
|
|
|
|
|
width: size,
|
|
|
|
|
height: size,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
gradient: LinearGradient(
|
|
|
|
|
begin: Alignment.topLeft,
|
|
|
|
|
end: Alignment.bottomRight,
|
2025-10-02 01:58:12 +05:30
|
|
|
colors: [baseColor, accentColor],
|
2025-08-10 01:20:45 +05:30
|
|
|
),
|
|
|
|
|
borderRadius: BorderRadius.circular(size / 2),
|
2025-10-03 00:12:25 +05:30
|
|
|
boxShadow: context != null
|
|
|
|
|
? ConduitShadows.glow(context)
|
|
|
|
|
: ConduitShadows.glowWithTokens(tokens),
|
2025-08-10 01:20:45 +05:30
|
|
|
),
|
|
|
|
|
child: Icon(
|
|
|
|
|
primaryIcon,
|
|
|
|
|
size: size * 0.5,
|
2025-10-03 00:12:25 +05:30
|
|
|
color: theme?.textInverse ?? tokens.neutralTone00,
|
2025-08-10 01:20:45 +05:30
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-10-02 01:58:12 +05:30
|
|
|
|
2025-10-18 13:58:15 +05:30
|
|
|
static TweakcnThemeDefinition _resolvePalette(BuildContext? context) {
|
2025-10-02 01:58:12 +05:30
|
|
|
if (context == null) {
|
2025-10-18 13:58:15 +05:30
|
|
|
return TweakcnThemes.t3Chat;
|
2025-10-02 01:58:12 +05:30
|
|
|
}
|
|
|
|
|
final extension = Theme.of(context).extension<AppPaletteThemeExtension>();
|
2025-10-18 13:58:15 +05:30
|
|
|
return extension?.palette ?? TweakcnThemes.t3Chat;
|
2025-10-02 01:58:12 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static Brightness _resolveBrightness(BuildContext? context) {
|
|
|
|
|
return context != null ? Theme.of(context).brightness : Brightness.light;
|
|
|
|
|
}
|
2025-10-03 00:12:25 +05:30
|
|
|
|
|
|
|
|
static AppColorTokens _resolveTokens(BuildContext? context) {
|
|
|
|
|
final palette = _resolvePalette(context);
|
|
|
|
|
final brightness = _resolveBrightness(context);
|
|
|
|
|
return brightness == Brightness.dark
|
2025-10-18 13:58:15 +05:30
|
|
|
? AppColorTokens.dark(theme: palette)
|
|
|
|
|
: AppColorTokens.light(theme: palette);
|
2025-10-03 00:12:25 +05:30
|
|
|
}
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|