правки по лаунчеру
This commit is contained in:
@@ -26,11 +26,14 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
|
||||
required PreferencesRepository preferencesRepository,
|
||||
required LauncherSession session,
|
||||
required ClientManifest manifest,
|
||||
Duration manifestRefreshInterval = const Duration(minutes: 5),
|
||||
Duration realmRefreshInterval = const Duration(minutes: 1),
|
||||
}) : _clientSyncUseCase = clientSyncUseCase,
|
||||
_gameInstallationService = gameInstallationService,
|
||||
_launcherApiClient = launcherApiClient,
|
||||
_preferencesRepository = preferencesRepository,
|
||||
_session = session,
|
||||
_startupManifest = manifest,
|
||||
super(
|
||||
HomeScreenState(
|
||||
model: HomeScreenModel.initial().copyWith(
|
||||
@@ -41,14 +44,28 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
|
||||
) {
|
||||
on<HomeScreenLoad>(_onHomeScreenLoad);
|
||||
on<HomeScreenSyncRequested>(_onHomeScreenSyncRequested);
|
||||
on<HomeScreenManifestRefreshRequested>(
|
||||
_onHomeScreenManifestRefreshRequested,
|
||||
);
|
||||
on<HomeScreenRealmRefreshRequested>(_onHomeScreenRealmRefreshRequested);
|
||||
on<HomeScreenAccountLoadRequested>(_onHomeScreenAccountLoadRequested);
|
||||
on<HomeScreenOutputDirRequested>(_onHomeScreenOutputDirRequested);
|
||||
on<HomeScreenPauseRequested>(_onHomeScreenPauseRequested);
|
||||
on<HomeScreenPlayRequested>(_onHomeScreenPlayRequested);
|
||||
on<HomeScreenGameExited>(_onHomeScreenGameExited);
|
||||
on<HomeScreenLogoutRequested>(_onHomeScreenLogoutRequested);
|
||||
on<HomeScreenSyncStatusChanged>(_onHomeScreenSyncStatusChanged);
|
||||
on<HomeScreenSyncFailed>(_onHomeScreenSyncFailed);
|
||||
|
||||
add(HomeScreenLoad());
|
||||
_manifestRefreshTimer = Timer.periodic(
|
||||
manifestRefreshInterval,
|
||||
(_) => add(HomeScreenManifestRefreshRequested()),
|
||||
);
|
||||
_realmRefreshTimer = Timer.periodic(
|
||||
realmRefreshInterval,
|
||||
(_) => add(HomeScreenRealmRefreshRequested()),
|
||||
);
|
||||
}
|
||||
|
||||
final ClientSyncUseCase _clientSyncUseCase;
|
||||
@@ -58,6 +75,10 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
|
||||
|
||||
StreamSubscription<ClientSyncStatus>? _syncSubscription;
|
||||
final LauncherSession _session;
|
||||
final ClientManifest _startupManifest;
|
||||
Timer? _manifestRefreshTimer;
|
||||
Timer? _realmRefreshTimer;
|
||||
int? _activeGamePid;
|
||||
bool _pauseRequested = false;
|
||||
|
||||
Future<void> _onHomeScreenLoad(
|
||||
@@ -82,6 +103,12 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
|
||||
),
|
||||
);
|
||||
|
||||
if (outputPath != null) {
|
||||
add(HomeScreenSyncRequested(manifest: _startupManifest));
|
||||
}
|
||||
add(HomeScreenRealmRefreshRequested());
|
||||
add(HomeScreenAccountLoadRequested());
|
||||
|
||||
try {
|
||||
final newsItems = await _launcherApiClient.fetchNews(
|
||||
_session.accessToken,
|
||||
@@ -107,11 +134,101 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onHomeScreenAccountLoadRequested(
|
||||
HomeScreenAccountLoadRequested event,
|
||||
Emitter<HomeScreenState> emit,
|
||||
) async {
|
||||
if (!state.model.isAuthenticated) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final account = await _launcherApiClient.fetchAccount(
|
||||
_session.accessToken,
|
||||
);
|
||||
if (!state.model.isAuthenticated) {
|
||||
return;
|
||||
}
|
||||
emit(
|
||||
HomeScreenState(
|
||||
model: state.model.copyWith(
|
||||
accountUsername: account.username.isEmpty ? null : account.username,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
// Account metadata does not block patching or launching the game.
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onHomeScreenRealmRefreshRequested(
|
||||
HomeScreenRealmRefreshRequested event,
|
||||
Emitter<HomeScreenState> emit,
|
||||
) async {
|
||||
if (!state.model.isAuthenticated) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final realms = await _launcherApiClient.fetchRealms();
|
||||
if (!state.model.isAuthenticated) {
|
||||
return;
|
||||
}
|
||||
emit(
|
||||
HomeScreenState(
|
||||
model: state.model.copyWith(
|
||||
realm: realms.isEmpty ? null : realms.first,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
// Keep the last known realm state and retry on the next timer tick.
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onHomeScreenManifestRefreshRequested(
|
||||
HomeScreenManifestRefreshRequested event,
|
||||
Emitter<HomeScreenState> emit,
|
||||
) async {
|
||||
if (!state.model.isAuthenticated ||
|
||||
state.model.isSyncing ||
|
||||
state.model.isGameRunning ||
|
||||
state.model.phase == HomeScreenPhase.paused ||
|
||||
state.model.outputPath == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final manifest = await _launcherApiClient.fetchManifest(
|
||||
_session.accessToken,
|
||||
);
|
||||
|
||||
if (state.model.isSyncing || !state.model.isAuthenticated) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (manifest.buildHash == state.model.localBuildHash) {
|
||||
if (manifest.buildHash != state.model.remoteBuildHash) {
|
||||
emit(
|
||||
HomeScreenState(
|
||||
model: state.model.copyWith(remoteBuildHash: manifest.buildHash),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
add(HomeScreenSyncRequested(manifest: manifest));
|
||||
} catch (_) {
|
||||
// Retry on the next timer tick without replacing the stable UI state.
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onHomeScreenSyncRequested(
|
||||
HomeScreenSyncRequested event,
|
||||
Emitter<HomeScreenState> emit,
|
||||
) async {
|
||||
if (state.model.isSyncing) {
|
||||
if (state.model.isSyncing || state.model.isGameRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -145,6 +262,7 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
|
||||
request: ClientSyncRequest(
|
||||
installationDir: outputPath.toFilePath(),
|
||||
accessToken: _session.accessToken,
|
||||
manifest: event.manifest,
|
||||
),
|
||||
isCancelled: () async => _pauseRequested,
|
||||
),
|
||||
@@ -228,12 +346,30 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
|
||||
try {
|
||||
final installationDir = outputPath.toFilePath();
|
||||
await _gameInstallationService.clearCache(installationDir);
|
||||
await _gameInstallationService.launchGame(installationDir);
|
||||
final process = await _gameInstallationService.launchGame(
|
||||
installationDir,
|
||||
);
|
||||
_activeGamePid = process.pid;
|
||||
unawaited(
|
||||
process.exitCode.then<void>(
|
||||
(exitCode) {
|
||||
if (!isClosed) {
|
||||
add(HomeScreenGameExited(pid: process.pid, exitCode: exitCode));
|
||||
}
|
||||
},
|
||||
onError: (_) {
|
||||
if (!isClosed) {
|
||||
add(HomeScreenGameExited(pid: process.pid, exitCode: null));
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
emit(
|
||||
HomeScreenState(
|
||||
model: state.model.copyWith(
|
||||
phase: HomeScreenPhase.readyToPlay,
|
||||
isGameRunning: true,
|
||||
statusText: 'Игра запущена. Папка Cache очищена перед стартом.',
|
||||
errorMessage: null,
|
||||
),
|
||||
@@ -249,11 +385,39 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onHomeScreenGameExited(
|
||||
HomeScreenGameExited event,
|
||||
Emitter<HomeScreenState> emit,
|
||||
) async {
|
||||
if (_activeGamePid != event.pid) {
|
||||
return;
|
||||
}
|
||||
|
||||
_activeGamePid = null;
|
||||
final hasClientExecutable = await _resolveExecutablePresence(
|
||||
state.model.outputPath,
|
||||
);
|
||||
emit(
|
||||
HomeScreenState(
|
||||
model: state.model.copyWith(
|
||||
phase: _stablePhase(hasClientExecutable),
|
||||
hasClientExecutable: hasClientExecutable,
|
||||
isGameRunning: false,
|
||||
statusText: hasClientExecutable
|
||||
? 'Игра закрыта. Клиент готов к запуску.'
|
||||
: 'Игра закрыта, но Wow.exe больше не найден.',
|
||||
errorMessage: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onHomeScreenLogoutRequested(
|
||||
HomeScreenLogoutRequested event,
|
||||
Emitter<HomeScreenState> emit,
|
||||
) async {
|
||||
_pauseRequested = true;
|
||||
_activeGamePid = null;
|
||||
await _syncSubscription?.cancel();
|
||||
_syncSubscription = null;
|
||||
await _preferencesRepository.clearLauncherSession();
|
||||
@@ -263,6 +427,8 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
|
||||
model: state.model.copyWith(
|
||||
phase: HomeScreenPhase.idle,
|
||||
isAuthenticated: false,
|
||||
isGameRunning: false,
|
||||
accountUsername: null,
|
||||
progress: const DownloadProgress.initial(),
|
||||
statusText: 'Сессия завершена. Войдите снова.',
|
||||
currentPath: null,
|
||||
@@ -274,6 +440,7 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
|
||||
newsItems: const [],
|
||||
isLoadingNews: false,
|
||||
newsErrorMessage: null,
|
||||
realm: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -424,6 +591,9 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
|
||||
@override
|
||||
Future<void> close() async {
|
||||
_pauseRequested = true;
|
||||
_activeGamePid = null;
|
||||
_manifestRefreshTimer?.cancel();
|
||||
_realmRefreshTimer?.cancel();
|
||||
await _syncSubscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,17 @@ sealed class HomeScreenEvent {}
|
||||
|
||||
final class HomeScreenLoad extends HomeScreenEvent {}
|
||||
|
||||
final class HomeScreenSyncRequested extends HomeScreenEvent {}
|
||||
final class HomeScreenSyncRequested extends HomeScreenEvent {
|
||||
final ClientManifest? manifest;
|
||||
|
||||
HomeScreenSyncRequested({this.manifest});
|
||||
}
|
||||
|
||||
final class HomeScreenManifestRefreshRequested extends HomeScreenEvent {}
|
||||
|
||||
final class HomeScreenRealmRefreshRequested extends HomeScreenEvent {}
|
||||
|
||||
final class HomeScreenAccountLoadRequested extends HomeScreenEvent {}
|
||||
|
||||
final class HomeScreenOutputDirRequested extends HomeScreenEvent {}
|
||||
|
||||
@@ -13,6 +23,13 @@ final class HomeScreenPauseRequested extends HomeScreenEvent {}
|
||||
|
||||
final class HomeScreenPlayRequested extends HomeScreenEvent {}
|
||||
|
||||
final class HomeScreenGameExited extends HomeScreenEvent {
|
||||
final int pid;
|
||||
final int? exitCode;
|
||||
|
||||
HomeScreenGameExited({required this.pid, required this.exitCode});
|
||||
}
|
||||
|
||||
final class HomeScreenLogoutRequested extends HomeScreenEvent {}
|
||||
|
||||
final class HomeScreenSyncStatusChanged extends HomeScreenEvent {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:moonwell_launcher/features/downloader/domain/entities/download_progress.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/domain/entities/client_sync_status.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_news_item.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_realm.dart';
|
||||
|
||||
enum HomeScreenPhase {
|
||||
idle,
|
||||
@@ -22,6 +23,8 @@ final class HomeScreenModel {
|
||||
final HomeScreenPhase phase;
|
||||
final bool isAuthenticated;
|
||||
final bool hasClientExecutable;
|
||||
final bool isGameRunning;
|
||||
final String? accountUsername;
|
||||
final String statusText;
|
||||
final String? currentPath;
|
||||
final String? errorMessage;
|
||||
@@ -32,6 +35,7 @@ final class HomeScreenModel {
|
||||
final List<LauncherNewsItem> newsItems;
|
||||
final bool isLoadingNews;
|
||||
final String? newsErrorMessage;
|
||||
final LauncherRealm? realm;
|
||||
final ClientSyncStage? syncStage;
|
||||
|
||||
const HomeScreenModel({
|
||||
@@ -40,6 +44,8 @@ final class HomeScreenModel {
|
||||
required this.phase,
|
||||
required this.isAuthenticated,
|
||||
required this.hasClientExecutable,
|
||||
required this.isGameRunning,
|
||||
required this.accountUsername,
|
||||
required this.statusText,
|
||||
required this.currentPath,
|
||||
required this.errorMessage,
|
||||
@@ -50,6 +56,7 @@ final class HomeScreenModel {
|
||||
required this.newsItems,
|
||||
required this.isLoadingNews,
|
||||
required this.newsErrorMessage,
|
||||
required this.realm,
|
||||
required this.syncStage,
|
||||
});
|
||||
|
||||
@@ -59,6 +66,8 @@ final class HomeScreenModel {
|
||||
phase = HomeScreenPhase.idle,
|
||||
isAuthenticated = false,
|
||||
hasClientExecutable = false,
|
||||
isGameRunning = false,
|
||||
accountUsername = null,
|
||||
statusText = 'Загрузка...',
|
||||
currentPath = null,
|
||||
errorMessage = null,
|
||||
@@ -69,6 +78,7 @@ final class HomeScreenModel {
|
||||
newsItems = const <LauncherNewsItem>[],
|
||||
isLoadingNews = true,
|
||||
newsErrorMessage = null,
|
||||
realm = null,
|
||||
syncStage = null;
|
||||
|
||||
bool get isBusy =>
|
||||
@@ -77,7 +87,7 @@ final class HomeScreenModel {
|
||||
|
||||
bool get isSyncing => phase == HomeScreenPhase.syncing;
|
||||
|
||||
bool get canPlay => hasClientExecutable && !isBusy;
|
||||
bool get canPlay => hasClientExecutable && !isBusy && !isGameRunning;
|
||||
|
||||
HomeScreenModel copyWith({
|
||||
DownloadProgress? progress,
|
||||
@@ -85,6 +95,8 @@ final class HomeScreenModel {
|
||||
HomeScreenPhase? phase,
|
||||
bool? isAuthenticated,
|
||||
bool? hasClientExecutable,
|
||||
bool? isGameRunning,
|
||||
Object? accountUsername = _sentinel,
|
||||
String? statusText,
|
||||
Object? currentPath = _sentinel,
|
||||
Object? errorMessage = _sentinel,
|
||||
@@ -95,6 +107,7 @@ final class HomeScreenModel {
|
||||
List<LauncherNewsItem>? newsItems,
|
||||
bool? isLoadingNews,
|
||||
Object? newsErrorMessage = _sentinel,
|
||||
Object? realm = _sentinel,
|
||||
Object? syncStage = _sentinel,
|
||||
}) {
|
||||
return HomeScreenModel(
|
||||
@@ -105,6 +118,10 @@ final class HomeScreenModel {
|
||||
phase: phase ?? this.phase,
|
||||
isAuthenticated: isAuthenticated ?? this.isAuthenticated,
|
||||
hasClientExecutable: hasClientExecutable ?? this.hasClientExecutable,
|
||||
isGameRunning: isGameRunning ?? this.isGameRunning,
|
||||
accountUsername: accountUsername == _sentinel
|
||||
? this.accountUsername
|
||||
: accountUsername as String?,
|
||||
statusText: statusText ?? this.statusText,
|
||||
currentPath: currentPath == _sentinel
|
||||
? this.currentPath
|
||||
@@ -125,6 +142,7 @@ final class HomeScreenModel {
|
||||
newsErrorMessage: newsErrorMessage == _sentinel
|
||||
? this.newsErrorMessage
|
||||
: newsErrorMessage as String?,
|
||||
realm: realm == _sentinel ? this.realm : realm as LauncherRealm?,
|
||||
syncStage: syncStage == _sentinel
|
||||
? this.syncStage
|
||||
: syncStage as ClientSyncStage?,
|
||||
|
||||
Reference in New Issue
Block a user