diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 0000000..c300356 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "stable" +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 0967ef4..1395495 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1 +1,3 @@ -{} +{ + "dart.flutterSdkPath": ".fvm/versions/stable" +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index fa85169..0bcaff0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,9 @@ Important areas: - `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/app/design_system` contains presentation-only catalog components. +- `widgetbook/` is the standalone component-catalog workspace. Stories and + fixture data stay in that workspace and import components from the launcher. - `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 @@ -32,26 +35,28 @@ Important areas: ## 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`. +MoonWell Launcher tracks the latest stable Flutter SDK through FVM. Run all +Dart and Flutter commands through `fvm` and keep `.fvmrc` on the stable channel. ## 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 --dart-define=MOONWELL_APPCAST_URL=https://host/launcher/updates/appcast.xml -flutter build windows --dart-define=MOONWELL_API_BASE_URL=https://host --dart-define=MOONWELL_APPCAST_URL=https://host/launcher/updates/appcast.xml +fvm flutter pub get +fvm dart run build_runner build --delete-conflicting-outputs +fvm dart format --set-exit-if-changed . +fvm flutter analyze +fvm flutter test +fvm flutter run -d windows --dart-define=MOONWELL_API_BASE_URL=https://host --dart-define=MOONWELL_APPCAST_URL=https://host/launcher/updates/appcast.xml +fvm flutter build windows --dart-define=MOONWELL_API_BASE_URL=https://host --dart-define=MOONWELL_APPCAST_URL=https://host/launcher/updates/appcast.xml +cd widgetbook +fvm flutter pub get +fvm dart run build_runner build +fvm flutter run -d windows ``` See `docs/linting.md` for the required format, analyze, test, and Lefthook pre-commit workflow. +Follow `docs/widgetbook.md` for all Widgetbook use-case generation. ## Configuration @@ -102,3 +107,5 @@ or local-only API URLs. - 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. +- Keep catalog widgets presentation-only. Do not call BLoCs, APIs, storage, + file pickers, dependency injection, or native window plugins from them. diff --git a/README.md b/README.md index 661e22d..132f07d 100644 --- a/README.md +++ b/README.md @@ -23,15 +23,14 @@ The launcher is responsible for: ## Prerequisites -Use the latest stable Flutter SDK available from the official Flutter release -channel. This project does not support FVM-pinned SDK workflows. +Install FVM. The repository `.fvmrc` tracks the latest stable Flutter channel, +and all Dart and Flutter commands are run through FVM. ```powershell -flutter pub get +fvm flutter pub get ``` -If `flutter` is unavailable, restart the shell after installing Flutter or -fix PATH so `flutter --version` works. +Verify the configured SDK with `fvm flutter --version`. ## Configuration @@ -53,21 +52,32 @@ feeds must use HTTPS. Example run command: ```powershell -flutter run -d windows --dart-define=MOONWELL_API_BASE_URL=https://host --dart-define=MOONWELL_APPCAST_URL=https://host/launcher/updates/appcast.xml +fvm flutter run -d windows --dart-define=MOONWELL_API_BASE_URL=https://host --dart-define=MOONWELL_APPCAST_URL=https://host/launcher/updates/appcast.xml ``` ## Development +Run the component catalog from its standalone workspace: + +```powershell +cd widgetbook +fvm dart run build_runner build +fvm flutter run -d windows +``` + +See `docs/widgetbook.md` for the required use-case naming, knobs, callback, +asset, and coverage conventions. + Regenerate dependency injection after changing injectable services: ```powershell -dart run build_runner build --delete-conflicting-outputs +fvm dart run build_runner build --delete-conflicting-outputs ``` Run static analysis: ```powershell -flutter analyze +fvm flutter analyze ``` See `docs/linting.md` for the full format, analyze, test, and Lefthook @@ -76,7 +86,7 @@ pre-commit workflow. Build the Windows launcher: ```powershell -flutter build windows --dart-define=MOONWELL_API_BASE_URL=https://host --dart-define=MOONWELL_APPCAST_URL=https://host/launcher/updates/appcast.xml +fvm flutter build windows --dart-define=MOONWELL_API_BASE_URL=https://host --dart-define=MOONWELL_APPCAST_URL=https://host/launcher/updates/appcast.xml ``` The Inno Setup installer script lives at `installer/moonwell_launcher.iss`. @@ -85,7 +95,9 @@ manual release procedure. ## Architecture -The UI lives in `lib/app`, with shared UI widgets under `lib/app/widgets`. +The UI lives in `lib/app`. Presentation-only design-system components live in +`lib/app/design_system` and are catalogued by the standalone `widgetbook/` +workspace before they are integrated into production screens. Launcher domain models, sync use cases, API clients, installation services, and preferences live under `lib/features`. @@ -111,3 +123,4 @@ See `docs/launcher_web_api_spec.md` for the API and sync contract. upgrade feasibility notes - `docs/launcher_web_api_spec.md`: current launcher Web API and sync behavior - `docs/launcher_self_update.md`: Windows self-update and release operations +- `docs/widgetbook.md`: Widgetbook use-case generation and catalog conventions diff --git a/assets/fonts/Cinzel-OFL.txt b/assets/fonts/Cinzel-OFL.txt new file mode 100644 index 0000000..afdfd14 --- /dev/null +++ b/assets/fonts/Cinzel-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Cinzel Project Authors (https://github.com/NDISCOVER/Cinzel) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/CormorantGaramond-OFL.txt b/assets/fonts/CormorantGaramond-OFL.txt new file mode 100644 index 0000000..507d70f --- /dev/null +++ b/assets/fonts/CormorantGaramond-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2015 the Cormorant Project Authors (github.com/CatharsisFonts/Cormorant) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/CormorantGaramond.ttf b/assets/fonts/CormorantGaramond.ttf new file mode 100644 index 0000000..d992a83 Binary files /dev/null and b/assets/fonts/CormorantGaramond.ttf differ diff --git a/assets/fonts/Inter-OFL.txt b/assets/fonts/Inter-OFL.txt new file mode 100644 index 0000000..21f6aff --- /dev/null +++ b/assets/fonts/Inter-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/Inter.ttf b/assets/fonts/Inter.ttf new file mode 100644 index 0000000..047c92f Binary files /dev/null and b/assets/fonts/Inter.ttf differ diff --git a/assets/fonts/JetBrainsMono-OFL.txt b/assets/fonts/JetBrainsMono-OFL.txt new file mode 100644 index 0000000..821a3da --- /dev/null +++ b/assets/fonts/JetBrainsMono-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/JetBrainsMono.ttf b/assets/fonts/JetBrainsMono.ttf new file mode 100644 index 0000000..aa310be Binary files /dev/null and b/assets/fonts/JetBrainsMono.ttf differ diff --git a/docs/launcher_self_update.md b/docs/launcher_self_update.md index 4d6bc52..3c938fb 100644 --- a/docs/launcher_self_update.md +++ b/docs/launcher_self_update.md @@ -14,7 +14,7 @@ launcher. Periodic WinSparkle checks are disabled. Pass the public feed URL at build time: ```powershell -flutter build windows ` +fvm flutter build windows ` --dart-define=MOONWELL_API_BASE_URL=https://host ` --dart-define=MOONWELL_APPCAST_URL=https://host/launcher/updates/appcast.xml ``` @@ -91,13 +91,13 @@ Perform Windows signing on the Windows release machine because the package invokes WinSparkle's Windows signing utility. 1. Update `pubspec.yaml` to the new `X.Y.Z+N` version. -2. Run `flutter pub get`, formatting, analysis, and all tests. +2. Run `fvm flutter pub get`, formatting, analysis, and all tests. 3. Build Windows with both production `--dart-define` values. 4. Compile the installer with `ISCC.exe /DMyAppVersion=X.Y.Z`. 5. Make the private key available outside the repository and sign the artifact: ```powershell - dart run auto_updater:sign_update ` + fvm dart run auto_updater:sign_update ` build\installer\moonwell_launcher_X.Y.Z_windows_setup.exe ` C:\secure-path\dsa_priv.pem ``` diff --git a/docs/launcher_web_api_spec.md b/docs/launcher_web_api_spec.md index 09970ae..0eca5b3 100644 --- a/docs/launcher_web_api_spec.md +++ b/docs/launcher_web_api_spec.md @@ -21,8 +21,8 @@ The launcher API base URL is provided via: Current integration mode is compile-time: -- `flutter run -d windows --dart-define=MOONWELL_API_BASE_URL=https://host` -- `flutter build windows --dart-define=MOONWELL_API_BASE_URL=https://host` +- `fvm flutter run -d windows --dart-define=MOONWELL_API_BASE_URL=https://host` +- `fvm flutter build windows --dart-define=MOONWELL_API_BASE_URL=https://host` ## API Flow diff --git a/docs/linting.md b/docs/linting.md index f41de17..491d906 100644 --- a/docs/linting.md +++ b/docs/linting.md @@ -1,19 +1,19 @@ # Linting And Test Flow -MoonWell Launcher supports the latest stable Flutter SDK only. Do not use FVM -for this project. +MoonWell Launcher tracks the latest stable Flutter SDK through FVM. Use `fvm` +for every Dart and Flutter command. ## Required Checks Run these commands before committing: ```powershell -dart format --set-exit-if-changed . -flutter analyze -flutter test +fvm dart format --set-exit-if-changed . +fvm flutter analyze +fvm flutter test ``` -Use `dart format .` to apply formatting fixes when the format check fails. +Use `fvm dart format .` to apply formatting fixes when the format check fails. ## Lefthook diff --git a/docs/project_review.md b/docs/project_review.md index b747eb3..0d2c918 100644 --- a/docs/project_review.md +++ b/docs/project_review.md @@ -83,13 +83,13 @@ 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`. +2. Run `fvm 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`. + `fvm dart run build_runner build --delete-conflicting-outputs`. +4. Run `fvm flutter analyze`. 5. Run a Windows build smoke test: - `flutter build windows --dart-define=MOONWELL_API_BASE_URL=https://host`. + `fvm flutter build windows --dart-define=MOONWELL_API_BASE_URL=https://host`. ## Recommended Refactoring Direction diff --git a/docs/widgetbook.md b/docs/widgetbook.md new file mode 100644 index 0000000..43571c0 --- /dev/null +++ b/docs/widgetbook.md @@ -0,0 +1,330 @@ +# Widgetbook Use-Case Generation Instructions + +## Workspace And Asset Contract + +The catalog is a standalone Flutter workspace under `widgetbook/` with a path +dependency on the launcher package. Production components live under +`lib/app/design_system`; stories, fixtures, and generated directories stay in +`widgetbook/lib`. + +```powershell +cd widgetbook +fvm flutter pub get +fvm dart run build_runner build +fvm flutter run -d windows +``` + +The catalog provides forest and temple themes, 1024×640, 1280×720, and +1600×900 Windows viewports, and text scaling from 100% through 200%. The +production launcher composition targets a fixed minimum window of 1280×720 to +preserve the mockup layout. + +The launcher compositions must follow `mockup.html`: a 240-pixel game rail, +brand and account header, hero copy over the key art, a 360-pixel floating news +panel, and the realm/progress/play strip above the status bar. The login shell +uses the mockup's split artwork and 480-pixel form panel. Widgetbook stories +exercise these production compositions; they are not alternate app layouts. + +Temporary launcher artwork uses a 1536×1024 source cropped with `BoxFit.cover`. +Final artwork should be at least 2560×1440, keep essential content out of the +outer 12% on every edge, and leave the central/right content areas readable +under a dark gradient. The MoonWell brand mark is composed from Flutter vector +icons and text and must remain legible between 96 and 360 logical pixels. + +Generate comprehensive Widgetbook use cases for Flutter components that showcase different variants and configurations effectively. + +## Core Requirements + +### UseCase Annotation Structure + +Each use case must have a properly configured `@UseCase` annotation: + +```dart +@UseCase( + name: 'variantName', // Required: Unique identifier for this variant + type: ComponentType, // Required: The Flutter widget class being showcased +) +``` + +**Naming Rules:** + +- Single use case: `name: 'default'` +- Multiple use cases: Use descriptive names like `'with_label'`, `'disabled'`, `'loading'` +- Names must be unique within the same component type + +### Method Signature Requirements + +**Exact signature pattern:** + +```dart +Widget build[ComponentName][VariantName]UseCase(BuildContext context) { + // Implementation +} +``` + +**Naming conventions:** + +- Single use case: `buildProgressIndicatorUseCase` +- Multiple use cases: `buildProgressIndicatorWithLabelUseCase`, `buildProgressIndicatorDisabledUseCase` +- Always return `Widget` +- Always accept exactly one parameter: `BuildContext context` + +## Parameter Configuration Strategy + +### Priority System + +1. **Critical parameters** (required, affects core functionality): Always use knobs +2. **Visual parameters** (colors, sizes, styles): Use knobs when they demonstrate component flexibility +3. **Behavioral parameters** (enabled/disabled, loading states): Use knobs for interactive demonstration +4. **Callback parameters**: Implement with descriptive print statements +5. **Complex objects**: Hardcode with meaningful defaults, add TODO comments + +### Knob Selection Logic + +- **Bounded numeric values**: Use sliders (opacity: 0.0-1.0, progress: 0-100) +- **Unbounded numeric values**: Use input fields (dimensions, counts) +- **Enums/predefined options**: Use list knobs +- **Text content**: Use string inputs +- **Feature toggles**: Use boolean checkboxes + +## Comprehensive Knobs API + +### Basic Types + +```dart +// Strings +context.knobs.string(label: 'text', initialValue: 'Hello World') +context.knobs.stringOrNull(label: 'optionalText', initialValue: null) + +// Booleans +context.knobs.boolean(label: 'enabled', initialValue: true) +context.knobs.booleanOrNull(label: 'optionalFlag', initialValue: null) + +// Integers +context.knobs.int.input(label: 'count', initialValue: 5) +context.knobs.int.slider(label: 'progress', initialValue: 50, min: 0, max: 100, divisions: 10) +context.knobs.intOrNull.input(label: 'optionalCount', initialValue: null) +context.knobs.intOrNull.slider(label: 'progress', initialValue: null, min: 0, max: 100, divisions: 10) + +// Doubles +context.knobs.double.input(label: 'value', initialValue: 1.5) +context.knobs.double.slider(label: 'opacity', initialValue: 0.8, min: 0.0, max: 1.0, divisions: 20) +context.knobs.doubleOrNull.input(label: 'value', initialValue: null) +context.knobs.doubleOrNull.slider(label: 'optionalOpacity', initialValue: null, min: 0.0, max: 1.0) +``` + +### Advanced Types + +```dart +// Dropdown lists +context.knobs.object.dropdown( + label: 'textAlign', + initialOption: TextAlign.center, + options: [TextAlign.left, TextAlign.center, TextAlign.right], + labelBuilder: (value) => value.name, +) + +// Colors +context.knobs.color(label: 'primaryColor', initialValue: Colors.blue) +context.knobs.colorOrNull(label: 'optionalColor', initialValue: null) + +// DateTime +context.knobs.dateTime( + label: 'selectedDate', + initialValue: DateTime.now(), + start: DateTime.now().subtract(const Duration(days: 365)), + end: DateTime.now().add(const Duration(days: 365)), +) + +// Duration +context.knobs.duration(label: 'animationDuration', initialValue: const Duration(milliseconds: 300)) +``` + +## Advanced Component Handling + +### State Management Integration + +For components requiring external state: + +```dart +// Example: Component requiring a provider +@UseCase(name: 'with_data', type: DataWidget) +Widget buildDataWidgetWithDataUseCase(BuildContext context) { + return MockDataProvider( + data: _generateMockData(), + child: DataWidget( + onItemSelected: (item) => print('Selected item: ${item.id}'), + showLoading: context.knobs.boolean(label: 'showLoading', initialValue: false), + ), + ); +} +``` + +### Complex Parameter Handling + +```dart +// For unmappable parameters +final customObject = CustomConfiguration( + // Hardcoded meaningful defaults + apiEndpoint: 'https://api.example.com', + timeout: const Duration(seconds: 30), +); // TODO: User should configure CustomConfiguration manually + +// For asset references +final iconPath = 'assets/icons/star.svg'; // Verify asset exists in pubspec.yaml +``` + +### Callback Implementation Patterns + +```dart +// Simple callbacks +onPressed: () => print('Button pressed'), + +// Callbacks with data +onChanged: (value) => print('Value changed to: $value'), + +// Complex callbacks +onFormSubmitted: (formData) { + print('Form submitted with data:'); + print(' - Name: ${formData.name}'); + print(' - Email: ${formData.email}'); +}, + +// Async callbacks +onSave: () async { + print('Save operation started'); + await Future.delayed(const Duration(seconds: 1)); + print('Save operation completed'); +}, +``` + +### Theme handling + +Themes are globally injected and must not be provided. + +## Use Case Variant Strategies + +### Single Component, Multiple Variants + +Create variants that showcase different states and configurations: + +```dart +// Default state +@UseCase(name: 'default', type: CustomButton) +Widget buildCustomButtonUseCase(BuildContext context) { /* ... */ } + +// Loading state +@UseCase(name: 'loading', type: CustomButton) +Widget buildCustomButtonLoadingUseCase(BuildContext context) { /* ... */ } + +// Disabled state +@UseCase(name: 'disabled', type: CustomButton) +Widget buildCustomButtonDisabledUseCase(BuildContext context) { /* ... */ } + +// With icon +@UseCase(name: 'with icon', type: CustomButton) +Widget buildCustomButtonWithIconUseCase(BuildContext context) { /* ... */ } +``` + +### Responsive and Theme Variants + +```dart +// Different sizes +@UseCase(name: 'small', type: ProfileCard) +@UseCase(name: 'medium', type: ProfileCard) +@UseCase(name: 'large', type: ProfileCard) +``` + +## Quality Standards + +### Code Quality + +- **No descriptive comments**: Code should be self-documenting +- **Consistent formatting**: Follow Dart style guidelines +- **Meaningful defaults**: Initial values should represent realistic usage +- **Error handling**: Wrap potentially failing operations in try-catch where appropriate + +### Use Case Coverage + +Ensure use cases demonstrate: + +- [ ] Default/primary functionality +- [ ] Edge cases (empty states, maximum values) +- [ ] Interactive behaviors (hover, focus, disabled states) +- [ ] Visual variants (different styles, sizes, colors) +- [ ] Error states when applicable + +### Testing Considerations + +```dart +// Include realistic data volumes +final items = List.generate(50, (index) => 'Item ${index + 1}'); + +// Test boundary conditions +final progress = context.knobs.double.slider( + label: 'progress', + initialValue: 0.7, + min: 0.0, + max: 1.0, + divisions: 100, +); // Allows testing 0%, 100%, and intermediate values +``` + +## Complete Example Template + +```dart +import 'package:flutter/material.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart'; + +@UseCase( + name: 'interactive', + type: CustomSlider, +) +Widget buildCustomSliderInteractiveUseCase(BuildContext context) { + return CustomSlider( + value: context.knobs.double.slider( + label: 'value', + initialValue: 0.5, + min: 0.0, + max: 1.0, + divisions: 20, + ), + min: context.knobs.double.input( + label: 'min', + initialValue: 0.0, + ), + max: context.knobs.double.input( + label: 'max', + initialValue: 1.0, + ), + enabled: context.knobs.boolean( + label: 'enabled', + initialValue: true, + ), + showLabels: context.knobs.boolean( + label: 'showLabels', + initialValue: true, + ), + activeColor: context.knobs.color( + label: 'activeColor', + initialValue: Colors.blue, + ), + onChanged: (value) => print('Slider value changed to: $value'), + onChangeStart: (value) => print('Slider interaction started at: $value'), + onChangeEnd: (value) => print('Slider interaction ended at: $value'), + ); +} +``` + +## Pre-Generation Checklist + +Before generating use cases, verify: + +- [ ] Component class name and import path +- [ ] Required vs optional parameters +- [ ] Parameter types and constraints +- [ ] Available enum values for list knobs +- [ ] Asset dependencies and paths +- [ ] State management requirements +- [ ] Theme/localization dependencies +- [ ] Callback signatures and expected behavior diff --git a/lib/app/design_system/mw_authentication.dart b/lib/app/design_system/mw_authentication.dart new file mode 100644 index 0000000..53c42d2 --- /dev/null +++ b/lib/app/design_system/mw_authentication.dart @@ -0,0 +1,218 @@ +import 'package:flutter/material.dart'; +import 'package:moonwell_launcher/app/design_system/mw_shells.dart'; +import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart'; + +class MwAuthenticationForm extends StatefulWidget { + const MwAuthenticationForm({ + super.key, + required this.usernameController, + required this.passwordController, + required this.onSubmit, + this.loading = false, + this.error, + }); + + final TextEditingController usernameController; + final TextEditingController passwordController; + final VoidCallback onSubmit; + final bool loading; + final String? error; + + @override + State createState() => _MwAuthenticationFormState(); +} + +class _MwAuthenticationFormState extends State { + bool _remember = true; + + @override + Widget build(BuildContext context) { + final accent = MoonWellDesignTokens.of(context).accent; + return AutofillGroup( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const MwBrandMark(width: 210, compact: true), + const SizedBox(height: 28), + Text( + 'ВХОД', + style: TextStyle(color: accent, fontSize: 10, letterSpacing: 2.4), + ), + const SizedBox(height: 10), + const Text( + 'С возвращением', + style: TextStyle( + fontFamily: 'Cinzel', + color: Color(0xFFECE4D0), + fontSize: 30, + letterSpacing: 1, + ), + ), + const SizedBox(height: 7), + const Text( + 'Войди в свой аккаунт, чтобы продолжить путь.', + style: TextStyle( + fontFamily: 'Cormorant Garamond', + fontStyle: FontStyle.italic, + color: Color(0x80ECE4D0), + fontSize: 15, + ), + ), + const SizedBox(height: 22), + Row( + children: [ + _Tab(label: 'ВХОД', selected: true, accent: accent), + _Tab(label: 'РЕГИСТРАЦИЯ', accent: accent), + ], + ), + const SizedBox(height: 18), + _FieldLabel(text: 'ЛОГИН ИЛИ EMAIL'), + const SizedBox(height: 7), + TextField( + controller: widget.usernameController, + enabled: !widget.loading, + textInputAction: TextInputAction.next, + autofillHints: const [AutofillHints.username], + decoration: const InputDecoration(hintText: 'Aranthel'), + ), + const SizedBox(height: 14), + _FieldLabel(text: 'ПАРОЛЬ'), + const SizedBox(height: 7), + TextField( + controller: widget.passwordController, + enabled: !widget.loading, + obscureText: true, + textInputAction: TextInputAction.done, + autofillHints: const [AutofillHints.password], + onSubmitted: (_) => widget.onSubmit(), + ), + if (widget.error != null) ...[ + const SizedBox(height: 10), + Text( + widget.error!, + style: const TextStyle(color: Color(0xFFD76A78), fontSize: 12), + ), + ], + const SizedBox(height: 8), + Row( + children: [ + SizedBox( + width: 24, + child: Checkbox( + value: _remember, + onChanged: widget.loading + ? null + : (value) => setState(() => _remember = value ?? false), + ), + ), + const SizedBox(width: 8), + const Text( + 'Запомнить меня', + style: TextStyle(color: Color(0x9AECE4D0), fontSize: 11), + ), + const Spacer(), + Text( + 'Забыли пароль?', + style: TextStyle(color: accent, fontSize: 11), + ), + ], + ), + const SizedBox(height: 18), + SizedBox( + height: 54, + child: ElevatedButton( + onPressed: widget.loading ? null : widget.onSubmit, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF6D499E), + foregroundColor: const Color(0xFFECE4D0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: widget.loading + ? const SizedBox.square( + dimension: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'ВОЙТИ', + style: TextStyle( + fontFamily: 'Cinzel', + letterSpacing: 2.5, + ), + ), + SizedBox(width: 14), + Icon(Icons.arrow_forward, size: 18), + ], + ), + ), + ), + const SizedBox(height: 28), + const Center( + child: Text( + 'MOONWELL · V 1.0.0 · LOGIN.MOONWELL.GG', + style: TextStyle( + fontFamily: 'JetBrains Mono', + color: Color(0x52ECE4D0), + fontSize: 8, + letterSpacing: 1.2, + ), + ), + ), + ], + ), + ); + } +} + +class _FieldLabel extends StatelessWidget { + const _FieldLabel({required this.text}); + final String text; + @override + Widget build(BuildContext context) => Text( + text, + style: const TextStyle( + fontFamily: 'JetBrains Mono', + color: Color(0x80ECE4D0), + fontSize: 9, + letterSpacing: 1.5, + ), + ); +} + +class _Tab extends StatelessWidget { + const _Tab({ + required this.label, + required this.accent, + this.selected = false, + }); + final String label; + final Color accent; + final bool selected; + @override + Widget build(BuildContext context) => Expanded( + child: Container( + height: 38, + alignment: Alignment.center, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: selected ? accent : const Color(0x247FB8D4), + ), + ), + ), + child: Text( + label, + style: TextStyle( + fontFamily: 'Cinzel', + color: selected ? const Color(0xFFECE4D0) : const Color(0x80ECE4D0), + fontSize: 9, + letterSpacing: 1.5, + ), + ), + ), + ); +} diff --git a/lib/app/design_system/mw_launcher_components.dart b/lib/app/design_system/mw_launcher_components.dart new file mode 100644 index 0000000..be8c7eb --- /dev/null +++ b/lib/app/design_system/mw_launcher_components.dart @@ -0,0 +1,844 @@ +import 'package:flutter/material.dart'; +import 'package:moonwell_launcher/app/design_system/mw_primitives.dart'; +import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart'; + +const _ivory = Color(0xFFECE4D0); +const _muted = Color(0x80ECE4D0); +const _border = Color(0x247FB8D4); + +class MwGameTile extends StatelessWidget { + const MwGameTile({ + super.key, + required this.title, + required this.icon, + this.subtitle, + this.selected = false, + this.enabled = true, + this.compact = false, + this.onPressed, + this.mark, + }); + + final String title; + final String? subtitle; + final IconData icon; + final bool selected; + final bool enabled; + final bool compact; + final VoidCallback? onPressed; + final String? mark; + + @override + Widget build(BuildContext context) { + final accent = MoonWellDesignTokens.of(context).accent; + return Opacity( + opacity: enabled ? 1 : .58, + child: Material( + color: selected ? accent.withAlpha(25) : accent.withAlpha(8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + side: BorderSide(color: selected ? accent : _border), + ), + child: InkWell( + onTap: enabled ? onPressed : null, + borderRadius: BorderRadius.circular(6), + child: SizedBox( + height: 52, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + child: Row( + children: [ + Container( + width: 32, + height: 32, + alignment: Alignment.center, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [const Color(0xFF5B3A8A), accent], + ), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + mark ?? title.characters.first, + style: const TextStyle( + fontFamily: 'Cinzel', + color: _ivory, + fontSize: 12, + ), + ), + ), + if (!compact) ...[ + const SizedBox(width: 10), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title.toUpperCase(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontFamily: 'Cinzel', + color: _ivory, + fontSize: 10, + letterSpacing: .8, + ), + ), + const SizedBox(height: 3), + Text( + (subtitle ?? (enabled ? 'OPEN BETA' : 'СКОРО')) + .toUpperCase(), + style: TextStyle( + fontFamily: 'JetBrains Mono', + color: selected ? accent : _muted, + fontSize: 8, + letterSpacing: 1.1, + ), + ), + ], + ), + ), + if (!enabled) + const Text( + 'SOON', + style: TextStyle( + fontFamily: 'JetBrains Mono', + color: _muted, + fontSize: 7, + letterSpacing: 1, + ), + ), + ], + ], + ), + ), + ), + ), + ), + ); + } +} + +class MwGameRail extends StatelessWidget { + const MwGameRail({super.key, this.compact = false}); + + final bool compact; + + @override + Widget build(BuildContext context) { + final accent = MoonWellDesignTokens.of(context).accent; + return Container( + padding: const EdgeInsets.fromLTRB(14, 48, 14, 30), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xB5070A18), Color(0x8F070A18)], + ), + border: Border(right: BorderSide(color: _border)), + ), + child: Column( + children: [ + Container( + width: 40, + height: 40, + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [accent, const Color(0xFF5B3A8A)], + ), + border: Border.all(color: accent), + boxShadow: [ + BoxShadow(color: accent.withAlpha(70), blurRadius: 12), + ], + ), + child: const Text('A', style: TextStyle(fontFamily: 'Cinzel')), + ), + const SizedBox(height: 14), + const Divider(color: _border), + const Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.fromLTRB(8, 7, 0, 8), + child: Text( + 'ИГРЫ', + style: TextStyle( + fontFamily: 'JetBrains Mono', + color: _muted, + fontSize: 9, + letterSpacing: 2.2, + ), + ), + ), + ), + MwGameTile( + title: 'MoonWell · WoW', + subtitle: 'Open Beta', + mark: 'MW', + icon: Icons.nights_stay, + selected: true, + compact: compact, + onPressed: () {}, + ), + const SizedBox(height: 8), + const MwGameTile( + title: 'MoonWell Karts', + subtitle: 'Closed Alpha', + mark: 'K', + icon: Icons.sports_motorsports, + enabled: false, + ), + const SizedBox(height: 8), + const MwGameTile( + title: 'Tides of Elune', + subtitle: 'В разработке', + mark: 'T', + icon: Icons.style, + enabled: false, + ), + const SizedBox(height: 8), + const MwGameTile( + title: 'Project Verge', + subtitle: 'Анонс', + mark: 'V', + icon: Icons.auto_awesome, + enabled: false, + ), + const Spacer(), + const Divider(color: _border), + Row( + children: [ + IconButton( + onPressed: null, + tooltip: 'Друзья', + icon: const Icon(Icons.people_outline, size: 18), + ), + const Spacer(), + Icon( + Icons.settings_outlined, + color: accent.withAlpha(150), + size: 18, + ), + const SizedBox(width: 12), + ], + ), + ], + ), + ); + } +} + +class MwNewsItemData { + const MwNewsItemData({ + required this.title, + required this.body, + this.createdAt, + this.imageUrl, + }); + final String title; + final String body; + final DateTime? createdAt; + final String? imageUrl; +} + +class MwNewsItem extends StatelessWidget { + const MwNewsItem({super.key, required this.item}); + final MwNewsItemData item; + + @override + Widget build(BuildContext context) { + final accent = MoonWellDesignTokens.of(context).accent; + final date = item.createdAt?.toLocal(); + return Padding( + padding: const EdgeInsets.fromLTRB(0, 13, 12, 13), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + border: Border.all(color: accent), + borderRadius: BorderRadius.circular(3), + ), + child: Text( + 'НОВОСТЬ', + style: TextStyle( + fontFamily: 'JetBrains Mono', + color: accent, + fontSize: 8, + letterSpacing: 1.5, + ), + ), + ), + const Spacer(), + Text( + date == null + ? 'СЕГОДНЯ' + : '${date.day.toString().padLeft(2, '0')}.${date.month.toString().padLeft(2, '0')}', + style: const TextStyle( + fontFamily: 'JetBrains Mono', + color: _muted, + fontSize: 9, + ), + ), + ], + ), + const SizedBox(height: 7), + Text( + item.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontFamily: 'Cormorant Garamond', + color: _ivory, + fontSize: 16, + height: 1.25, + ), + ), + ], + ), + ); + } +} + +enum MwNewsListState { loading, empty, failure, populated } + +class MwNewsList extends StatelessWidget { + const MwNewsList({ + super.key, + required this.state, + this.items = const [], + this.error, + this.onRetry, + }); + final MwNewsListState state; + final List items; + final String? error; + final VoidCallback? onRetry; + + @override + Widget build(BuildContext context) { + final accent = MoonWellDesignTokens.of(context).accent; + return Container( + decoration: BoxDecoration( + color: const Color(0xD00A0D1F), + border: Border.all(color: const Color(0x477FB8D4)), + borderRadius: BorderRadius.circular(10), + ), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + SizedBox( + height: 45, + child: Row( + children: [ + _NewsTab(label: 'НОВОСТИ', selected: true, accent: accent), + _NewsTab(label: 'СОБЫТИЯ', accent: accent), + _NewsTab(label: 'ПАТЧИ', accent: accent), + ], + ), + ), + const Divider(height: 1, color: _border), + Expanded(child: _content()), + Container( + height: 40, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: const BoxDecoration( + border: Border(top: BorderSide(color: _border)), + ), + child: Row( + children: [ + const Text( + 'MOONWELL.GG', + style: TextStyle( + fontFamily: 'JetBrains Mono', + color: _muted, + fontSize: 9, + letterSpacing: 1.5, + ), + ), + const Spacer(), + Text( + 'ВСЕ →', + style: TextStyle( + fontFamily: 'JetBrains Mono', + color: accent, + fontSize: 9, + letterSpacing: 1.5, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _content() => switch (state) { + MwNewsListState.loading => const Center( + child: CircularProgressIndicator(strokeWidth: 2), + ), + MwNewsListState.empty => const Center( + child: Text('Новостей пока нет', style: TextStyle(color: _muted)), + ), + MwNewsListState.failure => Center( + child: Text( + error ?? 'Не удалось загрузить новости', + style: const TextStyle(color: Color(0xFFD76A78)), + ), + ), + MwNewsListState.populated => ListView.separated( + padding: const EdgeInsets.only(left: 16, right: 4), + itemCount: items.length, + separatorBuilder: (_, _) => const Divider(height: 1, color: _border), + itemBuilder: (_, index) => MwNewsItem(item: items[index]), + ), + }; +} + +class _NewsTab extends StatelessWidget { + const _NewsTab({ + required this.label, + required this.accent, + this.selected = false, + }); + final String label; + final Color accent; + final bool selected; + @override + Widget build(BuildContext context) => Expanded( + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: selected ? accent.withAlpha(12) : Colors.transparent, + border: Border( + bottom: BorderSide(color: selected ? accent : Colors.transparent), + ), + ), + child: Text( + label, + style: TextStyle( + fontFamily: 'Cinzel', + color: selected ? _ivory : _muted, + fontSize: 9, + letterSpacing: 1.5, + ), + ), + ), + ); +} + +enum MwSyncPresentation { + install, + scanning, + downloading, + pausing, + paused, + verifying, + ready, + launched, + failure, +} + +class MwSyncActionArea extends StatelessWidget { + const MwSyncActionArea({ + super.key, + required this.state, + required this.status, + required this.onPrimary, + this.onPause, + this.progress, + this.currentPath, + this.error, + }); + final MwSyncPresentation state; + final String status; + final VoidCallback onPrimary; + final VoidCallback? onPause; + final double? progress; + final String? currentPath; + final String? error; + + @override + Widget build(BuildContext context) { + final accent = MoonWellDesignTokens.of(context).accent; + final ready = state == MwSyncPresentation.ready; + final active = { + MwSyncPresentation.scanning, + MwSyncPresentation.downloading, + MwSyncPresentation.pausing, + MwSyncPresentation.verifying, + }.contains(state); + final value = ready || state == MwSyncPresentation.launched + ? 1.0 + : (progress ?? 0); + final label = switch (state) { + MwSyncPresentation.install => 'УСТАНОВИТЬ', + MwSyncPresentation.paused => 'ПРОДОЛЖИТЬ', + MwSyncPresentation.ready => 'ИГРАТЬ', + MwSyncPresentation.launched => 'ИГРА ЗАПУЩЕНА', + MwSyncPresentation.failure => 'ПОВТОРИТЬ', + _ => 'ПАУЗА', + }; + return Container( + padding: const EdgeInsets.fromLTRB(32, 18, 32, 20), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0x0006091A), Color(0xF206091A)], + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: const Color(0x99070A18), + border: Border.all(color: _border), + borderRadius: BorderRadius.circular(6), + ), + child: const Row( + children: [ + _PulseDot(), + SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "ELUNE'S GRACE", + style: TextStyle( + fontFamily: 'Cinzel', + color: _ivory, + fontSize: 11, + letterSpacing: 1.5, + ), + ), + SizedBox(height: 3), + Text( + 'ONLINE · EU', + style: TextStyle( + fontFamily: 'JetBrains Mono', + color: _muted, + fontSize: 8, + letterSpacing: 1.1, + ), + ), + ], + ), + ], + ), + ), + const SizedBox(width: 24), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + Expanded( + child: Text( + error ?? status, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontFamily: 'JetBrains Mono', + color: error == null + ? accent + : const Color(0xFFD76A78), + fontSize: 10, + letterSpacing: 1.1, + ), + ), + ), + Text( + '${(value * 100).round()}%', + style: const TextStyle( + fontFamily: 'JetBrains Mono', + color: _ivory, + fontSize: 13, + ), + ), + ], + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular(2), + child: LinearProgressIndicator( + value: value, + minHeight: 4, + color: accent, + backgroundColor: accent.withAlpha(28), + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: Text( + currentPath ?? + (ready ? 'Все файлы целы' : 'Подготовка клиента'), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontFamily: 'JetBrains Mono', + color: _muted, + fontSize: 8, + ), + ), + ), + Text( + ready ? 'Последняя проверка: только что' : '', + style: const TextStyle( + fontFamily: 'JetBrains Mono', + color: _muted, + fontSize: 8, + ), + ), + ], + ), + ], + ), + ), + const SizedBox(width: 24), + SizedBox( + width: 220, + height: 64, + child: ElevatedButton.icon( + onPressed: active + ? onPause + : (state == MwSyncPresentation.launched ? null : onPrimary), + icon: Icon( + ready + ? Icons.play_arrow + : active + ? Icons.pause + : Icons.refresh, + size: 20, + ), + label: Text(label), + style: ElevatedButton.styleFrom( + backgroundColor: ready + ? const Color(0xFF60B17F) + : state == MwSyncPresentation.failure + ? const Color(0xFFD76A78) + : const Color(0xFF6D499E), + foregroundColor: ready ? const Color(0xFF07130F) : _ivory, + textStyle: const TextStyle( + fontFamily: 'Cinzel', + fontSize: 14, + letterSpacing: 2.4, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + ), + ), + ], + ), + ); + } +} + +class _PulseDot extends StatelessWidget { + const _PulseDot(); + @override + Widget build(BuildContext context) => Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + color: Color(0xFF6FC28A), + shape: BoxShape.circle, + boxShadow: [BoxShadow(color: Color(0xAA6FC28A), blurRadius: 8)], + ), + ); +} + +class MwLauncherStatusBar extends StatelessWidget { + const MwLauncherStatusBar({ + super.key, + required this.status, + this.fileCount, + this.speed, + this.eta, + this.buildHash, + }); + final String status; + final String? fileCount; + final String? speed; + 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, + ), + 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), + const Text('Лаунчер v1.0.0'), + ], + ), + ), + ); +} + +class MwAccountAffordance extends StatelessWidget { + const MwAccountAffordance({ + super.key, + required this.onLogout, + this.onSettings, + }); + final VoidCallback onLogout; + final VoidCallback? onSettings; + @override + Widget build(BuildContext context) => PopupMenuButton( + tooltip: 'Аккаунт', + onSelected: (value) => + value == 'settings' ? onSettings?.call() : onLogout(), + itemBuilder: (_) => const [ + PopupMenuItem(value: 'settings', child: Text('Настройки')), + PopupMenuItem(value: 'logout', child: Text('Выйти')), + ], + child: Container( + padding: const EdgeInsets.fromLTRB(8, 7, 15, 7), + decoration: BoxDecoration( + color: const Color(0x99070A18), + border: Border.all(color: _border), + borderRadius: BorderRadius.circular(99), + ), + child: const Row( + children: [ + CircleAvatar( + radius: 18, + backgroundColor: Color(0xFF5B3A8A), + child: Text( + 'Æ', + style: TextStyle(fontFamily: 'Cinzel', fontSize: 12), + ), + ), + SizedBox(width: 11), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Aranthel', + style: TextStyle( + fontFamily: 'Cinzel', + color: _ivory, + fontSize: 12, + letterSpacing: 1, + ), + ), + SizedBox(height: 2), + Text( + 'ХРАНИТЕЛЬ РОЩИ', + style: TextStyle( + fontFamily: 'JetBrains Mono', + color: _muted, + fontSize: 7, + letterSpacing: 1.1, + ), + ), + ], + ), + ], + ), + ), + ); +} + +class MwSettingsPanel extends StatelessWidget { + const MwSettingsPanel({ + super.key, + required this.installationPath, + required this.themeVariant, + required this.onChooseDirectory, + required this.onThemeChanged, + required this.onVerify, + required this.onLogout, + this.syncActive = false, + }); + final String? installationPath; + final MoonWellThemeVariant themeVariant; + final VoidCallback onChooseDirectory; + final ValueChanged onThemeChanged; + final VoidCallback onVerify; + final VoidCallback onLogout; + final bool syncActive; + @override + Widget build(BuildContext context) => MwPanel( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('Настройки', style: Theme.of(context).textTheme.headlineMedium), + const SizedBox(height: 20), + const Text('Папка установки'), + const SizedBox(height: 6), + MwStatusText( + text: installationPath ?? 'Папка не выбрана', + monospace: true, + ), + const SizedBox(height: 8), + MwButton( + label: 'Выбрать папку', + icon: Icons.folder_open_outlined, + onPressed: syncActive ? null : onChooseDirectory, + variant: MwButtonVariant.secondary, + ), + const SizedBox(height: 20), + SegmentedButton( + segments: const [ + ButtonSegment( + value: MoonWellThemeVariant.forest, + label: Text('Лес'), + ), + ButtonSegment( + value: MoonWellThemeVariant.temple, + label: Text('Храм'), + ), + ], + selected: {themeVariant}, + onSelectionChanged: (value) => onThemeChanged(value.single), + ), + const SizedBox(height: 20), + MwButton( + label: 'Проверить файлы', + onPressed: syncActive ? null : onVerify, + variant: MwButtonVariant.secondary, + ), + const SizedBox(height: 8), + MwButton( + label: 'Выйти', + onPressed: onLogout, + variant: MwButtonVariant.destructive, + ), + ], + ), + ); +} diff --git a/lib/app/design_system/mw_primitives.dart b/lib/app/design_system/mw_primitives.dart new file mode 100644 index 0000000..4b76d79 --- /dev/null +++ b/lib/app/design_system/mw_primitives.dart @@ -0,0 +1,448 @@ +import 'package:flutter/material.dart'; +import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart'; + +enum MwButtonVariant { primary, secondary, ghost, destructive } + +class MwButton extends StatelessWidget { + const MwButton({ + super.key, + required this.label, + required this.onPressed, + this.icon, + this.variant = MwButtonVariant.primary, + this.loading = false, + this.expand = false, + }); + + final String label; + final VoidCallback? onPressed; + final IconData? icon; + final MwButtonVariant variant; + final bool loading; + final bool expand; + + @override + Widget build(BuildContext context) { + final tokens = MoonWellDesignTokens.of(context); + final scheme = Theme.of(context).colorScheme; + final child = loading + ? const SizedBox.square( + dimension: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) ...[ + Icon(icon, size: 18), + const SizedBox(width: 8), + ], + Flexible(child: Text(label, overflow: TextOverflow.ellipsis)), + ], + ); + final callback = loading ? null : onPressed; + final button = switch (variant) { + MwButtonVariant.primary => ElevatedButton( + onPressed: callback, + child: child, + ), + MwButtonVariant.secondary => OutlinedButton( + onPressed: callback, + child: child, + ), + MwButtonVariant.ghost => TextButton(onPressed: callback, child: child), + MwButtonVariant.destructive => ElevatedButton( + onPressed: callback, + style: ElevatedButton.styleFrom( + backgroundColor: scheme.error, + foregroundColor: scheme.onError, + disabledBackgroundColor: scheme.error.withAlpha(50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(tokens.radiusMedium), + ), + ), + child: child, + ), + }; + return Semantics( + button: true, + enabled: callback != null, + child: SizedBox(width: expand ? double.infinity : null, child: button), + ); + } +} + +class MwIconButton extends StatelessWidget { + const MwIconButton({ + super.key, + required this.icon, + required this.tooltip, + required this.onPressed, + this.selected = false, + this.destructive = false, + }); + + final IconData icon; + final String tooltip; + final VoidCallback? onPressed; + final bool selected; + final bool destructive; + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + return Tooltip( + message: tooltip, + child: Semantics( + label: tooltip, + button: true, + enabled: onPressed != null, + selected: selected, + child: IconButton( + onPressed: onPressed, + isSelected: selected, + icon: Icon(icon), + color: destructive ? scheme.error : scheme.onSurfaceVariant, + style: IconButton.styleFrom(minimumSize: const Size(44, 44)), + ), + ), + ); + } +} + +class MwTextField extends StatelessWidget { + const MwTextField({ + super.key, + required this.label, + this.controller, + this.hint, + this.errorText, + this.prefixIcon, + this.enabled = true, + this.obscureText = false, + this.textInputAction, + this.onSubmitted, + }); + + final String label; + final TextEditingController? controller; + final String? hint; + final String? errorText; + final IconData? prefixIcon; + final bool enabled; + final bool obscureText; + final TextInputAction? textInputAction; + final ValueChanged? onSubmitted; + + @override + Widget build(BuildContext context) { + return TextField( + controller: controller, + enabled: enabled, + obscureText: obscureText, + textInputAction: textInputAction, + onSubmitted: onSubmitted, + decoration: InputDecoration( + labelText: label, + hintText: hint, + errorText: errorText, + prefixIcon: prefixIcon == null ? null : Icon(prefixIcon), + ), + ); + } +} + +class MwCheckbox extends StatelessWidget { + const MwCheckbox({ + super.key, + required this.label, + required this.value, + required this.onChanged, + }); + + final String label; + final bool value; + final ValueChanged? onChanged; + + @override + Widget build(BuildContext context) { + return CheckboxListTile( + value: value, + onChanged: onChanged, + title: Text(label), + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + dense: true, + ); + } +} + +class MwTabs extends StatelessWidget { + const MwTabs({ + super.key, + required this.labels, + required this.selectedIndex, + required this.onSelected, + this.disabledIndices = const {}, + }); + + final List labels; + final int selectedIndex; + final ValueChanged onSelected; + final Set disabledIndices; + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 4, + children: [ + for (var index = 0; index < labels.length; index++) + ChoiceChip( + label: Text(labels[index]), + selected: selectedIndex == index, + onSelected: disabledIndices.contains(index) + ? null + : (_) => onSelected(index), + ), + ], + ); + } +} + +enum MwBadgeTone { neutral, success, warning, error, comingSoon } + +class MwBadge extends StatelessWidget { + const MwBadge({ + super.key, + required this.label, + this.tone = MwBadgeTone.neutral, + }); + + final String label; + final MwBadgeTone tone; + + @override + Widget build(BuildContext context) { + final tokens = MoonWellDesignTokens.of(context); + final scheme = Theme.of(context).colorScheme; + final color = switch (tone) { + MwBadgeTone.neutral => scheme.onSurfaceVariant, + MwBadgeTone.success => tokens.success, + MwBadgeTone.warning => tokens.warning, + MwBadgeTone.error => scheme.error, + MwBadgeTone.comingSoon => tokens.accentMuted, + }; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: color.withAlpha(24), + border: Border.all(color: color.withAlpha(130)), + borderRadius: BorderRadius.circular(99), + ), + child: Text( + label, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: color, + fontWeight: FontWeight.w700, + ), + ), + ); + } +} + +enum MwPanelLevel { low, regular, high } + +class MwPanel extends StatelessWidget { + const MwPanel({ + super.key, + required this.child, + this.level = MwPanelLevel.regular, + this.padding = const EdgeInsets.all(24), + }); + + final Widget child; + final MwPanelLevel level; + final EdgeInsetsGeometry padding; + + @override + Widget build(BuildContext context) { + final tokens = MoonWellDesignTokens.of(context); + final color = switch (level) { + MwPanelLevel.low => tokens.surfaceLow, + MwPanelLevel.regular => tokens.surface, + MwPanelLevel.high => tokens.surfaceHigh, + }; + return DecoratedBox( + decoration: BoxDecoration( + color: color.withAlpha(238), + border: Border.fromBorderSide(tokens.border), + borderRadius: BorderRadius.circular(tokens.radiusLarge), + boxShadow: tokens.panelShadow, + ), + child: Padding(padding: padding, child: child), + ); + } +} + +enum MwProgressState { active, paused, completed, failure } + +class MwProgressBar extends StatelessWidget { + const MwProgressBar({ + super.key, + this.value, + this.label, + this.state = MwProgressState.active, + }); + + final double? value; + final String? label; + final MwProgressState state; + + @override + Widget build(BuildContext context) { + final tokens = MoonWellDesignTokens.of(context); + final scheme = Theme.of(context).colorScheme; + final color = switch (state) { + MwProgressState.active => scheme.primary, + MwProgressState.paused => tokens.warning, + MwProgressState.completed => tokens.success, + MwProgressState.failure => scheme.error, + }; + return Semantics( + label: label ?? 'Прогресс', + value: value == null ? null : '${(value! * 100).round()}%', + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (label != null) ...[ + Text(label!, style: Theme.of(context).textTheme.bodySmall), + const SizedBox(height: 8), + ], + ClipRRect( + borderRadius: BorderRadius.circular(99), + child: LinearProgressIndicator( + value: value, + color: color, + backgroundColor: color.withAlpha(35), + minHeight: 7, + ), + ), + ], + ), + ); + } +} + +enum MwStatusTone { operational, success, warning, error } + +class MwStatusText extends StatelessWidget { + const MwStatusText({ + super.key, + required this.text, + this.tone = MwStatusTone.operational, + this.monospace = false, + }); + + final String text; + final MwStatusTone tone; + final bool monospace; + + @override + Widget build(BuildContext context) { + final tokens = MoonWellDesignTokens.of(context); + final scheme = Theme.of(context).colorScheme; + final color = switch (tone) { + MwStatusTone.operational => scheme.onSurfaceVariant, + MwStatusTone.success => tokens.success, + MwStatusTone.warning => tokens.warning, + MwStatusTone.error => scheme.error, + }; + return Text( + text, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: color, + fontFamily: monospace ? tokens.monospaceFontFamily : null, + ), + ); + } +} + +enum MwStateKind { loading, empty, error } + +class MwStateView extends StatelessWidget { + const MwStateView({ + super.key, + required this.kind, + required this.title, + this.description, + this.onRetry, + }); + + final MwStateKind kind; + final String title; + final String? description; + final VoidCallback? onRetry; + + @override + Widget build(BuildContext context) { + final icon = switch (kind) { + MwStateKind.loading => null, + MwStateKind.empty => Icons.inbox_outlined, + MwStateKind.error => Icons.cloud_off_outlined, + }; + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon == null) + const SizedBox.square( + dimension: 28, + child: CircularProgressIndicator(strokeWidth: 2), + ) + else + Icon(icon, size: 34), + const SizedBox(height: 12), + Text(title, textAlign: TextAlign.center), + if (description != null) ...[ + const SizedBox(height: 6), + Text( + description!, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + if (onRetry != null) ...[ + const SizedBox(height: 16), + MwButton( + label: 'Повторить', + onPressed: onRetry, + variant: MwButtonVariant.secondary, + ), + ], + ], + ), + ), + ); + } +} + +class MwDisabledFeature extends StatelessWidget { + const MwDisabledFeature({ + super.key, + required this.message, + required this.child, + }); + + final String message; + final Widget child; + + @override + Widget build(BuildContext context) { + return Tooltip(message: message, child: child); + } +} diff --git a/lib/app/design_system/mw_shells.dart b/lib/app/design_system/mw_shells.dart new file mode 100644 index 0000000..3975010 --- /dev/null +++ b/lib/app/design_system/mw_shells.dart @@ -0,0 +1,447 @@ +import 'package:flutter/material.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'; + +const _ink = Color(0xFF06091A); +const _ivory = Color(0xFFECE4D0); + +class MwBrandMark extends StatelessWidget { + const MwBrandMark({super.key, this.width = 220, this.compact = false}); + + final double width; + final bool compact; + + @override + Widget build(BuildContext context) { + final accent = MoonWellDesignTokens.of(context).accent; + return Semantics( + image: true, + label: 'MoonWell', + child: SizedBox( + width: width, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _MoonMark(size: compact ? 28 : 40, color: accent), + const SizedBox(width: 12), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'MOONWELL', + style: TextStyle( + fontFamily: 'Cinzel', + color: _ivory, + fontSize: compact ? 15 : 22, + height: 1, + letterSpacing: compact ? 4 : 6, + ), + ), + if (!compact) ...[ + const SizedBox(height: 5), + Text( + 'LUNAR · LAUNCHER', + style: TextStyle( + fontFamily: 'JetBrains Mono', + color: accent, + fontSize: 9, + letterSpacing: 2.6, + ), + ), + ], + ], + ), + ], + ), + ), + ); + } +} + +class _MoonMark extends StatelessWidget { + const _MoonMark({required this.size, required this.color}); + + final double size; + final Color color; + + @override + Widget build(BuildContext context) { + return SizedBox.square( + dimension: size, + child: DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: color.withAlpha(90)), + boxShadow: [BoxShadow(color: color.withAlpha(70), blurRadius: 16)], + ), + child: Icon(Icons.nightlight_round, color: color, size: size * .72), + ), + ); + } +} + +class MwArtworkBackground extends StatelessWidget { + const MwArtworkBackground({ + super.key, + required this.child, + this.assetPackage, + }); + + final Widget child; + final String? assetPackage; + + @override + Widget build(BuildContext context) { + final accent = MoonWellDesignTokens.of(context).accent; + return Stack( + fit: StackFit.expand, + children: [ + const ColoredBox(color: _ink), + // Image.asset( + // 'assets/background.png', + // package: assetPackage, + // fit: BoxFit.cover, + // alignment: Alignment.center, + // errorBuilder: (_, _, _) => const SizedBox.shrink(), + // ), + DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + _ink.withAlpha(235), + _ink.withAlpha(85), + _ink.withAlpha(165), + ], + ), + ), + ), + DecoratedBox( + decoration: BoxDecoration( + gradient: RadialGradient( + center: const Alignment(.35, -.5), + radius: .8, + colors: [accent.withAlpha(42), Colors.transparent], + ), + ), + ), + child, + ], + ); + } +} + +class MwLoginShell extends StatelessWidget { + const MwLoginShell({ + super.key, + required this.form, + required this.onDrag, + required this.onDoubleTap, + required this.onMinimize, + required this.onMaximizeRestore, + required this.onClose, + this.assetPackage, + }); + + final Widget form; + final VoidCallback onDrag; + final VoidCallback onDoubleTap; + final VoidCallback onMinimize; + final VoidCallback onMaximizeRestore; + final VoidCallback onClose; + final String? assetPackage; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: MwArtworkBackground( + assetPackage: assetPackage, + child: Stack( + children: [ + Positioned.fill( + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(64, 80, 64, 58), + child: Align( + alignment: Alignment.bottomLeft, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500), + child: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '✦', + style: TextStyle( + color: Color(0xFF7FB8D4), + fontSize: 20, + ), + ), + SizedBox(height: 12), + Text( + '«Звёздный свет проведёт тебя сквозь самые ' + 'тёмные ночи — если ты готов идти туда, ' + 'куда он указывает.»', + style: TextStyle( + fontFamily: 'Cormorant Garamond', + fontStyle: FontStyle.italic, + color: Color(0xC6ECE4D0), + fontSize: 22, + height: 1.5, + ), + ), + SizedBox(height: 18), + Text( + '— МАЛФУРИОН ЯРОСТЬ БУРИ', + style: TextStyle( + fontFamily: 'JetBrains Mono', + color: Color(0x80ECE4D0), + fontSize: 10, + letterSpacing: 2, + ), + ), + ], + ), + ), + ), + ), + ), + Container( + width: 480, + decoration: const BoxDecoration( + color: Color(0xF20A0D1F), + border: Border( + left: BorderSide(color: Color(0x247FB8D4)), + ), + ), + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(56, 64, 56, 58), + child: form, + ), + ), + ], + ), + ), + Positioned( + left: 0, + right: 0, + top: 0, + child: MwWindowChrome( + onDrag: onDrag, + onDoubleTap: onDoubleTap, + onMinimize: onMinimize, + onMaximizeRestore: onMaximizeRestore, + onClose: onClose, + showMaximize: false, + ), + ), + const Positioned( + left: 0, + right: 0, + bottom: 0, + height: 26, + child: _LoginStatusBar(), + ), + ], + ), + ), + ); + } +} + +class _LoginStatusBar extends StatelessWidget { + const _LoginStatusBar(); + + @override + Widget build(BuildContext 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( + children: [ + Text('Версия лаунчера 1.0.0'), + SizedBox(width: 24), + Text('·'), + SizedBox(width: 24), + Text('Соединение защищено TLS'), + Spacer(), + Text('© 2026 MoonWell'), + ], + ), + ); + } +} + +class MwLauncherShell extends StatelessWidget { + const MwLauncherShell({ + super.key, + required this.news, + required this.syncActions, + required this.statusBar, + required this.account, + required this.onDrag, + required this.onDoubleTap, + required this.onMinimize, + required this.onMaximizeRestore, + required this.onClose, + this.assetPackage, + }); + + final Widget news; + final Widget syncActions; + final Widget statusBar; + final Widget account; + final VoidCallback onDrag; + final VoidCallback onDoubleTap; + final VoidCallback onMinimize; + final VoidCallback onMaximizeRestore; + final VoidCallback onClose; + final String? assetPackage; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: MwArtworkBackground( + assetPackage: assetPackage, + child: Stack( + children: [ + const Positioned( + left: 0, + top: 0, + bottom: 0, + width: 240, + child: MwGameRail(), + ), + Positioned( + left: 272, + top: 24, + child: const MwBrandMark(width: 245), + ), + Positioned(right: 28, top: 48, child: account), + const Positioned(left: 272, top: 160, child: _HeroCopy()), + Positioned( + right: 24, + top: 110, + bottom: 130, + width: 360, + child: news, + ), + Positioned( + left: 240, + right: 0, + bottom: 26, + height: 110, + child: syncActions, + ), + Positioned( + left: 240, + right: 0, + bottom: 0, + height: 26, + child: statusBar, + ), + Positioned( + left: 0, + right: 0, + top: 0, + child: MwWindowChrome( + onDrag: onDrag, + onDoubleTap: onDoubleTap, + onMinimize: onMinimize, + onMaximizeRestore: onMaximizeRestore, + onClose: onClose, + showMaximize: false, + ), + ), + ], + ), + ), + ); + } +} + +class _HeroCopy extends StatelessWidget { + const _HeroCopy(); + + @override + Widget build(BuildContext context) { + final accent = MoonWellDesignTokens.of(context).accent; + return SizedBox( + width: 520, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + border: Border.all(color: accent), + borderRadius: BorderRadius.circular(99), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: accent, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 10), + Text( + 'OPEN BETA', + style: TextStyle( + fontFamily: 'JetBrains Mono', + color: accent, + fontSize: 11, + letterSpacing: 2.4, + ), + ), + ], + ), + ), + const SizedBox(height: 18), + const Text( + 'Свет колодца', + style: TextStyle( + fontFamily: 'Cinzel', + color: _ivory, + fontSize: 50, + height: 1.05, + letterSpacing: 1.5, + ), + ), + Text( + 'зовёт тебя домой.', + style: TextStyle( + fontFamily: 'Cormorant Garamond', + fontStyle: FontStyle.italic, + color: accent, + fontSize: 54, + height: 1.05, + ), + ), + const SizedBox(height: 14), + const Text( + 'Wrath of the Lich King · приватный сервер', + style: TextStyle( + fontFamily: 'Cormorant Garamond', + fontStyle: FontStyle.italic, + color: Color(0xC6ECE4D0), + fontSize: 18, + ), + ), + ], + ), + ); + } +} diff --git a/lib/app/design_system/mw_window_chrome.dart b/lib/app/design_system/mw_window_chrome.dart new file mode 100644 index 0000000..0be90b3 --- /dev/null +++ b/lib/app/design_system/mw_window_chrome.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; + +class MwWindowChrome extends StatelessWidget { + const MwWindowChrome({ + super.key, + required this.onDrag, + required this.onDoubleTap, + required this.onMinimize, + required this.onMaximizeRestore, + required this.onClose, + this.showMaximize = true, + }); + + final VoidCallback onDrag; + final VoidCallback onDoubleTap; + final VoidCallback onMinimize; + final VoidCallback onMaximizeRestore; + final VoidCallback onClose; + final bool showMaximize; + + @override + Widget build(BuildContext context) { + return Container( + height: 40.0, + padding: .symmetric(horizontal: 12.0), + child: Row( + spacing: 4.0, + children: [ + const Spacer(), + _WindowButton( + icon: Icons.remove, + label: 'Свернуть', + onPressed: onMinimize, + ), + if (showMaximize) + _WindowButton( + icon: Icons.crop_square, + label: 'Развернуть или восстановить', + onPressed: onMaximizeRestore, + ), + _WindowButton( + icon: Icons.close, + label: 'Закрыть', + destructive: true, + onPressed: onClose, + ), + ], + ), + ); + } +} + +class _WindowButton extends StatelessWidget { + const _WindowButton({ + required this.icon, + required this.label, + required this.onPressed, + this.destructive = false, + }); + + final IconData icon; + final String label; + final VoidCallback onPressed; + final bool destructive; + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + return Tooltip( + message: label, + child: Semantics( + button: true, + label: label, + child: IconButton( + onPressed: onPressed, + icon: Icon(icon, size: 16), + color: scheme.onSurfaceVariant, + hoverColor: destructive + ? scheme.error.withAlpha(180) + : scheme.onSurface.withAlpha(20), + focusColor: scheme.primary.withAlpha(50), + style: IconButton.styleFrom( + tapTargetSize: .shrinkWrap, + visualDensity: VisualDensity( + horizontal: VisualDensity.minimumDensity, + vertical: VisualDensity.minimumDensity, + ), + fixedSize: const Size(36.0, 28.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4.0), + ), + ), + ), + ), + ); + } +} diff --git a/lib/app/home_screen/bloc/home_screen_bloc.dart b/lib/app/home_screen/bloc/home_screen_bloc.dart index a9f4231..b4c27fc 100644 --- a/lib/app/home_screen/bloc/home_screen_bloc.dart +++ b/lib/app/home_screen/bloc/home_screen_bloc.dart @@ -136,6 +136,7 @@ class HomeScreenBloc extends Bloc { errorMessage: null, processedFiles: 0, totalFiles: 0, + syncStage: null, ), ), ); @@ -306,6 +307,7 @@ class HomeScreenBloc extends Bloc { remoteBuildHash: event.status.remoteBuildHash, processedFiles: event.status.processedFiles, totalFiles: event.status.totalFiles, + syncStage: event.status.stage, ), ), ); @@ -330,6 +332,7 @@ class HomeScreenBloc extends Bloc { statusText: 'Обновление приостановлено.', currentPath: null, errorMessage: null, + syncStage: null, ), ), ); @@ -344,6 +347,7 @@ class HomeScreenBloc extends Bloc { statusText: 'Не удалось завершить обновление клиента.', currentPath: null, errorMessage: _formatError(event.error), + syncStage: null, ), ), ); diff --git a/lib/app/home_screen/bloc/home_screen_model.dart b/lib/app/home_screen/bloc/home_screen_model.dart index 638c5af..9a1a743 100644 --- a/lib/app/home_screen/bloc/home_screen_model.dart +++ b/lib/app/home_screen/bloc/home_screen_model.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:moonwell_launcher/features/downloader/domain/entities/download_progress.dart'; +import 'package:moonwell_launcher/features/launcher/domain/entities/client_sync_status.dart'; import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_news_item.dart'; enum HomeScreenPhase { @@ -31,6 +32,7 @@ final class HomeScreenModel { final List newsItems; final bool isLoadingNews; final String? newsErrorMessage; + final ClientSyncStage? syncStage; const HomeScreenModel({ required this.progress, @@ -48,6 +50,7 @@ final class HomeScreenModel { required this.newsItems, required this.isLoadingNews, required this.newsErrorMessage, + required this.syncStage, }); const HomeScreenModel.initial() @@ -65,7 +68,8 @@ final class HomeScreenModel { totalFiles = 0, newsItems = const [], isLoadingNews = true, - newsErrorMessage = null; + newsErrorMessage = null, + syncStage = null; bool get isBusy => phase == HomeScreenPhase.authenticating || @@ -91,6 +95,7 @@ final class HomeScreenModel { List? newsItems, bool? isLoadingNews, Object? newsErrorMessage = _sentinel, + Object? syncStage = _sentinel, }) { return HomeScreenModel( progress: progress ?? this.progress, @@ -120,6 +125,9 @@ final class HomeScreenModel { newsErrorMessage: newsErrorMessage == _sentinel ? this.newsErrorMessage : newsErrorMessage as String?, + syncStage: syncStage == _sentinel + ? this.syncStage + : syncStage as ClientSyncStage?, ); } } diff --git a/lib/app/home_screen/home_screen.dart b/lib/app/home_screen/home_screen.dart index 9a2c6fc..938d260 100644 --- a/lib/app/home_screen/home_screen.dart +++ b/lib/app/home_screen/home_screen.dart @@ -3,8 +3,6 @@ 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'; @@ -44,38 +42,7 @@ class HomeScreen extends StatelessWidget { ), ); }, - child: Scaffold( - body: Stack( - children: [ - Positioned.fill( - child: Image.asset('assets/background.png', fit: BoxFit.cover), - ), - Positioned.fill( - child: DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - MWColors.abyss.withAlpha(180), - MWColors.abyss.withAlpha(220), - MWColors.abyss.withAlpha(240), - ], - stops: const [0.0, 0.5, 1.0], - ), - ), - ), - ), - const Positioned.fill(child: HomeScreenContent()), - const Positioned( - top: 0, - left: 0, - right: 0, - child: WindowTitleBar(), - ), - ], - ), - ), + child: const HomeScreenContent(), ), ); } diff --git a/lib/app/home_screen/home_screen_content.dart b/lib/app/home_screen/home_screen_content.dart index dcfe617..069dbc9 100644 --- a/lib/app/home_screen/home_screen_content.dart +++ b/lib/app/home_screen/home_screen_content.dart @@ -1,618 +1,191 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:moonwell_launcher/app/design_system/mw_launcher_components.dart'; +import 'package:moonwell_launcher/app/design_system/mw_shells.dart'; import 'package:moonwell_launcher/app/home_screen/bloc/home_screen_bloc.dart'; import 'package:moonwell_launcher/app/home_screen/bloc/home_screen_model.dart'; -import 'package:moonwell_launcher/app/home_screen/widgets/gilded_progress_bar.dart'; -import 'package:moonwell_launcher/app/theme/mw_theme.dart'; -import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_news_item.dart'; -import 'package:pixelarticons/pixel.dart'; +import 'package:moonwell_launcher/app/mw_app.dart'; +import 'package:moonwell_launcher/features/launcher/domain/entities/client_sync_status.dart'; +import 'package:window_manager/window_manager.dart'; class HomeScreenContent extends StatelessWidget { const HomeScreenContent({super.key}); @override Widget build(BuildContext context) { - final state = context.watch().state; - final model = state.model; - final cs = Theme.of(context).colorScheme; - - return Padding( - padding: const EdgeInsets.only(top: 36), - child: Column( - children: [ - Expanded( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - flex: 3, - child: _NewsPanel(model: model, cs: cs), - ), - SizedBox( - width: 320, - child: _RightPanel(model: model, cs: cs), - ), - ], - ), - ), - _BottomBar(model: model, cs: cs), - ], - ), - ); - } -} - -class _NewsPanel extends StatelessWidget { - const _NewsPanel({required this.model, required this.cs}); - - final HomeScreenModel model; - final ColorScheme cs; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(left: 24, top: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Image.asset( - 'assets/logo.png', - height: 120, - filterQuality: FilterQuality.high, - ), - ), - ), - Text( - '\u041d\u043e\u0432\u043e\u0441\u0442\u0438', - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(color: cs.onSurface), - ), - const SizedBox(height: 12), - Expanded( - child: ShaderMask( - shaderCallback: (bounds) => LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Colors.white, Colors.white, Colors.white.withAlpha(0)], - stops: const [0.0, 0.85, 1.0], - ).createShader(bounds), - blendMode: BlendMode.dstIn, - child: _buildNewsBody(context), - ), - ), - ], + final model = context.watch().state.model; + return MwLauncherShell( + news: _buildNews(model), + syncActions: _buildSyncActions(context, model), + statusBar: _buildStatusBar(model), + account: MwAccountAffordance( + onSettings: () => _showSettings(context, model), + onLogout: () => + context.read().add(HomeScreenLogoutRequested()), ), + onDrag: windowManager.startDragging, + onDoubleTap: () => unawaited(_toggleMaximized()), + onMinimize: windowManager.minimize, + onMaximizeRestore: () => unawaited(_toggleMaximized()), + onClose: () => unawaited(windowManager.destroy()), ); } - Widget _buildNewsBody(BuildContext context) { - if (model.isLoadingNews) { - return Center( - child: SizedBox( - width: 26, - height: 26, - child: CircularProgressIndicator(strokeWidth: 2.2, color: cs.primary), - ), - ); - } - - if (model.newsItems.isEmpty && model.newsErrorMessage != null) { - return ListView( - padding: const EdgeInsets.only(right: 16, bottom: 24), - children: [ - _NewsStatusCard( - icon: Icons.cloud_off_rounded, - title: - '\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c ' - '\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c ' - '\u043d\u043e\u0432\u043e\u0441\u0442\u0438', - description: model.newsErrorMessage!, + Widget _buildNews(HomeScreenModel model) { + final state = model.isLoadingNews + ? MwNewsListState.loading + : model.newsItems.isNotEmpty + ? MwNewsListState.populated + : model.newsErrorMessage != null + ? MwNewsListState.failure + : MwNewsListState.empty; + return MwNewsList( + state: state, + error: model.newsErrorMessage, + items: [ + for (final item in model.newsItems) + MwNewsItemData( + title: item.title, + body: item.body, + createdAt: item.createdAt, + imageUrl: item.imageUrl, ), - ], - ); - } + ], + ); + } - if (model.newsItems.isEmpty) { - return ListView( - padding: const EdgeInsets.only(right: 16, bottom: 24), - children: const [ - _NewsStatusCard( - icon: Icons.inbox_outlined, - title: - '\u041d\u043e\u0432\u043e\u0441\u0442\u0435\u0439 ' - '\u043f\u043e\u043a\u0430 \u043d\u0435\u0442', - description: - '\u041a\u043e\u0433\u0434\u0430 \u0432 API \u043f\u043e\u044f' - '\u0432\u044f\u0442\u0441\u044f \u043f\u0443\u0431\u043b\u0438' - '\u043a\u0430\u0446\u0438\u0438, \u043e\u043d\u0438 ' - '\u043f\u043e\u044f\u0432\u044f\u0442\u0441\u044f \u0437\u0434' - '\u0435\u0441\u044c.', - ), - ], - ); - } - - return ListView.separated( - padding: const EdgeInsets.only(right: 16, bottom: 24), - itemCount: model.newsItems.length, - separatorBuilder: (_, _) => const SizedBox(height: 10), - itemBuilder: (context, index) { - return _NewsCard(item: model.newsItems[index]); + Widget _buildSyncActions(BuildContext context, HomeScreenModel model) { + final presentation = _syncPresentation(model); + final progress = model.progress.total > 0 + ? (model.progress.downloaded / model.progress.total).clamp(0.0, 1.0) + : null; + return MwSyncActionArea( + state: presentation, + status: model.statusText, + currentPath: model.currentPath, + error: model.errorMessage, + progress: progress, + onPrimary: () { + final bloc = context.read(); + if (model.phase == HomeScreenPhase.readyToPlay && model.canPlay) { + bloc.add(HomeScreenPlayRequested()); + } else { + bloc.add(HomeScreenSyncRequested()); + } }, - ); - } -} - -class _NewsStatusCard extends StatelessWidget { - const _NewsStatusCard({ - required this.icon, - required this.title, - required this.description, - }); - - final IconData icon; - final String title; - final String description; - - @override - Widget build(BuildContext context) { - final cs = Theme.of(context).colorScheme; - - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: cs.surface.withAlpha(140), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: cs.outline.withAlpha(100)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(icon, color: cs.tertiary), - const SizedBox(height: 12), - Text( - title, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: cs.tertiary, - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 8), - Text( - description, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: cs.onSurface.withAlpha(200), - height: 1.4, - ), - ), - ], - ), - ); - } -} - -class _NewsCard extends StatelessWidget { - const _NewsCard({required this.item}); - - final LauncherNewsItem item; - - @override - Widget build(BuildContext context) { - final cs = Theme.of(context).colorScheme; - - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: cs.surface.withAlpha(140), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: cs.outline.withAlpha(100)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - item.title, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: cs.tertiary, - fontWeight: FontWeight.w700, - ), - ), - if (item.createdAt != null) ...[ - const SizedBox(height: 6), - Text( - _formatCreatedAt(item.createdAt!), - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant), - ), - ], - if (item.imageUrl != null) ...[ - const SizedBox(height: 12), - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: AspectRatio( - aspectRatio: 16 / 7, - child: Image.network( - item.imageUrl!, - fit: BoxFit.cover, - errorBuilder: (_, _, _) => Container( - color: cs.surface.withAlpha(120), - alignment: Alignment.center, - child: Icon( - Icons.broken_image_outlined, - color: cs.onSurfaceVariant, - ), - ), - ), - ), - ), - ], - const SizedBox(height: 8), - Text( - item.body, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: cs.onSurface.withAlpha(200), - height: 1.4, - ), - ), - ], - ), + onPause: model.isSyncing + ? () => context.read().add(HomeScreenPauseRequested()) + : null, ); } - String _formatCreatedAt(DateTime value) { - final localValue = value.toLocal(); - final day = localValue.day.toString().padLeft(2, '0'); - final month = localValue.month.toString().padLeft(2, '0'); - final year = localValue.year.toString(); - return '$day.$month.$year'; - } -} - -class _RightPanel extends StatelessWidget { - const _RightPanel({required this.model, required this.cs}); - - final HomeScreenModel model; - final ColorScheme cs; - - @override - Widget build(BuildContext context) { - final deco = Theme.of(context).extension()!; - - return Container( - margin: const EdgeInsets.only(right: 20, top: 8, bottom: 12), - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: MWColors.abyss.withAlpha(200), - borderRadius: BorderRadius.circular(20), - border: Border.all(color: cs.outline.withAlpha(80)), - boxShadow: deco.cardGlow, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Align( - alignment: Alignment.centerRight, - child: TextButton.icon( - onPressed: () => context.read().add( - HomeScreenLogoutRequested(), - ), - icon: const Icon(Icons.logout_rounded, size: 16), - label: const Text('\u0412\u044b\u0439\u0442\u0438'), - style: TextButton.styleFrom( - foregroundColor: cs.onSurfaceVariant, - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 8, - ), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - ), - ), - const SizedBox(height: 12), - const Spacer(), - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => context.read().add( - HomeScreenOutputDirRequested(), - ), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), - decoration: BoxDecoration( - color: cs.surface.withAlpha(100), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: cs.outline.withAlpha(80)), - ), - child: Row( - children: [ - Icon(Pixel.folder, size: 18, color: cs.onSurfaceVariant), - const SizedBox(width: 10), - Expanded( - child: Text( - model.outputPath?.toFilePath() ?? - '\u0412\u044b\u0431\u0440\u0430\u0442\u044c ' - '\u043f\u0430\u043f\u043a\u0443', - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: cs.onSurfaceVariant, - fontSize: 13, - ), - ), - ), - Icon( - Icons.edit, - size: 14, - color: cs.onSurfaceVariant.withAlpha(120), - ), - ], - ), - ), - ), - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: cs.surface.withAlpha(80), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: cs.outline.withAlpha(60)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - model.statusText, - style: TextStyle( - color: cs.onSurface.withAlpha(180), - fontSize: 13, - ), - ), - if (model.currentPath != null) ...[ - const SizedBox(height: 6), - Text( - model.currentPath!, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: cs.onSurfaceVariant, fontSize: 11), - ), - ], - if (model.totalFiles > 0) ...[ - const SizedBox(height: 6), - Text( - '\u0424\u0430\u0439\u043b\u044b: ' - '${model.processedFiles}/${model.totalFiles}', - style: TextStyle(color: cs.onSurfaceVariant, fontSize: 11), - ), - ], - if (model.errorMessage != null) ...[ - const SizedBox(height: 8), - Text( - model.errorMessage!, - style: TextStyle(color: cs.error, fontSize: 12), - ), - ], - ], - ), - ), - const Spacer(), - if (model.remoteBuildHash != null || model.localBuildHash != null) - Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Text( - 'Build: ${_shortHash(model.localBuildHash)} / ' - '${_shortHash(model.remoteBuildHash)}', - textAlign: TextAlign.center, - style: TextStyle(color: cs.onSurfaceVariant, fontSize: 10), - ), - ), - ], - ), - ); - } - - String _shortHash(String? hash) { - if (hash == null || hash.isEmpty) { - return '-'; + MwSyncPresentation _syncPresentation(HomeScreenModel model) { + if (model.phase == HomeScreenPhase.paused) { + return MwSyncPresentation.paused; } - - if (hash.length <= 12) { - return hash; + if (model.phase == HomeScreenPhase.failure) { + return MwSyncPresentation.failure; } - - return '${hash.substring(0, 8)}...${hash.substring(hash.length - 4)}'; + if (model.phase == HomeScreenPhase.readyToPlay) { + return model.statusText.startsWith('Игра запущена') + ? MwSyncPresentation.launched + : MwSyncPresentation.ready; + } + if (model.phase != HomeScreenPhase.syncing) { + return MwSyncPresentation.install; + } + return switch (model.syncStage) { + ClientSyncStage.downloadingFiles => MwSyncPresentation.downloading, + ClientSyncStage.verifyingInstallation => MwSyncPresentation.verifying, + ClientSyncStage.completed => MwSyncPresentation.ready, + _ => MwSyncPresentation.scanning, + }; } -} -class _BottomBar extends StatelessWidget { - const _BottomBar({required this.model, required this.cs}); + Widget _buildStatusBar(HomeScreenModel model) { + return MwLauncherStatusBar( + status: model.outputPath?.toFilePath() ?? 'Папка установки не выбрана', + fileCount: model.totalFiles > 0 + ? '${model.processedFiles}/${model.totalFiles}' + : null, + speed: model.progress.speed > 0 + ? _formatSpeed(model.progress.speed) + : null, + eta: model.progress.eta > Duration.zero + ? _formatDuration(model.progress.eta) + : null, + buildHash: _shortHash(model.localBuildHash ?? model.remoteBuildHash), + ); + } - final HomeScreenModel model; - final ColorScheme cs; - - @override - Widget build(BuildContext context) { - final progressValue = model.progress.total == 0 - ? 0.0 - : model.progress.downloaded / model.progress.total; - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [MWColors.abyss.withAlpha(0), MWColors.abyss.withAlpha(230)], + Future _showSettings( + BuildContext context, + HomeScreenModel model, + ) async { + final bloc = context.read(); + final themeController = LauncherThemeScope.of(context); + await showDialog( + context: context, + builder: (dialogContext) => Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 460, maxHeight: 620), + child: AnimatedBuilder( + animation: themeController, + builder: (context, _) => SingleChildScrollView( + child: MwSettingsPanel( + installationPath: model.outputPath?.toFilePath(), + themeVariant: themeController.variant, + syncActive: model.isSyncing, + onChooseDirectory: () { + Navigator.of(dialogContext).pop(); + bloc.add(HomeScreenOutputDirRequested()); + }, + onThemeChanged: (variant) { + unawaited(themeController.setVariant(variant)); + }, + onVerify: () { + Navigator.of(dialogContext).pop(); + bloc.add(HomeScreenSyncRequested()); + }, + onLogout: () { + Navigator.of(dialogContext).pop(); + bloc.add(HomeScreenLogoutRequested()); + }, + ), + ), + ), ), ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (model.isSyncing) ...[ - Row( - children: [ - Expanded(child: GildedProgressBar(value: progressValue)), - const SizedBox(width: 12), - Text( - '${(progressValue * 100).clamp(0, 100).toStringAsFixed(1)}%', - style: TextStyle( - color: cs.onSurface, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(width: 12), - Text( - _formatSpeed(model.progress.speed), - style: TextStyle(color: cs.onSurfaceVariant, fontSize: 11), - ), - ], - ), - const SizedBox(height: 8), - ], - Row( - children: [ - SizedBox( - height: 44, - child: ElevatedButton.icon( - icon: Icon(_resolvePrimaryIcon(model), size: 20), - label: Text(_resolvePrimaryLabel(model)), - onPressed: model.isBusy - ? null - : () => _onPrimaryAction(context, model), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 28), - ), - ), - ), - if (model.canPlay || model.isSyncing) ...[ - const SizedBox(width: 8), - SizedBox( - height: 44, - child: OutlinedButton.icon( - icon: Icon(_resolveSecondaryIcon(model), size: 18), - label: Text(_resolveSecondaryLabel(model)), - onPressed: () => _onSecondaryAction(context, model), - ), - ), - ], - const SizedBox(width: 16), - Expanded( - child: Container( - height: 38, - padding: const EdgeInsets.symmetric(horizontal: 14), - decoration: BoxDecoration( - color: cs.surface.withAlpha(100), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: cs.outline.withAlpha(60)), - ), - alignment: Alignment.centerLeft, - child: Text( - model.currentPath ?? model.statusText, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: cs.onSurface.withAlpha(160), - fontSize: 12, - ), - ), - ), - ), - ], - ), - ], - ), ); } - void _onPrimaryAction(BuildContext context, HomeScreenModel model) { - final bloc = context.read(); - - if (model.canPlay) { - bloc.add(HomeScreenPlayRequested()); - return; + Future _toggleMaximized() async { + if (await windowManager.isMaximized()) { + await windowManager.unmaximize(); + } else { + await windowManager.maximize(); } - - bloc.add(HomeScreenSyncRequested()); - } - - void _onSecondaryAction(BuildContext context, HomeScreenModel model) { - final bloc = context.read(); - - if (model.isSyncing) { - bloc.add(HomeScreenPauseRequested()); - return; - } - - if (model.canPlay) { - bloc.add(HomeScreenSyncRequested()); - return; - } - - bloc.add(HomeScreenOutputDirRequested()); - } - - String _resolvePrimaryLabel(HomeScreenModel model) { - if (model.isSyncing) { - return '\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435...'; - } - - if (model.canPlay) { - return '\u0418\u0433\u0440\u0430\u0442\u044c'; - } - - return model.hasClientExecutable - ? '\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c' - : '\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c'; - } - - IconData _resolvePrimaryIcon(HomeScreenModel model) { - if (model.canPlay) { - return Icons.play_arrow_rounded; - } - - return Icons.download_rounded; - } - - String _resolveSecondaryLabel(HomeScreenModel model) { - if (model.isSyncing) { - return '\u041f\u0430\u0443\u0437\u0430'; - } - - if (model.canPlay) { - return '\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c'; - } - - return '\u041f\u0430\u043f\u043a\u0430'; - } - - IconData _resolveSecondaryIcon(HomeScreenModel model) { - if (model.isSyncing) { - return Pixel.pause; - } - - if (model.canPlay) { - return Icons.refresh_rounded; - } - - return Pixel.folder; } String _formatSpeed(int bytesPerSecond) { - if (bytesPerSecond <= 0) { - return '-'; + const megabyte = 1024 * 1024; + if (bytesPerSecond >= megabyte) { + return '${(bytesPerSecond / megabyte).toStringAsFixed(1)} MB/s'; } + return '${(bytesPerSecond / 1024).toStringAsFixed(0)} KB/s'; + } - if (bytesPerSecond >= 1024 * 1024) { - return '${(bytesPerSecond / (1024 * 1024)).toStringAsFixed(2)} MB/s'; - } + String _formatDuration(Duration duration) { + final hours = duration.inHours.toString().padLeft(2, '0'); + final minutes = (duration.inMinutes % 60).toString().padLeft(2, '0'); + final seconds = (duration.inSeconds % 60).toString().padLeft(2, '0'); + return '$hours:$minutes:$seconds'; + } - if (bytesPerSecond >= 1024) { - return '${(bytesPerSecond / 1024).toStringAsFixed(1)} KB/s'; - } - - return '$bytesPerSecond B/s'; + String? _shortHash(String? hash) { + if (hash == null || hash.isEmpty) return null; + return hash.length <= 8 ? hash : hash.substring(0, 8); } } diff --git a/lib/app/home_screen/widgets/gilded_progress_bar.dart b/lib/app/home_screen/widgets/gilded_progress_bar.dart deleted file mode 100644 index d5c0d0c..0000000 --- a/lib/app/home_screen/widgets/gilded_progress_bar.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:moonwell_launcher/app/theme/mw_theme.dart'; - -class GildedProgressBar extends StatelessWidget { - const GildedProgressBar({super.key, required this.value}); - final double value; - - @override - Widget build(BuildContext context) { - final cs = Theme.of(context).colorScheme; - final deco = Theme.of(context).extension()!; - - 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: [ - // Subtle top highlight - Positioned( - top: 0.0, - height: 6.0, - left: 0.0, - right: 0.0, - child: ColoredBox(color: Colors.white.withAlpha(10)), - ), - Positioned.fill( - child: FractionallySizedBox( - widthFactor: value.clamp(0.0, 1.0), - alignment: Alignment.centerLeft, - child: DecoratedBox( - decoration: BoxDecoration( - gradient: (deco.goldBevel as LinearGradient), - ), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/app/login_screen/login_screen.dart b/lib/app/login_screen/login_screen.dart index 8c8e9e6..3047890 100644 --- a/lib/app/login_screen/login_screen.dart +++ b/lib/app/login_screen/login_screen.dart @@ -1,10 +1,13 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:moonwell_launcher/app/design_system/mw_authentication.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/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}); @@ -76,114 +79,27 @@ class _LoginScreenState extends State { @override Widget build(BuildContext context) { - final cs = Theme.of(context).colorScheme; - - return Scaffold( - body: Stack( - children: [ - Positioned.fill( - child: Image.asset('assets/background.png', fit: BoxFit.cover), - ), - Positioned.fill( - child: DecoratedBox( - decoration: BoxDecoration( - gradient: RadialGradient( - center: Alignment.center, - radius: 1.0, - colors: [ - MWColors.abyss.withAlpha(200), - MWColors.abyss.withAlpha(240), - ], - ), - ), - ), - ), - const Positioned(top: 0, left: 0, right: 0, child: WindowTitleBar()), - Center( - child: SizedBox( - width: 340, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset( - 'assets/logo.png', - height: 160, - filterQuality: FilterQuality.high, - ), - const SizedBox(height: 32), - Container( - padding: const EdgeInsets.all(28), - decoration: BoxDecoration( - color: MWColors.abyss.withAlpha(210), - borderRadius: BorderRadius.circular(20), - border: Border.all(color: cs.outline.withAlpha(80)), - boxShadow: [ - BoxShadow( - color: MWColors.primary.withAlpha(30), - blurRadius: 40, - spreadRadius: 2, - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TextField( - controller: _usernameController, - enabled: !_isLoading, - decoration: const InputDecoration( - labelText: 'Логин', - prefixIcon: Icon(Icons.person_outline), - ), - textInputAction: TextInputAction.next, - ), - const SizedBox(height: 16), - TextField( - controller: _passwordController, - enabled: !_isLoading, - obscureText: true, - onSubmitted: (_) => _login(), - decoration: const InputDecoration( - labelText: 'Пароль', - prefixIcon: Icon(Icons.lock_outline), - ), - textInputAction: TextInputAction.done, - ), - const SizedBox(height: 24), - SizedBox( - height: 48, - child: ElevatedButton( - onPressed: _isLoading ? null : _login, - child: _isLoading - ? SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: cs.onPrimary, - ), - ) - : const Text('Войти'), - ), - ), - if (_error != null) ...[ - const SizedBox(height: 16), - Text( - _error!, - textAlign: TextAlign.center, - style: TextStyle(color: cs.error, fontSize: 13), - ), - ], - ], - ), - ), - ], - ), - ), - ), - ], + return MwLoginShell( + form: MwAuthenticationForm( + usernameController: _usernameController, + passwordController: _passwordController, + loading: _isLoading, + error: _error, + onSubmit: _login, ), + onDrag: windowManager.startDragging, + onDoubleTap: () => unawaited(_toggleMaximized()), + onMinimize: windowManager.minimize, + onMaximizeRestore: () => unawaited(_toggleMaximized()), + onClose: () => unawaited(windowManager.destroy()), ); } + + Future _toggleMaximized() async { + if (await windowManager.isMaximized()) { + await windowManager.unmaximize(); + } else { + await windowManager.maximize(); + } + } } diff --git a/lib/app/mw_app.dart b/lib/app/mw_app.dart index 0f9f351..139dd26 100644 --- a/lib/app/mw_app.dart +++ b/lib/app/mw_app.dart @@ -1,25 +1,68 @@ 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/login_screen/login_screen.dart'; -import 'package:moonwell_launcher/app/theme/mw_theme.dart'; +import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart'; import 'package:moonwell_launcher/features/launcher/application/restore_launcher_session_use_case.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'; -class MoonWellApp extends StatelessWidget { +class MoonWellApp extends StatefulWidget { const MoonWellApp({super.key}); + @override + State createState() => _MoonWellAppState(); +} + +class _MoonWellAppState extends State { + late final LauncherThemeController _themeController; + + @override + void initState() { + super.initState(); + _themeController = LauncherThemeController(getIt()); + _themeController.load(); + } + + @override + void dispose() { + _themeController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return MaterialApp( - title: 'MoonWell', - home: const _LauncherBootstrapScreen(), - theme: moonWellTheme(), + return AnimatedBuilder( + animation: _themeController, + builder: (context, _) => LauncherThemeScope( + controller: _themeController, + child: MaterialApp( + title: 'MoonWell', + debugShowCheckedModeBanner: false, + home: const _LauncherBootstrapScreen(), + theme: MoonWellTheme.create(_themeController.variant), + ), + ), ); } } +class LauncherThemeScope extends InheritedNotifier { + const LauncherThemeScope({ + super.key, + required LauncherThemeController controller, + required super.child, + }) : super(notifier: controller); + + static LauncherThemeController of(BuildContext context) { + final scope = context + .dependOnInheritedWidgetOfExactType(); + assert(scope != null, 'LauncherThemeScope is missing'); + return scope!.notifier!; + } +} + class _LauncherBootstrapScreen extends StatefulWidget { const _LauncherBootstrapScreen(); @@ -48,7 +91,6 @@ class _LauncherBootstrapScreenState extends State<_LauncherBootstrapScreen> { if (snapshot.connectionState != ConnectionState.done) { return const _LauncherBootstrapLoadingScreen(); } - final result = snapshot.data ?? const RestoreLauncherSessionResult.unauthenticated(); @@ -58,7 +100,6 @@ class _LauncherBootstrapScreenState extends State<_LauncherBootstrapScreen> { manifest: result.manifest!, ); } - return const LoginScreen(); }, ); @@ -70,50 +111,21 @@ class _LauncherBootstrapLoadingScreen extends StatelessWidget { @override Widget build(BuildContext context) { - final cs = Theme.of(context).colorScheme; - - return Scaffold( - body: Stack( - children: [ - Positioned.fill( - child: Image.asset('assets/background.png', fit: BoxFit.cover), - ), - Positioned.fill( - child: DecoratedBox( - decoration: BoxDecoration( - gradient: RadialGradient( - center: Alignment.center, - radius: 1.0, - colors: [ - MWColors.abyss.withAlpha(200), - MWColors.abyss.withAlpha(240), - ], - ), + return const Scaffold( + body: MwArtworkBackground( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + MwBrandMark(width: 220), + SizedBox(height: 24), + SizedBox.square( + dimension: 28, + child: CircularProgressIndicator(strokeWidth: 2), ), - ), + ], ), - Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset( - 'assets/logo.png', - height: 160, - filterQuality: FilterQuality.high, - ), - const SizedBox(height: 24), - SizedBox( - width: 28, - height: 28, - child: CircularProgressIndicator( - strokeWidth: 2.5, - color: cs.primary, - ), - ), - ], - ), - ), - ], + ), ), ); } diff --git a/lib/app/theme/moonwell_design_system.dart b/lib/app/theme/moonwell_design_system.dart new file mode 100644 index 0000000..e69f910 --- /dev/null +++ b/lib/app/theme/moonwell_design_system.dart @@ -0,0 +1,308 @@ +import 'package:flutter/material.dart'; +import 'package:moonwell_launcher/core/moonwell_theme_variant.dart'; +import 'package:moonwell_launcher/features/preferences/domain/repositories/preferences_repository.dart'; + +export 'package:moonwell_launcher/core/moonwell_theme_variant.dart'; + +@immutable +class MoonWellDesignTokens extends ThemeExtension { + const MoonWellDesignTokens({ + required this.background, + required this.surfaceLow, + required this.surface, + required this.surfaceHigh, + required this.accent, + required this.accentMuted, + required this.success, + required this.warning, + required this.border, + required this.focusRing, + required this.spacing, + required this.radiusSmall, + required this.radiusMedium, + required this.radiusLarge, + required this.panelShadow, + required this.motionFast, + required this.motionStandard, + required this.brandFontFamily, + required this.monospaceFontFamily, + }); + + final Color background; + final Color surfaceLow; + final Color surface; + final Color surfaceHigh; + final Color accent; + final Color accentMuted; + final Color success; + final Color warning; + final BorderSide border; + final Color focusRing; + final double spacing; + final double radiusSmall; + final double radiusMedium; + final double radiusLarge; + final List panelShadow; + final Duration motionFast; + final Duration motionStandard; + final String brandFontFamily; + final String monospaceFontFamily; + + static MoonWellDesignTokens of(BuildContext context) => + Theme.of(context).extension()!; + + @override + MoonWellDesignTokens copyWith({ + Color? background, + Color? surfaceLow, + Color? surface, + Color? surfaceHigh, + Color? accent, + Color? accentMuted, + Color? success, + Color? warning, + BorderSide? border, + Color? focusRing, + double? spacing, + double? radiusSmall, + double? radiusMedium, + double? radiusLarge, + List? panelShadow, + Duration? motionFast, + Duration? motionStandard, + String? brandFontFamily, + String? monospaceFontFamily, + }) { + return MoonWellDesignTokens( + background: background ?? this.background, + surfaceLow: surfaceLow ?? this.surfaceLow, + surface: surface ?? this.surface, + surfaceHigh: surfaceHigh ?? this.surfaceHigh, + accent: accent ?? this.accent, + accentMuted: accentMuted ?? this.accentMuted, + success: success ?? this.success, + warning: warning ?? this.warning, + border: border ?? this.border, + focusRing: focusRing ?? this.focusRing, + spacing: spacing ?? this.spacing, + radiusSmall: radiusSmall ?? this.radiusSmall, + radiusMedium: radiusMedium ?? this.radiusMedium, + radiusLarge: radiusLarge ?? this.radiusLarge, + panelShadow: panelShadow ?? this.panelShadow, + motionFast: motionFast ?? this.motionFast, + motionStandard: motionStandard ?? this.motionStandard, + brandFontFamily: brandFontFamily ?? this.brandFontFamily, + monospaceFontFamily: monospaceFontFamily ?? this.monospaceFontFamily, + ); + } + + @override + MoonWellDesignTokens lerp(covariant MoonWellDesignTokens? other, double t) { + if (other == null) return this; + return MoonWellDesignTokens( + background: Color.lerp(background, other.background, t)!, + surfaceLow: Color.lerp(surfaceLow, other.surfaceLow, t)!, + surface: Color.lerp(surface, other.surface, t)!, + surfaceHigh: Color.lerp(surfaceHigh, other.surfaceHigh, t)!, + accent: Color.lerp(accent, other.accent, t)!, + accentMuted: Color.lerp(accentMuted, other.accentMuted, t)!, + success: Color.lerp(success, other.success, t)!, + warning: Color.lerp(warning, other.warning, t)!, + border: BorderSide.lerp(border, other.border, t), + focusRing: Color.lerp(focusRing, other.focusRing, t)!, + spacing: lerpDouble(spacing, other.spacing, t), + radiusSmall: lerpDouble(radiusSmall, other.radiusSmall, t), + radiusMedium: lerpDouble(radiusMedium, other.radiusMedium, t), + radiusLarge: lerpDouble(radiusLarge, other.radiusLarge, t), + panelShadow: t < .5 ? panelShadow : other.panelShadow, + motionFast: t < .5 ? motionFast : other.motionFast, + motionStandard: t < .5 ? motionStandard : other.motionStandard, + brandFontFamily: t < .5 ? brandFontFamily : other.brandFontFamily, + monospaceFontFamily: t < .5 + ? monospaceFontFamily + : other.monospaceFontFamily, + ); + } + + static double lerpDouble(double a, double b, double t) => a + (b - a) * t; +} + +abstract final class MoonWellTheme { + static ThemeData create(MoonWellThemeVariant variant, {String? fontPackage}) { + final isForest = variant == MoonWellThemeVariant.forest; + const background = Color(0xFF070A18); + const surfaceLow = Color(0xFF0A0D1F); + const surface = Color(0xFF10142A); + const surfaceHigh = Color(0xFF1A1F3A); + final accent = isForest ? const Color(0xFF7FB8D4) : const Color(0xFF8A5CC8); + const accentMuted = Color(0xFF5B3A8A); + const outline = Color(0xFF34455E); + const foreground = Color(0xFFECE4D0); + const error = Color(0xFFFF8A86); + const success = Color(0xFF70D7A5); + const warning = Color(0xFFFFC865); + + final scheme = ColorScheme.dark( + primary: accent, + onPrimary: background, + secondary: accentMuted, + onSecondary: foreground, + tertiary: const Color(0xFF91C7D0), + onTertiary: background, + error: error, + onError: background, + surface: surface, + onSurface: foreground, + outline: outline, + shadow: Colors.black, + scrim: Colors.black87, + ); + String font(String family) => + fontPackage == null ? family : 'packages/$fontPackage/$family'; + + final base = ThemeData( + useMaterial3: true, + colorScheme: scheme, + scaffoldBackgroundColor: background, + canvasColor: background, + fontFamily: font('Inter'), + focusColor: accent.withAlpha(60), + visualDensity: VisualDensity.standard, + ); + final radius = BorderRadius.circular(10); + final textTheme = base.textTheme + .apply(bodyColor: foreground, displayColor: foreground) + .copyWith( + displayLarge: base.textTheme.displayLarge?.copyWith( + fontFamily: font('Cormorant Garamond'), + fontWeight: FontWeight.w600, + ), + displayMedium: base.textTheme.displayMedium?.copyWith( + fontFamily: font('Cormorant Garamond'), + fontWeight: FontWeight.w600, + ), + headlineLarge: base.textTheme.headlineLarge?.copyWith( + fontFamily: font('Cormorant Garamond'), + fontWeight: FontWeight.w600, + ), + headlineMedium: base.textTheme.headlineMedium?.copyWith( + fontFamily: font('Cormorant Garamond'), + fontWeight: FontWeight.w600, + ), + titleLarge: base.textTheme.titleLarge?.copyWith( + fontFamily: font('Cormorant Garamond'), + fontWeight: FontWeight.w700, + ), + labelLarge: base.textTheme.labelLarge?.copyWith( + fontFamily: font('Inter'), + fontWeight: FontWeight.w700, + ), + ); + + return base.copyWith( + textTheme: textTheme, + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: surfaceHigh, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + enabledBorder: OutlineInputBorder( + borderRadius: radius, + borderSide: BorderSide(color: outline), + ), + focusedBorder: OutlineInputBorder( + borderRadius: radius, + borderSide: BorderSide(color: accent, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: radius, + borderSide: const BorderSide(color: error), + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + minimumSize: const Size(44, 44), + backgroundColor: accent, + foregroundColor: background, + disabledBackgroundColor: outline.withAlpha(90), + disabledForegroundColor: foreground.withAlpha(120), + shape: RoundedRectangleBorder(borderRadius: radius), + textStyle: TextStyle( + fontFamily: font('Inter'), + fontWeight: FontWeight.w700, + ), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + minimumSize: const Size(44, 44), + foregroundColor: foreground, + side: BorderSide(color: outline), + shape: RoundedRectangleBorder(borderRadius: radius), + ), + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + minimumSize: const Size(44, 44), + foregroundColor: accent, + ), + ), + checkboxTheme: CheckboxThemeData( + side: BorderSide(color: outline), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + ), + extensions: [ + MoonWellDesignTokens( + background: background, + surfaceLow: surfaceLow, + surface: surface, + surfaceHigh: surfaceHigh, + accent: accent, + accentMuted: accentMuted, + success: success, + warning: warning, + border: BorderSide(color: outline.withAlpha(190)), + focusRing: accent, + spacing: 8, + radiusSmall: 6, + radiusMedium: 10, + radiusLarge: 16, + panelShadow: const [ + BoxShadow( + color: Color(0x66000000), + blurRadius: 24, + offset: Offset(0, 12), + ), + ], + motionFast: const Duration(milliseconds: 120), + motionStandard: const Duration(milliseconds: 220), + brandFontFamily: font('Cinzel'), + monospaceFontFamily: font('JetBrains Mono'), + ), + ], + ); + } +} + +class LauncherThemeController extends ChangeNotifier { + LauncherThemeController(this._preferencesRepository); + + final PreferencesRepository _preferencesRepository; + MoonWellThemeVariant _variant = MoonWellThemeVariant.forest; + + MoonWellThemeVariant get variant => _variant; + + Future load() async { + _variant = await _preferencesRepository.getThemeVariant(); + notifyListeners(); + } + + Future setVariant(MoonWellThemeVariant variant) async { + if (_variant == variant) return; + _variant = variant; + notifyListeners(); + await _preferencesRepository.setThemeVariant(variant); + } +} diff --git a/lib/app/theme/mw_colors.dart b/lib/app/theme/mw_colors.dart deleted file mode 100644 index 82c7fba..0000000 --- a/lib/app/theme/mw_colors.dart +++ /dev/null @@ -1,22 +0,0 @@ -part of 'mw_theme.dart'; - -/// Core palette -class MWColors { - // Surface & background - static const Color abyss = Color(0xFF090F2B); // page bg - static const Color deepNavy = Color(0xFF111840); // surfaces - static const Color stormNavy = Color(0xFF1A2759); // elevated surfaces - - // Primary & secondary - static const Color primary = Color(0xFF5460A2); - static const Color secondary = Color(0xFF5F6BD2); - static const Color tertiary = Color(0xFF6494EB); - - // Lines & states - static const Color outline = Color(0xFF4B4E6E); - - // Semantic - static const Color success = Color(0xFF3DDC97); - static const Color warning = Color(0xFFF0B429); - static const Color error = Color(0xFFD86A8A); -} diff --git a/lib/app/theme/mw_decorations.dart b/lib/app/theme/mw_decorations.dart deleted file mode 100644 index d1d0dec..0000000 --- a/lib/app/theme/mw_decorations.dart +++ /dev/null @@ -1,40 +0,0 @@ -part of 'mw_theme.dart'; - -class MoonWellDecorations extends ThemeExtension { - final Gradient goldBevel; // for gilded headers/buttons - final Shadow textGlow; // subtle moonlight glow - final List cardGlow; - - const MoonWellDecorations({ - required this.goldBevel, - required this.textGlow, - required this.cardGlow, - }); - - @override - MoonWellDecorations copyWith({ - Gradient? goldBevel, - Shadow? textGlow, - List? cardGlow, - }) => MoonWellDecorations( - goldBevel: goldBevel ?? this.goldBevel, - textGlow: textGlow ?? this.textGlow, - cardGlow: cardGlow ?? this.cardGlow, - ); - - @override - ThemeExtension lerp( - ThemeExtension? other, - double t, - ) { - if (other is! MoonWellDecorations) return this; - return MoonWellDecorations( - goldBevel: Gradient.lerp(goldBevel, other.goldBevel, t)!, - textGlow: Shadow.lerp(textGlow, other.textGlow, t)!, - cardGlow: [ - for (int i = 0; i < cardGlow.length; i++) - BoxShadow.lerp(cardGlow[i], other.cardGlow[i], t)!, - ], - ); - } -} diff --git a/lib/app/theme/mw_theme.dart b/lib/app/theme/mw_theme.dart deleted file mode 100644 index 0a731be..0000000 --- a/lib/app/theme/mw_theme.dart +++ /dev/null @@ -1,220 +0,0 @@ -import 'package:flutter/material.dart'; - -part 'mw_colors.dart'; -part 'mw_decorations.dart'; - -ThemeData moonWellTheme() { - const cs = ColorScheme.dark( - brightness: Brightness.dark, - primary: Color(0xFF5460A2), - onPrimary: Color(0xFFDDE0FB), - - secondary: Color(0xFF5F6BD2), - onSecondary: Color(0xFFDDE0FB), - - tertiary: Color(0xFF6494EB), - onTertiary: Color(0xFF090F2B), - - error: Color(0xFFD86A8A), - onError: Color(0xFF090F2B), - - surface: Color(0xFF1A2759), - onSurface: Color(0xFFDDE0FB), - - outline: Color(0xFF4B4E6E), - shadow: Color(0xFF000000), - scrim: Color(0xCC000000), - ); - - final base = ThemeData( - useMaterial3: true, - colorScheme: cs, - scaffoldBackgroundColor: cs.surface, - canvasColor: cs.surface, - ); - - final text = base.textTheme - .apply( - fontFamily: 'Cinzel', - bodyColor: cs.onSurface, - displayColor: cs.onSurface, - ) - .copyWith( - displayLarge: base.textTheme.displayLarge?.copyWith( - letterSpacing: 0.5, - fontWeight: FontWeight.w700, - shadows: const [Shadow(blurRadius: 10, color: Color(0x336BA3FF))], - ), - headlineMedium: base.textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.w700, - ), - titleLarge: base.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w700, - ), - ); - - return base.copyWith( - textTheme: text, - appBarTheme: AppBarTheme( - backgroundColor: Colors.transparent, - surfaceTintColor: Colors.transparent, - elevation: 0, - scrolledUnderElevation: 0, - foregroundColor: cs.onSurface, - centerTitle: true, - titleTextStyle: text.titleLarge, - toolbarHeight: 64, - ), - - cardTheme: CardThemeData( - color: cs.surface.withAlpha(50), - elevation: 0, - margin: const EdgeInsets.all(12), - shape: RoundedRectangleBorder( - side: BorderSide(color: MWColors.outline.withAlpha(150)), - borderRadius: BorderRadius.circular(20), - ), - shadowColor: MWColors.tertiary.withAlpha(63), - ), - - elevatedButtonTheme: ElevatedButtonThemeData( - style: ButtonStyle( - padding: const WidgetStatePropertyAll( - EdgeInsets.symmetric(horizontal: 18, vertical: 14), - ), - shape: WidgetStatePropertyAll( - RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), - ), - textStyle: WidgetStatePropertyAll( - TextStyle(fontWeight: FontWeight.bold, fontFamily: 'Cinzel'), - ), - elevation: const WidgetStatePropertyAll(6), - shadowColor: WidgetStatePropertyAll(MWColors.tertiary.withAlpha(89)), - backgroundColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.disabled)) { - return MWColors.primary.withAlpha(115); - } - return cs.primary; - }), - foregroundColor: const WidgetStatePropertyAll(Color(0xFFDDE0FB)), - ), - ), - - textButtonTheme: TextButtonThemeData( - style: ButtonStyle( - foregroundColor: WidgetStatePropertyAll(cs.tertiary), - overlayColor: WidgetStatePropertyAll(cs.tertiary.withAlpha(10)), - ), - ), - - filledButtonTheme: FilledButtonThemeData( - style: ButtonStyle( - backgroundColor: WidgetStatePropertyAll(cs.tertiaryContainer), - foregroundColor: WidgetStatePropertyAll(cs.onTertiaryContainer), - shape: WidgetStatePropertyAll( - RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), - ), - ), - ), - - outlinedButtonTheme: OutlinedButtonThemeData( - style: ButtonStyle( - side: WidgetStatePropertyAll( - BorderSide(color: MWColors.outline.withAlpha(230)), - ), - shape: WidgetStatePropertyAll( - RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), - ), - foregroundColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.disabled)) { - return cs.primary.withAlpha(100); - } - return cs.primary; - }), - backgroundColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.disabled)) { - return MWColors.stormNavy.withAlpha(100); - } - return MWColors.stormNavy; - }), - ), - ), - - inputDecorationTheme: InputDecorationTheme( - filled: true, - fillColor: cs.surfaceContainerHighest, - hintStyle: TextStyle(color: cs.onSurfaceVariant.withAlpha(179)), - contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: BorderSide(color: MWColors.outline), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: BorderSide(color: cs.primary, width: 1.6), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: BorderSide(color: cs.error), - ), - prefixIconColor: cs.onSurfaceVariant, - suffixIconColor: cs.onSurfaceVariant, - ), - - chipTheme: base.chipTheme.copyWith( - backgroundColor: cs.surfaceContainerHighest, - side: BorderSide(color: MWColors.outline), - selectedColor: cs.primaryContainer, - labelStyle: TextStyle(color: cs.onSurface), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - ), - - sliderTheme: base.sliderTheme.copyWith( - activeTrackColor: cs.primary, - inactiveTrackColor: cs.primary.withAlpha(63), - thumbColor: cs.primary, - ), - - dividerTheme: DividerThemeData( - color: MWColors.outline, - thickness: 1, - space: 24, - ), - - bottomNavigationBarTheme: BottomNavigationBarThemeData( - backgroundColor: cs.surface, - selectedItemColor: cs.primary, - unselectedItemColor: cs.onSurface.withAlpha(153), - elevation: 8, - type: BottomNavigationBarType.fixed, - ), - - extensions: >[ - const MoonWellDecorations( - goldBevel: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Color(0xFF7A85CC), // highlight - Color(0xFF5460A2), // body - Color(0xFF3E4880), // edge - ], - stops: [0.0, 0.55, 1.0], - ), - textGlow: Shadow( - color: Color(0x446494EB), - blurRadius: 14, - offset: Offset(0, 0), - ), - cardGlow: [ - BoxShadow( - color: Color(0x33143666), // cool rim light - blurRadius: 28, - spreadRadius: 2, - offset: Offset(0, 8), - ), - ], - ), - ], - ); -} diff --git a/lib/app/widgets/window_title_bar.dart b/lib/app/widgets/window_title_bar.dart deleted file mode 100644 index 5009b53..0000000 --- a/lib/app/widgets/window_title_bar.dart +++ /dev/null @@ -1,77 +0,0 @@ -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 _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)), - ), - ); - } -} diff --git a/lib/core/moonwell_theme_variant.dart b/lib/core/moonwell_theme_variant.dart new file mode 100644 index 0000000..78d1f84 --- /dev/null +++ b/lib/core/moonwell_theme_variant.dart @@ -0,0 +1,11 @@ +enum MoonWellThemeVariant { + forest, + temple; + + static MoonWellThemeVariant fromStoredValue(String? value) { + return MoonWellThemeVariant.values.firstWhere( + (variant) => variant.name == value, + orElse: () => MoonWellThemeVariant.forest, + ); + } +} diff --git a/lib/features/preferences/data/repositories/local_preferences_repository.dart b/lib/features/preferences/data/repositories/local_preferences_repository.dart index 2ee49b7..5f87217 100644 --- a/lib/features/preferences/data/repositories/local_preferences_repository.dart +++ b/lib/features/preferences/data/repositories/local_preferences_repository.dart @@ -2,12 +2,14 @@ import 'dart:convert'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:injectable/injectable.dart'; +import 'package:moonwell_launcher/core/moonwell_theme_variant.dart'; import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_session.dart'; import 'package:moonwell_launcher/features/preferences/domain/repositories/preferences_repository.dart'; import 'package:shared_preferences/shared_preferences.dart'; const _outputDirKey = 'output_dir'; const _launcherSessionKey = 'launcher_session'; +const _themeVariantKey = 'theme_variant'; @LazySingleton(as: PreferencesRepository, env: ['flutter']) class LocalPreferencesRepository implements PreferencesRepository { @@ -73,6 +75,18 @@ class LocalPreferencesRepository implements PreferencesRepository { await _maybeClearLegacyLauncherSession(); } + @override + Future getThemeVariant() async { + return MoonWellThemeVariant.fromStoredValue( + _preferences.getString(_themeVariantKey), + ); + } + + @override + Future setThemeVariant(MoonWellThemeVariant variant) { + return _preferences.setString(_themeVariantKey, variant.name); + } + Future _maybeClearLegacyLauncherSession() async { if (_preferences.containsKey(_launcherSessionKey)) { await _preferences.remove(_launcherSessionKey); diff --git a/lib/features/preferences/domain/repositories/preferences_repository.dart b/lib/features/preferences/domain/repositories/preferences_repository.dart index 5996783..421e007 100644 --- a/lib/features/preferences/domain/repositories/preferences_repository.dart +++ b/lib/features/preferences/domain/repositories/preferences_repository.dart @@ -1,3 +1,4 @@ +import 'package:moonwell_launcher/core/moonwell_theme_variant.dart'; import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_session.dart'; abstract interface class PreferencesRepository { @@ -10,4 +11,8 @@ abstract interface class PreferencesRepository { Future setLauncherSession(LauncherSession session); Future clearLauncherSession(); + + Future getThemeVariant(); + + Future setThemeVariant(MoonWellThemeVariant variant); } diff --git a/lib/main.dart b/lib/main.dart index 0c1a022..55c7241 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,8 +14,9 @@ Future main() async { await configureDependencies(env: 'flutter'); final launcherUpdateService = getIt(); - WindowOptions windowOptions = WindowOptions( - size: Size(960, 540), + const windowOptions = WindowOptions( + size: Size(1280, 720), + minimumSize: Size(1280, 720), center: true, backgroundColor: Colors.transparent, skipTaskbar: false, diff --git a/macos/Podfile.lock b/macos/Podfile.lock index ce8c07f..87cd068 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -2,29 +2,12 @@ PODS: - auto_updater_macos (0.0.1): - FlutterMacOS - Sparkle - - file_picker (0.0.1): - - FlutterMacOS - - flutter_secure_storage_darwin (10.0.0): - - Flutter - - FlutterMacOS - FlutterMacOS (1.0.0) - - screen_retriever_macos (0.0.1): - - FlutterMacOS - - shared_preferences_foundation (0.0.1): - - Flutter - - FlutterMacOS - Sparkle (2.9.3) - - window_manager (0.5.0): - - FlutterMacOS DEPENDENCIES: - auto_updater_macos (from `Flutter/ephemeral/.symlinks/plugins/auto_updater_macos/macos`) - - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) - - flutter_secure_storage_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_darwin/darwin`) - FlutterMacOS (from `Flutter/ephemeral`) - - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) - - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) SPEC REPOS: trunk: @@ -33,28 +16,13 @@ SPEC REPOS: EXTERNAL SOURCES: auto_updater_macos: :path: Flutter/ephemeral/.symlinks/plugins/auto_updater_macos/macos - file_picker: - :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos - flutter_secure_storage_darwin: - :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_darwin/darwin FlutterMacOS: :path: Flutter/ephemeral - screen_retriever_macos: - :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos - shared_preferences_foundation: - :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin - window_manager: - :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 - file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a - flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 - screen_retriever_macos: c5508cc3c66ff0d4db650480cf0ab691e220d933 - shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb Sparkle: 39dbdf28637f6056fbf5ee6494bdbfae54f49f41 - window_manager: b729e31d38fb04905235df9ea896128991cad99e PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 1c4e992..a7911ce 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; F90EB2907BAACC032B0C41D7 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D18013F1C238316DA220246 /* Pods_RunnerTests.framework */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -88,6 +89,7 @@ 8ACA2734D27028484BB5A7C4 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; CAB0C10E5CA9D8568F3B0CBA /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -103,6 +105,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, 020359DC38F00FB8C06FA10B /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -164,6 +167,7 @@ 33CEB47122A05771004F2AC0 /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, @@ -230,6 +234,9 @@ productType = "com.apple.product-type.bundle.unit-test"; }; 33CC10EC2044A3C60003C045 /* Runner */ = { + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( @@ -255,6 +262,9 @@ /* Begin PBXProject section */ 33CC10E52044A3C60003C045 /* Project object */ = { + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; @@ -800,6 +810,18 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index ab69357..5ba58f9 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + + + + + + MoonWell Launcher + + + + + + + + + + +
+
+
+
+ MOONWELL · LAUNCHER +
+
+ Главный экран, состояния загрузки, экран входа — всё в одном холсте. +
+
+
+
+
+ 01 · Главная — WoW · готов играть +
+
+
+
+
+
+
+ Place key art here2560×1440 · ночные эльфы / Тельдрассил +
+
+
+
+ +
+
+ +
+
+ + + + +
+ MOONWELLLUNAR · LAUNCHER +
+
+
+
Æ
+
+ AranthelХранитель рощи · 47 ур. +
+
+
+
+ Open Beta +

+ Свет колодца
зовёт тебя домой. +

+

+ Wrath of the Lich King · приватный сервер +

+
+ +
+
+
+
+
+
ELUNE'S GRACE
+
Online · 1247 players · 38ms
+
+
+
+
+ Готов к запуску100% +
+
+
+
+
+
+ Версия клиента 1.4.2Все файлы целы +
+
+ Последняя проверка: только что +
+
+
+ +
+
+
+ Версия клиента 1.4.2·Realmlist login.moonwell.gg·Регион EUЛаунчер v1.4.2 +
+
+
+
+
+ 02 · Скачивание клиента +
+
+
+
+
+
+
+ Place key art here2560×1440 · ночные эльфы / Тельдрассил +
+
+
+
+ +
+
+ +
+
+ + + + +
+ MOONWELLLUNAR · LAUNCHER +
+
+
+
Æ
+
+ AranthelХранитель рощи · 47 ур. +
+
+
+
+ Open Beta +

+ Свет колодца
зовёт тебя домой. +

+

+ Wrath of the Lich King · приватный сервер +

+
+ +
+
+
+
+
+
ELUNE'S GRACE
+
Online · 1247 players · 38ms
+
+
+
+
+ Загрузка6% +
+
+
+
+
+
+ 3.22 / 48.80 GB4.95 MB/s +
+
Осталось ~2:37:11
+
+
+ +
+
+
+ Версия клиента 1.4.2·Realmlist login.moonwell.gg·Регион EUЛаунчер v1.4.2 +
+
+
+
+
+ 03 · Пауза +
+
+
+
+
+
+
+ Place key art here2560×1440 · ночные эльфы / Тельдрассил +
+
+
+
+ +
+
+ +
+
+ + + + +
+ MOONWELLLUNAR · LAUNCHER +
+
+
+
Æ
+
+ AranthelХранитель рощи · 47 ур. +
+
+
+
+ Open Beta +

+ Свет колодца
зовёт тебя домой. +

+

+ Wrath of the Lich King · приватный сервер +

+
+ +
+
+
+
+
+
ELUNE'S GRACE
+
Online · 1247 players · 38ms
+
+
+
+
+ Пауза6% +
+
+
+
+
+
+ 3.22 / 48.80 GB +
+
+ Возобнови, чтобы продолжить +
+
+
+ +
+
+
+ Версия клиента 1.4.2·Realmlist login.moonwell.gg·Регион EUЛаунчер v1.4.2 +
+
+
+
+
+ 04 · Проверка файлов +
+
+
+
+
+
+
+ Place key art here2560×1440 · ночные эльфы / Тельдрассил +
+
+
+
+ +
+
+ +
+
+ + + + +
+ MOONWELLLUNAR · LAUNCHER +
+
+
+
Æ
+
+ AranthelХранитель рощи · 47 ур. +
+
+
+
+ Open Beta +

+ Свет колодца
зовёт тебя домой. +

+

+ Wrath of the Lich King · приватный сервер +

+
+ +
+
+
+
+
+
ELUNE'S GRACE
+
Online · 1247 players · 38ms
+
+
+
+
+ Проверка файлов38% +
+
+
+
+
+
+ 18 432 / 48 121 файловSHA-256 +
+
~1 мин
+
+
+ +
+
+
+ Версия клиента 1.4.2·Realmlist login.moonwell.gg·Регион EUЛаунчер v1.4.2 +
+
+
+
+
+ 05 · Обновление +
+
+
+
+
+
+
+ Place key art here2560×1440 · ночные эльфы / Тельдрассил +
+
+
+
+ +
+
+ +
+
+ + + + +
+ MOONWELLLUNAR · LAUNCHER +
+
+
+
Æ
+
+ AranthelХранитель рощи · 47 ур. +
+
+
+
+ Open Beta +

+ Свет колодца
зовёт тебя домой. +

+

+ Wrath of the Lich King · приватный сервер +

+
+ +
+
+
+
+
+
ELUNE'S GRACE
+
Online · 1247 players · 38ms
+
+
+
+
+ Обновление клиента72% +
+
+
+
+
+
+ 1.4.1 → 1.4.2180 MB / 250 MB +
+
~30 сек
+
+
+ +
+
+
+ Версия клиента 1.4.2·Realmlist login.moonwell.gg·Регион EUЛаунчер v1.4.2 +
+
+
+
+
+ 06 · MoonWell Karts (closed alpha) +
+
+
+
+
+
+
+ Place key art here2560×1440 · ночные эльфы / Тельдрассил +
+
+
+
+ +
+
+ +
+
+ + + + +
+ MOONWELLLUNAR · LAUNCHER +
+
+
+
Æ
+
+ AranthelХранитель рощи · 47 ур. +
+
+
+
+ Closed Alpha +

+ Гонки под
лунным светом. +

+

Аркадные гонки в нашей вселенной

+
+ +
+
+
+
+
+
ELUNE'S GRACE
+
Online · 1247 players · 38ms
+
+
+
+
+ Готов к запуску100% +
+
+
+
+
+
+ Версия клиента 1.4.2Все файлы целы +
+
+ Последняя проверка: только что +
+
+
+ +
+
+
+ Версия клиента 1.4.2·Realmlist login.moonwell.gg·Регион EUЛаунчер v1.4.2 +
+
+
+
+
+ 07 · Project Verge (анонс) +
+
+
+
+
+
+
+ Place key art here2560×1440 · ночные эльфы / Тельдрассил +
+
+
+
+ +
+
+ +
+
+ + + + +
+ MOONWELLLUNAR · LAUNCHER +
+
+
+
Æ
+
+ AranthelХранитель рощи · 47 ур. +
+
+
+
+ Анонс +

+ За гранью
привычного мира. +

+

ARPG-песочница · собственный движок

+
+ +
+
+
+
+
+
ELUNE'S GRACE
+
Online · 1247 players · 38ms
+
+
+
+
+ Готов к запуску100% +
+
+
+
+
+
+ Версия клиента 1.4.2Все файлы целы +
+
+ Последняя проверка: только что +
+
+
+ +
+
+
+ Версия клиента 1.4.2·Realmlist login.moonwell.gg·Регион EUЛаунчер v1.4.2 +
+
+
+
+
+ 08 · Вход +
+
+
+
+
+
+
+
+
+ +
+
+ +
+ Версия лаунчера 1.4.2·Соединение защищено TLS© 2026 MoonWell +
+
+
+
+
+ 09 · Регистрация +
+
+
+
+
+
+
+
+
+ +
+
+ +
+ Версия лаунчера 1.4.2·Соединение защищено TLS© 2026 MoonWell +
+
+
+
+
+
+ + + + + + + + + diff --git a/pubspec.lock b/pubspec.lock index 92798d1..f41efaa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -540,10 +540,10 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.19" material_color_utilities: dependency: transitive description: @@ -556,10 +556,10 @@ packages: 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: @@ -905,10 +905,10 @@ packages: dependency: transitive description: name: test_api - sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.11" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c5ee14f..157023c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,7 +19,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ^3.9.0 + sdk: ">=3.10.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -74,6 +74,10 @@ flutter: assets: - assets/background.png - assets/logo.png + - assets/fonts/Cinzel-OFL.txt + - assets/fonts/CormorantGaramond-OFL.txt + - assets/fonts/Inter-OFL.txt + - assets/fonts/JetBrainsMono-OFL.txt # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see @@ -86,3 +90,12 @@ flutter: - family: Cinzel fonts: - asset: assets/fonts/Cinzel.ttf + - family: Cormorant Garamond + fonts: + - asset: assets/fonts/CormorantGaramond.ttf + - family: Inter + fonts: + - asset: assets/fonts/Inter.ttf + - family: JetBrains Mono + fonts: + - asset: assets/fonts/JetBrainsMono.ttf diff --git a/test/app/home_screen/bloc/home_screen_bloc_test.dart b/test/app/home_screen/bloc/home_screen_bloc_test.dart index be0aef9..f4ae433 100644 --- a/test/app/home_screen/bloc/home_screen_bloc_test.dart +++ b/test/app/home_screen/bloc/home_screen_bloc_test.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; +import 'package:moonwell_launcher/core/moonwell_theme_variant.dart'; import 'package:moonwell_launcher/app/home_screen/bloc/home_screen_bloc.dart'; import 'package:moonwell_launcher/features/launcher/application/client_sync_use_case.dart'; import 'package:moonwell_launcher/features/launcher/data/game_installation_service.dart'; @@ -99,9 +100,16 @@ class _FakePreferencesRepository implements PreferencesRepository { @override Future getOutputDir() async => null; + @override + Future getThemeVariant() async => + MoonWellThemeVariant.forest; + @override Future setLauncherSession(LauncherSession session) async {} @override Future setOutputDir(Uri uri) async {} + + @override + Future setThemeVariant(MoonWellThemeVariant variant) async {} } diff --git a/test/features/launcher/application/restore_launcher_session_use_case_test.dart b/test/features/launcher/application/restore_launcher_session_use_case_test.dart index 490e85c..0c35c43 100644 --- a/test/features/launcher/application/restore_launcher_session_use_case_test.dart +++ b/test/features/launcher/application/restore_launcher_session_use_case_test.dart @@ -1,4 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:moonwell_launcher/core/moonwell_theme_variant.dart'; import 'package:moonwell_launcher/features/launcher/application/restore_launcher_session_use_case.dart'; import 'package:moonwell_launcher/features/launcher/data/launcher_api_client.dart'; import 'package:moonwell_launcher/features/launcher/domain/entities/client_manifest.dart'; @@ -107,6 +108,10 @@ class _FakePreferencesRepository implements PreferencesRepository { @override Future getOutputDir() async => null; + @override + Future getThemeVariant() async => + MoonWellThemeVariant.forest; + @override Future setLauncherSession(LauncherSession session) async { this.session = session; @@ -114,4 +119,7 @@ class _FakePreferencesRepository implements PreferencesRepository { @override Future setOutputDir(Uri uri) async {} + + @override + Future setThemeVariant(MoonWellThemeVariant variant) async {} } diff --git a/widgetbook/.fvmrc b/widgetbook/.fvmrc new file mode 100644 index 0000000..c300356 --- /dev/null +++ b/widgetbook/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "stable" +} \ No newline at end of file diff --git a/widgetbook/.gitignore b/widgetbook/.gitignore new file mode 100644 index 0000000..ac83f76 --- /dev/null +++ b/widgetbook/.gitignore @@ -0,0 +1,48 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/widgetbook/.metadata b/widgetbook/.metadata new file mode 100644 index 0000000..86426ac --- /dev/null +++ b/widgetbook/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "44a626f4f0027bc38a46dc68aed5964b05a83c18" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18 + base_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18 + - platform: macos + create_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18 + base_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18 + - platform: windows + create_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18 + base_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/widgetbook/.vscode/settings.json b/widgetbook/.vscode/settings.json new file mode 100644 index 0000000..1395495 --- /dev/null +++ b/widgetbook/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dart.flutterSdkPath": ".fvm/versions/stable" +} \ No newline at end of file diff --git a/widgetbook/README.md b/widgetbook/README.md new file mode 100644 index 0000000..fec6057 --- /dev/null +++ b/widgetbook/README.md @@ -0,0 +1,11 @@ +# MoonWell Widgetbook + +Standalone development catalog for MoonWell Launcher components. + +```powershell +fvm flutter pub get +fvm dart run build_runner build +fvm flutter run -d windows +``` + +Use cases must follow [`../docs/widgetbook.md`](../docs/widgetbook.md). diff --git a/widgetbook/analysis_options.yaml b/widgetbook/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/widgetbook/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/widgetbook/devtools_options.yaml b/widgetbook/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/widgetbook/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/widgetbook/lib/main.dart b/widgetbook/lib/main.dart new file mode 100644 index 0000000..d335a2b --- /dev/null +++ b/widgetbook/lib/main.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +import 'main.directories.g.dart'; + +void main() => runApp(const MoonWellWidgetbook()); + +@widgetbook.App() +class MoonWellWidgetbook extends StatelessWidget { + const MoonWellWidgetbook({super.key}); + + @override + Widget build(BuildContext context) { + return Widgetbook.material( + initialRoute: '/?path=compositions/mwlaunchershell/ready', + directories: directories, + addons: [ + MaterialThemeAddon( + themes: [ + WidgetbookTheme( + name: 'Forest', + data: MoonWellTheme.create( + MoonWellThemeVariant.forest, + fontPackage: 'moonwell_launcher', + ), + ), + WidgetbookTheme( + name: 'Temple', + data: MoonWellTheme.create( + MoonWellThemeVariant.temple, + fontPackage: 'moonwell_launcher', + ), + ), + ], + ), + ViewportAddon(const [ + ViewportData( + name: 'Minimum desktop · 1024×640', + width: 1024, + height: 640, + pixelRatio: 1, + platform: TargetPlatform.windows, + ), + ViewportData( + name: 'Target desktop · 1280×720', + width: 1280, + height: 720, + pixelRatio: 1, + platform: TargetPlatform.windows, + ), + ViewportData( + name: 'Large desktop · 1600×900', + width: 1600, + height: 900, + pixelRatio: 1, + platform: TargetPlatform.windows, + ), + ]), + TextScaleAddon(min: 1, max: 2, divisions: 4, initialScale: 1), + ], + ); + } +} diff --git a/widgetbook/lib/main.directories.g.dart b/widgetbook/lib/main.directories.g.dart new file mode 100644 index 0000000..91d5f15 --- /dev/null +++ b/widgetbook/lib/main.directories.g.dart @@ -0,0 +1,413 @@ +// dart format width=80 +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_import, prefer_relative_imports, directives_ordering + +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// AppGenerator +// ************************************************************************** + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:widgetbook/widgetbook.dart' as _widgetbook; +import 'package:widgetbook_workspace/stories/authentication_stories.dart' + as _widgetbook_workspace_stories_authentication_stories; +import 'package:widgetbook_workspace/stories/foundations_stories.dart' + as _widgetbook_workspace_stories_foundations_stories; +import 'package:widgetbook_workspace/stories/launcher_stories.dart' + as _widgetbook_workspace_stories_launcher_stories; +import 'package:widgetbook_workspace/stories/primitives_stories.dart' + as _widgetbook_workspace_stories_primitives_stories; +import 'package:widgetbook_workspace/stories/shell_stories.dart' + as _widgetbook_workspace_stories_shell_stories; +import 'package:widgetbook_workspace/stories/window_chrome_stories.dart' + as _widgetbook_workspace_stories_window_chrome_stories; + +final directories = <_widgetbook.WidgetbookNode>[ + _widgetbook.WidgetbookCategory( + name: 'Authentication', + children: [ + _widgetbook.WidgetbookComponent( + name: 'MwAuthenticationForm', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'api_error', + builder: _widgetbook_workspace_stories_authentication_stories + .buildMwAuthenticationFormApiErrorUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'default', + builder: _widgetbook_workspace_stories_authentication_stories + .buildMwAuthenticationFormUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'loading', + builder: _widgetbook_workspace_stories_authentication_stories + .buildMwAuthenticationFormLoadingUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'long_russian_copy', + builder: _widgetbook_workspace_stories_authentication_stories + .buildMwAuthenticationFormLongRussianCopyUseCase, + ), + ], + ), + ], + ), + _widgetbook.WidgetbookCategory( + name: 'Compositions', + children: [ + _widgetbook.WidgetbookComponent( + name: 'MwLauncherShell', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'downloading', + builder: _widgetbook_workspace_stories_shell_stories + .buildMwLauncherShellDownloadingUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'empty_news', + builder: _widgetbook_workspace_stories_shell_stories + .buildMwLauncherShellEmptyNewsUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'failure', + builder: _widgetbook_workspace_stories_shell_stories + .buildMwLauncherShellFailureUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'paused', + builder: _widgetbook_workspace_stories_shell_stories + .buildMwLauncherShellPausedUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'ready', + builder: _widgetbook_workspace_stories_shell_stories + .buildMwLauncherShellReadyUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'verifying', + builder: _widgetbook_workspace_stories_shell_stories + .buildMwLauncherShellVerifyingUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'MwLoginShell', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'api_error', + builder: _widgetbook_workspace_stories_shell_stories + .buildMwLoginShellApiErrorUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'default', + builder: _widgetbook_workspace_stories_shell_stories + .buildMwLoginShellUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'restored_session_loading', + builder: _widgetbook_workspace_stories_shell_stories + .buildMwLoginShellRestoredSessionLoadingUseCase, + ), + ], + ), + ], + ), + _widgetbook.WidgetbookCategory( + name: 'Foundations', + children: [ + _widgetbook.WidgetbookComponent( + name: 'ArtworkSlot', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'default', + builder: _widgetbook_workspace_stories_foundations_stories + .buildArtworkSlotUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'FoundationsGallery', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'default', + builder: _widgetbook_workspace_stories_foundations_stories + .buildFoundationsGalleryUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'MwArtworkBackground', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'default', + builder: _widgetbook_workspace_stories_shell_stories + .buildMwArtworkBackgroundUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'MwBrandMark', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'default', + builder: _widgetbook_workspace_stories_shell_stories + .buildMwBrandMarkUseCase, + ), + ], + ), + ], + ), + _widgetbook.WidgetbookCategory( + name: 'Launcher', + children: [ + _widgetbook.WidgetbookComponent( + name: 'MwAccountAffordance', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'default', + builder: _widgetbook_workspace_stories_launcher_stories + .buildMwAccountAffordanceUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'MwGameRail', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'compact', + builder: _widgetbook_workspace_stories_launcher_stories + .buildMwGameRailCompactUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'full', + builder: _widgetbook_workspace_stories_launcher_stories + .buildMwGameRailFullUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'MwGameTile', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'interactive', + builder: _widgetbook_workspace_stories_launcher_stories + .buildMwGameTileInteractiveUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'MwLauncherStatusBar', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'metadata', + builder: _widgetbook_workspace_stories_launcher_stories + .buildMwLauncherStatusBarMetadataUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'MwNewsItem', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'image_failure', + builder: _widgetbook_workspace_stories_launcher_stories + .buildMwNewsItemImageFailureUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'text_only', + builder: _widgetbook_workspace_stories_launcher_stories + .buildMwNewsItemTextOnlyUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'MwNewsList', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'empty', + builder: _widgetbook_workspace_stories_launcher_stories + .buildMwNewsListEmptyUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'failure', + builder: _widgetbook_workspace_stories_launcher_stories + .buildMwNewsListFailureUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'loading', + builder: _widgetbook_workspace_stories_launcher_stories + .buildMwNewsListLoadingUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'populated', + builder: _widgetbook_workspace_stories_launcher_stories + .buildMwNewsListPopulatedUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'MwSettingsPanel', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'interactive', + builder: _widgetbook_workspace_stories_launcher_stories + .buildMwSettingsPanelInteractiveUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'MwSyncActionArea', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'interactive', + builder: _widgetbook_workspace_stories_launcher_stories + .buildMwSyncActionAreaInteractiveUseCase, + ), + ], + ), + ], + ), + _widgetbook.WidgetbookCategory( + name: 'Primitives', + children: [ + _widgetbook.WidgetbookComponent( + name: 'MwBadge', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'interactive', + builder: _widgetbook_workspace_stories_primitives_stories + .buildMwBadgeInteractiveUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'MwButton', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'disabled', + builder: _widgetbook_workspace_stories_primitives_stories + .buildMwButtonDisabledUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'interactive', + builder: _widgetbook_workspace_stories_primitives_stories + .buildMwButtonInteractiveUseCase, + ), + _widgetbook.WidgetbookUseCase( + name: 'loading', + builder: _widgetbook_workspace_stories_primitives_stories + .buildMwButtonLoadingUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'MwCheckbox', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'interactive', + builder: _widgetbook_workspace_stories_primitives_stories + .buildMwCheckboxInteractiveUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'MwDisabledFeature', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'default', + builder: _widgetbook_workspace_stories_primitives_stories + .buildMwDisabledFeatureUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'MwIconButton', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'interactive', + builder: _widgetbook_workspace_stories_primitives_stories + .buildMwIconButtonInteractiveUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'MwPanel', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'interactive', + builder: _widgetbook_workspace_stories_primitives_stories + .buildMwPanelInteractiveUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'MwProgressBar', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'interactive', + builder: _widgetbook_workspace_stories_primitives_stories + .buildMwProgressBarInteractiveUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'MwStateView', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'interactive', + builder: _widgetbook_workspace_stories_primitives_stories + .buildMwStateViewInteractiveUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'MwStatusText', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'interactive', + builder: _widgetbook_workspace_stories_primitives_stories + .buildMwStatusTextInteractiveUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'MwTabs', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'interactive', + builder: _widgetbook_workspace_stories_primitives_stories + .buildMwTabsInteractiveUseCase, + ), + ], + ), + _widgetbook.WidgetbookComponent( + name: 'MwTextField', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'interactive', + builder: _widgetbook_workspace_stories_primitives_stories + .buildMwTextFieldInteractiveUseCase, + ), + ], + ), + ], + ), + _widgetbook.WidgetbookCategory( + name: 'Window Chrome', + children: [ + _widgetbook.WidgetbookComponent( + name: 'MwWindowChrome', + useCases: [ + _widgetbook.WidgetbookUseCase( + name: 'interactive', + builder: _widgetbook_workspace_stories_window_chrome_stories + .buildMwWindowChromeInteractiveUseCase, + ), + ], + ), + ], + ), +]; diff --git a/widgetbook/lib/stories/authentication_stories.dart b/widgetbook/lib/stories/authentication_stories.dart new file mode 100644 index 0000000..aaa04a4 --- /dev/null +++ b/widgetbook/lib/stories/authentication_stories.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:moonwell_launcher/app/design_system/mw_authentication.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +@widgetbook.UseCase( + name: 'default', + type: MwAuthenticationForm, + path: '[Authentication]', +) +Widget buildMwAuthenticationFormUseCase(BuildContext context) { + return SizedBox( + width: 420, + child: MwAuthenticationForm( + usernameController: TextEditingController( + text: context.knobs.string(label: 'username', initialValue: ''), + ), + passwordController: TextEditingController( + text: context.knobs.string(label: 'password', initialValue: ''), + ), + onSubmit: () => debugPrint('Authentication form submitted'), + ), + ); +} + +@widgetbook.UseCase( + name: 'loading', + type: MwAuthenticationForm, + path: '[Authentication]', +) +Widget buildMwAuthenticationFormLoadingUseCase(BuildContext context) { + return SizedBox( + width: 420, + child: MwAuthenticationForm( + usernameController: TextEditingController(text: 'moonwalker'), + passwordController: TextEditingController(text: 'password'), + loading: true, + onSubmit: () => debugPrint('Loading authentication form submitted'), + ), + ); +} + +@widgetbook.UseCase( + name: 'api_error', + type: MwAuthenticationForm, + path: '[Authentication]', +) +Widget buildMwAuthenticationFormApiErrorUseCase(BuildContext context) { + return SizedBox( + width: 420, + child: MwAuthenticationForm( + usernameController: TextEditingController(text: 'moonwalker'), + passwordController: TextEditingController(), + error: context.knobs.string( + label: 'error', + initialValue: 'Неверный логин или пароль.', + ), + onSubmit: () => debugPrint('Errored authentication form submitted'), + ), + ); +} + +@widgetbook.UseCase( + name: 'long_russian_copy', + type: MwAuthenticationForm, + path: '[Authentication]', +) +Widget buildMwAuthenticationFormLongRussianCopyUseCase(BuildContext context) { + return SizedBox( + width: 420, + child: MwAuthenticationForm( + usernameController: TextEditingController(), + passwordController: TextEditingController(), + error: + 'Не удалось связаться с сервером авторизации. Проверьте подключение ' + 'к интернету и повторите попытку через несколько минут.', + onSubmit: () => debugPrint('Long-copy authentication form submitted'), + ), + ); +} diff --git a/widgetbook/lib/stories/foundations_stories.dart b/widgetbook/lib/stories/foundations_stories.dart new file mode 100644 index 0000000..656c8a6 --- /dev/null +++ b/widgetbook/lib/stories/foundations_stories.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:moonwell_launcher/app/design_system/mw_primitives.dart'; +import 'package:moonwell_launcher/app/design_system/mw_shells.dart'; +import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +class FoundationsGallery extends StatelessWidget { + const FoundationsGallery({super.key}); + + @override + Widget build(BuildContext context) { + final tokens = MoonWellDesignTokens.of(context); + final colors = { + 'Background': tokens.background, + 'Surface low': tokens.surfaceLow, + 'Surface': tokens.surface, + 'Surface high': tokens.surfaceHigh, + 'Accent': tokens.accent, + 'Accent muted': tokens.accentMuted, + 'Success': tokens.success, + 'Warning': tokens.warning, + 'Error': Theme.of(context).colorScheme.error, + }; + return SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Semantic colors', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 12), + Wrap( + spacing: 12, + runSpacing: 12, + children: [ + for (final entry in colors.entries) + SizedBox( + width: 144, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 72, + decoration: BoxDecoration( + color: entry.value, + border: Border.all(color: Colors.white24), + borderRadius: BorderRadius.circular(8), + ), + ), + const SizedBox(height: 6), + Text(entry.key), + MwStatusText( + text: + '#${entry.value.toARGB32().toRadixString(16).substring(2).toUpperCase()}', + monospace: true, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 32), + Text('Typography', style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 12), + Text( + 'MoonWell', + style: TextStyle( + fontFamily: tokens.brandFontFamily, + fontSize: 32, + letterSpacing: 2, + ), + ), + Text( + 'Лунный колодец зовёт героев Нордскола', + style: Theme.of(context).textTheme.headlineMedium, + ), + const Text('Интерфейс, формы, навигация и рабочие статусы'), + MwStatusText( + text: 'build a93fd1c2 · 12.4 MB/s · 00:01:42', + monospace: true, + ), + const SizedBox(height: 32), + Text( + 'Surfaces & radii', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 12), + const Row( + children: [ + Expanded( + child: MwPanel(level: MwPanelLevel.low, child: Text('Low')), + ), + SizedBox(width: 12), + Expanded(child: MwPanel(child: Text('Regular'))), + SizedBox(width: 12), + Expanded( + child: MwPanel(level: MwPanelLevel.high, child: Text('High')), + ), + ], + ), + ], + ), + ); + } +} + +@widgetbook.UseCase( + name: 'default', + type: FoundationsGallery, + path: '[Foundations]', +) +Widget buildFoundationsGalleryUseCase(BuildContext context) { + return const FoundationsGallery(); +} + +class ArtworkSlot extends StatelessWidget { + const ArtworkSlot({super.key}); + + @override + Widget build(BuildContext context) { + return Stack( + fit: StackFit.expand, + children: [ + Image.asset( + 'assets/background.png', + package: 'moonwell_launcher', + fit: BoxFit.cover, + ), + const DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Color(0x22000000), Color(0xEE000000)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + ), + const Center(child: MwBrandMark(width: 240)), + ], + ); + } +} + +@widgetbook.UseCase(name: 'default', type: ArtworkSlot, path: '[Foundations]') +Widget buildArtworkSlotUseCase(BuildContext context) { + return const ArtworkSlot(); +} diff --git a/widgetbook/lib/stories/launcher_stories.dart b/widgetbook/lib/stories/launcher_stories.dart new file mode 100644 index 0000000..3226c64 --- /dev/null +++ b/widgetbook/lib/stories/launcher_stories.dart @@ -0,0 +1,230 @@ +import 'package:flutter/material.dart'; +import 'package:moonwell_launcher/app/design_system/mw_launcher_components.dart'; +import 'package:moonwell_launcher/core/moonwell_theme_variant.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +@widgetbook.UseCase(name: 'interactive', type: MwGameTile, path: '[Launcher]') +Widget buildMwGameTileInteractiveUseCase(BuildContext context) { + return SizedBox( + width: context.knobs.double.slider( + label: 'width', + initialValue: 240, + min: 72, + max: 360, + divisions: 36, + ), + child: MwGameTile( + title: context.knobs.string(label: 'title', initialValue: 'MoonWell'), + subtitle: context.knobs.stringOrNull( + label: 'subtitle', + initialValue: 'Wrath of the Lich King', + ), + icon: Icons.nights_stay_outlined, + selected: context.knobs.boolean(label: 'selected', initialValue: true), + enabled: context.knobs.boolean(label: 'enabled', initialValue: true), + compact: context.knobs.boolean(label: 'compact', initialValue: false), + onPressed: () => debugPrint('Game tile selected'), + ), + ); +} + +@widgetbook.UseCase(name: 'full', type: MwGameRail, path: '[Launcher]') +Widget buildMwGameRailFullUseCase(BuildContext context) { + return const SizedBox(width: 240, height: 560, child: MwGameRail()); +} + +@widgetbook.UseCase(name: 'compact', type: MwGameRail, path: '[Launcher]') +Widget buildMwGameRailCompactUseCase(BuildContext context) { + return const SizedBox( + width: 72, + height: 560, + child: MwGameRail(compact: true), + ); +} + +@widgetbook.UseCase(name: 'text_only', type: MwNewsItem, path: '[Launcher]') +Widget buildMwNewsItemTextOnlyUseCase(BuildContext context) { + return SizedBox( + width: 620, + child: MwNewsItem( + item: MwNewsItemData( + title: context.knobs.string( + label: 'title', + initialValue: 'Добро пожаловать в MoonWell', + ), + body: context.knobs.string( + label: 'body', + initialValue: 'Приключение в Нордсколе начинается здесь.', + ), + createdAt: DateTime(2026, 6, 22), + ), + ), + ); +} + +@widgetbook.UseCase(name: 'image_failure', type: MwNewsItem, path: '[Launcher]') +Widget buildMwNewsItemImageFailureUseCase(BuildContext context) { + return const SizedBox( + width: 620, + child: MwNewsItem( + item: MwNewsItemData( + title: 'Обновление игрового мира', + body: 'Изображение недоступно, но публикация остаётся читаемой.', + imageUrl: 'https://invalid.moonwell.local/news.jpg', + ), + ), + ); +} + +@widgetbook.UseCase(name: 'loading', type: MwNewsList, path: '[Launcher]') +Widget buildMwNewsListLoadingUseCase(BuildContext context) { + return const MwNewsList(state: MwNewsListState.loading); +} + +@widgetbook.UseCase(name: 'empty', type: MwNewsList, path: '[Launcher]') +Widget buildMwNewsListEmptyUseCase(BuildContext context) { + return const MwNewsList(state: MwNewsListState.empty); +} + +@widgetbook.UseCase(name: 'failure', type: MwNewsList, path: '[Launcher]') +Widget buildMwNewsListFailureUseCase(BuildContext context) { + return MwNewsList( + state: MwNewsListState.failure, + error: context.knobs.string( + label: 'error', + initialValue: 'Сервер новостей временно недоступен.', + ), + onRetry: () => debugPrint('News reload requested'), + ); +} + +@widgetbook.UseCase(name: 'populated', type: MwNewsList, path: '[Launcher]') +Widget buildMwNewsListPopulatedUseCase(BuildContext context) { + final count = context.knobs.int.slider( + label: 'itemCount', + initialValue: 8, + min: 1, + max: 50, + divisions: 49, + ); + return MwNewsList( + state: MwNewsListState.populated, + items: List.generate( + count, + (index) => MwNewsItemData( + title: 'Новость ${index + 1}', + body: + 'Реалистичный текст публикации о событиях и обновлениях MoonWell.', + createdAt: DateTime(2026, 6, 22).subtract(Duration(days: index)), + ), + ), + ); +} + +@widgetbook.UseCase( + name: 'interactive', + type: MwSyncActionArea, + path: '[Launcher]', +) +Widget buildMwSyncActionAreaInteractiveUseCase(BuildContext context) { + final state = context.knobs.object.dropdown( + label: 'state', + initialOption: MwSyncPresentation.downloading, + options: MwSyncPresentation.values, + labelBuilder: (value) => value.name, + ); + return SizedBox( + width: 420, + child: MwSyncActionArea( + state: state, + status: context.knobs.string( + label: 'status', + initialValue: 'Загружаем обновление клиента…', + ), + progress: context.knobs.double.slider( + label: 'progress', + initialValue: .46, + min: 0, + max: 1, + divisions: 100, + ), + currentPath: context.knobs.stringOrNull( + label: 'currentPath', + initialValue: r'Data\ruRU\patch-ruRU.MPQ', + ), + error: state == MwSyncPresentation.failure + ? context.knobs.string( + label: 'error', + initialValue: 'Проверка SHA-256 не пройдена.', + ) + : null, + onPrimary: () => debugPrint('Primary sync action requested: $state'), + onPause: state == MwSyncPresentation.downloading + ? () => debugPrint('Sync pause requested') + : null, + ), + ); +} + +@widgetbook.UseCase( + name: 'metadata', + type: MwLauncherStatusBar, + path: '[Launcher]', +) +Widget buildMwLauncherStatusBarMetadataUseCase(BuildContext context) { + return MwLauncherStatusBar( + status: context.knobs.string( + label: 'status', + initialValue: 'Загрузка игровых файлов', + ), + fileCount: 'Файлы 124/480', + speed: '12.4 MB/s', + eta: 'ETA 00:01:42', + buildHash: 'a93fd1c2', + ); +} + +@widgetbook.UseCase( + name: 'default', + type: MwAccountAffordance, + path: '[Launcher]', +) +Widget buildMwAccountAffordanceUseCase(BuildContext context) { + return MwAccountAffordance( + onSettings: () => debugPrint('Settings requested'), + onLogout: () => debugPrint('Logout requested'), + ); +} + +@widgetbook.UseCase( + name: 'interactive', + type: MwSettingsPanel, + path: '[Launcher]', +) +Widget buildMwSettingsPanelInteractiveUseCase(BuildContext context) { + final variant = context.knobs.object.dropdown( + label: 'themeVariant', + initialOption: MoonWellThemeVariant.forest, + options: MoonWellThemeVariant.values, + labelBuilder: (value) => value.name, + ); + return SizedBox( + width: 420, + child: MwSettingsPanel( + installationPath: context.knobs.stringOrNull( + label: 'installationPath', + initialValue: r'C:\Games\MoonWell', + ), + themeVariant: variant, + syncActive: context.knobs.boolean( + label: 'syncActive', + initialValue: false, + ), + onChooseDirectory: () => debugPrint('Directory selection requested'), + onThemeChanged: (value) => debugPrint('Theme changed: ${value.name}'), + onVerify: () => debugPrint('Manual verification requested'), + onLogout: () => debugPrint('Logout requested'), + ), + ); +} diff --git a/widgetbook/lib/stories/primitives_stories.dart b/widgetbook/lib/stories/primitives_stories.dart new file mode 100644 index 0000000..5894712 --- /dev/null +++ b/widgetbook/lib/stories/primitives_stories.dart @@ -0,0 +1,258 @@ +import 'package:flutter/material.dart'; +import 'package:moonwell_launcher/app/design_system/mw_primitives.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +@widgetbook.UseCase(name: 'interactive', type: MwButton, path: '[Primitives]') +Widget buildMwButtonInteractiveUseCase(BuildContext context) { + final variant = context.knobs.object.dropdown( + label: 'variant', + initialOption: MwButtonVariant.primary, + options: MwButtonVariant.values, + labelBuilder: (value) => value.name, + ); + return MwButton( + label: context.knobs.string(label: 'label', initialValue: 'Играть'), + icon: context.knobs.boolean(label: 'showIcon', initialValue: true) + ? Icons.play_arrow + : null, + variant: variant, + loading: context.knobs.boolean(label: 'loading', initialValue: false), + expand: context.knobs.boolean(label: 'expand', initialValue: false), + onPressed: context.knobs.boolean(label: 'enabled', initialValue: true) + ? () => debugPrint('MwButton pressed') + : null, + ); +} + +@widgetbook.UseCase(name: 'loading', type: MwButton, path: '[Primitives]') +Widget buildMwButtonLoadingUseCase(BuildContext context) { + return MwButton( + label: context.knobs.string(label: 'label', initialValue: 'Загрузка'), + loading: true, + onPressed: () => debugPrint('Loading MwButton pressed'), + ); +} + +@widgetbook.UseCase(name: 'disabled', type: MwButton, path: '[Primitives]') +Widget buildMwButtonDisabledUseCase(BuildContext context) { + return MwButton( + label: context.knobs.string(label: 'label', initialValue: 'Недоступно'), + onPressed: null, + ); +} + +@widgetbook.UseCase( + name: 'interactive', + type: MwIconButton, + path: '[Primitives]', +) +Widget buildMwIconButtonInteractiveUseCase(BuildContext context) { + return MwIconButton( + icon: Icons.settings, + tooltip: context.knobs.string(label: 'tooltip', initialValue: 'Настройки'), + selected: context.knobs.boolean(label: 'selected', initialValue: false), + destructive: context.knobs.boolean( + label: 'destructive', + initialValue: false, + ), + onPressed: context.knobs.boolean(label: 'enabled', initialValue: true) + ? () => debugPrint('MwIconButton pressed') + : null, + ); +} + +@widgetbook.UseCase( + name: 'interactive', + type: MwTextField, + path: '[Primitives]', +) +Widget buildMwTextFieldInteractiveUseCase(BuildContext context) { + return SizedBox( + width: 380, + child: MwTextField( + label: context.knobs.string(label: 'label', initialValue: 'Логин'), + hint: context.knobs.stringOrNull( + label: 'hint', + initialValue: 'Введите логин', + ), + errorText: context.knobs.stringOrNull( + label: 'errorText', + initialValue: null, + ), + enabled: context.knobs.boolean(label: 'enabled', initialValue: true), + obscureText: context.knobs.boolean( + label: 'obscureText', + initialValue: false, + ), + prefixIcon: context.knobs.boolean(label: 'showIcon', initialValue: true) + ? Icons.person_outline + : null, + onSubmitted: (value) => debugPrint('MwTextField submitted: $value'), + ), + ); +} + +@widgetbook.UseCase(name: 'interactive', type: MwCheckbox, path: '[Primitives]') +Widget buildMwCheckboxInteractiveUseCase(BuildContext context) { + return MwCheckbox( + label: context.knobs.string(label: 'label', initialValue: 'Запомнить меня'), + value: context.knobs.boolean(label: 'value', initialValue: false), + onChanged: context.knobs.boolean(label: 'enabled', initialValue: true) + ? (value) => debugPrint('MwCheckbox changed: $value') + : null, + ); +} + +@widgetbook.UseCase(name: 'interactive', type: MwTabs, path: '[Primitives]') +Widget buildMwTabsInteractiveUseCase(BuildContext context) { + return MwTabs( + labels: const ['Новости', 'Обновления', 'Очень длинный раздел'], + selectedIndex: context.knobs.int.slider( + label: 'selectedIndex', + initialValue: 0, + min: 0, + max: 2, + divisions: 2, + ), + disabledIndices: + context.knobs.boolean(label: 'disableFutureTabs', initialValue: true) + ? const {1, 2} + : const {}, + onSelected: (index) => debugPrint('MwTabs selected: $index'), + ); +} + +@widgetbook.UseCase(name: 'interactive', type: MwBadge, path: '[Primitives]') +Widget buildMwBadgeInteractiveUseCase(BuildContext context) { + return MwBadge( + label: context.knobs.string(label: 'label', initialValue: 'Готово'), + tone: context.knobs.object.dropdown( + label: 'tone', + initialOption: MwBadgeTone.success, + options: MwBadgeTone.values, + labelBuilder: (value) => value.name, + ), + ); +} + +@widgetbook.UseCase(name: 'interactive', type: MwPanel, path: '[Primitives]') +Widget buildMwPanelInteractiveUseCase(BuildContext context) { + return SizedBox( + width: 420, + child: MwPanel( + level: context.knobs.object.dropdown( + label: 'level', + initialOption: MwPanelLevel.regular, + options: MwPanelLevel.values, + labelBuilder: (value) => value.name, + ), + child: Text( + context.knobs.string( + label: 'content', + initialValue: 'Содержимое панели MoonWell', + ), + ), + ), + ); +} + +@widgetbook.UseCase( + name: 'interactive', + type: MwProgressBar, + path: '[Primitives]', +) +Widget buildMwProgressBarInteractiveUseCase(BuildContext context) { + return SizedBox( + width: 440, + child: MwProgressBar( + value: context.knobs.boolean(label: 'indeterminate', initialValue: false) + ? null + : context.knobs.double.slider( + label: 'value', + initialValue: .46, + min: 0, + max: 1, + divisions: 100, + ), + label: context.knobs.stringOrNull( + label: 'label', + initialValue: 'Загрузка файлов', + ), + state: context.knobs.object.dropdown( + label: 'state', + initialOption: MwProgressState.active, + options: MwProgressState.values, + labelBuilder: (value) => value.name, + ), + ), + ); +} + +@widgetbook.UseCase( + name: 'interactive', + type: MwStatusText, + path: '[Primitives]', +) +Widget buildMwStatusTextInteractiveUseCase(BuildContext context) { + return MwStatusText( + text: context.knobs.string( + label: 'text', + initialValue: 'Проверяем локальные файлы…', + ), + tone: context.knobs.object.dropdown( + label: 'tone', + initialOption: MwStatusTone.operational, + options: MwStatusTone.values, + labelBuilder: (value) => value.name, + ), + monospace: context.knobs.boolean(label: 'monospace', initialValue: false), + ); +} + +@widgetbook.UseCase( + name: 'interactive', + type: MwStateView, + path: '[Primitives]', +) +Widget buildMwStateViewInteractiveUseCase(BuildContext context) { + final kind = context.knobs.object.dropdown( + label: 'kind', + initialOption: MwStateKind.empty, + options: MwStateKind.values, + labelBuilder: (value) => value.name, + ); + return MwStateView( + kind: kind, + title: context.knobs.string( + label: 'title', + initialValue: 'Новостей пока нет', + ), + description: context.knobs.stringOrNull( + label: 'description', + initialValue: 'Новые публикации появятся здесь.', + ), + onRetry: kind == MwStateKind.error + ? () => debugPrint('MwStateView retry pressed') + : null, + ); +} + +@widgetbook.UseCase( + name: 'default', + type: MwDisabledFeature, + path: '[Primitives]', +) +Widget buildMwDisabledFeatureUseCase(BuildContext context) { + return MwDisabledFeature( + message: context.knobs.string( + label: 'message', + initialValue: 'Регистрация появится позже', + ), + child: const MwButton( + label: 'Регистрация · скоро', + onPressed: null, + variant: MwButtonVariant.secondary, + ), + ); +} diff --git a/widgetbook/lib/stories/shell_stories.dart b/widgetbook/lib/stories/shell_stories.dart new file mode 100644 index 0000000..73acc58 --- /dev/null +++ b/widgetbook/lib/stories/shell_stories.dart @@ -0,0 +1,246 @@ +import 'package:flutter/material.dart'; +import 'package:moonwell_launcher/app/design_system/mw_authentication.dart'; +import 'package:moonwell_launcher/app/design_system/mw_launcher_components.dart'; +import 'package:moonwell_launcher/app/design_system/mw_shells.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +VoidCallback _windowCallback(String action) { + return () => debugPrint('Window action requested: $action'); +} + +@widgetbook.UseCase(name: 'default', type: MwBrandMark, path: '[Foundations]') +Widget buildMwBrandMarkUseCase(BuildContext context) { + return MwBrandMark( + width: context.knobs.double.slider( + label: 'width', + initialValue: 220, + min: 96, + max: 360, + divisions: 33, + ), + ); +} + +@widgetbook.UseCase( + name: 'default', + type: MwArtworkBackground, + path: '[Foundations]', +) +Widget buildMwArtworkBackgroundUseCase(BuildContext context) { + return const MwArtworkBackground( + assetPackage: 'moonwell_launcher', + child: Center(child: Text('Safe content area')), + ); +} + +@widgetbook.UseCase(name: 'default', type: MwLoginShell, path: '[Compositions]') +Widget buildMwLoginShellUseCase(BuildContext context) { + return MwLoginShell( + assetPackage: 'moonwell_launcher', + form: MwAuthenticationForm( + usernameController: TextEditingController(), + passwordController: TextEditingController(), + onSubmit: () => debugPrint('Login submitted'), + ), + onDrag: _windowCallback('drag'), + onDoubleTap: _windowCallback('double_tap'), + onMinimize: _windowCallback('minimize'), + onMaximizeRestore: _windowCallback('maximize_restore'), + onClose: _windowCallback('close'), + ); +} + +@widgetbook.UseCase( + name: 'api_error', + type: MwLoginShell, + path: '[Compositions]', +) +Widget buildMwLoginShellApiErrorUseCase(BuildContext context) { + return MwLoginShell( + assetPackage: 'moonwell_launcher', + form: MwAuthenticationForm( + usernameController: TextEditingController(text: 'moonwalker'), + passwordController: TextEditingController(), + error: context.knobs.string( + label: 'error', + initialValue: 'Неверный логин или пароль.', + ), + onSubmit: () => debugPrint('Login submitted after API error'), + ), + onDrag: _windowCallback('drag'), + onDoubleTap: _windowCallback('double_tap'), + onMinimize: _windowCallback('minimize'), + onMaximizeRestore: _windowCallback('maximize_restore'), + onClose: _windowCallback('close'), + ); +} + +@widgetbook.UseCase( + name: 'restored_session_loading', + type: MwLoginShell, + path: '[Compositions]', +) +Widget buildMwLoginShellRestoredSessionLoadingUseCase(BuildContext context) { + return MwLoginShell( + assetPackage: 'moonwell_launcher', + form: const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Восстанавливаем защищённую сессию…'), + ], + ), + ), + onDrag: _windowCallback('drag'), + onDoubleTap: _windowCallback('double_tap'), + onMinimize: _windowCallback('minimize'), + onMaximizeRestore: _windowCallback('maximize_restore'), + onClose: _windowCallback('close'), + ); +} + +MwLauncherShell _launcherShell({ + required MwNewsListState newsState, + required MwSyncPresentation syncState, + required String status, + double? progress, + String? error, +}) { + return MwLauncherShell( + assetPackage: 'moonwell_launcher', + news: MwNewsList( + state: newsState, + error: newsState == MwNewsListState.failure + ? 'Сервер новостей временно недоступен.' + : null, + items: newsState == MwNewsListState.populated + ? List.generate( + 8, + (index) => MwNewsItemData( + title: 'Новость ${index + 1}', + body: 'События игрового мира и важные обновления MoonWell.', + createdAt: DateTime(2026, 6, 22 - index), + ), + ) + : const [], + onRetry: () => debugPrint('News reload requested'), + ), + syncActions: MwSyncActionArea( + state: syncState, + status: status, + progress: progress, + currentPath: syncState == MwSyncPresentation.downloading + ? r'Data\ruRU\patch-ruRU.MPQ' + : null, + error: error, + onPrimary: () => debugPrint('Primary sync action requested'), + onPause: syncState == MwSyncPresentation.downloading + ? () => debugPrint('Sync pause requested') + : null, + ), + statusBar: const MwLauncherStatusBar( + status: 'MoonWell Launcher · локальная установка', + fileCount: '124/480', + buildHash: 'a93fd1c2', + ), + account: MwAccountAffordance( + onSettings: () => debugPrint('Settings requested'), + onLogout: () => debugPrint('Logout requested'), + ), + onDrag: _windowCallback('drag'), + onDoubleTap: _windowCallback('double_tap'), + onMinimize: _windowCallback('minimize'), + onMaximizeRestore: _windowCallback('maximize_restore'), + onClose: _windowCallback('close'), + ); +} + +@widgetbook.UseCase( + name: 'ready', + type: MwLauncherShell, + path: '[Compositions]', +) +Widget buildMwLauncherShellReadyUseCase(BuildContext context) { + return _launcherShell( + newsState: MwNewsListState.populated, + syncState: MwSyncPresentation.ready, + status: 'Клиент готов к запуску.', + ); +} + +@widgetbook.UseCase( + name: 'downloading', + type: MwLauncherShell, + path: '[Compositions]', +) +Widget buildMwLauncherShellDownloadingUseCase(BuildContext context) { + return _launcherShell( + newsState: MwNewsListState.populated, + syncState: MwSyncPresentation.downloading, + status: 'Загружаем обновление клиента…', + progress: context.knobs.double.slider( + label: 'progress', + initialValue: .46, + min: 0, + max: 1, + divisions: 100, + ), + ); +} + +@widgetbook.UseCase( + name: 'paused', + type: MwLauncherShell, + path: '[Compositions]', +) +Widget buildMwLauncherShellPausedUseCase(BuildContext context) { + return _launcherShell( + newsState: MwNewsListState.populated, + syncState: MwSyncPresentation.paused, + status: 'Обновление приостановлено.', + progress: .46, + ); +} + +@widgetbook.UseCase( + name: 'verifying', + type: MwLauncherShell, + path: '[Compositions]', +) +Widget buildMwLauncherShellVerifyingUseCase(BuildContext context) { + return _launcherShell( + newsState: MwNewsListState.populated, + syncState: MwSyncPresentation.verifying, + status: 'Проверяем целостность установки…', + ); +} + +@widgetbook.UseCase( + name: 'failure', + type: MwLauncherShell, + path: '[Compositions]', +) +Widget buildMwLauncherShellFailureUseCase(BuildContext context) { + return _launcherShell( + newsState: MwNewsListState.failure, + syncState: MwSyncPresentation.failure, + status: 'Не удалось завершить обновление.', + error: 'Проверка SHA-256 не пройдена.', + ); +} + +@widgetbook.UseCase( + name: 'empty_news', + type: MwLauncherShell, + path: '[Compositions]', +) +Widget buildMwLauncherShellEmptyNewsUseCase(BuildContext context) { + return _launcherShell( + newsState: MwNewsListState.empty, + syncState: MwSyncPresentation.ready, + status: 'Клиент готов к запуску.', + ); +} diff --git a/widgetbook/lib/stories/window_chrome_stories.dart b/widgetbook/lib/stories/window_chrome_stories.dart new file mode 100644 index 0000000..c63087f --- /dev/null +++ b/widgetbook/lib/stories/window_chrome_stories.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:moonwell_launcher/app/design_system/mw_window_chrome.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +@widgetbook.UseCase( + name: 'interactive', + type: MwWindowChrome, + path: '[Window Chrome]', +) +Widget buildMwWindowChromeInteractiveUseCase(BuildContext context) { + return MwWindowChrome( + onDrag: () => debugPrint('Window drag requested'), + onDoubleTap: () => debugPrint('Window maximize/restore requested'), + onMinimize: () => debugPrint('Window minimize requested'), + onMaximizeRestore: () => debugPrint('Window maximize/restore requested'), + onClose: () => debugPrint('Window close requested'), + showMaximize: context.knobs.boolean(label: 'Show maximize button'), + ); +} diff --git a/widgetbook/macos/.gitignore b/widgetbook/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/widgetbook/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/widgetbook/macos/Flutter/Flutter-Debug.xcconfig b/widgetbook/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/widgetbook/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/widgetbook/macos/Flutter/Flutter-Release.xcconfig b/widgetbook/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/widgetbook/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/widgetbook/macos/Flutter/GeneratedPluginRegistrant.swift b/widgetbook/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..d8e7226 --- /dev/null +++ b/widgetbook/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,24 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import auto_updater_macos +import file_picker +import flutter_secure_storage_darwin +import screen_retriever_macos +import shared_preferences_foundation +import url_launcher_macos +import window_manager + +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")) + ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) +} diff --git a/widgetbook/macos/Podfile b/widgetbook/macos/Podfile new file mode 100644 index 0000000..ff5ddb3 --- /dev/null +++ b/widgetbook/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/widgetbook/macos/Podfile.lock b/widgetbook/macos/Podfile.lock new file mode 100644 index 0000000..87cd068 --- /dev/null +++ b/widgetbook/macos/Podfile.lock @@ -0,0 +1,29 @@ +PODS: + - auto_updater_macos (0.0.1): + - FlutterMacOS + - Sparkle + - FlutterMacOS (1.0.0) + - Sparkle (2.9.3) + +DEPENDENCIES: + - auto_updater_macos (from `Flutter/ephemeral/.symlinks/plugins/auto_updater_macos/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + +SPEC REPOS: + trunk: + - Sparkle + +EXTERNAL SOURCES: + auto_updater_macos: + :path: Flutter/ephemeral/.symlinks/plugins/auto_updater_macos/macos + FlutterMacOS: + :path: Flutter/ephemeral + +SPEC CHECKSUMS: + auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + Sparkle: 39dbdf28637f6056fbf5ee6494bdbfae54f49f41 + +PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 + +COCOAPODS: 1.16.2 diff --git a/widgetbook/macos/Runner.xcodeproj/project.pbxproj b/widgetbook/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..c4998d8 --- /dev/null +++ b/widgetbook/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,825 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + A82F444CC8B3A4A9D77658B5 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FF1E9F75AAD4A9DAA6A7192 /* Pods_Runner.framework */; }; + D75F855AEFA955416C9E3CB5 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF66BD0165F006AA79AB2869 /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1FF1E9F75AAD4A9DAA6A7192 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* widgetbook.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = widgetbook.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 35EAC1A04D0F64CA5C189356 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 515CCD8F3833D9CA297F10EF /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 5478AE1693BA39F1CE84F4BE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 7B8881A1E0279BE3DA0DA28B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 804A1BB5B85FA4B1AD3A3AFD /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + AF66BD0165F006AA79AB2869 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C27E9A4553B65B33F4434D34 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D75F855AEFA955416C9E3CB5 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + A82F444CC8B3A4A9D77658B5 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 542EB08614890ADE7265109E /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* widgetbook.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 542EB08614890ADE7265109E /* Pods */ = { + isa = PBXGroup; + children = ( + 515CCD8F3833D9CA297F10EF /* Pods-Runner.debug.xcconfig */, + 7B8881A1E0279BE3DA0DA28B /* Pods-Runner.release.xcconfig */, + 5478AE1693BA39F1CE84F4BE /* Pods-Runner.profile.xcconfig */, + 35EAC1A04D0F64CA5C189356 /* Pods-RunnerTests.debug.xcconfig */, + 804A1BB5B85FA4B1AD3A3AFD /* Pods-RunnerTests.release.xcconfig */, + C27E9A4553B65B33F4434D34 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1FF1E9F75AAD4A9DAA6A7192 /* Pods_Runner.framework */, + AF66BD0165F006AA79AB2869 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + A081369AD173B7B0E5A431B9 /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + E24F7DF6829BA92C9292551B /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + A7E3C45DA7734DF67E9E2B88 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* widgetbook.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + A081369AD173B7B0E5A431B9 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + A7E3C45DA7734DF67E9E2B88 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E24F7DF6829BA92C9292551B /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 35EAC1A04D0F64CA5C189356 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.widgetbook.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/widgetbook.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/widgetbook"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 804A1BB5B85FA4B1AD3A3AFD /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.widgetbook.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/widgetbook.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/widgetbook"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C27E9A4553B65B33F4434D34 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.widgetbook.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/widgetbook.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/widgetbook"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/widgetbook/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/widgetbook/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/widgetbook/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/widgetbook/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/widgetbook/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c3ad017 --- /dev/null +++ b/widgetbook/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/widgetbook/macos/Runner.xcworkspace/contents.xcworkspacedata b/widgetbook/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/widgetbook/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/widgetbook/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/widgetbook/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/widgetbook/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/widgetbook/macos/Runner/AppDelegate.swift b/widgetbook/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/widgetbook/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/widgetbook/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/widgetbook/macos/Runner/Base.lproj/MainMenu.xib b/widgetbook/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/widgetbook/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/widgetbook/macos/Runner/Configs/AppInfo.xcconfig b/widgetbook/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..af64304 --- /dev/null +++ b/widgetbook/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = widgetbook + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.widgetbook + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved. diff --git a/widgetbook/macos/Runner/Configs/Debug.xcconfig b/widgetbook/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/widgetbook/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/widgetbook/macos/Runner/Configs/Release.xcconfig b/widgetbook/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/widgetbook/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/widgetbook/macos/Runner/Configs/Warnings.xcconfig b/widgetbook/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/widgetbook/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/widgetbook/macos/Runner/DebugProfile.entitlements b/widgetbook/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/widgetbook/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/widgetbook/macos/Runner/Info.plist b/widgetbook/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/widgetbook/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/widgetbook/macos/Runner/MainFlutterWindow.swift b/widgetbook/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/widgetbook/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/widgetbook/macos/Runner/Release.entitlements b/widgetbook/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/widgetbook/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/widgetbook/macos/RunnerTests/RunnerTests.swift b/widgetbook/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/widgetbook/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/widgetbook/pubspec.lock b/widgetbook/pubspec.lock new file mode 100644 index 0000000..4d38310 --- /dev/null +++ b/widgetbook/pubspec.lock @@ -0,0 +1,1089 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b + url: "https://pub.dev" + source: hosted + version: "100.0.0" + accessibility_tools: + dependency: transitive + description: + name: accessibility_tools + sha256: c29732e423175a51e0a6ace7df1255f5d77812c7c7b7101d8ba0186a43d533aa + url: "https://pub.dev" + source: hosted + version: "2.8.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf" + url: "https://pub.dev" + source: hosted + version: "13.0.0" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + auto_updater: + dependency: transitive + description: + name: auto_updater + sha256: "74fd008b021d15e4fc50fb5f1923870e6374ae831bb1168241c23bc35a4e898b" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + auto_updater_macos: + dependency: transitive + description: + name: auto_updater_macos + sha256: ef9de0daf38d782d2243fe131503a9404d62df762f5386e5360020e43c01867f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + auto_updater_platform_interface: + dependency: transitive + description: + name: auto_updater_platform_interface + sha256: ed5dff8cea18c58bc5ac787ff9122fedd1644272e02015d28c9b592f8078c5dc + url: "https://pub.dev" + source: hosted + version: "1.0.0" + auto_updater_windows: + dependency: transitive + description: + name: auto_updater_windows + sha256: "2bba20a71eee072f49b7267fedd5c4f1406c4b1b1e5b83932c634dbab75b80c9" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: e03b235924e4f509c27b5d6b2f949200e0a91149a9818b4f65eeb56662b75413 + url: "https://pub.dev" + source: hosted + version: "9.2.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10 + url: "https://pub.dev" + source: hosted + version: "4.0.6" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + url: "https://pub.dev" + source: hosted + version: "4.1.1" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6" + url: "https://pub.dev" + source: hosted + version: "2.15.0" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56" + url: "https://pub.dev" + source: hosted + version: "8.12.6" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + 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: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" + url: "https://pub.dev" + source: hosted + version: "4.11.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: transitive + description: + name: cupertino_icons + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.dev" + source: hosted + version: "1.0.9" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "59d53ef8eaed9d288ed9767618e2b31c4fa0383a127db59d5eb2e737a7638a60" + url: "https://pub.dev" + source: hosted + version: "3.1.9" + dbus: + dependency: transitive + description: + name: dbus + sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" + url: "https://pub.dev" + source: hosted + version: "0.7.14" + device_frame_plus: + dependency: transitive + description: + name: device_frame_plus + sha256: ccc94abccd4d9f0a9f19ef239001b3a59896e678ad42601371d7065889f2bf78 + url: "https://pub.dev" + source: hosted + version: "1.5.0" + dio: + dependency: transitive + description: + name: dio + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c + url: "https://pub.dev" + source: hosted + version: "5.9.2" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_picker: + dependency: transitive + description: + name: file_picker + sha256: f13a03000d942e476bc1ff0a736d2e9de711d2f89a95cd4c1d88f861c3348387 + url: "https://pub.dev" + source: hosted + version: "11.0.2" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_bloc: + dependency: transitive + description: + name: flutter_bloc + sha256: cf51747952201a455a1c840f8171d273be009b932c75093020f9af64f2123e38 + url: "https://pub.dev" + source: hosted + version: "9.1.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785" + url: "https://pub.dev" + source: hosted + version: "2.0.35" + flutter_secure_storage: + dependency: transitive + 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 + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + get_it: + dependency: transitive + description: + name: get_it + sha256: "568d62f0e68666fb5d95519743b3c24a34c7f19d834b0658c46e26d778461f66" + url: "https://pub.dev" + source: hosted + version: "9.2.1" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + 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" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + injectable: + dependency: transitive + description: + name: injectable + sha256: "1da6399509e44ddb0d8a4a35291d7122c4efb5c625a9d733bf5e48d4a72e379e" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + inspector: + dependency: transitive + description: + name: inspector + sha256: "60782d94c8851b0eee3ffdfea9fd43e7c37f919b54cb587e4de9da0e09d491b5" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + 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: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" + url: "https://pub.dev" + source: hosted + version: "4.12.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + url: "https://pub.dev" + source: hosted + version: "1.18.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + moonwell_launcher: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "1.0.0+1" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + 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: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + 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: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + pixelarticons: + dependency: transitive + description: + name: pixelarticons + sha256: abe536bb9710cffc2a9a30d27666bbcd43742e391b6ba2f3de89313259d9f1c9 + url: "https://pub.dev" + source: hosted + version: "0.4.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + provider: + dependency: transitive + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + resizable_widget: + dependency: transitive + description: + name: resizable_widget + sha256: db2919754b93f386b9b3fb15e9f48f6c9d6d41f00a24397629133c99df86606a + url: "https://pub.dev" + source: hosted + version: "1.0.5" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + sha256: "42cc3b402a0f67d2455a0d067553d0f13453f6a008d98eababf8b63958d506bd" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + screen_retriever_linux: + dependency: transitive + description: + name: screen_retriever_linux + sha256: "2a476f1a5538065bc5badf376cfdc83d6ecf07d77eb2391b9c2bff5a76970048" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + screen_retriever_macos: + dependency: transitive + description: + name: screen_retriever_macos + sha256: b5abb900fcb86614ff10b738b34e37b9e1d03b0447280668e2bc8a98bdc7bd59 + url: "https://pub.dev" + source: hosted + version: "0.2.1" + screen_retriever_platform_interface: + dependency: transitive + description: + name: screen_retriever_platform_interface + sha256: "3af22d926bedf20c2caa308eea376776451a3af125919ce072e56525fded8901" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + screen_retriever_windows: + dependency: transitive + description: + name: screen_retriever_windows + sha256: c44b38a4c4bab34af259180a70a4eee1e29384e7b82e627c9faa68afcdab2e73 + url: "https://pub.dev" + source: hosted + version: "0.2.1" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + url: "https://pub.dev" + source: hosted + version: "2.5.5" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "93ae5884a9df5d3bb696825bceb3a17590754548b5d740eba51500afc8d088f5" + url: "https://pub.dev" + source: hosted + version: "2.4.26" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02 + url: "https://pub.dev" + source: hosted + version: "4.2.3" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 + url: "https://pub.dev" + source: hosted + version: "6.3.32" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + widgetbook: + dependency: "direct main" + description: + name: widgetbook + sha256: "7ce2a15c59990610aefb0a8a4d69f67dd842d41676b7af6b5b746aa3371d32cf" + url: "https://pub.dev" + source: hosted + version: "3.24.0" + widgetbook_annotation: + dependency: "direct main" + description: + name: widgetbook_annotation + sha256: ec98130f23579f14e304b6b45516612676ae4e0b0e6bb1debe9dba4ea604ab42 + url: "https://pub.dev" + source: hosted + version: "3.11.0" + widgetbook_generator: + dependency: "direct dev" + description: + name: widgetbook_generator + sha256: "8fd3b4208bb74cbe60cd6fc99b443561b65542658b20d6dd700a2496edbd5647" + url: "https://pub.dev" + source: hosted + version: "3.24.0" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + window_manager: + dependency: transitive + description: + name: window_manager + sha256: "7eb6d6c4164ec08e1bf978d6e733f3cebe792e2a23fb07cbca25c2872bfdbdcd" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" diff --git a/widgetbook/pubspec.yaml b/widgetbook/pubspec.yaml new file mode 100644 index 0000000..0435bb9 --- /dev/null +++ b/widgetbook/pubspec.yaml @@ -0,0 +1,25 @@ +name: widgetbook_workspace +description: "Widgetbook workspace for MoonWell Launcher" +publish_to: "none" +version: 0.1.0+1 + +environment: + sdk: ">=3.10.0" + +dependencies: + flutter: + sdk: flutter + widgetbook: ^3.23.0 + widgetbook_annotation: ^3.11.0 + moonwell_launcher: + path: ../ + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + widgetbook_generator: ^3.24.0 + build_runner: ^2.15.0 + +flutter: + uses-material-design: true diff --git a/widgetbook/windows/.gitignore b/widgetbook/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/widgetbook/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/widgetbook/windows/CMakeLists.txt b/widgetbook/windows/CMakeLists.txt new file mode 100644 index 0000000..e943714 --- /dev/null +++ b/widgetbook/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(widgetbook LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "widgetbook") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/widgetbook/windows/flutter/CMakeLists.txt b/widgetbook/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/widgetbook/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/widgetbook/windows/flutter/generated_plugin_registrant.cc b/widgetbook/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..4e09c01 --- /dev/null +++ b/widgetbook/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,26 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + AutoUpdaterWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AutoUpdaterWindowsPluginCApi")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); +} diff --git a/widgetbook/windows/flutter/generated_plugin_registrant.h b/widgetbook/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/widgetbook/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/widgetbook/windows/flutter/generated_plugins.cmake b/widgetbook/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..6f52a72 --- /dev/null +++ b/widgetbook/windows/flutter/generated_plugins.cmake @@ -0,0 +1,29 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + auto_updater_windows + flutter_secure_storage_windows + screen_retriever_windows + url_launcher_windows + window_manager +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/widgetbook/windows/runner/CMakeLists.txt b/widgetbook/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/widgetbook/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/widgetbook/windows/runner/Runner.rc b/widgetbook/windows/runner/Runner.rc new file mode 100644 index 0000000..e2b7559 --- /dev/null +++ b/widgetbook/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "widgetbook" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "widgetbook" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "widgetbook.exe" "\0" + VALUE "ProductName", "widgetbook" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/widgetbook/windows/runner/flutter_window.cpp b/widgetbook/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/widgetbook/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/widgetbook/windows/runner/flutter_window.h b/widgetbook/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/widgetbook/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/widgetbook/windows/runner/main.cpp b/widgetbook/windows/runner/main.cpp new file mode 100644 index 0000000..35d9e78 --- /dev/null +++ b/widgetbook/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"widgetbook", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/widgetbook/windows/runner/resource.h b/widgetbook/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/widgetbook/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/widgetbook/windows/runner/resources/app_icon.ico b/widgetbook/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/widgetbook/windows/runner/resources/app_icon.ico differ diff --git a/widgetbook/windows/runner/runner.exe.manifest b/widgetbook/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/widgetbook/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/widgetbook/windows/runner/utils.cpp b/widgetbook/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/widgetbook/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/widgetbook/windows/runner/utils.h b/widgetbook/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/widgetbook/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/widgetbook/windows/runner/win32_window.cpp b/widgetbook/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/widgetbook/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/widgetbook/windows/runner/win32_window.h b/widgetbook/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/widgetbook/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_