602 lines
17 KiB
Dart
602 lines
17 KiB
Dart
|
|
import 'dart:ui';
|
||
|
|
|
||
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
||
|
|
import 'package:flutter/material.dart';
|
||
|
|
|
||
|
|
import '../constants/breakpoints.dart';
|
||
|
|
import '../theme/app_theme.dart';
|
||
|
|
import 'banner_carousel_mixin.dart';
|
||
|
|
import 'circle_nav_icon_button.dart';
|
||
|
|
import 'frosted_panel.dart';
|
||
|
|
import 'smart_image.dart';
|
||
|
|
|
||
|
|
|
||
|
|
class BannerSlideData {
|
||
|
|
final String? imageUrl;
|
||
|
|
final String? fallbackImageUrl;
|
||
|
|
final Map<String, String>? imageHeaders;
|
||
|
|
|
||
|
|
|
||
|
|
final String? logoUrl;
|
||
|
|
final String title;
|
||
|
|
final String? overview;
|
||
|
|
|
||
|
|
|
||
|
|
final String? rating;
|
||
|
|
|
||
|
|
|
||
|
|
final List<String> primaryMetaLabels;
|
||
|
|
|
||
|
|
|
||
|
|
final List<String> secondaryMetaLabels;
|
||
|
|
|
||
|
|
const BannerSlideData({
|
||
|
|
required this.imageUrl,
|
||
|
|
required this.title,
|
||
|
|
this.fallbackImageUrl,
|
||
|
|
this.imageHeaders,
|
||
|
|
this.logoUrl,
|
||
|
|
this.overview,
|
||
|
|
this.rating,
|
||
|
|
this.primaryMetaLabels = const [],
|
||
|
|
this.secondaryMetaLabels = const [],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
class BannerCarouselScaffold extends StatelessWidget {
|
||
|
|
final BannerCarouselMixin controller;
|
||
|
|
final List<BannerSlideData> slides;
|
||
|
|
final ValueChanged<int> onSlideTap;
|
||
|
|
|
||
|
|
|
||
|
|
final bool reserveLogoArea;
|
||
|
|
|
||
|
|
const BannerCarouselScaffold({
|
||
|
|
super.key,
|
||
|
|
required this.controller,
|
||
|
|
required this.slides,
|
||
|
|
required this.onSlideTap,
|
||
|
|
this.reserveLogoArea = false,
|
||
|
|
});
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
if (slides.isEmpty) return const SizedBox.shrink();
|
||
|
|
|
||
|
|
final compact = Breakpoints.isCompact(MediaQuery.sizeOf(context).width);
|
||
|
|
if (compact) return _buildCompact(context);
|
||
|
|
return _buildDesktop(context);
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
Widget _contentSwitcher({required bool compact}) {
|
||
|
|
return IgnorePointer(
|
||
|
|
child: AnimatedSwitcher(
|
||
|
|
duration: const Duration(milliseconds: 300),
|
||
|
|
switchInCurve: Curves.easeOut,
|
||
|
|
switchOutCurve: Curves.easeOut,
|
||
|
|
layoutBuilder: (currentChild, previousChildren) => Stack(
|
||
|
|
fit: StackFit.expand,
|
||
|
|
children: [
|
||
|
|
...previousChildren,
|
||
|
|
if (currentChild != null) currentChild,
|
||
|
|
],
|
||
|
|
),
|
||
|
|
child: _BannerSlideContent(
|
||
|
|
key: ValueKey(controller.currentIndex),
|
||
|
|
data: slides[controller.currentIndex],
|
||
|
|
compact: compact,
|
||
|
|
reserveLogoArea: reserveLogoArea,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _pageIndicator() {
|
||
|
|
return FrostedPanel(
|
||
|
|
radius: 999,
|
||
|
|
enableBlur: false,
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||
|
|
color: Colors.black.withValues(alpha: 0.24),
|
||
|
|
border: Border.all(color: Colors.white.withValues(alpha: 0.12)),
|
||
|
|
child: Row(
|
||
|
|
mainAxisSize: MainAxisSize.min,
|
||
|
|
children: [
|
||
|
|
for (var index = 0; index < slides.length; index++)
|
||
|
|
GestureDetector(
|
||
|
|
behavior: HitTestBehavior.opaque,
|
||
|
|
onTap: () => controller.goToPage(index),
|
||
|
|
child: AnimatedContainer(
|
||
|
|
duration: const Duration(milliseconds: 240),
|
||
|
|
curve: Curves.easeOut,
|
||
|
|
width: controller.currentIndex == index ? 22 : 7,
|
||
|
|
height: 7,
|
||
|
|
margin: EdgeInsets.only(
|
||
|
|
right: index == slides.length - 1 ? 0 : 6,
|
||
|
|
),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: controller.currentIndex == index
|
||
|
|
? Colors.white
|
||
|
|
: const Color(0x66FFFFFF),
|
||
|
|
borderRadius: BorderRadius.circular(999),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _pageView({required bool compact}) {
|
||
|
|
return PageView.builder(
|
||
|
|
controller: controller.pageController,
|
||
|
|
physics: compact ? null : const NeverScrollableScrollPhysics(),
|
||
|
|
itemCount: slides.length,
|
||
|
|
onPageChanged: controller.onPageChanged,
|
||
|
|
itemBuilder: (context, index) {
|
||
|
|
return _BannerSlide(data: slides[index], compact: compact);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildCompact(BuildContext context) {
|
||
|
|
final screenHeight = MediaQuery.sizeOf(context).height;
|
||
|
|
final height = screenHeight * 0.55;
|
||
|
|
final screenWidth = MediaQuery.sizeOf(context).width;
|
||
|
|
controller.lastBannerWidth = screenWidth.isFinite && screenWidth > 0
|
||
|
|
? screenWidth
|
||
|
|
: null;
|
||
|
|
|
||
|
|
return SizedBox(
|
||
|
|
height: height,
|
||
|
|
child: GestureDetector(
|
||
|
|
behavior: HitTestBehavior.opaque,
|
||
|
|
onTap: () => onSlideTap(controller.currentIndex),
|
||
|
|
child: Stack(
|
||
|
|
fit: StackFit.expand,
|
||
|
|
children: [
|
||
|
|
_pageView(compact: true),
|
||
|
|
_contentSwitcher(compact: true),
|
||
|
|
Positioned(
|
||
|
|
left: 0,
|
||
|
|
right: 0,
|
||
|
|
bottom: 20,
|
||
|
|
child: Center(child: _pageIndicator()),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildDesktop(BuildContext context) {
|
||
|
|
final tokens = context.appTokens;
|
||
|
|
|
||
|
|
return Padding(
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||
|
|
child: LayoutBuilder(
|
||
|
|
builder: (context, constraints) {
|
||
|
|
final width = constraints.maxWidth;
|
||
|
|
controller.lastBannerWidth = width.isFinite && width > 0
|
||
|
|
? width
|
||
|
|
: null;
|
||
|
|
final height = (width * 7 / 16).clamp(260.0, 500.0);
|
||
|
|
return SizedBox(
|
||
|
|
height: height,
|
||
|
|
child: ClipRRect(
|
||
|
|
borderRadius: BorderRadius.circular(tokens.panelRadius + 6),
|
||
|
|
child: MouseRegion(
|
||
|
|
cursor: SystemMouseCursors.click,
|
||
|
|
onEnter: (_) => controller.setHovering(true),
|
||
|
|
onExit: (_) => controller.setHovering(false),
|
||
|
|
child: Stack(
|
||
|
|
fit: StackFit.expand,
|
||
|
|
children: [
|
||
|
|
_BlurredBackdropLayer(
|
||
|
|
data: slides[controller.currentIndex],
|
||
|
|
animateSwitch: slides.length > 1,
|
||
|
|
),
|
||
|
|
GestureDetector(
|
||
|
|
behavior: HitTestBehavior.opaque,
|
||
|
|
onTap: () => onSlideTap(controller.currentIndex),
|
||
|
|
child: _pageView(compact: false),
|
||
|
|
),
|
||
|
|
_contentSwitcher(compact: false),
|
||
|
|
Positioned(right: 22, bottom: 20, child: _pageIndicator()),
|
||
|
|
if (controller.hovering && slides.length > 1) ...[
|
||
|
|
Positioned(
|
||
|
|
left: 14,
|
||
|
|
top: 0,
|
||
|
|
bottom: 0,
|
||
|
|
child: Center(
|
||
|
|
child: CircleNavIconButton(
|
||
|
|
icon: Icons.chevron_left_rounded,
|
||
|
|
onPressed: controller.prevPage,
|
||
|
|
iconSize: 28,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
Positioned(
|
||
|
|
right: 14,
|
||
|
|
top: 0,
|
||
|
|
bottom: 0,
|
||
|
|
child: Center(
|
||
|
|
child: CircleNavIconButton(
|
||
|
|
icon: Icons.chevron_right_rounded,
|
||
|
|
onPressed: controller.nextPage,
|
||
|
|
iconSize: 28,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
class _BlurredBackdropLayer extends StatelessWidget {
|
||
|
|
final BannerSlideData data;
|
||
|
|
final bool animateSwitch;
|
||
|
|
|
||
|
|
const _BlurredBackdropLayer({
|
||
|
|
required this.data,
|
||
|
|
required this.animateSwitch,
|
||
|
|
});
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
final image = _BlurredBackdropImage(
|
||
|
|
key: ValueKey<String>(data.imageUrl ?? '__none__'),
|
||
|
|
data: data,
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!animateSwitch) return image;
|
||
|
|
|
||
|
|
return AnimatedSwitcher(
|
||
|
|
duration: const Duration(milliseconds: 200),
|
||
|
|
switchInCurve: Curves.easeOut,
|
||
|
|
switchOutCurve: Curves.easeOut,
|
||
|
|
layoutBuilder: (currentChild, previousChildren) {
|
||
|
|
return Stack(
|
||
|
|
fit: StackFit.expand,
|
||
|
|
children: [
|
||
|
|
...previousChildren,
|
||
|
|
if (currentChild != null) currentChild,
|
||
|
|
],
|
||
|
|
);
|
||
|
|
},
|
||
|
|
child: image,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class _BlurredBackdropImage extends StatelessWidget {
|
||
|
|
final BannerSlideData data;
|
||
|
|
|
||
|
|
const _BlurredBackdropImage({super.key, required this.data});
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
return ClipRect(
|
||
|
|
child: Stack(
|
||
|
|
fit: StackFit.expand,
|
||
|
|
children: [
|
||
|
|
ImageFiltered(
|
||
|
|
imageFilter: ImageFilter.blur(
|
||
|
|
sigmaX: 36,
|
||
|
|
sigmaY: 36,
|
||
|
|
tileMode: TileMode.clamp,
|
||
|
|
),
|
||
|
|
child: SmartImage(
|
||
|
|
url: data.imageUrl,
|
||
|
|
fallbackUrls: [
|
||
|
|
if (data.fallbackImageUrl != null) data.fallbackImageUrl!,
|
||
|
|
],
|
||
|
|
httpHeaders: data.imageHeaders,
|
||
|
|
borderRadius: 0,
|
||
|
|
fit: BoxFit.cover,
|
||
|
|
memCacheWidth: 800,
|
||
|
|
fallbackIcon: null,
|
||
|
|
placeholder: const ColoredBox(color: Colors.black),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
IgnorePointer(
|
||
|
|
child: ColoredBox(color: Colors.black.withValues(alpha: 0.30)),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
class _BannerSlide extends StatelessWidget {
|
||
|
|
final BannerSlideData data;
|
||
|
|
final bool compact;
|
||
|
|
|
||
|
|
const _BannerSlide({required this.data, this.compact = false});
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
if (compact) {
|
||
|
|
return Stack(
|
||
|
|
fit: StackFit.expand,
|
||
|
|
children: [
|
||
|
|
SmartImage(
|
||
|
|
url: data.imageUrl,
|
||
|
|
fallbackUrls: [
|
||
|
|
if (data.fallbackImageUrl != null) data.fallbackImageUrl!,
|
||
|
|
],
|
||
|
|
borderRadius: 0,
|
||
|
|
fit: BoxFit.cover,
|
||
|
|
httpHeaders: data.imageHeaders,
|
||
|
|
fallbackIcon: Icons.movie_outlined,
|
||
|
|
placeholder: const SizedBox.expand(),
|
||
|
|
),
|
||
|
|
DecoratedBox(
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
gradient: LinearGradient(
|
||
|
|
begin: Alignment.topCenter,
|
||
|
|
end: Alignment.bottomCenter,
|
||
|
|
stops: const [0.0, 0.12, 0.4, 0.75, 1.0],
|
||
|
|
colors: [
|
||
|
|
Colors.black.withValues(alpha: 0.35),
|
||
|
|
Colors.black.withValues(alpha: 0.12),
|
||
|
|
Colors.black.withValues(alpha: 0.0),
|
||
|
|
Colors.black.withValues(alpha: 0.35),
|
||
|
|
Colors.black.withValues(alpha: 0.70),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return Stack(
|
||
|
|
fit: StackFit.expand,
|
||
|
|
children: [
|
||
|
|
ImageFiltered(
|
||
|
|
imageFilter: ImageFilter.blur(
|
||
|
|
sigmaX: 36,
|
||
|
|
sigmaY: 36,
|
||
|
|
tileMode: TileMode.mirror,
|
||
|
|
),
|
||
|
|
child: SmartImage(
|
||
|
|
url: data.imageUrl,
|
||
|
|
fallbackUrls: [
|
||
|
|
if (data.fallbackImageUrl != null) data.fallbackImageUrl!,
|
||
|
|
],
|
||
|
|
borderRadius: 0,
|
||
|
|
fit: BoxFit.cover,
|
||
|
|
httpHeaders: data.imageHeaders,
|
||
|
|
fallbackIcon: null,
|
||
|
|
placeholder: const SizedBox.expand(),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
DecoratedBox(
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: Colors.black.withValues(alpha: 0.32),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
SmartImage(
|
||
|
|
url: data.imageUrl,
|
||
|
|
fallbackUrls: [
|
||
|
|
if (data.fallbackImageUrl != null) data.fallbackImageUrl!,
|
||
|
|
],
|
||
|
|
borderRadius: 0,
|
||
|
|
fit: BoxFit.contain,
|
||
|
|
httpHeaders: data.imageHeaders,
|
||
|
|
fallbackIcon: Icons.movie_outlined,
|
||
|
|
placeholder: const SizedBox.expand(),
|
||
|
|
),
|
||
|
|
DecoratedBox(
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
gradient: LinearGradient(
|
||
|
|
begin: Alignment.topCenter,
|
||
|
|
end: Alignment.bottomCenter,
|
||
|
|
stops: const [0.0, 0.35, 0.6, 1.0],
|
||
|
|
colors: [
|
||
|
|
Colors.black.withValues(alpha: 0.0),
|
||
|
|
Colors.black.withValues(alpha: 0.03),
|
||
|
|
Colors.black.withValues(alpha: 0.25),
|
||
|
|
Colors.black.withValues(alpha: 0.55),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
class _BannerSlideContent extends StatelessWidget {
|
||
|
|
final BannerSlideData data;
|
||
|
|
final bool compact;
|
||
|
|
final bool reserveLogoArea;
|
||
|
|
|
||
|
|
const _BannerSlideContent({
|
||
|
|
super.key,
|
||
|
|
required this.data,
|
||
|
|
required this.compact,
|
||
|
|
required this.reserveLogoArea,
|
||
|
|
});
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
final theme = Theme.of(context);
|
||
|
|
final tokens = context.appTokens;
|
||
|
|
final ratingColor = tokens.ratingColor;
|
||
|
|
|
||
|
|
final hasMetaInfo =
|
||
|
|
data.rating != null ||
|
||
|
|
data.primaryMetaLabels.isNotEmpty ||
|
||
|
|
data.secondaryMetaLabels.isNotEmpty;
|
||
|
|
|
||
|
|
final titleFontSize = compact ? 24.0 : 32.0;
|
||
|
|
final titleText = Text(
|
||
|
|
data.title,
|
||
|
|
maxLines: 2,
|
||
|
|
overflow: TextOverflow.ellipsis,
|
||
|
|
style: theme.textTheme.titleLarge?.copyWith(
|
||
|
|
color: Colors.white,
|
||
|
|
fontSize: titleFontSize,
|
||
|
|
height: 1.0,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
final padding = compact
|
||
|
|
? const EdgeInsets.only(left: 24, bottom: 64, right: 24)
|
||
|
|
: const EdgeInsets.only(left: 32, bottom: 32);
|
||
|
|
|
||
|
|
return Align(
|
||
|
|
alignment: Alignment.bottomLeft,
|
||
|
|
child: Padding(
|
||
|
|
padding: padding,
|
||
|
|
child: ConstrainedBox(
|
||
|
|
constraints: const BoxConstraints(maxWidth: 480),
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
mainAxisSize: MainAxisSize.min,
|
||
|
|
children: [
|
||
|
|
if (reserveLogoArea) _buildLogoArea(titleText) else titleText,
|
||
|
|
if (hasMetaInfo) ...[
|
||
|
|
const SizedBox(height: 10),
|
||
|
|
_MetaRow(
|
||
|
|
rating: data.rating,
|
||
|
|
ratingColor: ratingColor,
|
||
|
|
primaryLabels: data.primaryMetaLabels,
|
||
|
|
secondaryLabels: data.secondaryMetaLabels,
|
||
|
|
),
|
||
|
|
],
|
||
|
|
if ((data.overview ?? '').trim().isNotEmpty) ...[
|
||
|
|
const SizedBox(height: 12),
|
||
|
|
Text(
|
||
|
|
data.overview!,
|
||
|
|
maxLines: 2,
|
||
|
|
overflow: TextOverflow.ellipsis,
|
||
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||
|
|
color: Colors.white.withValues(alpha: 0.78),
|
||
|
|
height: 1.5,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
Widget _buildLogoArea(Widget titleText) {
|
||
|
|
final logoHeight = compact ? 56.0 : 72.0;
|
||
|
|
final logoMaxWidth = compact ? 240.0 : 320.0;
|
||
|
|
|
||
|
|
return ConstrainedBox(
|
||
|
|
constraints: BoxConstraints(maxWidth: logoMaxWidth),
|
||
|
|
child: SizedBox(
|
||
|
|
width: double.infinity,
|
||
|
|
height: logoHeight,
|
||
|
|
child: data.logoUrl != null
|
||
|
|
? CachedNetworkImage(
|
||
|
|
imageUrl: data.logoUrl!,
|
||
|
|
httpHeaders: data.imageHeaders,
|
||
|
|
fit: BoxFit.contain,
|
||
|
|
alignment: Alignment.bottomLeft,
|
||
|
|
memCacheWidth: 960,
|
||
|
|
fadeInDuration: const Duration(milliseconds: 300),
|
||
|
|
placeholderFadeInDuration: Duration.zero,
|
||
|
|
fadeOutDuration: const Duration(milliseconds: 300),
|
||
|
|
placeholder: (_, _) => const SizedBox.shrink(),
|
||
|
|
errorWidget: (_, _, _) =>
|
||
|
|
Align(alignment: Alignment.bottomLeft, child: titleText),
|
||
|
|
)
|
||
|
|
: Align(alignment: Alignment.bottomLeft, child: titleText),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class _MetaRow extends StatelessWidget {
|
||
|
|
final String? rating;
|
||
|
|
final Color ratingColor;
|
||
|
|
final List<String> primaryLabels;
|
||
|
|
final List<String> secondaryLabels;
|
||
|
|
|
||
|
|
const _MetaRow({
|
||
|
|
required this.rating,
|
||
|
|
required this.ratingColor,
|
||
|
|
required this.primaryLabels,
|
||
|
|
required this.secondaryLabels,
|
||
|
|
});
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
final style = Theme.of(context).textTheme.bodySmall;
|
||
|
|
final children = <Widget>[];
|
||
|
|
|
||
|
|
void addSep() {
|
||
|
|
children.add(
|
||
|
|
Padding(
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||
|
|
child: Text(
|
||
|
|
'·',
|
||
|
|
style: style?.copyWith(color: Colors.white.withValues(alpha: 0.38)),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (rating != null) {
|
||
|
|
children.add(
|
||
|
|
Row(
|
||
|
|
mainAxisSize: MainAxisSize.min,
|
||
|
|
children: [
|
||
|
|
Icon(Icons.star_rounded, size: 13, color: ratingColor),
|
||
|
|
const SizedBox(width: 3),
|
||
|
|
Text(
|
||
|
|
rating!,
|
||
|
|
style: style?.copyWith(
|
||
|
|
color: Colors.white,
|
||
|
|
fontWeight: FontWeight.w700,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
for (final label in primaryLabels) {
|
||
|
|
if (children.isNotEmpty) addSep();
|
||
|
|
children.add(
|
||
|
|
Text(
|
||
|
|
label,
|
||
|
|
style: style?.copyWith(color: Colors.white.withValues(alpha: 0.80)),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
for (final label in secondaryLabels) {
|
||
|
|
if (children.isNotEmpty) addSep();
|
||
|
|
children.add(
|
||
|
|
Text(
|
||
|
|
label,
|
||
|
|
style: style?.copyWith(color: Colors.white.withValues(alpha: 0.62)),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return Row(mainAxisSize: MainAxisSize.min, children: children);
|
||
|
|
}
|
||
|
|
}
|