384 lines
13 KiB
Dart
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);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|