Files
sm-emby-share/lib/providers/server_sync_provider.dart
2026-07-14 11:11:36 +08:00

191 lines
6.0 KiB
Dart

import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/domain/errors.dart';
import '../shared/utils/app_logger.dart';
import 'danmaku_settings_provider.dart';
import 'di_providers.dart';
import 'server_provider.dart';
import 'session_provider.dart';
import 'sm_account_provider.dart';
enum ServerSyncStatus { success, info, failure, needsLogin, needsVip }
class ServerSyncOutcome {
final ServerSyncStatus status;
final String message;
const ServerSyncOutcome(this.status, this.message);
bool get isError =>
status == ServerSyncStatus.failure ||
status == ServerSyncStatus.needsLogin ||
status == ServerSyncStatus.needsVip;
}
class ServerSyncState {
final bool busy;
final DateTime? lastSyncAt;
final String? lastSyncError;
const ServerSyncState({
this.busy = false,
this.lastSyncAt,
this.lastSyncError,
});
}
class ServerSyncNotifier extends Notifier<ServerSyncState> {
static const _tag = 'ServerSync';
@override
ServerSyncState build() => const ServerSyncState();
Future<ServerSyncOutcome> upload() async {
if (state.busy) {
return const ServerSyncOutcome(ServerSyncStatus.info, '正在同步,请稍候');
}
final access = await _resolveAccess();
if (access.outcome != null) return access.outcome!;
final creds = access.creds!;
state = const ServerSyncState(busy: true);
try {
final uc = await ref.read(uploadServerSyncUseCaseProvider.future);
final r = await uc.execute(
baseUrl: creds.baseUrl,
accessToken: creds.accessToken,
);
final outcome = ServerSyncOutcome(
ServerSyncStatus.success,
'已上传 ${r.serverCount} 个服务器、${r.sessionCount} 个登录态、'
'${r.danmakuSourceCount} 个弹幕源、'
'${r.scriptWidgetCount} 个模块、${r.scriptWidgetSubscriptionCount} 个订阅到云端',
);
state = ServerSyncState(lastSyncAt: DateTime.now());
return outcome;
} on DomainError catch (e) {
state = ServerSyncState(lastSyncError: '上传失败:${e.message}');
return ServerSyncOutcome(ServerSyncStatus.failure, '上传失败:${e.message}');
} catch (e) {
AppLogger.warn(_tag, 'upload failed', e);
state = const ServerSyncState(lastSyncError: '上传失败,请稍后重试');
return const ServerSyncOutcome(ServerSyncStatus.failure, '上传失败,请稍后重试');
}
}
Future<ServerSyncOutcome> pull() async {
if (state.busy) {
return const ServerSyncOutcome(ServerSyncStatus.info, '正在同步,请稍候');
}
final access = await _resolveAccess();
if (access.outcome != null) return access.outcome!;
final creds = access.creds!;
state = const ServerSyncState(busy: true);
try {
final uc = await ref.read(pullServerSyncUseCaseProvider.future);
final r = await uc.execute(
baseUrl: creds.baseUrl,
accessToken: creds.accessToken,
);
if (r.empty) {
return const ServerSyncOutcome(ServerSyncStatus.info, '云端还没有备份');
}
await ref.read(serverListProvider.notifier).refresh();
await ref.read(sessionProvider.notifier).refresh();
ref.invalidate(danmakuSettingsProvider);
ref.invalidate(installedScriptWidgetsProvider);
ref.invalidate(scriptWidgetSubscriptionsProvider);
unawaited(_repairInBackground());
final outcome = ServerSyncOutcome(
ServerSyncStatus.success,
'已从云端恢复 ${r.serverCount} 个服务器、${r.sessionCount} 个登录态、'
'${r.danmakuSourceCount} 个弹幕源、'
'${r.scriptWidgetCount} 个模块、${r.scriptWidgetSubscriptionCount} 个订阅',
);
state = ServerSyncState(lastSyncAt: DateTime.now());
return outcome;
} on DomainError catch (e) {
state = ServerSyncState(lastSyncError: '拉取失败:${e.message}');
return ServerSyncOutcome(ServerSyncStatus.failure, '拉取失败:${e.message}');
} catch (e) {
AppLogger.warn(_tag, 'pull failed', e);
state = const ServerSyncState(lastSyncError: '拉取失败,请稍后重试');
return const ServerSyncOutcome(ServerSyncStatus.failure, '拉取失败,请稍后重试');
}
}
Future<void> _repairInBackground() async {
try {
final uc = await ref.read(repairSessionsUseCaseProvider.future);
final repaired = await uc.execute();
if (repaired > 0) {
await ref.read(sessionProvider.notifier).refresh();
AppLogger.info(_tag, 'repaired $repaired session(s) after pull');
}
} catch (e) {
AppLogger.warn(_tag, 'background repair failed', e);
}
}
Future<({_SyncCredentials? creds, ServerSyncOutcome? outcome})>
_resolveAccess() async {
const needsLogin = (
creds: null,
outcome: ServerSyncOutcome(ServerSyncStatus.needsLogin, '请先登录 sm 账号后再同步'),
);
var account = await ref.read(smAccountProvider.future);
if (!account.canUseDataPlane) return needsLogin;
var token = await ref.read(smAccountProvider.notifier).getAccessToken();
if (token == null || token.isEmpty) return needsLogin;
account = await ref.read(smAccountProvider.future);
if (!account.isVip) {
token = await ref
.read(smAccountProvider.notifier)
.getAccessToken(force: true);
if (token == null || token.isEmpty) return needsLogin;
account = await ref.read(smAccountProvider.future);
}
if (!account.isVip) {
return (
creds: null,
outcome: const ServerSyncOutcome(
ServerSyncStatus.needsVip,
'服务器云同步为 VIP 功能,请开通 VIP 后使用',
),
);
}
return (
creds: _SyncCredentials(baseUrl: account.baseUrl, accessToken: token),
outcome: null,
);
}
}
class _SyncCredentials {
final String baseUrl;
final String accessToken;
const _SyncCredentials({required this.baseUrl, required this.accessToken});
}
final serverSyncProvider =
NotifierProvider<ServerSyncNotifier, ServerSyncState>(
ServerSyncNotifier.new,
);