feat(flutter): 应用克莱因蓝主题与印尼语适配
This commit is contained in:
1 parent
e1147cfcba
commit
10d448c8ba
31 files changed
+891
-521
No files matched your search
+109
-90
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
||||
import '../../core/data/remote/api_client.dart';
|
||||
import '../../core/data/services/auth_controller.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import 'auth_logic.dart';
|
||||
|
||||
@@ -82,102 +83,117 @@ class _AuthPageState extends State<AuthPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.ink,
|
||||
appBar: AppBar(
|
||||
return Theme(
|
||||
data: AppTheme.dark(),
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColors.ink,
|
||||
elevation: 0,
|
||||
foregroundColor: Colors.white,
|
||||
title: Text(_logic.signUpMode ? l10n.signUp : l10n.signIn),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 28),
|
||||
children: [
|
||||
Text(
|
||||
_logic.signUpMode ? l10n.authSignUpTitle : l10n.authSignInTitle,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
l10n.authSubtitle,
|
||||
style: const TextStyle(color: AppColors.muted, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 22),
|
||||
_field(
|
||||
label: l10n.authEmailLabel,
|
||||
hint: 'name@example.com',
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
onChanged: (value) => _logic.email = value,
|
||||
autofocus: true,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_field(
|
||||
label: l10n.authPasswordLabel,
|
||||
hint: l10n.authPasswordHint,
|
||||
obscure: true,
|
||||
onChanged: (value) => _logic.password = value,
|
||||
),
|
||||
if (_logic.errorMessage != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: Color(0xFFFF6B6B), size: 16),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_logic.errorMessage!,
|
||||
style: const TextStyle(color: Color(0xFFFF6B6B), fontSize: 13),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 22),
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: AppColors.blue,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.ink,
|
||||
elevation: 0,
|
||||
foregroundColor: Colors.white,
|
||||
title: Text(_logic.signUpMode ? l10n.signUp : l10n.signIn),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 28),
|
||||
children: [
|
||||
Text(
|
||||
_logic.signUpMode ? l10n.authSignUpTitle : l10n.authSignInTitle,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
onPressed: _logic.busy ? null : _submit,
|
||||
child: _logic.busy
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
_logic.signUpMode ? l10n.signUp : l10n.signIn,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
l10n.authSubtitle,
|
||||
style: const TextStyle(color: AppColors.muted, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 22),
|
||||
_field(
|
||||
label: l10n.authEmailLabel,
|
||||
hint: 'name@example.com',
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
onChanged: (value) => _logic.email = value,
|
||||
autofocus: true,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_field(
|
||||
label: l10n.authPasswordLabel,
|
||||
hint: l10n.authPasswordHint,
|
||||
obscure: true,
|
||||
onChanged: (value) => _logic.password = value,
|
||||
),
|
||||
if (_logic.errorMessage != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: Color(0xFFFF6B6B),
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_logic.errorMessage!,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFFFF6B6B),
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 22),
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: AppColors.blue,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
),
|
||||
onPressed: _logic.busy ? null : _submit,
|
||||
child: _logic.busy
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
_logic.signUpMode ? l10n.signUp : l10n.signIn,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
TextButton(
|
||||
onPressed: _logic.busy
|
||||
? null
|
||||
: () => setState(() => _logic.toggleMode()),
|
||||
child: Text(
|
||||
_logic.signUpMode ? l10n.authSwitchToSignIn : l10n.authSwitchToSignUp,
|
||||
style: const TextStyle(color: AppColors.blueSoft, fontSize: 13),
|
||||
const SizedBox(height: 14),
|
||||
TextButton(
|
||||
onPressed: _logic.busy
|
||||
? null
|
||||
: () => setState(() => _logic.toggleMode()),
|
||||
child: Text(
|
||||
_logic.signUpMode
|
||||
? l10n.authSwitchToSignIn
|
||||
: l10n.authSwitchToSignUp,
|
||||
style: const TextStyle(
|
||||
color: AppColors.blueSoft,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -214,7 +230,10 @@ class _AuthPageState extends State<AuthPage> {
|
||||
hintStyle: const TextStyle(color: Color(0xFF6C7280), fontSize: 14),
|
||||
filled: true,
|
||||
fillColor: AppColors.card,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 14,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../../core/data/models/home_feed.dart';
|
||||
import '../../core/data/repositories/watch_history_repository.dart';
|
||||
import '../../core/data/services/category_controller.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../core/widgets/remote_image.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import 'discover_binding.dart';
|
||||
@@ -139,7 +140,7 @@ class _DiscoverPageState extends State<DiscoverPage> {
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.light,
|
||||
child: Theme(
|
||||
data: ThemeData.dark(useMaterial3: true),
|
||||
data: AppTheme.dark(),
|
||||
child: widget.embedded
|
||||
? content
|
||||
: Scaffold(backgroundColor: const Color(0xFF0B0C10), body: content),
|
||||
@@ -254,7 +255,7 @@ class _DiscoverPageState extends State<DiscoverPage> {
|
||||
Text(
|
||||
l10n.seeAll,
|
||||
style: const TextStyle(
|
||||
color: AppColors.blue,
|
||||
color: AppColors.blueSoft,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
@@ -417,7 +418,9 @@ class _DiscoverPageState extends State<DiscoverPage> {
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? AppColors.blue : const Color(0xFF1C1E24),
|
||||
color: selected
|
||||
? AppColors.bluePressed
|
||||
: const Color(0xFF1C1E24),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
@@ -501,12 +504,12 @@ class _DiscoverNavItem extends StatelessWidget {
|
||||
Icon(
|
||||
icon,
|
||||
size: 21,
|
||||
color: selected ? _orange : const Color(0xFFCAC4C7),
|
||||
color: selected ? _accent : const Color(0xFFCAC4C7),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: selected ? _orange : const Color(0xFFCAC4C7),
|
||||
color: selected ? _accent : const Color(0xFFCAC4C7),
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
@@ -549,7 +552,7 @@ List<_ShowcaseCard> _cardsFor(List<Drama> dramas, String query) {
|
||||
];
|
||||
}
|
||||
|
||||
const _orange = AppColors.blue;
|
||||
const _accent = AppColors.blueSoft;
|
||||
const _coin = Color(0xFFF5C451);
|
||||
|
||||
class _BrandMark extends StatelessWidget {
|
||||
|
||||
@@ -10,7 +10,9 @@ import '../../core/data/models/download_policy.dart';
|
||||
import '../../core/data/services/auth_controller.dart';
|
||||
import '../../core/data/services/download_controller.dart';
|
||||
import '../../core/formatting/error_text.dart';
|
||||
import '../../core/formatting/localized_date_time.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../core/widgets/auth_gate.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import 'downloads_binding.dart';
|
||||
@@ -46,17 +48,20 @@ class _DownloadsPageState extends State<DownloadsPage> {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final signedIn = context.watch<AuthController>().isSignedIn;
|
||||
return Scaffold(
|
||||
key: const Key('downloads-page'),
|
||||
backgroundColor: const Color(0xFF0B0C10),
|
||||
appBar: AppBar(
|
||||
return Theme(
|
||||
data: AppTheme.dark(),
|
||||
child: Scaffold(
|
||||
key: const Key('downloads-page'),
|
||||
backgroundColor: const Color(0xFF0B0C10),
|
||||
foregroundColor: Colors.white,
|
||||
title: Text(l10n.downloads),
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xFF0B0C10),
|
||||
foregroundColor: Colors.white,
|
||||
title: Text(l10n.downloads),
|
||||
),
|
||||
body: signedIn
|
||||
? _DownloadsBody(downloads: context.watch<DownloadController>())
|
||||
: const AuthGatePanel(),
|
||||
),
|
||||
body: signedIn
|
||||
? _DownloadsBody(downloads: context.watch<DownloadController>())
|
||||
: const AuthGatePanel(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -380,7 +385,9 @@ class _LicenseTile extends StatelessWidget {
|
||||
if (license.expireAt != null) ...[
|
||||
const SizedBox(height: 7),
|
||||
Text(
|
||||
l10n.downloadExpiresAt(_formatTime(license.expireAt!)),
|
||||
l10n.downloadExpiresAt(
|
||||
formatLocalizedDateTime(context, license.expireAt!),
|
||||
),
|
||||
style: const TextStyle(color: Color(0xFF8D8990), fontSize: 11),
|
||||
),
|
||||
],
|
||||
@@ -398,12 +405,3 @@ List<String> _qualityChoices(String current) {
|
||||
}
|
||||
return choices;
|
||||
}
|
||||
|
||||
String _formatTime(DateTime value) {
|
||||
final local = value.toLocal();
|
||||
final month = local.month.toString().padLeft(2, '0');
|
||||
final day = local.day.toString().padLeft(2, '0');
|
||||
final hour = local.hour.toString().padLeft(2, '0');
|
||||
final minute = local.minute.toString().padLeft(2, '0');
|
||||
return '${local.year}-$month-$day $hour:$minute';
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import '../../core/data/services/favorite_controller.dart';
|
||||
import '../../core/data/services/interaction_controller.dart';
|
||||
import '../../core/formatting/error_text.dart';
|
||||
import '../../core/formatting/localized_content.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../core/widgets/remote_image.dart';
|
||||
import '../../core/widgets/auth_gate.dart';
|
||||
import '../../core/widgets/download_action.dart';
|
||||
@@ -62,9 +64,12 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: logic,
|
||||
builder: (context, _) => _page(context),
|
||||
return Theme(
|
||||
data: AppTheme.dark(),
|
||||
child: ListenableBuilder(
|
||||
listenable: logic,
|
||||
builder: (context, _) => _page(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -158,7 +163,7 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
|
||||
l10n.technicalDemoCount(drama.episodes.length),
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
letterSpacing: 1.4,
|
||||
color: const Color(0xFF2F7BFF),
|
||||
color: AppColors.blueSoft,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
@@ -248,9 +253,7 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
|
||||
child: ListTile(
|
||||
minTileHeight: 76,
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: const Color(
|
||||
0xFF2F7BFF,
|
||||
).withValues(alpha: 0.18),
|
||||
backgroundColor: AppColors.blue.withValues(alpha: 0.18),
|
||||
child: Text('${episode.number}'),
|
||||
),
|
||||
title: Text(l10n.episodeLabel(episode.number)),
|
||||
@@ -303,10 +306,7 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
|
||||
tooltip: l10n.more,
|
||||
onSelected: (_) => unawaited(_reportDrama(l10n)),
|
||||
itemBuilder: (_) => [
|
||||
PopupMenuItem(
|
||||
value: _reportMenuValue,
|
||||
child: Text(l10n.reportDrama),
|
||||
),
|
||||
PopupMenuItem(value: _reportMenuValue, child: Text(l10n.reportDrama)),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -378,7 +378,7 @@ class _ResumeBar extends StatelessWidget {
|
||||
value: fraction,
|
||||
minHeight: 3,
|
||||
backgroundColor: Colors.white24,
|
||||
valueColor: const AlwaysStoppedAnimation(Color(0xFF2F7BFF)),
|
||||
valueColor: const AlwaysStoppedAnimation(AppColors.blueSoft),
|
||||
),
|
||||
),
|
||||
if (label != null) ...[
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../../core/data/models/drama.dart';
|
||||
import '../../core/data/services/auth_controller.dart';
|
||||
import '../../core/data/services/favorite_controller.dart';
|
||||
import '../../core/formatting/error_text.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../core/widgets/auth_gate.dart';
|
||||
import '../../core/widgets/library_drama_tile.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
@@ -46,16 +47,19 @@ class _FavoritesPageState extends State<FavoritesPage> {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final signedIn = context.watch<AuthController>().isSignedIn;
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF0B0C10),
|
||||
appBar: AppBar(
|
||||
return Theme(
|
||||
data: AppTheme.dark(),
|
||||
child: Scaffold(
|
||||
backgroundColor: const Color(0xFF0B0C10),
|
||||
foregroundColor: Colors.white,
|
||||
title: Text(l10n.myFavorites),
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xFF0B0C10),
|
||||
foregroundColor: Colors.white,
|
||||
title: Text(l10n.myFavorites),
|
||||
),
|
||||
body: signedIn
|
||||
? _FavoritesBody(favorites: context.watch<FavoriteController>())
|
||||
: const AuthGatePanel(),
|
||||
),
|
||||
body: signedIn
|
||||
? _FavoritesBody(favorites: context.watch<FavoriteController>())
|
||||
: const AuthGatePanel(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,9 @@ import '../../core/config/app_config.dart';
|
||||
import '../../core/data/models/drama.dart';
|
||||
import '../../core/data/services/auth_controller.dart';
|
||||
import '../../core/data/services/catalog_controller.dart';
|
||||
import '../../core/widgets/auth_gate.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../core/widgets/auth_gate.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../discover/discover_view.dart';
|
||||
import '../mine/mine_view.dart';
|
||||
@@ -26,7 +27,7 @@ class HomePage extends StatelessWidget {
|
||||
final catalog = context.watch<CatalogController>();
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return Theme(
|
||||
data: ThemeData.dark(useMaterial3: true),
|
||||
data: AppTheme.dark(),
|
||||
child: _buildHome(context, catalog, l10n),
|
||||
);
|
||||
}
|
||||
@@ -258,7 +259,7 @@ class _TabButton extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 3),
|
||||
decoration: selected
|
||||
? BoxDecoration(
|
||||
color: AppColors.blue,
|
||||
color: AppColors.bluePressed,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
)
|
||||
: null,
|
||||
@@ -276,7 +277,7 @@ class _TabButton extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
|
||||
color: selected ? AppColors.blue : AppColors.muted,
|
||||
color: selected ? AppColors.blueSoft : AppColors.muted,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
|
||||
import '../../../core/config/locale_controller.dart';
|
||||
import '../../../core/data/models/drama.dart';
|
||||
import '../../../core/formatting/localized_content.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/widgets/remote_image.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
|
||||
@@ -30,7 +31,7 @@ class HomeQuickDrawer extends StatelessWidget {
|
||||
|
||||
static const _panel = Color(0xFF222225);
|
||||
static const _card = Color(0xFF303033);
|
||||
static const _orange = Color(0xFF2F7BFF);
|
||||
static const _accent = AppColors.blueSoft;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -128,7 +129,7 @@ class HomeQuickDrawer extends StatelessWidget {
|
||||
key: const Key('drawer-open-series'),
|
||||
onPressed: onOpenSeries,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: _orange,
|
||||
backgroundColor: _accent,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
icon: const Icon(Icons.play_arrow_rounded),
|
||||
@@ -192,22 +193,21 @@ class HomeQuickDrawer extends StatelessWidget {
|
||||
const SizedBox(height: 14),
|
||||
_SectionCard(
|
||||
title: l10n.appLanguage,
|
||||
child: Row(
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final (code, label) in [
|
||||
('en', l10n.english),
|
||||
('zh', l10n.simplifiedChinese),
|
||||
]) ...[
|
||||
Expanded(
|
||||
child: ChoiceChip(
|
||||
label: Text(label, maxLines: 1),
|
||||
selected: languageCode == code,
|
||||
onSelected: (_) =>
|
||||
context.read<LocaleController>().select(code),
|
||||
),
|
||||
('id', l10n.bahasaIndonesia),
|
||||
])
|
||||
ChoiceChip(
|
||||
label: Text(label, maxLines: 1),
|
||||
selected: languageCode == code,
|
||||
onSelected: (_) =>
|
||||
context.read<LocaleController>().select(code),
|
||||
),
|
||||
if (code == 'en') const SizedBox(width: 8),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -268,7 +268,7 @@ class _QuickAction extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 7),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, color: HomeQuickDrawer._orange, size: 25),
|
||||
Icon(icon, color: HomeQuickDrawer._accent, size: 25),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
label,
|
||||
|
||||
@@ -26,6 +26,7 @@ class MineLogic extends ChangeNotifier {
|
||||
List<WatchProgress> recent = const [];
|
||||
bool recentLoading = false;
|
||||
String? recentError;
|
||||
|
||||
/// 续看位置读取失败:与"没有记录"区分开,失败要能重试(不能假装成从 0 开始)
|
||||
bool positionFailed = false;
|
||||
int section;
|
||||
@@ -123,6 +124,7 @@ class MineLogic extends ChangeNotifier {
|
||||
for (final (code, label) in [
|
||||
('en', l10n.english),
|
||||
('zh', l10n.simplifiedChinese),
|
||||
('id', l10n.bahasaIndonesia),
|
||||
])
|
||||
ListTile(
|
||||
title: Text(label),
|
||||
|
||||
@@ -14,6 +14,7 @@ import '../../core/data/services/favorite_controller.dart';
|
||||
import '../../core/formatting/error_text.dart';
|
||||
import '../../core/widgets/remote_image.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import 'mine_binding.dart';
|
||||
import 'mine_logic.dart';
|
||||
@@ -149,7 +150,7 @@ class _MyPageState extends State<MyPage> {
|
||||
),
|
||||
);
|
||||
return Theme(
|
||||
data: ThemeData.dark(useMaterial3: true),
|
||||
data: AppTheme.dark(),
|
||||
child: widget.embedded
|
||||
? content
|
||||
: Scaffold(backgroundColor: const Color(0xFF0B0C10), body: content),
|
||||
@@ -162,7 +163,7 @@ class _MyPageState extends State<MyPage> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.play_circle_fill_rounded, color: _orange, size: 27),
|
||||
const Icon(Icons.play_circle_fill_rounded, color: _accent, size: 27),
|
||||
const SizedBox(width: 7),
|
||||
Text(
|
||||
l10n.myListTab,
|
||||
@@ -407,7 +408,7 @@ class _MyPageState extends State<MyPage> {
|
||||
title: Text(l10n.earnRewards),
|
||||
trailing: Text(
|
||||
l10n.freeBonus,
|
||||
style: const TextStyle(color: AppColors.blue, fontSize: 12),
|
||||
style: const TextStyle(color: AppColors.blueSoft, fontSize: 12),
|
||||
),
|
||||
onTap: () => _mockSnack(l10n),
|
||||
),
|
||||
@@ -505,7 +506,7 @@ class _MyPageState extends State<MyPage> {
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: logic.section == index
|
||||
? AppColors.blue
|
||||
? AppColors.bluePressed
|
||||
: const Color(0xFF24262E),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
@@ -540,7 +541,7 @@ class _MyPageState extends State<MyPage> {
|
||||
showCheckmark: false,
|
||||
visualDensity: VisualDensity.compact,
|
||||
backgroundColor: const Color(0xFF242328),
|
||||
selectedColor: _orange,
|
||||
selectedColor: _accent,
|
||||
labelStyle: TextStyle(
|
||||
color: logic.filter == index
|
||||
? Colors.white
|
||||
@@ -613,7 +614,7 @@ class _MyPageState extends State<MyPage> {
|
||||
backgroundColor: Color(0xFF313137),
|
||||
child: Icon(
|
||||
Icons.play_arrow_rounded,
|
||||
color: _orange,
|
||||
color: _accent,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
@@ -872,12 +873,12 @@ class _NavItem extends StatelessWidget {
|
||||
Icon(
|
||||
icon,
|
||||
size: 21,
|
||||
color: selected ? _orange : const Color(0xFFCAC4C7),
|
||||
color: selected ? _accent : const Color(0xFFCAC4C7),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: selected ? _orange : const Color(0xFFCAC4C7),
|
||||
color: selected ? _accent : const Color(0xFFCAC4C7),
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
@@ -893,4 +894,4 @@ String _clock(Duration duration) {
|
||||
return '${(seconds ~/ 60).toString().padLeft(2, '0')}:${(seconds % 60).toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
const _orange = AppColors.blue;
|
||||
const _accent = AppColors.blueSoft;
|
||||
@@ -20,6 +20,7 @@ import '../../core/data/services/playback_queue.dart';
|
||||
import '../../core/data/services/playback_session.dart';
|
||||
import '../../core/data/services/video_player_port.dart';
|
||||
import '../../core/formatting/localized_content.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../discover/discover_logic.dart';
|
||||
import '../discover/discover_view.dart';
|
||||
@@ -48,12 +49,15 @@ class PlayerLogic extends ChangeNotifier {
|
||||
Episode? trackedEpisode;
|
||||
Drama? trackedDrama;
|
||||
DateTime lastProgressWrite = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
/// 进度上报节流:契约建议每 10 秒一次
|
||||
Duration progressInterval = const Duration(seconds: 10);
|
||||
Future<void> pendingWrite = Future<void>.value();
|
||||
|
||||
/// 服务端续看点:进入播放页时定位到"那一集的哪一秒"
|
||||
ResumePoint? resumePoint;
|
||||
bool resumeLoaded = false;
|
||||
|
||||
/// 票据三态:加载中 / 失败(可重试)/ 被后端判定为不可播放
|
||||
bool ticketLoading = false;
|
||||
String? ticketError;
|
||||
@@ -86,6 +90,7 @@ class PlayerLogic extends ChangeNotifier {
|
||||
ValueChanged<Drama>? onDramaChanged;
|
||||
ValueChanged<bool>? onImmersiveChanged;
|
||||
bool _disposed = false;
|
||||
|
||||
/// [start] 是否已经跑过:没跑过时 session/pages 还是 late 未初始化,
|
||||
/// dispose 不能直接碰它们(例如页面构建失败或纯逻辑测试)。
|
||||
bool _started = false;
|
||||
@@ -702,7 +707,7 @@ class PlayerLogic extends ChangeNotifier {
|
||||
) {
|
||||
final selected = episode.number == currentNumber;
|
||||
return Material(
|
||||
color: selected ? const Color(0xFF2F7BFF) : const Color(0xFF2A2C33),
|
||||
color: selected ? AppColors.bluePressed : const Color(0xFF2A2C33),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
|
||||
@@ -15,6 +15,8 @@ import '../../core/data/services/playback_session.dart';
|
||||
import '../../core/data/services/video_player_port.dart';
|
||||
import '../../core/formatting/error_text.dart';
|
||||
import '../../core/formatting/localized_content.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../core/widgets/auth_gate.dart';
|
||||
import '../../core/widgets/download_action.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
@@ -130,7 +132,7 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.light,
|
||||
child: Theme(
|
||||
data: ThemeData.dark(useMaterial3: true),
|
||||
data: AppTheme.dark(),
|
||||
child: widget.embedded
|
||||
? content
|
||||
: Scaffold(backgroundColor: Colors.black, body: content),
|
||||
@@ -203,7 +205,7 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
// 票据三态之一:起播前正在向后端要签名地址
|
||||
if (selected && logic.ticketLoading)
|
||||
const Center(
|
||||
child: CircularProgressIndicator(color: Color(0xFF2F7BFF)),
|
||||
child: CircularProgressIndicator(color: AppColors.blueSoft),
|
||||
),
|
||||
// 票据三态之二:拿票失败(断网/服务异常)→ 可重试,不假装在加载
|
||||
if (selected && !logic.ticketLoading && logic.ticketError != null)
|
||||
@@ -235,7 +237,7 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
),
|
||||
if (selected && logic.session.loading)
|
||||
const Center(
|
||||
child: CircularProgressIndicator(color: Color(0xFF2F7BFF)),
|
||||
child: CircularProgressIndicator(color: AppColors.blueSoft),
|
||||
),
|
||||
if (selected && !ready && logic.session.error != null)
|
||||
Center(
|
||||
@@ -764,7 +766,7 @@ class _RailAction extends StatelessWidget {
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints.tightFor(width: 42, height: 36),
|
||||
color: selected ? const Color(0xFF8FB4FF) : Colors.white,
|
||||
color: selected ? AppColors.blueSoft : Colors.white,
|
||||
icon: Icon(icon, size: 24),
|
||||
),
|
||||
Text(
|
||||
@@ -804,16 +806,14 @@ class _BottomAction extends StatelessWidget {
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: selected ? const Color(0xFF2F7BFF) : const Color(0xFFF3EEEA),
|
||||
color: selected ? AppColors.blueSoft : const Color(0xFFF3EEEA),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: selected
|
||||
? const Color(0xFF2F7BFF)
|
||||
: const Color(0xFFF3EEEA),
|
||||
color: selected ? AppColors.blueSoft : const Color(0xFFF3EEEA),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
|
||||
/// Keeps seeks within the playable range so dragging to the edge does not
|
||||
/// trigger the player's completion callback before playback resumes.
|
||||
Duration seekPositionForFraction(double fraction, Duration duration) {
|
||||
@@ -118,7 +120,7 @@ class _SeekProgressBarState extends State<SeekProgressBar> {
|
||||
child: SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
trackHeight: scrubbing ? 4 : 3,
|
||||
activeTrackColor: const Color(0xFF2F7BFF),
|
||||
activeTrackColor: AppColors.blueSoft,
|
||||
inactiveTrackColor: Colors.white30,
|
||||
thumbColor: Colors.white,
|
||||
thumbShape: RoundSliderThumbShape(
|
||||
|
||||
@@ -12,7 +12,9 @@ import '../../core/data/repositories/watch_history_repository.dart';
|
||||
import '../../core/data/services/auth_controller.dart';
|
||||
import '../../core/data/services/watch_records_controller.dart';
|
||||
import '../../core/formatting/error_text.dart';
|
||||
import '../../core/formatting/localized_date_time.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../core/widgets/auth_gate.dart';
|
||||
import '../../core/widgets/remote_image.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
@@ -27,15 +29,18 @@ class WatchRecordsPage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final signedIn = context.watch<AuthController>().isSignedIn;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return Scaffold(
|
||||
key: const Key('watch-records-page'),
|
||||
backgroundColor: const Color(0xFF0B0C10),
|
||||
appBar: AppBar(
|
||||
return Theme(
|
||||
data: AppTheme.dark(),
|
||||
child: Scaffold(
|
||||
key: const Key('watch-records-page'),
|
||||
backgroundColor: const Color(0xFF0B0C10),
|
||||
foregroundColor: Colors.white,
|
||||
title: Text(l10n.watchHistory),
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xFF0B0C10),
|
||||
foregroundColor: Colors.white,
|
||||
title: Text(l10n.watchHistory),
|
||||
),
|
||||
body: signedIn ? const _WatchRecordsBody() : const AuthGatePanel(),
|
||||
),
|
||||
body: signedIn ? const _WatchRecordsBody() : const AuthGatePanel(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -276,7 +281,10 @@ class _RecordTile extends StatelessWidget {
|
||||
if (record.progress.updatedAt != null)
|
||||
Text(
|
||||
l10n.updatedAtLabel(
|
||||
_formatUpdatedAt(record.progress.updatedAt!),
|
||||
formatLocalizedDateTime(
|
||||
context,
|
||||
record.progress.updatedAt!,
|
||||
),
|
||||
),
|
||||
key: ValueKey('watch-record-updated-${record.dramaId}'),
|
||||
style: const TextStyle(
|
||||
@@ -375,12 +383,3 @@ String _clock(Duration value) {
|
||||
final seconds = value.inSeconds.remainder(60);
|
||||
return '$minutes:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
String _formatUpdatedAt(DateTime value) {
|
||||
final local = value.toLocal();
|
||||
final month = local.month.toString().padLeft(2, '0');
|
||||
final day = local.day.toString().padLeft(2, '0');
|
||||
final hour = local.hour.toString().padLeft(2, '0');
|
||||
final minute = local.minute.toString().padLeft(2, '0');
|
||||
return '${local.year}-$month-$day $hour:$minute';
|
||||
}
|
||||
Reference in new issue
Block a user