import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:moonwell_launcher/app/design_system/mw_launcher_components.dart'; import 'package:moonwell_launcher/app/design_system/mw_shells.dart'; import 'package:moonwell_launcher/app/home_screen/bloc/home_screen_bloc.dart'; import 'package:moonwell_launcher/app/home_screen/bloc/home_screen_model.dart'; import 'package:moonwell_launcher/app/mw_app.dart'; import 'package:moonwell_launcher/features/launcher/domain/entities/client_sync_status.dart'; import 'package:window_manager/window_manager.dart'; class HomeScreenContent extends StatelessWidget { const HomeScreenContent({super.key}); @override Widget build(BuildContext context) { final model = context.watch().state.model; return MwLauncherShell( news: _buildNews(model), syncActions: _buildSyncActions(context, model), statusBar: _buildStatusBar(model), account: MwAccountAffordance( onSettings: () => _showSettings(context, model), onLogout: () => context.read().add(HomeScreenLogoutRequested()), ), onDrag: windowManager.startDragging, onDoubleTap: () => unawaited(_toggleMaximized()), onMinimize: windowManager.minimize, onMaximizeRestore: () => unawaited(_toggleMaximized()), onClose: () => unawaited(windowManager.destroy()), ); } Widget _buildNews(HomeScreenModel model) { final state = model.isLoadingNews ? MwNewsListState.loading : model.newsItems.isNotEmpty ? MwNewsListState.populated : model.newsErrorMessage != null ? MwNewsListState.failure : MwNewsListState.empty; return MwNewsList( state: state, error: model.newsErrorMessage, items: [ for (final item in model.newsItems) MwNewsItemData( title: item.title, body: item.body, createdAt: item.createdAt, imageUrl: item.imageUrl, ), ], ); } Widget _buildSyncActions(BuildContext context, HomeScreenModel model) { final presentation = _syncPresentation(model); final progress = model.progress.total > 0 ? (model.progress.downloaded / model.progress.total).clamp(0.0, 1.0) : null; return MwSyncActionArea( state: presentation, status: model.statusText, currentPath: model.currentPath, error: model.errorMessage, progress: progress, onPrimary: () { final bloc = context.read(); if (model.phase == HomeScreenPhase.readyToPlay && model.canPlay) { bloc.add(HomeScreenPlayRequested()); } else { bloc.add(HomeScreenSyncRequested()); } }, onPause: model.isSyncing ? () => context.read().add(HomeScreenPauseRequested()) : null, ); } MwSyncPresentation _syncPresentation(HomeScreenModel model) { if (model.phase == HomeScreenPhase.paused) { return MwSyncPresentation.paused; } if (model.phase == HomeScreenPhase.failure) { return MwSyncPresentation.failure; } if (model.phase == HomeScreenPhase.readyToPlay) { return model.statusText.startsWith('Игра запущена') ? MwSyncPresentation.launched : MwSyncPresentation.ready; } if (model.phase != HomeScreenPhase.syncing) { return MwSyncPresentation.install; } return switch (model.syncStage) { ClientSyncStage.downloadingFiles => MwSyncPresentation.downloading, ClientSyncStage.verifyingInstallation => MwSyncPresentation.verifying, ClientSyncStage.completed => MwSyncPresentation.ready, _ => MwSyncPresentation.scanning, }; } Widget _buildStatusBar(HomeScreenModel model) { return MwLauncherStatusBar( status: model.outputPath?.toFilePath() ?? 'Папка установки не выбрана', fileCount: model.totalFiles > 0 ? '${model.processedFiles}/${model.totalFiles}' : null, speed: model.progress.speed > 0 ? _formatSpeed(model.progress.speed) : null, eta: model.progress.eta > Duration.zero ? _formatDuration(model.progress.eta) : null, buildHash: _shortHash(model.localBuildHash ?? model.remoteBuildHash), ); } Future _showSettings( BuildContext context, HomeScreenModel model, ) async { final bloc = context.read(); final themeController = LauncherThemeScope.of(context); await showDialog( context: context, builder: (dialogContext) => Dialog( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 460, maxHeight: 620), child: AnimatedBuilder( animation: themeController, builder: (context, _) => SingleChildScrollView( child: MwSettingsPanel( installationPath: model.outputPath?.toFilePath(), themeVariant: themeController.variant, syncActive: model.isSyncing, onChooseDirectory: () { Navigator.of(dialogContext).pop(); bloc.add(HomeScreenOutputDirRequested()); }, onThemeChanged: (variant) { unawaited(themeController.setVariant(variant)); }, onVerify: () { Navigator.of(dialogContext).pop(); bloc.add(HomeScreenSyncRequested()); }, onLogout: () { Navigator.of(dialogContext).pop(); bloc.add(HomeScreenLogoutRequested()); }, ), ), ), ), ), ); } Future _toggleMaximized() async { if (await windowManager.isMaximized()) { await windowManager.unmaximize(); } else { await windowManager.maximize(); } } String _formatSpeed(int bytesPerSecond) { const megabyte = 1024 * 1024; if (bytesPerSecond >= megabyte) { return '${(bytesPerSecond / megabyte).toStringAsFixed(1)} MB/s'; } return '${(bytesPerSecond / 1024).toStringAsFixed(0)} KB/s'; } String _formatDuration(Duration duration) { final hours = duration.inHours.toString().padLeft(2, '0'); final minutes = (duration.inMinutes % 60).toString().padLeft(2, '0'); final seconds = (duration.inSeconds % 60).toString().padLeft(2, '0'); return '$hours:$minutes:$seconds'; } String? _shortHash(String? hash) { if (hash == null || hash.isEmpty) return null; return hash.length <= 8 ? hash : hash.substring(0, 8); } }