191 lines
6.0 KiB
Dart
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,
|
|
);
|