6 Commits

Author SHA1 Message Date
gasaichandesu 07b9e44350 refactor(auth): Migrate session storage to flutter_secure_storage 2026-06-19 22:58:27 +04:00
gasaichandesu 8f6ed5504b chore: Add lefthook configuration 2026-06-19 22:49:35 +04:00
gasaichandesu 05d1bb2428 deps: Add flutter_secure_storage 2026-06-19 17:52:12 +04:00
gasaichandesu 0363e915ee docs: Add basic AI guidelines and documentation 2026-06-18 23:51:07 +04:00
gasaichandesu 2bc54f1cd7 refactor(ui): Minor refactoring, extract repeating title widget 2026-06-18 23:48:19 +04:00
gasaichandesu 51f6a91bbf build: Upgrade to Flutter 3.44.2 2026-06-18 23:43:36 +04:00
22 changed files with 854 additions and 815 deletions
+92
View File
@@ -0,0 +1,92 @@
# AGENTS.md
This file is the working guide for people and agents changing MoonWell
Launcher. Keep it current when architecture, commands, or repository rules
change.
## Project Shape
MoonWell Launcher is a Flutter desktop app for a private World of Warcraft:
Wrath of the Lich King server. It logs players in, downloads and synchronizes
client files, displays launcher news, and starts `Wow.exe`.
Important areas:
- `lib/main.dart` initializes Flutter, the desktop window, dependency
injection, and `MoonWellApp`.
- `lib/app` contains UI screens, BLoC state, widgets, and theme code.
- `lib/app/widgets` contains shared app-level UI such as the custom desktop
title bar.
- `lib/features/launcher/domain` contains launcher entities such as sessions,
manifests, news items, sync status, and exceptions.
- `lib/features/launcher/application` contains use cases for session restore
and client synchronization.
- `lib/features/launcher/data` contains API, installation, and log services.
- `lib/features/preferences` stores selected install directory and launcher
session data. Launcher sessions must use secure storage; non-sensitive
preferences may use `shared_preferences`.
- `lib/service_container.dart` and `lib/service_container.config.dart` provide
`get_it`/`injectable` dependency wiring.
- `docs/launcher_web_api_spec.md` documents the current launcher Web API and
sync behavior.
## Flutter SDK Policy
MoonWell Launcher supports the latest stable Flutter SDK only. Do not introduce
or rely on FVM-pinned workflows.
If `flutter` is not available after installing or updating Flutter, restart the
shell and verify PATH with `flutter --version`.
## Common Commands
```powershell
flutter pub get
dart run build_runner build --delete-conflicting-outputs
dart format --set-exit-if-changed .
flutter analyze
flutter test
flutter run -d windows --dart-define=MOONWELL_API_BASE_URL=https://host
flutter build windows --dart-define=MOONWELL_API_BASE_URL=https://host
```
See `docs/linting.md` for the required format, analyze, test, and Lefthook
pre-commit workflow.
## Configuration
The launcher API base URL is compile-time configuration:
```powershell
--dart-define=MOONWELL_API_BASE_URL=https://host
```
Do not commit private credentials, production secrets, temporary bearer tokens,
or local-only API URLs.
## Architecture Notes
- All backend calls should go through `LauncherApiClient`.
- Launcher bearer tokens must be persisted through `flutter_secure_storage`.
Do not store sessions or other secrets in `shared_preferences`.
- File sync behavior should stay in `ClientSyncUseCase` and
`GameInstallationService`.
- The server manifest is the source of truth for client files.
- Downloads must write to `*.moonwell.part`, verify size and SHA-256, then
replace the destination file only after successful verification.
- Local scans should ignore volatile client directories and launcher metadata:
`Cache`, `Errors`, `Logs`, `Screenshots`, `WTF`, and `.moonwell_launcher`.
- Path normalization and traversal checks are safety-critical. Do not bypass
`GameInstallationService.resolveClientPath` for manifest-relative paths.
- `lib/service_container.config.dart` is generated by injectable. Do not
hand-edit it unless you are intentionally repairing generated output; prefer
regenerating it with build_runner.
## Repository Rules
- Preserve unrelated work in a dirty tree. This repository currently may have
user edits in dependency files.
- Keep changes scoped to the requested behavior.
- Always keep `README.md` and files under `docs/` up-to-date with any
architecture, setup, API, sync, build, or operational change.
- Keep documentation links current when files move.
+95 -10
View File
@@ -1,16 +1,101 @@
# moonwell_launcher
# MoonWell Launcher
A new Flutter project.
MoonWell Launcher is the desktop launcher for MoonWell, a private World of
Warcraft: Wrath of the Lich King server focused on a player-friendly experience
without excessive grind and with stronger social play.
## Getting Started
The launcher is responsible for:
This project is a starting point for a Flutter application.
- logging in to a player's MoonWell account
- downloading and synchronizing client files
- displaying launcher news
- launching the game client
A few resources to get you started if this is your first Flutter project:
## Stack
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
- Flutter desktop
- Dart
- `flutter_bloc` for launcher UI state
- `get_it` and `injectable` for dependency injection
- `dio` for Web API and file download requests
- `flutter_secure_storage` for launcher sessions
- `shared_preferences` for non-sensitive launcher preferences
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
## Prerequisites
Use the latest stable Flutter SDK available from the official Flutter release
channel. This project does not support FVM-pinned SDK workflows.
```powershell
flutter pub get
```
If `flutter` is unavailable, restart the shell after installing Flutter or
fix PATH so `flutter --version` works.
## Configuration
Set the launcher Web API base URL at compile time:
```powershell
--dart-define=MOONWELL_API_BASE_URL=https://host
```
Example run command:
```powershell
flutter run -d windows --dart-define=MOONWELL_API_BASE_URL=https://host
```
## Development
Regenerate dependency injection after changing injectable services:
```powershell
dart run build_runner build --delete-conflicting-outputs
```
Run static analysis:
```powershell
flutter analyze
```
See `docs/linting.md` for the full format, analyze, test, and Lefthook
pre-commit workflow.
Build the Windows launcher:
```powershell
flutter build windows --dart-define=MOONWELL_API_BASE_URL=https://host
```
The Inno Setup installer script lives at `installer/moonwell_launcher.iss`.
## Architecture
The UI lives in `lib/app`, with shared UI widgets under `lib/app/widgets`.
Launcher domain models, sync use cases, API clients, installation services, and
preferences live under `lib/features`.
The launcher sync flow is manifest-based:
1. authenticate against the MoonWell Web API
2. fetch the client manifest
3. scan the selected installation directory
4. compare local files to the manifest
5. remove stale files outside ignored directories
6. download missing or changed files to temporary part files
7. verify size and SHA-256 before replacing client files
8. launch `Wow.exe` from the selected client directory
See `docs/launcher_web_api_spec.md` for the API and sync contract.
## Documentation
- `AGENTS.md`: architecture, commands, and repository rules for contributors
- `TODO.md`: remaining verification and maintenance tasks
- `docs/linting.md`: format, analyze, test, and Lefthook workflow
- `docs/project_review.md`: project review with strengths, weaknesses, and
upgrade feasibility notes
- `docs/launcher_web_api_spec.md`: current launcher Web API and sync behavior
+6
View File
@@ -0,0 +1,6 @@
# TODO
## Verification
- [ ] Run a Windows build smoke test:
`flutter build windows --dart-define=MOONWELL_API_BASE_URL=https://host`.
+42
View File
@@ -0,0 +1,42 @@
# Linting And Test Flow
MoonWell Launcher supports the latest stable Flutter SDK only. Do not use FVM
for this project.
## Required Checks
Run these commands before committing:
```powershell
dart format --set-exit-if-changed .
flutter analyze
flutter test
```
Use `dart format .` to apply formatting fixes when the format check fails.
## Lefthook
This repository includes `lefthook.yml` with a `pre-commit` hook that runs the
same format, analyze, and test flow.
Install Lefthook once on your machine, then install the Git hook from the repo
root:
```powershell
lefthook install
```
Run the pre-commit flow manually with:
```powershell
lefthook run pre-commit
```
If `lefthook` is not available, install it with your preferred package manager
from the official Lefthook distribution, then rerun `lefthook install`.
## Windows Troubleshooting
If Flutter reports that building with plugins requires symlink support, enable
Windows Developer Mode and rerun the command.
+99
View File
@@ -0,0 +1,99 @@
# MoonWell Launcher Project Review
Reviewed on 2026-06-18.
## Overview
MoonWell Launcher is a Flutter desktop application for a private World of
Warcraft: Wrath of the Lich King server. Its core responsibilities are account
login, client installation/synchronization, news display, and launching
`Wow.exe`.
The project is already more than a Flutter template: it has a clear launcher
API contract, domain models for manifests and sessions, a sync use case,
installation scanning, logging, dependency injection, and tests around the
most important file verification behavior.
## Strong Points
- The project uses a readable layered structure: UI under `lib/app`, feature
code under `lib/features`, and shared primitives under `lib/core`.
- Client sync is manifest driven and verifies downloaded files by size and
SHA-256 before replacing local files.
- Local path handling normalizes manifest paths and rejects absolute paths or
traversal attempts before resolving files under the selected installation
directory.
- Player-generated or volatile client directories are excluded from
verification: `Cache`, `Errors`, `Logs`, `Screenshots`, `WTF`, and launcher
metadata.
- Hash caching is implemented as an optimization while keeping the server
manifest as the source of truth.
- Sync diagnostics are written to `.moonwell_launcher/launcher.log`, and failed
downloads can be preserved for inspection.
- The Web API integration is documented in `docs/launcher_web_api_spec.md`,
including login, manifest, presigned download, news, pause/resume, and launch
behavior.
- Tests cover deterministic build hash behavior, ignored directories, hash
cache behavior, path traversal rejection, sync success, stale file removal,
download verification failure, session restore, preferences, and news loading.
## Weak Points And Risks
- `README.md` was still the generated Flutter starter text before this review,
so new contributors had no project-specific setup or operation guide.
- There was no `AGENTS.md`, so future automated or human contributors had to
infer architecture, commands, and safety rules from source code.
- At initial review, `lib/main.dart` contained a large inactive demo
`LauncherHome` implementation. This has since been removed from the active
entrypoint.
- At initial review, several Russian UI strings appeared mojibake-encoded in
the active login and home flow. These active strings have since been
normalized to UTF-8.
- At initial review, custom title-bar widgets were duplicated between login and
home screens. They have since been extracted into a shared widget.
- Session tokens were originally persisted through `shared_preferences`. They
now use `flutter_secure_storage`, while `shared_preferences` remains limited
to non-sensitive launcher preferences.
- The dependency injection output file `lib/service_container.config.dart` is
committed, but the regeneration command was not previously documented.
- UI/widget/integration coverage is still light compared with the risk in login,
bootstrap, directory selection, news failure handling, sync controls, and
launch behavior.
- There is no visible CI configuration in the repository.
- The current working tree already had modified `pubspec.yaml` and
`pubspec.lock` before this documentation pass. Those changes were not part of
this review and should be preserved.
## Flutter 3.44.2 Upgrade Feasibility
Assumption: `3.44.2` means Flutter SDK `3.44.2`.
Current local indicators:
- `.fvmrc` is absent.
- `pubspec.yaml` declares Dart SDK `^3.9.0`.
- `pubspec.lock` declares Flutter SDK `>=3.38.0`.
- The project policy is latest stable Flutter SDK only; FVM-pinned workflows are
not supported.
- The local shell now finds Flutter `3.44.2` on the stable channel.
Flutter `3.44.2` has been validated for dependency resolution, formatting,
analysis, and tests. A production-like Windows build smoke test remains open.
Recommended upgrade path:
1. Keep the latest stable Flutter SDK on PATH.
2. Run `flutter pub get`.
3. Regenerate dependency injection only if dependency resolution or generator
output requires it:
`dart run build_runner build --delete-conflicting-outputs`.
4. Run `flutter analyze`.
5. Run a Windows build smoke test:
`flutter build windows --dart-define=MOONWELL_API_BASE_URL=https://host`.
## Recommended Refactoring Direction
- Keep secure session persistence covered by tests when login or session
restore behavior changes.
- Add CI or a documented local verification workflow that runs analysis and a
Windows build smoke test with the latest stable Flutter SDK.
+8
View File
@@ -0,0 +1,8 @@
pre-commit:
commands:
format:
run: dart format --set-exit-if-changed .
analyze:
run: flutter analyze
test:
run: flutter test
@@ -165,7 +165,7 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
return;
}
final directory = await FilePicker.platform.getDirectoryPath(
final directory = await FilePicker.getDirectoryPath(
dialogTitle: 'Выберите папку установки Moonwell',
);
+9 -78
View File
@@ -1,11 +1,10 @@
import 'dart:async';
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/home_screen_content.dart';
import 'package:moonwell_launcher/app/login_screen/login_screen.dart';
import 'package:moonwell_launcher/app/theme/mw_theme.dart';
import 'package:moonwell_launcher/app/widgets/window_title_bar.dart';
import 'package:moonwell_launcher/features/launcher/application/client_sync_use_case.dart';
import 'package:moonwell_launcher/features/launcher/data/game_installation_service.dart';
import 'package:moonwell_launcher/features/launcher/data/launcher_api_client.dart';
@@ -13,7 +12,6 @@ import 'package:moonwell_launcher/features/launcher/domain/entities/client_manif
import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_session.dart';
import 'package:moonwell_launcher/features/preferences/domain/repositories/preferences_repository.dart';
import 'package:moonwell_launcher/service_container.dart';
import 'package:window_manager/window_manager.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key, required this.session, required this.manifest});
@@ -38,8 +36,8 @@ class HomeScreen extends StatelessWidget {
listener: (context, state) async {
await Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (_, __, ___) => const LoginScreen(),
transitionsBuilder: (_, animation, __, child) {
pageBuilder: (_, _, _) => const LoginScreen(),
transitionsBuilder: (_, animation, _, child) {
return FadeTransition(opacity: animation, child: child);
},
transitionDuration: const Duration(milliseconds: 250),
@@ -49,11 +47,9 @@ class HomeScreen extends StatelessWidget {
child: Scaffold(
body: Stack(
children: [
// Background image
Positioned.fill(
child: Image.asset('assets/background.png', fit: BoxFit.cover),
),
// Gradient overlay
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
@@ -70,10 +66,13 @@ class HomeScreen extends StatelessWidget {
),
),
),
// Main content
const Positioned.fill(child: HomeScreenContent()),
// Custom title bar
const Positioned(top: 0, left: 0, right: 0, child: _TitleBar()),
const Positioned(
top: 0,
left: 0,
right: 0,
child: WindowTitleBar(),
),
],
),
),
@@ -81,71 +80,3 @@ class HomeScreen extends StatelessWidget {
);
}
}
class _TitleBar extends StatelessWidget {
const _TitleBar();
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return GestureDetector(
onPanStart: (_) => windowManager.startDragging(),
child: SizedBox(
height: 36,
child: Row(
children: [
const Spacer(),
_TitleBarButton(
icon: Icons.remove,
onTap: () => windowManager.minimize(),
color: cs.onSurface,
),
_TitleBarButton(
icon: Icons.crop_square,
onTap: () async {
if (await windowManager.isMaximized()) {
await windowManager.unmaximize();
} else {
await windowManager.maximize();
}
},
color: cs.onSurface,
),
_TitleBarButton(
icon: Icons.close,
onTap: () {
unawaited(windowManager.destroy());
},
color: MWColors.error,
),
],
),
),
);
}
}
class _TitleBarButton extends StatelessWidget {
const _TitleBarButton({
required this.icon,
required this.onTap,
required this.color,
});
final IconData icon;
final VoidCallback onTap;
final Color color;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: SizedBox(
width: 40,
height: 36,
child: Icon(icon, size: 16, color: color.withAlpha(180)),
),
);
}
}
+2 -2
View File
@@ -139,7 +139,7 @@ class _NewsPanel extends StatelessWidget {
return ListView.separated(
padding: const EdgeInsets.only(right: 16, bottom: 24),
itemCount: model.newsItems.length,
separatorBuilder: (_, __) => const SizedBox(height: 10),
separatorBuilder: (_, _) => const SizedBox(height: 10),
itemBuilder: (context, index) {
return _NewsCard(item: model.newsItems[index]);
},
@@ -239,7 +239,7 @@ class _NewsCard extends StatelessWidget {
child: Image.network(
item.imageUrl!,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
errorBuilder: (_, _, _) => Container(
color: cs.surface.withAlpha(120),
alignment: Alignment.center,
child: Icon(
+4 -78
View File
@@ -1,12 +1,10 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:moonwell_launcher/app/home_screen/home_screen.dart';
import 'package:moonwell_launcher/app/theme/mw_theme.dart';
import 'package:moonwell_launcher/app/widgets/window_title_bar.dart';
import 'package:moonwell_launcher/features/launcher/data/launcher_api_client.dart';
import 'package:moonwell_launcher/features/preferences/domain/repositories/preferences_repository.dart';
import 'package:moonwell_launcher/service_container.dart';
import 'package:window_manager/window_manager.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@@ -53,9 +51,9 @@ class _LoginScreenState extends State<LoginScreen> {
await Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (_, __, ___) =>
pageBuilder: (_, _, _) =>
HomeScreen(session: session, manifest: manifest),
transitionsBuilder: (_, animation, __, child) {
transitionsBuilder: (_, animation, _, child) {
return FadeTransition(opacity: animation, child: child);
},
transitionDuration: const Duration(milliseconds: 400),
@@ -83,7 +81,6 @@ class _LoginScreenState extends State<LoginScreen> {
return Scaffold(
body: Stack(
children: [
// Background
Positioned.fill(
child: Image.asset('assets/background.png', fit: BoxFit.cover),
),
@@ -101,23 +98,19 @@ class _LoginScreenState extends State<LoginScreen> {
),
),
),
// Title bar
Positioned(top: 0, left: 0, right: 0, child: _TitleBar(cs: cs)),
// Login form — centered
const Positioned(top: 0, left: 0, right: 0, child: WindowTitleBar()),
Center(
child: SizedBox(
width: 340,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Logo
Image.asset(
'assets/logo.png',
height: 160,
filterQuality: FilterQuality.high,
),
const SizedBox(height: 32),
// Card
Container(
padding: const EdgeInsets.all(28),
decoration: BoxDecoration(
@@ -194,70 +187,3 @@ class _LoginScreenState extends State<LoginScreen> {
);
}
}
class _TitleBar extends StatelessWidget {
const _TitleBar({required this.cs});
final ColorScheme cs;
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanStart: (_) => windowManager.startDragging(),
child: SizedBox(
height: 36,
child: Row(
children: [
const Spacer(),
_TitleBarButton(
icon: Icons.remove,
onTap: () => windowManager.minimize(),
color: cs.onSurface,
),
_TitleBarButton(
icon: Icons.crop_square,
onTap: () async {
if (await windowManager.isMaximized()) {
await windowManager.unmaximize();
} else {
await windowManager.maximize();
}
},
color: cs.onSurface,
),
_TitleBarButton(
icon: Icons.close,
onTap: () {
unawaited(windowManager.destroy());
},
color: MWColors.error,
),
],
),
),
);
}
}
class _TitleBarButton extends StatelessWidget {
const _TitleBarButton({
required this.icon,
required this.onTap,
required this.color,
});
final IconData icon;
final VoidCallback onTap;
final Color color;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: SizedBox(
width: 40,
height: 36,
child: Icon(icon, size: 16, color: color.withAlpha(180)),
),
);
}
}
+77
View File
@@ -0,0 +1,77 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:moonwell_launcher/app/theme/mw_theme.dart';
import 'package:window_manager/window_manager.dart';
class WindowTitleBar extends StatelessWidget {
const WindowTitleBar({super.key});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return GestureDetector(
onPanStart: (_) => windowManager.startDragging(),
child: SizedBox(
height: 36,
child: Row(
children: [
const Spacer(),
_TitleBarButton(
icon: Icons.remove,
onTap: () => windowManager.minimize(),
color: cs.onSurface,
),
_TitleBarButton(
icon: Icons.crop_square,
onTap: () {
unawaited(_toggleMaximized());
},
color: cs.onSurface,
),
_TitleBarButton(
icon: Icons.close,
onTap: () {
unawaited(windowManager.destroy());
},
color: MWColors.error,
),
],
),
),
);
}
Future<void> _toggleMaximized() async {
if (await windowManager.isMaximized()) {
await windowManager.unmaximize();
} else {
await windowManager.maximize();
}
}
}
class _TitleBarButton extends StatelessWidget {
const _TitleBarButton({
required this.icon,
required this.onTap,
required this.color,
});
final IconData icon;
final VoidCallback onTap;
final Color color;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: SizedBox(
width: 40,
height: 36,
child: Icon(icon, size: 16, color: color.withAlpha(180)),
),
);
}
}
@@ -2,9 +2,11 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:injectable/injectable.dart';
import 'package:moonwell_launcher/config.dart';
import 'package:path/path.dart' as p;
@lazySingleton
class LauncherLogService {
static Future<void> _pendingWrite = Future<void>.value();
@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:injectable/injectable.dart';
import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_session.dart';
import 'package:moonwell_launcher/features/preferences/domain/repositories/preferences_repository.dart';
@@ -9,12 +10,15 @@ const _outputDirKey = 'output_dir';
const _launcherSessionKey = 'launcher_session';
@LazySingleton(as: PreferencesRepository, env: ['flutter'])
class SharedPrefsPreferencesRepository implements PreferencesRepository {
class LocalPreferencesRepository implements PreferencesRepository {
final SharedPreferences _preferences;
final FlutterSecureStorage _secureStorage;
const SharedPrefsPreferencesRepository({
const LocalPreferencesRepository({
required SharedPreferences preferences,
}) : _preferences = preferences;
required FlutterSecureStorage secureStorage,
}) : _preferences = preferences,
_secureStorage = secureStorage;
@override
Future<Uri?> getOutputDir() {
@@ -33,35 +37,45 @@ class SharedPrefsPreferencesRepository implements PreferencesRepository {
}
@override
Future<LauncherSession?> getLauncherSession() {
final rawSession = _preferences.getString(_launcherSessionKey);
Future<LauncherSession?> getLauncherSession() async {
await _maybeClearLegacyLauncherSession();
final rawSession = await _secureStorage.read(key: _launcherSessionKey);
if (rawSession == null || rawSession.isEmpty) {
return Future.value(null);
return null;
}
try {
final decoded = jsonDecode(rawSession);
if (decoded is! Map) {
return Future.value(null);
return null;
}
final json = decoded.map((key, value) => MapEntry(key.toString(), value));
return Future.value(LauncherSession.fromJson(json));
return LauncherSession.fromJson(json);
} catch (_) {
return Future.value(null);
return null;
}
}
@override
Future<void> setLauncherSession(LauncherSession session) {
return _preferences.setString(
_launcherSessionKey,
jsonEncode(session.toJson()),
Future<void> setLauncherSession(LauncherSession session) async {
await _secureStorage.write(
key: _launcherSessionKey,
value: jsonEncode(session.toJson()),
);
await _maybeClearLegacyLauncherSession();
}
@override
Future<void> clearLauncherSession() {
return _preferences.remove(_launcherSessionKey);
Future<void> clearLauncherSession() async {
await _secureStorage.delete(key: _launcherSessionKey);
await _maybeClearLegacyLauncherSession();
}
Future<void> _maybeClearLegacyLauncherSession() async {
if (_preferences.containsKey(_launcherSessionKey)) {
await _preferences.remove(_launcherSessionKey);
}
}
}
+2 -440
View File
@@ -1,11 +1,8 @@
// main.dart
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, TargetPlatform;
import 'package:flutter/material.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';
@@ -32,438 +29,3 @@ Future<void> main() async {
runApp(const MoonWellApp());
}
bool get _isDesktop => const {
TargetPlatform.macOS,
TargetPlatform.linux,
TargetPlatform.windows,
}.contains(defaultTargetPlatform);
class LauncherHome extends StatefulWidget {
const LauncherHome({super.key});
@override
State<LauncherHome> createState() => _LauncherHomeState();
}
class _LauncherHomeState extends State<LauncherHome> {
double _progress = 0.0; // 0..1
bool _isDownloading = false;
double _kbps = 0; // fake speed
Duration _eta = Duration.zero;
Timer? _timer;
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
void _startDownload() {
if (_isDownloading || _progress >= 1.0) return;
setState(() => _isDownloading = true);
_timer = Timer.periodic(const Duration(milliseconds: 160), (t) {
// Fake speed & progress for demo
final chunk = 0.002 + math.Random().nextDouble() * 0.004;
_kbps = 800 + math.Random().nextDouble() * 2200; // 0.8–3.0 MB/s
_progress = (_progress + chunk).clamp(0.0, 1.0);
final remaining = (1 - _progress);
// naive ETA based on current "speed"
_eta = Duration(seconds: math.max(1, (remaining * 120).round()));
if (_progress >= 1.0) {
_isDownloading = false;
t.cancel();
}
setState(() {});
});
}
void _pause() {
_timer?.cancel();
setState(() => _isDownloading = false);
}
void _reset() {
_timer?.cancel();
setState(() {
_progress = 0;
_isDownloading = false;
_kbps = 0;
_eta = Duration.zero;
});
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final deco = Theme.of(context).extension<MoonWellDecorations>()!;
final title = ShaderMask(
shaderCallback: (rect) =>
(deco.goldBevel as LinearGradient).createShader(rect),
child: Text(
'MOONWELL',
style: Theme.of(
context,
).textTheme.displaySmall?.copyWith(shadows: [deco.textGlow]),
),
);
if (!_isDesktop) {
return Scaffold(
body: Center(
child: Text(
'Desktop only. Please run on Windows, macOS, or Linux.',
style: Theme.of(context).textTheme.titleLarge,
),
),
);
}
return Scaffold(
body: Container(
decoration: const BoxDecoration(
// subtle vignette for “night” vibe
gradient: RadialGradient(
center: Alignment(0, -0.5),
radius: 1.2,
colors: [Color(0xFF0F1522), Color(0xFF0B101A)],
),
),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 1100, maxHeight: 760),
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
// LEFT: Game title + download controls
Expanded(
flex: 3,
child: _LeftPane(
title: title,
progress: _progress,
isDownloading: _isDownloading,
kbps: _kbps,
eta: _eta,
onStart: _startDownload,
onPause: _pause,
onReset: _reset,
),
),
const SizedBox(width: 20),
// RIGHT: Patch notes
Expanded(flex: 4, child: _PatchNotesPane(color: cs.surface)),
],
),
),
),
),
),
);
}
}
class _LeftPane extends StatelessWidget {
const _LeftPane({
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 get _speedLabel {
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 percent = (progress * 100).clamp(0, 100).toStringAsFixed(1);
final canPlay = progress >= 1.0 && !isDownloading;
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 MMO Launcher',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: cs.onSurface.withValues(alpha: 0.85),
),
),
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(Icons.speed, size: 18, color: cs.onSurfaceVariant),
const SizedBox(width: 6),
Text(_speedLabel, style: TextStyle(color: cs.onSurfaceVariant)),
const SizedBox(width: 16),
Icon(
Icons.timer_outlined,
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.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: cs.outline),
boxShadow: (deco.cardGlow),
),
child: Row(
children: [
const Icon(Icons.folder_open),
const SizedBox(width: 8),
Expanded(
child: Text(
'Install path: C:/Games/MoonWell',
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 12),
const Icon(Icons.info_outline, size: 18),
const SizedBox(width: 6),
Text(
'Build: 1.2.0 (live)',
style: TextStyle(color: cs.onSurfaceVariant),
),
],
),
),
],
),
),
);
}
}
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.withValues(alpha: 0.08),
),
),
],
),
);
}
}
class _PatchNotesPane extends StatelessWidget {
const _PatchNotesPane({required this.color});
final Color color;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
// Example static notes — wire your real source here.
final notes = [
(
'1.2.0 — “Moonlit March”',
[
'New dungeon: *Hall of Tides*.',
'Launcher supports delta updates with blockmaps.',
'Improved shader warmup (faster first launch).',
'Fixed login race condition on slow networks.',
],
),
(
'1.1.5',
[
'Balance pass on Ranger & Warlock.',
'Memory usage reduced ~12% in large towns.',
],
),
];
return Card(
margin: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
decoration: BoxDecoration(
color: color,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(20),
),
border: Border(bottom: BorderSide(color: cs.outline)),
),
child: Row(
children: [
const Icon(Icons.article_outlined),
const SizedBox(width: 10),
Text(
'Patch Notes',
style: Theme.of(context).textTheme.titleLarge,
),
const Spacer(),
TextButton.icon(
onPressed: () {}, // hook to "Open full changelog"
icon: const Icon(Icons.open_in_new),
label: const Text('Full changelog'),
),
],
),
),
// Body
Expanded(
child: Scrollbar(
child: ListView.separated(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
itemCount: notes.length,
separatorBuilder: (_, __) => const Divider(),
itemBuilder: (context, i) {
final (title, items) = notes[i];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
...items.map(
(s) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('• '),
Expanded(
child: Text(
s,
style: TextStyle(
color: cs.onSurface.withValues(alpha: 0.9),
),
),
),
],
),
),
],
);
},
),
),
),
],
),
);
}
}
+26 -10
View File
@@ -1,5 +1,5 @@
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
// **************************************************************************
// InjectableConfigGenerator
@@ -9,6 +9,7 @@
// coverage:ignore-file
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:flutter_secure_storage/flutter_secure_storage.dart' as _i558;
import 'package:get_it/get_it.dart' as _i174;
import 'package:injectable/injectable.dart' as _i526;
import 'package:shared_preferences/shared_preferences.dart' as _i460;
@@ -16,10 +17,12 @@ import 'package:shared_preferences/shared_preferences.dart' as _i460;
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/preferences/data/repositories/shared_prefs_preferences_repository.dart'
as _i662;
import 'features/launcher/data/launcher_log_service.dart' as _i43;
import 'features/preferences/data/repositories/local_preferences_repository.dart'
as _i658;
import 'features/preferences/domain/repositories/preferences_repository.dart'
as _i44;
import 'third_party/flutter_secure_storage.dart' as _i782;
import 'third_party/shared_preferences.dart' as _i1006;
const String _flutter = 'flutter';
@@ -32,6 +35,7 @@ extension GetItInjectableX on _i174.GetIt {
}) async {
final gh = _i526.GetItHelper(this, environment, environmentFilter);
final sharedPreferencesModule = _$SharedPreferencesModule();
final flutterSecureStorageModule = _$FlutterSecureStorageModule();
await gh.factoryAsync<_i460.SharedPreferences>(
() => sharedPreferencesModule.prefs,
preResolve: true,
@@ -39,21 +43,33 @@ extension GetItInjectableX on _i174.GetIt {
gh.lazySingleton<_i315.GameInstallationService>(
() => _i315.GameInstallationService(),
);
gh.lazySingleton<_i703.LauncherApiClient>(() => _i703.LauncherApiClient());
gh.lazySingleton<_i43.LauncherLogService>(() => _i43.LauncherLogService());
gh.lazySingleton<_i558.FlutterSecureStorage>(
() => flutterSecureStorageModule.storage,
);
gh.lazySingleton<_i703.LauncherApiClient>(
() => _i703.LauncherApiClient(
launcherLogService: gh<_i43.LauncherLogService>(),
),
);
gh.lazySingleton<_i44.PreferencesRepository>(
() => _i658.LocalPreferencesRepository(
preferences: gh<_i460.SharedPreferences>(),
secureStorage: gh<_i558.FlutterSecureStorage>(),
),
registerFor: {_flutter},
);
gh.lazySingleton<_i757.ClientSyncUseCase>(
() => _i757.ClientSyncUseCase(
launcherApiClient: gh<_i703.LauncherApiClient>(),
installationService: gh<_i315.GameInstallationService>(),
launcherLogService: gh<_i43.LauncherLogService>(),
),
);
gh.lazySingleton<_i44.PreferencesRepository>(
() => _i662.SharedPrefsPreferencesRepository(
preferences: gh<_i460.SharedPreferences>(),
),
registerFor: {_flutter},
);
return this;
}
}
class _$SharedPreferencesModule extends _i1006.SharedPreferencesModule {}
class _$FlutterSecureStorageModule extends _i782.FlutterSecureStorageModule {}
+8
View File
@@ -0,0 +1,8 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:injectable/injectable.dart';
@module
abstract class FlutterSecureStorageModule {
@lazySingleton
FlutterSecureStorage get storage => const FlutterSecureStorage();
}
+252 -124
View File
@@ -5,18 +5,26 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d"
url: "https://pub.dev"
source: hosted
version: "85.0.0"
version: "93.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d"
sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b
url: "https://pub.dev"
source: hosted
version: "7.7.1"
version: "10.0.1"
ansicolor:
dependency: transitive
description:
name: ansicolor
sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
args:
dependency: transitive
description:
@@ -29,18 +37,18 @@ packages:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.0"
version: "2.13.1"
bloc:
dependency: transitive
description:
name: bloc
sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189"
sha256: e03b235924e4f509c27b5d6b2f949200e0a91149a9818b4f65eeb56662b75413
url: "https://pub.dev"
source: hosted
version: "9.0.0"
version: "9.2.1"
boolean_selector:
dependency: transitive
description:
@@ -53,50 +61,34 @@ packages:
dependency: transitive
description:
name: build
sha256: "6439a9c71a4e6eca8d9490c1b380a25b02675aa688137dfbe66d2062884a23ac"
sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "4.0.6"
build_config:
dependency: transitive
description:
name: build_config
sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187"
sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.3.0"
build_daemon:
dependency: transitive
description:
name: build_daemon
sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa"
sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957
url: "https://pub.dev"
source: hosted
version: "4.0.4"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: "2b21a125d66a86b9511cc3fb6c668c42e9a1185083922bf60e46d483a81a9712"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "4.1.1"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: fd3c09f4bbff7fa6e8d8ef688a0b2e8a6384e6483a25af0dac75fef362bcfe6f
sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6"
url: "https://pub.dev"
source: hosted
version: "2.7.0"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: ab27e46c8aa233e610cf6084ee6d8a22c6f873a0a9929241d8855b7a72978ae7
url: "https://pub.dev"
source: hosted
version: "9.3.0"
version: "2.15.0"
built_collection:
dependency: transitive
description:
@@ -109,18 +101,18 @@ packages:
dependency: transitive
description:
name: built_value
sha256: ba95c961bafcd8686d1cf63be864eb59447e795e124d98d6a27d91fcd13602fb
sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56"
url: "https://pub.dev"
source: hosted
version: "8.11.1"
version: "8.12.6"
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
checked_yaml:
dependency: transitive
description:
@@ -137,14 +129,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
url: "https://pub.dev"
source: hosted
version: "1.2.1"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d"
url: "https://pub.dev"
source: hosted
version: "4.10.1"
version: "4.11.1"
collection:
dependency: transitive
description:
@@ -165,58 +165,58 @@ packages:
dependency: transitive
description:
name: cross_file
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
url: "https://pub.dev"
source: hosted
version: "0.3.4+2"
version: "0.3.5+2"
crypto:
dependency: "direct main"
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.6"
version: "3.0.7"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.dev"
source: hosted
version: "1.0.8"
version: "1.0.9"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb"
sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
version: "3.1.7"
dbus:
dependency: transitive
description:
name: dbus
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.14"
dio:
dependency: "direct main"
description:
name: dio
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c
url: "https://pub.dev"
source: hosted
version: "5.9.0"
version: "5.9.2"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
fake_async:
dependency: transitive
description:
@@ -229,10 +229,10 @@ packages:
dependency: transitive
description:
name: ffi
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.2.0"
file:
dependency: transitive
description:
@@ -245,10 +245,10 @@ packages:
dependency: "direct main"
description:
name: file_picker
sha256: e7e16c9d15c36330b94ca0e2ad8cb61f93cd5282d0158c09805aed13b5452f22
sha256: f13a03000d942e476bc1ff0a736d2e9de711d2f89a95cd4c1d88f861c3348387
url: "https://pub.dev"
source: hosted
version: "10.3.2"
version: "11.0.2"
fixnum:
dependency: transitive
description:
@@ -274,18 +274,66 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
version: "6.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
url: "https://pub.dev"
source: hosted
version: "2.0.30"
version: "2.0.35"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e"
url: "https://pub.dev"
source: hosted
version: "10.3.1"
flutter_secure_storage_darwin:
dependency: transitive
description:
name: flutter_secure_storage_darwin
sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149"
url: "https://pub.dev"
source: hosted
version: "0.3.2"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5
url: "https://pub.dev"
source: hosted
version: "3.0.1"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -308,10 +356,10 @@ packages:
dependency: "direct main"
description:
name: get_it
sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b
sha256: "568d62f0e68666fb5d95519743b3c24a34c7f19d834b0658c46e26d778461f66"
url: "https://pub.dev"
source: hosted
version: "8.2.0"
version: "9.2.1"
glob:
dependency: transitive
description:
@@ -328,6 +376,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.2"
hooks:
dependency: transitive
description:
name: hooks
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
hotreloader:
dependency: transitive
description:
name: hotreloader
sha256: "66871df468fc24eee81f1a0a7cb98acc104716f9b7376d355437b48d633c4ebf"
url: "https://pub.dev"
source: hosted
version: "4.4.0"
http_multi_server:
dependency: transitive
description:
@@ -348,18 +412,18 @@ packages:
dependency: "direct main"
description:
name: injectable
sha256: "1b86fab6a98c11a97e5c718afb00e628d47d183c2a2256392e995a4c561141c1"
sha256: "1da6399509e44ddb0d8a4a35291d7122c4efb5c625a9d733bf5e48d4a72e379e"
url: "https://pub.dev"
source: hosted
version: "2.5.1"
version: "3.0.0"
injectable_generator:
dependency: "direct dev"
description:
name: injectable_generator
sha256: "09c55dba52b53d17411b90134a6751270b8930abd2529e2637d700fc99b0d5b5"
sha256: "433ad4d94837ff187803b2bf04c6707db6fad8d245f1d693e2e34053bc07ec28"
url: "https://pub.dev"
source: hosted
version: "2.8.1"
version: "3.0.2"
io:
dependency: transitive
description:
@@ -368,22 +432,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
jni:
dependency: transitive
description:
name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
url: "https://pub.dev"
source: hosted
version: "1.0.0"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
version: "4.12.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0"
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.1"
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
@@ -400,14 +480,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lean_builder:
dependency: transitive
description:
name: lean_builder
sha256: c16e95ddf7b2d49dd551357b7212fe2ce9f13ec7ad1b1e660c157184031e96c0
url: "https://pub.dev"
source: hosted
version: "0.1.10"
lints:
dependency: transitive
description:
name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
version: "6.1.0"
logging:
dependency: transitive
description:
@@ -420,26 +508,26 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
version: "1.18.0"
mime:
dependency: transitive
description:
@@ -456,6 +544,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
url: "https://pub.dev"
source: hosted
version: "9.4.1"
package_config:
dependency: transitive
description:
@@ -472,6 +568,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
url: "https://pub.dev"
source: hosted
version: "2.1.6"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
@@ -484,10 +604,10 @@ packages:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.3"
path_provider_windows:
dependency: transitive
description:
@@ -500,10 +620,10 @@ packages:
dependency: transitive
description:
name: petitparser
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
version: "7.0.2"
pixelarticons:
dependency: "direct main"
description:
@@ -532,10 +652,10 @@ packages:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
version: "1.5.2"
provider:
dependency: transitive
description:
@@ -568,70 +688,78 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.0"
record_use:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
screen_retriever:
dependency: transitive
description:
name: screen_retriever
sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c"
sha256: "42cc3b402a0f67d2455a0d067553d0f13453f6a008d98eababf8b63958d506bd"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
version: "0.2.1"
screen_retriever_linux:
dependency: transitive
description:
name: screen_retriever_linux
sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18
sha256: "2a476f1a5538065bc5badf376cfdc83d6ecf07d77eb2391b9c2bff5a76970048"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
version: "0.2.1"
screen_retriever_macos:
dependency: transitive
description:
name: screen_retriever_macos
sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149"
sha256: b5abb900fcb86614ff10b738b34e37b9e1d03b0447280668e2bc8a98bdc7bd59
url: "https://pub.dev"
source: hosted
version: "0.2.0"
version: "0.2.1"
screen_retriever_platform_interface:
dependency: transitive
description:
name: screen_retriever_platform_interface
sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0
sha256: "3af22d926bedf20c2caa308eea376776451a3af125919ce072e56525fded8901"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
version: "0.2.1"
screen_retriever_windows:
dependency: transitive
description:
name: screen_retriever_windows
sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13"
sha256: c44b38a4c4bab34af259180a70a4eee1e29384e7b82e627c9faa68afcdab2e73
url: "https://pub.dev"
source: hosted
version: "0.2.0"
version: "0.2.1"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
url: "https://pub.dev"
source: hosted
version: "2.5.3"
version: "2.5.5"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
url: "https://pub.dev"
source: hosted
version: "2.4.12"
version: "2.4.23"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
version: "2.5.6"
shared_preferences_linux:
dependency: transitive
description:
@@ -644,10 +772,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "2.4.2"
shared_preferences_web:
dependency: transitive
description:
@@ -689,18 +817,18 @@ packages:
dependency: transitive
description:
name: source_gen
sha256: "7b19d6ba131c6eb98bfcbf8d56c1a7002eba438af2e7ae6f8398b2b0f4f381e3"
sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "4.2.3"
source_span:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
version: "1.10.2"
stack_trace:
dependency: transitive
description:
@@ -745,18 +873,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
version: "0.7.7"
timing:
dependency: transitive
description:
name: timing
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
version: "0.7.11"
typed_data:
dependency: transitive
description:
@@ -777,18 +897,18 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.0.2"
version: "15.2.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c"
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.1.3"
version: "1.2.1"
web:
dependency: transitive
description:
@@ -817,10 +937,10 @@ packages:
dependency: transitive
description:
name: win32
sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
source: hosted
version: "5.14.0"
version: "5.15.0"
window_manager:
dependency: "direct main"
description:
@@ -845,6 +965,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.6.1"
xxh3:
dependency: transitive
description:
name: xxh3
sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
yaml:
dependency: transitive
description:
@@ -854,5 +982,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.29.0"
dart: ">=3.10.3 <4.0.0"
flutter: ">=3.38.4"
+6 -5
View File
@@ -36,12 +36,13 @@ dependencies:
cupertino_icons: ^1.0.8
dio: ^5.9.0
path: ^1.9.1
injectable: ^2.5.1
injectable: ^3.0.0
crypto: ^3.0.6
get_it: ^8.2.0
get_it: ^9.2.1
flutter_bloc: ^9.1.1
file_picker: ^10.3.2
file_picker: ^11.0.2
pixelarticons: ^0.4.0
flutter_secure_storage: ^10.3.1
shared_preferences: ^2.5.3
window_manager: ^0.5.1
@@ -54,8 +55,8 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
injectable_generator: ^2.8.1
flutter_lints: ^6.0.0
injectable_generator: ^3.0.2
build_runner: ^2.7.0
# For information on the generic Dart part of this file, see the
@@ -0,0 +1,89 @@
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_session.dart';
import 'package:moonwell_launcher/features/preferences/data/repositories/local_preferences_repository.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
group('LocalPreferencesRepository', () {
setUp(() {
SharedPreferences.setMockInitialValues(<String, Object>{});
FlutterSecureStorage.setMockInitialValues(<String, String>{});
});
Future<LocalPreferencesRepository> createRepository() async {
final preferences = await SharedPreferences.getInstance();
return LocalPreferencesRepository(
preferences: preferences,
secureStorage: const FlutterSecureStorage(),
);
}
test('persists output directory through shared preferences', () async {
final repository = await createRepository();
final uri = Uri.file(r'C:\Games\MoonWell');
await repository.setOutputDir(uri);
expect(await repository.getOutputDir(), uri);
});
test('persists launcher session through secure storage', () async {
final repository = await createRepository();
final session = LauncherSession(
accessToken: 'token',
tokenType: 'Bearer',
expiresAt: DateTime.parse('2030-01-01T00:00:00Z'),
);
await repository.setLauncherSession(session);
final restoredSession = await repository.getLauncherSession();
expect(restoredSession?.accessToken, 'token');
expect(restoredSession?.tokenType, 'Bearer');
expect(
restoredSession?.expiresAt?.toUtc(),
DateTime.parse('2030-01-01T00:00:00Z'),
);
});
test('clears persisted launcher session', () async {
final repository = await createRepository();
await repository.setLauncherSession(
const LauncherSession(
accessToken: 'token',
tokenType: 'Bearer',
expiresAt: null,
),
);
await repository.clearLauncherSession();
expect(await repository.getLauncherSession(), isNull);
});
test(
'removes legacy shared preferences session without restoring it',
() async {
final legacySession = LauncherSession(
accessToken: 'legacy-token',
tokenType: 'Bearer',
expiresAt: DateTime.parse('2030-01-01T00:00:00Z'),
);
SharedPreferences.setMockInitialValues(<String, Object>{
'launcher_session': jsonEncode(legacySession.toJson()),
});
final preferences = await SharedPreferences.getInstance();
final repository = LocalPreferencesRepository(
preferences: preferences,
secureStorage: const FlutterSecureStorage(),
);
expect(await repository.getLauncherSession(), isNull);
expect(preferences.containsKey('launcher_session'), isFalse);
},
);
});
}
@@ -1,52 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_session.dart';
import 'package:moonwell_launcher/features/preferences/data/repositories/shared_prefs_preferences_repository.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
group('SharedPrefsPreferencesRepository', () {
setUp(() {
SharedPreferences.setMockInitialValues(<String, Object>{});
});
test('persists launcher session between reads', () async {
final preferences = await SharedPreferences.getInstance();
final repository = SharedPrefsPreferencesRepository(
preferences: preferences,
);
final session = LauncherSession(
accessToken: 'token',
tokenType: 'Bearer',
expiresAt: DateTime.parse('2030-01-01T00:00:00Z'),
);
await repository.setLauncherSession(session);
final restoredSession = await repository.getLauncherSession();
expect(restoredSession?.accessToken, 'token');
expect(restoredSession?.tokenType, 'Bearer');
expect(
restoredSession?.expiresAt?.toUtc(),
DateTime.parse('2030-01-01T00:00:00Z'),
);
});
test('clears persisted launcher session', () async {
final preferences = await SharedPreferences.getInstance();
final repository = SharedPrefsPreferencesRepository(
preferences: preferences,
);
await repository.setLauncherSession(
LauncherSession(
accessToken: 'token',
tokenType: 'Bearer',
expiresAt: null,
),
);
await repository.clearLauncherSession();
expect(await repository.getLauncherSession(), isNull);
});
});
}
@@ -6,10 +6,13 @@
#include "generated_plugin_registrant.h"
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
WindowManagerPluginRegisterWithRegistrar(
+2
View File
@@ -3,11 +3,13 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_windows
screen_retriever_windows
window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)
set(PLUGIN_BUNDLED_LIBRARIES)