diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 35d401b..6c5ae4e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -154,6 +154,8 @@ "createAiSubtitle": "Start from a story or an idea", "createUploadTitle": "Upload and publish", "createUploadSubtitle": "Publish a finished short drama", + "createMottoPrimary": "Your imagination", + "createMottoSecondary": "Can be seen by more people", "weekdayMon": "Mon", "weekdayTue": "Tue", "weekdayWed": "Wed", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index ffa2ba1..4a47fe5 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -133,6 +133,8 @@ "createAiSubtitle": "Mulai dari cerita atau ide", "createUploadTitle": "Unggah dan terbitkan", "createUploadSubtitle": "Terbitkan drama pendek yang sudah selesai", + "createMottoPrimary": "Imajinasimu", + "createMottoSecondary": "Bisa dilihat lebih banyak orang", "weekdayMon": "Sen", "weekdayTue": "Sel", "weekdayWed": "Rab", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index cfe633f..817d79b 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -664,6 +664,18 @@ abstract class AppLocalizations { /// **'Publish a finished short drama'** String get createUploadSubtitle; + /// No description provided for @createMottoPrimary. + /// + /// In en, this message translates to: + /// **'Your imagination'** + String get createMottoPrimary; + + /// No description provided for @createMottoSecondary. + /// + /// In en, this message translates to: + /// **'Can be seen by more people'** + String get createMottoSecondary; + /// No description provided for @weekdayMon. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 1c38a02..63e059f 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -317,6 +317,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get createUploadSubtitle => 'Publish a finished short drama'; + @override + String get createMottoPrimary => 'Your imagination'; + + @override + String get createMottoSecondary => 'Can be seen by more people'; + @override String get weekdayMon => 'Mon'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 0e12a5c..c8a1408 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -318,6 +318,12 @@ class AppLocalizationsId extends AppLocalizations { String get createUploadSubtitle => 'Terbitkan drama pendek yang sudah selesai'; + @override + String get createMottoPrimary => 'Imajinasimu'; + + @override + String get createMottoSecondary => 'Bisa dilihat lebih banyak orang'; + @override String get weekdayMon => 'Sen'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 074008a..00adeb8 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -312,6 +312,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get createUploadSubtitle => '发布已完成的漫剧'; + @override + String get createMottoPrimary => '你的想象'; + + @override + String get createMottoSecondary => '可以被更多人看见'; + @override String get weekdayMon => '周一'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index e98add6..b52e17e 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -133,6 +133,8 @@ "createAiSubtitle": "从故事或想法开始", "createUploadTitle": "上传发布", "createUploadSubtitle": "发布已完成的漫剧", + "createMottoPrimary": "你的想象", + "createMottoSecondary": "可以被更多人看见", "weekdayMon": "周一", "weekdayTue": "周二", "weekdayWed": "周三", diff --git a/lib/pages/create/create_view.dart b/lib/pages/create/create_view.dart index 2004eaf..d5cf3a4 100644 --- a/lib/pages/create/create_view.dart +++ b/lib/pages/create/create_view.dart @@ -3,11 +3,19 @@ import 'package:flutter/material.dart'; import '../../core/theme/app_colors.dart'; -import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_theme.dart'; import '../../l10n/app_localizations.dart'; -/// K28-B 创作页:AI 制作与上传发布入口。 +const double _cardHeight = 196; +const double _cardRadius = 20; +const double _arrowSize = 36; +const double _illustrationWidth = 156; +const double _illustrationHeight = 88; +const double _mottoLineWidth = 96; +const List _aiGradientStart = [Color(0xFFFFE3D0), Color(0xFFFFC9AC)]; +const List _uploadGradient = [Color(0xFFFFEAD9), Color(0xFFFFD3B3)]; + +/// 创作页:AI 制作与上传发布两张主卡,底部品牌标语。 class CreatePage extends StatelessWidget { const CreatePage({super.key}); @@ -20,46 +28,72 @@ class CreatePage extends StatelessWidget { bottom: false, child: ListView( key: const Key('create-page'), - padding: const EdgeInsets.fromLTRB( - AppDimens.pageHorizontal, - AppDimens.spaceXl, - AppDimens.pageHorizontal, - AppDimens.spaceXl, - ), + padding: const EdgeInsets.fromLTRB(16, 28, 16, 24), children: [ Text( l10n.createTab, style: const TextStyle( fontFamily: 'Outfit', color: AppColors.onSurface, - fontSize: AppDimens.heroTitle, + fontSize: 28, fontWeight: FontWeight.w800, ), ), - const SizedBox(height: AppDimens.spaceSm), + const SizedBox(height: 6), Text( l10n.createSubtitle, - style: const TextStyle( - color: AppColors.muted, - fontSize: AppDimens.body, - ), + style: const TextStyle(color: AppColors.muted, fontSize: 13), ), - const SizedBox(height: AppDimens.spaceXl), - _CreateEntry( + const SizedBox(height: 24), + _CreateCard( key: const Key('create-ai'), - icon: Icons.auto_awesome_rounded, title: l10n.createAiTitle, subtitle: l10n.createAiSubtitle, + illustration: const _AiIllustration(), onTap: () => _openPlaceholder(context, l10n.createAiTitle), ), - const SizedBox(height: AppDimens.spaceMd), - _CreateEntry( + const SizedBox(height: 16), + _CreateCard( key: const Key('create-upload'), - icon: Icons.upload_file_rounded, title: l10n.createUploadTitle, subtitle: l10n.createUploadSubtitle, + illustration: const _UploadIllustration(), onTap: () => _openPlaceholder(context, l10n.createUploadTitle), ), + const SizedBox(height: 32), + Text( + l10n.createMottoPrimary, + key: const Key('create-motto-primary'), + textAlign: TextAlign.center, + style: const TextStyle( + color: AppColors.onSurface, + fontSize: 18, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 4), + Text( + l10n.createMottoSecondary, + key: const Key('create-motto-secondary'), + textAlign: TextAlign.center, + style: const TextStyle( + color: AppColors.onSurface, + fontSize: 18, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 10), + Center( + child: Container( + key: const Key('create-motto-line'), + width: _mottoLineWidth, + height: 2, + decoration: BoxDecoration( + color: AppColors.blue, + borderRadius: BorderRadius.circular(1), + ), + ), + ), ], ), ), @@ -73,18 +107,18 @@ class CreatePage extends StatelessWidget { } } -class _CreateEntry extends StatelessWidget { - const _CreateEntry({ +class _CreateCard extends StatelessWidget { + const _CreateCard({ super.key, - required this.icon, required this.title, required this.subtitle, + required this.illustration, required this.onTap, }); - final IconData icon; final String title; final String subtitle; + final Widget illustration; final VoidCallback onTap; @override @@ -92,57 +126,66 @@ class _CreateEntry extends StatelessWidget { return Container( decoration: BoxDecoration( color: AppColors.card, - borderRadius: BorderRadius.circular(AppDimens.cardRadius), + borderRadius: BorderRadius.circular(_cardRadius), boxShadow: const [AppTheme.cardShadow], ), child: Material( color: Colors.transparent, - borderRadius: BorderRadius.circular(AppDimens.cardRadius), + borderRadius: BorderRadius.circular(_cardRadius), clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, - child: Padding( - padding: const EdgeInsets.all(AppDimens.cardPadding), - child: Row( + child: SizedBox( + height: _cardHeight, + child: Stack( children: [ - Container( - width: AppDimens.buttonLarge, - height: AppDimens.buttonLarge, - decoration: BoxDecoration( - color: AppColors.badgePink, - borderRadius: BorderRadius.circular(AppDimens.innerRadius), - ), - child: Icon(icon, color: AppColors.secondary, size: 22), - ), - const SizedBox(width: AppDimens.spaceMd), - Expanded( + Padding( + padding: const EdgeInsets.fromLTRB(20, 18, 68, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ Text( title, + maxLines: 1, + overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.onSurface, - fontSize: AppDimens.listTitle, - fontWeight: FontWeight.w700, + fontSize: 22, + fontWeight: FontWeight.w900, ), ), - const SizedBox(height: AppDimens.spaceXs), + const SizedBox(height: 6), Text( subtitle, + maxLines: 2, + overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.muted, - fontSize: AppDimens.caption, + fontSize: 13, ), ), ], ), ), - const Icon( - Icons.chevron_right_rounded, - color: AppColors.subtle, - size: 20, + Positioned( + top: 16, + right: 16, + child: Container( + width: _arrowSize, + height: _arrowSize, + decoration: const BoxDecoration( + color: AppColors.raised, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.arrow_forward_rounded, + color: AppColors.blue, + size: 18, + ), + ), ), + Positioned(right: 14, bottom: 14, child: illustration), ], ), ), @@ -152,6 +195,146 @@ class _CreateEntry extends StatelessWidget { } } +class _AiIllustration extends StatelessWidget { + const _AiIllustration(); + + @override + Widget build(BuildContext context) { + return Container( + width: _illustrationWidth, + height: _illustrationHeight, + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: _aiGradientStart, + ), + borderRadius: BorderRadius.circular(16), + ), + child: Stack( + children: [ + const Positioned( + left: 12, + top: 10, + child: Icon( + Icons.auto_awesome_rounded, + color: AppColors.secondary, + size: 30, + ), + ), + const Positioned( + right: 14, + top: 8, + child: Icon( + Icons.auto_awesome_rounded, + color: AppColors.blue, + size: 12, + ), + ), + Positioned( + right: 32, + top: 30, + child: Container( + width: 5, + height: 5, + decoration: BoxDecoration( + color: AppColors.blue.withValues(alpha: 0.55), + shape: BoxShape.circle, + ), + ), + ), + const Positioned( + left: 12, + bottom: 10, + child: Text( + 'Every Story Matters', + style: TextStyle( + color: AppColors.muted, + fontSize: 10, + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + ); + } +} + +class _UploadIllustration extends StatelessWidget { + const _UploadIllustration(); + + @override + Widget build(BuildContext context) { + return Container( + width: _illustrationWidth, + height: _illustrationHeight, + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: _uploadGradient, + ), + borderRadius: BorderRadius.circular(16), + ), + child: Stack( + children: [ + const Positioned( + left: 12, + top: 8, + child: Icon( + Icons.cloud_upload_rounded, + color: AppColors.secondary, + size: 30, + ), + ), + Positioned( + right: 16, + top: 12, + child: Container( + width: 30, + height: 20, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.55), + borderRadius: BorderRadius.circular(4), + ), + child: const Icon( + Icons.movie_creation_outlined, + color: AppColors.muted, + size: 12, + ), + ), + ), + Positioned( + right: 52, + top: 34, + child: Container( + width: 5, + height: 5, + decoration: BoxDecoration( + color: AppColors.blue.withValues(alpha: 0.55), + shape: BoxShape.circle, + ), + ), + ), + const Positioned( + left: 12, + bottom: 10, + child: Text( + 'Ready to publish', + style: TextStyle( + color: AppColors.muted, + fontSize: 10, + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + ); + } +} + class _CreatePlaceholderPage extends StatelessWidget { const _CreatePlaceholderPage({required this.title}); @@ -173,12 +356,12 @@ class _CreatePlaceholderPage extends StatelessWidget { color: AppColors.blue, size: 42, ), - const SizedBox(height: AppDimens.spaceMd), + const SizedBox(height: 12), Text( l10n.comingSoon, style: const TextStyle( color: AppColors.onSurface, - fontSize: AppDimens.sectionTitle, + fontSize: 16, fontWeight: FontWeight.w700, ), ), diff --git a/lib/pages/discover/widgets/drama_grid.dart b/lib/pages/discover/widgets/drama_grid.dart index 3f003b4..b58d8cf 100644 --- a/lib/pages/discover/widgets/drama_grid.dart +++ b/lib/pages/discover/widgets/drama_grid.dart @@ -23,7 +23,6 @@ const double _tagChipFontSize = 11; const double _heatFontSize = 9; const double _heatIconSize = 11; const int _maxTags = 3; -const Color _freeBadgeColor = Color(0xFF12B76A); const Color _heatBackground = Color(0x99000000); const Color _tagChipBackground = Color(0xFFF4F1EE); final RegExp _tagSplitter = RegExp(r'[,/、·]'); @@ -86,9 +85,6 @@ class DramaGridCard extends StatelessWidget { final Drama drama; final VoidCallback onTap; - bool get _isFree => - drama.freeEpisodes > 0 || drama.episodes.any((episode) => episode.isFree); - @override Widget build(BuildContext context) { return GestureDetector( @@ -118,7 +114,6 @@ class DramaGridCard extends StatelessWidget { fit: StackFit.expand, children: [ _DiscoverCoverImage(path: drama.posterUrl), - Positioned(top: 8, left: 8, child: _tagBadge()), Positioned(left: 8, bottom: 8, child: _heatBadge()), ], ), @@ -126,25 +121,6 @@ class DramaGridCard extends StatelessWidget { ); } - Widget _tagBadge() { - return Container( - key: Key('drama-tag-${drama.id}'), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), - decoration: BoxDecoration( - color: _freeBadgeColor, - borderRadius: BorderRadius.circular(AppDimens.pillRadius), - ), - child: Text( - _isFree ? l10n.free : l10n.originalTag, - style: const TextStyle( - color: AppColors.onPrimary, - fontSize: 10, - fontWeight: FontWeight.w700, - ), - ), - ); - } - Widget _heatBadge() { return Container( key: Key('drama-heat-${drama.id}'), diff --git a/test/discover/home_all_dramas_grid_test.dart b/test/discover/home_all_dramas_grid_test.dart index 2212541..aba5f18 100644 --- a/test/discover/home_all_dramas_grid_test.dart +++ b/test/discover/home_all_dramas_grid_test.dart @@ -47,6 +47,7 @@ void main() { find.byKey(const Key('drama-tag-chip-city-lights-demo-0')), findsOneWidget, ); + expect(find.byKey(const Key('drama-tag-city-lights-demo')), findsNothing); expect(tester.takeException(), isNull); }); diff --git a/test/home/k28_a_navigation_test.dart b/test/home/k28_a_navigation_test.dart index e4f86c5..89324f9 100644 --- a/test/home/k28_a_navigation_test.dart +++ b/test/home/k28_a_navigation_test.dart @@ -40,6 +40,13 @@ void main() { expect(find.text('Stories become short dramas'), findsOneWidget); expect(find.byKey(const Key('create-ai')), findsOneWidget); expect(find.byKey(const Key('create-upload')), findsOneWidget); + expect(tester.getSize(find.byKey(const Key('create-ai'))).height, 196); + expect(find.byKey(const Key('create-motto-primary')), findsOneWidget); + expect(find.byKey(const Key('create-motto-secondary')), findsOneWidget); + expect( + tester.getSize(find.byKey(const Key('create-motto-line'))), + const Size(96, 2), + ); await tester.tap(find.byKey(const Key('create-ai'))); await tester.pumpAndSettle();