优化:生成图片页面
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 primary = Color(0xFF8B5CF6);
|
||||||
static const Color primaryLight = Color(0x338B5CF6); // #8B5CF620
|
static const Color primaryLight = Color(0x338B5CF6); // #8B5CF620
|
||||||
static const Color primaryShadow = Color(0x338B5CF6); // #8B5CF620 for shadow
|
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
|
// Neutrals
|
||||||
static const Color background = Color(0xFFFAFAFA);
|
static const Color background = Color(0xFFFAFAFA);
|
||||||
|
|||||||
@ -215,44 +215,66 @@ class _GenerateVideoScreenState extends State<GenerateVideoScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final topInset = MediaQuery.paddingOf(context).top;
|
||||||
|
final creditsDisplay =
|
||||||
|
UserCreditsData.of(context)?.creditsDisplay ?? '--';
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: AppColors.background,
|
extendBodyBehindAppBar: true,
|
||||||
|
backgroundColor: Colors.black,
|
||||||
appBar: PreferredSize(
|
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(
|
child: TopNavBar(
|
||||||
title: 'Generate',
|
title: 'Generate',
|
||||||
|
credits: creditsDisplay,
|
||||||
|
onCreditsTap: () => Navigator.of(context).pushNamed('/recharge'),
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
onBack: () => Navigator.of(context).pop(),
|
onBack: () => Navigator.of(context).pop(),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
foregroundColor: AppColors.surface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Column(
|
),
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
_GenerateFullScreenBackground(
|
||||||
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(
|
|
||||||
videoUrl: widget.task?.previewVideoUrl,
|
videoUrl: widget.task?.previewVideoUrl,
|
||||||
imageUrl: widget.task?.previewImageUrl,
|
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(
|
SafeArea(
|
||||||
top: false,
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: const EdgeInsets.fromLTRB(
|
||||||
AppSpacing.screenPaddingLarge,
|
AppSpacing.screenPaddingLarge,
|
||||||
@ -261,13 +283,16 @@ class _GenerateVideoScreenState extends State<GenerateVideoScreen> {
|
|||||||
AppSpacing.screenPaddingLarge,
|
AppSpacing.screenPaddingLarge,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
|
const Spacer(),
|
||||||
if (_hasVideo)
|
if (_hasVideo)
|
||||||
_ResolutionToggle(
|
Center(
|
||||||
|
child: _ResolutionToggle(
|
||||||
selected: _selectedResolution,
|
selected: _selectedResolution,
|
||||||
onChanged: (r) => setState(() => _selectedResolution = r),
|
onChanged: (r) =>
|
||||||
|
setState(() => _selectedResolution = r),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (_hasVideo) const SizedBox(height: AppSpacing.xxl),
|
if (_hasVideo) const SizedBox(height: AppSpacing.xxl),
|
||||||
_GenerateButton(
|
_GenerateButton(
|
||||||
@ -285,65 +310,9 @@ class _GenerateVideoScreenState extends State<GenerateVideoScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CreditsCard extends StatelessWidget {
|
/// 列表传入的预览图/视频:全屏背景层,超出视口 [BoxFit.cover] 裁切。
|
||||||
const _CreditsCard({required this.credits});
|
class _GenerateFullScreenBackground extends StatefulWidget {
|
||||||
|
const _GenerateFullScreenBackground({
|
||||||
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({
|
|
||||||
this.videoUrl,
|
this.videoUrl,
|
||||||
this.imageUrl,
|
this.imageUrl,
|
||||||
});
|
});
|
||||||
@ -352,10 +321,12 @@ class _VideoPreviewArea extends StatefulWidget {
|
|||||||
final String? imageUrl;
|
final String? imageUrl;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_VideoPreviewArea> createState() => _VideoPreviewAreaState();
|
State<_GenerateFullScreenBackground> createState() =>
|
||||||
|
_GenerateFullScreenBackgroundState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _VideoPreviewAreaState extends State<_VideoPreviewArea> {
|
class _GenerateFullScreenBackgroundState
|
||||||
|
extends State<_GenerateFullScreenBackground> {
|
||||||
VideoPlayerController? _controller;
|
VideoPlayerController? _controller;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -374,7 +345,7 @@ class _VideoPreviewAreaState extends State<_VideoPreviewArea> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(covariant _VideoPreviewArea oldWidget) {
|
void didUpdateWidget(covariant _GenerateFullScreenBackground oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (oldWidget.videoUrl != widget.videoUrl) {
|
if (oldWidget.videoUrl != widget.videoUrl) {
|
||||||
_controller?.dispose();
|
_controller?.dispose();
|
||||||
@ -394,11 +365,15 @@ class _VideoPreviewAreaState extends State<_VideoPreviewArea> {
|
|||||||
try {
|
try {
|
||||||
final file = await DefaultCacheManager().getSingleFile(url);
|
final file = await DefaultCacheManager().getSingleFile(url);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final controller = VideoPlayerController.file(file);
|
final controller = VideoPlayerController.file(
|
||||||
|
file,
|
||||||
|
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
|
||||||
|
);
|
||||||
await controller.initialize();
|
await controller.initialize();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
await controller.setVolume(1.0);
|
||||||
|
await controller.setLooping(true);
|
||||||
await controller.play();
|
await controller.play();
|
||||||
controller.setLooping(true);
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_controller = controller;
|
_controller = controller;
|
||||||
@ -416,206 +391,85 @@ class _VideoPreviewAreaState extends State<_VideoPreviewArea> {
|
|||||||
final hasVideo = widget.videoUrl != null && widget.videoUrl!.isNotEmpty;
|
final hasVideo = widget.videoUrl != null && widget.videoUrl!.isNotEmpty;
|
||||||
final hasImage = widget.imageUrl != null && widget.imageUrl!.isNotEmpty;
|
final hasImage = widget.imageUrl != null && widget.imageUrl!.isNotEmpty;
|
||||||
|
|
||||||
// 图片模式:宽度=组件宽度,高度按图片宽高比自适应
|
return Stack(
|
||||||
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(
|
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
if (isReady)
|
if (!hasVideo && hasImage)
|
||||||
SizedBox.expand(
|
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(
|
child: FittedBox(
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.cover,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: _controller!.value.size.width,
|
width: _controller!.value.size.width,
|
||||||
height: _controller!.value.size.height,
|
height: _controller!.value.size.height,
|
||||||
child: VideoPlayer(_controller!),
|
child: VideoPlayer(_controller!),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else if (hasImage)
|
else if (hasVideo && hasImage)
|
||||||
CachedNetworkImage(
|
Positioned.fill(
|
||||||
|
child: CachedNetworkImage(
|
||||||
imageUrl: widget.imageUrl!,
|
imageUrl: widget.imageUrl!,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.cover,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
placeholder: (_, __) =>
|
placeholder: (_, __) => const _BgLoadingPlaceholder(),
|
||||||
const _LoadingOverlay(isLoading: true),
|
errorWidget: (_, __, ___) => const _BgErrorPlaceholder(),
|
||||||
errorWidget: (_, __, ___) =>
|
),
|
||||||
const _LoadingOverlay(isLoading: false),
|
|
||||||
)
|
)
|
||||||
|
else if (hasVideo)
|
||||||
|
const Positioned.fill(child: _BgLoadingPlaceholder())
|
||||||
else
|
else
|
||||||
const _LoadingOverlay(isLoading: false),
|
const Positioned.fill(child: _BgErrorPlaceholder()),
|
||||||
if (hasVideo && !isReady)
|
|
||||||
const Positioned.fill(
|
|
||||||
child: _LoadingOverlay(isLoading: true),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 图片展示:宽度=组件宽度,高度按图片宽高比自适应
|
class _BgLoadingPlaceholder extends StatelessWidget {
|
||||||
class _AspectRatioImage extends StatefulWidget {
|
const _BgLoadingPlaceholder();
|
||||||
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;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
color: AppColors.surfaceAlt,
|
color: AppColors.textPrimary,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: isLoading
|
child: const SizedBox(
|
||||||
? const SizedBox(
|
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2,
|
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',
|
'Resolution',
|
||||||
style: AppTypography.bodyMedium.copyWith(
|
style: AppTypography.bodyMedium.copyWith(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.surface,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -685,14 +539,22 @@ class _ResolutionOption extends StatelessWidget {
|
|||||||
height: 36,
|
height: 36,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected ? AppColors.primary : AppColors.surfaceAlt,
|
color: isSelected
|
||||||
|
? AppColors.primaryGlass
|
||||||
|
: AppColors.surface.withValues(alpha: 0.18),
|
||||||
borderRadius: BorderRadius.circular(18),
|
borderRadius: BorderRadius.circular(18),
|
||||||
border:
|
border: isSelected
|
||||||
isSelected ? null : Border.all(color: AppColors.border, width: 1),
|
? 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: isSelected
|
||||||
? [
|
? [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: AppColors.primaryShadow.withValues(alpha: 0.19),
|
color: AppColors.primaryShadow.withValues(alpha: 0.22),
|
||||||
blurRadius: 4,
|
blurRadius: 4,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
@ -704,7 +566,9 @@ class _ResolutionOption extends StatelessWidget {
|
|||||||
label,
|
label,
|
||||||
style: AppTypography.bodyMedium.copyWith(
|
style: AppTypography.bodyMedium.copyWith(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: isSelected ? AppColors.surface : AppColors.textSecondary,
|
color: isSelected
|
||||||
|
? AppColors.surface
|
||||||
|
: AppColors.surface.withValues(alpha: 0.85),
|
||||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -733,11 +597,15 @@ class _GenerateButton extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
height: 56,
|
height: 56,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.primary,
|
color: AppColors.primaryGlassEmphasis,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.primary.withValues(alpha: 0.45),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: AppColors.primaryShadow.withValues(alpha: 0.25),
|
color: AppColors.primaryShadow.withValues(alpha: 0.28),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -10,13 +10,23 @@ class CreditsBadge extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.credits,
|
required this.credits,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
|
this.foregroundColor,
|
||||||
|
this.capsuleColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String credits;
|
final String credits;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
/// 图标与数字颜色;默认 [AppColors.primary]
|
||||||
|
final Color? foregroundColor;
|
||||||
|
/// 胶囊背景;默认 [AppColors.primaryLight]
|
||||||
|
final Color? capsuleColor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final fg = foregroundColor ?? AppColors.primary;
|
||||||
|
final capsule = capsuleColor ?? AppColors.primaryLight;
|
||||||
|
final lightOnDarkNav = foregroundColor != null;
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
@ -25,9 +35,17 @@ class CreditsBadge extends StatelessWidget {
|
|||||||
vertical: AppSpacing.sm,
|
vertical: AppSpacing.sm,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.primaryLight,
|
color: capsule,
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
boxShadow: [
|
boxShadow: lightOnDarkNav
|
||||||
|
? [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.28),
|
||||||
|
blurRadius: 6,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: AppColors.primaryShadow.withValues(alpha: 0.13),
|
color: AppColors.primaryShadow.withValues(alpha: 0.13),
|
||||||
blurRadius: 6,
|
blurRadius: 6,
|
||||||
@ -38,15 +56,14 @@ class CreditsBadge extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Icon(LucideIcons.sparkles,
|
Icon(LucideIcons.sparkles, size: 16, color: fg),
|
||||||
size: 16, color: AppColors.primary),
|
|
||||||
const SizedBox(width: AppSpacing.sm),
|
const SizedBox(width: AppSpacing.sm),
|
||||||
Text(
|
Text(
|
||||||
credits,
|
credits,
|
||||||
style: AppTypography.bodyRegular.copyWith(
|
style: AppTypography.bodyRegular.copyWith(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: AppColors.primary,
|
color: fg,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -14,6 +14,8 @@ class TopNavBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
this.showBackButton = false,
|
this.showBackButton = false,
|
||||||
this.onBack,
|
this.onBack,
|
||||||
this.onCreditsTap,
|
this.onCreditsTap,
|
||||||
|
this.backgroundColor = AppColors.surface,
|
||||||
|
this.foregroundColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
@ -21,18 +23,29 @@ class TopNavBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
final bool showBackButton;
|
final bool showBackButton;
|
||||||
final VoidCallback? onBack;
|
final VoidCallback? onBack;
|
||||||
final VoidCallback? onCreditsTap;
|
final VoidCallback? onCreditsTap;
|
||||||
|
/// 例如全屏背景页上叠半透明导航栏时用 [Colors.transparent]
|
||||||
|
final Color backgroundColor;
|
||||||
|
/// 标题与返回键颜色;默认 [AppColors.textPrimary]
|
||||||
|
final Color? foregroundColor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Size get preferredSize => const Size.fromHeight(56);
|
Size get preferredSize => const Size.fromHeight(56);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final fg = foregroundColor ?? AppColors.textPrimary;
|
||||||
|
final titleStyle = foregroundColor != null
|
||||||
|
? AppTypography.navTitle.copyWith(color: foregroundColor)
|
||||||
|
: AppTypography.navTitle;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: 56,
|
height: 56,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.screenPadding),
|
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.screenPadding),
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.surface,
|
color: backgroundColor,
|
||||||
boxShadow: [
|
boxShadow: backgroundColor.a < 0.02
|
||||||
|
? null
|
||||||
|
: const [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: AppColors.shadowLight,
|
color: AppColors.shadowLight,
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
@ -46,14 +59,14 @@ class TopNavBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: onBack ?? () => Navigator.of(context).pop(),
|
onTap: onBack ?? () => Navigator.of(context).pop(),
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
child: const SizedBox(
|
child: SizedBox(
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
LucideIcons.arrow_left,
|
LucideIcons.arrow_left,
|
||||||
size: 24,
|
size: 24,
|
||||||
color: AppColors.textPrimary,
|
color: fg,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -63,19 +76,25 @@ class TopNavBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
? Center(
|
? Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
title,
|
title,
|
||||||
style: AppTypography.navTitle,
|
style: titleStyle,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Align(
|
: Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Text(
|
child: Text(
|
||||||
title,
|
title,
|
||||||
style: AppTypography.navTitle,
|
style: titleStyle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (credits != null)
|
if (credits != null)
|
||||||
CreditsBadge(credits: credits!, onTap: onCreditsTap)
|
CreditsBadge(
|
||||||
|
credits: credits!,
|
||||||
|
onTap: onCreditsTap,
|
||||||
|
foregroundColor: foregroundColor,
|
||||||
|
capsuleColor:
|
||||||
|
foregroundColor?.withValues(alpha: 0.22),
|
||||||
|
)
|
||||||
else
|
else
|
||||||
const SizedBox(width: 40),
|
const SizedBox(width: 40),
|
||||||
],
|
],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user