Initial commit
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:moonwell_launcher/app/home_screen/bloc/home_screen_model.dart';
|
||||
import 'package:moonwell_launcher/features/downloader/application/download_manager.dart';
|
||||
import 'package:moonwell_launcher/features/downloader/domain/entities/download_progress.dart';
|
||||
import 'package:moonwell_launcher/features/downloader/domain/entities/download_request.dart';
|
||||
import 'package:moonwell_launcher/features/preferences/domain/repositories/preferences_repository.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
part 'home_screen_event.dart';
|
||||
part 'home_screen_state.dart';
|
||||
|
||||
const _downloadId = 'client';
|
||||
|
||||
class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
|
||||
final DownloadManager _downloadManager;
|
||||
final PreferencesRepository _preferencesRepository;
|
||||
|
||||
StreamSubscription<DownloadProgress>? _downloadProgressSubscription;
|
||||
|
||||
HomeScreenBloc({
|
||||
required DownloadManager downloadManager,
|
||||
required PreferencesRepository preferencesRepository,
|
||||
}) : _downloadManager = downloadManager,
|
||||
_preferencesRepository = preferencesRepository,
|
||||
super(HomeScreenInitialState(model: HomeScreenModel.initial())) {
|
||||
_setupHandlers();
|
||||
|
||||
add(HomeScreenLoad());
|
||||
}
|
||||
|
||||
void _setupHandlers() {
|
||||
on<HomeScreenLoad>(_onHomeScreenLoad);
|
||||
on<HomeScreenDownloadRequested>(_onHomeScreenDownloadRequested);
|
||||
|
||||
on<HomeScreenDownloadPaused>((event, emit) {
|
||||
// _downloadManager.pauseDownload();
|
||||
// emit(HomeScreenDownloadPausedState());
|
||||
});
|
||||
|
||||
on<HomeScreenDownloadProgressUpdated>(
|
||||
_onHomeScreenDownloadProgressUpdated,
|
||||
transformer: (events, mapper) =>
|
||||
events.throttleTime(const Duration(seconds: 2)).switchMap(mapper),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onHomeScreenLoad(
|
||||
HomeScreenLoad event,
|
||||
Emitter<HomeScreenState> emit,
|
||||
) async {
|
||||
final outputDir = await _preferencesRepository.getOutputDir();
|
||||
|
||||
emit(
|
||||
HomeScreenReadyToDownloadState(
|
||||
model: state.model.copyWith(outputPath: outputDir),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onHomeScreenDownloadRequested(
|
||||
HomeScreenDownloadRequested event,
|
||||
Emitter<HomeScreenState> emit,
|
||||
) async {
|
||||
if (state.model.outputPath == null) {
|
||||
final directory = await FilePicker.platform.getDirectoryPath(
|
||||
dialogTitle: 'Please select installation directory',
|
||||
);
|
||||
|
||||
if (directory == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _preferencesRepository.setOutputDir(Uri.directory(directory));
|
||||
|
||||
emit(
|
||||
HomeScreenReadyToDownloadState(
|
||||
model: state.model.copyWith(outputPath: Uri.directory(directory)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_downloadProgressSubscription = _downloadManager
|
||||
.start(
|
||||
id: _downloadId,
|
||||
request: DownloadRequest(
|
||||
destinationPath: state.model.outputPath!.toFilePath(),
|
||||
),
|
||||
)
|
||||
.listen((progress) => add(HomeScreenDownloadProgressUpdated(progress)));
|
||||
}
|
||||
|
||||
void _onHomeScreenDownloadProgressUpdated(
|
||||
HomeScreenDownloadProgressUpdated event,
|
||||
Emitter<HomeScreenState> emit,
|
||||
) {
|
||||
emit(
|
||||
HomeScreenDownloadingState(
|
||||
model: state.model.copyWith(progress: event.progress),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _downloadProgressSubscription?.cancel();
|
||||
await _downloadManager.cancel(_downloadId);
|
||||
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
part of 'home_screen_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class HomeScreenEvent {}
|
||||
|
||||
final class HomeScreenLoad extends HomeScreenEvent {}
|
||||
|
||||
final class HomeScreenDownloadRequested extends HomeScreenEvent {}
|
||||
|
||||
final class HomeScreenDownloadPaused extends HomeScreenEvent {}
|
||||
|
||||
final class HomeScreenDownloadProgressUpdated extends HomeScreenEvent {
|
||||
final DownloadProgress progress;
|
||||
|
||||
HomeScreenDownloadProgressUpdated(this.progress);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:moonwell_launcher/features/downloader/domain/entities/download_progress.dart';
|
||||
|
||||
@immutable
|
||||
final class HomeScreenModel {
|
||||
/// The current download progress.
|
||||
final DownloadProgress progress;
|
||||
|
||||
/// The output path for the downloaded file.
|
||||
final Uri? outputPath;
|
||||
|
||||
const HomeScreenModel({required this.progress, this.outputPath});
|
||||
|
||||
const HomeScreenModel.initial()
|
||||
: progress = const DownloadProgress.initial(),
|
||||
outputPath = null;
|
||||
|
||||
HomeScreenModel copyWith({DownloadProgress? progress, Uri? outputPath}) {
|
||||
return HomeScreenModel(
|
||||
progress: progress ?? this.progress,
|
||||
outputPath: outputPath ?? this.outputPath,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
part of 'home_screen_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class HomeScreenState {
|
||||
final HomeScreenModel model;
|
||||
|
||||
const HomeScreenState({required this.model});
|
||||
}
|
||||
|
||||
final class HomeScreenInitialState extends HomeScreenState {
|
||||
const HomeScreenInitialState({required super.model});
|
||||
}
|
||||
|
||||
final class HomeScreenReadyToDownloadState extends HomeScreenState {
|
||||
const HomeScreenReadyToDownloadState({required super.model});
|
||||
}
|
||||
|
||||
final class HomeScreenDownloadPausedState extends HomeScreenState {
|
||||
const HomeScreenDownloadPausedState({required super.model});
|
||||
}
|
||||
|
||||
final class HomeScreenDownloadingState extends HomeScreenState {
|
||||
const HomeScreenDownloadingState({required super.model});
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:moonwell_launcher/app/home_screen/bloc/home_screen_bloc.dart';
|
||||
import 'package:moonwell_launcher/app/home_screen/left_pane.dart';
|
||||
import 'package:moonwell_launcher/app/theme/mw_theme.dart';
|
||||
import 'package:moonwell_launcher/features/downloader/application/download_manager.dart';
|
||||
import 'package:moonwell_launcher/features/preferences/domain/repositories/preferences_repository.dart';
|
||||
import 'package:moonwell_launcher/service_container.dart';
|
||||
|
||||
class HomeScreen extends StatelessWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return BlocProvider<HomeScreenBloc>(
|
||||
create: (context) => HomeScreenBloc(
|
||||
downloadManager: getIt<DownloadManager>(),
|
||||
preferencesRepository: getIt<PreferencesRepository>(),
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(
|
||||
// subtle vignette for “night” vibe
|
||||
gradient: RadialGradient(
|
||||
center: Alignment(0, -0.5),
|
||||
radius: 1.2,
|
||||
colors: [MWColors.deepNavy, MWColors.abyss],
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: LeftPane(
|
||||
title: Text(
|
||||
'MoonWell',
|
||||
style: theme.textTheme.headlineLarge,
|
||||
),
|
||||
progress: 0.0,
|
||||
isDownloading: false,
|
||||
kbps: 0.0,
|
||||
eta: Duration.zero,
|
||||
onStart: () {
|
||||
context.read<HomeScreenBloc>().add(
|
||||
HomeScreenDownloadRequested(),
|
||||
);
|
||||
},
|
||||
onPause: () {},
|
||||
onReset: () {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:moonwell_launcher/app/home_screen/bloc/home_screen_bloc.dart';
|
||||
import 'package:moonwell_launcher/app/home_screen/widgets/progress_bar.dart';
|
||||
import 'package:moonwell_launcher/app/theme/mw_theme.dart';
|
||||
import 'package:pixelarticons/pixel.dart';
|
||||
|
||||
class LeftPane extends StatelessWidget {
|
||||
const LeftPane({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.progress,
|
||||
required this.isDownloading,
|
||||
required this.kbps,
|
||||
required this.eta,
|
||||
required this.onStart,
|
||||
required this.onPause,
|
||||
required this.onReset,
|
||||
});
|
||||
|
||||
final Widget title;
|
||||
final double progress;
|
||||
final bool isDownloading;
|
||||
final double kbps;
|
||||
final Duration eta;
|
||||
final VoidCallback onStart;
|
||||
final VoidCallback onPause;
|
||||
final VoidCallback onReset;
|
||||
|
||||
String _speedLabel(BuildContext context) {
|
||||
final kbps = context.read<HomeScreenBloc>().state.model.progress.speed;
|
||||
|
||||
if (kbps <= 0) return '—';
|
||||
if (kbps >= 1024) {
|
||||
return '${(kbps / 1024).toStringAsFixed(2)} MB/s';
|
||||
}
|
||||
return '${kbps.toStringAsFixed(0)} kB/s';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final deco = Theme.of(context).extension<MoonWellDecorations>()!;
|
||||
|
||||
return BlocBuilder<HomeScreenBloc, HomeScreenState>(
|
||||
builder: (context, state) {
|
||||
final percent = _resolvePercentage(context);
|
||||
final eta = state.model.progress.eta;
|
||||
|
||||
final canPlay = progress >= 1.0 && state is! HomeScreenDownloadingState;
|
||||
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
title,
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Classic+ Wrath of the Lich King experience',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: cs.onSurface.withAlpha(200),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
|
||||
// Progress bar
|
||||
GildedProgressBar(value: progress),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'$percent%',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const Spacer(),
|
||||
Icon(Pixel.speedfast, size: 18, color: cs.onSurfaceVariant),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
_speedLabel(context),
|
||||
style: TextStyle(color: cs.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Icon(
|
||||
Pixel.timeline, // Updated to use PixelArtIcons
|
||||
size: 18,
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
eta == Duration.zero
|
||||
? '—'
|
||||
: '${eta.inMinutes}:${(eta.inSeconds % 60).toString().padLeft(2, '0')}',
|
||||
style: TextStyle(color: cs.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Controls row
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
icon: Icon(
|
||||
canPlay ? Icons.play_arrow_rounded : Icons.download,
|
||||
),
|
||||
label: Text(
|
||||
canPlay
|
||||
? 'Play'
|
||||
: (isDownloading
|
||||
? 'Downloading…'
|
||||
: (progress == 0 ? 'Install' : 'Resume')),
|
||||
),
|
||||
onPressed: canPlay
|
||||
? () {}
|
||||
: (isDownloading ? null : onStart),
|
||||
),
|
||||
),
|
||||
// const SizedBox(width: 12),
|
||||
// OutlinedButton.icon(
|
||||
// icon: Icon(
|
||||
// isDownloading ? Icons.pause_rounded : Icons.replay,
|
||||
// ),
|
||||
// label: Text(isDownloading ? 'Pause' : 'Reset'),
|
||||
// onPressed: isDownloading
|
||||
// ? onPause
|
||||
// : (progress > 0 ? onReset : null),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
// Disk path / build info (placeholders)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest.withAlpha(128),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: cs.outline),
|
||||
boxShadow: (deco.cardGlow),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Pixel.folder),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Install path: ${state.model.outputPath?.toFilePath() ?? '—'}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _resolvePercentage(BuildContext context) {
|
||||
final state = context.read<HomeScreenBloc>().state;
|
||||
|
||||
if (state is HomeScreenDownloadingState) {
|
||||
final percentage =
|
||||
state.model.progress.downloaded / state.model.progress.total;
|
||||
|
||||
return (percentage * 100).clamp(0, 100).toStringAsFixed(1);
|
||||
}
|
||||
|
||||
return '0.0';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moonwell_launcher/app/theme/mw_theme.dart';
|
||||
|
||||
class GildedProgressBar extends StatelessWidget {
|
||||
const GildedProgressBar({required this.value});
|
||||
final double value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final deco = Theme.of(context).extension<MoonWellDecorations>()!;
|
||||
return Container(
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Stack(
|
||||
children: [
|
||||
// Fill with gold bevel
|
||||
FractionallySizedBox(
|
||||
widthFactor: value.clamp(0.0, 1.0),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: (deco.goldBevel as LinearGradient),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Subtle top highlight
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Container(height: 6, color: Colors.white.withAlpha(10)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user