import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../media/media_item.dart'; import '../media/media_item_types.dart'; import '../media/media_kind.dart'; import '../services/settings_service.dart'; import '../theme/mono_tokens.dart'; import 'app_icon.dart'; import 'media_progress_bar.dart'; import 'unwatched_count_badge.dart'; /// Size preset for [WatchedIndicator]: [standard] for grid/poster cards, /// [compact] for dense surfaces (folder tree rows, episode thumbnails). /// Add a preset here instead of hand-rolling a new overlay variant. enum WatchedIndicatorSize { standard( checkInset: 4, checkPadding: 4, checkIconSize: 16, badgeSize: 24, badgeFontSize: 12, barRadius: 8, barMinHeight: 4, ), compact( checkInset: 3, checkPadding: 2, checkIconSize: 12, badgeSize: 20, badgeFontSize: 10, barRadius: 6, barMinHeight: 3, ); const WatchedIndicatorSize({ required this.checkInset, required this.checkPadding, required this.checkIconSize, required this.badgeSize, required this.badgeFontSize, required this.barRadius, required this.barMinHeight, }); final double checkInset; final double checkPadding; final double checkIconSize; final double badgeSize; final double badgeFontSize; final double barRadius; final double barMinHeight; } /// Watched/progress overlay for media artwork: watched checkmark, /// unwatched-count pill (shows/seasons), active-progress bar, and season /// completion bar. The single implementation behind every surface that /// stamps watch state onto a poster/thumbnail. class WatchedIndicator extends StatelessWidget { final MediaItem item; final WatchedIndicatorSize size; /// Overrides the settings read for the unwatched-count pill — pass it when /// the caller already watches [SettingsService.showUnwatchedCount] so pill /// visibility updates reactively with the caller's rebuilds. final bool? showUnwatchedCount; /// When false, in-progress state is ignored (no bar; checkmark still shown /// for watched items) — pass false where progress isn't tracked (offline). final bool progressAvailable; const WatchedIndicator({ super.key, required this.item, this.size = WatchedIndicatorSize.standard, this.showUnwatchedCount, this.progressAvailable = true, }); @override Widget build(BuildContext context) { final bool showCount = showUnwatchedCount ?? SettingsService.instance.read(SettingsService.showUnwatchedCount); final hasActiveProgress = progressAvailable && item.hasActiveProgress; final unwatched = item.unwatchedCount; final barRadius = BorderRadius.only( bottomLeft: Radius.circular(size.barRadius), bottomRight: Radius.circular(size.barRadius), ); return Stack( children: [ // Watched checkmark if (item.isWatched && !hasActiveProgress) Positioned( top: size.checkInset, right: size.checkInset, child: Container( padding: EdgeInsets.all(size.checkPadding), decoration: BoxDecoration( color: tokens(context).text, shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.3), blurRadius: 4)], ), child: AppIcon(Symbols.check_rounded, fill: 1, color: tokens(context).bg, size: size.checkIconSize), ), ), // Unwatched count for shows/seasons if (showCount && !item.isWatched && (item.kind == MediaKind.show || item.kind == MediaKind.season) && unwatched != null && unwatched > 0) Positioned( top: size.checkInset, right: size.checkInset, child: UnwatchedCountBadge(count: unwatched, size: size.badgeSize, fontSize: size.badgeFontSize), ), // Progress bar for partially watched content (episodes/movies) if (hasActiveProgress) Positioned( bottom: 0, left: 0, right: 0, child: ClipRRect( borderRadius: barRadius, child: MediaProgressBar( viewOffset: item.viewOffsetMs!, duration: item.durationMs!, minHeight: size.barMinHeight, ), ), ), // Progress bar for seasons (viewed leaves / total leaves). if (item.isSeason && item.isPartiallyWatched) Positioned( bottom: 0, left: 0, right: 0, child: ClipRRect( borderRadius: barRadius, child: LinearProgressIndicator( value: item.leafWatchFraction, backgroundColor: tokens(context).outline, valueColor: AlwaysStoppedAnimation(Theme.of(context).colorScheme.primary), minHeight: size.barMinHeight, ), ), ), ], ); } }