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

294 lines
7.8 KiB
Dart

import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/contracts/app_update.dart';
import '../features/update/update_service.dart';
import '../shared/utils/app_logger.dart';
import 'shared/settings_persistence.dart';
enum UpdateStatus {
idle,
checking,
upToDate,
available,
downloading,
readyToInstall,
error,
}
class UpdateState {
final UpdateStatus status;
final AppUpdateInfo? info;
final int received;
final int total;
final double speedBytesPerSec;
final String? error;
final bool silent;
final String? installerPath;
const UpdateState({
this.status = UpdateStatus.idle,
this.info,
this.received = 0,
this.total = 0,
this.speedBytesPerSec = 0,
this.error,
this.silent = false,
this.installerPath,
});
UpdateState copyWith({
UpdateStatus? status,
AppUpdateInfo? info,
int? received,
int? total,
double? speedBytesPerSec,
String? error,
bool? silent,
String? installerPath,
bool clearInfo = false,
bool clearError = false,
bool clearInstallerPath = false,
}) {
return UpdateState(
status: status ?? this.status,
info: clearInfo ? null : info ?? this.info,
received: received ?? this.received,
total: total ?? this.total,
speedBytesPerSec: speedBytesPerSec ?? this.speedBytesPerSec,
error: clearError ? null : error ?? this.error,
silent: silent ?? this.silent,
installerPath: clearInstallerPath
? null
: installerPath ?? this.installerPath,
);
}
}
final updateProvider = NotifierProvider<UpdateNotifier, UpdateState>(
UpdateNotifier.new,
);
class UpdateNotifier extends Notifier<UpdateState> with SettingsPersistence {
static const _tag = 'Update';
static const _lastCheckKey = 'smplayer.update.last_check';
static const _silentThrottle = Duration(hours: 12);
final UpdateService _service;
CancelToken? _cancelToken;
DateTime? _lastSpeedSampleAt;
int _lastSpeedSampleBytes = 0;
UpdateNotifier({UpdateService? service})
: _service = service ?? UpdateService();
@override
UpdateState build() => const UpdateState();
Future<void> silentCheck() async {
final prefs = await getPrefs();
final lastCheck = prefs.getInt(_lastCheckKey);
final now = DateTime.now().millisecondsSinceEpoch;
if (lastCheck != null && now - lastCheck < _silentThrottle.inMilliseconds) {
AppLogger.debug(_tag, 'silent check throttled');
return;
}
try {
await prefs.setInt(_lastCheckKey, now);
state = state.copyWith(
status: UpdateStatus.checking,
silent: true,
received: 0,
total: 0,
clearError: true,
clearInstallerPath: true,
);
final result = await _service.check();
if (result.hasUpdate && result.info != null) {
state = state.copyWith(
status: UpdateStatus.available,
info: result.info,
silent: true,
);
} else {
state = state.copyWith(
status: UpdateStatus.idle,
silent: true,
clearInfo: true,
);
}
} catch (e) {
AppLogger.warn(_tag, 'silent check failed', e);
state = state.copyWith(
status: UpdateStatus.idle,
silent: true,
clearError: true,
);
}
}
Future<void> manualCheck() async {
try {
state = state.copyWith(
status: UpdateStatus.checking,
silent: false,
received: 0,
total: 0,
clearError: true,
clearInstallerPath: true,
);
final result = await _service.check();
await persistField(_lastCheckKey, DateTime.now().millisecondsSinceEpoch);
if (result.hasUpdate && result.info != null) {
state = state.copyWith(
status: UpdateStatus.available,
info: result.info,
silent: false,
);
} else {
state = state.copyWith(
status: UpdateStatus.upToDate,
silent: false,
clearInfo: true,
);
}
} catch (e, stack) {
AppLogger.error(_tag, 'manual check failed', e, stack);
state = state.copyWith(
status: UpdateStatus.error,
error: '检查更新失败,请稍后重试',
silent: false,
);
}
}
Future<void> startDownload() async {
final info = state.info;
final asset = info?.selectedAsset;
if (info == null || asset == null) return;
_cancelToken?.cancel('restart download');
final cancelToken = CancelToken();
_cancelToken = cancelToken;
_lastSpeedSampleAt = null;
_lastSpeedSampleBytes = 0;
try {
state = state.copyWith(
status: UpdateStatus.downloading,
received: 0,
total: asset.size,
speedBytesPerSec: 0,
clearError: true,
clearInstallerPath: true,
);
final file = await _service.download(
asset,
cancelToken: cancelToken,
onReceiveProgress: (received, total) {
if (cancelToken.isCancelled) return;
state = state.copyWith(
status: UpdateStatus.downloading,
received: received,
total: total > 0 ? total : asset.size,
speedBytesPerSec: _sampleSpeed(received),
);
},
);
if (cancelToken.isCancelled) return;
state = state.copyWith(
status: UpdateStatus.readyToInstall,
received: state.total,
speedBytesPerSec: 0,
installerPath: file.path,
clearError: true,
);
} on UpdateVerificationException catch (e, stack) {
AppLogger.error(_tag, 'update verification failed', e, stack);
state = state.copyWith(
status: UpdateStatus.error,
speedBytesPerSec: 0,
error: e.message,
);
return;
} on DioException catch (e, stack) {
if (CancelToken.isCancel(e)) {
state = state.copyWith(
status: UpdateStatus.available,
received: 0,
total: asset.size,
speedBytesPerSec: 0,
clearError: true,
);
return;
}
AppLogger.error(_tag, 'download failed', e, stack);
state = state.copyWith(
status: UpdateStatus.error,
speedBytesPerSec: 0,
error: '下载失败,请稍后重试',
);
} catch (e, stack) {
AppLogger.error(_tag, 'download failed', e, stack);
state = state.copyWith(
status: UpdateStatus.error,
speedBytesPerSec: 0,
error: '下载失败,请稍后重试',
);
} finally {
if (identical(_cancelToken, cancelToken)) {
_cancelToken = null;
}
}
}
double? _sampleSpeed(int received) {
final now = DateTime.now();
final lastAt = _lastSpeedSampleAt;
if (lastAt == null) {
_lastSpeedSampleAt = now;
_lastSpeedSampleBytes = received;
return null;
}
final elapsedMs = now.difference(lastAt).inMilliseconds;
if (elapsedMs < 500) return null;
final delta = received - _lastSpeedSampleBytes;
_lastSpeedSampleAt = now;
_lastSpeedSampleBytes = received;
return delta > 0 ? delta * 1000.0 / elapsedMs : 0.0;
}
void cancel() {
_cancelToken?.cancel('cancelled by user');
_cancelToken = null;
final info = state.info;
state = state.copyWith(
status: info == null ? UpdateStatus.idle : UpdateStatus.available,
received: 0,
total: info?.selectedAsset?.size ?? 0,
speedBytesPerSec: 0,
clearError: true,
);
}
Future<void> install() async {
final path = state.installerPath;
if (path == null || path.isEmpty) return;
try {
await _service.launchInstaller(path);
} catch (e, stack) {
AppLogger.error(_tag, 'launch installer failed', e, stack);
state = state.copyWith(status: UpdateStatus.error, error: '安装程序启动失败');
}
}
}