Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions lib/config/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -400,9 +400,7 @@ class _SidebarDestination extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
color: selected
? NullFeedTheme.primaryColor.withValues(alpha: 0.13)
: Colors.transparent,
color: selected ? NullFeedTheme.selectionFill : Colors.transparent,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(13)),
clipBehavior: Clip.antiAlias,
child: InkWell(
Expand Down
62 changes: 40 additions & 22 deletions lib/config/theme.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import 'package:flutter/material.dart';

/// NullFeed's visual language: quiet, cinematic surfaces with a purple
/// brand accent for actions and interactive state, plus a bright lime
/// "ready" signal reserved for success states (downloaded, connected).
/// NullFeed's visual language: quiet, cinematic surfaces with a violet
/// brand base and one loud lime signal.
///
/// The violet has two jobs: [primaryColor] paints fills, borders, and
/// interactive surfaces; [accentColor] is violet-as-text — the only violet
/// legible as small text on the dark surfaces (primary tops out at ~4.1:1).
/// [successColor] is intentionally loud and reserved for signal moments —
/// resume/ready cues, watch progress, offline pins, connection confirmed —
/// never ambiance, and never states that appear on every row of a list
/// (on the downloads screen every row is saved, so "Saved" stays muted).
/// Over imagery or video, use pure Colors.white on a solid dark scrim or
/// black chip; textPrimary is for app surfaces.
class NullFeedTheme {
NullFeedTheme._();

Expand All @@ -21,9 +30,18 @@ class NullFeedTheme {
static const Color errorColor = Color(0xFFFF7185);
static const Color successColor = Color(0xFFB8FF5C);
static const Color warningColor = Color(0xFFFFCC66);
static const Color progressBackground = Color(0xFF303B49);
static const Color infoColor = Color(0xFF4DB6AC);
static const Color violetDeepColor = Color(0xFF171526);
static const Color progressBackground = cardHoverColor;
static const Color progressForeground = primaryColor;

/// Passive "how far you are" bars (thumbnail slivers, resume bars) — the
/// lime signal. The interactive player scrubber keeps [progressForeground].
static const Color watchProgressColor = successColor;

/// Shared fill for selected navigation/chip states.
static final Color selectionFill = primaryColor.withValues(alpha: 0.16);

static const LinearGradient ambientGradient = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
Expand All @@ -38,7 +56,7 @@ class NullFeedTheme {
surface: surfaceColor,
error: errorColor,
onPrimary: Colors.white,
onSecondary: Colors.white,
onSecondary: Color(0xFF1B0E33),
onSurface: textPrimary,
onError: Color(0xFF210006),
outline: borderColor,
Expand Down Expand Up @@ -85,7 +103,7 @@ class NullFeedTheme {
height: 68,
backgroundColor: surfaceColor.withValues(alpha: 0.96),
surfaceTintColor: Colors.transparent,
indicatorColor: primaryColor.withValues(alpha: 0.16),
indicatorColor: selectionFill,
indicatorShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
Expand All @@ -110,16 +128,16 @@ class NullFeedTheme {
);
}),
),
navigationRailTheme: const NavigationRailThemeData(
navigationRailTheme: NavigationRailThemeData(
backgroundColor: Colors.transparent,
indicatorColor: Color(0x297C4DFF),
selectedIconTheme: IconThemeData(color: primaryColor),
unselectedIconTheme: IconThemeData(color: textMuted),
selectedLabelTextStyle: TextStyle(
indicatorColor: selectionFill,
selectedIconTheme: const IconThemeData(color: primaryColor),
unselectedIconTheme: const IconThemeData(color: textMuted),
selectedLabelTextStyle: const TextStyle(
color: textPrimary,
fontWeight: FontWeight.w700,
),
unselectedLabelTextStyle: TextStyle(color: textMuted),
unselectedLabelTextStyle: const TextStyle(color: textMuted),
),
textTheme: base.textTheme.copyWith(
displaySmall: const TextStyle(
Expand Down Expand Up @@ -232,7 +250,7 @@ class NullFeedTheme {
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: primaryColor,
foregroundColor: accentColor,
minimumSize: const Size(44, 44),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
Expand All @@ -253,7 +271,7 @@ class NullFeedTheme {
filled: true,
fillColor: cardColor,
labelStyle: const TextStyle(color: textSecondary),
floatingLabelStyle: const TextStyle(color: primaryColor),
floatingLabelStyle: const TextStyle(color: accentColor),
prefixIconColor: textMuted,
suffixIconColor: textMuted,
border: OutlineInputBorder(
Expand All @@ -280,7 +298,7 @@ class NullFeedTheme {
),
chipTheme: base.chipTheme.copyWith(
backgroundColor: cardColor,
selectedColor: primaryColor.withValues(alpha: 0.16),
selectedColor: selectionFill,
disabledColor: cardColor,
side: const BorderSide(color: borderColor),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
Expand All @@ -292,19 +310,19 @@ class NullFeedTheme {
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
),
dialogTheme: const DialogThemeData(
backgroundColor: surfaceColor,
backgroundColor: cardColor,
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
side: BorderSide(color: borderColor),
borderRadius: BorderRadius.all(Radius.circular(24)),
),
),
bottomSheetTheme: const BottomSheetThemeData(
backgroundColor: surfaceColor,
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: cardColor,
surfaceTintColor: Colors.transparent,
modalBackgroundColor: surfaceColor,
modalBarrierColor: Color(0xB307090D),
shape: RoundedRectangleBorder(
modalBackgroundColor: cardColor,
modalBarrierColor: backgroundColor.withValues(alpha: 0.7),
shape: const RoundedRectangleBorder(
side: BorderSide(color: borderColor),
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
),
Expand All @@ -315,7 +333,7 @@ class NullFeedTheme {
color: textPrimary,
fontWeight: FontWeight.w600,
),
actionTextColor: primaryColor,
actionTextColor: accentColor,
shape: RoundedRectangleBorder(
side: const BorderSide(color: borderColor),
borderRadius: BorderRadius.circular(14),
Expand Down
18 changes: 16 additions & 2 deletions lib/screens/add_profile_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import '../models/youtube_import.dart';
import '../providers/auth_provider.dart';
import '../services/api_service.dart';
import '../services/storage_service.dart';
import '../widgets/app_ui.dart';
import '../widgets/profile_avatar.dart';

/// Full-screen profile creation flow.
Expand Down Expand Up @@ -304,7 +305,10 @@ class _AddProfileScreenState extends ConsumerState<AddProfileScreen> {
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
child: CircularProgressIndicator(
strokeWidth: 2,
color: NullFeedTheme.textSecondary,
),
)
: const Text('Lookup'),
),
Expand All @@ -322,6 +326,13 @@ class _AddProfileScreenState extends ConsumerState<AddProfileScreen> {
],
if (_resolvedProfile != null) ...[
const SizedBox(height: 16),
// The flow's earned green moment: the lookup succeeded.
const AppStatusPill(
label: 'PROFILE FOUND',
icon: Icons.check_circle_rounded,
color: NullFeedTheme.successColor,
),
const SizedBox(height: 10),
_IdentityPreviewCard(
profile: _resolvedProfile!,
onClear: _creating ? null : _clearImport,
Expand Down Expand Up @@ -380,7 +391,10 @@ class _AddProfileScreenState extends ConsumerState<AddProfileScreen> {
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
child: CircularProgressIndicator(
strokeWidth: 2,
color: NullFeedTheme.textSecondary,
),
),
const SizedBox(width: 12),
Text(_busyStatus ?? 'Creating profile…'),
Expand Down
28 changes: 20 additions & 8 deletions lib/screens/channel_detail_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,10 @@ class _ChannelDetailScreenState extends ConsumerState<ChannelDetailScreen> {
SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
child: CircularProgressIndicator(
strokeWidth: 2,
color: NullFeedTheme.textSecondary,
),
),
SizedBox(width: 10),
Text('Updating…'),
Expand All @@ -206,7 +209,7 @@ class _ChannelDetailScreenState extends ConsumerState<ChannelDetailScreen> {
style: OutlinedButton.styleFrom(
foregroundColor: NullFeedTheme.errorColor,
side: BorderSide(
color: NullFeedTheme.errorColor.withValues(alpha: 0.5),
color: NullFeedTheme.errorColor.withValues(alpha: 0.7),
),
),
child: content,
Expand Down Expand Up @@ -354,7 +357,7 @@ class _ChannelDetailScreenState extends ConsumerState<ChannelDetailScreen> {
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: NullFeedTheme.primaryColor,
color: NullFeedTheme.accentColor,
),
),
),
Expand All @@ -380,6 +383,9 @@ class _ChannelDetailScreenState extends ConsumerState<ChannelDetailScreen> {
).textTheme.bodyMedium,
),
const SizedBox(height: 8),
// Membership is persistent state, not a
// signal moment — no lime, and "not in
// library" is neutral, not a warning.
AppStatusPill(
label: channel.isSubscribed
? 'IN YOUR LIBRARY'
Expand All @@ -388,8 +394,8 @@ class _ChannelDetailScreenState extends ConsumerState<ChannelDetailScreen> {
? Icons.check_circle_rounded
: Icons.add_circle_outline_rounded,
color: channel.isSubscribed
? NullFeedTheme.successColor
: NullFeedTheme.warningColor,
? NullFeedTheme.textSecondary
: NullFeedTheme.textMuted,
),
],
),
Expand All @@ -416,7 +422,15 @@ class _ChannelDetailScreenState extends ConsumerState<ChannelDetailScreen> {
);
invalidateFeedProviders(ref);
},
icon: const Icon(Icons.play_arrow, size: 24),
// A lime icon marks the resume signal; "Play
// latest" stays plain violet.
icon: Icon(
Icons.play_arrow,
size: 24,
color: target.label == 'Resume'
? NullFeedTheme.successColor
: null,
),
label: Text(target.label),
),
);
Expand Down Expand Up @@ -592,7 +606,6 @@ class _ChannelDetailScreenState extends ConsumerState<ChannelDetailScreen> {

showModalBottomSheet<void>(
context: context,
backgroundColor: NullFeedTheme.cardColor,
builder: (sheetContext) => StatefulBuilder(
builder: (context, setSheetState) => SafeArea(
child: Column(
Expand Down Expand Up @@ -661,7 +674,6 @@ class _ChannelDetailScreenState extends ConsumerState<ChannelDetailScreen> {
void _showVideoMenu(Video video) {
showModalBottomSheet<void>(
context: context,
backgroundColor: NullFeedTheme.cardColor,
builder: (sheetContext) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
Expand Down
6 changes: 3 additions & 3 deletions lib/screens/discover_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ class _RecommendationCardState extends State<_RecommendationCard> {
child: AppStatusPill(
label: 'FOR YOU',
icon: Icons.auto_awesome_rounded,
color: NullFeedTheme.accentColor,
onImage: true,
),
),
],
Expand Down Expand Up @@ -378,7 +378,7 @@ class _RecommendationAvatar extends StatelessWidget {
width: 54,
height: 54,
decoration: BoxDecoration(
color: NullFeedTheme.elevatedSurfaceColor,
color: NullFeedTheme.primaryColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(17),
border: Border.all(color: NullFeedTheme.surfaceColor, width: 3),
image: url == null
Expand All @@ -394,7 +394,7 @@ class _RecommendationAvatar extends StatelessWidget {
? Text(
rec.channelName.isEmpty ? '?' : rec.channelName[0].toUpperCase(),
style: const TextStyle(
color: NullFeedTheme.primaryColor,
color: NullFeedTheme.accentColor,
fontSize: 20,
fontWeight: FontWeight.w800,
),
Expand Down
1 change: 0 additions & 1 deletion lib/screens/downloads_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ class _DownloadsScreenState extends ConsumerState<DownloadsScreen> {
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
backgroundColor: NullFeedTheme.cardColor,
title: const Text('Delete from this device?'),
content: Text(
title.isEmpty
Expand Down
6 changes: 4 additions & 2 deletions lib/screens/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,8 @@ class _SpotlightCard extends StatelessWidget {
? Icons.history_rounded
: Icons.auto_awesome_rounded,
label: canResume ? 'CONTINUE WATCHING' : 'READY TO WATCH',
color: NullFeedTheme.successColor,
onImage: true,
),
const SizedBox(height: 13),
Text(
Expand All @@ -330,6 +332,7 @@ class _SpotlightCard extends StatelessWidget {
child: NullFeedProgressBar(
progress: item.video.watchProgress,
height: 4,
foregroundColor: NullFeedTheme.watchProgressColor,
),
),
],
Expand Down Expand Up @@ -381,7 +384,7 @@ class _SpotlightFallback extends StatelessWidget {
colors: [
NullFeedTheme.elevatedSurfaceColor,
NullFeedTheme.cardColor,
Color(0xFF19162B),
NullFeedTheme.violetDeepColor,
],
),
),
Expand Down Expand Up @@ -596,7 +599,6 @@ class _RecommendationRailCard extends StatelessWidget {
const AppStatusPill(
label: 'WHY IT FITS',
icon: Icons.auto_awesome_rounded,
color: NullFeedTheme.accentColor,
),
const SizedBox(height: 9),
Expanded(
Expand Down
2 changes: 0 additions & 2 deletions lib/screens/library_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen> {
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
backgroundColor: NullFeedTheme.cardColor,
title: Text('Unsubscribe from ${channel.name}?'),
content: const Text(
'This removes the channel from this profile\'s Library. New uploads '
Expand Down Expand Up @@ -376,7 +375,6 @@ class _SubscribeDialogState extends ConsumerState<_SubscribeDialog> {
@override
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: NullFeedTheme.cardColor,
title: const Text('Add a YouTube channel'),
content: Column(
mainAxisSize: MainAxisSize.min,
Expand Down
Loading
Loading