feat(ui): 更新品牌启动页与选集交互

This commit is contained in:
Zhou Weidong committed 2026-10-19 09:28:16 +08:00
1 parent 4778ce2c75
commit c1cb7d1c11
87 files changed
+919 -552

No files matched your search

@@ -18,6 +18,9 @@ class CatalogController extends ChangeNotifier {
HomeFeed feed = const HomeFeed();
bool loading = false;
/// 第一次首页请求结束后为 true。之后的重试不再回到开屏。
bool settled = false;
String? error;
List<Drama> get dramas => feed.dramas;
@@ -36,6 +39,7 @@ class CatalogController extends ChangeNotifier {
error = 'catalogError';
} finally {
loading = false;
settled = true;
notifyListeners();
}
}
+40 -15
View File
@@ -1,24 +1,49 @@
// Author: fruit.com.cn
import 'package:flutter/material.dart';
/// Shared colors. Blue is the primary action color.
/// Evening palette for the Indonesia launch.
///
/// Chili is the action color. Gold marks coins, rewards, and playback
/// progress. Cocoa surfaces keep the app in night-market light instead of
/// a cold utility screen.
abstract final class AppColors {
/// Klein Blue primary used by actions and selected states.
static const blue = Color(0xFF002FA7);
/// Chili fill for buttons, selected chips, and brand marks.
static const blue = Color(0xFFC4322A);
/// Pressed/active shade for blue surfaces.
static const bluePressed = Color(0xFF00227A);
/// Pressed chili.
static const bluePressed = Color(0xFF8E221C);
/// High-contrast blue for text and icons on dark surfaces.
static const blueSoft = Color(0xFF5B7BE0);
/// Coral for text and icons sitting on dark surfaces.
static const blueSoft = Color(0xFFFF8A72);
/// Coins, rewards, and the playback progress line.
static const gold = Color(0xFFF6C453);
/// Text placed on [gold].
static const onGold = Color(0xFF2A1810);
/// Text and icons placed directly on [blue].
static const onPrimary = Color(0xFFFFFFFF);
static const ink = Color(0xFF0B0C10);
static const bar = Color(0xFF101114);
static const card = Color(0xFF17191F);
static const muted = Color(0xFFB7BDC9);
static const outlineDark = Color(0xFF3A3C45);
static const outlineVariantDark = Color(0xFF242329);
static const outlineLight = Color(0xFF8A8E99);
static const outlineVariantLight = Color(0xFFD6D9E2);
/// Page background.
static const ink = Color(0xFF100C0A);
/// Bottom bar.
static const bar = Color(0xFF1A1412);
/// Cards lifted off [ink].
static const card = Color(0xFF241C19);
/// Chips and nested rows.
static const raised = Color(0xFF322824);
/// Primary text on dark surfaces.
static const cream = Color(0xFFF8F1EA);
static const muted = Color(0xFFB7A59C);
static const outlineDark = Color(0xFF4A3C36);
static const outlineVariantDark = Color(0xFF2A221F);
static const outlineLight = Color(0xFF8C7A72);
static const outlineVariantLight = Color(0xFFE4D5CC);
}
+4 -4
View File
@@ -4,10 +4,10 @@ import 'package:flutter/material.dart';
import 'app_colors.dart';
/// App-wide light and dark themes built from the approved Klein Blue palette.
/// Light paper and warm-cocoa themes for the Indonesia evening palette.
abstract final class AppTheme {
static const _lightBackground = Color(0xFFF8F7FC);
static const _darkBackground = Color(0xFF101019);
static const _lightBackground = Color(0xFFFFF6F1);
static const _darkBackground = AppColors.ink;
/// Light theme for system light mode.
static ThemeData light() => _build(
@@ -40,7 +40,7 @@ abstract final class AppTheme {
secondaryContainer: AppColors.bluePressed,
onSecondaryContainer: AppColors.onPrimary,
surface: _darkBackground,
onSurface: Colors.white,
onSurface: AppColors.cream,
outline: AppColors.outlineDark,
outlineVariant: AppColors.outlineVariantDark,
surfaceTint: AppColors.blueSoft,
+109 -98
View File
@@ -4,19 +4,17 @@ import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
const double _regularArtSize = 84;
const double _compactArtSize = 60;
const double _regularInnerSize = 56;
const double _compactInnerSize = 40;
const double _regularIconSize = 27;
const double _compactIconSize = 21;
const double _regularIconSize = 48;
const double _compactIconSize = 32;
const double _maxContentWidth = 260;
const double _regularHorizontal = 24;
const double _regularVertical = 28;
const double _compactPad = 16;
const double _regularGap = 20;
const double _compactGap = 12;
const double _actionMinHeight = 44;
const double _outerArtOpacity = 0.055;
const double _outerBorderOpacity = 0.11;
const double _innerArtOpacity = 0.11;
/// 统一空列表占位:柔和图标、标题、可选说明与操作。
/// 统一空列表占位:一枚图标、标题、可选说明与操作。
class EmptyState extends StatelessWidget {
const EmptyState({
super.key,
@@ -24,6 +22,7 @@ class EmptyState extends StatelessWidget {
required this.title,
this.subtitle,
this.actionLabel,
this.actionKey,
this.onAction,
this.compact = false,
});
@@ -32,17 +31,12 @@ class EmptyState extends StatelessWidget {
final String title;
final String? subtitle;
final String? actionLabel;
final Key? actionKey;
final VoidCallback? onAction;
final bool compact;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final dark = Theme.of(context).brightness == Brightness.dark;
final iconColor = dark ? AppColors.blueSoft : AppColors.blue;
final artSize = compact ? _compactArtSize : _regularArtSize;
final innerSize = compact ? _compactInnerSize : _regularInnerSize;
final iconSize = compact ? _compactIconSize : _regularIconSize;
final hasAction = actionLabel != null && onAction != null;
return Center(
child: ConstrainedBox(
@@ -51,8 +45,8 @@ class EmptyState extends StatelessWidget {
child: Padding(
key: const Key('empty-state-padding'),
padding: EdgeInsets.symmetric(
horizontal: compact ? 16 : 24,
vertical: compact ? 16 : 28,
horizontal: compact ? _compactPad : _regularHorizontal,
vertical: compact ? _compactPad : _regularVertical,
),
child: Semantics(
container: true,
@@ -61,88 +55,16 @@ class EmptyState extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
ExcludeSemantics(
child: Container(
key: const Key('empty-state-art'),
width: artSize,
height: artSize,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.blue.withValues(alpha: _outerArtOpacity),
border: Border.all(
color: AppColors.blue.withValues(
alpha: _outerBorderOpacity,
),
),
),
child: Container(
key: const Key('empty-state-art-inner'),
width: innerSize,
height: innerSize,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.blue.withValues(
alpha: _innerArtOpacity,
),
),
child: Icon(
icon,
key: const Key('empty-state-icon'),
size: iconSize,
color: iconColor,
),
),
),
child: _EmptyMark(icon: icon, compact: compact),
),
SizedBox(height: compact ? 10 : 14),
ExcludeSemantics(
child: Text(
title,
key: const Key('empty-state-title'),
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
color: scheme.onSurface,
fontSize: 15,
fontWeight: FontWeight.w600,
height: 1.35,
),
),
SizedBox(height: compact ? _compactGap : _regularGap),
_EmptyCopy(
title: title,
subtitle: subtitle,
actionLabel: hasAction ? actionLabel : null,
actionKey: actionKey,
onAction: hasAction ? onAction : null,
),
if (subtitle != null) ...[
const SizedBox(height: 6),
ExcludeSemantics(
child: Text(
subtitle!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
color: AppColors.muted,
fontSize: 12.5,
height: 1.45,
),
),
),
],
if (hasAction) ...[
const SizedBox(height: 8),
TextButton(
key: const Key('empty-state-action'),
onPressed: onAction,
style: TextButton.styleFrom(
foregroundColor: scheme.primary,
minimumSize: const Size(44, _actionMinHeight),
padding: const EdgeInsets.symmetric(horizontal: 14),
),
child: Text(
actionLabel!,
style: const TextStyle(fontSize: 13),
),
),
],
],
),
),
@@ -151,3 +73,92 @@ class EmptyState extends StatelessWidget {
);
}
}
/// 空状态只留一枚图标,不加水印底和描边。
class _EmptyMark extends StatelessWidget {
const _EmptyMark({required this.icon, required this.compact});
final IconData icon;
final bool compact;
@override
Widget build(BuildContext context) {
final dark = Theme.of(context).brightness == Brightness.dark;
return Icon(
icon,
key: const Key('empty-state-icon'),
size: compact ? _compactIconSize : _regularIconSize,
color: dark ? AppColors.gold : AppColors.blue,
);
}
}
class _EmptyCopy extends StatelessWidget {
const _EmptyCopy({
required this.title,
required this.subtitle,
required this.actionLabel,
required this.actionKey,
required this.onAction,
});
final String title;
final String? subtitle;
final String? actionLabel;
final Key? actionKey;
final VoidCallback? onAction;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
ExcludeSemantics(
child: Text(
title,
key: const Key('empty-state-title'),
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
color: scheme.onSurface,
fontSize: 15,
fontWeight: FontWeight.w600,
height: 1.35,
),
),
),
if (subtitle != null) ...[
const SizedBox(height: 6),
ExcludeSemantics(
child: Text(
subtitle!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
color: AppColors.muted,
fontSize: 12.5,
height: 1.45,
),
),
),
],
if (actionLabel != null && onAction != null) ...[
const SizedBox(height: 8),
TextButton(
key: actionKey ?? const Key('empty-state-action'),
onPressed: onAction,
style: TextButton.styleFrom(
foregroundColor: scheme.primary,
minimumSize: const Size(44, _actionMinHeight),
padding: const EdgeInsets.symmetric(horizontal: 14),
),
child: Text(actionLabel!, style: const TextStyle(fontSize: 13)),
),
],
],
);
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ class LibraryDramaTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
color: const Color(0xFF1B1C21),
color: AppColors.card,
borderRadius: BorderRadius.circular(12),
clipBehavior: Clip.antiAlias,
child: InkWell(