Initial commit
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user