Files
SpotiFLAC-Mobile/lib/widgets/explore_featured_section.dart
T
zarzet 4c5af08cdd fix(ui): resolve album titles and show Home explicit badges
Hide the profile setup hint once a name or photo is saved.
2026-09-21 19:02:13 +07:00

170 lines
7.0 KiB
Dart

import 'package:flutter/material.dart';
import 'package:spotiflac_android/providers/explore_provider.dart';
import 'package:spotiflac_android/widgets/cached_cover_image.dart';
import 'package:spotiflac_android/widgets/audio_quality_badges.dart';
/// Large editorial cards requested by a home-feed provider.
class ExploreFeaturedSection extends StatelessWidget {
final ExploreSection section;
final ValueChanged<ExploreItem> onItemTap;
const ExploreFeaturedSection({
super.key,
required this.section,
required this.onItemTap,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colors = theme.colorScheme;
final scaler = MediaQuery.textScalerOf(context);
return LayoutBuilder(
builder: (context, constraints) {
final width = (constraints.maxWidth - 48).clamp(160.0, 560.0);
final imageHeight = width / 1.5;
final headingHeight = scaler.scale(12) * 1.5;
final titleHeight = scaler.scale(22) * 1.4;
final artistHeight = scaler.scale(16) * 1.5;
final descriptionHeight = scaler.scale(14) * 1.5 * 2;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 20, 16, 12),
child: Text(
section.title,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
SizedBox(
height:
headingHeight +
titleHeight +
artistHeight +
imageHeight +
descriptionHeight +
24,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: section.items.length,
separatorBuilder: (_, _) => const SizedBox(width: 12),
itemBuilder: (context, index) {
final item = section.items[index];
final image = item.featuredCoverUrl?.isNotEmpty == true
? item.featuredCoverUrl
: item.coverUrl;
final fallback = ColoredBox(
color: colors.surfaceContainerHighest,
child: Center(
child: Icon(
Icons.album_outlined,
size: 48,
color: colors.onSurfaceVariant,
),
),
);
return Semantics(
button: true,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => onItemTap(item),
child: SizedBox(
width: width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: headingHeight,
child: Text(
item.heading ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
height: 1.5,
fontWeight: FontWeight.w600,
color: colors.primary,
),
),
),
SizedBox(
height: titleHeight,
child: ExplicitTrackTitle(
title: item.name,
explicit: item.explicit == true,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 22,
height: 1.4,
),
),
),
SizedBox(
height: artistHeight,
child: Text(
item.artists,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 16,
height: 1.5,
color: colors.onSurfaceVariant,
),
),
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SizedBox(
width: width,
height: imageHeight,
child: image == null || image.isEmpty
? fallback
: CachedCoverImage(
imageUrl: image,
// Decode one axis so wide editorial art
// keeps its ratio on high-density screens.
memCacheWidth:
(width *
MediaQuery.devicePixelRatioOf(
context,
))
.ceil()
.clamp(320, 960),
fit: BoxFit.cover,
placeholder: (_, _) => fallback,
errorWidget: (_, _, _) => fallback,
),
),
),
const SizedBox(height: 8),
Text(
item.description ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
height: 1.5,
color: colors.onSurfaceVariant,
),
),
],
),
),
),
);
},
),
),
],
);
},
);
}
}