diff --git a/README.md b/README.md
index 99fb5bd..bb33306 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,10 @@ The Windows executable requests administrator privileges on startup so it can
synchronize game clients installed under protected directories such as
`Program Files`.
+Launcher version labels are read from the packaged application metadata. Change
+only the `version: X.Y.Z+N` value in `pubspec.yaml`; login, launcher status,
+Windows metadata, installer packaging, and AppCast are synchronized from it.
+
The launcher is responsible for:
- logging in to a player's MoonWell account
diff --git a/appcast.xml b/appcast.xml
index 8fb510e..4a97c55 100644
--- a/appcast.xml
+++ b/appcast.xml
@@ -1,22 +1,14 @@
-
-
+
+
MoonWell Launcher
- Обновления MoonWell Launcher
+ MoonWell Launcher updates
ru
-
- MoonWell Launcher 1.0.1
- Обновление интерфейса, синхронизации клиента и интеграции с сервером MoonWell.
- Tue, 28 Jul 2026 20:33:00 +0400
-
+ MoonWell Launcher 1.0.2
+ Исправлено отображение версии и добавлен запуск с правами администратора.
+ Tue, 28 Jul 2026 21:23:41 +04:00
+
-
+
\ No newline at end of file
diff --git a/docs/production_deploy.md b/docs/production_deploy.md
index b7732b4..1d4172d 100644
--- a/docs/production_deploy.md
+++ b/docs/production_deploy.md
@@ -84,6 +84,11 @@ so the script writes that same complete value to `sparkle:version` and writes
`X.Y.Z` to `sparkle:shortVersionString`. Every production release must have an
`X.Y.Z` value greater than the release currently published in AppCast.
+The UI reads `X.Y.Z` from the packaged application metadata through
+`package_info_plus`. Version labels on the login and launcher screens therefore
+update automatically after changing `pubspec.yaml`; do not add separate
+hardcoded version strings.
+
Run a dry run first:
```powershell
diff --git a/lib/app/design_system/mw_authentication.dart b/lib/app/design_system/mw_authentication.dart
index edfd57d..5e91c60 100644
--- a/lib/app/design_system/mw_authentication.dart
+++ b/lib/app/design_system/mw_authentication.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:moonwell_launcher/app/launcher_version.dart';
import 'package:moonwell_launcher/app/design_system/mw_shells.dart';
import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart';
@@ -47,6 +48,7 @@ class _MwAuthenticationFormState extends State {
Widget build(BuildContext context) {
final accent = MoonWellDesignTokens.of(context).accent;
final registering = widget.mode == MwAuthenticationMode.registration;
+ final launcherVersion = LauncherVersionScope.of(context);
return AutofillGroup(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -259,10 +261,10 @@ class _MwAuthenticationFormState extends State {
),
),
const SizedBox(height: 28),
- const Center(
+ Center(
child: Text(
- 'MOONWELL · V 1.0.0 · PLAY.MOON-WELL.ONLINE',
- style: TextStyle(
+ 'MOONWELL · V $launcherVersion · PLAY.MOON-WELL.ONLINE',
+ style: const TextStyle(
fontFamily: 'JetBrains Mono',
color: Color(0x52ECE4D0),
fontSize: 8,
diff --git a/lib/app/design_system/mw_launcher_components.dart b/lib/app/design_system/mw_launcher_components.dart
index 0aa7e59..86bbb18 100644
--- a/lib/app/design_system/mw_launcher_components.dart
+++ b/lib/app/design_system/mw_launcher_components.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:moonwell_launcher/app/launcher_version.dart';
import 'package:moonwell_launcher/app/design_system/mw_primitives.dart';
import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart';
@@ -718,37 +719,40 @@ class MwLauncherStatusBar extends StatelessWidget {
final String? eta;
final String? buildHash;
@override
- Widget build(BuildContext context) => Container(
- padding: const EdgeInsets.symmetric(horizontal: 32),
- decoration: const BoxDecoration(
- color: Color(0xD904060E),
- border: Border(top: BorderSide(color: _border)),
- ),
- child: DefaultTextStyle(
- style: const TextStyle(
- fontFamily: 'JetBrains Mono',
- color: _muted,
- fontSize: 8,
- letterSpacing: .8,
+ Widget build(BuildContext context) {
+ final launcherVersion = LauncherVersionScope.of(context);
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 32),
+ decoration: const BoxDecoration(
+ color: Color(0xD904060E),
+ border: Border(top: BorderSide(color: _border)),
),
- child: Row(
- children: [
- Expanded(
- child: Text(status, maxLines: 1, overflow: TextOverflow.ellipsis),
- ),
- if (fileCount != null) Text(fileCount!),
- if (speed != null) ...[const SizedBox(width: 24), Text(speed!)],
- if (eta != null) ...[const SizedBox(width: 24), Text(eta!)],
- if (buildHash != null) ...[
+ child: DefaultTextStyle(
+ style: const TextStyle(
+ fontFamily: 'JetBrains Mono',
+ color: _muted,
+ fontSize: 8,
+ letterSpacing: .8,
+ ),
+ child: Row(
+ children: [
+ Expanded(
+ child: Text(status, maxLines: 1, overflow: TextOverflow.ellipsis),
+ ),
+ if (fileCount != null) Text(fileCount!),
+ if (speed != null) ...[const SizedBox(width: 24), Text(speed!)],
+ if (eta != null) ...[const SizedBox(width: 24), Text(eta!)],
+ if (buildHash != null) ...[
+ const SizedBox(width: 24),
+ Text(buildHash!),
+ ],
const SizedBox(width: 24),
- Text(buildHash!),
+ Text('Лаунчер v$launcherVersion'),
],
- const SizedBox(width: 24),
- const Text('Лаунчер v1.0.0'),
- ],
+ ),
),
- ),
- );
+ );
+ }
}
class MwAccountAffordance extends StatelessWidget {
diff --git a/lib/app/design_system/mw_shells.dart b/lib/app/design_system/mw_shells.dart
index 3d4fc15..882267b 100644
--- a/lib/app/design_system/mw_shells.dart
+++ b/lib/app/design_system/mw_shells.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:moonwell_launcher/app/launcher_version.dart';
import 'package:moonwell_launcher/app/design_system/mw_launcher_components.dart';
import 'package:moonwell_launcher/app/design_system/mw_window_chrome.dart';
import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart';
@@ -302,21 +303,22 @@ class _LoginStatusBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
+ final launcherVersion = LauncherVersionScope.of(context);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 32),
decoration: const BoxDecoration(
color: Color(0xD904060E),
border: Border(top: BorderSide(color: Color(0x247FB8D4))),
),
- child: const Row(
+ child: Row(
children: [
- Text('Версия лаунчера 1.0.0'),
- SizedBox(width: 24),
- Text('·'),
- SizedBox(width: 24),
- Text('Соединение защищено TLS'),
- Spacer(),
- Text('© 2026 MoonWell'),
+ Text('Версия лаунчера $launcherVersion'),
+ const SizedBox(width: 24),
+ const Text('·'),
+ const SizedBox(width: 24),
+ const Text('Соединение защищено TLS'),
+ const Spacer(),
+ const Text('© 2026 MoonWell'),
],
),
);
diff --git a/lib/app/launcher_version.dart b/lib/app/launcher_version.dart
new file mode 100644
index 0000000..f43724a
--- /dev/null
+++ b/lib/app/launcher_version.dart
@@ -0,0 +1,23 @@
+import 'package:flutter/widgets.dart';
+
+class LauncherVersionScope extends InheritedWidget {
+ const LauncherVersionScope({
+ super.key,
+ required this.version,
+ required super.child,
+ });
+
+ final String version;
+
+ static String of(BuildContext context) {
+ return context
+ .dependOnInheritedWidgetOfExactType()
+ ?.version ??
+ 'dev';
+ }
+
+ @override
+ bool updateShouldNotify(LauncherVersionScope oldWidget) {
+ return version != oldWidget.version;
+ }
+}
diff --git a/lib/app/mw_app.dart b/lib/app/mw_app.dart
index 139dd26..10dd488 100644
--- a/lib/app/mw_app.dart
+++ b/lib/app/mw_app.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:moonwell_launcher/app/design_system/mw_shells.dart';
import 'package:moonwell_launcher/app/home_screen/home_screen.dart';
+import 'package:moonwell_launcher/app/launcher_version.dart';
import 'package:moonwell_launcher/app/login_screen/login_screen.dart';
import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart';
import 'package:moonwell_launcher/features/launcher/application/restore_launcher_session_use_case.dart';
@@ -9,7 +10,9 @@ import 'package:moonwell_launcher/features/preferences/domain/repositories/prefe
import 'package:moonwell_launcher/service_container.dart';
class MoonWellApp extends StatefulWidget {
- const MoonWellApp({super.key});
+ const MoonWellApp({super.key, required this.launcherVersion});
+
+ final String launcherVersion;
@override
State createState() => _MoonWellAppState();
@@ -35,13 +38,16 @@ class _MoonWellAppState extends State {
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _themeController,
- builder: (context, _) => LauncherThemeScope(
- controller: _themeController,
- child: MaterialApp(
- title: 'MoonWell',
- debugShowCheckedModeBanner: false,
- home: const _LauncherBootstrapScreen(),
- theme: MoonWellTheme.create(_themeController.variant),
+ builder: (context, _) => LauncherVersionScope(
+ version: widget.launcherVersion,
+ child: LauncherThemeScope(
+ controller: _themeController,
+ child: MaterialApp(
+ title: 'MoonWell',
+ debugShowCheckedModeBanner: false,
+ home: const _LauncherBootstrapScreen(),
+ theme: MoonWellTheme.create(_themeController.variant),
+ ),
),
),
);
diff --git a/lib/main.dart b/lib/main.dart
index 55c7241..ff1bb31 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:moonwell_launcher/app/mw_app.dart';
import 'package:moonwell_launcher/features/launcher/data/launcher_update_service.dart';
import 'package:moonwell_launcher/service_container.dart';
+import 'package:package_info_plus/package_info_plus.dart';
import 'package:window_manager/window_manager.dart';
Future main() async {
@@ -13,6 +14,7 @@ Future main() async {
await configureDependencies(env: 'flutter');
final launcherUpdateService = getIt();
+ final launcherVersion = await _readLauncherVersion();
const windowOptions = WindowOptions(
size: Size(1280, 720),
@@ -31,5 +33,14 @@ Future main() async {
}),
);
- runApp(const MoonWellApp());
+ runApp(MoonWellApp(launcherVersion: launcherVersion));
+}
+
+Future _readLauncherVersion() async {
+ try {
+ final packageInfo = await PackageInfo.fromPlatform();
+ return packageInfo.version;
+ } catch (_) {
+ return 'dev';
+ }
}
diff --git a/linux/flutter/ephemeral/.plugin_symlinks/package_info_plus b/linux/flutter/ephemeral/.plugin_symlinks/package_info_plus
new file mode 120000
index 0000000..f6afd7a
--- /dev/null
+++ b/linux/flutter/ephemeral/.plugin_symlinks/package_info_plus
@@ -0,0 +1 @@
+C:/Users/sindo/AppData/Local/Pub/Cache/hosted/pub.dev/package_info_plus-9.0.1/
\ No newline at end of file
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index d8e7226..4490c27 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -8,6 +8,7 @@ import Foundation
import auto_updater_macos
import file_picker
import flutter_secure_storage_darwin
+import package_info_plus
import screen_retriever_macos
import shared_preferences_foundation
import url_launcher_macos
@@ -17,6 +18,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AutoUpdaterMacosPlugin.register(with: registry.registrar(forPlugin: "AutoUpdaterMacosPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
+ FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
diff --git a/pubspec.lock b/pubspec.lock
index 1157d98..79a58e3 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -424,6 +424,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.4.0"
+ http:
+ dependency: transitive
+ description:
+ name: http
+ sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.6.0"
http_multi_server:
dependency: transitive
description:
@@ -592,6 +600,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.0"
+ package_info_plus:
+ dependency: "direct main"
+ description:
+ name: package_info_plus
+ sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20"
+ url: "https://pub.dev"
+ source: hosted
+ version: "9.0.1"
+ package_info_plus_platform_interface:
+ dependency: transitive
+ description:
+ name: package_info_plus_platform_interface
+ sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.1"
path:
dependency: "direct main"
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 5c82846..8b6000a 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
-version: 1.0.1+2
+version: 1.0.2+4
environment:
sdk: ">=3.10.0"
@@ -47,6 +47,7 @@ dependencies:
window_manager: ^0.5.1
auto_updater: 1.0.0
url_launcher: ^6.3.2
+ package_info_plus: ^9.0.1
dev_dependencies:
flutter_test:
diff --git a/test/app/design_system/mw_authentication_test.dart b/test/app/design_system/mw_authentication_test.dart
index ba00af0..8567297 100644
--- a/test/app/design_system/mw_authentication_test.dart
+++ b/test/app/design_system/mw_authentication_test.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:moonwell_launcher/app/design_system/mw_authentication.dart';
+import 'package:moonwell_launcher/app/launcher_version.dart';
import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart';
void main() {
@@ -117,4 +118,26 @@ void main() {
hasLength(1),
);
});
+
+ testWidgets('authentication footer uses the packaged launcher version', (
+ tester,
+ ) async {
+ await tester.pumpWidget(
+ LauncherVersionScope(
+ version: '9.8.7',
+ child: MaterialApp(
+ theme: MoonWellTheme.create(MoonWellThemeVariant.forest),
+ home: Scaffold(
+ body: MwAuthenticationForm(
+ usernameController: TextEditingController(),
+ passwordController: TextEditingController(),
+ onSubmit: () {},
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(find.textContaining('V 9.8.7'), findsOneWidget);
+ });
}
diff --git a/test/app/design_system/mw_launcher_components_test.dart b/test/app/design_system/mw_launcher_components_test.dart
index 2618ab1..b92814f 100644
--- a/test/app/design_system/mw_launcher_components_test.dart
+++ b/test/app/design_system/mw_launcher_components_test.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:moonwell_launcher/app/design_system/mw_launcher_components.dart';
+import 'package:moonwell_launcher/app/launcher_version.dart';
import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart';
void main() {
@@ -137,6 +138,25 @@ void main() {
expect(find.text('Проверить файлы'), findsOneWidget);
expect(find.text('Выйти'), findsNothing);
});
+
+ testWidgets('status bar uses the packaged launcher version', (tester) async {
+ await tester.pumpWidget(
+ LauncherVersionScope(
+ version: '9.8.7',
+ child: MaterialApp(
+ theme: MoonWellTheme.create(MoonWellThemeVariant.forest),
+ home: const Scaffold(
+ body: SizedBox(
+ height: 26,
+ child: MwLauncherStatusBar(status: 'Ready'),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(find.textContaining('v9.8.7'), findsOneWidget);
+ });
}
void _noop() {}
diff --git a/test/app/design_system/mw_shells_test.dart b/test/app/design_system/mw_shells_test.dart
index 4cc6c5e..c62cc8b 100644
--- a/test/app/design_system/mw_shells_test.dart
+++ b/test/app/design_system/mw_shells_test.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:moonwell_launcher/app/design_system/mw_shells.dart';
+import 'package:moonwell_launcher/app/launcher_version.dart';
import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart';
void main() {
@@ -24,13 +25,16 @@ void main() {
await tester.pumpWidget(
MaterialApp(
theme: MoonWellTheme.create(MoonWellThemeVariant.forest),
- home: MwLoginShell(
- form: const SizedBox(),
- onDrag: _noop,
- onDoubleTap: _noop,
- onMinimize: _noop,
- onMaximizeRestore: _noop,
- onClose: _noop,
+ home: LauncherVersionScope(
+ version: '9.8.7',
+ child: MwLoginShell(
+ form: const SizedBox(),
+ onDrag: _noop,
+ onDoubleTap: _noop,
+ onMinimize: _noop,
+ onMaximizeRestore: _noop,
+ onClose: _noop,
+ ),
),
),
);
@@ -38,6 +42,7 @@ void main() {
final image = tester.widget(find.byType(Image));
expect((image.image as AssetImage).assetName, 'assets/login_key_art.png');
+ expect(find.textContaining('9.8.7'), findsOneWidget);
expect(tester.takeException(), isNull);
});