优化:生成图片页面
This commit is contained in:
parent
20d43b4ae4
commit
846dd5e9f5
@ -6,6 +6,10 @@ abstract final class AppColors {
|
||||
static const Color primary = Color(0xFF8B5CF6);
|
||||
static const Color primaryLight = Color(0x338B5CF6); // #8B5CF620
|
||||
static const Color primaryShadow = Color(0x338B5CF6); // #8B5CF620 for shadow
|
||||
/// 底栏选中、分辨率 chip 等:透出底层,字仍用 [surface] 可读
|
||||
static Color get primaryGlass => primary.withValues(alpha: 0.58);
|
||||
/// 生成页主按钮:略不透明,保证主操作仍醒目
|
||||
static Color get primaryGlassEmphasis => primary.withValues(alpha: 0.72);
|
||||
|
||||
// Neutrals
|
||||
static const Color background = Color(0xFFFAFAFA);
|
||||
|
||||
@ -215,44 +215,66 @@ class _GenerateVideoScreenState extends State<GenerateVideoScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final topInset = MediaQuery.paddingOf(context).top;
|
||||
final creditsDisplay =
|
||||
UserCreditsData.of(context)?.creditsDisplay ?? '--';
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
extendBodyBehindAppBar: true,
|
||||
backgroundColor: Colors.black,
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(56),
|
||||
preferredSize: Size.fromHeight(topInset + 56),
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(top: topInset),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withValues(alpha: 0.72),
|
||||
Colors.black.withValues(alpha: 0.35),
|
||||
Colors.black.withValues(alpha: 0.0),
|
||||
],
|
||||
stops: const [0.0, 0.55, 1.0],
|
||||
),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 56,
|
||||
child: TopNavBar(
|
||||
title: 'Generate',
|
||||
credits: creditsDisplay,
|
||||
onCreditsTap: () => Navigator.of(context).pushNamed('/recharge'),
|
||||
showBackButton: true,
|
||||
onBack: () => Navigator.of(context).pop(),
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: AppColors.surface,
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
),
|
||||
),
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppSpacing.screenPaddingLarge,
|
||||
AppSpacing.screenPaddingLarge,
|
||||
AppSpacing.screenPaddingLarge,
|
||||
AppSpacing.lg,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_CreditsCard(
|
||||
credits:
|
||||
UserCreditsData.of(context)?.creditsDisplay ?? '--',
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xxl),
|
||||
_VideoPreviewArea(
|
||||
_GenerateFullScreenBackground(
|
||||
videoUrl: widget.task?.previewVideoUrl,
|
||||
imageUrl: widget.task?.previewImageUrl,
|
||||
),
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withValues(alpha: 0.12),
|
||||
Colors.black.withValues(alpha: 0.55),
|
||||
],
|
||||
stops: const [0.45, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppSpacing.screenPaddingLarge,
|
||||
@ -261,13 +283,16 @@ class _GenerateVideoScreenState extends State<GenerateVideoScreen> {
|
||||
AppSpacing.screenPaddingLarge,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Spacer(),
|
||||
if (_hasVideo)
|
||||
_ResolutionToggle(
|
||||
Center(
|
||||
child: _ResolutionToggle(
|
||||
selected: _selectedResolution,
|
||||
onChanged: (r) => setState(() => _selectedResolution = r),
|
||||
onChanged: (r) =>
|
||||
setState(() => _selectedResolution = r),
|
||||
),
|
||||
),
|
||||
if (_hasVideo) const SizedBox(height: AppSpacing.xxl),
|
||||
_GenerateButton(
|
||||
@ -285,65 +310,9 @@ class _GenerateVideoScreenState extends State<GenerateVideoScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
class _CreditsCard extends StatelessWidget {
|
||||
const _CreditsCard({required this.credits});
|
||||
|
||||
final String credits;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.xxl,
|
||||
vertical: AppSpacing.xl,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: AppColors.primary.withValues(alpha: 0.5),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.primaryShadow.withValues(alpha: 0.25),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(LucideIcons.sparkles, size: 28, color: AppColors.surface),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Available Credits',
|
||||
style: AppTypography.bodyRegular.copyWith(
|
||||
color: AppColors.surface.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
credits,
|
||||
style: AppTypography.bodyLarge.copyWith(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.surface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Video preview area - video URL from card click. Auto-load and play on init.
|
||||
/// Video fit: contain (no crop). Loading animation until ready.
|
||||
class _VideoPreviewArea extends StatefulWidget {
|
||||
const _VideoPreviewArea({
|
||||
/// 列表传入的预览图/视频:全屏背景层,超出视口 [BoxFit.cover] 裁切。
|
||||
class _GenerateFullScreenBackground extends StatefulWidget {
|
||||
const _GenerateFullScreenBackground({
|
||||
this.videoUrl,
|
||||
this.imageUrl,
|
||||
});
|
||||
@ -352,10 +321,12 @@ class _VideoPreviewArea extends StatefulWidget {
|
||||
final String? imageUrl;
|
||||
|
||||
@override
|
||||
State<_VideoPreviewArea> createState() => _VideoPreviewAreaState();
|
||||
State<_GenerateFullScreenBackground> createState() =>
|
||||
_GenerateFullScreenBackgroundState();
|
||||
}
|
||||
|
||||
class _VideoPreviewAreaState extends State<_VideoPreviewArea> {
|
||||
class _GenerateFullScreenBackgroundState
|
||||
extends State<_GenerateFullScreenBackground> {
|
||||
VideoPlayerController? _controller;
|
||||
|
||||
@override
|
||||
@ -374,7 +345,7 @@ class _VideoPreviewAreaState extends State<_VideoPreviewArea> {
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _VideoPreviewArea oldWidget) {
|
||||
void didUpdateWidget(covariant _GenerateFullScreenBackground oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.videoUrl != widget.videoUrl) {
|
||||
_controller?.dispose();
|
||||
@ -394,11 +365,15 @@ class _VideoPreviewAreaState extends State<_VideoPreviewArea> {
|
||||
try {
|
||||
final file = await DefaultCacheManager().getSingleFile(url);
|
||||
if (!mounted) return;
|
||||
final controller = VideoPlayerController.file(file);
|
||||
final controller = VideoPlayerController.file(
|
||||
file,
|
||||
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
|
||||
);
|
||||
await controller.initialize();
|
||||
if (!mounted) return;
|
||||
await controller.setVolume(1.0);
|
||||
await controller.setLooping(true);
|
||||
await controller.play();
|
||||
controller.setLooping(true);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_controller = controller;
|
||||
@ -416,206 +391,85 @@ class _VideoPreviewAreaState extends State<_VideoPreviewArea> {
|
||||
final hasVideo = widget.videoUrl != null && widget.videoUrl!.isNotEmpty;
|
||||
final hasImage = widget.imageUrl != null && widget.imageUrl!.isNotEmpty;
|
||||
|
||||
// 图片模式:宽度=组件宽度,高度按图片宽高比自适应
|
||||
if (!hasVideo && hasImage) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Container(
|
||||
width: constraints.maxWidth,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceAlt,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: AppColors.border,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: _AspectRatioImage(
|
||||
imageUrl: widget.imageUrl!,
|
||||
maxWidth: constraints.maxWidth,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 视频模式:aspect ratio 来自视频或 16:9 占位
|
||||
final aspectRatio = isReady &&
|
||||
_controller!.value.size.width > 0 &&
|
||||
_controller!.value.size.height > 0
|
||||
? _controller!.value.size.width / _controller!.value.size.height
|
||||
: 16 / 9;
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width = constraints.maxWidth;
|
||||
final height = width / aspectRatio;
|
||||
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceAlt,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: AppColors.border,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Stack(
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
if (isReady)
|
||||
SizedBox.expand(
|
||||
if (!hasVideo && hasImage)
|
||||
Positioned.fill(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: widget.imageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
placeholder: (_, __) => const _BgLoadingPlaceholder(),
|
||||
errorWidget: (_, __, ___) => const _BgErrorPlaceholder(),
|
||||
),
|
||||
)
|
||||
else if (hasVideo && isReady)
|
||||
Positioned.fill(
|
||||
child: ClipRect(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
fit: BoxFit.cover,
|
||||
child: SizedBox(
|
||||
width: _controller!.value.size.width,
|
||||
height: _controller!.value.size.height,
|
||||
child: VideoPlayer(_controller!),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (hasImage)
|
||||
CachedNetworkImage(
|
||||
else if (hasVideo && hasImage)
|
||||
Positioned.fill(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: widget.imageUrl!,
|
||||
fit: BoxFit.contain,
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
placeholder: (_, __) =>
|
||||
const _LoadingOverlay(isLoading: true),
|
||||
errorWidget: (_, __, ___) =>
|
||||
const _LoadingOverlay(isLoading: false),
|
||||
placeholder: (_, __) => const _BgLoadingPlaceholder(),
|
||||
errorWidget: (_, __, ___) => const _BgErrorPlaceholder(),
|
||||
),
|
||||
)
|
||||
else if (hasVideo)
|
||||
const Positioned.fill(child: _BgLoadingPlaceholder())
|
||||
else
|
||||
const _LoadingOverlay(isLoading: false),
|
||||
if (hasVideo && !isReady)
|
||||
const Positioned.fill(
|
||||
child: _LoadingOverlay(isLoading: true),
|
||||
),
|
||||
const Positioned.fill(child: _BgErrorPlaceholder()),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 图片展示:宽度=组件宽度,高度按图片宽高比自适应
|
||||
class _AspectRatioImage extends StatefulWidget {
|
||||
const _AspectRatioImage({
|
||||
required this.imageUrl,
|
||||
required this.maxWidth,
|
||||
});
|
||||
|
||||
final String imageUrl;
|
||||
final double maxWidth;
|
||||
|
||||
@override
|
||||
State<_AspectRatioImage> createState() => _AspectRatioImageState();
|
||||
}
|
||||
|
||||
class _AspectRatioImageState extends State<_AspectRatioImage> {
|
||||
double? _aspectRatio;
|
||||
ImageStream? _stream;
|
||||
late ImageStreamListener _listener;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_listener = ImageStreamListener(_onImageLoaded, onError: _onImageError);
|
||||
_resolveImage();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _AspectRatioImage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.imageUrl != widget.imageUrl) {
|
||||
_stream?.removeListener(_listener);
|
||||
_aspectRatio = null;
|
||||
_resolveImage();
|
||||
}
|
||||
}
|
||||
|
||||
void _resolveImage() {
|
||||
final provider = CachedNetworkImageProvider(widget.imageUrl);
|
||||
_stream = provider.resolve(const ImageConfiguration());
|
||||
_stream!.addListener(_listener);
|
||||
}
|
||||
|
||||
void _onImageLoaded(ImageInfo info, bool sync) {
|
||||
if (!mounted) return;
|
||||
final w = info.image.width.toDouble();
|
||||
final h = info.image.height.toDouble();
|
||||
if (w > 0 && h > 0) {
|
||||
setState(() => _aspectRatio = w / h);
|
||||
}
|
||||
}
|
||||
|
||||
void _onImageError(dynamic exception, StackTrace? stackTrace) {
|
||||
if (mounted) setState(() => _aspectRatio = 1);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stream?.removeListener(_listener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ratio = _aspectRatio ?? 1;
|
||||
final height = widget.maxWidth / ratio;
|
||||
|
||||
return SizedBox(
|
||||
width: widget.maxWidth,
|
||||
height: height,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: widget.imageUrl,
|
||||
fit: BoxFit.contain,
|
||||
width: widget.maxWidth,
|
||||
height: height,
|
||||
placeholder: (_, __) => SizedBox(
|
||||
width: widget.maxWidth,
|
||||
height: widget.maxWidth,
|
||||
child: const _LoadingOverlay(isLoading: true),
|
||||
),
|
||||
errorWidget: (_, __, ___) => SizedBox(
|
||||
width: widget.maxWidth,
|
||||
height: widget.maxWidth,
|
||||
child: const _LoadingOverlay(isLoading: false),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LoadingOverlay extends StatelessWidget {
|
||||
const _LoadingOverlay({this.isLoading = true});
|
||||
|
||||
final bool isLoading;
|
||||
class _BgLoadingPlaceholder extends StatelessWidget {
|
||||
const _BgLoadingPlaceholder();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: AppColors.surfaceAlt,
|
||||
color: AppColors.textPrimary,
|
||||
alignment: Alignment.center,
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
child: const SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: AppColors.primary,
|
||||
color: AppColors.surface,
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
LucideIcons.video,
|
||||
size: 48,
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BgErrorPlaceholder extends StatelessWidget {
|
||||
const _BgErrorPlaceholder();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: AppColors.textPrimary,
|
||||
alignment: Alignment.center,
|
||||
child: Icon(
|
||||
LucideIcons.image_off,
|
||||
size: 56,
|
||||
color: AppColors.surface.withValues(alpha: 0.45),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -644,7 +498,7 @@ class _ResolutionToggle extends StatelessWidget {
|
||||
'Resolution',
|
||||
style: AppTypography.bodyMedium.copyWith(
|
||||
fontSize: 14,
|
||||
color: AppColors.textPrimary,
|
||||
color: AppColors.surface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
@ -685,14 +539,22 @@ class _ResolutionOption extends StatelessWidget {
|
||||
height: 36,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.primary : AppColors.surfaceAlt,
|
||||
color: isSelected
|
||||
? AppColors.primaryGlass
|
||||
: AppColors.surface.withValues(alpha: 0.18),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border:
|
||||
isSelected ? null : Border.all(color: AppColors.border, width: 1),
|
||||
border: isSelected
|
||||
? Border.all(
|
||||
color: AppColors.primary.withValues(alpha: 0.42),
|
||||
width: 1)
|
||||
: Border.all(
|
||||
color: AppColors.surface.withValues(alpha: 0.45),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: isSelected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: AppColors.primaryShadow.withValues(alpha: 0.19),
|
||||
color: AppColors.primaryShadow.withValues(alpha: 0.22),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@ -704,7 +566,9 @@ class _ResolutionOption extends StatelessWidget {
|
||||
label,
|
||||
style: AppTypography.bodyMedium.copyWith(
|
||||
fontSize: 13,
|
||||
color: isSelected ? AppColors.surface : AppColors.textSecondary,
|
||||
color: isSelected
|
||||
? AppColors.surface
|
||||
: AppColors.surface.withValues(alpha: 0.85),
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@ -733,11 +597,15 @@ class _GenerateButton extends StatelessWidget {
|
||||
child: Container(
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary,
|
||||
color: AppColors.primaryGlassEmphasis,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: AppColors.primary.withValues(alpha: 0.45),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.primaryShadow.withValues(alpha: 0.25),
|
||||
color: AppColors.primaryShadow.withValues(alpha: 0.28),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
|
||||
@ -10,13 +10,23 @@ class CreditsBadge extends StatelessWidget {
|
||||
super.key,
|
||||
required this.credits,
|
||||
this.onTap,
|
||||
this.foregroundColor,
|
||||
this.capsuleColor,
|
||||
});
|
||||
|
||||
final String credits;
|
||||
final VoidCallback? onTap;
|
||||
/// 图标与数字颜色;默认 [AppColors.primary]
|
||||
final Color? foregroundColor;
|
||||
/// 胶囊背景;默认 [AppColors.primaryLight]
|
||||
final Color? capsuleColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fg = foregroundColor ?? AppColors.primary;
|
||||
final capsule = capsuleColor ?? AppColors.primaryLight;
|
||||
final lightOnDarkNav = foregroundColor != null;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
@ -25,9 +35,17 @@ class CreditsBadge extends StatelessWidget {
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryLight,
|
||||
color: capsule,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: [
|
||||
boxShadow: lightOnDarkNav
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.28),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: [
|
||||
BoxShadow(
|
||||
color: AppColors.primaryShadow.withValues(alpha: 0.13),
|
||||
blurRadius: 6,
|
||||
@ -38,15 +56,14 @@ class CreditsBadge extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(LucideIcons.sparkles,
|
||||
size: 16, color: AppColors.primary),
|
||||
Icon(LucideIcons.sparkles, size: 16, color: fg),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Text(
|
||||
credits,
|
||||
style: AppTypography.bodyRegular.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.primary,
|
||||
color: fg,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@ -14,6 +14,8 @@ class TopNavBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
this.showBackButton = false,
|
||||
this.onBack,
|
||||
this.onCreditsTap,
|
||||
this.backgroundColor = AppColors.surface,
|
||||
this.foregroundColor,
|
||||
});
|
||||
|
||||
final String title;
|
||||
@ -21,18 +23,29 @@ class TopNavBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final bool showBackButton;
|
||||
final VoidCallback? onBack;
|
||||
final VoidCallback? onCreditsTap;
|
||||
/// 例如全屏背景页上叠半透明导航栏时用 [Colors.transparent]
|
||||
final Color backgroundColor;
|
||||
/// 标题与返回键颜色;默认 [AppColors.textPrimary]
|
||||
final Color? foregroundColor;
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(56);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fg = foregroundColor ?? AppColors.textPrimary;
|
||||
final titleStyle = foregroundColor != null
|
||||
? AppTypography.navTitle.copyWith(color: foregroundColor)
|
||||
: AppTypography.navTitle;
|
||||
|
||||
return Container(
|
||||
height: 56,
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.screenPadding),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
boxShadow: [
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
boxShadow: backgroundColor.a < 0.02
|
||||
? null
|
||||
: const [
|
||||
BoxShadow(
|
||||
color: AppColors.shadowLight,
|
||||
blurRadius: 8,
|
||||
@ -46,14 +59,14 @@ class TopNavBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
GestureDetector(
|
||||
onTap: onBack ?? () => Navigator.of(context).pop(),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: const SizedBox(
|
||||
child: SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
LucideIcons.arrow_left,
|
||||
size: 24,
|
||||
color: AppColors.textPrimary,
|
||||
color: fg,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -63,19 +76,25 @@ class TopNavBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
? Center(
|
||||
child: Text(
|
||||
title,
|
||||
style: AppTypography.navTitle,
|
||||
style: titleStyle,
|
||||
),
|
||||
)
|
||||
: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
title,
|
||||
style: AppTypography.navTitle,
|
||||
style: titleStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (credits != null)
|
||||
CreditsBadge(credits: credits!, onTap: onCreditsTap)
|
||||
CreditsBadge(
|
||||
credits: credits!,
|
||||
onTap: onCreditsTap,
|
||||
foregroundColor: foregroundColor,
|
||||
capsuleColor:
|
||||
foregroundColor?.withValues(alpha: 0.22),
|
||||
)
|
||||
else
|
||||
const SizedBox(width: 40),
|
||||
],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user