feat(ui): 登录页重设计并统一弹框主题渐变外壳

This commit is contained in:
Vicky authored and trae-solo-agent committed 2026-10-28 16:36:43 +08:00
1 parent c027acc91f
commit de7da64886
25 files changed
+905 -317

No files matched your search

+119
View File
@@ -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<Widget>? 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],
],
],
),
],
],
),
),
),
);
}
}
+7 -5
View File
@@ -5,6 +5,7 @@ import '../../l10n/app_localizations.dart';
import '../../pages/auth/auth_view.dart'; import '../../pages/auth/auth_view.dart';
import '../data/services/auth_controller.dart'; import '../data/services/auth_controller.dart';
import '../theme/app_colors.dart'; import '../theme/app_colors.dart';
import 'app_glow_panel.dart';
/// 打开登录 / 注册页(沿用现有 Navigator 风格,不引路由库) /// 打开登录 / 注册页(沿用现有 Navigator 风格,不引路由库)
Future<void> openAuthPage(BuildContext context, {bool signUpMode = false}) { Future<void> openAuthPage(BuildContext context, {bool signUpMode = false}) {
@@ -23,11 +24,11 @@ Future<bool> ensureSignedIn(BuildContext context) async {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final go = await showModalBottomSheet<bool>( final go = await showModalBottomSheet<bool>(
context: context, context: context,
backgroundColor: AppColors.card, backgroundColor: AppColors.transparent,
shape: const RoundedRectangleBorder( builder: (sheetContext) => AppGlowPanel(
borderRadius: BorderRadius.vertical(top: Radius.circular(18)), borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
), border: false,
builder: (sheetContext) => SafeArea( child: SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(20, 18, 20, 20), padding: const EdgeInsets.fromLTRB(20, 18, 20, 20),
child: Column( child: Column(
@@ -71,6 +72,7 @@ Future<bool> ensureSignedIn(BuildContext context) async {
], ],
), ),
), ),
),
), ),
); );
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import '../config/app_config.dart'; import '../config/app_config.dart';
import '../config/server_endpoint.dart'; import '../config/server_endpoint.dart';
import '../theme/app_colors.dart'; import '../theme/app_colors.dart';
import 'app_glow_panel.dart';
const String _title = '设置服务地址'; const String _title = '设置服务地址';
const String _save = '保存'; const String _save = '保存';
@@ -72,9 +73,9 @@ class _CatalogEndpointDialogState extends State<_CatalogEndpointDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AppGlowDialog(
title: const Text(_title), title: _title,
content: SingleChildScrollView( contentChild: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
+49 -4
View File
@@ -20,7 +20,6 @@
"featured": "Featured", "featured": "Featured",
"recommended": "Recommended", "recommended": "Recommended",
"fruitShortsBrand": "FruitShorts", "fruitShortsBrand": "FruitShorts",
"authLandingSubtitle": "Sign in to sync progress, favorites and coins",
"comingSoon": "Coming soon", "comingSoon": "Coming soon",
"authLegal": "By continuing, you agree to the Terms of Service and Privacy Policy.", "authLegal": "By continuing, you agree to the Terms of Service and Privacy Policy.",
"authLegalLead": "By continuing, you agree to the", "authLegalLead": "By continuing, you agree to the",
@@ -61,7 +60,7 @@
"episodes": {"type": "String"} "episodes": {"type": "String"}
} }
}, },
"feedEpisodePrefix": "EP{episode} | ", "feedEpisodePrefix": "EP{episode}",
"@feedEpisodePrefix": { "@feedEpisodePrefix": {
"placeholders": { "placeholders": {
"episode": {"type": "int"} "episode": {"type": "int"}
@@ -275,6 +274,7 @@
"shareFailed": "Could not open the share sheet.", "shareFailed": "Could not open the share sheet.",
"nowPlaying": "Playing now", "nowPlaying": "Playing now",
"pictureInPicture": "Picture in picture", "pictureInPicture": "Picture in picture",
"autoEnterPip": "Auto mini player on exit",
"pipUnavailable": "Picture in picture is unavailable on this device", "pipUnavailable": "Picture in picture is unavailable on this device",
"originalTag": "Original", "originalTag": "Original",
"originalProduction": "Original", "originalProduction": "Original",
@@ -442,6 +442,7 @@
"restoreDefault": "Restore default", "restoreDefault": "Restore default",
"invalidServerAddress": "Enter a valid http(s) server address.", "invalidServerAddress": "Enter a valid http(s) server address.",
"signOut": "Sign Out", "signOut": "Sign Out",
"signOutConfirm": "Sign out of your current account?",
"mockUserName": "Vicky sheng", "mockUserName": "Vicky sheng",
"mockUserId": "ID: M1CZWVYE", "mockUserId": "ID: M1CZWVYE",
"mockNotice": "Sample data only.", "mockNotice": "Sample data only.",
@@ -787,8 +788,14 @@
"offlineDownloads": "Offline downloads", "offlineDownloads": "Offline downloads",
"watchPreferences": "Watch preferences", "watchPreferences": "Watch preferences",
"contactSupport": "Contact support", "contactSupport": "Contact support",
"vipBrandName": "Netshort VIP", "vipBrandName": "FruitShorts VIP",
"mineVipSubtitle": "Enjoy exclusive benefits", "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": "Episode {episode} / {total} total",
"@favoriteEpisodeProgress": { "@favoriteEpisodeProgress": {
"placeholders": { "placeholders": {
@@ -796,6 +803,44 @@
"total": { "type": "int" } "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": "Watched to Episode {episode}",
"@watchedToEpisode": { "placeholders": { "episode": { "type": "int" } } }, "@watchedToEpisode": { "placeholders": { "episode": { "type": "int" } } },
"homeWatchAll": "Watch all", "homeWatchAll": "Watch all",
+49 -4
View File
@@ -20,7 +20,6 @@
"featured": "Unggulan", "featured": "Unggulan",
"recommended": "Rekomendasi", "recommended": "Rekomendasi",
"fruitShortsBrand": "FruitShorts", "fruitShortsBrand": "FruitShorts",
"authLandingSubtitle": "Masuk untuk menyinkronkan progres, favorit, dan koin",
"comingSoon": "Segera hadir", "comingSoon": "Segera hadir",
"authLegal": "Dengan melanjutkan, Anda menyetujui Ketentuan Layanan dan Kebijakan Privasi.", "authLegal": "Dengan melanjutkan, Anda menyetujui Ketentuan Layanan dan Kebijakan Privasi.",
"authLegalLead": "Dengan melanjutkan, Anda menyetujui", "authLegalLead": "Dengan melanjutkan, Anda menyetujui",
@@ -55,7 +54,7 @@
"episodes": {"type": "String"} "episodes": {"type": "String"}
} }
}, },
"feedEpisodePrefix": "EP{episode} | ", "feedEpisodePrefix": "EP{episode}",
"@feedEpisodePrefix": { "@feedEpisodePrefix": {
"placeholders": { "placeholders": {
"episode": {"type": "int"} "episode": {"type": "int"}
@@ -254,6 +253,7 @@
"shareFailed": "Tidak bisa membuka menu berbagi.", "shareFailed": "Tidak bisa membuka menu berbagi.",
"nowPlaying": "Sedang diputar", "nowPlaying": "Sedang diputar",
"pictureInPicture": "Gambar dalam gambar", "pictureInPicture": "Gambar dalam gambar",
"autoEnterPip": "Mini player otomatis saat keluar",
"pipUnavailable": "Gambar dalam gambar tidak tersedia di perangkat ini", "pipUnavailable": "Gambar dalam gambar tidak tersedia di perangkat ini",
"originalTag": "Original", "originalTag": "Original",
"originalProduction": "Original", "originalProduction": "Original",
@@ -421,6 +421,7 @@
"restoreDefault": "Pulihkan default", "restoreDefault": "Pulihkan default",
"invalidServerAddress": "Masukkan alamat server http(s) yang valid.", "invalidServerAddress": "Masukkan alamat server http(s) yang valid.",
"signOut": "Keluar", "signOut": "Keluar",
"signOutConfirm": "Keluar dari akun saat ini?",
"mockUserName": "Vicky sheng", "mockUserName": "Vicky sheng",
"mockUserId": "ID: M1CZWVYE", "mockUserId": "ID: M1CZWVYE",
"mockNotice": "Hanya data contoh.", "mockNotice": "Hanya data contoh.",
@@ -749,8 +750,14 @@
"offlineDownloads": "Unduhan offline", "offlineDownloads": "Unduhan offline",
"watchPreferences": "Preferensi tontonan", "watchPreferences": "Preferensi tontonan",
"contactSupport": "Hubungi dukungan", "contactSupport": "Hubungi dukungan",
"vipBrandName": "Netshort VIP", "vipBrandName": "FruitShorts VIP",
"mineVipSubtitle": "Nikmati manfaat eksklusif", "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": "Episode {episode} / total {total}",
"@favoriteEpisodeProgress": { "@favoriteEpisodeProgress": {
"placeholders": { "placeholders": {
@@ -758,6 +765,44 @@
"total": { "type": "int" } "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": "Ditonton hingga Episode {episode}",
"@watchedToEpisode": { "placeholders": { "episode": { "type": "int" } } }, "@watchedToEpisode": { "placeholders": { "episode": { "type": "int" } } },
"homeWatchAll": "Tonton semua", "homeWatchAll": "Tonton semua",
+106 -10
View File
@@ -220,12 +220,6 @@ abstract class AppLocalizations {
/// **'FruitShorts'** /// **'FruitShorts'**
String get fruitShortsBrand; 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. /// No description provided for @comingSoon.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -403,7 +397,7 @@ abstract class AppLocalizations {
/// No description provided for @feedEpisodePrefix. /// No description provided for @feedEpisodePrefix.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'EP{episode} | '** /// **'EP{episode}'**
String feedEpisodePrefix(int episode); String feedEpisodePrefix(int episode);
/// No description provided for @feedWatchingCount. /// No description provided for @feedWatchingCount.
@@ -1198,6 +1192,12 @@ abstract class AppLocalizations {
/// **'Picture in picture'** /// **'Picture in picture'**
String get pictureInPicture; 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. /// No description provided for @pipUnavailable.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -1828,6 +1828,12 @@ abstract class AppLocalizations {
/// **'Sign Out'** /// **'Sign Out'**
String get signOut; 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. /// No description provided for @mockUserName.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -2125,7 +2131,7 @@ abstract class AppLocalizations {
/// No description provided for @myFavorites. /// No description provided for @myFavorites.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'My favorites'** /// **'My Favorites'**
String get myFavorites; String get myFavorites;
/// No description provided for @noFavorites. /// No description provided for @noFavorites.
@@ -3427,13 +3433,49 @@ abstract class AppLocalizations {
/// No description provided for @vipBrandName. /// No description provided for @vipBrandName.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Netshort VIP'** /// **'FruitShorts VIP'**
String get vipBrandName; 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. /// No description provided for @mineVipSubtitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Enjoy exclusive benefits'** /// **'Unlock excitement, binge freely'**
String get mineVipSubtitle; String get mineVipSubtitle;
/// No description provided for @favoriteEpisodeProgress. /// No description provided for @favoriteEpisodeProgress.
@@ -3442,6 +3484,60 @@ abstract class AppLocalizations {
/// **'Episode {episode} / {total} total'** /// **'Episode {episode} / {total} total'**
String favoriteEpisodeProgress(int episode, int 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. /// No description provided for @watchedToEpisode.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
+65 -8
View File
@@ -69,10 +69,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get fruitShortsBrand => 'FruitShorts'; String get fruitShortsBrand => 'FruitShorts';
@override
String get authLandingSubtitle =>
'Sign in to sync progress, favorites and coins';
@override @override
String get comingSoon => 'Coming soon'; String get comingSoon => 'Coming soon';
@@ -167,7 +163,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String feedEpisodePrefix(int episode) { String feedEpisodePrefix(int episode) {
return 'EP$episode | '; return 'EP$episode';
} }
@override @override
@@ -597,6 +593,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get pictureInPicture => 'Picture in picture'; String get pictureInPicture => 'Picture in picture';
@override
String get autoEnterPip => 'Auto mini player on exit';
@override @override
String get pipUnavailable => String get pipUnavailable =>
'Picture in picture is unavailable on this device'; 'Picture in picture is unavailable on this device';
@@ -933,6 +932,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get signOut => 'Sign Out'; String get signOut => 'Sign Out';
@override
String get signOutConfirm => 'Sign out of your current account?';
@override @override
String get mockUserName => 'Vicky sheng'; String get mockUserName => 'Vicky sheng';
@@ -1087,7 +1089,7 @@ class AppLocalizationsEn extends AppLocalizations {
'Could not update favorites. Please retry.'; 'Could not update favorites. Please retry.';
@override @override
String get myFavorites => 'My favorites'; String get myFavorites => 'My Favorites';
@override @override
String get noFavorites => 'No favorites yet.'; String get noFavorites => 'No favorites yet.';
@@ -1790,16 +1792,71 @@ class AppLocalizationsEn extends AppLocalizations {
String get contactSupport => 'Contact support'; String get contactSupport => 'Contact support';
@override @override
String get vipBrandName => 'Netshort VIP'; String get vipBrandName => 'FruitShorts VIP';
@override @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 @override
String favoriteEpisodeProgress(int episode, int total) { String favoriteEpisodeProgress(int episode, int total) {
return 'Episode $episode / $total 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 @override
String watchedToEpisode(int episode) { String watchedToEpisode(int episode) {
return 'Watched to Episode $episode'; return 'Watched to Episode $episode';
+66 -9
View File
@@ -69,10 +69,6 @@ class AppLocalizationsId extends AppLocalizations {
@override @override
String get fruitShortsBrand => 'FruitShorts'; String get fruitShortsBrand => 'FruitShorts';
@override
String get authLandingSubtitle =>
'Masuk untuk menyinkronkan progres, favorit, dan koin';
@override @override
String get comingSoon => 'Segera hadir'; String get comingSoon => 'Segera hadir';
@@ -167,7 +163,7 @@ class AppLocalizationsId extends AppLocalizations {
@override @override
String feedEpisodePrefix(int episode) { String feedEpisodePrefix(int episode) {
return 'EP$episode | '; return 'EP$episode';
} }
@override @override
@@ -299,7 +295,7 @@ class AppLocalizationsId extends AppLocalizations {
String get localDemoProfile => 'Profil demo lokal'; String get localDemoProfile => 'Profil demo lokal';
@override @override
String get continueWatching => 'Lanjut Tonton'; String get continueWatching => 'Lanjutkan';
@override @override
String episodeRemaining(int episode, int minutes) { String episodeRemaining(int episode, int minutes) {
@@ -598,6 +594,9 @@ class AppLocalizationsId extends AppLocalizations {
@override @override
String get pictureInPicture => 'Gambar dalam gambar'; String get pictureInPicture => 'Gambar dalam gambar';
@override
String get autoEnterPip => 'Mini player otomatis saat keluar';
@override @override
String get pipUnavailable => String get pipUnavailable =>
'Gambar dalam gambar tidak tersedia di perangkat ini'; 'Gambar dalam gambar tidak tersedia di perangkat ini';
@@ -937,6 +936,9 @@ class AppLocalizationsId extends AppLocalizations {
@override @override
String get signOut => 'Keluar'; String get signOut => 'Keluar';
@override
String get signOutConfirm => 'Keluar dari akun saat ini?';
@override @override
String get mockUserName => 'Vicky sheng'; String get mockUserName => 'Vicky sheng';
@@ -1089,7 +1091,7 @@ class AppLocalizationsId extends AppLocalizations {
String get favoriteToggleFailed => 'Gagal memperbarui favorit. Coba lagi.'; String get favoriteToggleFailed => 'Gagal memperbarui favorit. Coba lagi.';
@override @override
String get myFavorites => 'Favorit saya'; String get myFavorites => 'Favorit Saya';
@override @override
String get noFavorites => 'Belum ada favorit.'; String get noFavorites => 'Belum ada favorit.';
@@ -1792,16 +1794,71 @@ class AppLocalizationsId extends AppLocalizations {
String get contactSupport => 'Hubungi dukungan'; String get contactSupport => 'Hubungi dukungan';
@override @override
String get vipBrandName => 'Netshort VIP'; String get vipBrandName => 'FruitShorts VIP';
@override @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 @override
String favoriteEpisodeProgress(int episode, int total) { String favoriteEpisodeProgress(int episode, int total) {
return 'Episode $episode / total $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 @override
String watchedToEpisode(int episode) { String watchedToEpisode(int episode) {
return 'Ditonton hingga Episode $episode'; return 'Ditonton hingga Episode $episode';
+65 -7
View File
@@ -66,10 +66,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get recommended => '推荐'; String get recommended => '推荐';
@override @override
String get fruitShortsBrand => '水果短剧'; String get fruitShortsBrand => 'FruitShorts';
@override
String get authLandingSubtitle => '登录后同步进度、收藏与金币权益';
@override @override
String get comingSoon => '即将上线'; String get comingSoon => '即将上线';
@@ -164,7 +161,7 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String feedEpisodePrefix(int episode) { String feedEpisodePrefix(int episode) {
return '第$episode集|'; return '第$episode集';
} }
@override @override
@@ -591,6 +588,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get pictureInPicture => '画中画'; String get pictureInPicture => '画中画';
@override
String get autoEnterPip => '退出后自动小窗';
@override @override
String get pipUnavailable => '此设备无法使用画中画'; String get pipUnavailable => '此设备无法使用画中画';
@@ -922,6 +922,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get signOut => '退出登录'; String get signOut => '退出登录';
@override
String get signOutConfirm => '确定要退出当前账号吗?';
@override @override
String get mockUserName => 'Vicky sheng'; String get mockUserName => 'Vicky sheng';
@@ -1764,16 +1767,71 @@ class AppLocalizationsZh extends AppLocalizations {
String get contactSupport => '联系客服'; String get contactSupport => '联系客服';
@override @override
String get vipBrandName => 'Netshort VIP'; String get vipBrandName => 'FruitShorts VIP';
@override @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 @override
String favoriteEpisodeProgress(int episode, int total) { String favoriteEpisodeProgress(int episode, int total) {
return '第 $episode 集 / 共 $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 @override
String watchedToEpisode(int episode) { String watchedToEpisode(int episode) {
return '已看至 第 $episode 集'; return '已看至 第 $episode 集';
+50 -5
View File
@@ -19,8 +19,7 @@
"searchCancel": "取消", "searchCancel": "取消",
"featured": "精选", "featured": "精选",
"recommended": "推荐", "recommended": "推荐",
"fruitShortsBrand": "水果短剧", "fruitShortsBrand": "FruitShorts",
"authLandingSubtitle": "登录后同步进度、收藏与金币权益",
"comingSoon": "即将上线", "comingSoon": "即将上线",
"authLegal": "继续即代表同意《用户协议》与《隐私政策》", "authLegal": "继续即代表同意《用户协议》与《隐私政策》",
"authLegalLead": "继续即代表同意", "authLegalLead": "继续即代表同意",
@@ -55,7 +54,7 @@
"episodes": {"type": "String"} "episodes": {"type": "String"}
} }
}, },
"feedEpisodePrefix": "第{episode}集|", "feedEpisodePrefix": "第{episode}集",
"@feedEpisodePrefix": { "@feedEpisodePrefix": {
"placeholders": { "placeholders": {
"episode": {"type": "int"} "episode": {"type": "int"}
@@ -254,6 +253,7 @@
"shareFailed": "无法打开系统分享面板。", "shareFailed": "无法打开系统分享面板。",
"nowPlaying": "正在播放", "nowPlaying": "正在播放",
"pictureInPicture": "画中画", "pictureInPicture": "画中画",
"autoEnterPip": "退出后自动小窗",
"pipUnavailable": "此设备无法使用画中画", "pipUnavailable": "此设备无法使用画中画",
"originalTag": "原创", "originalTag": "原创",
"originalProduction": "原创出品", "originalProduction": "原创出品",
@@ -383,6 +383,7 @@
"restoreDefault": "恢复默认", "restoreDefault": "恢复默认",
"invalidServerAddress": "请输入有效的 http(s) 服务器地址。", "invalidServerAddress": "请输入有效的 http(s) 服务器地址。",
"signOut": "退出登录", "signOut": "退出登录",
"signOutConfirm": "确定要退出当前账号吗?",
"mockUserName": "Vicky sheng", "mockUserName": "Vicky sheng",
"mockUserId": "ID: M1CZWVYE", "mockUserId": "ID: M1CZWVYE",
"mockNotice": "当前是示例数据。", "mockNotice": "当前是示例数据。",
@@ -711,8 +712,14 @@
"offlineDownloads": "离线缓存", "offlineDownloads": "离线缓存",
"watchPreferences": "观看偏好", "watchPreferences": "观看偏好",
"contactSupport": "联系客服", "contactSupport": "联系客服",
"vipBrandName": "Netshort VIP", "vipBrandName": "FruitShorts VIP",
"mineVipSubtitle": "享受专属权益", "vipOpenNow": "立即开通",
"vipBenefitFree": "全剧免费看",
"vipBenefitNoAds": "免广告打扰",
"vipBenefitHd": "1080P高清",
"vipBenefitFastCache": "极速缓存",
"moreServices": "更多服务",
"mineVipSubtitle": "解锁精彩,畅看好剧",
"favoriteEpisodeProgress": "第 {episode} 集 / 共 {total} 集", "favoriteEpisodeProgress": "第 {episode} 集 / 共 {total} 集",
"@favoriteEpisodeProgress": { "@favoriteEpisodeProgress": {
"placeholders": { "placeholders": {
@@ -720,6 +727,44 @@
"total": { "type": "int" } "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": "已看至 第 {episode} 集",
"@watchedToEpisode": { "placeholders": { "episode": { "type": "int" } } }, "@watchedToEpisode": { "placeholders": { "episode": { "type": "int" } } },
"homeWatchAll": "观看全集", "homeWatchAll": "观看全集",
@@ -11,6 +11,7 @@ import '../../core/formatting/localized_date_time.dart';
import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors.dart';
import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_dimens.dart';
import '../../core/theme/app_theme.dart'; import '../../core/theme/app_theme.dart';
import '../../core/widgets/app_glow_panel.dart';
import '../../core/widgets/auth_gate.dart'; import '../../core/widgets/auth_gate.dart';
import '../../core/widgets/plain_page_scaffold.dart'; import '../../core/widgets/plain_page_scaffold.dart';
import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations.dart';
@@ -172,15 +173,15 @@ class _AccountDeactivatePageState extends State<AccountDeactivatePage> {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (dialogContext) => AlertDialog( builder: (dialogContext) => AppGlowDialog(
title: Text(l10n.accountConfirmDeactivateTitle), title: l10n.accountConfirmDeactivateTitle,
content: Text(l10n.accountConfirmDeactivateMessage), content: l10n.accountConfirmDeactivateMessage,
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false), onPressed: () => Navigator.of(dialogContext).pop(false),
child: Text(l10n.cancel), child: Text(l10n.cancel),
), ),
FilledButton( TextButton(
key: const Key('account-deactivate-confirm'), key: const Key('account-deactivate-confirm'),
onPressed: () => Navigator.of(dialogContext).pop(true), onPressed: () => Navigator.of(dialogContext).pop(true),
child: Text(l10n.accountConfirmDeactivate), child: Text(l10n.accountConfirmDeactivate),
+37 -55
View File
@@ -61,7 +61,7 @@ class _AuthFields extends StatelessWidget {
onChanged: callbacks.onEmailChanged, onChanged: callbacks.onEmailChanged,
onSubmitted: (_) => state.fields.passwordFocus.requestFocus(), onSubmitted: (_) => state.fields.passwordFocus.requestFocus(),
style: const TextStyle(color: AppColors.onSurface, fontSize: 15), style: const TextStyle(color: AppColors.onSurface, fontSize: 15),
cursorColor: AppColors.blue, cursorColor: AppColors.brandRed,
decoration: _decoration( decoration: _decoration(
context: context, context: context,
hint: 'name@example.com', hint: 'name@example.com',
@@ -93,7 +93,7 @@ class _AuthFields extends StatelessWidget {
onChanged: callbacks.onPasswordChanged, onChanged: callbacks.onPasswordChanged,
onSubmitted: (_) => callbacks.onSubmit(), onSubmitted: (_) => callbacks.onSubmit(),
style: const TextStyle(color: AppColors.onSurface, fontSize: 15), style: const TextStyle(color: AppColors.onSurface, fontSize: 15),
cursorColor: AppColors.blue, cursorColor: AppColors.brandRed,
decoration: _decoration( decoration: _decoration(
context: context, context: context,
hint: l10n.authPasswordHint, hint: l10n.authPasswordHint,
@@ -128,7 +128,7 @@ class _AuthFields extends StatelessWidget {
style: const TextStyle( style: const TextStyle(
color: AppColors.muted, color: AppColors.muted,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w400,
), ),
); );
@@ -142,10 +142,7 @@ class _AuthFields extends StatelessWidget {
}) { }) {
final border = OutlineInputBorder( final border = OutlineInputBorder(
borderRadius: BorderRadius.circular(_fieldRadius), borderRadius: BorderRadius.circular(_fieldRadius),
borderSide: const BorderSide( borderSide: BorderSide.none,
color: AppColors.border,
width: _fieldBorderWidth,
),
); );
final errorColor = Theme.of(context).colorScheme.error; final errorColor = Theme.of(context).colorScheme.error;
final errorBorder = OutlineInputBorder( final errorBorder = OutlineInputBorder(
@@ -158,7 +155,7 @@ class _AuthFields extends StatelessWidget {
prefixIcon: Icon( prefixIcon: Icon(
icon, icon,
size: 21, size: 21,
color: focused ? AppColors.blue : AppColors.muted, color: focused ? AppColors.brandRed : AppColors.muted,
), ),
prefixIconConstraints: const BoxConstraints( prefixIconConstraints: const BoxConstraints(
minWidth: _minTapTarget, minWidth: _minTapTarget,
@@ -166,7 +163,7 @@ class _AuthFields extends StatelessWidget {
), ),
suffixIcon: suffix, suffixIcon: suffix,
filled: true, filled: true,
fillColor: AppColors.card, fillColor: Colors.white.withValues(alpha: 0.05),
isDense: true, isDense: true,
contentPadding: const EdgeInsets.only(right: 16), contentPadding: const EdgeInsets.only(right: 16),
border: border, border: border,
@@ -175,7 +172,7 @@ class _AuthFields extends StatelessWidget {
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(_fieldRadius), borderRadius: BorderRadius.circular(_fieldRadius),
borderSide: const BorderSide( borderSide: const BorderSide(
color: AppColors.blue, color: AppColors.brandRed,
width: _fieldBorderWidth, width: _fieldBorderWidth,
), ),
), ),
@@ -204,17 +201,17 @@ class _AuthSubmitButton extends StatelessWidget {
style: ButtonStyle( style: ButtonStyle(
backgroundColor: WidgetStateProperty.resolveWith((states) { backgroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) { if (states.contains(WidgetState.disabled)) {
return AppColors.blue.withValues(alpha: 0.5); return AppColors.brandRed.withValues(alpha: 0.5);
} }
if (states.contains(WidgetState.pressed)) { if (states.contains(WidgetState.pressed)) {
return AppColors.bluePressed; return AppColors.brandRedPressed;
} }
return AppColors.blue; return AppColors.brandRed;
}), }),
foregroundColor: const WidgetStatePropertyAll(AppColors.onPrimary), foregroundColor: const WidgetStatePropertyAll(AppColors.onPrimary),
elevation: const WidgetStatePropertyAll(2), elevation: const WidgetStatePropertyAll(2),
shadowColor: WidgetStatePropertyAll( shadowColor: WidgetStatePropertyAll(
AppColors.blue.withValues(alpha: 0.22), AppColors.brandRed.withValues(alpha: 0.22),
), ),
padding: const WidgetStatePropertyAll(EdgeInsets.zero), padding: const WidgetStatePropertyAll(EdgeInsets.zero),
shape: WidgetStatePropertyAll( shape: WidgetStatePropertyAll(
@@ -235,7 +232,7 @@ class _AuthSubmitButton extends StatelessWidget {
status.signUpMode ? l10n.signUp : l10n.signIn, status.signUpMode ? l10n.signUp : l10n.signIn,
style: const TextStyle( style: const TextStyle(
fontSize: 15, fontSize: 15,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w600,
), ),
), ),
), ),
@@ -243,18 +240,16 @@ class _AuthSubmitButton extends StatelessWidget {
} }
} }
class _ModeSegment extends StatelessWidget { class _ModeTab extends StatelessWidget {
const _ModeSegment({ const _ModeTab({
required this.segmentKey, required this.segmentKey,
required this.label, required this.label,
required this.hiddenLabel,
required this.selected, required this.selected,
required this.onTap, required this.onTap,
}); });
final Key segmentKey; final Key segmentKey;
final String label; final String label;
final String hiddenLabel;
final bool selected; final bool selected;
final VoidCallback? onTap; final VoidCallback? onTap;
@@ -264,43 +259,30 @@ class _ModeSegment extends StatelessWidget {
key: segmentKey, key: segmentKey,
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: onTap, onTap: onTap,
child: AnimatedContainer( child: Column(
duration: _segmentDuration, crossAxisAlignment: CrossAxisAlignment.start,
alignment: Alignment.center, mainAxisSize: MainAxisSize.min,
decoration: BoxDecoration( children: [
color: selected ? AppColors.blue : AppColors.transparent, Text(
borderRadius: BorderRadius.circular(18), label,
), style: TextStyle(
child: Stack( color: selected ? AppColors.textPrimary : AppColors.muted,
fit: StackFit.expand, fontSize: AppDimens.followingSegmentFont,
children: [ fontWeight: FontWeight.w400,
Center( ),
child: Text( ),
label, const SizedBox(height: AppDimens.followingSegmentIndicatorTopGap),
style: TextStyle( Container(
color: selected ? AppColors.onPrimary : AppColors.onSurface, width: AppDimens.followingSegmentIndicatorWidth,
fontSize: 14, height: AppDimens.followingSegmentIndicatorHeight,
fontWeight: FontWeight.w700, 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( style: const TextStyle(
color: AppColors.onSurface, color: AppColors.onSurface,
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w700,
height: 1.1, height: 1.1,
), ),
), ),
+1 -1
View File
@@ -50,7 +50,7 @@ class _AuthLegalLinksState extends State<AuthLegalLinks> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final muted = TextStyle(color: AppColors.muted, fontSize: _legalFontSize, height: 1.6); 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( return Text.rich(
TextSpan( TextSpan(
style: muted, style: muted,
+88 -64
View File
@@ -24,18 +24,14 @@ part 'auth_widgets.dart';
part 'auth_form_widgets.dart'; part 'auth_form_widgets.dart';
const double _minTapTarget = 44; const double _minTapTarget = 44;
const double _brandMarkSize = 72; const double _brandMarkSize = 88;
const double _compactBrandMarkSize = 48; const double _compactBrandMarkSize = 48;
const double _brandMarkRadius = 20; const double _brandMarkRadius = 26;
const double _compactBrandMarkRadius = 14; const double _compactBrandMarkRadius = 14;
const double _landingLogoTopGap = 28;
const String _appLogoAsset = 'assets/icon/fruitshorts.png'; const String _appLogoAsset = 'assets/icon/fruitshorts.png';
const double _contentMaxWidth = 440; 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 _fieldHeight = 52;
const double _fieldRadius = 14; const double _fieldRadius = 12;
const double _fieldBorderWidth = 1; const double _fieldBorderWidth = 1;
const double _buttonHeight = 50; const double _buttonHeight = 50;
const double _buttonRadius = 25; const double _buttonRadius = 25;
@@ -47,14 +43,11 @@ const double _bottomContentPadding = 28;
const double _compactTopContentPadding = 40; const double _compactTopContentPadding = 40;
const double _compactHeightBreakpoint = 720; const double _compactHeightBreakpoint = 720;
const double _compactBrandGap = 16; const double _compactBrandGap = 16;
const double _glowSize = 280; const double _entryButtonHeight = 52;
const double _warmGlowOpacity = 0.18; const double _entryButtonRadius = 26;
const double _entryButtonHeight = 50;
const double _entryButtonRadius = 25;
const double _entryButtonFontSize = 15; const double _entryButtonFontSize = 15;
const double _entryIconSize = 20; const double _entryIconSize = 20;
const Duration _enterDuration = Duration(milliseconds: 220); const Duration _enterDuration = Duration(milliseconds: 220);
const Duration _segmentDuration = Duration(milliseconds: 160);
Widget _brandMarkWidget({ Widget _brandMarkWidget({
Key? key, Key? key,
@@ -267,45 +260,53 @@ class _AuthPageState extends State<AuthPage> {
child: IgnorePointer( child: IgnorePointer(
child: DecoratedBox( child: DecoratedBox(
key: Key('auth-page-background'), 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( decoration: BoxDecoration(
shape: BoxShape.circle, gradient: LinearGradient(
gradient: RadialGradient( begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [ colors: [
AppColors.surfaceTop, AppColors.followingGlow,
AppColors.surfaceTop.withValues(alpha: 0), AppColors.followingGlowMid,
AppColors.appBackground,
], ],
stops: [0, 0.14, 0.3],
), ),
), ),
), ),
), ),
), ),
Positioned( // 全屏海报背景:压暗保证文字可读,顶部再叠品牌红光。
left: -110, Positioned.fill(
bottom: -90, child: IgnorePointer(child: _AuthPosterBackdrop()),
width: _glowSize, ),
height: _glowSize, const Positioned.fill(
child: IgnorePointer( child: IgnorePointer(
child: Container( child: DecoratedBox(
key: const Key('auth-background-warm-glow'), key: Key('auth-poster-scrim'),
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, gradient: LinearGradient(
gradient: RadialGradient( begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [ colors: [
AppColors.gold.withValues(alpha: _warmGlowOpacity), Color(0x66000000),
AppColors.gold.withValues(alpha: 0), 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<AuthPage> {
constraints: const BoxConstraints( constraints: const BoxConstraints(
maxWidth: _contentMaxWidth, maxWidth: _contentMaxWidth,
), ),
child: _content(l10n, compact: compact), child: _content(
l10n,
compact: compact,
minHeight: minHeight,
),
), ),
), ),
), ),
@@ -387,7 +392,11 @@ class _AuthPageState extends State<AuthPage> {
); );
} }
Widget _content(AppLocalizations l10n, {required bool compact}) => Widget _content(
AppLocalizations l10n, {
required bool compact,
required double minHeight,
}) =>
TweenAnimationBuilder<double>( TweenAnimationBuilder<double>(
key: const Key('auth-enter-animation'), key: const Key('auth-enter-animation'),
tween: Tween(begin: 0, end: 1), tween: Tween(begin: 0, end: 1),
@@ -409,15 +418,18 @@ class _AuthPageState extends State<AuthPage> {
_authCard(l10n), _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'), key: const Key('auth-landing'),
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const SizedBox(height: _landingLogoTopGap), const Spacer(flex: 5),
Center( Center(
child: _brandMarkWidget( child: _brandMarkWidget(
key: const Key('auth-brand-logo'), key: const Key('auth-brand-logo'),
@@ -425,24 +437,19 @@ class _AuthPageState extends State<AuthPage> {
radius: _brandMarkRadius, radius: _brandMarkRadius,
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 10),
Text( Text(
l10n.fruitShortsBrand, l10n.fruitShortsBrand,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
fontFamily: 'Outfit', fontFamily: 'Outfit',
color: AppColors.onSurface, color: AppColors.onSurface,
fontSize: 24, fontSize: 26,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w700,
letterSpacing: 2,
), ),
), ),
const SizedBox(height: 6), const Spacer(flex: 3),
Text(
l10n.authLandingSubtitle,
textAlign: TextAlign.center,
style: const TextStyle(color: AppColors.muted, fontSize: 13),
),
const SizedBox(height: 28),
_AuthEntryButton( _AuthEntryButton(
key: const Key('auth-entry-phone'), key: const Key('auth-entry-phone'),
label: l10n.authPhoneSignIn, label: l10n.authPhoneSignIn,
@@ -458,10 +465,12 @@ class _AuthPageState extends State<AuthPage> {
primary: false, primary: false,
onTap: () => setState(() => _emailEntryOpen = true), onTap: () => setState(() => _emailEntryOpen = true),
), ),
const SizedBox(height: 18), const SizedBox(height: 14),
const AuthLegalLinks(), const Opacity(opacity: 0.75, child: AuthLegalLinks()),
], const Spacer(flex: 6),
); ],
),
);
Future<void> _openPhoneSheet() async { Future<void> _openPhoneSheet() async {
final signedIn = await showPhoneAuthSheet( final signedIn = await showPhoneAuthSheet(
@@ -526,15 +535,15 @@ class _AuthEntryButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { 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; final foreground = primary ? AppColors.onPrimary : AppColors.onSurface;
return Material( return Material(
color: background, color: background,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(_entryButtonRadius), borderRadius: BorderRadius.circular(_entryButtonRadius),
side: primary
? BorderSide.none
: const BorderSide(color: AppColors.border),
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: InkWell( child: InkWell(
@@ -556,7 +565,7 @@ class _AuthEntryButton extends StatelessWidget {
style: TextStyle( style: TextStyle(
color: foreground, color: foreground,
fontSize: _entryButtonFontSize, 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,
);
}
}
+49 -80
View File
@@ -70,52 +70,45 @@ class _AuthCard extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = state.l10n; final l10n = state.l10n;
final status = state.status; final status = state.status;
return Container( // 不再做卡片盒子,表单直接排在红光晕背景上。
return Column(
key: const Key('auth-card'), key: const Key('auth-card'),
padding: const EdgeInsets.all(_cardPadding), crossAxisAlignment: CrossAxisAlignment.stretch,
decoration: BoxDecoration( children: [
color: AppColors.card, _AuthModeSwitch(
borderRadius: BorderRadius.circular(_cardRadius), l10n: l10n,
border: Border.all(color: AppColors.border), signUpMode: status.signUpMode,
), busy: status.busy,
child: Column( onSelected: callbacks.onModeSelected,
crossAxisAlignment: CrossAxisAlignment.stretch, ),
children: [ const SizedBox(height: 24),
_AuthModeSwitch( Text(
l10n: l10n, status.signUpMode ? l10n.authSignUpTitle : l10n.authSignInTitle,
signUpMode: status.signUpMode, style: const TextStyle(
busy: status.busy, color: AppColors.onSurface,
onSelected: callbacks.onModeSelected, fontSize: 24,
fontWeight: FontWeight.w600,
height: 1.2,
), ),
const SizedBox(height: 22), ),
Text( const SizedBox(height: 6),
status.signUpMode ? l10n.authSignUpTitle : l10n.authSignInTitle, Text(
style: const TextStyle( status.signUpMode ? l10n.authSignUpSubtitle : l10n.authSubtitle,
color: AppColors.onSurface, style: const TextStyle(
fontSize: 25, color: AppColors.muted,
fontWeight: FontWeight.w800, fontSize: AppDimens.body,
height: 1.2, height: 1.45,
),
), ),
const SizedBox(height: 6), ),
Text( const SizedBox(height: 20),
status.signUpMode ? l10n.authSignUpSubtitle : l10n.authSubtitle, _AuthFields(state: state, callbacks: callbacks),
style: const TextStyle( if (status.errorMessage != null) ...[
color: AppColors.muted, const SizedBox(height: 14),
fontSize: AppDimens.body, _AuthError(message: status.errorMessage!),
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: 22),
_AuthSubmitButton(state: state, onSubmit: callbacks.onSubmit),
],
); );
} }
} }
@@ -135,47 +128,23 @@ class _AuthModeSwitch extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TextButton( return Row(
key: const Key('auth-switch-mode'), key: const Key('auth-switch-mode'),
onPressed: busy ? null : () => onSelected(!signUpMode), children: [
style: TextButton.styleFrom( _ModeTab(
minimumSize: const Size(0, _minTapTarget), segmentKey: const Key('auth-mode-sign-in'),
padding: EdgeInsets.zero, label: l10n.signIn,
overlayColor: AppColors.transparent, selected: !signUpMode,
splashFactory: NoSplash.splashFactory, onTap: busy ? null : () => onSelected(false),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(22)),
),
child: Container(
height: 36,
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: AppColors.raised,
borderRadius: BorderRadius.circular(18),
), ),
child: Row( const SizedBox(width: AppDimens.followingSegmentGap),
children: [ _ModeTab(
Expanded( segmentKey: const Key('auth-mode-sign-up'),
child: _ModeSegment( label: l10n.signUp,
segmentKey: const Key('auth-mode-sign-in'), selected: signUpMode,
label: l10n.signIn, onTap: busy ? null : () => onSelected(true),
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),
),
),
],
), ),
), ],
); );
} }
} }
+12 -10
View File
@@ -11,6 +11,7 @@ import '../../core/data/services/auth_controller.dart';
import '../../core/platform/haptics.dart'; import '../../core/platform/haptics.dart';
import '../../core/platform/keyboard_inset.dart'; import '../../core/platform/keyboard_inset.dart';
import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors.dart';
import '../../core/widgets/app_glow_panel.dart';
import '../../core/widgets/centered_toast.dart'; import '../../core/widgets/centered_toast.dart';
import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations.dart';
@@ -252,15 +253,15 @@ class _PhoneAuthSheetState extends State<PhoneAuthSheet> {
final bottom = effectiveKeyboardInset(context, _keyboardInset); final bottom = effectiveKeyboardInset(context, _keyboardInset);
return Padding( return Padding(
padding: EdgeInsets.only(bottom: bottom), padding: EdgeInsets.only(bottom: bottom),
child: Container( child: AppGlowPanel(
key: const Key('phone-auth-sheet'), borderRadius: const BorderRadius.vertical(
decoration: const BoxDecoration( top: Radius.circular(_sheetRadius),
color: AppColors.card,
borderRadius: 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, controller: _scrollController,
padding: const EdgeInsets.fromLTRB(22, 10, 22, 26), padding: const EdgeInsets.fromLTRB(22, 10, 22, 26),
child: Column( child: Column(
@@ -313,6 +314,7 @@ class _PhoneAuthSheetState extends State<PhoneAuthSheet> {
], ],
), ),
), ),
),
), ),
); );
} }
@@ -403,7 +405,7 @@ class _PhoneAuthSheetState extends State<PhoneAuthSheet> {
key: const Key('auth-phone-submit'), key: const Key('auth-phone-submit'),
onPressed: _submitting ? null : _submit, onPressed: _submitting ? null : _submit,
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
backgroundColor: AppColors.blue, backgroundColor: AppColors.brandRed,
foregroundColor: AppColors.onPrimary, foregroundColor: AppColors.onPrimary,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(_buttonHeight / 2), borderRadius: BorderRadius.circular(_buttonHeight / 2),
@@ -439,7 +441,7 @@ class _PhoneAuthSheetState extends State<PhoneAuthSheet> {
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(_fieldRadius), borderRadius: BorderRadius.circular(_fieldRadius),
borderSide: const BorderSide(color: AppColors.blue, width: 1.5), borderSide: const BorderSide(color: AppColors.brandRed, width: 1.5),
), ),
); );
} }
+6 -5
View File
@@ -16,6 +16,7 @@ import '../../core/platform/haptics.dart';
import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors.dart';
import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_dimens.dart';
import '../../core/theme/app_theme.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/app_pull_to_refresh.dart';
import '../../core/widgets/auth_gate.dart'; import '../../core/widgets/auth_gate.dart';
import '../../core/widgets/empty_state.dart'; import '../../core/widgets/empty_state.dart';
@@ -181,15 +182,15 @@ class _DownloadsBody extends StatelessWidget {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (dialogContext) => AlertDialog( builder: (dialogContext) => AppGlowDialog(
title: Text(l10n.clearDownloadsTitle), title: l10n.clearDownloadsTitle,
content: Text(l10n.clearDownloadsMessage), content: l10n.clearDownloadsMessage,
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false), onPressed: () => Navigator.of(dialogContext).pop(false),
child: Text(l10n.cancel), child: Text(l10n.cancel),
), ),
FilledButton( TextButton(
onPressed: () => Navigator.of(dialogContext).pop(true), onPressed: () => Navigator.of(dialogContext).pop(true),
child: Text(l10n.confirmClear), child: Text(l10n.confirmClear),
), ),
@@ -435,7 +436,7 @@ class _LicenseTile extends StatelessWidget {
minHeight: AppDimens.progressHeight, minHeight: AppDimens.progressHeight,
value: progress.clamp(0.0, 1.0), value: progress.clamp(0.0, 1.0),
backgroundColor: AppColors.border, backgroundColor: AppColors.border,
valueColor: const AlwaysStoppedAnimation(AppColors.blueSoft), valueColor: const AlwaysStoppedAnimation(AppColors.brandRed),
), ),
), ),
if (license.expireAt != null) ...[ if (license.expireAt != null) ...[
@@ -3,6 +3,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../core/data/models/interaction.dart'; import '../../../core/data/models/interaction.dart';
import '../../../core/widgets/app_glow_panel.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
const int _maxDescriptionLength = 300; const int _maxDescriptionLength = 300;
@@ -67,9 +68,9 @@ class _ReportDialogState extends State<_ReportDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
return AlertDialog( return AppGlowDialog(
title: Text(widget.title), title: widget.title,
content: SingleChildScrollView( contentChild: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
+4 -3
View File
@@ -13,6 +13,7 @@ import '../../core/formatting/localized_date_time.dart';
import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors.dart';
import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_dimens.dart';
import '../../core/theme/app_theme.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/app_pull_to_refresh.dart';
import '../../core/widgets/empty_state.dart'; import '../../core/widgets/empty_state.dart';
import '../../core/widgets/plain_page_scaffold.dart'; import '../../core/widgets/plain_page_scaffold.dart';
@@ -191,9 +192,9 @@ class _NoticesPageState extends State<NoticesPage> {
: formatLocalizedDateTime(context, notice.publishAt!); : formatLocalizedDateTime(context, notice.publishAt!);
return showDialog<void>( return showDialog<void>(
context: context, context: context,
builder: (dialogContext) => AlertDialog( builder: (dialogContext) => AppGlowDialog(
title: Text(notice.title), title: notice.title,
content: SizedBox( contentChild: SizedBox(
width: double.maxFinite, width: double.maxFinite,
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
@@ -14,6 +14,7 @@ import '../../../core/data/repositories/interaction_repository.dart';
import '../../../core/data/services/comment_controller.dart'; import '../../../core/data/services/comment_controller.dart';
import '../../../core/formatting/error_text.dart'; import '../../../core/formatting/error_text.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/widgets/app_glow_panel.dart';
import '../../../core/widgets/empty_state.dart'; import '../../../core/widgets/empty_state.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
import 'comment_composer_sheet.dart'; import 'comment_composer_sheet.dart';
@@ -176,14 +177,14 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
child: SizedBox( child: SizedBox(
key: const Key('comment-sheet-constraints'), key: const Key('comment-sheet-constraints'),
height: sheetHeight, height: sheetHeight,
child: Container( child: AppGlowPanel(
decoration: const BoxDecoration( borderRadius: const BorderRadius.vertical(
color: AppColors.card, top: Radius.circular(_sheetRadius),
borderRadius: BorderRadius.vertical(
top: Radius.circular(_sheetRadius),
),
), ),
child: SafeArea( border: false,
child: Container(
decoration: const BoxDecoration(color: AppColors.transparent),
child: SafeArea(
top: false, top: false,
child: ListenableBuilder( child: ListenableBuilder(
listenable: _controller, listenable: _controller,
@@ -193,6 +194,7 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
child: _content(context), child: _content(context),
); );
}, },
),
), ),
), ),
), ),
@@ -238,7 +240,7 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
style: const TextStyle( style: const TextStyle(
color: AppColors.onSurface, color: AppColors.onSurface,
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w400,
), ),
), ),
Positioned( Positioned(
@@ -7,6 +7,7 @@ import '../../../core/data/models/drama.dart';
import '../../../core/formatting/localized_content.dart'; import '../../../core/formatting/localized_content.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart'; import '../../../core/theme/app_dimens.dart';
import '../../../core/widgets/app_glow_panel.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
import 'episode_picker_cell.dart'; import 'episode_picker_cell.dart';
@@ -47,16 +48,16 @@ class EpisodePickerSheet extends StatelessWidget {
systemNavigationBarIconBrightness: Brightness.light, systemNavigationBarIconBrightness: Brightness.light,
systemNavigationBarContrastEnforced: false, systemNavigationBarContrastEnforced: false,
), ),
child: Container( child: AppGlowPanel(
key: const Key('player-episodes-sheet'), borderRadius: const BorderRadius.vertical(
height: MediaQuery.sizeOf(context).height * _sheetHeightFactor, top: Radius.circular(_sheetRadius),
decoration: const BoxDecoration(
color: AppColors.card,
borderRadius: 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, top: false,
bottom: true, bottom: true,
child: Column( child: Column(
@@ -139,6 +140,7 @@ class EpisodePickerSheet extends StatelessWidget {
], ],
), ),
), ),
),
), ),
); );
} }
@@ -3,6 +3,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/widgets/app_glow_panel.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
const double _sheetRadius = 20; const double _sheetRadius = 20;
@@ -52,6 +53,8 @@ class PlayerMoreActions {
required this.onDownload, required this.onDownload,
required this.onQualityChanged, required this.onQualityChanged,
this.onPip, this.onPip,
this.autoPipEnabled = false,
this.onAutoPipChanged,
}); });
/// 倍速变化。 /// 倍速变化。
@@ -68,6 +71,12 @@ class PlayerMoreActions {
/// 进入小窗播放;平台不支持时为 null。 /// 进入小窗播放;平台不支持时为 null。
final VoidCallback? onPip; final VoidCallback? onPip;
/// 退出 App 自动小窗开关的当前状态(仅支持自动小窗的平台展示开关行)。
final bool autoPipEnabled;
/// 切换退出 App 自动小窗;为 null 时隐藏开关行。
final ValueChanged<bool>? onAutoPipChanged;
} }
/// K29.6 精简版播放页更多面板。 /// K29.6 精简版播放页更多面板。
@@ -96,16 +105,19 @@ class PlayerMoreSheet extends StatefulWidget {
class _PlayerMoreSheetState extends State<PlayerMoreSheet> { class _PlayerMoreSheetState extends State<PlayerMoreSheet> {
late double _speed = widget.state.speed; late double _speed = widget.state.speed;
late String _quality = widget.state.selectedQuality.toUpperCase(); late String _quality = widget.state.selectedQuality.toUpperCase();
late bool _autoPip = widget.actions.autoPipEnabled;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = widget.l10n; final l10n = widget.l10n;
return Material( return AppGlowPanel(
key: const Key('player-more-sheet'),
color: AppColors.ink,
borderRadius: const BorderRadius.vertical( borderRadius: const BorderRadius.vertical(
top: Radius.circular(_sheetRadius), top: Radius.circular(_sheetRadius),
), ),
border: false,
child: Material(
key: const Key('player-more-sheet'),
type: MaterialType.transparency,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: SafeArea( child: SafeArea(
top: false, top: false,
@@ -142,6 +154,17 @@ class _PlayerMoreSheetState extends State<PlayerMoreSheet> {
title: l10n.pictureInPicture, title: l10n.pictureInPicture,
onTap: () => _closeThen(widget.actions.onPip!), 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( _actionRow(
key: const Key('player-more-offline'), key: const Key('player-more-offline'),
icon: Icons.download_outlined, icon: Icons.download_outlined,
@@ -153,6 +176,7 @@ class _PlayerMoreSheetState extends State<PlayerMoreSheet> {
), ),
), ),
), ),
),
); );
} }
@@ -160,7 +184,7 @@ class _PlayerMoreSheetState extends State<PlayerMoreSheet> {
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: _groupVerticalPadding), padding: const EdgeInsets.symmetric(vertical: _groupVerticalPadding),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.card, color: Colors.white.withValues(alpha: 0.06),
borderRadius: BorderRadius.circular(_groupRadius), borderRadius: BorderRadius.circular(_groupRadius),
), ),
child: Column(mainAxisSize: MainAxisSize.min, children: rows), child: Column(mainAxisSize: MainAxisSize.min, children: rows),
@@ -241,7 +265,7 @@ class _PlayerMoreSheetState extends State<PlayerMoreSheet> {
horizontal: _segmentTrackPadding, horizontal: _segmentTrackPadding,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.raised, color: Colors.white.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(_segmentTrackRadius), borderRadius: BorderRadius.circular(_segmentTrackRadius),
), ),
child: Row( child: Row(
@@ -286,6 +310,54 @@ class _PlayerMoreSheetState extends State<PlayerMoreSheet> {
); );
} }
/// 开关行:左侧图标+标题,右侧主题色 Switch。
Widget _switchRow({
required Key key,
required IconData icon,
required String title,
required bool value,
required ValueChanged<bool> 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({ Widget _actionRow({
required Key key, required Key key,
required IconData icon, required IconData icon,
@@ -380,7 +452,9 @@ class _SegmentButton extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: _segmentPadding), padding: const EdgeInsets.symmetric(horizontal: _segmentPadding),
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: selected ? AppColors.card : AppColors.transparent, color: selected
? Colors.white.withValues(alpha: 0.16)
: AppColors.transparent,
borderRadius: BorderRadius.circular(_segmentRadius), borderRadius: BorderRadius.circular(_segmentRadius),
boxShadow: selected boxShadow: selected
? const [ ? const [
@@ -3,6 +3,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/widgets/app_glow_panel.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
const List<double> _speedOptions = [0.75, 1.0, 1.25, 1.5, 2.0, 3.0]; const List<double> _speedOptions = [0.75, 1.0, 1.25, 1.5, 2.0, 3.0];
@@ -19,14 +20,16 @@ class PlayerSpeedSheet extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
return Material( return AppGlowPanel(
key: const Key('player-speed-sheet'),
color: AppColors.card,
borderRadius: const BorderRadius.vertical( borderRadius: const BorderRadius.vertical(
top: Radius.circular(_sheetRadius), top: Radius.circular(_sheetRadius),
), ),
clipBehavior: Clip.antiAlias, border: false,
child: SafeArea( child: Material(
key: const Key('player-speed-sheet'),
type: MaterialType.transparency,
clipBehavior: Clip.antiAlias,
child: SafeArea(
top: false, top: false,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -50,6 +53,7 @@ class PlayerSpeedSheet extends StatelessWidget {
], ],
), ),
), ),
),
); );
} }
@@ -6,6 +6,7 @@ import '../../../core/data/models/drama.dart';
import '../../../core/data/models/play_ticket.dart'; import '../../../core/data/models/play_ticket.dart';
import '../../../core/formatting/error_text.dart'; import '../../../core/formatting/error_text.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/widgets/app_glow_panel.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
const double _cardRadius = 20; const double _cardRadius = 20;
@@ -209,12 +210,10 @@ class _PlayerUnlockSheetState extends State<PlayerUnlockSheet> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = widget.l10n; final l10n = widget.l10n;
return Container( return AppGlowPanel(
key: const Key('player-unlock-panel'), key: const Key('player-unlock-panel'),
decoration: const BoxDecoration( borderRadius: const BorderRadius.vertical(top: Radius.circular(_cardRadius)),
color: AppColors.card, border: false,
borderRadius: BorderRadius.vertical(top: Radius.circular(_cardRadius)),
),
child: SafeArea( child: SafeArea(
top: false, top: false,
child: Padding( child: Padding(
@@ -14,6 +14,7 @@ import '../../core/formatting/error_text.dart';
import '../../core/platform/haptics.dart'; import '../../core/platform/haptics.dart';
import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors.dart';
import '../../core/theme/app_theme.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/app_pull_to_refresh.dart';
import '../../core/widgets/empty_state.dart'; import '../../core/widgets/empty_state.dart';
import '../../core/widgets/paged_list_footer.dart'; import '../../core/widgets/paged_list_footer.dart';
@@ -255,15 +256,15 @@ class _WatchRecordsBodyState extends State<_WatchRecordsBody> {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (dialogContext) => AlertDialog( builder: (dialogContext) => AppGlowDialog(
title: Text(l10n.clearWatchHistoryTitle), title: l10n.clearWatchHistoryTitle,
content: Text(l10n.clearWatchHistoryMessage), content: l10n.clearWatchHistoryMessage,
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false), onPressed: () => Navigator.of(dialogContext).pop(false),
child: Text(l10n.cancel), child: Text(l10n.cancel),
), ),
FilledButton( TextButton(
onPressed: () => Navigator.of(dialogContext).pop(true), onPressed: () => Navigator.of(dialogContext).pop(true),
child: Text(l10n.confirmClear), child: Text(l10n.confirmClear),
), ),