From 3b01afeeb2de8177c5ad7e644b8d79501a69cb47 Mon Sep 17 00:00:00 2001 From: sindoring Date: Tue, 28 Jul 2026 21:25:06 +0400 Subject: [PATCH] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++ appcast.xml | 24 +++----- docs/production_deploy.md | 5 ++ lib/app/design_system/mw_authentication.dart | 8 ++- .../design_system/mw_launcher_components.dart | 58 ++++++++++--------- lib/app/design_system/mw_shells.dart | 18 +++--- lib/app/launcher_version.dart | 23 ++++++++ lib/app/mw_app.dart | 22 ++++--- lib/main.dart | 13 ++++- .../.plugin_symlinks/package_info_plus | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 24 ++++++++ pubspec.yaml | 3 +- .../design_system/mw_authentication_test.dart | 23 ++++++++ .../mw_launcher_components_test.dart | 20 +++++++ test/app/design_system/mw_shells_test.dart | 19 +++--- 16 files changed, 196 insertions(+), 71 deletions(-) create mode 100644 lib/app/launcher_version.dart create mode 120000 linux/flutter/ephemeral/.plugin_symlinks/package_info_plus 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); });