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 createState() => TmdbSettingsSectionState(); } class TmdbSettingsSectionState extends ConsumerState { 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( 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( 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 _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 _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( 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); } }, ); } }