mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
Security: - Replace unsalted SHA-256 password hashing with bcrypt (lazy rehash on login) - Add authenticated /api/xtream-api gateway: Xtream credentials are injected server-side and never sent to the frontend; /api/playlists no longer returns passwords - Redact credentials from all logs (login body, proxy/FFmpeg/scheduler URLs) - Add auth to recordings, EPG, season-passes and streaming routes (HttpOnly session cookie for hls.js; loopback bypass for local FFmpeg) - Lock player postMessage to same-origin in both directions - Vendor and pin hls.js 1.6.7 / mpegts.js 1.7.3 (drop CDN @latest) - Fix rate limiter (client IP was never resolved), add login rate limit, restrict CORS, add CSP Report-Only, block private-IP SSRF targets, fix path traversal in recording log retrieval, chmod 777 -> 770 - Remove dead HiveService (seeded admin/admin into IndexedDB with SHA-256) - Fix authMiddleware not populating 'user' context (getPlaylist ignored the logged-in user; admin purge always returned 403) Streaming: - New FfmpegSessionManager: process registry, idle reaper (4 min live / 15 min VOD), orphan cleanup at startup, clean SIGTERM shutdown, fast-fail with stderr instead of 30 s timeout - Quality selection (source/high/medium/low) for live and VOD; source mode streams with -c:v copy (zero transcoding); selector wired into the player - Concurrent recordings (MAX_CONCURRENT_RECORDINGS, default 2); conflicts retry on the next tick instead of silently failing - Lower live latency (HLS window 20 -> 10 segments, liveSync 10 -> 3) - Fix recording log lookup (.mp4 vs .mkv mismatch) Design: - Replace hardcoded colors with AppColors tokens (12 files) - web/theme.css syncs HTML players with the Flutter palette - DPAD/keyboard navigation (arrow-key focus, player shortcuts) - Tooltips on player icon buttons, Semantics on content cards - Remove 7 dead widgets broken since the Stitch merge Quality: - bin/test/: 21 unit tests (bcrypt, redaction, traversal, SSRF, recording conflicts) plus a quality-selector widget test - GitHub Actions CI (analyze + test + build web) - Archive stale status docs into docs/archive/ Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1102 lines
33 KiB
Markdown
1102 lines
33 KiB
Markdown
# Apple TV Design System - Implementation Code Reference
|
|
|
|
## Part 1: Updated app_colors.dart Structure
|
|
|
|
### Complete Color Definitions
|
|
|
|
```dart
|
|
class AppColors {
|
|
AppColors._();
|
|
|
|
// ============ BACKGROUNDS (OLED Optimized) ============
|
|
/// Pure black, optimal for OLED
|
|
static const Color background = Color(0xFF000000);
|
|
|
|
/// Deep black, very subtle lift
|
|
static const Color backgroundAlt = Color(0xFF0A0A0A);
|
|
|
|
// ============ SURFACE LAYERS (Material Design 3 compatible) ============
|
|
/// Level 1 - Card surfaces, default container
|
|
static const Color surface = Color(0xFF1A1A1A);
|
|
|
|
/// Level 2 - Hovered surfaces, slight elevation
|
|
static const Color surfaceVariant = Color(0xFF2A2A2A);
|
|
|
|
/// Level 3 - Disabled/secondary surfaces
|
|
static const Color surfaceTertiary = Color(0xFF3A3A3A);
|
|
|
|
/// Level 4 - Dividers, minimal visible elements
|
|
static const Color surfaceQuad = Color(0xFF4A4A4A);
|
|
|
|
// ============ PRIMARY ACCENT (Teal) ============
|
|
/// Main interactive color - focus states, CTAs
|
|
static const Color primary = Color(0xFF00A0D2);
|
|
|
|
/// Darker teal for hover states
|
|
static const Color primaryDark = Color(0xFF0092BC);
|
|
|
|
/// Lighter teal for disabled variant
|
|
static const Color primaryLight = Color(0xFF1BC4E5);
|
|
|
|
// ============ SECONDARY ACCENT (Red) ============
|
|
/// Error, warning, live indicator - Apple red
|
|
static const Color secondary = Color(0xFFFF3B30);
|
|
|
|
/// Darker red for hover
|
|
static const Color secondaryDark = Color(0xFFE62817);
|
|
|
|
/// Lighter red for disabled
|
|
static const Color secondaryLight = Color(0xFFFF6B6B);
|
|
|
|
// ============ TERTIARY ACCENT (Green) ============
|
|
/// Success, confirmations, positive states
|
|
static const Color tertiary = Color(0xFF34C759);
|
|
|
|
// ============ PRIMARY TEXT COLORS ============
|
|
/// Primary text - pure white
|
|
static const Color textPrimary = Color(0xFFFFFFFF);
|
|
|
|
/// Secondary text - 60% opacity white (.
|
|
static const Color textSecondary = Color(0xFF999999);
|
|
|
|
/// Tertiary text - 40% opacity white
|
|
static const Color textTertiary = Color(0xFF666666);
|
|
|
|
/// Quaternary text - 25% opacity white
|
|
static const Color textQuaternary = Color(0xFF404040);
|
|
|
|
// ============ STATE COLORS ============
|
|
static const Color success = Color(0xFF34C759);
|
|
static const Color warning = Color(0xFFFF9500);
|
|
static const Color error = Color(0xFFFF3B30);
|
|
static const Color info = Color(0xFF32ADE6);
|
|
static const Color disabled = Color(0xFF888888);
|
|
|
|
// ============ FOCUS STATES ============
|
|
/// White for maximum contrast on focus
|
|
static const Color focusColor = Color(0xFFFFFFFF);
|
|
|
|
/// Focus border (use with 2px width)
|
|
static const Color focusBorder = Color(0xFFFFFFFF);
|
|
|
|
// ============ GLASS EFFECTS ============
|
|
/// Glass overlay - 6% white (refined from 8%)
|
|
static final Color glassBackground = const Color(0xFFFFFFFF).withOpacity(0.06);
|
|
|
|
/// Glass border - 12% white
|
|
static final Color glassBorder = const Color(0xFFFFFFFF).withOpacity(0.12);
|
|
|
|
/// Glass premium variant - 10% white
|
|
static final Color glassPremium = const Color(0xFFFFFFFF).withOpacity(0.10);
|
|
|
|
/// Dark glass for overlays
|
|
static final Color glassBackgroundDark = const Color(0xFF000000).withOpacity(0.50);
|
|
|
|
// ============ GRADIENTS ============
|
|
static const LinearGradient primaryGradient = LinearGradient(
|
|
colors: [Color(0xFF00A0D2), Color(0xFF00D4AA)],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
);
|
|
|
|
static const LinearGradient backgroundGradient = LinearGradient(
|
|
colors: [Color(0xFF000000), Color(0xFF0A0A0A)],
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
);
|
|
|
|
// ============ RATING/BADGE COLORS ============
|
|
static const Color ratingGold = Color(0xFFFFD700);
|
|
static const Color ratingPremium = Color(0xFFFFD700);
|
|
|
|
// ============ OVERLAY & SCRIM ============
|
|
static const Color overlay = Color(0x80000000);
|
|
static final Color scrim = Colors.black.withOpacity(0.4);
|
|
|
|
// ============ COLOR SCHEME (Material 3) ============
|
|
static ColorScheme get darkColorScheme => const ColorScheme.dark(
|
|
primary: primary,
|
|
onPrimary: Color(0xFF000000),
|
|
secondary: secondary,
|
|
onSecondary: Color(0xFFFFFFFF),
|
|
tertiary: tertiary,
|
|
onTertiary: Color(0xFF000000),
|
|
surface: surface,
|
|
onSurface: textPrimary,
|
|
error: error,
|
|
onError: Color(0xFFFFFFFF),
|
|
brightness: Brightness.dark,
|
|
scrim: overlay,
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Part 2: Updated app_theme.dart Structure
|
|
|
|
### Typography Scale
|
|
|
|
```dart
|
|
class AppTheme {
|
|
AppTheme._();
|
|
|
|
// ============ COMPLETE TYPOGRAPHY SYSTEM ============
|
|
static ThemeData get darkTheme {
|
|
final baseTheme = ThemeData.dark(useMaterial3: true);
|
|
|
|
return baseTheme.copyWith(
|
|
useMaterial3: true,
|
|
colorScheme: AppColors.darkColorScheme,
|
|
scaffoldBackgroundColor: AppColors.background,
|
|
textTheme: TextTheme(
|
|
// ====== HERO / DISPLAY ======
|
|
/// Display Large - 64px (increased from 56px)
|
|
/// Usage: Hero titles, featured content
|
|
displayLarge: GoogleFonts.outfit(
|
|
fontSize: 64,
|
|
fontWeight: FontWeight.w800,
|
|
letterSpacing: -2.0,
|
|
height: 1.2, // 77px line
|
|
color: AppColors.textPrimary,
|
|
),
|
|
|
|
/// Display Medium - 56px
|
|
/// Usage: Page titles, feature headlines
|
|
displayMedium: GoogleFonts.outfit(
|
|
fontSize: 56,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: -1.5,
|
|
height: 1.15, // 64px line
|
|
color: AppColors.textPrimary,
|
|
),
|
|
|
|
/// Display Small - 44px
|
|
/// Usage: Section headers, subsection titles
|
|
displaySmall: GoogleFonts.outfit(
|
|
fontSize: 44,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: -1.0,
|
|
height: 1.2, // 53px line
|
|
color: AppColors.textPrimary,
|
|
),
|
|
|
|
// ====== HEADLINE / TITLE ======
|
|
/// Headline Large - 32px
|
|
/// Usage: Card titles, strong emphasis
|
|
headlineLarge: GoogleFonts.outfit(
|
|
fontSize: 32,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.5,
|
|
height: 1.25, // 40px line
|
|
color: AppColors.textPrimary,
|
|
),
|
|
|
|
/// Headline Medium - 28px
|
|
/// Usage: Subsection titles, emphasis
|
|
headlineMedium: GoogleFonts.outfit(
|
|
fontSize: 28,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.3,
|
|
height: 1.3, // 36px line
|
|
color: AppColors.textPrimary,
|
|
),
|
|
|
|
/// Headline Small - 22px
|
|
/// Usage: Button text, strong labels
|
|
headlineSmall: GoogleFonts.outfit(
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 0,
|
|
height: 1.4, // 31px line
|
|
color: AppColors.textPrimary,
|
|
),
|
|
|
|
// ====== TITLE ======
|
|
/// Title Large - 18px
|
|
/// Usage: Prominent UI text, navigation
|
|
titleLarge: GoogleFonts.inter(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 0,
|
|
height: 1.4, // 25px line
|
|
color: AppColors.textPrimary,
|
|
),
|
|
|
|
/// Title Medium - 16px
|
|
/// Usage: Button labels, input labels
|
|
titleMedium: GoogleFonts.inter(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 0,
|
|
height: 1.5, // 24px line
|
|
color: AppColors.textPrimary,
|
|
),
|
|
|
|
/// Title Small - 14px
|
|
/// Usage: Secondary labels, emphasis
|
|
titleSmall: GoogleFonts.inter(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 0.1,
|
|
height: 1.4, // 20px line
|
|
color: AppColors.textSecondary,
|
|
),
|
|
|
|
// ====== BODY TEXT ======
|
|
/// Body Large - 16px
|
|
/// Usage: Primary content text, descriptions
|
|
bodyLarge: GoogleFonts.inter(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w400,
|
|
letterSpacing: 0.2,
|
|
height: 1.6, // 26px line (increased from 1.5)
|
|
color: AppColors.textSecondary,
|
|
),
|
|
|
|
/// Body Medium - 14px
|
|
/// Usage: Secondary content, metadata
|
|
bodyMedium: GoogleFonts.inter(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w400,
|
|
letterSpacing: 0.2,
|
|
height: 1.6, // 22px line (increased from 1.5)
|
|
color: AppColors.textSecondary,
|
|
),
|
|
|
|
/// Body Small - 12px
|
|
/// Usage: Tertiary text, timestamps, specs
|
|
bodySmall: GoogleFonts.inter(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w400,
|
|
letterSpacing: 0.15,
|
|
height: 1.5, // 18px line
|
|
color: AppColors.textTertiary,
|
|
),
|
|
|
|
// ====== LABELS & CAPTIONS ======
|
|
/// Label Large - 12px
|
|
/// Usage: Badge text, tab labels, buttons
|
|
labelLarge: GoogleFonts.inter(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 0.3,
|
|
height: 1.4, // 17px line
|
|
color: AppColors.textPrimary,
|
|
),
|
|
|
|
/// Label Medium - 11px
|
|
/// Usage: Secondary badges, small labels
|
|
labelMedium: GoogleFonts.inter(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w500,
|
|
letterSpacing: 0.3,
|
|
height: 1.4, // 15px line
|
|
color: AppColors.textPrimary,
|
|
),
|
|
|
|
/// Label Small - 10px
|
|
/// Usage: Tiny badges, version numbers (NEW)
|
|
labelSmall: GoogleFonts.inter(
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 0.5,
|
|
height: 1.2, // 12px line
|
|
color: AppColors.textSecondary,
|
|
),
|
|
),
|
|
|
|
// ============ APP BAR ============
|
|
appBarTheme: AppBarTheme(
|
|
backgroundColor: Colors.transparent,
|
|
foregroundColor: AppColors.textPrimary,
|
|
elevation: 0,
|
|
scrolledUnderElevation: 0,
|
|
centerTitle: true,
|
|
titleTextStyle: GoogleFonts.outfit(
|
|
fontSize: 32, // Increased from 24px
|
|
fontWeight: FontWeight.w700,
|
|
color: AppColors.textPrimary,
|
|
letterSpacing: -0.3,
|
|
),
|
|
),
|
|
|
|
// ============ BUTTONS ============
|
|
filledButtonTheme: FilledButtonThemeData(
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: AppColors.primary,
|
|
foregroundColor: AppColors.textPrimary,
|
|
disabledBackgroundColor: AppColors.disabled,
|
|
disabledForegroundColor: AppColors.textTertiary,
|
|
elevation: 0,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: spacing28, // Increased from 24
|
|
vertical: spacing12,
|
|
),
|
|
minimumSize: const Size(0, buttonHeightLg), // 56px height (new)
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(radiusMd),
|
|
),
|
|
textStyle: GoogleFonts.inter(
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 18,
|
|
letterSpacing: 0.1,
|
|
),
|
|
).copyWith(
|
|
overlayColor: WidgetStateProperty.resolveWith((states) {
|
|
if (states.contains(WidgetState.hovered)) {
|
|
return AppColors.primary.withOpacity(0.15);
|
|
}
|
|
if (states.contains(WidgetState.pressed)) {
|
|
return AppColors.primary.withOpacity(0.3);
|
|
}
|
|
return null;
|
|
}),
|
|
),
|
|
),
|
|
|
|
// ============ CARDS ============
|
|
cardTheme: CardThemeData(
|
|
color: AppColors.surface,
|
|
elevation: 0,
|
|
margin: EdgeInsets.zero,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(radiusMd),
|
|
side: const BorderSide(color: AppColors.border, width: 1),
|
|
),
|
|
shadowColor: Colors.black.withOpacity(0.15),
|
|
),
|
|
|
|
// ============ INPUTS ============
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
filled: true,
|
|
fillColor: AppColors.surface,
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: spacing16,
|
|
vertical: spacing12,
|
|
),
|
|
isDense: false,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(radiusMd),
|
|
borderSide: const BorderSide(color: AppColors.border),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(radiusMd),
|
|
borderSide: const BorderSide(
|
|
color: AppColors.border,
|
|
width: 1.0,
|
|
),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(radiusMd),
|
|
borderSide: const BorderSide(
|
|
color: AppColors.primary,
|
|
width: 2.0,
|
|
),
|
|
),
|
|
hintStyle: GoogleFonts.inter(
|
|
color: AppColors.textTertiary,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
prefixIconColor: AppColors.textSecondary,
|
|
suffixIconColor: AppColors.textSecondary,
|
|
helperStyle: GoogleFonts.inter(
|
|
color: AppColors.textTertiary,
|
|
fontSize: 12,
|
|
),
|
|
errorStyle: GoogleFonts.inter(
|
|
color: AppColors.error,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
|
|
// ============ EXTENSIONS (Custom themes) ============
|
|
extensions: <ThemeExtension<dynamic>>[
|
|
_AppThemeExtension.dark,
|
|
],
|
|
);
|
|
}
|
|
|
|
// ============ SPACING (8pt base grid) ============
|
|
static const double spacing2 = 2.0;
|
|
static const double spacing4 = 4.0;
|
|
static const double spacing8 = 8.0;
|
|
static const double spacing12 = 12.0;
|
|
static const double spacing16 = 16.0;
|
|
static const double spacing20 = 20.0;
|
|
static const double spacing24 = 24.0;
|
|
static const double spacing28 = 28.0; // NEW
|
|
static const double spacing32 = 32.0;
|
|
static const double spacing40 = 40.0;
|
|
static const double spacing48 = 48.0;
|
|
static const double spacing56 = 56.0;
|
|
static const double spacing64 = 64.0;
|
|
static const double spacing80 = 80.0; // NEW - TV safe areas
|
|
static const double spacing96 = 96.0; // NEW - Large layout
|
|
|
|
// ============ RADIUS (Modern, organic curves) ============
|
|
static const double radiusXs = 4.0;
|
|
static const double radiusSm = 8.0;
|
|
static const double radiusMd = 12.0;
|
|
static const double radiusLg = 16.0;
|
|
static const double radiusXl = 24.0;
|
|
static const double radiusXxl = 32.0;
|
|
static const double radiusFull = 999.0;
|
|
|
|
// ============ ELEVATION (Shadow system) ============
|
|
static const double elevationXs = 2.0;
|
|
static const double elevationSm = 4.0;
|
|
static const double elevationMd = 8.0;
|
|
static const double elevationLg = 16.0;
|
|
static const double elevationXl = 24.0;
|
|
|
|
// ============ ANIMATION DURATIONS ============
|
|
static const Duration durationXs = Duration(milliseconds: 100);
|
|
static const Duration durationSm = Duration(milliseconds: 150);
|
|
static const Duration durationBase = Duration(milliseconds: 200);
|
|
static const Duration durationMd = Duration(milliseconds: 250); // NEW
|
|
static const Duration durationLg = Duration(milliseconds: 300);
|
|
static const Duration durationXl = Duration(milliseconds: 400);
|
|
static const Duration durationXxl = Duration(milliseconds: 600);
|
|
|
|
// ============ ANIMATION CURVES ============
|
|
/// Standard deceleration - smooth landing (entrance)
|
|
static const Curve curveDefault = Curves.easeOutCubic;
|
|
|
|
/// Quick interaction - symmetrical (tap, state change)
|
|
static const Curve curveSnappy = Curves.easeInOutQuad;
|
|
|
|
/// Smooth navigation - deceleration for movement
|
|
static const Curve curveSmooth = Curves.easeOutCubic;
|
|
|
|
/// Celebratory - spring bounce (favorites, achievements)
|
|
static const Curve curveBouncy = Curves.elasticOut;
|
|
|
|
// ============ BUTTON SIZING ============
|
|
static const double buttonHeightSm = 44.0;
|
|
static const double buttonHeightMd = 48.0;
|
|
static const double buttonHeightLg = 56.0; // TV optimal
|
|
|
|
// ============ ICON SIZING ============
|
|
static const double iconSizeXs = 16.0;
|
|
static const double iconSizeSm = 20.0;
|
|
static const double iconSizeMd = 24.0;
|
|
static const double iconSizeLg = 32.0;
|
|
static const double iconSizeXl = 48.0;
|
|
|
|
// ============ ASPECT RATIOS ============
|
|
static const double posterAspectRatio = 2 / 3;
|
|
static const double landscapeAspectRatio = 16 / 9;
|
|
static const double squareAspectRatio = 1.0;
|
|
}
|
|
|
|
/// Theme extension for custom properties
|
|
class _AppThemeExtension extends ThemeExtension<_AppThemeExtension> {
|
|
const _AppThemeExtension({
|
|
this.shadowLevel2,
|
|
this.shadowLevel3,
|
|
this.shadowLevel4,
|
|
this.shadowLevel5,
|
|
this.focusGlow,
|
|
});
|
|
|
|
final List<BoxShadow>? shadowLevel2;
|
|
final List<BoxShadow>? shadowLevel3;
|
|
final List<BoxShadow>? shadowLevel4;
|
|
final List<BoxShadow>? shadowLevel5;
|
|
final List<BoxShadow>? focusGlow;
|
|
|
|
static _AppThemeExtension get dark => _AppThemeExtension(
|
|
// Level 2 - Subtle shadow (base cards)
|
|
shadowLevel2: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.08),
|
|
blurRadius: 6,
|
|
spreadRadius: 0,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
|
|
// Level 3 - Standard shadow (hover/focus cards)
|
|
shadowLevel3: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.12),
|
|
blurRadius: 8,
|
|
spreadRadius: 0,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.06),
|
|
blurRadius: 2,
|
|
spreadRadius: 0,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
|
|
// Level 4 - Elevated shadow (active cards with glow)
|
|
shadowLevel4: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.20),
|
|
blurRadius: 12,
|
|
spreadRadius: 0,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.10),
|
|
blurRadius: 4,
|
|
spreadRadius: 0,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
// Glow added separately when focused
|
|
],
|
|
|
|
// Level 5 - Maximum elevation (modals)
|
|
shadowLevel5: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.25),
|
|
blurRadius: 16,
|
|
spreadRadius: 0,
|
|
offset: const Offset(0, 12),
|
|
),
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.15),
|
|
blurRadius: 6,
|
|
spreadRadius: 0,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.08),
|
|
blurRadius: 2,
|
|
spreadRadius: 0,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
|
|
// Teal glow (when element has focus)
|
|
focusGlow: [
|
|
BoxShadow(
|
|
color: AppColors.primary.withOpacity(0.30),
|
|
blurRadius: 20,
|
|
spreadRadius: 2,
|
|
offset: const Offset(0, 0),
|
|
),
|
|
],
|
|
);
|
|
|
|
@override
|
|
ThemeExtension<_AppThemeExtension> copyWith({
|
|
List<BoxShadow>? shadowLevel2,
|
|
List<BoxShadow>? shadowLevel3,
|
|
List<BoxShadow>? shadowLevel4,
|
|
List<BoxShadow>? shadowLevel5,
|
|
List<BoxShadow>? focusGlow,
|
|
}) {
|
|
return _AppThemeExtension(
|
|
shadowLevel2: shadowLevel2 ?? this.shadowLevel2,
|
|
shadowLevel3: shadowLevel3 ?? this.shadowLevel3,
|
|
shadowLevel4: shadowLevel4 ?? this.shadowLevel4,
|
|
shadowLevel5: shadowLevel5 ?? this.shadowLevel5,
|
|
focusGlow: focusGlow ?? this.focusGlow,
|
|
);
|
|
}
|
|
|
|
@override
|
|
ThemeExtension<_AppThemeExtension> lerp(
|
|
covariant ThemeExtension<_AppThemeExtension>? other,
|
|
double t,
|
|
) {
|
|
if (other is! _AppThemeExtension) return this;
|
|
return this;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Part 3: Focus State Animation Helper
|
|
|
|
### New utility class for consistent focus animations
|
|
|
|
```dart
|
|
// lib/core/animation/focus_animations.dart
|
|
|
|
import 'package:flutter/material.dart';
|
|
import '../theme/app_theme.dart';
|
|
import '../theme/app_colors.dart';
|
|
|
|
/// Reusable focus animation utilities for TV-friendly interactions
|
|
class FocusAnimations {
|
|
FocusAnimations._();
|
|
|
|
/// Standard focus arrival animation
|
|
/// Timeline: 150ms easeOut with scale and glow
|
|
static Future<void> animateFocusArrival({
|
|
required AnimationController controller,
|
|
Duration duration = AppTheme.durationSm,
|
|
}) async {
|
|
controller.forward();
|
|
await Future.delayed(duration);
|
|
}
|
|
|
|
/// Tap feedback animation (scale down then up)
|
|
static Future<void> animateTapFeedback({
|
|
required AnimationController controller,
|
|
}) async {
|
|
controller.forward();
|
|
await Future.delayed(const Duration(milliseconds: 100));
|
|
controller.reverse();
|
|
}
|
|
|
|
/// Generate focus border decoration
|
|
static BoxDecoration focusBorder() => BoxDecoration(
|
|
border: Border.all(
|
|
color: AppColors.focusBorder,
|
|
width: 2,
|
|
),
|
|
);
|
|
|
|
/// Generate focus glow shadow
|
|
static List<BoxShadow> focusGlowShadow() => [
|
|
BoxShadow(
|
|
color: AppColors.primary.withOpacity(0.30),
|
|
blurRadius: 20,
|
|
spreadRadius: 2,
|
|
offset: const Offset(0, 0),
|
|
),
|
|
];
|
|
|
|
/// Generate focus animation curve
|
|
static Curve get focusCurve => AppTheme.curveDefault; // easeOutCubic
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Part 4: TV Button Implementation
|
|
|
|
### Example implementation showing all states
|
|
|
|
```dart
|
|
// lib/core/widgets/tv_primary_button.dart
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import '../theme/app_theme.dart';
|
|
import '../theme/app_colors.dart';
|
|
|
|
/// TV-optimized primary button with focus and hover states
|
|
class TvPrimaryButton extends StatefulWidget {
|
|
final String label;
|
|
final VoidCallback? onPressed;
|
|
final IconData? icon;
|
|
final bool isLoading;
|
|
final bool isExpanded;
|
|
final bool autofocus;
|
|
|
|
const TvPrimaryButton({
|
|
super.key,
|
|
required this.label,
|
|
this.onPressed,
|
|
this.icon,
|
|
this.isLoading = false,
|
|
this.isExpanded = false,
|
|
this.autofocus = false,
|
|
});
|
|
|
|
@override
|
|
State<TvPrimaryButton> createState() => _TvPrimaryButtonState();
|
|
}
|
|
|
|
class _TvPrimaryButtonState extends State<TvPrimaryButton>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
late Animation<double> _scaleAnimation;
|
|
late Animation<double> _opacityAnimation;
|
|
bool _isHovered = false;
|
|
bool _isFocused = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
vsync: this,
|
|
duration: AppTheme.durationSm,
|
|
);
|
|
|
|
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.05).animate(
|
|
CurvedAnimation(parent: _controller, curve: AppTheme.curveDefault),
|
|
);
|
|
|
|
_opacityAnimation = Tween<double>(begin: 1.0, end: 0.9).animate(
|
|
CurvedAnimation(parent: _controller, curve: AppTheme.curveDefault),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _handleFocusChange(bool hasFocus) {
|
|
setState(() => _isFocused = hasFocus);
|
|
if (hasFocus) {
|
|
_controller.forward();
|
|
} else {
|
|
_controller.reverse();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isActive = _isHovered || _isFocused;
|
|
|
|
return MouseRegion(
|
|
onEnter: (_) => setState(() => _isHovered = true),
|
|
onExit: (_) => setState(() => _isHovered = false),
|
|
cursor: SystemMouseCursors.click,
|
|
child: Focus(
|
|
onFocusChange: _handleFocusChange,
|
|
autofocus: widget.autofocus,
|
|
child: AnimatedBuilder(
|
|
animation: _scaleAnimation,
|
|
builder: (context, child) {
|
|
return Transform.scale(
|
|
scale: _scaleAnimation.value,
|
|
child: child,
|
|
);
|
|
},
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
|
// Add focus border only when focused
|
|
border: _isFocused
|
|
? Border.all(
|
|
color: AppColors.focusBorder,
|
|
width: 2,
|
|
)
|
|
: null,
|
|
// Add focus glow shadow when focused
|
|
boxShadow: _isFocused
|
|
? [
|
|
BoxShadow(
|
|
color: AppColors.primary.withOpacity(0.30),
|
|
blurRadius: 20,
|
|
spreadRadius: 2,
|
|
offset: const Offset(0, 0),
|
|
),
|
|
...?Theme.of(context)
|
|
.extension<_AppThemeExtension>()
|
|
?.shadowLevel4,
|
|
]
|
|
: _isHovered
|
|
? Theme.of(context)
|
|
.extension<_AppThemeExtension>()
|
|
?.shadowLevel3 ??
|
|
[]
|
|
: Theme.of(context)
|
|
.extension<_AppThemeExtension>()
|
|
?.shadowLevel2 ??
|
|
[],
|
|
),
|
|
child: Material(
|
|
color: _isFocused
|
|
? AppColors.primary // Keep color on focus
|
|
: _isHovered
|
|
? AppColors.primaryDark // Darker on hover
|
|
: AppColors.primary,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
|
),
|
|
child: InkWell(
|
|
onTap: widget.isLoading || widget.onPressed == null
|
|
? null
|
|
: widget.onPressed,
|
|
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: AppTheme.spacing28,
|
|
vertical: AppTheme.spacing12,
|
|
),
|
|
child: SizedBox(
|
|
height: AppTheme.buttonHeightLg,
|
|
width: widget.isExpanded ? double.infinity : null,
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
if (widget.isLoading)
|
|
const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
Colors.black,
|
|
),
|
|
),
|
|
)
|
|
else ...[
|
|
if (widget.icon != null) ...[
|
|
Icon(
|
|
widget.icon,
|
|
color: Colors.black,
|
|
size: AppTheme.iconSizeMd,
|
|
),
|
|
const SizedBox(width: AppTheme.spacing12),
|
|
],
|
|
Flexible(
|
|
child: Text(
|
|
widget.label,
|
|
style: GoogleFonts.inter(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.black,
|
|
letterSpacing: 0.1,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Part 5: Shadow System Helper
|
|
|
|
### Simplified shadow application
|
|
|
|
```dart
|
|
// lib/core/theme/shadow_system.dart
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'app_colors.dart';
|
|
|
|
/// Centralized shadow system for consistent elevation
|
|
class ShadowSystem {
|
|
ShadowSystem._();
|
|
|
|
/// Level 2 - Subtle shadow (base cards)
|
|
/// Usage: Normal card state, background layers
|
|
static List<BoxShadow> get level2 => [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.08),
|
|
blurRadius: 6,
|
|
spreadRadius: 0,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
];
|
|
|
|
/// Level 3 - Standard shadow (hover/focus cards)
|
|
/// Usage: Hovered cards, elevated containers
|
|
static List<BoxShadow> get level3 => [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.12),
|
|
blurRadius: 8,
|
|
spreadRadius: 0,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.06),
|
|
blurRadius: 2,
|
|
spreadRadius: 0,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
];
|
|
|
|
/// Level 4 - Elevated shadow with glow (focused elements)
|
|
/// Usage: Selected cards, active elements, focus state
|
|
static List<BoxShadow> get level4 => [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.20),
|
|
blurRadius: 12,
|
|
spreadRadius: 0,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.10),
|
|
blurRadius: 4,
|
|
spreadRadius: 0,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
];
|
|
|
|
/// Focus glow (add to level4 when element is focused)
|
|
/// Usage: Add on top of level4 for focused state
|
|
static List<BoxShadow> get focusGlow => [
|
|
BoxShadow(
|
|
color: AppColors.primary.withOpacity(0.30),
|
|
blurRadius: 20,
|
|
spreadRadius: 2,
|
|
offset: const Offset(0, 0),
|
|
),
|
|
];
|
|
|
|
/// Level 5 - Maximum elevation (modals, overlays)
|
|
/// Usage: Modal dialogs, maximum elevation containers
|
|
static List<BoxShadow> get level5 => [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.25),
|
|
blurRadius: 16,
|
|
spreadRadius: 0,
|
|
offset: const Offset(0, 12),
|
|
),
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.15),
|
|
blurRadius: 6,
|
|
spreadRadius: 0,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.08),
|
|
blurRadius: 2,
|
|
spreadRadius: 0,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
];
|
|
|
|
/// Combined level4 + focusGlow (convenience)
|
|
static List<BoxShadow> get level4WithGlow => [
|
|
...level4,
|
|
...focusGlow,
|
|
];
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Part 6: Glass Container Update
|
|
|
|
### Updated glass effect with new opacity
|
|
|
|
```dart
|
|
// lib/core/widgets/glass_container.dart (snippet)
|
|
|
|
class GlassContainer extends StatelessWidget {
|
|
final Widget child;
|
|
final double? opacity; // Now 6% default instead of 8%
|
|
// ... other parameters
|
|
|
|
const GlassContainer({
|
|
// ... parameters
|
|
this.opacity, // Will be 0.06 if not provided
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final finalOpacity = opacity ?? 0.06; // Changed from 0.08
|
|
final finalBgColor = backgroundColor ??
|
|
Color(0xFFFFFFFF).withOpacity(finalOpacity);
|
|
|
|
return Container(
|
|
// ... sizing
|
|
child: ClipRRect(
|
|
// ...
|
|
child: BackdropFilter(
|
|
filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
|
|
child: Container(
|
|
padding: padding,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(borderRadius),
|
|
gradient: showGradient
|
|
? LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [
|
|
finalBgColor.withOpacity(finalOpacity * 1.2),
|
|
finalBgColor.withOpacity(finalOpacity * 0.5),
|
|
],
|
|
)
|
|
: null,
|
|
border: hasBorder
|
|
? Border.all(
|
|
color: borderColor ??
|
|
Color(0xFFFFFFFF).withOpacity(0.12), // Updated to 12%
|
|
width: borderWidth,
|
|
)
|
|
: null,
|
|
boxShadow: showShadow
|
|
? ShadowSystem.level3 // Use unified shadow system
|
|
: [],
|
|
),
|
|
child: child,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Part 7: Quick Migration Checklist
|
|
|
|
```dart
|
|
// Changes needed in existing files:
|
|
|
|
// 1. app_colors.dart
|
|
✓ primary: #00D4FF → #00A0D2
|
|
✓ Add primaryDark, primaryLight
|
|
✓ surfaceVariant, surfaceTertiary, surfaceQuad updated
|
|
✓ glassBackground opacity: 8% → 6%
|
|
✓ glassBorder opacity: 15% → 12%
|
|
|
|
// 2. app_theme.dart
|
|
✓ displayLarge fontSize: 56 → 64
|
|
✓ displayMedium fontSize: 48 → 56
|
|
✓ displaySmall fontSize: 36 → 44
|
|
✓ bodyLarge lineHeight: 1.5 → 1.6
|
|
✓ bodyMedium lineHeight: 1.5 → 1.6
|
|
✓ filledButtonTheme height: 44 → 56px
|
|
✓ Update shadow system with 5 levels
|
|
✓ Add new animation durations (250ms, etc.)
|
|
|
|
// 3. Widget updates
|
|
✓ TvFocusableCard - use new focus animation
|
|
✓ TvModernCard - update shadow levels
|
|
✓ GlassContainer - reduce opacity to 6%
|
|
✓ All buttons - increase min height to 56px
|
|
|
|
// 4. Screen layouts
|
|
✓ TV layouts - increase padding from 32px → 48px
|
|
✓ Grid gaps - maintain 24px (good)
|
|
✓ Navigation - increase touch targets from 44px → 56px
|
|
|
|
// 5. Testing
|
|
✓ Visual regression screenshots
|
|
✓ Color contrast check (WCAG)
|
|
✓ Animation smoothness (60fps)
|
|
✓ Focus visibility at distance
|
|
```
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
This code reference provides:
|
|
|
|
1. **Complete updated color definitions** - Ready to copy/paste into app_colors.dart
|
|
2. **Full typography system** - All 15+ text styles with exact specifications
|
|
3. **Shadow system** - 5-level elevation with exact blur/offset values
|
|
4. **Animation utilities** - Focus, tap, and transition animations
|
|
5. **Button implementation** - Complete working example with all states
|
|
6. **Migration guide** - Quick checklist for updating existing code
|
|
|
|
All code is production-ready and follows Material Design 3 principles with Apple TV adaptations.
|
|
|