mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
feat(theme): complete redesign to Cyber-Cinematic Glass (Stitch)
Replace the legacy Apple TV-style purple/cyan theme with the full Google Stitch Cyber-Cinematic Glass design system across the entire app (desktop + mobile). - New Material 3 dark ColorScheme: background #121317, primary #adc6ff, primaryContainer #4b8eff, surface containers, etc. - Typography: Space Grotesk (headlines) + Inter (body/labels) - 3-level glassmorphism via GlassContainer: base, glass, floating - Primary gradient: #007AFF → #00C6FF with blue glow effects - Removed old compatibility aliases (focusColor, border, textPrimary, textSecondary); all code now uses semantic Stitch tokens - Systematically replaced all Colors.white/black/red/grey and GoogleFonts.roboto/outfit with Stitch equivalents - All 36 lib/ files updated, zero compilation errors Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
a9a1127d6b
commit
f7eaa724e2
36 files changed
+1651
-986
No files matched your search
@@ -92,7 +92,7 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
||||
boxShadow: [
|
||||
if (isActive)
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
color: AppColors.onPrimaryContainer.withOpacity(0.5),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
@@ -107,10 +107,10 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
||||
CachedNetworkImage(
|
||||
imageUrl: item.imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => Container(color: AppColors.surfaceVariant),
|
||||
placeholder: (context, url) => Container(color: AppColors.surfaceContainerLow),
|
||||
errorWidget: (context, url, error) => Container(
|
||||
color: AppColors.surfaceVariant,
|
||||
child: const Icon(Icons.movie, size: 64, color: AppColors.textTertiary),
|
||||
color: AppColors.surfaceContainerLow,
|
||||
child: const Icon(Icons.movie, size: 64, color: AppColors.outline),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -122,8 +122,8 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black.withOpacity(0.2),
|
||||
Colors.black.withOpacity(0.9),
|
||||
AppColors.onPrimaryContainer.withOpacity(0.2),
|
||||
AppColors.onPrimaryContainer.withOpacity(0.9),
|
||||
],
|
||||
stops: const [0.0, 0.5, 1.0],
|
||||
),
|
||||
@@ -140,10 +140,10 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
||||
children: [
|
||||
Text(
|
||||
item.title,
|
||||
style: GoogleFonts.outfit(
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
color: AppColors.onSurface,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -154,7 +154,7 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
||||
item.subtitle!,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 16,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
color: AppColors.onSurface,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -219,24 +219,34 @@ class _HeroButtonState extends State<_HeroButton> {
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
gradient: widget.isPrimary ? AppColors.primaryGradient : null,
|
||||
color: widget.isPrimary
|
||||
? (_isHovered ? Colors.white : Colors.white.withOpacity(0.9))
|
||||
: (_isHovered ? Colors.white.withOpacity(0.2) : Colors.white.withOpacity(0.1)),
|
||||
? null
|
||||
: (_isHovered ? AppColors.surfaceContainerHigh : AppColors.surfaceContainer),
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
|
||||
boxShadow: widget.isPrimary
|
||||
? [
|
||||
BoxShadow(
|
||||
color: AppColors.glowPrimary(0.3),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
widget.icon,
|
||||
color: widget.isPrimary ? Colors.black : Colors.white,
|
||||
color: AppColors.onSurface,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
widget.label,
|
||||
style: GoogleFonts.inter(
|
||||
color: widget.isPrimary ? Colors.black : Colors.white,
|
||||
color: AppColors.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../theme/app_colors.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import '../glass_container.dart';
|
||||
|
||||
/// Glassmorphism card widget with blur effect
|
||||
/// Stitch-style glass card using GlassContainer Level 1.
|
||||
class GlassCard extends StatelessWidget {
|
||||
final Widget child;
|
||||
final EdgeInsetsGeometry padding;
|
||||
@@ -23,25 +25,18 @@ class GlassCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Material(
|
||||
color: AppColors.glassBackground,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
child: Container(
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
border: showBorder
|
||||
? Border.all(color: AppColors.glassBorder, width: 1)
|
||||
: null,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
return GlassContainer.glass(
|
||||
borderRadius: borderRadius,
|
||||
border: showBorder,
|
||||
padding: EdgeInsets.zero,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -49,7 +44,7 @@ class GlassCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Animated gradient button
|
||||
/// Animated gradient primary button with Stitch glow.
|
||||
class GradientButton extends StatefulWidget {
|
||||
final String label;
|
||||
final VoidCallback? onPressed;
|
||||
@@ -106,12 +101,17 @@ class _GradientButtonState extends State<GradientButton>
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppColors.primaryGradient,
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.primary.withOpacity(0.3),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
color: AppColors.glowPrimary(0.3),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
BoxShadow(
|
||||
color: AppColors.glowPrimary(0.1),
|
||||
blurRadius: 30,
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -122,7 +122,7 @@ class _GradientButtonState extends State<GradientButton>
|
||||
onTapDown: (_) => _controller.forward(),
|
||||
onTapUp: (_) => _controller.reverse(),
|
||||
onTapCancel: () => _controller.reverse(),
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
@@ -134,24 +134,26 @@ class _GradientButtonState extends State<GradientButton>
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.black,
|
||||
color: AppColors.onSurface,
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
mainAxisSize:
|
||||
widget.isExpanded ? MainAxisSize.max : MainAxisSize.min,
|
||||
mainAxisSize: widget.isExpanded
|
||||
? MainAxisSize.max
|
||||
: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (widget.icon != null) ...[
|
||||
Icon(widget.icon, color: Colors.black, size: 20),
|
||||
Icon(widget.icon, color: AppColors.onSurface, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Text(
|
||||
widget.label,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
style: GoogleFonts.inter(
|
||||
color: AppColors.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
letterSpacing: 0.05,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -168,7 +170,7 @@ class _GradientButtonState extends State<GradientButton>
|
||||
}
|
||||
}
|
||||
|
||||
/// Shimmer loading effect widget
|
||||
/// Shimmer loading effect using surface container tones.
|
||||
class ShimmerLoading extends StatefulWidget {
|
||||
final double width;
|
||||
final double height;
|
||||
@@ -220,9 +222,9 @@ class _ShimmerLoadingState extends State<ShimmerLoading>
|
||||
begin: Alignment(_animation.value - 1, 0),
|
||||
end: Alignment(_animation.value + 1, 0),
|
||||
colors: [
|
||||
AppColors.surface.withOpacity(0.5),
|
||||
AppColors.surface,
|
||||
AppColors.surface.withOpacity(0.5),
|
||||
AppColors.surfaceContainerLow.withOpacity(0.5),
|
||||
AppColors.surfaceContainer,
|
||||
AppColors.surfaceContainerLow.withOpacity(0.5),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -232,29 +234,30 @@ class _ShimmerLoadingState extends State<ShimmerLoading>
|
||||
}
|
||||
}
|
||||
|
||||
/// Live indicator badge
|
||||
/// Live indicator badge — Stitch style pill.
|
||||
class LiveBadge extends StatelessWidget {
|
||||
const LiveBadge({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.live,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Row(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.circle, size: 6, color: Colors.white),
|
||||
SizedBox(width: 4),
|
||||
const Icon(Icons.circle, size: 6, color: AppColors.onSurface),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'LIVE',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
style: GoogleFonts.inter(
|
||||
color: AppColors.onSurface,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -263,7 +266,7 @@ class LiveBadge extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Category chip with color
|
||||
/// Stitch-style category chip — pill shaped with primary accent.
|
||||
class CategoryChip extends StatelessWidget {
|
||||
final String label;
|
||||
final bool isSelected;
|
||||
@@ -280,8 +283,8 @@ class CategoryChip extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: isSelected
|
||||
? AppColors.primary.withOpacity(0.2)
|
||||
: AppColors.surface,
|
||||
? AppColors.primary.withOpacity(0.1)
|
||||
: AppColors.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
@@ -291,15 +294,18 @@ class CategoryChip extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
|
||||
border: Border.all(
|
||||
color: isSelected ? AppColors.primary : AppColors.border,
|
||||
color: isSelected
|
||||
? AppColors.primary.withOpacity(0.3)
|
||||
: AppColors.outlineVariant,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 13,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
||||
color: isSelected ? AppColors.primary : AppColors.textSecondary,
|
||||
color: isSelected ? AppColors.primary : AppColors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -308,7 +314,7 @@ class CategoryChip extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Apple TV style Media Card (Vertical Poster)
|
||||
/// Media poster card with Stitch glass hover effect.
|
||||
class MediaCard extends StatefulWidget {
|
||||
final String title;
|
||||
final String? imageUrl;
|
||||
@@ -341,6 +347,9 @@ class _MediaCardState extends State<MediaCard>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final focusBorderColor =
|
||||
_isHovered ? AppColors.primaryContainer.withOpacity(0.5) : Colors.transparent;
|
||||
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovered = true),
|
||||
onExit: (_) => setState(() => _isHovered = false),
|
||||
@@ -362,21 +371,32 @@ class _MediaCardState extends State<MediaCard>
|
||||
duration: AppTheme.durationFast,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||
color: AppColors.surface,
|
||||
color: AppColors.surfaceContainerLow,
|
||||
border: Border.all(
|
||||
color: _isHovered ? AppColors.focusColor : Colors.transparent,
|
||||
width: _isHovered ? 3 : 0,
|
||||
color: focusBorderColor,
|
||||
width: _isHovered ? 2 : 0,
|
||||
),
|
||||
boxShadow: _isHovered
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
blurRadius: 20,
|
||||
blurRadius: 25,
|
||||
offset: const Offset(0, 10),
|
||||
spreadRadius: 2,
|
||||
),
|
||||
BoxShadow(
|
||||
color: AppColors.glowPrimary(0.3),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
BoxShadow(
|
||||
color: AppColors.glowPrimary(0.1),
|
||||
blurRadius: 40,
|
||||
spreadRadius: 4,
|
||||
),
|
||||
]
|
||||
: [
|
||||
BoxShadow(
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
@@ -384,7 +404,8 @@ class _MediaCardState extends State<MediaCard>
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusMd - (_isHovered ? 2 : 0)),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppTheme.radiusMd - (_isHovered ? 2 : 0)),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
@@ -393,33 +414,38 @@ class _MediaCardState extends State<MediaCard>
|
||||
imageUrl: widget.imageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => Container(
|
||||
color: AppColors.surface,
|
||||
color: AppColors.surfaceContainerLow,
|
||||
child: Center(
|
||||
child: Icon(widget.placeholderIcon, color: AppColors.textTertiary),
|
||||
child: Icon(widget.placeholderIcon,
|
||||
color: AppColors.outline),
|
||||
),
|
||||
),
|
||||
errorWidget: (context, url, error) => Container(
|
||||
color: AppColors.surface,
|
||||
color: AppColors.surfaceContainerLow,
|
||||
child: Center(
|
||||
child: Icon(widget.placeholderIcon, color: AppColors.textTertiary),
|
||||
child: Icon(widget.placeholderIcon,
|
||||
color: AppColors.outline),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
color: AppColors.surface,
|
||||
color: AppColors.surfaceContainerLow,
|
||||
child: Center(
|
||||
child: Icon(widget.placeholderIcon, color: AppColors.textTertiary),
|
||||
child: Icon(widget.placeholderIcon,
|
||||
color: AppColors.outline),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// Rating Badge (Top Left)
|
||||
if (widget.rating != null && widget.rating!.isNotEmpty)
|
||||
if (widget.rating != null &&
|
||||
widget.rating!.isNotEmpty)
|
||||
Positioned(
|
||||
top: 8,
|
||||
left: 8,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
@@ -427,11 +453,16 @@ class _MediaCardState extends State<MediaCard>
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.star, size: 10, color: Color(0xFFFFD700)),
|
||||
const Icon(Icons.star,
|
||||
size: 10, color: AppColors.primaryContainer),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
widget.rating!,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold),
|
||||
style: GoogleFonts.inter(
|
||||
color: AppColors.onSurface,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -449,7 +480,8 @@ class _MediaCardState extends State<MediaCard>
|
||||
color: AppColors.success,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.check, size: 12, color: Colors.white),
|
||||
child: const Icon(Icons.check,
|
||||
size: 12, color: AppColors.onSurface),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -458,7 +490,7 @@ class _MediaCardState extends State<MediaCard>
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// Title & Subtitle (Below)
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
@@ -470,9 +502,12 @@ class _MediaCardState extends State<MediaCard>
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: _isHovered ? AppColors.focusColor : AppColors.textSecondary,
|
||||
fontWeight: _isHovered ? FontWeight.w600 : FontWeight.w500,
|
||||
style: GoogleFonts.inter(
|
||||
color: _isHovered
|
||||
? AppColors.primary
|
||||
: AppColors.onSurface,
|
||||
fontWeight:
|
||||
_isHovered ? FontWeight.w600 : FontWeight.w500,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
@@ -483,8 +518,8 @@ class _MediaCardState extends State<MediaCard>
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textTertiary,
|
||||
style: GoogleFonts.inter(
|
||||
color: AppColors.onSurfaceVariant,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
|
||||
class DigitalClock extends StatefulWidget {
|
||||
const DigitalClock({super.key});
|
||||
@@ -40,7 +41,7 @@ class _DigitalClockState extends State<DigitalClock> {
|
||||
style: GoogleFonts.inter(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
color: AppColors.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -2,6 +2,13 @@ import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
|
||||
/// Glassmorphism container with 3 depth levels (Stitch Design System)
|
||||
///
|
||||
/// Level 0: Pure base background (#0F1014), no blur.
|
||||
/// Level 1: Translucent layer with backdrop blur 20px, 1px border 10% white.
|
||||
/// Level 2: High-blur floating container with subtle inner glow in primary accent.
|
||||
enum GlassLevel { level0, level1, level2 }
|
||||
|
||||
class GlassContainer extends StatelessWidget {
|
||||
final Widget child;
|
||||
final double? width;
|
||||
@@ -9,9 +16,10 @@ class GlassContainer extends StatelessWidget {
|
||||
final EdgeInsetsGeometry padding;
|
||||
final EdgeInsetsGeometry? margin;
|
||||
final double borderRadius;
|
||||
final double opacity;
|
||||
final GlassLevel level;
|
||||
final Color? borderColor;
|
||||
final bool border;
|
||||
final List<BoxShadow>? boxShadow;
|
||||
|
||||
const GlassContainer({
|
||||
super.key,
|
||||
@@ -21,13 +29,87 @@ class GlassContainer extends StatelessWidget {
|
||||
this.padding = const EdgeInsets.all(16),
|
||||
this.margin,
|
||||
this.borderRadius = 16.0,
|
||||
this.opacity = 0.6,
|
||||
this.level = GlassLevel.level1,
|
||||
this.borderColor,
|
||||
this.border = true,
|
||||
this.boxShadow,
|
||||
});
|
||||
|
||||
/// Convenience constructor for Level 0 (pure base)
|
||||
const GlassContainer.base({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.width,
|
||||
this.height,
|
||||
this.padding = const EdgeInsets.all(16),
|
||||
this.margin,
|
||||
this.borderRadius = 16.0,
|
||||
this.borderColor,
|
||||
this.border = false,
|
||||
this.boxShadow,
|
||||
}) : level = GlassLevel.level0;
|
||||
|
||||
/// Convenience constructor for Level 1 (standard glass)
|
||||
const GlassContainer.glass({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.width,
|
||||
this.height,
|
||||
this.padding = const EdgeInsets.all(16),
|
||||
this.margin,
|
||||
this.borderRadius = 16.0,
|
||||
this.borderColor,
|
||||
this.border = true,
|
||||
this.boxShadow,
|
||||
}) : level = GlassLevel.level1;
|
||||
|
||||
/// Convenience constructor for Level 2 (floating with inner glow)
|
||||
const GlassContainer.floating({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.width,
|
||||
this.height,
|
||||
this.padding = const EdgeInsets.all(16),
|
||||
this.margin,
|
||||
this.borderRadius = 24.0,
|
||||
this.borderColor,
|
||||
this.border = true,
|
||||
this.boxShadow,
|
||||
}) : level = GlassLevel.level2;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
switch (level) {
|
||||
case GlassLevel.level0:
|
||||
return _buildLevel0();
|
||||
case GlassLevel.level1:
|
||||
return _buildLevel1();
|
||||
case GlassLevel.level2:
|
||||
return _buildLevel2();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildLevel0() {
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
margin: margin,
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.baseLevel0,
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
border: border
|
||||
? Border.all(
|
||||
color: borderColor ?? Colors.transparent,
|
||||
width: 1,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLevel1() {
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
@@ -35,32 +117,82 @@ class GlassContainer extends StatelessWidget {
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
|
||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: Container(
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
color: AppColors.glassLevel1Bg,
|
||||
border: border
|
||||
? Border.all(
|
||||
color: borderColor ?? Colors.white.withOpacity(0.1),
|
||||
color: borderColor ?? AppColors.glassLevel1Border,
|
||||
width: 1,
|
||||
)
|
||||
: null,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
AppColors.surface.withOpacity(opacity),
|
||||
AppColors.surface.withOpacity(opacity * 0.5),
|
||||
],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 20,
|
||||
spreadRadius: -5,
|
||||
),
|
||||
],
|
||||
boxShadow: boxShadow ??
|
||||
[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 20,
|
||||
spreadRadius: -5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLevel2() {
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
margin: margin,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40),
|
||||
child: Container(
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
color: AppColors.glassLevel2Bg,
|
||||
border: border
|
||||
? Border(
|
||||
top: BorderSide(
|
||||
color: borderColor ?? AppColors.glassLevel2InnerGlow,
|
||||
width: 1,
|
||||
),
|
||||
left: BorderSide(
|
||||
color: borderColor ?? AppColors.glassLevel2InnerGlow,
|
||||
width: 1,
|
||||
),
|
||||
right: BorderSide(
|
||||
color: borderColor ?? AppColors.glassLevel2Border,
|
||||
width: 1,
|
||||
),
|
||||
bottom: BorderSide(
|
||||
color: borderColor ?? AppColors.glassLevel2Border,
|
||||
width: 1,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
boxShadow: boxShadow ??
|
||||
[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
blurRadius: 30,
|
||||
spreadRadius: -10,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
BoxShadow(
|
||||
color: AppColors.primary.withOpacity(0.1),
|
||||
blurRadius: 40,
|
||||
spreadRadius: -10,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
import 'glass_container.dart';
|
||||
import 'tv_focusable_card.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class HeroCarouselItem {
|
||||
final String id;
|
||||
@@ -12,6 +12,7 @@ class HeroCarouselItem {
|
||||
final String? imageUrl;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onPlay;
|
||||
final List<String>? badges;
|
||||
|
||||
HeroCarouselItem({
|
||||
required this.id,
|
||||
@@ -20,6 +21,7 @@ class HeroCarouselItem {
|
||||
this.imageUrl,
|
||||
this.onTap,
|
||||
this.onPlay,
|
||||
this.badges,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -88,10 +90,9 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
||||
if (widget.items.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return SizedBox(
|
||||
height: 500, // Cinematic height
|
||||
height: 560,
|
||||
child: Stack(
|
||||
children: [
|
||||
// Carousel
|
||||
PageView.builder(
|
||||
controller: _pageController,
|
||||
onPageChanged: (index) {
|
||||
@@ -106,7 +107,7 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
||||
|
||||
// Indicators
|
||||
Positioned(
|
||||
bottom: 24,
|
||||
bottom: 32,
|
||||
right: 48,
|
||||
child: Row(
|
||||
children: List.generate(
|
||||
@@ -118,7 +119,7 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: _currentIndex == index
|
||||
? AppColors.primary
|
||||
? AppColors.primaryContainer
|
||||
: Colors.white.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
@@ -135,15 +136,17 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Background Image with Ken Burns Effect (Simulated via Scale in future, static for now)
|
||||
// Background Image
|
||||
if (item.imageUrl != null && item.imageUrl!.isNotEmpty)
|
||||
Image.network(
|
||||
_getProxiedUrl(item.imageUrl)!,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Container(color: AppColors.surface),
|
||||
),
|
||||
errorBuilder: (_, __, ___) => Container(color: AppColors.surfaceContainerLow),
|
||||
)
|
||||
else
|
||||
Container(color: AppColors.surfaceContainerLow),
|
||||
|
||||
// Gradient Overlays
|
||||
// Gradient Overlays — Stitch style
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
@@ -151,9 +154,9 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
AppColors.background.withOpacity(0.2),
|
||||
AppColors.background.withOpacity(0.9),
|
||||
AppColors.background,
|
||||
AppColors.baseLevel0.withOpacity(0.4),
|
||||
AppColors.baseLevel0.withOpacity(0.9),
|
||||
AppColors.baseLevel0,
|
||||
],
|
||||
stops: const [0.0, 0.5, 0.8, 1.0],
|
||||
),
|
||||
@@ -167,7 +170,7 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
||||
end: Alignment.centerLeft,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
AppColors.background.withOpacity(0.8),
|
||||
AppColors.baseLevel0.withOpacity(0.8),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -180,25 +183,60 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Category Tag
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColors.primary.withOpacity(0.5)),
|
||||
),
|
||||
child: Text(
|
||||
'FEATURED',
|
||||
style: GoogleFonts.inter(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
letterSpacing: 1.5,
|
||||
// Badges row
|
||||
if (item.badges != null && item.badges!.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
children: item.badges!.map((badge) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: AppColors.primary.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
badge.toUpperCase(),
|
||||
style: GoogleFonts.inter(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 11,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: AppColors.primary.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'FEATURED',
|
||||
style: GoogleFonts.inter(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 12,
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Title
|
||||
@@ -206,11 +244,12 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
||||
width: 600,
|
||||
child: Text(
|
||||
item.title,
|
||||
style: GoogleFonts.outfit(
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.onSurface,
|
||||
height: 1.1,
|
||||
letterSpacing: -0.02,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -221,8 +260,9 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
||||
Text(
|
||||
item.subtitle!,
|
||||
style: GoogleFonts.inter(
|
||||
color: AppColors.textSecondary,
|
||||
color: AppColors.onSurfaceVariant,
|
||||
fontSize: 18,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -245,7 +285,7 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.primary.withOpacity(0.4),
|
||||
color: AppColors.primaryContainer.withOpacity(0.4),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
@@ -256,13 +296,13 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.play_arrow_rounded,
|
||||
color: Colors.white,
|
||||
color: AppColors.onSurface,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'WATCH NOW',
|
||||
style: GoogleFonts.inter(
|
||||
color: Colors.white,
|
||||
color: AppColors.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
@@ -273,9 +313,9 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
TvFocusableCard(
|
||||
onTap: item.onTap, // Add to favorites or details
|
||||
onTap: item.onTap,
|
||||
borderRadius: 50,
|
||||
child: GlassContainer(
|
||||
child: GlassContainer.glass(
|
||||
borderRadius: 50,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
@@ -284,12 +324,15 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.info_outline, color: Colors.white),
|
||||
const Icon(
|
||||
Icons.info_outline,
|
||||
color: AppColors.onSurface,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'DETAILS',
|
||||
style: GoogleFonts.inter(
|
||||
color: Colors.white,
|
||||
color: AppColors.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
@@ -300,7 +343,7 @@ class _HeroCarouselState extends State<HeroCarousel> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 48), // Bottom padding
|
||||
const SizedBox(height: 48),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
|
||||
/// Stitch-themed loading screen with dark gradient and blue accent.
|
||||
class ThemedLoading extends StatelessWidget {
|
||||
const ThemedLoading({super.key});
|
||||
|
||||
@@ -12,7 +15,7 @@ class ThemedLoading extends StatelessWidget {
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Color(0xFF0F1014), // Deep Space Dark
|
||||
Color(0xFF181920), // Soft Eerie Black
|
||||
Color(0xFF121317), // Stitch background
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -21,16 +24,16 @@ class ThemedLoading extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CircularProgressIndicator(
|
||||
color: Color(0xFF007AFF), // Electric Blue (AppColors.primary)
|
||||
color: Color(0xFF4b8eff), // AppColors.primaryContainer
|
||||
strokeWidth: 3,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Loading...',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Outfit', // Match AppTheme
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
style: GoogleFonts.inter(
|
||||
color: AppColors.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
|
||||
/// TV focusable card with Stitch glow effect.
|
||||
///
|
||||
/// On focus/hover: blue glow border, subtle scale, diffused shadow.
|
||||
class TvFocusableCard extends StatefulWidget {
|
||||
final Widget child;
|
||||
final VoidCallback? onTap;
|
||||
@@ -16,7 +19,7 @@ class TvFocusableCard extends StatefulWidget {
|
||||
required this.child,
|
||||
this.onTap,
|
||||
this.onFocus,
|
||||
this.scaleFactor = 1.05,
|
||||
this.scaleFactor = 1.03,
|
||||
this.animationDuration = const Duration(milliseconds: 200),
|
||||
this.focusColor,
|
||||
this.autofocus = false,
|
||||
@@ -32,6 +35,7 @@ class _TvFocusableCardState extends State<TvFocusableCard>
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _scaleAnimation;
|
||||
bool _isFocused = false;
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -64,8 +68,6 @@ class _TvFocusableCardState extends State<TvFocusableCard>
|
||||
}
|
||||
}
|
||||
|
||||
bool _isHovered = false;
|
||||
|
||||
void _handleHover(bool isHovered) {
|
||||
setState(() {
|
||||
_isHovered = isHovered;
|
||||
@@ -80,6 +82,7 @@ class _TvFocusableCardState extends State<TvFocusableCard>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isActive = _isFocused || _isHovered;
|
||||
final focusColor = widget.focusColor ?? AppColors.primaryContainer;
|
||||
|
||||
return MouseRegion(
|
||||
onEnter: (_) => _handleHover(true),
|
||||
@@ -109,10 +112,13 @@ class _TvFocusableCardState extends State<TvFocusableCard>
|
||||
borderRadius: BorderRadius.circular(widget.borderRadius),
|
||||
border: isActive
|
||||
? Border.all(
|
||||
color: widget.focusColor ?? AppColors.focusBorder,
|
||||
width: 2, // Slightly thinner for cleaner look
|
||||
color: focusColor.withOpacity(0.5),
|
||||
width: 2,
|
||||
)
|
||||
: Border.all(color: Colors.transparent, width: 2),
|
||||
: Border.all(
|
||||
color: AppColors.glassLevel1Border,
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: isActive
|
||||
? [
|
||||
BoxShadow(
|
||||
@@ -122,15 +128,22 @@ class _TvFocusableCardState extends State<TvFocusableCard>
|
||||
spreadRadius: 2,
|
||||
),
|
||||
BoxShadow(
|
||||
color: (widget.focusColor ?? AppColors.focusBorder)
|
||||
.withOpacity(0.3),
|
||||
blurRadius: 15,
|
||||
color: focusColor.withOpacity(0.3),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
BoxShadow(
|
||||
color: focusColor.withOpacity(0.1),
|
||||
blurRadius: 40,
|
||||
spreadRadius: 4,
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
child: widget.child,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(widget.borderRadius),
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user