WIP: feat(ui): Migrate to new UI implementation

This commit is contained in:
2026-06-22 06:47:16 +04:00
parent 873aa8d7b5
commit bb4334f68d
109 changed files with 13499 additions and 1257 deletions
+3 -1
View File
@@ -1 +1,3 @@
{}
{
"dart.flutterSdkPath": ".fvm/versions/stable"
}
+19 -12
View File
@@ -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.
+23 -10
View File
@@ -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
+93
View File
@@ -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.
+93
View File
@@ -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.
Binary file not shown.
+93
View File
@@ -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.
Binary file not shown.
+93
View File
@@ -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.
Binary file not shown.
+3 -3
View File
@@ -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
```
+2 -2
View File
@@ -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
+6 -6
View File
@@ -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
+4 -4
View File
@@ -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
+330
View File
@@ -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<TextAlign>(
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
@@ -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<MwAuthenticationForm> createState() => _MwAuthenticationFormState();
}
class _MwAuthenticationFormState extends State<MwAuthenticationForm> {
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,
),
),
),
);
}
@@ -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<MwNewsItemData> 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<String>(
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<MoonWellThemeVariant> 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<MoonWellThemeVariant>(
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,
),
],
),
);
}
+448
View File
@@ -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<String>? 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<bool?>? 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<String> labels;
final int selectedIndex;
final ValueChanged<int> onSelected;
final Set<int> 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);
}
}
+447
View File
@@ -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,
),
),
],
),
);
}
}
@@ -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),
),
),
),
),
);
}
}
@@ -136,6 +136,7 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
errorMessage: null,
processedFiles: 0,
totalFiles: 0,
syncStage: null,
),
),
);
@@ -306,6 +307,7 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
remoteBuildHash: event.status.remoteBuildHash,
processedFiles: event.status.processedFiles,
totalFiles: event.status.totalFiles,
syncStage: event.status.stage,
),
),
);
@@ -330,6 +332,7 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
statusText: 'Обновление приостановлено.',
currentPath: null,
errorMessage: null,
syncStage: null,
),
),
);
@@ -344,6 +347,7 @@ class HomeScreenBloc extends Bloc<HomeScreenEvent, HomeScreenState> {
statusText: 'Не удалось завершить обновление клиента.',
currentPath: null,
errorMessage: _formatError(event.error),
syncStage: null,
),
),
);
@@ -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<LauncherNewsItem> 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 <LauncherNewsItem>[],
isLoadingNews = true,
newsErrorMessage = null;
newsErrorMessage = null,
syncStage = null;
bool get isBusy =>
phase == HomeScreenPhase.authenticating ||
@@ -91,6 +95,7 @@ final class HomeScreenModel {
List<LauncherNewsItem>? 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?,
);
}
}
+1 -34
View File
@@ -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(),
),
);
}
+153 -580
View File
@@ -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<HomeScreenBloc>().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<HomeScreenBloc>().state.model;
return MwLauncherShell(
news: _buildNews(model),
syncActions: _buildSyncActions(context, model),
statusBar: _buildStatusBar(model),
account: MwAccountAffordance(
onSettings: () => _showSettings(context, model),
onLogout: () =>
context.read<HomeScreenBloc>().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]);
},
);
}
}
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,
),
),
],
),
);
}
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<MoonWellDecorations>()!;
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<HomeScreenBloc>().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<HomeScreenBloc>().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 '-';
}
if (hash.length <= 12) {
return hash;
}
return '${hash.substring(0, 8)}...${hash.substring(hash.length - 4)}';
}
}
class _BottomBar extends StatelessWidget {
const _BottomBar({required this.model, required this.cs});
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)],
),
),
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) {
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<HomeScreenBloc>();
if (model.canPlay) {
if (model.phase == HomeScreenPhase.readyToPlay && model.canPlay) {
bloc.add(HomeScreenPlayRequested());
return;
}
} else {
bloc.add(HomeScreenSyncRequested());
}
},
onPause: model.isSyncing
? () => context.read<HomeScreenBloc>().add(HomeScreenPauseRequested())
: null,
);
}
void _onSecondaryAction(BuildContext context, HomeScreenModel model) {
MwSyncPresentation _syncPresentation(HomeScreenModel model) {
if (model.phase == HomeScreenPhase.paused) {
return MwSyncPresentation.paused;
}
if (model.phase == HomeScreenPhase.failure) {
return MwSyncPresentation.failure;
}
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,
};
}
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),
);
}
Future<void> _showSettings(
BuildContext context,
HomeScreenModel model,
) async {
final bloc = context.read<HomeScreenBloc>();
if (model.isSyncing) {
bloc.add(HomeScreenPauseRequested());
return;
}
if (model.canPlay) {
bloc.add(HomeScreenSyncRequested());
return;
}
final themeController = LauncherThemeScope.of(context);
await showDialog<void>(
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());
},
),
),
),
),
),
);
}
String _resolvePrimaryLabel(HomeScreenModel model) {
if (model.isSyncing) {
return '\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435...';
Future<void> _toggleMaximized() async {
if (await windowManager.isMaximized()) {
await windowManager.unmaximize();
} else {
await windowManager.maximize();
}
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);
}
}
@@ -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<MoonWellDecorations>()!;
return Container(
height: 20,
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: cs.outline),
),
clipBehavior: Clip.antiAlias,
child: Stack(
children: [
// 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),
),
),
),
),
],
),
);
}
}
+25 -109
View File
@@ -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<LoginScreen> {
@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<void> _toggleMaximized() async {
if (await windowManager.isMaximized()) {
await windowManager.unmaximize();
} else {
await windowManager.maximize();
}
}
}
+55 -43
View File
@@ -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<MoonWellApp> createState() => _MoonWellAppState();
}
class _MoonWellAppState extends State<MoonWellApp> {
late final LauncherThemeController _themeController;
@override
void initState() {
super.initState();
_themeController = LauncherThemeController(getIt<PreferencesRepository>());
_themeController.load();
}
@override
void dispose() {
_themeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
return AnimatedBuilder(
animation: _themeController,
builder: (context, _) => LauncherThemeScope(
controller: _themeController,
child: MaterialApp(
title: 'MoonWell',
debugShowCheckedModeBanner: false,
home: const _LauncherBootstrapScreen(),
theme: moonWellTheme(),
theme: MoonWellTheme.create(_themeController.variant),
),
),
);
}
}
class LauncherThemeScope extends InheritedNotifier<LauncherThemeController> {
const LauncherThemeScope({
super.key,
required LauncherThemeController controller,
required super.child,
}) : super(notifier: controller);
static LauncherThemeController of(BuildContext context) {
final scope = context
.dependOnInheritedWidgetOfExactType<LauncherThemeScope>();
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),
],
),
),
),
),
Center(
return const Scaffold(
body: MwArtworkBackground(
child: 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,
),
MwBrandMark(width: 220),
SizedBox(height: 24),
SizedBox.square(
dimension: 28,
child: CircularProgressIndicator(strokeWidth: 2),
),
],
),
),
],
),
);
}
+308
View File
@@ -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<MoonWellDesignTokens> {
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<BoxShadow> panelShadow;
final Duration motionFast;
final Duration motionStandard;
final String brandFontFamily;
final String monospaceFontFamily;
static MoonWellDesignTokens of(BuildContext context) =>
Theme.of(context).extension<MoonWellDesignTokens>()!;
@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<BoxShadow>? 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<void> load() async {
_variant = await _preferencesRepository.getThemeVariant();
notifyListeners();
}
Future<void> setVariant(MoonWellThemeVariant variant) async {
if (_variant == variant) return;
_variant = variant;
notifyListeners();
await _preferencesRepository.setThemeVariant(variant);
}
}
-22
View File
@@ -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);
}
-40
View File
@@ -1,40 +0,0 @@
part of 'mw_theme.dart';
class MoonWellDecorations extends ThemeExtension<MoonWellDecorations> {
final Gradient goldBevel; // for gilded headers/buttons
final Shadow textGlow; // subtle moonlight glow
final List<BoxShadow> cardGlow;
const MoonWellDecorations({
required this.goldBevel,
required this.textGlow,
required this.cardGlow,
});
@override
MoonWellDecorations copyWith({
Gradient? goldBevel,
Shadow? textGlow,
List<BoxShadow>? cardGlow,
}) => MoonWellDecorations(
goldBevel: goldBevel ?? this.goldBevel,
textGlow: textGlow ?? this.textGlow,
cardGlow: cardGlow ?? this.cardGlow,
);
@override
ThemeExtension<MoonWellDecorations> lerp(
ThemeExtension<MoonWellDecorations>? 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)!,
],
);
}
}
-220
View File
@@ -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: <ThemeExtension<dynamic>>[
const MoonWellDecorations(
goldBevel: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: <Color>[
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),
),
],
),
],
);
}
-77
View File
@@ -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<void> _toggleMaximized() async {
if (await windowManager.isMaximized()) {
await windowManager.unmaximize();
} else {
await windowManager.maximize();
}
}
}
class _TitleBarButton extends StatelessWidget {
const _TitleBarButton({
required this.icon,
required this.onTap,
required this.color,
});
final IconData icon;
final VoidCallback onTap;
final Color color;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: SizedBox(
width: 40,
height: 36,
child: Icon(icon, size: 16, color: color.withAlpha(180)),
),
);
}
}
+11
View File
@@ -0,0 +1,11 @@
enum MoonWellThemeVariant {
forest,
temple;
static MoonWellThemeVariant fromStoredValue(String? value) {
return MoonWellThemeVariant.values.firstWhere(
(variant) => variant.name == value,
orElse: () => MoonWellThemeVariant.forest,
);
}
}
@@ -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<MoonWellThemeVariant> getThemeVariant() async {
return MoonWellThemeVariant.fromStoredValue(
_preferences.getString(_themeVariantKey),
);
}
@override
Future<void> setThemeVariant(MoonWellThemeVariant variant) {
return _preferences.setString(_themeVariantKey, variant.name);
}
Future<void> _maybeClearLegacyLauncherSession() async {
if (_preferences.containsKey(_launcherSessionKey)) {
await _preferences.remove(_launcherSessionKey);
@@ -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<void> setLauncherSession(LauncherSession session);
Future<void> clearLauncherSession();
Future<MoonWellThemeVariant> getThemeVariant();
Future<void> setThemeVariant(MoonWellThemeVariant variant);
}
+3 -2
View File
@@ -14,8 +14,9 @@ Future<void> main() async {
await configureDependencies(env: 'flutter');
final launcherUpdateService = getIt<LauncherUpdateService>();
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,
-32
View File
@@ -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
+22
View File
@@ -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 = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
/* 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 */;
}
@@ -5,6 +5,24 @@
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "&quot;$FLUTTER_ROOT&quot;/packages/flutter_tools/bin/macos_assemble.sh prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "moonwell_launcher.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
+4612
View File
File diff suppressed because one or more lines are too long
+6 -6
View File
@@ -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:
+14 -1
View File
@@ -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
@@ -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<Uri?> getOutputDir() async => null;
@override
Future<MoonWellThemeVariant> getThemeVariant() async =>
MoonWellThemeVariant.forest;
@override
Future<void> setLauncherSession(LauncherSession session) async {}
@override
Future<void> setOutputDir(Uri uri) async {}
@override
Future<void> setThemeVariant(MoonWellThemeVariant variant) async {}
}
@@ -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<Uri?> getOutputDir() async => null;
@override
Future<MoonWellThemeVariant> getThemeVariant() async =>
MoonWellThemeVariant.forest;
@override
Future<void> setLauncherSession(LauncherSession session) async {
this.session = session;
@@ -114,4 +119,7 @@ class _FakePreferencesRepository implements PreferencesRepository {
@override
Future<void> setOutputDir(Uri uri) async {}
@override
Future<void> setThemeVariant(MoonWellThemeVariant variant) async {}
}
+3
View File
@@ -0,0 +1,3 @@
{
"flutter": "stable"
}
+48
View File
@@ -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/
+33
View File
@@ -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'
+3
View File
@@ -0,0 +1,3 @@
{
"dart.flutterSdkPath": ".fvm/versions/stable"
}
+11
View File
@@ -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).
+1
View File
@@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml
+3
View File
@@ -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:
+65
View File
@@ -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),
],
);
}
}
+413
View File
@@ -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,
),
],
),
],
),
];
@@ -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'),
),
);
}
@@ -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 = <String, Color>{
'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();
}
@@ -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<MwSyncPresentation>(
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<MoonWellThemeVariant>(
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'),
),
);
}
@@ -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<MwButtonVariant>(
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<MwBadgeTone>(
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<MwPanelLevel>(
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<MwProgressState>(
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<MwStatusTone>(
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<MwStateKind>(
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,
),
);
}
+246
View File
@@ -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: 'Клиент готов к запуску.',
);
}
@@ -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'),
);
}
+7
View File
@@ -0,0 +1,7 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"
@@ -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"))
}
+42
View File
@@ -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
+29
View File
@@ -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
@@ -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 = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
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 = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
/* 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 = "<group>";
};
33BA886A226E78AF003329D5 /* Configs */ = {
isa = PBXGroup;
children = (
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
);
path = Configs;
sourceTree = "<group>";
};
33CC10E42044A3C60003C045 = {
isa = PBXGroup;
children = (
33FAB671232836740065AC1E /* Runner */,
33CEB47122A05771004F2AC0 /* Flutter */,
331C80D6294CF71000263BE5 /* RunnerTests */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
542EB08614890ADE7265109E /* Pods */,
);
sourceTree = "<group>";
};
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
33CC10ED2044A3C60003C045 /* widgetbook.app */,
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
33CC11242044D66E0003C045 /* Resources */ = {
isa = PBXGroup;
children = (
33CC10F22044A3C60003C045 /* Assets.xcassets */,
33CC10F42044A3C60003C045 /* MainMenu.xib */,
33CC10F72044A3C60003C045 /* Info.plist */,
);
name = Resources;
path = ..;
sourceTree = "<group>";
};
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 = "<group>";
};
33FAB671232836740065AC1E /* Runner */ = {
isa = PBXGroup;
children = (
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
33E51914231749380026EE4D /* Release.entitlements */,
33CC11242044D66E0003C045 /* Resources */,
33BA886A226E78AF003329D5 /* Configs */,
);
path = Runner;
sourceTree = "<group>";
};
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 = "<group>";
};
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
1FF1E9F75AAD4A9DAA6A7192 /* Pods_Runner.framework */,
AF66BD0165F006AA79AB2869 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* 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 = "<group>";
};
/* 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 */;
}
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "&quot;$FLUTTER_ROOT&quot;/packages/flutter_tools/bin/macos_assemble.sh prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "widgetbook.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "widgetbook.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "widgetbook.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C80D4294CF70F00263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "widgetbook.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "widgetbook.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
+13
View File
@@ -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
}
}
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1,343 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
<connections>
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
</connections>
</customObject>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="APP_NAME" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About APP_NAME" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
<menuItem title="Services" id="NMo-om-nkz">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
<connections>
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
</connections>
</menuItem>
<menuItem title="Show All" id="Kd2-mp-pUS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Edit" id="5QF-Oa-p0T">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
<items>
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
<connections>
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
<connections>
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
<connections>
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
<connections>
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
<connections>
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
</connections>
</menuItem>
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
</connections>
</menuItem>
<menuItem title="Delete" id="pa3-QI-u2k">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
<connections>
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
<menuItem title="Find" id="4EN-yA-p0u">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Find" id="1b7-l0-nxx">
<items>
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
<connections>
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
</connections>
</menuItem>
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
</connections>
</menuItem>
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
<connections>
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
</connections>
</menuItem>
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
<connections>
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
</connections>
</menuItem>
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
<connections>
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
</connections>
</menuItem>
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
<connections>
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
<items>
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
<connections>
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
</connections>
</menuItem>
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
<connections>
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
</connections>
</menuItem>
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
</connections>
</menuItem>
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Substitutions" id="9ic-FL-obx">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
<items>
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
</connections>
</menuItem>
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
</connections>
</menuItem>
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
</connections>
</menuItem>
<menuItem title="Smart Links" id="cwL-P1-jid">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
</connections>
</menuItem>
<menuItem title="Data Detectors" id="tRr-pd-1PS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
</connections>
</menuItem>
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Transformations" id="2oI-Rn-ZJC">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
<items>
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
</connections>
</menuItem>
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
</connections>
</menuItem>
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Speech" id="xrE-MZ-jX0">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
<items>
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
</connections>
</menuItem>
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="View" id="H8h-7b-M4v">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="View" id="HyV-fh-RgO">
<items>
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Window" id="aUF-d1-5bR">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
<items>
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
<connections>
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
</connections>
</menuItem>
<menuItem title="Zoom" id="R4o-n2-Eq4">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="EPT-qC-fAb">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="rJ0-wn-3NY"/>
</menuItem>
</items>
<point key="canvasLocation" x="142" y="-258"/>
</menu>
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</window>
</objects>
</document>
@@ -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.
@@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Debug.xcconfig"
#include "Warnings.xcconfig"
@@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Release.xcconfig"
#include "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
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
+32
View File
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
@@ -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()
}
}
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
</dict>
</plist>
@@ -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.
}
}
File diff suppressed because it is too large Load Diff
+25
View File
@@ -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
+17
View File
@@ -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/
+108
View File
@@ -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 "$<$<CONFIG:Debug>:_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 "$<TARGET_FILE_DIR:${BINARY_NAME}>")
# 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)
+109
View File
@@ -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} $<CONFIG>
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
)
@@ -0,0 +1,26 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <auto_updater_windows/auto_updater_windows_plugin_c_api.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <window_manager/window_manager_plugin.h>
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"));
}
@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter/plugin_registry.h>
// Registers Flutter plugins.
void RegisterPlugins(flutter::PluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_
@@ -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 $<TARGET_FILE:${plugin}_plugin>)
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)
+40
View File
@@ -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)
+121
View File
@@ -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
@@ -0,0 +1,71 @@
#include "flutter_window.h"
#include <optional>
#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<flutter::FlutterViewController>(
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<LRESULT> 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);
}

Some files were not shown because too many files have changed in this diff Show More