From de7da648863d79dd3ee1dafd8d8b5c75218f28d7 Mon Sep 17 00:00:00 2001 From: Vicky <10212746+haifeng-jiang@user.noreply.gitee.com> Date: Wed, 28 Oct 2026 16:36:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E7=99=BB=E5=BD=95=E9=A1=B5?= =?UTF-8?q?=E9=87=8D=E8=AE=BE=E8=AE=A1=E5=B9=B6=E7=BB=9F=E4=B8=80=E5=BC=B9?= =?UTF-8?q?=E6=A1=86=E4=B8=BB=E9=A2=98=E6=B8=90=E5=8F=98=E5=A4=96=E5=A3=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/widgets/app_glow_panel.dart | 119 ++++++++++++++ lib/core/widgets/auth_gate.dart | 12 +- lib/core/widgets/catalog_endpoint_editor.dart | 7 +- lib/l10n/app_en.arb | 53 +++++- lib/l10n/app_id.arb | 53 +++++- lib/l10n/app_localizations.dart | 116 +++++++++++-- lib/l10n/app_localizations_en.dart | 73 ++++++++- lib/l10n/app_localizations_id.dart | 75 +++++++-- lib/l10n/app_localizations_zh.dart | 72 ++++++++- lib/l10n/app_zh.arb | 55 ++++++- .../account/account_deactivate_page.dart | 9 +- lib/pages/auth/auth_form_widgets.dart | 92 +++++------ lib/pages/auth/auth_legal_links.dart | 2 +- lib/pages/auth/auth_view.dart | 152 ++++++++++-------- lib/pages/auth/auth_widgets.dart | 129 ++++++--------- lib/pages/auth/phone_auth_sheet.dart | 22 +-- lib/pages/downloads/downloads_page.dart | 11 +- .../interaction/widgets/report_dialog.dart | 7 +- lib/pages/notices/notices_page.dart | 7 +- .../player/widgets/drama_comments_sheet.dart | 18 ++- .../player/widgets/episode_picker_sheet.dart | 20 +-- .../player/widgets/player_more_sheet.dart | 86 +++++++++- .../player/widgets/player_speed_sheet.dart | 14 +- .../player/widgets/player_unlock_sheet.dart | 9 +- .../watch_records/watch_records_page.dart | 9 +- 25 files changed, 905 insertions(+), 317 deletions(-) create mode 100644 lib/core/widgets/app_glow_panel.dart diff --git a/lib/core/widgets/app_glow_panel.dart b/lib/core/widgets/app_glow_panel.dart new file mode 100644 index 0000000..e92533d --- /dev/null +++ b/lib/core/widgets/app_glow_panel.dart @@ -0,0 +1,119 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; + +import '../theme/app_colors.dart'; +import '../theme/app_dimens.dart'; + +/// 主题红晕渐变容器:弹框(居中)与底部弹层(半屏)共用的外壳。 +/// 纯黑弹框统一升级为「顶部红光 → 近黑」的家族渐变。 +class AppGlowPanel extends StatelessWidget { + const AppGlowPanel({ + super.key, + required this.child, + this.borderRadius = const BorderRadius.all(Radius.circular(20)), + this.border = true, + }); + + final Widget child; + final BorderRadius borderRadius; + final bool border; + + static const _gradient = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + AppColors.followingGlow, + AppColors.followingGlowMid, + Color(0xFF101012), + ], + stops: [0, 0.22, 0.62], + ); + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + gradient: _gradient, + borderRadius: borderRadius, + border: border + ? Border.all(color: AppColors.contentBorder) + : null, + ), + child: child, + ); + } +} + +/// 主题渐变确认弹框:替代 AlertDialog 的家族化外壳。 +/// 按钮沿用项目惯例:纯文字 TextButton,主操作品牌红。 +class AppGlowDialog extends StatelessWidget { + const AppGlowDialog({ + super.key, + this.title, + this.content, + this.contentChild, + this.actions, + this.contentTextStyle, + }); + + final String? title; + final String? content; + + /// 富内容(表单/滚动区),与 [content] 二选一。 + final Widget? contentChild; + final List? actions; + final TextStyle? contentTextStyle; + + @override + Widget build(BuildContext context) { + final bodyStyle = contentTextStyle ?? + TextStyle( + color: AppColors.muted, + fontSize: AppDimens.body, + fontWeight: FontWeight.w400, + ); + return Dialog( + backgroundColor: AppColors.transparent, + elevation: 0, + child: AppGlowPanel( + child: Padding( + padding: const EdgeInsets.fromLTRB(22, 22, 22, 10), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (title != null) ...[ + Text( + title!, + style: TextStyle( + color: AppColors.onSurface, + fontSize: AppDimens.sectionTitle, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 14), + ], + if (content != null) + Text(content!, style: bodyStyle) + else + ?contentChild, + if (actions != null && actions!.isNotEmpty) ...[ + const SizedBox(height: 18), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + for (var i = 0; i < actions!.length; i++) ...[ + if (i > 0) const SizedBox(width: 4), + actions![i], + ], + ], + ), + ], + ], + ), + ), + ), + ); + } +} diff --git a/lib/core/widgets/auth_gate.dart b/lib/core/widgets/auth_gate.dart index b238dfc..9f85fc6 100644 --- a/lib/core/widgets/auth_gate.dart +++ b/lib/core/widgets/auth_gate.dart @@ -5,6 +5,7 @@ import '../../l10n/app_localizations.dart'; import '../../pages/auth/auth_view.dart'; import '../data/services/auth_controller.dart'; import '../theme/app_colors.dart'; +import 'app_glow_panel.dart'; /// 打开登录 / 注册页(沿用现有 Navigator 风格,不引路由库) Future openAuthPage(BuildContext context, {bool signUpMode = false}) { @@ -23,11 +24,11 @@ Future ensureSignedIn(BuildContext context) async { final l10n = AppLocalizations.of(context)!; final go = await showModalBottomSheet( context: context, - backgroundColor: AppColors.card, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(18)), - ), - builder: (sheetContext) => SafeArea( + backgroundColor: AppColors.transparent, + builder: (sheetContext) => AppGlowPanel( + borderRadius: const BorderRadius.vertical(top: Radius.circular(18)), + border: false, + child: SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(20, 18, 20, 20), child: Column( @@ -71,6 +72,7 @@ Future ensureSignedIn(BuildContext context) async { ], ), ), + ), ), ); diff --git a/lib/core/widgets/catalog_endpoint_editor.dart b/lib/core/widgets/catalog_endpoint_editor.dart index 1016672..cafd56c 100644 --- a/lib/core/widgets/catalog_endpoint_editor.dart +++ b/lib/core/widgets/catalog_endpoint_editor.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import '../config/app_config.dart'; import '../config/server_endpoint.dart'; import '../theme/app_colors.dart'; +import 'app_glow_panel.dart'; const String _title = '设置服务地址'; const String _save = '保存'; @@ -72,9 +73,9 @@ class _CatalogEndpointDialogState extends State<_CatalogEndpointDialog> { @override Widget build(BuildContext context) { - return AlertDialog( - title: const Text(_title), - content: SingleChildScrollView( + return AppGlowDialog( + title: _title, + contentChild: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0536a3e..4d11cd9 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -20,7 +20,6 @@ "featured": "Featured", "recommended": "Recommended", "fruitShortsBrand": "FruitShorts", - "authLandingSubtitle": "Sign in to sync progress, favorites and coins", "comingSoon": "Coming soon", "authLegal": "By continuing, you agree to the Terms of Service and Privacy Policy.", "authLegalLead": "By continuing, you agree to the", @@ -61,7 +60,7 @@ "episodes": {"type": "String"} } }, - "feedEpisodePrefix": "EP{episode} | ", + "feedEpisodePrefix": "EP{episode}", "@feedEpisodePrefix": { "placeholders": { "episode": {"type": "int"} @@ -275,6 +274,7 @@ "shareFailed": "Could not open the share sheet.", "nowPlaying": "Playing now", "pictureInPicture": "Picture in picture", + "autoEnterPip": "Auto mini player on exit", "pipUnavailable": "Picture in picture is unavailable on this device", "originalTag": "Original", "originalProduction": "Original", @@ -442,6 +442,7 @@ "restoreDefault": "Restore default", "invalidServerAddress": "Enter a valid http(s) server address.", "signOut": "Sign Out", + "signOutConfirm": "Sign out of your current account?", "mockUserName": "Vicky sheng", "mockUserId": "ID: M1CZWVYE", "mockNotice": "Sample data only.", @@ -787,8 +788,14 @@ "offlineDownloads": "Offline downloads", "watchPreferences": "Watch preferences", "contactSupport": "Contact support", - "vipBrandName": "Netshort VIP", - "mineVipSubtitle": "Enjoy exclusive benefits", + "vipBrandName": "FruitShorts VIP", + "vipOpenNow": "Get VIP", + "vipBenefitFree": "Free full access", + "vipBenefitNoAds": "Ad-free viewing", + "vipBenefitHd": "1080P HD", + "vipBenefitFastCache": "Fast downloads", + "moreServices": "More", + "mineVipSubtitle": "Unlock excitement, binge freely", "favoriteEpisodeProgress": "Episode {episode} / {total} total", "@favoriteEpisodeProgress": { "placeholders": { @@ -796,6 +803,44 @@ "total": { "type": "int" } } }, + "myFavorites": "My Favorites", + "manage": "Manage", + "continueWatching": "Continue Watching", + "continueWatchingSubtitle": "Episode {episode} of {total}", + "@continueWatchingSubtitle": { + "placeholders": { + "episode": { "type": "int" }, + "total": { "type": "int" } + } + }, + "favoriteDramas": "Favorites", + "totalDramasCount": "{count} titles", + "@totalDramasCount": { + "placeholders": { + "count": { "type": "int" } + } + }, + "watchedEpisodeTotal": "EP {episode} / {total}", + "@watchedEpisodeTotal": { + "placeholders": { + "episode": { "type": "int" }, + "total": { "type": "int" } + } + }, + "hasUpdateBadge": "UPDATED", + "updatedToEpisode": "Updated to EP {episode}", + "@updatedToEpisode": { + "placeholders": { + "episode": { "type": "int" } + } + }, + "finishedAllEpisodes": "{total} eps · Completed", + "@finishedAllEpisodes": { + "placeholders": { + "total": { "type": "int" } + } + }, + "noFavoritesYet": "No favorites yet", "watchedToEpisode": "Watched to Episode {episode}", "@watchedToEpisode": { "placeholders": { "episode": { "type": "int" } } }, "homeWatchAll": "Watch all", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index c6be6ec..7380926 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -20,7 +20,6 @@ "featured": "Unggulan", "recommended": "Rekomendasi", "fruitShortsBrand": "FruitShorts", - "authLandingSubtitle": "Masuk untuk menyinkronkan progres, favorit, dan koin", "comingSoon": "Segera hadir", "authLegal": "Dengan melanjutkan, Anda menyetujui Ketentuan Layanan dan Kebijakan Privasi.", "authLegalLead": "Dengan melanjutkan, Anda menyetujui", @@ -55,7 +54,7 @@ "episodes": {"type": "String"} } }, - "feedEpisodePrefix": "EP{episode} | ", + "feedEpisodePrefix": "EP{episode}", "@feedEpisodePrefix": { "placeholders": { "episode": {"type": "int"} @@ -254,6 +253,7 @@ "shareFailed": "Tidak bisa membuka menu berbagi.", "nowPlaying": "Sedang diputar", "pictureInPicture": "Gambar dalam gambar", + "autoEnterPip": "Mini player otomatis saat keluar", "pipUnavailable": "Gambar dalam gambar tidak tersedia di perangkat ini", "originalTag": "Original", "originalProduction": "Original", @@ -421,6 +421,7 @@ "restoreDefault": "Pulihkan default", "invalidServerAddress": "Masukkan alamat server http(s) yang valid.", "signOut": "Keluar", + "signOutConfirm": "Keluar dari akun saat ini?", "mockUserName": "Vicky sheng", "mockUserId": "ID: M1CZWVYE", "mockNotice": "Hanya data contoh.", @@ -749,8 +750,14 @@ "offlineDownloads": "Unduhan offline", "watchPreferences": "Preferensi tontonan", "contactSupport": "Hubungi dukungan", - "vipBrandName": "Netshort VIP", - "mineVipSubtitle": "Nikmati manfaat eksklusif", + "vipBrandName": "FruitShorts VIP", + "vipOpenNow": "Buka VIP", + "vipBenefitFree": "Nonton semua gratis", + "vipBenefitNoAds": "Bebas iklan", + "vipBenefitHd": "HD 1080P", + "vipBenefitFastCache": "Unduhan cepat", + "moreServices": "Lainnya", + "mineVipSubtitle": "Buka keseruan, tonton sepuasnya", "favoriteEpisodeProgress": "Episode {episode} / total {total}", "@favoriteEpisodeProgress": { "placeholders": { @@ -758,6 +765,44 @@ "total": { "type": "int" } } }, + "myFavorites": "Favorit Saya", + "manage": "Kelola", + "continueWatching": "Lanjutkan", + "continueWatchingSubtitle": "Episode {episode} dari {total}", + "@continueWatchingSubtitle": { + "placeholders": { + "episode": { "type": "int" }, + "total": { "type": "int" } + } + }, + "favoriteDramas": "Koleksi", + "totalDramasCount": "{count} judul", + "@totalDramasCount": { + "placeholders": { + "count": { "type": "int" } + } + }, + "watchedEpisodeTotal": "EP {episode} / {total}", + "@watchedEpisodeTotal": { + "placeholders": { + "episode": { "type": "int" }, + "total": { "type": "int" } + } + }, + "hasUpdateBadge": "UPDATE", + "updatedToEpisode": "Diperbarui ke EP {episode}", + "@updatedToEpisode": { + "placeholders": { + "episode": { "type": "int" } + } + }, + "finishedAllEpisodes": "{total} eps · Selesai", + "@finishedAllEpisodes": { + "placeholders": { + "total": { "type": "int" } + } + }, + "noFavoritesYet": "Belum ada favorit", "watchedToEpisode": "Ditonton hingga Episode {episode}", "@watchedToEpisode": { "placeholders": { "episode": { "type": "int" } } }, "homeWatchAll": "Tonton semua", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 0975ce6..25752dc 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -220,12 +220,6 @@ abstract class AppLocalizations { /// **'FruitShorts'** String get fruitShortsBrand; - /// No description provided for @authLandingSubtitle. - /// - /// In en, this message translates to: - /// **'Sign in to sync progress, favorites and coins'** - String get authLandingSubtitle; - /// No description provided for @comingSoon. /// /// In en, this message translates to: @@ -403,7 +397,7 @@ abstract class AppLocalizations { /// No description provided for @feedEpisodePrefix. /// /// In en, this message translates to: - /// **'EP{episode} | '** + /// **'EP{episode}'** String feedEpisodePrefix(int episode); /// No description provided for @feedWatchingCount. @@ -1198,6 +1192,12 @@ abstract class AppLocalizations { /// **'Picture in picture'** String get pictureInPicture; + /// No description provided for @autoEnterPip. + /// + /// In en, this message translates to: + /// **'Auto mini player on exit'** + String get autoEnterPip; + /// No description provided for @pipUnavailable. /// /// In en, this message translates to: @@ -1828,6 +1828,12 @@ abstract class AppLocalizations { /// **'Sign Out'** String get signOut; + /// No description provided for @signOutConfirm. + /// + /// In en, this message translates to: + /// **'Sign out of your current account?'** + String get signOutConfirm; + /// No description provided for @mockUserName. /// /// In en, this message translates to: @@ -2125,7 +2131,7 @@ abstract class AppLocalizations { /// No description provided for @myFavorites. /// /// In en, this message translates to: - /// **'My favorites'** + /// **'My Favorites'** String get myFavorites; /// No description provided for @noFavorites. @@ -3427,13 +3433,49 @@ abstract class AppLocalizations { /// No description provided for @vipBrandName. /// /// In en, this message translates to: - /// **'Netshort VIP'** + /// **'FruitShorts VIP'** String get vipBrandName; + /// No description provided for @vipOpenNow. + /// + /// In en, this message translates to: + /// **'Get VIP'** + String get vipOpenNow; + + /// No description provided for @vipBenefitFree. + /// + /// In en, this message translates to: + /// **'Free full access'** + String get vipBenefitFree; + + /// No description provided for @vipBenefitNoAds. + /// + /// In en, this message translates to: + /// **'Ad-free viewing'** + String get vipBenefitNoAds; + + /// No description provided for @vipBenefitHd. + /// + /// In en, this message translates to: + /// **'1080P HD'** + String get vipBenefitHd; + + /// No description provided for @vipBenefitFastCache. + /// + /// In en, this message translates to: + /// **'Fast downloads'** + String get vipBenefitFastCache; + + /// No description provided for @moreServices. + /// + /// In en, this message translates to: + /// **'More'** + String get moreServices; + /// No description provided for @mineVipSubtitle. /// /// In en, this message translates to: - /// **'Enjoy exclusive benefits'** + /// **'Unlock excitement, binge freely'** String get mineVipSubtitle; /// No description provided for @favoriteEpisodeProgress. @@ -3442,6 +3484,60 @@ abstract class AppLocalizations { /// **'Episode {episode} / {total} total'** String favoriteEpisodeProgress(int episode, int total); + /// No description provided for @manage. + /// + /// In en, this message translates to: + /// **'Manage'** + String get manage; + + /// No description provided for @continueWatchingSubtitle. + /// + /// In en, this message translates to: + /// **'Episode {episode} of {total}'** + String continueWatchingSubtitle(int episode, int total); + + /// No description provided for @favoriteDramas. + /// + /// In en, this message translates to: + /// **'Favorites'** + String get favoriteDramas; + + /// No description provided for @totalDramasCount. + /// + /// In en, this message translates to: + /// **'{count} titles'** + String totalDramasCount(int count); + + /// No description provided for @watchedEpisodeTotal. + /// + /// In en, this message translates to: + /// **'EP {episode} / {total}'** + String watchedEpisodeTotal(int episode, int total); + + /// No description provided for @hasUpdateBadge. + /// + /// In en, this message translates to: + /// **'UPDATED'** + String get hasUpdateBadge; + + /// No description provided for @updatedToEpisode. + /// + /// In en, this message translates to: + /// **'Updated to EP {episode}'** + String updatedToEpisode(int episode); + + /// No description provided for @finishedAllEpisodes. + /// + /// In en, this message translates to: + /// **'{total} eps · Completed'** + String finishedAllEpisodes(int total); + + /// No description provided for @noFavoritesYet. + /// + /// In en, this message translates to: + /// **'No favorites yet'** + String get noFavoritesYet; + /// No description provided for @watchedToEpisode. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 2ba335d..9a769fb 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -69,10 +69,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get fruitShortsBrand => 'FruitShorts'; - @override - String get authLandingSubtitle => - 'Sign in to sync progress, favorites and coins'; - @override String get comingSoon => 'Coming soon'; @@ -167,7 +163,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String feedEpisodePrefix(int episode) { - return 'EP$episode | '; + return 'EP$episode'; } @override @@ -597,6 +593,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get pictureInPicture => 'Picture in picture'; + @override + String get autoEnterPip => 'Auto mini player on exit'; + @override String get pipUnavailable => 'Picture in picture is unavailable on this device'; @@ -933,6 +932,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get signOut => 'Sign Out'; + @override + String get signOutConfirm => 'Sign out of your current account?'; + @override String get mockUserName => 'Vicky sheng'; @@ -1087,7 +1089,7 @@ class AppLocalizationsEn extends AppLocalizations { 'Could not update favorites. Please retry.'; @override - String get myFavorites => 'My favorites'; + String get myFavorites => 'My Favorites'; @override String get noFavorites => 'No favorites yet.'; @@ -1790,16 +1792,71 @@ class AppLocalizationsEn extends AppLocalizations { String get contactSupport => 'Contact support'; @override - String get vipBrandName => 'Netshort VIP'; + String get vipBrandName => 'FruitShorts VIP'; @override - String get mineVipSubtitle => 'Enjoy exclusive benefits'; + String get vipOpenNow => 'Get VIP'; + + @override + String get vipBenefitFree => 'Free full access'; + + @override + String get vipBenefitNoAds => 'Ad-free viewing'; + + @override + String get vipBenefitHd => '1080P HD'; + + @override + String get vipBenefitFastCache => 'Fast downloads'; + + @override + String get moreServices => 'More'; + + @override + String get mineVipSubtitle => 'Unlock excitement, binge freely'; @override String favoriteEpisodeProgress(int episode, int total) { return 'Episode $episode / $total total'; } + @override + String get manage => 'Manage'; + + @override + String continueWatchingSubtitle(int episode, int total) { + return 'Episode $episode of $total'; + } + + @override + String get favoriteDramas => 'Favorites'; + + @override + String totalDramasCount(int count) { + return '$count titles'; + } + + @override + String watchedEpisodeTotal(int episode, int total) { + return 'EP $episode / $total'; + } + + @override + String get hasUpdateBadge => 'UPDATED'; + + @override + String updatedToEpisode(int episode) { + return 'Updated to EP $episode'; + } + + @override + String finishedAllEpisodes(int total) { + return '$total eps · Completed'; + } + + @override + String get noFavoritesYet => 'No favorites yet'; + @override String watchedToEpisode(int episode) { return 'Watched to Episode $episode'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 4e9d1dd..2172eeb 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -69,10 +69,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get fruitShortsBrand => 'FruitShorts'; - @override - String get authLandingSubtitle => - 'Masuk untuk menyinkronkan progres, favorit, dan koin'; - @override String get comingSoon => 'Segera hadir'; @@ -167,7 +163,7 @@ class AppLocalizationsId extends AppLocalizations { @override String feedEpisodePrefix(int episode) { - return 'EP$episode | '; + return 'EP$episode'; } @override @@ -299,7 +295,7 @@ class AppLocalizationsId extends AppLocalizations { String get localDemoProfile => 'Profil demo lokal'; @override - String get continueWatching => 'Lanjut Tonton'; + String get continueWatching => 'Lanjutkan'; @override String episodeRemaining(int episode, int minutes) { @@ -598,6 +594,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get pictureInPicture => 'Gambar dalam gambar'; + @override + String get autoEnterPip => 'Mini player otomatis saat keluar'; + @override String get pipUnavailable => 'Gambar dalam gambar tidak tersedia di perangkat ini'; @@ -937,6 +936,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get signOut => 'Keluar'; + @override + String get signOutConfirm => 'Keluar dari akun saat ini?'; + @override String get mockUserName => 'Vicky sheng'; @@ -1089,7 +1091,7 @@ class AppLocalizationsId extends AppLocalizations { String get favoriteToggleFailed => 'Gagal memperbarui favorit. Coba lagi.'; @override - String get myFavorites => 'Favorit saya'; + String get myFavorites => 'Favorit Saya'; @override String get noFavorites => 'Belum ada favorit.'; @@ -1792,16 +1794,71 @@ class AppLocalizationsId extends AppLocalizations { String get contactSupport => 'Hubungi dukungan'; @override - String get vipBrandName => 'Netshort VIP'; + String get vipBrandName => 'FruitShorts VIP'; @override - String get mineVipSubtitle => 'Nikmati manfaat eksklusif'; + String get vipOpenNow => 'Buka VIP'; + + @override + String get vipBenefitFree => 'Nonton semua gratis'; + + @override + String get vipBenefitNoAds => 'Bebas iklan'; + + @override + String get vipBenefitHd => 'HD 1080P'; + + @override + String get vipBenefitFastCache => 'Unduhan cepat'; + + @override + String get moreServices => 'Lainnya'; + + @override + String get mineVipSubtitle => 'Buka keseruan, tonton sepuasnya'; @override String favoriteEpisodeProgress(int episode, int total) { return 'Episode $episode / total $total'; } + @override + String get manage => 'Kelola'; + + @override + String continueWatchingSubtitle(int episode, int total) { + return 'Episode $episode dari $total'; + } + + @override + String get favoriteDramas => 'Koleksi'; + + @override + String totalDramasCount(int count) { + return '$count judul'; + } + + @override + String watchedEpisodeTotal(int episode, int total) { + return 'EP $episode / $total'; + } + + @override + String get hasUpdateBadge => 'UPDATE'; + + @override + String updatedToEpisode(int episode) { + return 'Diperbarui ke EP $episode'; + } + + @override + String finishedAllEpisodes(int total) { + return '$total eps · Selesai'; + } + + @override + String get noFavoritesYet => 'Belum ada favorit'; + @override String watchedToEpisode(int episode) { return 'Ditonton hingga Episode $episode'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index dbf6af9..9bce81c 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -66,10 +66,7 @@ class AppLocalizationsZh extends AppLocalizations { String get recommended => '推荐'; @override - String get fruitShortsBrand => '水果短剧'; - - @override - String get authLandingSubtitle => '登录后同步进度、收藏与金币权益'; + String get fruitShortsBrand => 'FruitShorts'; @override String get comingSoon => '即将上线'; @@ -164,7 +161,7 @@ class AppLocalizationsZh extends AppLocalizations { @override String feedEpisodePrefix(int episode) { - return '第$episode集|'; + return '第$episode集'; } @override @@ -591,6 +588,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get pictureInPicture => '画中画'; + @override + String get autoEnterPip => '退出后自动小窗'; + @override String get pipUnavailable => '此设备无法使用画中画'; @@ -922,6 +922,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get signOut => '退出登录'; + @override + String get signOutConfirm => '确定要退出当前账号吗?'; + @override String get mockUserName => 'Vicky sheng'; @@ -1764,16 +1767,71 @@ class AppLocalizationsZh extends AppLocalizations { String get contactSupport => '联系客服'; @override - String get vipBrandName => 'Netshort VIP'; + String get vipBrandName => 'FruitShorts VIP'; @override - String get mineVipSubtitle => '享受专属权益'; + String get vipOpenNow => '立即开通'; + + @override + String get vipBenefitFree => '全剧免费看'; + + @override + String get vipBenefitNoAds => '免广告打扰'; + + @override + String get vipBenefitHd => '1080P高清'; + + @override + String get vipBenefitFastCache => '极速缓存'; + + @override + String get moreServices => '更多服务'; + + @override + String get mineVipSubtitle => '解锁精彩,畅看好剧'; @override String favoriteEpisodeProgress(int episode, int total) { return '第 $episode 集 / 共 $total 集'; } + @override + String get manage => '管理'; + + @override + String continueWatchingSubtitle(int episode, int total) { + return '看到第 $episode 集 · 全 $total 集'; + } + + @override + String get favoriteDramas => '收藏短剧'; + + @override + String totalDramasCount(int count) { + return '共 $count 部'; + } + + @override + String watchedEpisodeTotal(int episode, int total) { + return '看到第 $episode 集 / 全 $total 集'; + } + + @override + String get hasUpdateBadge => '有更新'; + + @override + String updatedToEpisode(int episode) { + return '更新至第 $episode 集'; + } + + @override + String finishedAllEpisodes(int total) { + return '全 $total 集 · 已完结'; + } + + @override + String get noFavoritesYet => '还没有收藏的短剧'; + @override String watchedToEpisode(int episode) { return '已看至 第 $episode 集'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index b94416d..414be2f 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -19,8 +19,7 @@ "searchCancel": "取消", "featured": "精选", "recommended": "推荐", - "fruitShortsBrand": "水果短剧", - "authLandingSubtitle": "登录后同步进度、收藏与金币权益", + "fruitShortsBrand": "FruitShorts", "comingSoon": "即将上线", "authLegal": "继续即代表同意《用户协议》与《隐私政策》", "authLegalLead": "继续即代表同意", @@ -55,7 +54,7 @@ "episodes": {"type": "String"} } }, - "feedEpisodePrefix": "第{episode}集|", + "feedEpisodePrefix": "第{episode}集", "@feedEpisodePrefix": { "placeholders": { "episode": {"type": "int"} @@ -254,6 +253,7 @@ "shareFailed": "无法打开系统分享面板。", "nowPlaying": "正在播放", "pictureInPicture": "画中画", + "autoEnterPip": "退出后自动小窗", "pipUnavailable": "此设备无法使用画中画", "originalTag": "原创", "originalProduction": "原创出品", @@ -383,6 +383,7 @@ "restoreDefault": "恢复默认", "invalidServerAddress": "请输入有效的 http(s) 服务器地址。", "signOut": "退出登录", + "signOutConfirm": "确定要退出当前账号吗?", "mockUserName": "Vicky sheng", "mockUserId": "ID: M1CZWVYE", "mockNotice": "当前是示例数据。", @@ -711,8 +712,14 @@ "offlineDownloads": "离线缓存", "watchPreferences": "观看偏好", "contactSupport": "联系客服", - "vipBrandName": "Netshort VIP", - "mineVipSubtitle": "享受专属权益", + "vipBrandName": "FruitShorts VIP", + "vipOpenNow": "立即开通", + "vipBenefitFree": "全剧免费看", + "vipBenefitNoAds": "免广告打扰", + "vipBenefitHd": "1080P高清", + "vipBenefitFastCache": "极速缓存", + "moreServices": "更多服务", + "mineVipSubtitle": "解锁精彩,畅看好剧", "favoriteEpisodeProgress": "第 {episode} 集 / 共 {total} 集", "@favoriteEpisodeProgress": { "placeholders": { @@ -720,6 +727,44 @@ "total": { "type": "int" } } }, + "myFavorites": "我的收藏", + "manage": "管理", + "continueWatching": "继续观看", + "continueWatchingSubtitle": "看到第 {episode} 集 · 全 {total} 集", + "@continueWatchingSubtitle": { + "placeholders": { + "episode": { "type": "int" }, + "total": { "type": "int" } + } + }, + "favoriteDramas": "收藏短剧", + "totalDramasCount": "共 {count} 部", + "@totalDramasCount": { + "placeholders": { + "count": { "type": "int" } + } + }, + "watchedEpisodeTotal": "看到第 {episode} 集 / 全 {total} 集", + "@watchedEpisodeTotal": { + "placeholders": { + "episode": { "type": "int" }, + "total": { "type": "int" } + } + }, + "hasUpdateBadge": "有更新", + "updatedToEpisode": "更新至第 {episode} 集", + "@updatedToEpisode": { + "placeholders": { + "episode": { "type": "int" } + } + }, + "finishedAllEpisodes": "全 {total} 集 · 已完结", + "@finishedAllEpisodes": { + "placeholders": { + "total": { "type": "int" } + } + }, + "noFavoritesYet": "还没有收藏的短剧", "watchedToEpisode": "已看至 第 {episode} 集", "@watchedToEpisode": { "placeholders": { "episode": { "type": "int" } } }, "homeWatchAll": "观看全集", diff --git a/lib/pages/account/account_deactivate_page.dart b/lib/pages/account/account_deactivate_page.dart index a2c9e8e..2b23112 100644 --- a/lib/pages/account/account_deactivate_page.dart +++ b/lib/pages/account/account_deactivate_page.dart @@ -11,6 +11,7 @@ import '../../core/formatting/localized_date_time.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_theme.dart'; +import '../../core/widgets/app_glow_panel.dart'; import '../../core/widgets/auth_gate.dart'; import '../../core/widgets/plain_page_scaffold.dart'; import '../../l10n/app_localizations.dart'; @@ -172,15 +173,15 @@ class _AccountDeactivatePageState extends State { final l10n = AppLocalizations.of(context)!; final confirmed = await showDialog( context: context, - builder: (dialogContext) => AlertDialog( - title: Text(l10n.accountConfirmDeactivateTitle), - content: Text(l10n.accountConfirmDeactivateMessage), + builder: (dialogContext) => AppGlowDialog( + title: l10n.accountConfirmDeactivateTitle, + content: l10n.accountConfirmDeactivateMessage, actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), child: Text(l10n.cancel), ), - FilledButton( + TextButton( key: const Key('account-deactivate-confirm'), onPressed: () => Navigator.of(dialogContext).pop(true), child: Text(l10n.accountConfirmDeactivate), diff --git a/lib/pages/auth/auth_form_widgets.dart b/lib/pages/auth/auth_form_widgets.dart index 50ec19c..7402a35 100644 --- a/lib/pages/auth/auth_form_widgets.dart +++ b/lib/pages/auth/auth_form_widgets.dart @@ -61,7 +61,7 @@ class _AuthFields extends StatelessWidget { onChanged: callbacks.onEmailChanged, onSubmitted: (_) => state.fields.passwordFocus.requestFocus(), style: const TextStyle(color: AppColors.onSurface, fontSize: 15), - cursorColor: AppColors.blue, + cursorColor: AppColors.brandRed, decoration: _decoration( context: context, hint: 'name@example.com', @@ -93,7 +93,7 @@ class _AuthFields extends StatelessWidget { onChanged: callbacks.onPasswordChanged, onSubmitted: (_) => callbacks.onSubmit(), style: const TextStyle(color: AppColors.onSurface, fontSize: 15), - cursorColor: AppColors.blue, + cursorColor: AppColors.brandRed, decoration: _decoration( context: context, hint: l10n.authPasswordHint, @@ -128,7 +128,7 @@ class _AuthFields extends StatelessWidget { style: const TextStyle( color: AppColors.muted, fontSize: 12, - fontWeight: FontWeight.w700, + fontWeight: FontWeight.w400, ), ); @@ -142,10 +142,7 @@ class _AuthFields extends StatelessWidget { }) { final border = OutlineInputBorder( borderRadius: BorderRadius.circular(_fieldRadius), - borderSide: const BorderSide( - color: AppColors.border, - width: _fieldBorderWidth, - ), + borderSide: BorderSide.none, ); final errorColor = Theme.of(context).colorScheme.error; final errorBorder = OutlineInputBorder( @@ -158,7 +155,7 @@ class _AuthFields extends StatelessWidget { prefixIcon: Icon( icon, size: 21, - color: focused ? AppColors.blue : AppColors.muted, + color: focused ? AppColors.brandRed : AppColors.muted, ), prefixIconConstraints: const BoxConstraints( minWidth: _minTapTarget, @@ -166,7 +163,7 @@ class _AuthFields extends StatelessWidget { ), suffixIcon: suffix, filled: true, - fillColor: AppColors.card, + fillColor: Colors.white.withValues(alpha: 0.05), isDense: true, contentPadding: const EdgeInsets.only(right: 16), border: border, @@ -175,7 +172,7 @@ class _AuthFields extends StatelessWidget { focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(_fieldRadius), borderSide: const BorderSide( - color: AppColors.blue, + color: AppColors.brandRed, width: _fieldBorderWidth, ), ), @@ -204,17 +201,17 @@ class _AuthSubmitButton extends StatelessWidget { style: ButtonStyle( backgroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.disabled)) { - return AppColors.blue.withValues(alpha: 0.5); + return AppColors.brandRed.withValues(alpha: 0.5); } if (states.contains(WidgetState.pressed)) { - return AppColors.bluePressed; + return AppColors.brandRedPressed; } - return AppColors.blue; + return AppColors.brandRed; }), foregroundColor: const WidgetStatePropertyAll(AppColors.onPrimary), elevation: const WidgetStatePropertyAll(2), shadowColor: WidgetStatePropertyAll( - AppColors.blue.withValues(alpha: 0.22), + AppColors.brandRed.withValues(alpha: 0.22), ), padding: const WidgetStatePropertyAll(EdgeInsets.zero), shape: WidgetStatePropertyAll( @@ -235,7 +232,7 @@ class _AuthSubmitButton extends StatelessWidget { status.signUpMode ? l10n.signUp : l10n.signIn, style: const TextStyle( fontSize: 15, - fontWeight: FontWeight.w800, + fontWeight: FontWeight.w600, ), ), ), @@ -243,18 +240,16 @@ class _AuthSubmitButton extends StatelessWidget { } } -class _ModeSegment extends StatelessWidget { - const _ModeSegment({ +class _ModeTab extends StatelessWidget { + const _ModeTab({ required this.segmentKey, required this.label, - required this.hiddenLabel, required this.selected, required this.onTap, }); final Key segmentKey; final String label; - final String hiddenLabel; final bool selected; final VoidCallback? onTap; @@ -264,43 +259,30 @@ class _ModeSegment extends StatelessWidget { key: segmentKey, behavior: HitTestBehavior.opaque, onTap: onTap, - child: AnimatedContainer( - duration: _segmentDuration, - alignment: Alignment.center, - decoration: BoxDecoration( - color: selected ? AppColors.blue : AppColors.transparent, - borderRadius: BorderRadius.circular(18), - ), - child: Stack( - fit: StackFit.expand, - children: [ - Center( - child: Text( - label, - style: TextStyle( - color: selected ? AppColors.onPrimary : AppColors.onSurface, - fontSize: 14, - fontWeight: FontWeight.w700, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + label, + style: TextStyle( + color: selected ? AppColors.textPrimary : AppColors.muted, + fontSize: AppDimens.followingSegmentFont, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: AppDimens.followingSegmentIndicatorTopGap), + Container( + width: AppDimens.followingSegmentIndicatorWidth, + height: AppDimens.followingSegmentIndicatorHeight, + decoration: BoxDecoration( + color: selected ? AppColors.brandRed : AppColors.transparent, + borderRadius: BorderRadius.circular( + AppDimens.followingSegmentIndicatorRadius, ), ), - Positioned.fill( - child: ExcludeSemantics( - child: Opacity( - opacity: 0, - child: Center( - child: Text( - hiddenLabel, - maxLines: 1, - softWrap: false, - style: const TextStyle(fontSize: 14), - ), - ), - ), - ), - ), - ], - ), + ), + ], ), ); } @@ -349,7 +331,7 @@ class _AuthBrand extends StatelessWidget { style: const TextStyle( color: AppColors.onSurface, fontSize: 24, - fontWeight: FontWeight.w900, + fontWeight: FontWeight.w700, height: 1.1, ), ), diff --git a/lib/pages/auth/auth_legal_links.dart b/lib/pages/auth/auth_legal_links.dart index ca4b75a..0acc1c9 100644 --- a/lib/pages/auth/auth_legal_links.dart +++ b/lib/pages/auth/auth_legal_links.dart @@ -50,7 +50,7 @@ class _AuthLegalLinksState extends State { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final muted = TextStyle(color: AppColors.muted, fontSize: _legalFontSize, height: 1.6); - final link = muted.copyWith(color: AppColors.blue); + final link = muted.copyWith(color: AppColors.brandRed); return Text.rich( TextSpan( style: muted, diff --git a/lib/pages/auth/auth_view.dart b/lib/pages/auth/auth_view.dart index 0f02fb6..0e63c13 100644 --- a/lib/pages/auth/auth_view.dart +++ b/lib/pages/auth/auth_view.dart @@ -24,18 +24,14 @@ part 'auth_widgets.dart'; part 'auth_form_widgets.dart'; const double _minTapTarget = 44; -const double _brandMarkSize = 72; +const double _brandMarkSize = 88; const double _compactBrandMarkSize = 48; -const double _brandMarkRadius = 20; +const double _brandMarkRadius = 26; const double _compactBrandMarkRadius = 14; -const double _landingLogoTopGap = 28; const String _appLogoAsset = 'assets/icon/fruitshorts.png'; const double _contentMaxWidth = 440; -// Auth keeps its approved 20px card silhouette. -const double _cardRadius = AppDimens.authCardRadius; -const double _cardPadding = 20; const double _fieldHeight = 52; -const double _fieldRadius = 14; +const double _fieldRadius = 12; const double _fieldBorderWidth = 1; const double _buttonHeight = 50; const double _buttonRadius = 25; @@ -47,14 +43,11 @@ const double _bottomContentPadding = 28; const double _compactTopContentPadding = 40; const double _compactHeightBreakpoint = 720; const double _compactBrandGap = 16; -const double _glowSize = 280; -const double _warmGlowOpacity = 0.18; -const double _entryButtonHeight = 50; -const double _entryButtonRadius = 25; +const double _entryButtonHeight = 52; +const double _entryButtonRadius = 26; const double _entryButtonFontSize = 15; const double _entryIconSize = 20; const Duration _enterDuration = Duration(milliseconds: 220); -const Duration _segmentDuration = Duration(milliseconds: 160); Widget _brandMarkWidget({ Key? key, @@ -267,45 +260,53 @@ class _AuthPageState extends State { child: IgnorePointer( child: DecoratedBox( key: Key('auth-page-background'), - decoration: BoxDecoration(gradient: AppTheme.pageGradient), - ), - ), - ), - Positioned( - top: -90, - right: -70, - width: _glowSize, - height: _glowSize, - child: IgnorePointer( - child: Container( - key: const Key('auth-background-glow'), decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, colors: [ - AppColors.surfaceTop, - AppColors.surfaceTop.withValues(alpha: 0), + AppColors.followingGlow, + AppColors.followingGlowMid, + AppColors.appBackground, ], + stops: [0, 0.14, 0.3], ), ), ), ), ), - Positioned( - left: -110, - bottom: -90, - width: _glowSize, - height: _glowSize, + // 全屏海报背景:压暗保证文字可读,顶部再叠品牌红光。 + Positioned.fill( + child: IgnorePointer(child: _AuthPosterBackdrop()), + ), + const Positioned.fill( child: IgnorePointer( - child: Container( - key: const Key('auth-background-warm-glow'), + child: DecoratedBox( + key: Key('auth-poster-scrim'), decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, colors: [ - AppColors.gold.withValues(alpha: _warmGlowOpacity), - AppColors.gold.withValues(alpha: 0), + Color(0x66000000), + Color(0x14000000), + Color(0xE0000000), ], + stops: [0, 0.42, 1], + ), + ), + ), + ), + ), + const Positioned.fill( + child: IgnorePointer( + child: DecoratedBox( + key: Key('auth-red-glow'), + decoration: BoxDecoration( + gradient: RadialGradient( + center: Alignment(0, -1.15), + radius: 1.3, + colors: [AppColors.primaryBorder, AppColors.transparent], ), ), ), @@ -347,7 +348,11 @@ class _AuthPageState extends State { constraints: const BoxConstraints( maxWidth: _contentMaxWidth, ), - child: _content(l10n, compact: compact), + child: _content( + l10n, + compact: compact, + minHeight: minHeight, + ), ), ), ), @@ -387,7 +392,11 @@ class _AuthPageState extends State { ); } - Widget _content(AppLocalizations l10n, {required bool compact}) => + Widget _content( + AppLocalizations l10n, { + required bool compact, + required double minHeight, + }) => TweenAnimationBuilder( key: const Key('auth-enter-animation'), tween: Tween(begin: 0, end: 1), @@ -409,15 +418,18 @@ class _AuthPageState extends State { _authCard(l10n), ], ) - : _landing(l10n), + : _landing(l10n, minHeight: minHeight), ); - Widget _landing(AppLocalizations l10n) => Column( + /// 落地页内容需要有限高度才能用 Spacer 三段分割,这里显式钉住可用高度。 + Widget _landing(AppLocalizations l10n, {required double minHeight}) => + SizedBox( + height: minHeight, + child: Column( key: const Key('auth-landing'), - mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const SizedBox(height: _landingLogoTopGap), + const Spacer(flex: 5), Center( child: _brandMarkWidget( key: const Key('auth-brand-logo'), @@ -425,24 +437,19 @@ class _AuthPageState extends State { radius: _brandMarkRadius, ), ), - const SizedBox(height: 16), + const SizedBox(height: 10), Text( l10n.fruitShortsBrand, textAlign: TextAlign.center, style: const TextStyle( fontFamily: 'Outfit', color: AppColors.onSurface, - fontSize: 24, - fontWeight: FontWeight.w900, + fontSize: 26, + fontWeight: FontWeight.w700, + letterSpacing: 2, ), ), - const SizedBox(height: 6), - Text( - l10n.authLandingSubtitle, - textAlign: TextAlign.center, - style: const TextStyle(color: AppColors.muted, fontSize: 13), - ), - const SizedBox(height: 28), + const Spacer(flex: 3), _AuthEntryButton( key: const Key('auth-entry-phone'), label: l10n.authPhoneSignIn, @@ -458,10 +465,12 @@ class _AuthPageState extends State { primary: false, onTap: () => setState(() => _emailEntryOpen = true), ), - const SizedBox(height: 18), - const AuthLegalLinks(), - ], - ); + const SizedBox(height: 14), + const Opacity(opacity: 0.75, child: AuthLegalLinks()), + const Spacer(flex: 6), + ], + ), + ); Future _openPhoneSheet() async { final signedIn = await showPhoneAuthSheet( @@ -526,15 +535,15 @@ class _AuthEntryButton extends StatelessWidget { @override Widget build(BuildContext context) { - final background = primary ? AppColors.blue : AppColors.card; + // 主按钮纯品牌红;次按钮为无边框深色玻璃(白 6%),Apple 风格。 + final background = primary + ? AppColors.brandRed + : Colors.white.withValues(alpha: 0.06); final foreground = primary ? AppColors.onPrimary : AppColors.onSurface; return Material( color: background, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(_entryButtonRadius), - side: primary - ? BorderSide.none - : const BorderSide(color: AppColors.border), ), clipBehavior: Clip.antiAlias, child: InkWell( @@ -556,7 +565,7 @@ class _AuthEntryButton extends StatelessWidget { style: TextStyle( color: foreground, fontSize: _entryButtonFontSize, - fontWeight: FontWeight.w800, + fontWeight: FontWeight.w600, ), ), ), @@ -567,3 +576,18 @@ class _AuthEntryButton extends StatelessWidget { ); } } + +/// 登录页全屏海报背景(单张,无拼缝)。 +class _AuthPosterBackdrop extends StatelessWidget { + const _AuthPosterBackdrop(); + + @override + Widget build(BuildContext context) { + return Image.asset( + key: const Key('auth-poster-backdrop'), + 'assets/auth/bg_3.jpg', + fit: BoxFit.cover, + alignment: Alignment.center, + ); + } +} diff --git a/lib/pages/auth/auth_widgets.dart b/lib/pages/auth/auth_widgets.dart index aaf377a..6630b55 100644 --- a/lib/pages/auth/auth_widgets.dart +++ b/lib/pages/auth/auth_widgets.dart @@ -70,52 +70,45 @@ class _AuthCard extends StatelessWidget { Widget build(BuildContext context) { final l10n = state.l10n; final status = state.status; - return Container( + // 不再做卡片盒子,表单直接排在红光晕背景上。 + return Column( key: const Key('auth-card'), - padding: const EdgeInsets.all(_cardPadding), - decoration: BoxDecoration( - color: AppColors.card, - borderRadius: BorderRadius.circular(_cardRadius), - border: Border.all(color: AppColors.border), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _AuthModeSwitch( - l10n: l10n, - signUpMode: status.signUpMode, - busy: status.busy, - onSelected: callbacks.onModeSelected, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _AuthModeSwitch( + l10n: l10n, + signUpMode: status.signUpMode, + busy: status.busy, + onSelected: callbacks.onModeSelected, + ), + const SizedBox(height: 24), + Text( + status.signUpMode ? l10n.authSignUpTitle : l10n.authSignInTitle, + style: const TextStyle( + color: AppColors.onSurface, + fontSize: 24, + fontWeight: FontWeight.w600, + height: 1.2, ), - const SizedBox(height: 22), - Text( - status.signUpMode ? l10n.authSignUpTitle : l10n.authSignInTitle, - style: const TextStyle( - color: AppColors.onSurface, - fontSize: 25, - fontWeight: FontWeight.w800, - height: 1.2, - ), + ), + const SizedBox(height: 6), + Text( + status.signUpMode ? l10n.authSignUpSubtitle : l10n.authSubtitle, + style: const TextStyle( + color: AppColors.muted, + fontSize: AppDimens.body, + height: 1.45, ), - const SizedBox(height: 6), - Text( - status.signUpMode ? l10n.authSignUpSubtitle : l10n.authSubtitle, - style: const TextStyle( - color: AppColors.muted, - fontSize: AppDimens.body, - height: 1.45, - ), - ), - const SizedBox(height: 22), - _AuthFields(state: state, callbacks: callbacks), - if (status.errorMessage != null) ...[ - const SizedBox(height: 14), - _AuthError(message: status.errorMessage!), - ], - const SizedBox(height: 22), - _AuthSubmitButton(state: state, onSubmit: callbacks.onSubmit), + ), + const SizedBox(height: 20), + _AuthFields(state: state, callbacks: callbacks), + if (status.errorMessage != null) ...[ + const SizedBox(height: 14), + _AuthError(message: status.errorMessage!), ], - ), + const SizedBox(height: 22), + _AuthSubmitButton(state: state, onSubmit: callbacks.onSubmit), + ], ); } } @@ -135,47 +128,23 @@ class _AuthModeSwitch extends StatelessWidget { @override Widget build(BuildContext context) { - return TextButton( + return Row( key: const Key('auth-switch-mode'), - onPressed: busy ? null : () => onSelected(!signUpMode), - style: TextButton.styleFrom( - minimumSize: const Size(0, _minTapTarget), - padding: EdgeInsets.zero, - overlayColor: AppColors.transparent, - splashFactory: NoSplash.splashFactory, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(22)), - ), - child: Container( - height: 36, - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - color: AppColors.raised, - borderRadius: BorderRadius.circular(18), + children: [ + _ModeTab( + segmentKey: const Key('auth-mode-sign-in'), + label: l10n.signIn, + selected: !signUpMode, + onTap: busy ? null : () => onSelected(false), ), - child: Row( - children: [ - Expanded( - child: _ModeSegment( - segmentKey: const Key('auth-mode-sign-in'), - label: l10n.signIn, - hiddenLabel: l10n.authSwitchToSignIn, - selected: !signUpMode, - onTap: busy ? null : () => onSelected(false), - ), - ), - const SizedBox(width: 4), - Expanded( - child: _ModeSegment( - segmentKey: const Key('auth-mode-sign-up'), - label: l10n.signUp, - hiddenLabel: l10n.authSwitchToSignUp, - selected: signUpMode, - onTap: busy ? null : () => onSelected(true), - ), - ), - ], + const SizedBox(width: AppDimens.followingSegmentGap), + _ModeTab( + segmentKey: const Key('auth-mode-sign-up'), + label: l10n.signUp, + selected: signUpMode, + onTap: busy ? null : () => onSelected(true), ), - ), + ], ); } } diff --git a/lib/pages/auth/phone_auth_sheet.dart b/lib/pages/auth/phone_auth_sheet.dart index 9fe651b..5b7b5dd 100644 --- a/lib/pages/auth/phone_auth_sheet.dart +++ b/lib/pages/auth/phone_auth_sheet.dart @@ -11,6 +11,7 @@ import '../../core/data/services/auth_controller.dart'; import '../../core/platform/haptics.dart'; import '../../core/platform/keyboard_inset.dart'; import '../../core/theme/app_colors.dart'; +import '../../core/widgets/app_glow_panel.dart'; import '../../core/widgets/centered_toast.dart'; import '../../l10n/app_localizations.dart'; @@ -252,15 +253,15 @@ class _PhoneAuthSheetState extends State { final bottom = effectiveKeyboardInset(context, _keyboardInset); return Padding( padding: EdgeInsets.only(bottom: bottom), - child: Container( - key: const Key('phone-auth-sheet'), - decoration: const BoxDecoration( - color: AppColors.card, - borderRadius: BorderRadius.vertical( - top: Radius.circular(_sheetRadius), - ), + child: AppGlowPanel( + borderRadius: const BorderRadius.vertical( + top: Radius.circular(_sheetRadius), ), - child: SingleChildScrollView( + border: false, + child: Container( + key: const Key('phone-auth-sheet'), + decoration: const BoxDecoration(color: AppColors.transparent), + child: SingleChildScrollView( controller: _scrollController, padding: const EdgeInsets.fromLTRB(22, 10, 22, 26), child: Column( @@ -313,6 +314,7 @@ class _PhoneAuthSheetState extends State { ], ), ), + ), ), ); } @@ -403,7 +405,7 @@ class _PhoneAuthSheetState extends State { key: const Key('auth-phone-submit'), onPressed: _submitting ? null : _submit, style: FilledButton.styleFrom( - backgroundColor: AppColors.blue, + backgroundColor: AppColors.brandRed, foregroundColor: AppColors.onPrimary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(_buttonHeight / 2), @@ -439,7 +441,7 @@ class _PhoneAuthSheetState extends State { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(_fieldRadius), - borderSide: const BorderSide(color: AppColors.blue, width: 1.5), + borderSide: const BorderSide(color: AppColors.brandRed, width: 1.5), ), ); } diff --git a/lib/pages/downloads/downloads_page.dart b/lib/pages/downloads/downloads_page.dart index e6ada06..f268916 100644 --- a/lib/pages/downloads/downloads_page.dart +++ b/lib/pages/downloads/downloads_page.dart @@ -16,6 +16,7 @@ import '../../core/platform/haptics.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_theme.dart'; +import '../../core/widgets/app_glow_panel.dart'; import '../../core/widgets/app_pull_to_refresh.dart'; import '../../core/widgets/auth_gate.dart'; import '../../core/widgets/empty_state.dart'; @@ -181,15 +182,15 @@ class _DownloadsBody extends StatelessWidget { final l10n = AppLocalizations.of(context)!; final confirmed = await showDialog( context: context, - builder: (dialogContext) => AlertDialog( - title: Text(l10n.clearDownloadsTitle), - content: Text(l10n.clearDownloadsMessage), + builder: (dialogContext) => AppGlowDialog( + title: l10n.clearDownloadsTitle, + content: l10n.clearDownloadsMessage, actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), child: Text(l10n.cancel), ), - FilledButton( + TextButton( onPressed: () => Navigator.of(dialogContext).pop(true), child: Text(l10n.confirmClear), ), @@ -435,7 +436,7 @@ class _LicenseTile extends StatelessWidget { minHeight: AppDimens.progressHeight, value: progress.clamp(0.0, 1.0), backgroundColor: AppColors.border, - valueColor: const AlwaysStoppedAnimation(AppColors.blueSoft), + valueColor: const AlwaysStoppedAnimation(AppColors.brandRed), ), ), if (license.expireAt != null) ...[ diff --git a/lib/pages/interaction/widgets/report_dialog.dart b/lib/pages/interaction/widgets/report_dialog.dart index e6fae25..5ab21cc 100644 --- a/lib/pages/interaction/widgets/report_dialog.dart +++ b/lib/pages/interaction/widgets/report_dialog.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../../../core/data/models/interaction.dart'; +import '../../../core/widgets/app_glow_panel.dart'; import '../../../l10n/app_localizations.dart'; const int _maxDescriptionLength = 300; @@ -67,9 +68,9 @@ class _ReportDialogState extends State<_ReportDialog> { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - return AlertDialog( - title: Text(widget.title), - content: SingleChildScrollView( + return AppGlowDialog( + title: widget.title, + contentChild: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/pages/notices/notices_page.dart b/lib/pages/notices/notices_page.dart index f564a03..d347132 100644 --- a/lib/pages/notices/notices_page.dart +++ b/lib/pages/notices/notices_page.dart @@ -13,6 +13,7 @@ import '../../core/formatting/localized_date_time.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_theme.dart'; +import '../../core/widgets/app_glow_panel.dart'; import '../../core/widgets/app_pull_to_refresh.dart'; import '../../core/widgets/empty_state.dart'; import '../../core/widgets/plain_page_scaffold.dart'; @@ -191,9 +192,9 @@ class _NoticesPageState extends State { : formatLocalizedDateTime(context, notice.publishAt!); return showDialog( context: context, - builder: (dialogContext) => AlertDialog( - title: Text(notice.title), - content: SizedBox( + builder: (dialogContext) => AppGlowDialog( + title: notice.title, + contentChild: SizedBox( width: double.maxFinite, child: SingleChildScrollView( child: Column( diff --git a/lib/pages/player/widgets/drama_comments_sheet.dart b/lib/pages/player/widgets/drama_comments_sheet.dart index 4e7204e..5425178 100644 --- a/lib/pages/player/widgets/drama_comments_sheet.dart +++ b/lib/pages/player/widgets/drama_comments_sheet.dart @@ -14,6 +14,7 @@ import '../../../core/data/repositories/interaction_repository.dart'; import '../../../core/data/services/comment_controller.dart'; import '../../../core/formatting/error_text.dart'; import '../../../core/theme/app_colors.dart'; +import '../../../core/widgets/app_glow_panel.dart'; import '../../../core/widgets/empty_state.dart'; import '../../../l10n/app_localizations.dart'; import 'comment_composer_sheet.dart'; @@ -176,14 +177,14 @@ class _DramaCommentsSheetState extends State { child: SizedBox( key: const Key('comment-sheet-constraints'), height: sheetHeight, - child: Container( - decoration: const BoxDecoration( - color: AppColors.card, - borderRadius: BorderRadius.vertical( - top: Radius.circular(_sheetRadius), - ), + child: AppGlowPanel( + borderRadius: const BorderRadius.vertical( + top: Radius.circular(_sheetRadius), ), - child: SafeArea( + border: false, + child: Container( + decoration: const BoxDecoration(color: AppColors.transparent), + child: SafeArea( top: false, child: ListenableBuilder( listenable: _controller, @@ -193,6 +194,7 @@ class _DramaCommentsSheetState extends State { child: _content(context), ); }, + ), ), ), ), @@ -238,7 +240,7 @@ class _DramaCommentsSheetState extends State { style: const TextStyle( color: AppColors.onSurface, fontSize: 16, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w400, ), ), Positioned( diff --git a/lib/pages/player/widgets/episode_picker_sheet.dart b/lib/pages/player/widgets/episode_picker_sheet.dart index df4b9b6..2a4fd39 100644 --- a/lib/pages/player/widgets/episode_picker_sheet.dart +++ b/lib/pages/player/widgets/episode_picker_sheet.dart @@ -7,6 +7,7 @@ import '../../../core/data/models/drama.dart'; import '../../../core/formatting/localized_content.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_dimens.dart'; +import '../../../core/widgets/app_glow_panel.dart'; import '../../../l10n/app_localizations.dart'; import 'episode_picker_cell.dart'; @@ -47,16 +48,16 @@ class EpisodePickerSheet extends StatelessWidget { systemNavigationBarIconBrightness: Brightness.light, systemNavigationBarContrastEnforced: false, ), - child: Container( - key: const Key('player-episodes-sheet'), - height: MediaQuery.sizeOf(context).height * _sheetHeightFactor, - decoration: const BoxDecoration( - color: AppColors.card, - borderRadius: BorderRadius.vertical( - top: Radius.circular(_sheetRadius), - ), + child: AppGlowPanel( + borderRadius: const BorderRadius.vertical( + top: Radius.circular(_sheetRadius), ), - child: SafeArea( + border: false, + child: Container( + key: const Key('player-episodes-sheet'), + height: MediaQuery.sizeOf(context).height * _sheetHeightFactor, + decoration: const BoxDecoration(color: AppColors.transparent), + child: SafeArea( top: false, bottom: true, child: Column( @@ -139,6 +140,7 @@ class EpisodePickerSheet extends StatelessWidget { ], ), ), + ), ), ); } diff --git a/lib/pages/player/widgets/player_more_sheet.dart b/lib/pages/player/widgets/player_more_sheet.dart index f6fbdad..033da56 100644 --- a/lib/pages/player/widgets/player_more_sheet.dart +++ b/lib/pages/player/widgets/player_more_sheet.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../../../core/theme/app_colors.dart'; +import '../../../core/widgets/app_glow_panel.dart'; import '../../../l10n/app_localizations.dart'; const double _sheetRadius = 20; @@ -52,6 +53,8 @@ class PlayerMoreActions { required this.onDownload, required this.onQualityChanged, this.onPip, + this.autoPipEnabled = false, + this.onAutoPipChanged, }); /// 倍速变化。 @@ -68,6 +71,12 @@ class PlayerMoreActions { /// 进入小窗播放;平台不支持时为 null。 final VoidCallback? onPip; + + /// 退出 App 自动小窗开关的当前状态(仅支持自动小窗的平台展示开关行)。 + final bool autoPipEnabled; + + /// 切换退出 App 自动小窗;为 null 时隐藏开关行。 + final ValueChanged? onAutoPipChanged; } /// K29.6 精简版播放页更多面板。 @@ -96,16 +105,19 @@ class PlayerMoreSheet extends StatefulWidget { class _PlayerMoreSheetState extends State { late double _speed = widget.state.speed; late String _quality = widget.state.selectedQuality.toUpperCase(); + late bool _autoPip = widget.actions.autoPipEnabled; @override Widget build(BuildContext context) { final l10n = widget.l10n; - return Material( - key: const Key('player-more-sheet'), - color: AppColors.ink, + return AppGlowPanel( borderRadius: const BorderRadius.vertical( top: Radius.circular(_sheetRadius), ), + border: false, + child: Material( + key: const Key('player-more-sheet'), + type: MaterialType.transparency, clipBehavior: Clip.antiAlias, child: SafeArea( top: false, @@ -142,6 +154,17 @@ class _PlayerMoreSheetState extends State { title: l10n.pictureInPicture, onTap: () => _closeThen(widget.actions.onPip!), ), + if (widget.actions.onAutoPipChanged != null) + _switchRow( + key: const Key('player-more-auto-pip'), + icon: Icons.auto_awesome_motion_outlined, + title: l10n.autoEnterPip, + value: _autoPip, + onChanged: (value) { + setState(() => _autoPip = value); + widget.actions.onAutoPipChanged!(value); + }, + ), _actionRow( key: const Key('player-more-offline'), icon: Icons.download_outlined, @@ -153,6 +176,7 @@ class _PlayerMoreSheetState extends State { ), ), ), + ), ); } @@ -160,7 +184,7 @@ class _PlayerMoreSheetState extends State { width: double.infinity, padding: const EdgeInsets.symmetric(vertical: _groupVerticalPadding), decoration: BoxDecoration( - color: AppColors.card, + color: Colors.white.withValues(alpha: 0.06), borderRadius: BorderRadius.circular(_groupRadius), ), child: Column(mainAxisSize: MainAxisSize.min, children: rows), @@ -241,7 +265,7 @@ class _PlayerMoreSheetState extends State { horizontal: _segmentTrackPadding, ), decoration: BoxDecoration( - color: AppColors.raised, + color: Colors.white.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(_segmentTrackRadius), ), child: Row( @@ -286,6 +310,54 @@ class _PlayerMoreSheetState extends State { ); } + /// 开关行:左侧图标+标题,右侧主题色 Switch。 + Widget _switchRow({ + required Key key, + required IconData icon, + required String title, + required bool value, + required ValueChanged onChanged, + }) { + return SizedBox( + key: key, + height: _rowHeight, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: _rowHorizontalPadding), + child: Row( + children: [ + Icon(icon, color: AppColors.onSurface, size: _rowIconSize), + const SizedBox(width: _rowIconGap), + Expanded( + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.onSurface, + fontSize: _rowLabelSize, + fontWeight: FontWeight.w500, + ), + ), + ), + Transform.scale( + scale: 0.7, + child: Switch( + key: const Key('player-more-auto-pip-switch'), + value: value, + activeThumbColor: AppColors.onSurface, + activeTrackColor: AppColors.brandRed, + inactiveThumbColor: AppColors.textSecondary, + inactiveTrackColor: AppColors.contentBorder, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onChanged: onChanged, + ), + ), + ], + ), + ), + ); + } + Widget _actionRow({ required Key key, required IconData icon, @@ -380,7 +452,9 @@ class _SegmentButton extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: _segmentPadding), alignment: Alignment.center, decoration: BoxDecoration( - color: selected ? AppColors.card : AppColors.transparent, + color: selected + ? Colors.white.withValues(alpha: 0.16) + : AppColors.transparent, borderRadius: BorderRadius.circular(_segmentRadius), boxShadow: selected ? const [ diff --git a/lib/pages/player/widgets/player_speed_sheet.dart b/lib/pages/player/widgets/player_speed_sheet.dart index 475ac5c..e881ce1 100644 --- a/lib/pages/player/widgets/player_speed_sheet.dart +++ b/lib/pages/player/widgets/player_speed_sheet.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../../../core/theme/app_colors.dart'; +import '../../../core/widgets/app_glow_panel.dart'; import '../../../l10n/app_localizations.dart'; const List _speedOptions = [0.75, 1.0, 1.25, 1.5, 2.0, 3.0]; @@ -19,14 +20,16 @@ class PlayerSpeedSheet extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - return Material( - key: const Key('player-speed-sheet'), - color: AppColors.card, + return AppGlowPanel( borderRadius: const BorderRadius.vertical( top: Radius.circular(_sheetRadius), ), - clipBehavior: Clip.antiAlias, - child: SafeArea( + border: false, + child: Material( + key: const Key('player-speed-sheet'), + type: MaterialType.transparency, + clipBehavior: Clip.antiAlias, + child: SafeArea( top: false, child: Column( mainAxisSize: MainAxisSize.min, @@ -50,6 +53,7 @@ class PlayerSpeedSheet extends StatelessWidget { ], ), ), + ), ); } diff --git a/lib/pages/player/widgets/player_unlock_sheet.dart b/lib/pages/player/widgets/player_unlock_sheet.dart index 8b2a9c1..55de30d 100644 --- a/lib/pages/player/widgets/player_unlock_sheet.dart +++ b/lib/pages/player/widgets/player_unlock_sheet.dart @@ -6,6 +6,7 @@ import '../../../core/data/models/drama.dart'; import '../../../core/data/models/play_ticket.dart'; import '../../../core/formatting/error_text.dart'; import '../../../core/theme/app_colors.dart'; +import '../../../core/widgets/app_glow_panel.dart'; import '../../../l10n/app_localizations.dart'; const double _cardRadius = 20; @@ -209,12 +210,10 @@ class _PlayerUnlockSheetState extends State { @override Widget build(BuildContext context) { final l10n = widget.l10n; - return Container( + return AppGlowPanel( key: const Key('player-unlock-panel'), - decoration: const BoxDecoration( - color: AppColors.card, - borderRadius: BorderRadius.vertical(top: Radius.circular(_cardRadius)), - ), + borderRadius: const BorderRadius.vertical(top: Radius.circular(_cardRadius)), + border: false, child: SafeArea( top: false, child: Padding( diff --git a/lib/pages/watch_records/watch_records_page.dart b/lib/pages/watch_records/watch_records_page.dart index 1d8c471..d8e2e7d 100644 --- a/lib/pages/watch_records/watch_records_page.dart +++ b/lib/pages/watch_records/watch_records_page.dart @@ -14,6 +14,7 @@ import '../../core/formatting/error_text.dart'; import '../../core/platform/haptics.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_theme.dart'; +import '../../core/widgets/app_glow_panel.dart'; import '../../core/widgets/app_pull_to_refresh.dart'; import '../../core/widgets/empty_state.dart'; import '../../core/widgets/paged_list_footer.dart'; @@ -255,15 +256,15 @@ class _WatchRecordsBodyState extends State<_WatchRecordsBody> { final l10n = AppLocalizations.of(context)!; final confirmed = await showDialog( context: context, - builder: (dialogContext) => AlertDialog( - title: Text(l10n.clearWatchHistoryTitle), - content: Text(l10n.clearWatchHistoryMessage), + builder: (dialogContext) => AppGlowDialog( + title: l10n.clearWatchHistoryTitle, + content: l10n.clearWatchHistoryMessage, actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), child: Text(l10n.cancel), ), - FilledButton( + TextButton( onPressed: () => Navigator.of(dialogContext).pop(true), child: Text(l10n.confirmClear), ),