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