Files
2026-07-14 11:11:36 +08:00

402 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../providers/session_provider.dart';
import '../../providers/sm_account_provider.dart';
import '../../providers/update_provider.dart';
import '../../shared/app_info.dart';
import '../../shared/theme/app_theme.dart';
import '../../shared/widgets/app_card.dart';
import '../../shared/widgets/app_loading_ring.dart';
import '../../shared/widgets/app_inline_alert.dart';
import '../../shared/widgets/app_sliver_header.dart';
import '../../shared/widgets/page_background.dart';
import '../../shared/widgets/server_avatar.dart';
import '../modules/modules_home_page.dart';
import '../settings/settings_page.dart';
class ProfilePage extends ConsumerWidget {
const ProfilePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final activeSession = ref.watch(activeSessionProvider);
final account = ref.watch(smAccountProvider).value;
final appVersion =
ref.watch(appVersionProvider).value ?? kSmPlayerVersionFallback;
final updateState = ref.watch(updateProvider);
return Scaffold(
backgroundColor: Colors.transparent,
extendBodyBehindAppBar: true,
body: Stack(
children: [
const Positioned.fill(child: PageBackground()),
CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(
parent: BouncingScrollPhysics(),
),
slivers: [
const AppSliverHeader(title: '我的'),
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 32),
sliver: SliverList(
delegate: SliverChildListDelegate.fixed([
_SearchEntry(onTap: () => context.push('/global-search')),
const SizedBox(height: 12),
_ServerAccountCard(
serverName: activeSession?.serverName ?? '未连接服务器',
userName: activeSession?.userName ?? '添加或登录 Emby 服务器',
iconUrl: '',
active: activeSession != null,
onTap: () => context.go('/servers'),
),
const SizedBox(height: 12),
_SmAccountCard(
account: account,
onTap: () => context.push('/account-center'),
),
const SizedBox(height: 18),
const _SectionTitle('快捷入口'),
_MenuCard(
children: [
_ProfileMenuItem(
icon: Icons.history_rounded,
title: '观看历史',
subtitle: '跨服务器继续观看',
onTap: () => context.go('/history'),
),
_ProfileMenuItem(
icon: Icons.favorite_outline_rounded,
title: '我的收藏',
subtitle: '查看所有服务器收藏',
onTap: () => context.go('/favorites-all'),
),
_ProfileMenuItem(
icon: Icons.new_releases_outlined,
title: '最近添加',
subtitle: '按服务器聚合最新内容',
onTap: () => context.go('/recent'),
),
],
),
const SizedBox(height: 18),
const _SectionTitle('应用'),
_MenuCard(
children: [
_ProfileMenuItem(
icon: Icons.extension_rounded,
title: '模块中心',
subtitle: '导入、配置与浏览 ForwardWidgets 模块',
onTap: () => showModuleCenterSheet(context),
),
_ProfileMenuItem(
icon: Icons.settings_outlined,
title: '设置',
subtitle: '播放、账号与外观',
onTap: () => showSettingsDialog(context),
),
_ProfileMenuItem(
icon: Icons.update_outlined,
title: '检查更新',
subtitle: _updateSubtitle(updateState, appVersion),
trailing: updateState.status == UpdateStatus.checking
? const AppLoadingRing(size: 18)
: null,
onTap:
updateState.status == UpdateStatus.checking ||
updateState.status ==
UpdateStatus.downloading ||
updateState.status ==
UpdateStatus.readyToInstall
? null
: () => ref
.read(updateProvider.notifier)
.manualCheck(),
),
],
),
if (updateState.status == UpdateStatus.error) ...[
const SizedBox(height: 12),
AppInlineAlert(
message: updateState.error ?? '检查更新失败',
tone: AppAlertTone.error,
),
],
]),
),
),
],
),
],
),
);
}
String _updateSubtitle(UpdateState state, String appVersion) {
return switch (state.status) {
UpdateStatus.checking => '当前版本 $appVersion,正在检查',
UpdateStatus.upToDate => '当前版本 $appVersion,已是最新版本',
UpdateStatus.available => '发现新版本 ${state.info?.version ?? ''}',
UpdateStatus.downloading => '正在下载更新',
UpdateStatus.readyToInstall => '正在打开安装程序',
UpdateStatus.error => state.error ?? '检查失败',
UpdateStatus.idle => '当前版本 $appVersion',
};
}
}
class _SearchEntry extends StatelessWidget {
final VoidCallback onTap;
const _SearchEntry({required this.onTap});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AppCard(
onTap: onTap,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
Icon(Icons.search_rounded, color: theme.colorScheme.primary),
const SizedBox(width: 12),
Expanded(
child: Text(
'搜索电影、剧集、演员',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.62),
),
),
),
const Icon(Icons.chevron_right_rounded, size: 20),
],
),
);
}
}
class _ServerAccountCard extends StatelessWidget {
final String serverName;
final String userName;
final String iconUrl;
final bool active;
final VoidCallback onTap;
const _ServerAccountCard({
required this.serverName,
required this.userName,
required this.iconUrl,
required this.active,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AppCard(
onTap: onTap,
padding: const EdgeInsets.all(16),
child: Row(
children: [
ServerAvatar(
name: serverName,
iconUrl: iconUrl,
size: 52,
isActive: active,
showActiveBorder: true,
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
serverName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 3),
Text(
userName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.58),
),
),
],
),
),
const Icon(Icons.chevron_right_rounded),
],
),
);
}
}
class _SmAccountCard extends StatelessWidget {
final SmAccountState? account;
final VoidCallback onTap;
const _SmAccountCard({required this.account, required this.onTap});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final status = account?.status ?? SmAuthStatus.unauthenticated;
final loggedIn = status == SmAuthStatus.authenticated;
final title = loggedIn ? 'player 账号' : '登录 player 账号';
final subtitle = loggedIn
? (account?.email.isNotEmpty == true ? account!.email : '已登录')
: '用于 TMDB、Trakt 等扩展服务';
return AppCard(
onTap: onTap,
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: theme.colorScheme.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(14),
),
child: Icon(
loggedIn ? Icons.verified_user_outlined : Icons.person_add_alt_1,
color: theme.colorScheme.primary,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: theme.textTheme.titleSmall),
const SizedBox(height: 3),
Text(
subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.58),
),
),
],
),
),
const Icon(Icons.chevron_right_rounded),
],
),
);
}
}
class _SectionTitle extends StatelessWidget {
final String text;
const _SectionTitle(this.text);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(4, 0, 4, 8),
child: Text(
text,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: context.appTokens.mutedForeground,
fontWeight: FontWeight.w600,
),
),
);
}
}
class _MenuCard extends StatelessWidget {
final List<Widget> children;
const _MenuCard({required this.children});
@override
Widget build(BuildContext context) {
return AppCard(
padding: EdgeInsets.zero,
child: Column(
children: [
for (var i = 0; i < children.length; i++) ...[
if (i > 0)
Divider(
height: 1,
indent: 56,
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.08),
),
children[i],
],
],
),
);
}
}
class _ProfileMenuItem extends StatelessWidget {
final IconData icon;
final String title;
final String? subtitle;
final Widget? trailing;
final VoidCallback? onTap;
const _ProfileMenuItem({
required this.icon,
required this.title,
this.subtitle,
this.trailing,
this.onTap,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
child: Row(
children: [
Icon(icon, size: 22, color: theme.colorScheme.primary),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: theme.textTheme.bodyMedium),
if (subtitle != null) ...[
const SizedBox(height: 2),
Text(
subtitle!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withValues(
alpha: 0.54,
),
),
),
],
],
),
),
trailing ?? const Icon(Icons.chevron_right_rounded, size: 20),
],
),
),
);
}
}