feature(updater): Implement auto-updating system using AppCast protocol

This commit is contained in:
2026-06-22 02:30:50 +04:00
parent c23b4bd0da
commit 602d294da4
18 changed files with 524 additions and 9 deletions
+3
View File
@@ -2,6 +2,9 @@ final class Config {
static const launcherApiBaseUrl = String.fromEnvironment(
'MOONWELL_API_BASE_URL',
);
static const launcherAppcastUrl = String.fromEnvironment(
'MOONWELL_APPCAST_URL',
);
static const launcherLogFileName = 'launcher.log';
static const gameExecutableName = 'Wow.exe';
static const cacheDirectoryName = 'Cache';
@@ -0,0 +1,99 @@
import 'dart:io';
import 'package:auto_updater/auto_updater.dart';
import 'package:injectable/injectable.dart';
import 'package:moonwell_launcher/config.dart';
import 'package:moonwell_launcher/features/launcher/data/launcher_log_service.dart';
abstract interface class LauncherUpdateClient {
Future<void> setFeedUrl(String feedUrl);
Future<void> disableScheduledChecks();
Future<void> checkInBackground();
}
final class AutoUpdaterLauncherUpdateClient implements LauncherUpdateClient {
@override
Future<void> setFeedUrl(String feedUrl) => autoUpdater.setFeedURL(feedUrl);
@override
Future<void> disableScheduledChecks() =>
autoUpdater.setScheduledCheckInterval(0);
@override
Future<void> checkInBackground() =>
autoUpdater.checkForUpdates(inBackground: true);
}
@lazySingleton
class LauncherUpdateService {
LauncherUpdateService(LauncherLogService launcherLogService)
: this.forTesting(
launcherLogService: launcherLogService,
updateClient: AutoUpdaterLauncherUpdateClient(),
isWindows: Platform.isWindows,
);
LauncherUpdateService.forTesting({
required LauncherLogService launcherLogService,
required LauncherUpdateClient updateClient,
required bool isWindows,
}) : _launcherLogService = launcherLogService,
_updateClient = updateClient,
_isWindows = isWindows;
final LauncherLogService _launcherLogService;
final LauncherUpdateClient _updateClient;
final bool _isWindows;
bool _hasChecked = false;
Future<void> checkForUpdates({
String feedUrl = Config.launcherAppcastUrl,
}) async {
if (!_isWindows || _hasChecked) {
return;
}
final normalizedFeedUrl = feedUrl.trim();
if (normalizedFeedUrl.isEmpty) {
await _launcherLogService.info(
'Launcher self-update is disabled because no AppCast URL is configured.',
);
return;
}
final uri = Uri.tryParse(normalizedFeedUrl);
if (uri == null ||
!uri.isAbsolute ||
uri.scheme != 'https' ||
!uri.hasAuthority ||
uri.host.isEmpty) {
await _launcherLogService.error(
'Launcher self-update AppCast URL is invalid.',
details: <String, Object?>{'feedUrl': normalizedFeedUrl},
);
return;
}
_hasChecked = true;
try {
await _updateClient.setFeedUrl(normalizedFeedUrl);
await _updateClient.disableScheduledChecks();
await _updateClient.checkInBackground();
await _launcherLogService.info(
'Launcher self-update check started.',
details: <String, Object?>{'feedUrl': normalizedFeedUrl},
);
} catch (error, stackTrace) {
await _launcherLogService.error(
'Launcher self-update check failed.',
details: <String, Object?>{'feedUrl': normalizedFeedUrl},
error: error,
stackTrace: stackTrace,
);
}
}
}
+5 -2
View File
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:moonwell_launcher/app/mw_app.dart';
import 'package:moonwell_launcher/features/launcher/data/launcher_update_service.dart';
import 'package:moonwell_launcher/service_container.dart';
import 'package:window_manager/window_manager.dart';
@@ -10,6 +11,9 @@ Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await windowManager.ensureInitialized();
await configureDependencies(env: 'flutter');
final launcherUpdateService = getIt<LauncherUpdateService>();
WindowOptions windowOptions = WindowOptions(
size: Size(960, 540),
center: true,
@@ -22,10 +26,9 @@ Future<void> main() async {
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
await launcherUpdateService.checkForUpdates();
}),
);
await configureDependencies(env: 'flutter');
runApp(const MoonWellApp());
}
+4
View File
@@ -18,6 +18,7 @@ import 'features/launcher/application/client_sync_use_case.dart' as _i757;
import 'features/launcher/data/game_installation_service.dart' as _i315;
import 'features/launcher/data/launcher_api_client.dart' as _i703;
import 'features/launcher/data/launcher_log_service.dart' as _i43;
import 'features/launcher/data/launcher_update_service.dart' as _i605;
import 'features/preferences/data/repositories/local_preferences_repository.dart'
as _i658;
import 'features/preferences/domain/repositories/preferences_repository.dart'
@@ -47,6 +48,9 @@ extension GetItInjectableX on _i174.GetIt {
gh.lazySingleton<_i558.FlutterSecureStorage>(
() => flutterSecureStorageModule.storage,
);
gh.lazySingleton<_i605.LauncherUpdateService>(
() => _i605.LauncherUpdateService(gh<_i43.LauncherLogService>()),
);
gh.lazySingleton<_i703.LauncherApiClient>(
() => _i703.LauncherApiClient(
launcherLogService: gh<_i43.LauncherLogService>(),