feat: major ui/ux improvements

This commit is contained in:
gasaichandesu
2025-08-30 21:46:16 +04:00
parent 58d7808a8d
commit 4b1cecbefd
20 changed files with 440 additions and 261 deletions
+38 -21
View File
@@ -35,12 +35,8 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
void _setupHandlers() {
on<HomeScreenLoad>(_onHomeScreenLoad);
on<HomeScreenDownloadRequested>(_onHomeScreenDownloadRequested);
on<HomeScreenDownloadPaused>((event, emit) {
// _downloadManager.pauseDownload();
// emit(HomeScreenDownloadPausedState());
});
on<HomeScreenDownloadPaused>(_onHomeScreenDownloadPaused);
on<HomeScreenOutputDirRequested>(_onHomeOutputDirRequested);
on<HomeScreenDownloadProgressUpdated>(
_onHomeScreenDownloadProgressUpdated,
transformer: (events, mapper) =>
@@ -66,21 +62,8 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
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)),
),
);
emit(HomeScreenOutputDirSelectionState(model: state.model.copyWith()));
return;
}
_downloadProgressSubscription = _downloadManager
@@ -91,6 +74,8 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
),
)
.listen((progress) => add(HomeScreenDownloadProgressUpdated(progress)));
emit(HomeScreenDownloadInitializing(model: state.model.copyWith()));
}
void _onHomeScreenDownloadProgressUpdated(
@@ -111,4 +96,36 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
return super.close();
}
FutureOr<void> _onHomeScreenDownloadPaused(
HomeScreenDownloadPaused event,
Emitter<HomeScreenState> emit,
) async {
await _downloadManager.pause(_downloadId);
emit(HomeScreenDownloadPausedState(model: state.model.copyWith()));
}
FutureOr<void> _onHomeOutputDirRequested(
HomeScreenOutputDirRequested event,
Emitter<HomeScreenState> emit,
) async {
emit(HomeScreenReadyToDownloadState(model: state.model.copyWith()));
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)),
),
);
}
}
@@ -7,6 +7,8 @@ final class HomeScreenLoad extends HomeScreenEvent {}
final class HomeScreenDownloadRequested extends HomeScreenEvent {}
final class HomeScreenOutputDirRequested extends HomeScreenEvent {}
final class HomeScreenDownloadPaused extends HomeScreenEvent {}
final class HomeScreenDownloadProgressUpdated extends HomeScreenEvent {
@@ -19,6 +19,18 @@ final class HomeScreenDownloadPausedState extends HomeScreenState {
const HomeScreenDownloadPausedState({required super.model});
}
final class HomeScreenDownloadInitializing extends HomeScreenState {
const HomeScreenDownloadInitializing({required super.model});
}
final class HomeScreenDownloadingState extends HomeScreenState {
const HomeScreenDownloadingState({required super.model});
}
final class HomeScreenOutputDirSelectionState extends HomeScreenState {
const HomeScreenOutputDirSelectionState({required super.model});
}
final class HomeScreenReadyToPlayState extends HomeScreenState {
const HomeScreenReadyToPlayState({required super.model});
}
+64 -36
View File
@@ -1,7 +1,7 @@
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/home_screen/home_screen_content.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';
@@ -19,46 +19,74 @@ class HomeScreen extends StatelessWidget {
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: BlocConsumer<HomeScreenBloc, HomeScreenState>(
listener: (context, state) {
if (state is HomeScreenOutputDirSelectionState) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text('Alert'),
content: Text('Please select client location.'),
actions: [
ElevatedButton.icon(
icon: const Icon(Icons.folder_open),
onPressed: () => context.read<HomeScreenBloc>().add(
HomeScreenOutputDirRequested(),
),
label: Text('Select directory'),
),
],
),
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: () {},
);
}
},
builder: (context, state) {
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: 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: Stack(
children: [
Positioned.fill(
child: Image.asset(
'assets/background.png',
fit: BoxFit.fitWidth,
),
),
Row(
children: [
Expanded(
child: HomeScreenContent(
title: Text(
'MoonWell',
style: theme.textTheme.headlineLarge,
),
),
),
],
),
],
),
),
],
),
),
),
),
if (state is HomeScreenDownloadInitializing)
Center(child: const CircularProgressIndicator()),
],
),
);
},
@@ -0,0 +1,184 @@
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/gilded_progress_bar.dart';
import 'package:moonwell_launcher/app/theme/mw_theme.dart';
import 'package:pixelarticons/pixel.dart';
class HomeScreenContent extends StatelessWidget {
const HomeScreenContent({super.key, required this.title});
final Widget title;
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>()!;
final state = context.read<HomeScreenBloc>().state;
final percent = _resolvePercentage(context);
final eta = state.model.progress.eta;
final isDownloading = state is HomeScreenDownloadingState;
final canPlay = state is HomeScreenReadyToPlayState;
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),
const Spacer(),
// Progress bar
GildedProgressBar(
value: state.model.progress.total == 0
? 0
: state.model.progress.downloaded /
state.model.progress.total,
),
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(
spacing: 12.0,
children: [
Expanded(
child: ElevatedButton.icon(
icon: Icon(
canPlay ? Icons.play_arrow_rounded : Icons.download,
),
label: Text(
canPlay
? 'Play'
: (isDownloading
? 'Downloading…'
: (state.model.progress.downloaded == 0
? 'Install'
: 'Resume')),
),
onPressed: canPlay
? null
: (isDownloading ? null : () => _onStart(context)),
),
),
OutlinedButton.icon(
icon: Icon(Pixel.pause),
label: Text('Pause'),
onPressed: isDownloading ? () => _onPause(context) : 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: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => context.read<HomeScreenBloc>().add(
HomeScreenOutputDirRequested(),
),
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.model.progress.total == 0) {
return '0.0';
}
final percentage =
state.model.progress.downloaded / state.model.progress.total;
return (percentage * 100).clamp(0, 100).toStringAsFixed(1);
}
void _onStart(BuildContext context) {
context.read<HomeScreenBloc>().add(HomeScreenDownloadRequested());
}
void _onPause(BuildContext context) {
context.read<HomeScreenBloc>().add(HomeScreenDownloadPaused());
}
void _onReset(BuildContext context) {
// context.read<HomeScreenBloc>().add(HomeScreenDownloadReset());
}
}
-181
View File
@@ -1,181 +0,0 @@
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';
}
}
@@ -2,13 +2,14 @@ import 'package:flutter/material.dart';
import 'package:moonwell_launcher/app/theme/mw_theme.dart';
class GildedProgressBar extends StatelessWidget {
const GildedProgressBar({required this.value});
const GildedProgressBar({super.key, 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(
@@ -19,21 +20,25 @@ class GildedProgressBar extends StatelessWidget {
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
Positioned(
top: 0.0,
height: 6.0,
left: 0.0,
right: 0.0,
child: ColoredBox(color: Colors.white.withAlpha(10)),
),
Positioned.fill(
child: 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)),
),
],
),
);
+16 -2
View File
@@ -81,7 +81,7 @@ ThemeData moonWellTheme() {
),
cardTheme: CardThemeData(
color: cs.surface,
color: cs.surface.withAlpha(50),
elevation: 0,
margin: const EdgeInsets.all(12),
shape: RoundedRectangleBorder(
@@ -99,6 +99,9 @@ ThemeData moonWellTheme() {
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
textStyle: WidgetStatePropertyAll(
TextStyle(fontWeight: FontWeight.bold, fontFamily: 'Cinzel'),
),
elevation: const WidgetStatePropertyAll(6),
shadowColor: WidgetStatePropertyAll(MWColors.moonBlue.withAlpha(89)),
backgroundColor: WidgetStateProperty.resolveWith((states) {
@@ -138,7 +141,18 @@ ThemeData moonWellTheme() {
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
foregroundColor: WidgetStatePropertyAll(cs.primary),
foregroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return cs.primary.withAlpha(100);
}
return cs.primary;
}),
backgroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return MWColors.stormNavy.withAlpha(100);
}
return MWColors.stormNavy;
}),
),
),
@@ -47,7 +47,7 @@ class DownloadObjectUseCase
);
final total = meta.size;
final pathToFile = '${request.destinationPath}/${Config.objectName}';
final pathToFile = '${request.destinationPath}${Config.objectName}';
await _fileSystem.ensureParentExists(pathToFile);
+17
View File
@@ -7,9 +7,26 @@ import 'package:flutter/foundation.dart'
import 'package:moonwell_launcher/app/mw_app.dart';
import 'package:moonwell_launcher/app/theme/mw_theme.dart';
import 'package:moonwell_launcher/service_container.dart';
import 'package:window_manager/window_manager.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await windowManager.ensureInitialized();
WindowOptions windowOptions = WindowOptions(
size: Size(960, 540),
center: true,
backgroundColor: Colors.transparent,
skipTaskbar: false,
titleBarStyle: TitleBarStyle.hidden,
);
unawaited(
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
}),
);
await configureDependencies(env: 'flutter');