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

384 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/domain/ports/tmdb_gateway.dart';
import '../../../providers/di_providers.dart';
import '../../../providers/sm_account_provider.dart';
import '../../../providers/tmdb_settings_provider.dart';
import '../../../shared/widgets/app_inline_alert.dart';
import '../../../shared/widgets/app_loading_ring.dart';
import '../../../shared/widgets/setting_select.dart';
import '../utils/settings_labels.dart';
import '../widgets/settings_card.dart';
import '../widgets/settings_text_field_row.dart';
class TmdbSettingsTab extends ConsumerWidget {
final void Function(String message, {AppAlertTone tone}) onFeedback;
const TmdbSettingsTab({super.key, required this.onFeedback});
@override
Widget build(BuildContext context, WidgetRef ref) {
final tmdb = ref.watch(tmdbSettingsProvider);
return SettingsTabScroll(
children: [
tmdb.when(
data: (data) =>
TmdbSettingsSection(state: data, onFeedback: onFeedback),
loading: () => const Padding(
padding: EdgeInsets.symmetric(vertical: 32),
child: Center(child: AppLoadingRing()),
),
error: (e, _) => SettingsCard(children: [settingsLoadError(e)]),
),
],
);
}
}
class TmdbSettingsSection extends ConsumerStatefulWidget {
final TmdbSettingsState state;
final void Function(String message, {AppAlertTone tone}) onFeedback;
const TmdbSettingsSection({
super.key,
required this.state,
required this.onFeedback,
});
@override
ConsumerState<TmdbSettingsSection> createState() =>
TmdbSettingsSectionState();
}
class TmdbSettingsSectionState extends ConsumerState<TmdbSettingsSection> {
late final TextEditingController _apiKeyCtrl;
late final TextEditingController _accessTokenCtrl;
final _apiKeyFocus = FocusNode();
final _accessTokenFocus = FocusNode();
bool _testingConnection = false;
@override
void initState() {
super.initState();
_apiKeyCtrl = TextEditingController(text: widget.state.apiKey);
_accessTokenCtrl = TextEditingController(text: widget.state.accessToken);
}
@override
void didUpdateWidget(covariant TmdbSettingsSection oldWidget) {
super.didUpdateWidget(oldWidget);
if (!_apiKeyFocus.hasFocus && _apiKeyCtrl.text != widget.state.apiKey) {
_apiKeyCtrl.text = widget.state.apiKey;
}
if (!_accessTokenFocus.hasFocus &&
_accessTokenCtrl.text != widget.state.accessToken) {
_accessTokenCtrl.text = widget.state.accessToken;
}
}
@override
void dispose() {
_apiKeyCtrl.dispose();
_accessTokenCtrl.dispose();
_apiKeyFocus.dispose();
_accessTokenFocus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final s = widget.state;
final account = ref.watch(smAccountProvider).value;
final accountStatus = account?.status ?? SmAuthStatus.unauthenticated;
final canRequest = s.canRequestWithAccount(
dataPlaneReady: account?.canUseDataPlane ?? false,
);
final String statusSubtitle;
if (!s.enabled) {
statusSubtitle = '关闭时不请求 TMDB 数据';
} else if (s.accessMode == TmdbAccessMode.smAccount) {
statusSubtitle = switch (accountStatus) {
SmAuthStatus.authenticated => '已启用(内置代理),使用账号 ${account!.email}',
SmAuthStatus.restoring => '已启用(内置代理),正在恢复登录…',
SmAuthStatus.expired => '已启用(内置代理),登录已过期,请重新登录',
SmAuthStatus.degraded => '已启用(内置代理),账号凭据不完整,请重新登录',
SmAuthStatus.unauthenticated => '已启用(内置代理),需登录 sm-server 账号',
};
} else {
statusSubtitle = canRequest ? '已启用,媒体详情和发现页会请求 TMDB 数据' : '已启用,请补全认证信息';
}
return Column(
children: [
SettingsCard(
children: [
SettingRow(
title: '启用 TMDB',
subtitle: statusSubtitle,
trailing: FSwitch(
value: s.enabled,
onChange: (v) =>
ref.read(tmdbSettingsProvider.notifier).setEnabled(v),
),
),
if (s.enabled)
SettingRow(
title: '接入方式',
subtitle: s.accessMode == TmdbAccessMode.smAccount
? null
: '数据接口:${TmdbSettingsState.officialApiBaseUrl}',
trailing: SettingSelect<TmdbAccessMode>(
value: s.accessMode,
options: TmdbAccessMode.values,
labelOf: tmdbAccessModeLabel,
onChanged: (v) {
if (v != null) {
ref.read(tmdbSettingsProvider.notifier).setAccessMode(v);
}
},
),
),
],
),
if (s.enabled && s.accessMode == TmdbAccessMode.smAccount) ...[
_SmAccountTmdbSection(
accountState: account,
language: s.language,
onFeedback: widget.onFeedback,
onTestConnection: _testConnectionSmAccount,
testingConnection: _testingConnection,
),
_LanguageCard(language: s.language),
],
if (s.enabled && s.accessMode == TmdbAccessMode.official) ...[
SettingsCard(
children: [
SettingRow(
title: '认证方式',
trailing: SettingSelect<TmdbAuthMode>(
value: s.authMode,
options: TmdbAuthMode.values,
labelOf: tmdbAuthModeLabel,
onChanged: (v) {
if (v != null) {
ref.read(tmdbSettingsProvider.notifier).setAuthMode(v);
}
},
),
),
if (s.authMode == TmdbAuthMode.apiKey)
SettingsTextFieldRow(
title: 'API Key',
hintText: '粘贴 TMDB API Key',
controller: _apiKeyCtrl,
focusNode: _apiKeyFocus,
onChanged: (v) =>
ref.read(tmdbSettingsProvider.notifier).setApiKey(v),
)
else
SettingsTextFieldRow(
title: 'Bearer Token',
hintText: '粘贴 TMDB Read Access Token',
controller: _accessTokenCtrl,
focusNode: _accessTokenFocus,
obscureText: true,
onChanged: (v) =>
ref.read(tmdbSettingsProvider.notifier).setAccessToken(v),
),
_LanguageRow(language: s.language),
],
),
SettingsCard(
children: [
SettingRow(
title: '配置状态',
subtitle: s.isConfigured ? '认证信息已填写' : '请填写当前认证方式对应的密钥',
trailing: Icon(
s.canRequest ? Icons.check_circle : Icons.info_outline,
size: 18,
color: s.canRequest
? const Color(0xFF52C41A)
: const Color(0xFFFAAD14),
),
),
SettingRow(
title: '测试连接',
subtitle: '请求 TMDB /configuration 验证认证和网络',
trailing: FilledButton.tonalIcon(
onPressed: s.isConfigured && !_testingConnection
? _testConnectionOfficial
: null,
icon: _testingConnection
? const AppLoadingRing(size: 14)
: const Icon(Icons.network_check_rounded, size: 16),
label: const Text('测试'),
),
),
],
),
],
],
);
}
Future<void> _testConnectionOfficial() async {
final s = widget.state;
final accessToken = _accessTokenCtrl.text.trim();
final apiKey = _apiKeyCtrl.text.trim();
final hasCredential = switch (s.authMode) {
TmdbAuthMode.bearer => accessToken.isNotEmpty,
TmdbAuthMode.apiKey => apiKey.isNotEmpty,
};
if (!hasCredential) {
widget.onFeedback('请先填写当前认证方式对应的密钥', tone: AppAlertTone.warning);
return;
}
setState(() => _testingConnection = true);
final ok = await ref
.read(tmdbGatewayProvider)
.testConnection(
TmdbRequestConfig(
apiBaseUrl: TmdbSettingsState.officialApiBaseUrl,
imageBaseUrl: TmdbSettingsState.officialImageBaseUrl,
accessToken: s.authMode == TmdbAuthMode.bearer ? accessToken : '',
apiKey: s.authMode == TmdbAuthMode.apiKey ? apiKey : '',
),
language: s.language,
);
if (!mounted) return;
setState(() => _testingConnection = false);
widget.onFeedback(
ok ? 'TMDB 连接测试成功' : 'TMDB 连接测试失败,请检查密钥或网络',
tone: ok ? AppAlertTone.success : AppAlertTone.error,
);
}
Future<void> _testConnectionSmAccount() async {
setState(() => _testingConnection = true);
final token = await ref
.read(smAccountProvider.notifier)
.getAccessToken(force: true);
if (!mounted) return;
if (token == null) {
setState(() => _testingConnection = false);
widget.onFeedback('无法获取访问凭据,请重新登录账号', tone: AppAlertTone.error);
return;
}
final account = ref.read(smAccountProvider).value;
final accountState = account ?? const SmAccountState();
final ok = await ref
.read(tmdbGatewayProvider)
.testConnection(
TmdbRequestConfig(
apiBaseUrl: accountState.tmdbApiBaseUrl,
imageBaseUrl: TmdbSettingsState.officialImageBaseUrl,
accessToken: token,
apiKey: '',
),
language: widget.state.language,
);
if (!mounted) return;
setState(() => _testingConnection = false);
widget.onFeedback(
ok ? 'TMDB 内置代理测试成功' : 'TMDB 内置代理测试失败,请检查账号或网络',
tone: ok ? AppAlertTone.success : AppAlertTone.error,
);
}
}
class _SmAccountTmdbSection extends ConsumerWidget {
final SmAccountState? accountState;
final String language;
final void Function(String message, {AppAlertTone tone}) onFeedback;
final VoidCallback onTestConnection;
final bool testingConnection;
const _SmAccountTmdbSection({
required this.accountState,
required this.language,
required this.onFeedback,
required this.onTestConnection,
required this.testingConnection,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final dataPlaneReady = accountState?.canUseDataPlane == true;
return SettingsCard(
children: [
SettingRow(
title: '测试连接',
subtitle: '验证当前账号凭据与 TMDB 代理可达性',
trailing: FilledButton.tonalIcon(
onPressed: dataPlaneReady && !testingConnection
? onTestConnection
: null,
icon: testingConnection
? const AppLoadingRing(size: 14)
: const Icon(Icons.network_check_rounded, size: 16),
label: const Text('测试'),
),
),
],
);
}
}
class _LanguageRow extends ConsumerWidget {
final String language;
const _LanguageRow({required this.language});
@override
Widget build(BuildContext context, WidgetRef ref) {
return SettingRow(
title: '语言',
trailing: _LanguageSelect(language: language),
);
}
}
class _LanguageCard extends ConsumerWidget {
final String language;
const _LanguageCard({required this.language});
@override
Widget build(BuildContext context, WidgetRef ref) {
return SettingsCard(
children: [
SettingRow(
title: '语言',
trailing: _LanguageSelect(language: language),
),
],
);
}
}
class _LanguageSelect extends ConsumerWidget {
final String language;
const _LanguageSelect({required this.language});
@override
Widget build(BuildContext context, WidgetRef ref) {
return SettingSelect<String>(
value: language,
options: const ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'ko-KR'],
labelOf: tmdbLanguageLabel,
onChanged: (v) {
if (v != null) {
ref.read(tmdbSettingsProvider.notifier).setLanguage(v);
}
},
);
}
}