feat: add MobilePosterCard widget and MobileLiveTVTab for IPTV live TV functionality.

This commit is contained in:
Michael committed 2026-03-27 15:24:10 +01:00
1 parent a1b1363bc8
commit f07a6e91f9
2 files changed
+93 -96

No files matched your search

@@ -197,10 +197,10 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab>
slivers: [ slivers: [
// Floating Header (Search + Navigation) // Floating Header (Search + Navigation)
SliverAppBar( SliverAppBar(
floating: true, floating: false,
pinned: false, pinned: true,
snap: true, snap: false,
backgroundColor: Colors.transparent, // Minimal background backgroundColor: const Color(0xFF000000), // Opaque black
elevation: 0, elevation: 0,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
+89 -92
View File
@@ -30,8 +30,9 @@ class MobilePosterCard extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
// Shadow lives on this outer Container so ClipRRect doesn't swallow it // FIXED Aspect Ratio for the image part to avoid stretching
Expanded( AspectRatio(
aspectRatio: 2 / 3,
child: Container( child: Container(
decoration: AppDecorations.glossyCard(context, radius: 12), decoration: AppDecorations.glossyCard(context, radius: 12),
child: TVFocusable( child: TVFocusable(
@@ -48,97 +49,32 @@ class MobilePosterCard extends StatelessWidget {
? CachedNetworkImage( ? CachedNetworkImage(
imageUrl: imageUrl!, imageUrl: imageUrl!,
fit: BoxFit.cover, fit: BoxFit.cover,
memCacheWidth: 120, alignment: Alignment.center,
memCacheHeight: 180, memCacheWidth: 200, // Increased for better quality
maxWidthDiskCache: 120, memCacheHeight: 300,
maxHeightDiskCache: 180, maxWidthDiskCache: 400,
maxHeightDiskCache: 600,
cacheManager: AppCacheManager.instance, cacheManager: AppCacheManager.instance,
errorWidget: (_, __, ___) => Container( errorWidget: (_, __, ___) => _buildPlaceholder(context),
decoration: placeholder: (_, __) => _buildPlaceholder(context),
AppDecorations.channelCardBase(context),
child: Icon(
placeholderIcon,
color: AppDecorations.iconMuted(context),
),
),
placeholder: (_, __) => Container(
decoration:
AppDecorations.channelCardBase(context),
child: Icon(
placeholderIcon,
color: AppDecorations.iconMuted(context),
),
),
) )
: Container( : _buildPlaceholder(context),
decoration: AppDecorations.channelCardBase(context),
child: Icon(
placeholderIcon,
color: AppDecorations.iconMuted(context),
),
),
), ),
// Rating badge // Rating badge
if (rating != null) if (rating != null && rating!.isNotEmpty)
Positioned( _buildRatingBadge(),
top: 6,
left: 6,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
const Icon(
Icons.star,
color: Colors.amber,
size: 10,
),
const SizedBox(width: 2),
Text(
rating!,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
// Watched badge // Watched badge
if (isWatched) if (isWatched)
Positioned( _buildWatchedBadge(),
top: 6,
right: 6,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Color(0xFF30D158),
shape: BoxShape.circle,
),
child: const Icon(
Icons.check,
size: 10,
color: Colors.white,
),
),
),
// Glossy highlight overlay (top shimmer) // Glossy highlight overlay (top shimmer)
Positioned( Positioned(
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
height: 80, // Taller for posters height: 60,
child: IgnorePointer( child: IgnorePointer(
child: Container( child: Container(
decoration: AppDecorations.glossShimmer(context, radius: 12), decoration: AppDecorations.glossShimmer(context, radius: 12),
@@ -151,24 +87,85 @@ class MobilePosterCard extends StatelessWidget {
), ),
), ),
const SizedBox(height: 6), const SizedBox(height: 4),
// Title // Title with fixed height to avoid pushing the image
Padding( SizedBox(
padding: const EdgeInsets.symmetric(horizontal: 4), height: 16,
child: Text( child: Padding(
title, padding: const EdgeInsets.symmetric(horizontal: 4),
textAlign: TextAlign.center, child: Text(
maxLines: 1, title,
overflow: TextOverflow.ellipsis, textAlign: TextAlign.center,
style: TextStyle( maxLines: 1,
color: AppDecorations.textPrimary(context), overflow: TextOverflow.ellipsis,
fontSize: 12, style: TextStyle(
fontWeight: FontWeight.w500, color: AppDecorations.textPrimary(context),
fontSize: 11,
fontWeight: FontWeight.w500,
),
), ),
), ),
), ),
], ],
); );
} }
Widget _buildPlaceholder(BuildContext context) {
return Container(
decoration: AppDecorations.channelCardBase(context),
child: Icon(
placeholderIcon,
color: AppDecorations.iconMuted(context),
),
);
}
Widget _buildRatingBadge() {
return Positioned(
top: 6,
left: 6,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.star, color: Colors.amber, size: 10),
const SizedBox(width: 2),
Text(
rating!,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
Widget _buildWatchedBadge() {
return Positioned(
top: 6,
right: 6,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Color(0xFF30D158),
shape: BoxShape.circle,
),
child: const Icon(
Icons.check,
size: 10,
color: Colors.white,
),
),
);
}
} }