mirror of
https://github.com/MAKS-IT-COM/uscheduler.git
synced 2026-09-29 20:48:09 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92b8746971 |
15
CHANGELOG.md
15
CHANGELOG.md
@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [1.2.0] - 2026-09-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Windows setup exe offers **Standard** or **Portable** install. Portable keeps binaries, scripts, logs, and settings in the selected folder (`Scripts`, `Logs`, and `Data` subfolders). `--prepare-data --portable` does the same from the CLI; the portable zip includes a `portable` marker so unpacked copies stay self-contained.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Relicensed the project from MIT to **Apache 2.0**.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Linux Flatpak GNOME app icon uses X11/XWayland (`UsePlatformDetect` only). Avalonia 12.1.2 native Wayland still hangs on GNOME `xdg_toplevel.configure(0, 0)`. App id is lowercase `com.maks_it.uscheduler`. Uninstall the old `com.maks_it.UScheduler` id before installing a new bundle. AppStream lives in `data/`.
|
||||||
|
- Flatpak AppStream version is the same shared release version as zip and MSI (`DotNetReleaseVersion` from the UI csproj / `Directory.Build.props`), so `flatpak info` matches the bundle file name.
|
||||||
|
|
||||||
## [1.1.1] - 2026-09-06
|
## [1.1.1] - 2026-09-06
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@ -173,4 +173,8 @@ Follow [Keep a Changelog](https://keepachangelog.com/) format:
|
|||||||
- Bug fixes
|
- Bug fixes
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
By contributing, you agree that your contributions are licensed under the terms in [LICENSE.md](LICENSE.md) (Apache 2.0).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
214
LICENSE.md
214
LICENSE.md
@ -1,21 +1,201 @@
|
|||||||
MIT License
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
Copyright (c) 2017 - 2026 Maksym Sadovnychyy (Maks-IT)
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
1. Definitions.
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
copies or substantial portions of the Software.
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
the copyright owner that is granting the License.
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
other entities that control, are controlled by, or are under common
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
control with that entity. For the purposes of this definition,
|
||||||
SOFTWARE.
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2017-2026 Maksym Sadovnychyy (MAKS-IT)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|||||||
68
README.md
68
README.md
@ -4,7 +4,7 @@
|
|||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
A modern scheduler built on **.NET 10** for running PowerShell scripts and console applications on **Windows and Linux**.
|
A modern scheduler built on **.NET 10** for running PowerShell scripts and console applications on **Windows and Linux**.
|
||||||
@ -57,7 +57,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, commit format, and
|
|||||||
|
|
||||||
## Scripts Examples
|
## Scripts Examples
|
||||||
|
|
||||||
> **Note:** These examples are **bundled with the release** and copied to `C:\MaksIT\Scripts` by the Windows setup exe (and by `--install` / `--prepare-data`) **only if that folder or script is not already present**. Existing files and script folders are never overwritten or merged. They are listed in the default configuration but **disabled by default**. To enable an example, set `"Disabled": false` in `%ProgramData%\MaksIT\UScheduler\settings.json` (or use the UI).
|
> **Note:** These examples are **bundled with the release** and copied to `C:\MaksIT\Scripts` (standard) or the install folder's `Scripts` directory (portable) by the Windows setup exe (and by `--install` / `--prepare-data`) **only if that folder or script is not already present**. Existing files and script folders are never overwritten or merged. They are listed in the default configuration but **disabled by default**. To enable an example, set `"Disabled": false` in the shared `settings.json` (or use the UI).
|
||||||
|
|
||||||
- [Hyper-V Backup](./src/Scripts/HyperV-Backup/README.md) - Production-ready Hyper-V VM backup solution with scheduling and retention management
|
- [Hyper-V Backup](./src/Scripts/HyperV-Backup/README.md) - Production-ready Hyper-V VM backup solution with scheduling and retention management
|
||||||
- [Native-Sync](./src/Scripts/Native-Sync/README.md) - Production-ready file synchronization solution using pure PowerShell with no external dependencies
|
- [Native-Sync](./src/Scripts/Native-Sync/README.md) - Production-ready file synchronization solution using pure PowerShell with no external dependencies
|
||||||
@ -88,6 +88,10 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, commit format, and
|
|||||||
|
|
||||||
### Install layout
|
### Install layout
|
||||||
|
|
||||||
|
The Windows setup exe lets you choose **Standard** or **Portable**.
|
||||||
|
|
||||||
|
**Standard** (default):
|
||||||
|
|
||||||
| Location | Purpose | Who can write |
|
| Location | Purpose | Who can write |
|
||||||
|----------|---------|----------------|
|
|----------|---------|----------------|
|
||||||
| `C:\Program Files\MaksIT\UScheduler` | Worker (`MaksIT.UScheduler.exe`) and UI (`MaksIT.UScheduler.UI.exe`) | Administrators |
|
| `C:\Program Files\MaksIT\UScheduler` | Worker (`MaksIT.UScheduler.exe`) and UI (`MaksIT.UScheduler.UI.exe`) | Administrators |
|
||||||
@ -96,7 +100,19 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, commit format, and
|
|||||||
| `%ProgramData%\MaksIT\UScheduler\settings.json` | Shared schedule configuration | Users (after install) |
|
| `%ProgramData%\MaksIT\UScheduler\settings.json` | Shared schedule configuration | Users (after install) |
|
||||||
| `%AppData%\MaksIT\UScheduler\settings.json` | Per-user UI prefs (service bin path override) | Current user |
|
| `%AppData%\MaksIT\UScheduler\settings.json` | Per-user UI prefs (service bin path override) | Current user |
|
||||||
|
|
||||||
The Windows setup exe, registering the service, or `MaksIT.UScheduler --prepare-data` creates the `C:\MaksIT` and ProgramData folders, copies bundled example scripts when missing, and grants the Users group modify rights so the UI can stay unelevated.
|
**Portable** (everything in one folder):
|
||||||
|
|
||||||
|
| Location | Purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| *Install folder* (you choose it) | Worker and UI |
|
||||||
|
| *Install folder*`\Scripts` | Scheduled scripts |
|
||||||
|
| *Install folder*`\Logs` | Service and script logs |
|
||||||
|
| *Install folder*`\Data\settings.json` | Shared schedule configuration |
|
||||||
|
| *Install folder*`\Data\ui-settings.json` | UI prefs |
|
||||||
|
|
||||||
|
A `portable` file in that folder (or a parent folder) marks the layout. Pick a writable location such as `C:\MaksIT\UScheduler` if you do not want Program Files. The Users group is granted modify rights on `Scripts`, `Logs`, and `Data` so the UI can stay unelevated.
|
||||||
|
|
||||||
|
The Windows setup exe, registering the service, or `MaksIT.UScheduler --prepare-data` creates the data folders, copies bundled example scripts when missing, and grants the Users group modify rights so the UI can stay unelevated. Add `--portable` to keep data next to the binaries instead of `C:\MaksIT` and ProgramData.
|
||||||
|
|
||||||
### Using CLI Commands
|
### Using CLI Commands
|
||||||
|
|
||||||
@ -123,6 +139,9 @@ MaksIT.UScheduler --help
|
|||||||
|
|
||||||
# Create data folders and ACLs without installing the service
|
# Create data folders and ACLs without installing the service
|
||||||
MaksIT.UScheduler --prepare-data
|
MaksIT.UScheduler --prepare-data
|
||||||
|
|
||||||
|
# Portable layout: scripts, logs, and settings next to the executable
|
||||||
|
MaksIT.UScheduler --prepare-data --portable
|
||||||
```
|
```
|
||||||
|
|
||||||
On Windows the file is `MaksIT.UScheduler.exe`.
|
On Windows the file is `MaksIT.UScheduler.exe`.
|
||||||
@ -134,7 +153,8 @@ On Windows the file is `MaksIT.UScheduler.exe`.
|
|||||||
| `--start` | | Start the service |
|
| `--start` | | Start the service |
|
||||||
| `--stop` | | Stop the service |
|
| `--stop` | | Stop the service |
|
||||||
| `--status` | | Query service status |
|
| `--status` | | Query service status |
|
||||||
| `--prepare-data` | | Create `C:\MaksIT\Scripts`, `C:\MaksIT\Logs`, and shared settings (elevated) |
|
| `--prepare-data` | | Create scripts, logs, and shared settings (elevated) |
|
||||||
|
| `--portable` | | With `--install` / `--prepare-data`: keep data in the install folder |
|
||||||
| `--help` | `-h` | Show help message |
|
| `--help` | `-h` | Show help message |
|
||||||
|
|
||||||
> **Note:** Service management commands require administrator / root privileges.
|
> **Note:** Service management commands require administrator / root privileges.
|
||||||
@ -172,11 +192,35 @@ The UI is an **Avalonia** desktop app (Windows and Linux) for service registrati
|
|||||||
|
|
||||||
### Getting Started
|
### Getting Started
|
||||||
|
|
||||||
When you unpack the portable zip, launch `MaksIT.UScheduler.UI.exe`. GitHub releases also ship a Windows setup exe (installs worker + UI to `C:\Program Files\MaksIT\UScheduler`) and a Flatpak of the UI.
|
When you unpack the portable zip, launch `MaksIT.UScheduler.UI.exe` (or `Start-UScheduler.bat`). The zip is a portable layout: scripts, logs, and settings stay in the extracted folder. GitHub releases also ship a Windows setup exe (worker + UI; choose Standard or Portable on the install page) and a Flatpak of the UI.
|
||||||
|
|
||||||
|
#### Linux (Flatpak)
|
||||||
|
|
||||||
|
**User** (this account only):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flatpak install --user ./maksit-uscheduler-{version}.flatpak
|
||||||
|
flatpak run com.maks_it.uscheduler
|
||||||
|
```
|
||||||
|
|
||||||
|
**System** (all users):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo flatpak install --system ./maksit-uscheduler-{version}.flatpak
|
||||||
|
flatpak run com.maks_it.uscheduler
|
||||||
|
```
|
||||||
|
|
||||||
|
Uninstall: `flatpak uninstall --user com.maks_it.uscheduler` or `sudo flatpak uninstall --system com.maks_it.uscheduler`.
|
||||||
|
|
||||||
|
The previous id `com.maks_it.UScheduler` is replaced by this lowercase id. Uninstall the old app before installing the new bundle if it was installed.
|
||||||
|
|
||||||
|
If GNOME or KDE does not show a launcher icon, `flatpak run` may warn that `/var/lib/flatpak/exports/share` and `~/.local/share/flatpak/exports/share` are not on `XDG_DATA_DIRS`. Log out and back in once so the session picks up those paths.
|
||||||
|
|
||||||
|
Linux uses X11/XWayland (Avalonia native Wayland still hangs on GNOME). The sandbox grants `--filesystem=home` for scripts and logs. The Flatpak is the UI; the worker is a separate Linux systemd install. AppStream and the desktop file live in [`data/`](data/).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
> **Note:** Service management (register, start, stop, unregister) prompts for administrator approval without restarting the UI. Schedule edits go to `%ProgramData%\MaksIT\UScheduler\settings.json` and do not require elevation after the first install.
|
> **Note:** Service management (register, start, stop, unregister) prompts for administrator approval without restarting the UI. Schedule edits go to `%ProgramData%\MaksIT\UScheduler\settings.json` on a standard install (or `Data\settings.json` next to a portable install) and do not require elevation after the first install.
|
||||||
|
|
||||||
### Settings View
|
### Settings View
|
||||||
|
|
||||||
@ -186,12 +230,12 @@ The Settings view is your starting point for configuring UScheduler.
|
|||||||
|
|
||||||
| Feature | Description |
|
| Feature | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| **Service Bin Path** | Path to the worker folder (auto-detected from Program Files or the UI directory; override stored in `%AppData%/MaksIT/UScheduler/settings.json`) |
|
| **Service Bin Path** | Path to the worker folder (auto-detected from Program Files, the UI directory, or a portable folder; override stored in `%AppData%/MaksIT/UScheduler/settings.json` or `Data\ui-settings.json` when portable) |
|
||||||
| **Service Status** | Real-time status indicator (Running, Stopped, Starting, Stopping, Paused, Not Installed) |
|
| **Service Status** | Real-time status indicator (Running, Stopped, Starting, Stopping, Paused, Not Installed) |
|
||||||
| **Register/Unregister** | Install or remove the Windows service or systemd unit (UAC / polkit prompt, UI stays open) |
|
| **Register/Unregister** | Install or remove the Windows service or systemd unit (UAC / polkit prompt, UI stays open) |
|
||||||
| **Start/Stop** | Control the service state (same in-app elevation) |
|
| **Start/Stop** | Control the service state (same in-app elevation) |
|
||||||
| **Refresh** | Update the current service status display |
|
| **Refresh** | Update the current service status display |
|
||||||
| **Reload Settings** | Refresh shared configuration from `%ProgramData%\MaksIT\UScheduler\settings.json` |
|
| **Reload Settings** | Refresh shared configuration from `%ProgramData%\MaksIT\UScheduler\settings.json` (or `Data\settings.json` when portable) |
|
||||||
|
|
||||||
### Main View — Schedule Management
|
### Main View — Schedule Management
|
||||||
|
|
||||||
@ -262,9 +306,9 @@ Features:
|
|||||||
|
|
||||||
### Machine-wide `settings.json`
|
### Machine-wide `settings.json`
|
||||||
|
|
||||||
Host logging (log levels, Event Log source) stays in shipped `appsettings.json` next to `MaksIT.UScheduler.exe` under Program Files. Schedule configuration is **not** written there — a leftover `Configuration` block is copied once into the machine-wide file:
|
Host logging (log levels, Event Log source) stays in shipped `appsettings.json` next to `MaksIT.UScheduler.exe`. Schedule configuration is **not** written there — a leftover `Configuration` block is copied once into the machine-wide file:
|
||||||
|
|
||||||
`%ProgramData%\MaksIT\UScheduler\settings.json`
|
`%ProgramData%\MaksIT\UScheduler\settings.json` (standard) or `{install folder}\Data\settings.json` (portable)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -438,7 +482,7 @@ Invoke-ScheduledExecution -Config $Config -Automated:$Automated -CurrentDateTime
|
|||||||
1. Copy template
|
1. Copy template
|
||||||
2. Modify `$Config`
|
2. Modify `$Config`
|
||||||
3. Implement `Start-BusinessLogic`
|
3. Implement `Start-BusinessLogic`
|
||||||
4. Add script to `%ProgramData%\MaksIT\UScheduler\settings.json` (or use the UI)
|
4. Add script to the shared `settings.json` (ProgramData on a standard install, or `Data\settings.json` when portable — or use the UI)
|
||||||
|
|
||||||
That’s it — the full scheduling engine is reused automatically.
|
That’s it — the full scheduling engine is reused automatically.
|
||||||
|
|
||||||
@ -515,6 +559,6 @@ Email: maksym.sadovnychyy@gmail.com
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the MIT License. See [LICENSE.md](LICENSE.md) for details.
|
Apache 2.0 — see [LICENSE.md](LICENSE.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
12
data/com.maks_it.uscheduler.desktop
Normal file
12
data/com.maks_it.uscheduler.desktop
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=UScheduler
|
||||||
|
GenericName=Job scheduler
|
||||||
|
Comment=Schedule PowerShell scripts and processes
|
||||||
|
Exec=maksit-uscheduler
|
||||||
|
Icon=com.maks_it.uscheduler
|
||||||
|
StartupWMClass=com.maks_it.uscheduler
|
||||||
|
StartupNotify=true
|
||||||
|
Terminal=false
|
||||||
|
Categories=Utility;System;
|
||||||
|
Keywords=scheduler;powershell;cron;jobs;
|
||||||
49
data/com.maks_it.uscheduler.metainfo.xml
Normal file
49
data/com.maks_it.uscheduler.metainfo.xml
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<component type="desktop-application">
|
||||||
|
<id>com.maks_it.uscheduler</id>
|
||||||
|
<name>UScheduler</name>
|
||||||
|
<summary>Schedule PowerShell scripts and processes</summary>
|
||||||
|
<developer id="com.maks_it">
|
||||||
|
<name>MaksIT</name>
|
||||||
|
</developer>
|
||||||
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
|
<project_license>Apache-2.0</project_license>
|
||||||
|
<description>
|
||||||
|
<p>Schedule and run PowerShell scripts and console processes from an Avalonia desktop UI. Register the worker, edit schedules, and follow logs without a browser.</p>
|
||||||
|
<p>The Flatpak is the UI. Script files and logs live under the home folder. The worker is a separate install on Linux (systemd) or Windows (service).</p>
|
||||||
|
<ul>
|
||||||
|
<li>Script list, schedules, and log viewing</li>
|
||||||
|
<li>Register, start, and stop the worker (prompts for elevation)</li>
|
||||||
|
<li>Windows and Linux script compatibility flags</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
<launchable type="desktop-id">com.maks_it.uscheduler.desktop</launchable>
|
||||||
|
<provides>
|
||||||
|
<id>com.maks_it.UScheduler</id>
|
||||||
|
</provides>
|
||||||
|
<replaces>
|
||||||
|
<id>com.maks_it.UScheduler</id>
|
||||||
|
</replaces>
|
||||||
|
<url type="homepage">https://maks-it.com/</url>
|
||||||
|
<url type="bugtracker">https://github.com/MAKS-IT-COM/uscheduler/issues</url>
|
||||||
|
<url type="vcs-browser">https://github.com/MAKS-IT-COM/uscheduler</url>
|
||||||
|
<url type="donation">https://www.buymeacoffee.com/maksitcom</url>
|
||||||
|
<branding>
|
||||||
|
<color type="primary" scheme_preference="light">#33A5CF</color>
|
||||||
|
<color type="primary" scheme_preference="dark">#003D7A</color>
|
||||||
|
</branding>
|
||||||
|
<content_rating type="oars-1.1"/>
|
||||||
|
<requires>
|
||||||
|
<control>keyboard</control>
|
||||||
|
<control>pointing</control>
|
||||||
|
<display_length compare="ge">768</display_length>
|
||||||
|
</requires>
|
||||||
|
<releases>
|
||||||
|
<release version="1.2.0" date="2026-09-21">
|
||||||
|
<url type="details">https://github.com/MAKS-IT-COM/uscheduler/blob/v1.2.0/CHANGELOG.md</url>
|
||||||
|
<description>
|
||||||
|
<p>Linux Flatpak uses X11/XWayland from the GNOME app icon and a lowercase app id.</p>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
</releases>
|
||||||
|
</component>
|
||||||
@ -62,7 +62,8 @@ public class ProcessConfiguration : TaskConfiguration {
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Root configuration class for the UScheduler service.
|
/// Root configuration class for the UScheduler service.
|
||||||
/// Stored in machine-wide settings.json (ProgramData), not next to the exe.
|
/// Stored in machine-wide settings.json (ProgramData), or under the install
|
||||||
|
/// folder when a portable layout is used.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Configuration {
|
public class Configuration {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -103,12 +104,17 @@ public class Configuration {
|
|||||||
Processes ??= [];
|
Processes ??= [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetEffectiveScriptsDirectory() =>
|
public string GetEffectiveScriptsDirectory(string? baseDirectory = null) =>
|
||||||
HostPaths.ResolveScriptsDirectory(ScriptsDir);
|
HostPaths.ResolveScriptsDirectory(ScriptsDir, baseDirectory);
|
||||||
|
|
||||||
public string GetEffectiveLogDirectory(string? baseDirectory = null) {
|
public string GetEffectiveLogDirectory(string? baseDirectory = null) {
|
||||||
var logDir = string.IsNullOrWhiteSpace(LogDir) ? HostPaths.DefaultLogDirectory : LogDir;
|
var start = baseDirectory ?? AppContext.BaseDirectory;
|
||||||
return PathHelper.ResolvePath(logDir, baseDirectory ?? AppContext.BaseDirectory);
|
if (string.IsNullOrWhiteSpace(LogDir)) {
|
||||||
|
var root = HostPaths.FindPortableRoot(start);
|
||||||
|
return root is null ? HostPaths.DefaultLogDirectory : HostPaths.GetPortableLogDirectory(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PathHelper.ResolvePath(LogDir, start);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ResolveScriptPath(string path) =>
|
public string ResolveScriptPath(string path) =>
|
||||||
|
|||||||
@ -6,8 +6,10 @@ namespace MaksIT.UScheduler.Shared;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads and saves machine-wide <c>Configuration</c> under ProgramData
|
/// Loads and saves machine-wide <c>Configuration</c> under ProgramData
|
||||||
/// (all-users AppData). Shipped <c>appsettings.json</c> next to the worker is
|
/// (all-users AppData), or under the portable Data folder when a
|
||||||
/// seed-only; host logging stays in that file and is never copied.
|
/// <see cref="HostPaths.PortableMarkerFileName"/> file is present.
|
||||||
|
/// Shipped <c>appsettings.json</c> next to the worker is seed-only; host
|
||||||
|
/// logging stays in that file and is never copied.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ConfigurationFileService {
|
public sealed class ConfigurationFileService {
|
||||||
public const string ProductFolder = HostPaths.ProductFolder;
|
public const string ProductFolder = HostPaths.ProductFolder;
|
||||||
@ -27,7 +29,7 @@ public sealed class ConfigurationFileService {
|
|||||||
|
|
||||||
public ConfigurationFileService(string? configurationPath = null, string? seedPath = null) {
|
public ConfigurationFileService(string? configurationPath = null, string? seedPath = null) {
|
||||||
FilePath = string.IsNullOrWhiteSpace(configurationPath)
|
FilePath = string.IsNullOrWhiteSpace(configurationPath)
|
||||||
? HostPaths.SharedSettingsFile
|
? HostPaths.ResolveSharedSettingsFile()
|
||||||
: configurationPath;
|
: configurationPath;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(seedPath))
|
if (!string.IsNullOrWhiteSpace(seedPath))
|
||||||
@ -93,6 +95,7 @@ public sealed class ConfigurationFileService {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
HostPaths.ApplyPortableDefaults(_current, Path.GetDirectoryName(FilePath));
|
||||||
Save(_current);
|
Save(_current);
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException) {
|
catch (UnauthorizedAccessException) {
|
||||||
|
|||||||
@ -4,39 +4,88 @@ using System.Runtime.Versioning;
|
|||||||
|
|
||||||
namespace MaksIT.UScheduler.Shared;
|
namespace MaksIT.UScheduler.Shared;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scripts, logs, and shared-settings locations for one install.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct HostDataLayout(
|
||||||
|
string Root,
|
||||||
|
string ScriptsDirectory,
|
||||||
|
string LogDirectory,
|
||||||
|
string SharedSettingsFile,
|
||||||
|
bool Portable);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates machine-wide data folders (scripts, logs, shared settings) and
|
/// Creates machine-wide data folders (scripts, logs, shared settings) and
|
||||||
/// grants the Users group modify rights so the UI can run without elevation.
|
/// grants the Users group modify rights so the UI can run without elevation.
|
||||||
/// Call from an elevated <c>--install</c> / <c>--prepare-data</c> process.
|
/// Call from an elevated <c>--install</c> / <c>--prepare-data</c> process.
|
||||||
|
/// Pass <c>--portable</c> (or drop a <see cref="HostPaths.PortableMarkerFileName"/>
|
||||||
|
/// file) to keep scripts, logs, and settings under the install folder.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class HostDataDirectories {
|
public static class HostDataDirectories {
|
||||||
/// <summary>Windows Built-in Users (locale-independent).</summary>
|
/// <summary>Windows Built-in Users (locale-independent).</summary>
|
||||||
private const string BuiltinUsersSid = "*S-1-5-32-545";
|
private const string BuiltinUsersSid = "*S-1-5-32-545";
|
||||||
|
|
||||||
public static HostServiceOperationResult Prepare(string? installDirectory = null) {
|
public static HostDataLayout ResolveLayout(string? installDirectory = null, bool portableRequested = false) {
|
||||||
|
var installDir = string.IsNullOrWhiteSpace(installDirectory)
|
||||||
|
? AppContext.BaseDirectory
|
||||||
|
: Path.GetFullPath(installDirectory);
|
||||||
|
|
||||||
|
var portableRoot = HostPaths.FindPortableRoot(installDir);
|
||||||
|
var portable = portableRequested || portableRoot is not null;
|
||||||
|
if (!portable) {
|
||||||
|
return new HostDataLayout(
|
||||||
|
HostPaths.DefaultDataRoot,
|
||||||
|
HostPaths.DefaultScriptsDirectory,
|
||||||
|
HostPaths.DefaultLogDirectory,
|
||||||
|
HostPaths.SharedSettingsFile,
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var root = portableRoot ?? installDir;
|
||||||
|
return new HostDataLayout(
|
||||||
|
root,
|
||||||
|
HostPaths.GetPortableScriptsDirectory(root),
|
||||||
|
HostPaths.GetPortableLogDirectory(root),
|
||||||
|
HostPaths.GetPortableSharedSettingsFile(root),
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HostServiceOperationResult Prepare(string? installDirectory = null, bool portable = false) {
|
||||||
var installDir = string.IsNullOrWhiteSpace(installDirectory)
|
var installDir = string.IsNullOrWhiteSpace(installDirectory)
|
||||||
? AppContext.BaseDirectory
|
? AppContext.BaseDirectory
|
||||||
: installDirectory;
|
: installDirectory;
|
||||||
var messages = new List<string>();
|
var messages = new List<string>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Directory.CreateDirectory(HostPaths.DefaultDataRoot);
|
if (portable)
|
||||||
Directory.CreateDirectory(HostPaths.DefaultScriptsDirectory);
|
HostPaths.WritePortableMarker(Path.GetFullPath(installDir));
|
||||||
Directory.CreateDirectory(HostPaths.DefaultLogDirectory);
|
|
||||||
|
|
||||||
var settingsDir = Path.GetDirectoryName(HostPaths.SharedSettingsFile);
|
var layout = ResolveLayout(installDir, portable);
|
||||||
|
Directory.CreateDirectory(layout.Root);
|
||||||
|
Directory.CreateDirectory(layout.ScriptsDirectory);
|
||||||
|
Directory.CreateDirectory(layout.LogDirectory);
|
||||||
|
|
||||||
|
var settingsDir = Path.GetDirectoryName(layout.SharedSettingsFile);
|
||||||
if (!string.IsNullOrEmpty(settingsDir))
|
if (!string.IsNullOrEmpty(settingsDir))
|
||||||
Directory.CreateDirectory(settingsDir);
|
Directory.CreateDirectory(settingsDir);
|
||||||
|
|
||||||
if (OperatingSystem.IsWindows()) {
|
if (OperatingSystem.IsWindows()) {
|
||||||
GrantUsersModify(HostPaths.DefaultDataRoot, messages);
|
if (layout.Portable) {
|
||||||
if (!string.IsNullOrEmpty(settingsDir))
|
GrantUsersModify(layout.ScriptsDirectory, messages);
|
||||||
GrantUsersModify(settingsDir, messages);
|
GrantUsersModify(layout.LogDirectory, messages);
|
||||||
|
if (!string.IsNullOrEmpty(settingsDir))
|
||||||
|
GrantUsersModify(settingsDir, messages);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
GrantUsersModify(layout.Root, messages);
|
||||||
|
if (!string.IsNullOrEmpty(settingsDir))
|
||||||
|
GrantUsersModify(settingsDir, messages);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) {
|
else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) {
|
||||||
GrantUnixGroupWrite(HostPaths.DefaultDataRoot, messages);
|
GrantUnixGroupWrite(layout.Root, messages);
|
||||||
GrantUnixGroupWrite(HostPaths.DefaultScriptsDirectory, messages);
|
GrantUnixGroupWrite(layout.ScriptsDirectory, messages);
|
||||||
GrantUnixGroupWrite(HostPaths.DefaultLogDirectory, messages);
|
GrantUnixGroupWrite(layout.LogDirectory, messages);
|
||||||
if (!string.IsNullOrEmpty(settingsDir))
|
if (!string.IsNullOrEmpty(settingsDir))
|
||||||
GrantUnixGroupWrite(settingsDir, messages);
|
GrantUnixGroupWrite(settingsDir, messages);
|
||||||
}
|
}
|
||||||
@ -44,23 +93,26 @@ public static class HostDataDirectories {
|
|||||||
var source = HostPaths.FindBundledScriptsDirectory(installDir);
|
var source = HostPaths.FindBundledScriptsDirectory(installDir);
|
||||||
if (source is null || !Directory.Exists(source)) {
|
if (source is null || !Directory.Exists(source)) {
|
||||||
messages.Add(
|
messages.Add(
|
||||||
$"Bundled scripts were not found next to '{installDir}'; left {HostPaths.DefaultScriptsDirectory} unchanged.");
|
$"Bundled scripts were not found next to '{installDir}'; left {layout.ScriptsDirectory} unchanged.");
|
||||||
}
|
}
|
||||||
else if (PathsEqual(source, HostPaths.DefaultScriptsDirectory)) {
|
else if (PathsEqual(source, layout.ScriptsDirectory)) {
|
||||||
messages.Add($"Scripts directory: {HostPaths.DefaultScriptsDirectory}");
|
messages.Add($"Scripts directory: {layout.ScriptsDirectory}");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var copy = CopySeedScriptsIfMissing(source, HostPaths.DefaultScriptsDirectory);
|
var copy = CopySeedScriptsIfMissing(source, layout.ScriptsDirectory);
|
||||||
if (copy.CopiedFiles > 0)
|
if (copy.CopiedFiles > 0)
|
||||||
messages.Add($"Copied {copy.CopiedFiles} new seed script file(s) to {HostPaths.DefaultScriptsDirectory}.");
|
messages.Add($"Copied {copy.CopiedFiles} new seed script file(s) to {layout.ScriptsDirectory}.");
|
||||||
if (copy.SkippedItems > 0)
|
if (copy.SkippedItems > 0)
|
||||||
messages.Add($"Left {copy.SkippedItems} existing script folder(s)/file(s) unchanged in {HostPaths.DefaultScriptsDirectory}.");
|
messages.Add($"Left {copy.SkippedItems} existing script folder(s)/file(s) unchanged in {layout.ScriptsDirectory}.");
|
||||||
if (copy.CopiedFiles == 0 && copy.SkippedItems == 0)
|
if (copy.CopiedFiles == 0 && copy.SkippedItems == 0)
|
||||||
messages.Add($"Scripts directory: {HostPaths.DefaultScriptsDirectory}");
|
messages.Add($"Scripts directory: {layout.ScriptsDirectory}");
|
||||||
}
|
}
|
||||||
|
|
||||||
messages.Add($"Logs directory: {HostPaths.DefaultLogDirectory}");
|
WritePortableSettingsIfNeeded(layout, installDir);
|
||||||
messages.Add($"Shared settings: {HostPaths.SharedSettingsFile}");
|
messages.Add($"Logs directory: {layout.LogDirectory}");
|
||||||
|
messages.Add($"Shared settings: {layout.SharedSettingsFile}");
|
||||||
|
if (layout.Portable)
|
||||||
|
messages.Add("Install type: portable (everything under the install folder).");
|
||||||
return new HostServiceOperationResult(true, string.Join(Environment.NewLine, messages));
|
return new HostServiceOperationResult(true, string.Join(Environment.NewLine, messages));
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
@ -102,6 +154,16 @@ public static class HostDataDirectories {
|
|||||||
return new SeedCopyResult(copied, skipped);
|
return new SeedCopyResult(copied, skipped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void WritePortableSettingsIfNeeded(HostDataLayout layout, string installDir) {
|
||||||
|
if (!layout.Portable)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var seed = Path.Combine(installDir, ConfigurationFileService.SeedFileName);
|
||||||
|
_ = new ConfigurationFileService(
|
||||||
|
layout.SharedSettingsFile,
|
||||||
|
File.Exists(seed) ? seed : null);
|
||||||
|
}
|
||||||
|
|
||||||
private static int CopyNewDirectory(string source, string destination) {
|
private static int CopyNewDirectory(string source, string destination) {
|
||||||
Directory.CreateDirectory(destination);
|
Directory.CreateDirectory(destination);
|
||||||
var copied = 0;
|
var copied = 0;
|
||||||
@ -146,10 +208,7 @@ public static class HostDataDirectories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static bool PathsEqual(string left, string right) =>
|
private static bool PathsEqual(string left, string right) =>
|
||||||
string.Equals(
|
HostPaths.PathsEqual(left, right);
|
||||||
Path.GetFullPath(left).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
|
|
||||||
Path.GetFullPath(right).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
|
|
||||||
OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
|
||||||
|
|
||||||
private static (int ExitCode, string Message) RunProcess(string fileName, string arguments) {
|
private static (int ExitCode, string Message) RunProcess(string fileName, string arguments) {
|
||||||
using var process = Process.Start(new ProcessStartInfo {
|
using var process = Process.Start(new ProcessStartInfo {
|
||||||
|
|||||||
@ -4,13 +4,21 @@ using MaksIT.UScheduler.Shared.Helpers;
|
|||||||
namespace MaksIT.UScheduler.Shared;
|
namespace MaksIT.UScheduler.Shared;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default install and data locations. Binaries live under Program Files;
|
/// Default install and data locations. Standard layout keeps binaries under
|
||||||
/// scripts, logs, and machine settings are outside Program Files so normal
|
/// Program Files and scripts, logs, and machine settings outside Program Files
|
||||||
/// users can read and write them after <c>--prepare-data</c> / <c>--install</c>.
|
/// so normal users can read and write them after <c>--prepare-data</c> /
|
||||||
|
/// <c>--install</c>. A <see cref="PortableMarkerFileName"/> file next to the
|
||||||
|
/// worker (or in a parent folder) switches to a portable layout: scripts, logs,
|
||||||
|
/// and settings stay under that folder.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class HostPaths {
|
public static class HostPaths {
|
||||||
public const string ProductFolder = "UScheduler";
|
public const string ProductFolder = "UScheduler";
|
||||||
public const string Manufacturer = UserSettingsPath.Manufacturer;
|
public const string Manufacturer = UserSettingsPath.Manufacturer;
|
||||||
|
public const string PortableMarkerFileName = "portable";
|
||||||
|
public const string PortableScriptsFolderName = "Scripts";
|
||||||
|
public const string PortableLogsFolderName = "Logs";
|
||||||
|
public const string PortableDataFolderName = "Data";
|
||||||
|
public const string PortableMarkerContent = "MaksIT.UScheduler portable layout";
|
||||||
|
|
||||||
public static string SharedSettingsFile =>
|
public static string SharedSettingsFile =>
|
||||||
UserSettingsPath.GetShared(ProductFolder);
|
UserSettingsPath.GetShared(ProductFolder);
|
||||||
@ -53,21 +61,111 @@ public static class HostPaths {
|
|||||||
public static string DefaultDataRoot =>
|
public static string DefaultDataRoot =>
|
||||||
OperatingSystem.IsWindows() ? @"C:\MaksIT" : "/var/lib/maksit";
|
OperatingSystem.IsWindows() ? @"C:\MaksIT" : "/var/lib/maksit";
|
||||||
|
|
||||||
|
public static bool IsPortableLayout(string? startDirectory = null) =>
|
||||||
|
FindPortableRoot(startDirectory) is not null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Scripts folder to use at runtime: configured path if it exists, then
|
/// Directory that contains <see cref="PortableMarkerFileName"/>, walking up
|
||||||
/// <see cref="DefaultScriptsDirectory"/>, then a bundled/dev tree.
|
/// from <paramref name="startDirectory"/> (or the current app base).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string ResolveScriptsDirectory(string? configured = null) {
|
public static string? FindPortableRoot(string? startDirectory = null) {
|
||||||
|
var current = startDirectory ?? AppContext.BaseDirectory;
|
||||||
|
if (string.IsNullOrWhiteSpace(current))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
current = Path.GetFullPath(current);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(current))
|
||||||
|
current = Path.GetDirectoryName(current) ?? current;
|
||||||
|
|
||||||
|
for (var i = 0; i < 10 && !string.IsNullOrEmpty(current); i++) {
|
||||||
|
if (File.Exists(Path.Combine(current, PortableMarkerFileName)))
|
||||||
|
return current;
|
||||||
|
|
||||||
|
current = Directory.GetParent(current)?.FullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetPortableScriptsDirectory(string portableRoot) =>
|
||||||
|
Path.Combine(portableRoot, PortableScriptsFolderName);
|
||||||
|
|
||||||
|
public static string GetPortableLogDirectory(string portableRoot) =>
|
||||||
|
Path.Combine(portableRoot, PortableLogsFolderName);
|
||||||
|
|
||||||
|
public static string GetPortableDataDirectory(string portableRoot) =>
|
||||||
|
Path.Combine(portableRoot, PortableDataFolderName);
|
||||||
|
|
||||||
|
public static string GetPortableSharedSettingsFile(string portableRoot) =>
|
||||||
|
Path.Combine(GetPortableDataDirectory(portableRoot), "settings.json");
|
||||||
|
|
||||||
|
public static string GetPortableUiSettingsFile(string portableRoot) =>
|
||||||
|
Path.Combine(GetPortableDataDirectory(portableRoot), "ui-settings.json");
|
||||||
|
|
||||||
|
public static string ResolveSharedSettingsFile(string? startDirectory = null) {
|
||||||
|
var root = FindPortableRoot(startDirectory);
|
||||||
|
return root is null ? SharedSettingsFile : GetPortableSharedSettingsFile(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ResolveUiSettingsFile(string? startDirectory = null) {
|
||||||
|
var root = FindPortableRoot(startDirectory);
|
||||||
|
return root is null ? UserSettingsPath.Get(ProductFolder) : GetPortableUiSettingsFile(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void WritePortableMarker(string portableRoot) {
|
||||||
|
Directory.CreateDirectory(portableRoot);
|
||||||
|
var path = Path.Combine(portableRoot, PortableMarkerFileName);
|
||||||
|
if (!File.Exists(path))
|
||||||
|
File.WriteAllText(path, PortableMarkerContent + Environment.NewLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When a portable root is present, replace machine-wide default script/log
|
||||||
|
/// paths with folders under that root. Custom paths are left unchanged.
|
||||||
|
/// </summary>
|
||||||
|
public static void ApplyPortableDefaults(Configuration configuration, string? startDirectory = null) {
|
||||||
|
ArgumentNullException.ThrowIfNull(configuration);
|
||||||
|
var root = FindPortableRoot(startDirectory);
|
||||||
|
if (root is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(configuration.LogDir) || PathsEqual(configuration.LogDir, DefaultLogDirectory))
|
||||||
|
configuration.LogDir = GetPortableLogDirectory(root);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(configuration.ScriptsDir) || PathsEqual(configuration.ScriptsDir, DefaultScriptsDirectory))
|
||||||
|
configuration.ScriptsDir = GetPortableScriptsDirectory(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scripts folder to use at runtime: configured path if it exists, then a
|
||||||
|
/// portable <c>Scripts</c> folder, then <see cref="DefaultScriptsDirectory"/>,
|
||||||
|
/// then a bundled/dev tree.
|
||||||
|
/// </summary>
|
||||||
|
public static string ResolveScriptsDirectory(string? configured = null, string? startDirectory = null) {
|
||||||
|
var start = startDirectory ?? AppContext.BaseDirectory;
|
||||||
if (!string.IsNullOrWhiteSpace(configured)) {
|
if (!string.IsNullOrWhiteSpace(configured)) {
|
||||||
var resolved = PathHelper.ResolvePath(configured);
|
var resolved = PathHelper.ResolvePath(configured, start);
|
||||||
if (Directory.Exists(resolved))
|
if (Directory.Exists(resolved))
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var portableRoot = FindPortableRoot(start);
|
||||||
|
if (portableRoot is not null) {
|
||||||
|
var portableScripts = GetPortableScriptsDirectory(portableRoot);
|
||||||
|
if (Directory.Exists(portableScripts))
|
||||||
|
return portableScripts;
|
||||||
|
}
|
||||||
|
|
||||||
if (Directory.Exists(DefaultScriptsDirectory))
|
if (Directory.Exists(DefaultScriptsDirectory))
|
||||||
return DefaultScriptsDirectory;
|
return DefaultScriptsDirectory;
|
||||||
|
|
||||||
var bundled = FindBundledScriptsDirectory();
|
var bundled = FindBundledScriptsDirectory(start);
|
||||||
return bundled ?? DefaultScriptsDirectory;
|
return bundled ?? DefaultScriptsDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,8 +173,8 @@ public static class HostPaths {
|
|||||||
var current = startDirectory ?? AppContext.BaseDirectory;
|
var current = startDirectory ?? AppContext.BaseDirectory;
|
||||||
for (var i = 0; i < 10 && !string.IsNullOrEmpty(current); i++) {
|
for (var i = 0; i < 10 && !string.IsNullOrEmpty(current); i++) {
|
||||||
foreach (var candidate in new[] {
|
foreach (var candidate in new[] {
|
||||||
Path.Combine(current, "Scripts"),
|
Path.Combine(current, PortableScriptsFolderName),
|
||||||
Path.Combine(current, "src", "Scripts")
|
Path.Combine(current, "src", PortableScriptsFolderName)
|
||||||
}) {
|
}) {
|
||||||
if (LooksLikeScriptsRoot(candidate))
|
if (LooksLikeScriptsRoot(candidate))
|
||||||
return candidate;
|
return candidate;
|
||||||
@ -140,6 +238,12 @@ public static class HostPaths {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static bool PathsEqual(string left, string right) =>
|
||||||
|
string.Equals(
|
||||||
|
Path.GetFullPath(left).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
|
||||||
|
Path.GetFullPath(right).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
|
||||||
|
OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||||
|
|
||||||
private static bool LooksLikeScriptsRoot(string path) =>
|
private static bool LooksLikeScriptsRoot(string path) =>
|
||||||
Directory.Exists(path)
|
Directory.Exists(path)
|
||||||
&& (Directory.Exists(Path.Combine(path, "File-Sync"))
|
&& (Directory.Exists(Path.Combine(path, "File-Sync"))
|
||||||
|
|||||||
@ -173,6 +173,105 @@ public class HostPathsTests {
|
|||||||
path);
|
path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FindPortableRoot_returns_null_without_marker() {
|
||||||
|
var root = Path.Combine(Path.GetTempPath(), $"uscheduler-no-portable-{Guid.NewGuid():N}");
|
||||||
|
Directory.CreateDirectory(root);
|
||||||
|
try {
|
||||||
|
Assert.Null(HostPaths.FindPortableRoot(root));
|
||||||
|
Assert.False(HostPaths.IsPortableLayout(root));
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Directory.Delete(root, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FindPortableRoot_finds_marker_in_start_directory() {
|
||||||
|
var root = Path.Combine(Path.GetTempPath(), $"uscheduler-portable-{Guid.NewGuid():N}");
|
||||||
|
Directory.CreateDirectory(root);
|
||||||
|
File.WriteAllText(Path.Combine(root, HostPaths.PortableMarkerFileName), HostPaths.PortableMarkerContent);
|
||||||
|
try {
|
||||||
|
Assert.Equal(Path.GetFullPath(root), HostPaths.FindPortableRoot(root));
|
||||||
|
Assert.True(HostPaths.IsPortableLayout(root));
|
||||||
|
Assert.Equal(
|
||||||
|
Path.GetFullPath(Path.Combine(root, "Data", "settings.json")),
|
||||||
|
Path.GetFullPath(HostPaths.ResolveSharedSettingsFile(root)));
|
||||||
|
Assert.Equal(
|
||||||
|
Path.GetFullPath(Path.Combine(root, "Data", "ui-settings.json")),
|
||||||
|
Path.GetFullPath(HostPaths.ResolveUiSettingsFile(root)));
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Directory.Delete(root, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FindPortableRoot_walks_up_to_parent_marker() {
|
||||||
|
var root = Path.Combine(Path.GetTempPath(), $"uscheduler-portable-parent-{Guid.NewGuid():N}");
|
||||||
|
var worker = Path.Combine(root, "MaksIT.UScheduler");
|
||||||
|
Directory.CreateDirectory(worker);
|
||||||
|
File.WriteAllText(Path.Combine(root, HostPaths.PortableMarkerFileName), HostPaths.PortableMarkerContent);
|
||||||
|
try {
|
||||||
|
Assert.Equal(Path.GetFullPath(root), HostPaths.FindPortableRoot(worker));
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Directory.Delete(root, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApplyPortableDefaults_replaces_machine_wide_paths() {
|
||||||
|
var root = Path.Combine(Path.GetTempPath(), $"uscheduler-portable-defaults-{Guid.NewGuid():N}");
|
||||||
|
Directory.CreateDirectory(root);
|
||||||
|
File.WriteAllText(Path.Combine(root, HostPaths.PortableMarkerFileName), HostPaths.PortableMarkerContent);
|
||||||
|
try {
|
||||||
|
var config = new Configuration {
|
||||||
|
LogDir = HostPaths.DefaultLogDirectory,
|
||||||
|
ScriptsDir = HostPaths.DefaultScriptsDirectory
|
||||||
|
};
|
||||||
|
HostPaths.ApplyPortableDefaults(config, root);
|
||||||
|
Assert.Equal(Path.GetFullPath(Path.Combine(root, "Logs")), Path.GetFullPath(config.LogDir));
|
||||||
|
Assert.Equal(Path.GetFullPath(Path.Combine(root, "Scripts")), Path.GetFullPath(config.ScriptsDir));
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Directory.Delete(root, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Copies_seed_into_portable_data_folder_with_local_paths() {
|
||||||
|
var root = Path.Combine(Path.GetTempPath(), $"uscheduler-portable-seed-{Guid.NewGuid():N}");
|
||||||
|
var data = Path.Combine(root, "Data");
|
||||||
|
Directory.CreateDirectory(data);
|
||||||
|
File.WriteAllText(Path.Combine(root, HostPaths.PortableMarkerFileName), HostPaths.PortableMarkerContent);
|
||||||
|
var seed = Path.Combine(root, "appsettings.json");
|
||||||
|
var user = Path.Combine(data, "settings.json");
|
||||||
|
var defaultLog = HostPaths.DefaultLogDirectory.Replace("\\", "\\\\", StringComparison.Ordinal);
|
||||||
|
var defaultScripts = HostPaths.DefaultScriptsDirectory.Replace("\\", "\\\\", StringComparison.Ordinal);
|
||||||
|
File.WriteAllText(seed, $$"""
|
||||||
|
{
|
||||||
|
"Logging": { "LogLevel": { "Default": "Information" } },
|
||||||
|
"Configuration": {
|
||||||
|
"LogDir": "{{defaultLog}}",
|
||||||
|
"ScriptsDir": "{{defaultScripts}}",
|
||||||
|
"Powershell": [ { "Path": "File-Sync\\file-sync.ps1", "Disabled": true } ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
try {
|
||||||
|
var service = new ConfigurationFileService(user, seed);
|
||||||
|
Assert.True(File.Exists(user));
|
||||||
|
Assert.Equal(Path.GetFullPath(Path.Combine(root, "Logs")), Path.GetFullPath(service.Current.LogDir));
|
||||||
|
Assert.Equal(Path.GetFullPath(Path.Combine(root, "Scripts")), Path.GetFullPath(service.Current.ScriptsDir));
|
||||||
|
Assert.Single(service.Current.Powershell);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Directory.Delete(root, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FindBundledScriptsDirectory_finds_scripts_next_to_start() {
|
public void FindBundledScriptsDirectory_finds_scripts_next_to_start() {
|
||||||
var root = Path.Combine(Path.GetTempPath(), $"uscheduler-bundled-{Guid.NewGuid():N}");
|
var root = Path.Combine(Path.GetTempPath(), $"uscheduler-bundled-{Guid.NewGuid():N}");
|
||||||
|
|||||||
@ -99,4 +99,55 @@ public class HostDataDirectoriesTests {
|
|||||||
Directory.Delete(root, true);
|
Directory.Delete(root, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveLayout_standard_uses_machine_wide_paths() {
|
||||||
|
var install = Path.Combine(Path.GetTempPath(), $"uscheduler-layout-std-{Guid.NewGuid():N}");
|
||||||
|
Directory.CreateDirectory(install);
|
||||||
|
try {
|
||||||
|
var layout = HostDataDirectories.ResolveLayout(install);
|
||||||
|
Assert.False(layout.Portable);
|
||||||
|
Assert.Equal(HostPaths.DefaultDataRoot, layout.Root);
|
||||||
|
Assert.Equal(HostPaths.DefaultScriptsDirectory, layout.ScriptsDirectory);
|
||||||
|
Assert.Equal(HostPaths.DefaultLogDirectory, layout.LogDirectory);
|
||||||
|
Assert.Equal(HostPaths.SharedSettingsFile, layout.SharedSettingsFile);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Directory.Delete(install, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveLayout_portable_requested_keeps_everything_under_install_folder() {
|
||||||
|
var install = Path.Combine(Path.GetTempPath(), $"uscheduler-layout-port-{Guid.NewGuid():N}");
|
||||||
|
Directory.CreateDirectory(install);
|
||||||
|
try {
|
||||||
|
var layout = HostDataDirectories.ResolveLayout(install, portableRequested: true);
|
||||||
|
Assert.True(layout.Portable);
|
||||||
|
Assert.Equal(Path.GetFullPath(install), Path.GetFullPath(layout.Root));
|
||||||
|
Assert.Equal(Path.GetFullPath(Path.Combine(install, "Scripts")), Path.GetFullPath(layout.ScriptsDirectory));
|
||||||
|
Assert.Equal(Path.GetFullPath(Path.Combine(install, "Logs")), Path.GetFullPath(layout.LogDirectory));
|
||||||
|
Assert.Equal(
|
||||||
|
Path.GetFullPath(Path.Combine(install, "Data", "settings.json")),
|
||||||
|
Path.GetFullPath(layout.SharedSettingsFile));
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Directory.Delete(install, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveLayout_detects_existing_portable_marker() {
|
||||||
|
var install = Path.Combine(Path.GetTempPath(), $"uscheduler-layout-mark-{Guid.NewGuid():N}");
|
||||||
|
Directory.CreateDirectory(install);
|
||||||
|
File.WriteAllText(Path.Combine(install, HostPaths.PortableMarkerFileName), HostPaths.PortableMarkerContent);
|
||||||
|
try {
|
||||||
|
var layout = HostDataDirectories.ResolveLayout(install);
|
||||||
|
Assert.True(layout.Portable);
|
||||||
|
Assert.Equal(Path.GetFullPath(install), Path.GetFullPath(layout.Root));
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Directory.Delete(install, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<RootNamespace>MaksIT.UScheduler.UI</RootNamespace>
|
<RootNamespace>MaksIT.UScheduler.UI</RootNamespace>
|
||||||
<Version>1.1.1</Version>
|
<Version>1.2.0</Version>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||||
@ -14,10 +14,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Avalonia" Version="12.1.0" />
|
<PackageReference Include="Avalonia" Version="12.1.2" />
|
||||||
<PackageReference Include="Avalonia.Desktop" Version="12.1.0" />
|
<PackageReference Include="Avalonia.Desktop" Version="12.1.2" />
|
||||||
<PackageReference Include="Avalonia.Themes.Simple" Version="12.1.0" />
|
<PackageReference Include="Avalonia.Themes.Simple" Version="12.1.2" />
|
||||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="12.1.0" />
|
<PackageReference Include="Avalonia.Controls.DataGrid" Version="12.1.2" />
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,8 @@ internal static class Program {
|
|||||||
public static void Main(string[] args) =>
|
public static void Main(string[] args) =>
|
||||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||||
|
|
||||||
|
// Linux: X11/XWayland. Avalonia 12.1.2 native Wayland still hangs on GNOME's
|
||||||
|
// xdg_toplevel.configure(0, 0) and never maps a window (GNOME app icon).
|
||||||
public static AppBuilder BuildAvaloniaApp() =>
|
public static AppBuilder BuildAvaloniaApp() =>
|
||||||
AppBuilder.Configure<App>()
|
AppBuilder.Configure<App>()
|
||||||
.UsePlatformDetect()
|
.UsePlatformDetect()
|
||||||
|
|||||||
@ -5,7 +5,8 @@ using MaksIT.UScheduler.Shared.Helpers;
|
|||||||
namespace MaksIT.UScheduler.UI.Services;
|
namespace MaksIT.UScheduler.UI.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads and saves machine-wide service configuration (ProgramData settings.json).
|
/// Loads and saves machine-wide service configuration (ProgramData settings.json,
|
||||||
|
/// or the portable Data folder).
|
||||||
/// Shipped appsettings.json next to the worker is seed-only.
|
/// Shipped appsettings.json next to the worker is seed-only.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AppSettingsService {
|
public class AppSettingsService {
|
||||||
@ -24,10 +25,10 @@ public class AppSettingsService {
|
|||||||
if (string.IsNullOrEmpty(seed) && !string.IsNullOrEmpty(serviceDirectory))
|
if (string.IsNullOrEmpty(seed) && !string.IsNullOrEmpty(serviceDirectory))
|
||||||
seed = Path.Combine(serviceDirectory, ConfigurationFileService.SeedFileName);
|
seed = Path.Combine(serviceDirectory, ConfigurationFileService.SeedFileName);
|
||||||
|
|
||||||
_files = new ConfigurationFileService(HostPaths.SharedSettingsFile, seed);
|
_files = new ConfigurationFileService(HostPaths.ResolveSharedSettingsFile(serviceDirectory), seed);
|
||||||
var config = _files.Current;
|
var config = _files.Current;
|
||||||
config.EnsureDefaults();
|
config.EnsureDefaults();
|
||||||
ScriptsDirectory = config.GetEffectiveScriptsDirectory();
|
ScriptsDirectory = config.GetEffectiveScriptsDirectory(_serviceDirectory);
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,7 +38,7 @@ public class AppSettingsService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
_files.Save(configuration);
|
_files.Save(configuration);
|
||||||
ScriptsDirectory = configuration.GetEffectiveScriptsDirectory();
|
ScriptsDirectory = configuration.GetEffectiveScriptsDirectory(_serviceDirectory);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
|
|||||||
@ -7,7 +7,8 @@ using MaksIT.UScheduler.Shared.Helpers;
|
|||||||
namespace MaksIT.UScheduler.UI.Services;
|
namespace MaksIT.UScheduler.UI.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per-user UI prefs stored in roaming AppData (not next to the exe).
|
/// Per-user UI prefs stored in roaming AppData (not next to the exe), or under
|
||||||
|
/// the portable Data folder when a portable layout is used.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class UISettings {
|
public class UISettings {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -18,17 +19,19 @@ public class UISettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Service for loading and saving UI settings under the current user's AppData.
|
/// Service for loading and saving UI settings under the current user's AppData
|
||||||
/// Machine-wide schedule configuration lives in ProgramData via
|
/// (or the portable Data folder). Machine-wide schedule configuration lives in
|
||||||
/// <see cref="ConfigurationFileService"/>.
|
/// ProgramData (or that same Data folder) via <see cref="ConfigurationFileService"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class UISettingsService {
|
public class UISettingsService {
|
||||||
public const string ProductFolder = HostPaths.ProductFolder;
|
public const string ProductFolder = HostPaths.ProductFolder;
|
||||||
|
|
||||||
private readonly string _settingsFilePath;
|
private readonly string _settingsFilePath;
|
||||||
|
|
||||||
public UISettingsService() {
|
public UISettingsService(string? settingsFilePath = null) {
|
||||||
_settingsFilePath = UserSettingsPath.Get(ProductFolder);
|
_settingsFilePath = string.IsNullOrWhiteSpace(settingsFilePath)
|
||||||
|
? HostPaths.ResolveUiSettingsFile()
|
||||||
|
: settingsFilePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public UISettings Load() {
|
public UISettings Load() {
|
||||||
|
|||||||
@ -78,7 +78,8 @@ public partial class MainViewModel : ObservableObject {
|
|||||||
var seedPath = string.IsNullOrEmpty(bin)
|
var seedPath = string.IsNullOrEmpty(bin)
|
||||||
? null
|
? null
|
||||||
: Path.Combine(bin, ConfigurationFileService.SeedFileName);
|
: Path.Combine(bin, ConfigurationFileService.SeedFileName);
|
||||||
var sharedExists = File.Exists(HostPaths.SharedSettingsFile);
|
var sharedSettingsPath = HostPaths.ResolveSharedSettingsFile(bin);
|
||||||
|
var sharedExists = File.Exists(sharedSettingsPath);
|
||||||
var seedExists = !string.IsNullOrEmpty(seedPath) && File.Exists(seedPath);
|
var seedExists = !string.IsNullOrEmpty(seedPath) && File.Exists(seedPath);
|
||||||
|
|
||||||
if (!sharedExists && !seedExists) {
|
if (!sharedExists && !seedExists) {
|
||||||
@ -86,8 +87,8 @@ public partial class MainViewModel : ObservableObject {
|
|||||||
AppSettingsLoadError = "Set Service Bin Path to the worker folder (seed appsettings.json), or register the service to create shared settings.";
|
AppSettingsLoadError = "Set Service Bin Path to the worker folder (seed appsettings.json), or register the service to create shared settings.";
|
||||||
ServiceName = string.Empty;
|
ServiceName = string.Empty;
|
||||||
LogDirectory = string.Empty;
|
LogDirectory = string.Empty;
|
||||||
ScriptsDirectory = HostPaths.DefaultScriptsDirectory;
|
ScriptsDirectory = HostPaths.ResolveScriptsDirectory(startDirectory: bin);
|
||||||
SharedSettingsPath = HostPaths.SharedSettingsFile;
|
SharedSettingsPath = string.IsNullOrEmpty(bin) ? HostPaths.ResolveSharedSettingsFile() : sharedSettingsPath;
|
||||||
ProcessList.Clear();
|
ProcessList.Clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -98,8 +99,8 @@ public partial class MainViewModel : ObservableObject {
|
|||||||
_serviceManager = new HostServiceManager(_serviceConfig.ServiceName);
|
_serviceManager = new HostServiceManager(_serviceConfig.ServiceName);
|
||||||
ServiceName = _serviceConfig.ServiceName;
|
ServiceName = _serviceConfig.ServiceName;
|
||||||
LogDirectory = _serviceConfig.GetEffectiveLogDirectory(bin);
|
LogDirectory = _serviceConfig.GetEffectiveLogDirectory(bin);
|
||||||
ScriptsDirectory = _serviceConfig.GetEffectiveScriptsDirectory();
|
ScriptsDirectory = _serviceConfig.GetEffectiveScriptsDirectory(bin);
|
||||||
SharedSettingsPath = _appSettingsService.SettingsPath ?? HostPaths.SharedSettingsFile;
|
SharedSettingsPath = _appSettingsService.SettingsPath ?? HostPaths.ResolveSharedSettingsFile(bin);
|
||||||
ProcessList.Clear();
|
ProcessList.Clear();
|
||||||
foreach (var p in _serviceConfig.Processes)
|
foreach (var p in _serviceConfig.Processes)
|
||||||
ProcessList.Add(p);
|
ProcessList.Add(p);
|
||||||
@ -108,8 +109,8 @@ public partial class MainViewModel : ObservableObject {
|
|||||||
AppSettingsLoadError = "Failed to load shared settings. Register the service once to create writable data folders.";
|
AppSettingsLoadError = "Failed to load shared settings. Register the service once to create writable data folders.";
|
||||||
ServiceName = string.Empty;
|
ServiceName = string.Empty;
|
||||||
LogDirectory = string.Empty;
|
LogDirectory = string.Empty;
|
||||||
ScriptsDirectory = HostPaths.DefaultScriptsDirectory;
|
ScriptsDirectory = HostPaths.ResolveScriptsDirectory(startDirectory: bin);
|
||||||
SharedSettingsPath = HostPaths.SharedSettingsFile;
|
SharedSettingsPath = HostPaths.ResolveSharedSettingsFile(bin);
|
||||||
ProcessList.Clear();
|
ProcessList.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -132,7 +133,7 @@ public partial class MainViewModel : ObservableObject {
|
|||||||
private string _scriptsDirectory = HostPaths.DefaultScriptsDirectory;
|
private string _scriptsDirectory = HostPaths.DefaultScriptsDirectory;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _sharedSettingsPath = HostPaths.SharedSettingsFile;
|
private string _sharedSettingsPath = HostPaths.ResolveSharedSettingsFile();
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[NotifyPropertyChangedFor(nameof(HasAppSettingsLoadError))]
|
[NotifyPropertyChangedFor(nameof(HasAppSettingsLoadError))]
|
||||||
@ -205,7 +206,7 @@ public partial class MainViewModel : ObservableObject {
|
|||||||
if (string.IsNullOrWhiteSpace(ResolvedExecutablePath) || !File.Exists(ResolvedExecutablePath)) {
|
if (string.IsNullOrWhiteSpace(ResolvedExecutablePath) || !File.Exists(ResolvedExecutablePath)) {
|
||||||
await _dialogs.ShowMessageAsync(
|
await _dialogs.ShowMessageAsync(
|
||||||
"Register Service",
|
"Register Service",
|
||||||
"Could not find MaksIT.UScheduler.exe. Set Service Bin Path to the Program Files install folder.");
|
"Could not find MaksIT.UScheduler.exe. Set Service Bin Path to the worker install folder.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<RootNamespace>$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
<RootNamespace>$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
||||||
<Version>1.1.1</Version>
|
<Version>1.2.0</Version>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<UserSecretsId>dotnet-UScheduler-040d8105-9e07-4024-a632-cbe091387b66</UserSecretsId>
|
<UserSecretsId>dotnet-UScheduler-040d8105-9e07-4024-a632-cbe091387b66</UserSecretsId>
|
||||||
|
|||||||
@ -9,18 +9,20 @@ using MaksIT.UScheduler.BackgroundServices;
|
|||||||
|
|
||||||
var basePath = AppContext.BaseDirectory;
|
var basePath = AppContext.BaseDirectory;
|
||||||
var seedPath = Path.Combine(basePath, ConfigurationFileService.SeedFileName);
|
var seedPath = Path.Combine(basePath, ConfigurationFileService.SeedFileName);
|
||||||
var settingsPath = HostPaths.SharedSettingsFile;
|
var portableRequested = HasFlag(args, "--portable");
|
||||||
|
|
||||||
if (args.Length > 0) {
|
if (args.Length > 0) {
|
||||||
var early = args[0].ToLowerInvariant();
|
var early = args[0].ToLowerInvariant();
|
||||||
if (early is "--install" or "-i" or "--prepare-data") {
|
if (early is "--install" or "-i" or "--prepare-data") {
|
||||||
var prepared = HostDataDirectories.Prepare(basePath);
|
var prepared = HostDataDirectories.Prepare(basePath, portableRequested);
|
||||||
Console.WriteLine(prepared.Message);
|
Console.WriteLine(prepared.Message);
|
||||||
if (!prepared.Success)
|
if (!prepared.Success)
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var settingsPath = HostPaths.ResolveSharedSettingsFile(basePath);
|
||||||
|
|
||||||
_ = new ConfigurationFileService(settingsPath, seedPath);
|
_ = new ConfigurationFileService(settingsPath, seedPath);
|
||||||
|
|
||||||
var configurationRoot = new ConfigurationBuilder()
|
var configurationRoot = new ConfigurationBuilder()
|
||||||
@ -63,7 +65,7 @@ if (args.Length > 0) {
|
|||||||
return GetServiceStatus(serviceName);
|
return GetServiceStatus(serviceName);
|
||||||
|
|
||||||
case "--prepare-data":
|
case "--prepare-data":
|
||||||
return PrepareDataDirectories(basePath);
|
return PrepareDataDirectories(basePath, portableRequested);
|
||||||
|
|
||||||
case "--help":
|
case "--help":
|
||||||
case "-h":
|
case "-h":
|
||||||
@ -123,11 +125,12 @@ static void PrintHelp(string name) =>
|
|||||||
--start Start the service
|
--start Start the service
|
||||||
--stop Stop the service
|
--stop Stop the service
|
||||||
--status Query service status
|
--status Query service status
|
||||||
--prepare-data Create C:\MaksIT\Scripts, C:\MaksIT\Logs, and shared settings (elevated)
|
--prepare-data Create scripts, logs, and shared settings (elevated)
|
||||||
|
--portable With --install / --prepare-data: keep data in the install folder
|
||||||
--help, -h Show this help message
|
--help, -h Show this help message
|
||||||
|
|
||||||
Service Name: {name}
|
Service Name: {name}
|
||||||
Config File: {HostPaths.SharedSettingsFile}
|
Config File: {HostPaths.ResolveSharedSettingsFile()}
|
||||||
Seed File: appsettings.json (next to the executable, logging + first-run seed)
|
Seed File: appsettings.json (next to the executable, logging + first-run seed)
|
||||||
|
|
||||||
Note: Install/uninstall/prepare-data typically require administrator / root privileges.
|
Note: Install/uninstall/prepare-data typically require administrator / root privileges.
|
||||||
@ -187,14 +190,19 @@ static int InstallService(string name, string exePath, string description) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int PrepareDataDirectories(string installDirectory) {
|
static int PrepareDataDirectories(string installDirectory, bool portable) {
|
||||||
|
var layout = HostDataDirectories.ResolveLayout(installDirectory, portable);
|
||||||
Console.WriteLine($"Install: {installDirectory}");
|
Console.WriteLine($"Install: {installDirectory}");
|
||||||
Console.WriteLine($"Settings: {HostPaths.SharedSettingsFile}");
|
Console.WriteLine($"Type: {(layout.Portable ? "portable" : "standard")}");
|
||||||
Console.WriteLine($"Scripts: {HostPaths.DefaultScriptsDirectory}");
|
Console.WriteLine($"Settings: {layout.SharedSettingsFile}");
|
||||||
Console.WriteLine($"Logs: {HostPaths.DefaultLogDirectory}");
|
Console.WriteLine($"Scripts: {layout.ScriptsDirectory}");
|
||||||
|
Console.WriteLine($"Logs: {layout.LogDirectory}");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool HasFlag(string[] args, string name) =>
|
||||||
|
args.Any(a => string.Equals(a, name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
static int UninstallService(string name) {
|
static int UninstallService(string name) {
|
||||||
if (OperatingSystem.IsWindows())
|
if (OperatingSystem.IsWindows())
|
||||||
return UninstallWindowsService(name);
|
return UninstallWindowsService(name);
|
||||||
|
|||||||
@ -245,7 +245,9 @@ DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|||||||
exec "$DIR/MaksIT.UScheduler.UI/MaksIT.UScheduler.UI" "$@"
|
exec "$DIR/MaksIT.UScheduler.UI/MaksIT.UScheduler.UI" "$@"
|
||||||
'@
|
'@
|
||||||
Set-Content -Path $shPath -Value $shBody -Encoding utf8
|
Set-Content -Path $shPath -Value $shBody -Encoding utf8
|
||||||
Write-Log -Level "OK" -Message " Created launchers."
|
$portableMarkerPath = Join-Path $bundleDirectory "portable"
|
||||||
|
Set-Content -Path $portableMarkerPath -Value "MaksIT.UScheduler portable layout" -Encoding ascii
|
||||||
|
Write-Log -Level "OK" -Message " Created launchers and portable marker."
|
||||||
|
|
||||||
$installerPayload = if ($pluginSettings.PSObject.Properties['installerPayloadDir'] -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.installerPayloadDir)) {
|
$installerPayload = if ($pluginSettings.PSObject.Properties['installerPayloadDir'] -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.installerPayloadDir)) {
|
||||||
Resolve-PluginPath -Path ([string]$pluginSettings.installerPayloadDir) -BasePath $scriptDir
|
Resolve-PluginPath -Path ([string]$pluginSettings.installerPayloadDir) -BasePath $scriptDir
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft-07/schema",
|
"$schema": "https://json-schema.org/draft-07/schema",
|
||||||
"title": "MaksIT.UScheduler Release Script Settings",
|
"title": "MaksIT.UScheduler Release Script Settings",
|
||||||
"description": "Community desktop pipeline. GitHub assets are siblings: portable zip (win-x64 worker, UI, Scripts), Windows setup exe (Avalonia UI), Flatpak (Avalonia UI). The zip never contains the installer or Flatpak.",
|
"description": "Community desktop pipeline. GitHub assets are siblings: portable zip (win-x64 worker, UI, Scripts, portable marker), Windows setup exe (Avalonia UI; Standard or Portable layout), Flatpak (Avalonia UI). The zip never contains the installer or Flatpak.",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "DotNetReleaseVersion",
|
"name": "DotNetReleaseVersion",
|
||||||
@ -88,15 +88,28 @@
|
|||||||
"name": "FlatpakPack",
|
"name": "FlatpakPack",
|
||||||
"stageLabel": "build",
|
"stageLabel": "build",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"appId": "com.maks_it.UScheduler",
|
"appId": "com.maks_it.uscheduler",
|
||||||
"appName": "MaksIT UScheduler",
|
"appName": "MaksIT UScheduler",
|
||||||
"command": "maksit-uscheduler",
|
"command": "maksit-uscheduler",
|
||||||
"executableName": "MaksIT.UScheduler.UI",
|
"executableName": "MaksIT.UScheduler.UI",
|
||||||
"runtimeIdentifier": "linux-x64",
|
"runtimeIdentifier": "linux-x64",
|
||||||
"publishDir": "..\\..\\..\\releases\\MaksIT.UScheduler.UI\\linux-x64",
|
"publishDir": "..\\..\\..\\releases\\MaksIT.UScheduler.UI\\linux-x64",
|
||||||
"summary": "Schedule PowerShell scripts and processes",
|
"summary": "Schedule PowerShell scripts and processes",
|
||||||
"projectLicense": "MIT",
|
"description": "Schedule and run PowerShell scripts and console processes from an Avalonia desktop UI.",
|
||||||
|
"projectLicense": "Apache-2.0",
|
||||||
|
"categories": "Utility;System;",
|
||||||
"iconPath": "..\\..\\..\\src\\MaksIT.UScheduler.UI\\Assets\\icon.png",
|
"iconPath": "..\\..\\..\\src\\MaksIT.UScheduler.UI\\Assets\\icon.png",
|
||||||
|
"metainfoPath": "..\\..\\..\\data\\com.maks_it.uscheduler.metainfo.xml",
|
||||||
|
"desktopPath": "..\\..\\..\\data\\com.maks_it.uscheduler.desktop",
|
||||||
|
"finishArgs": [
|
||||||
|
"--share=ipc",
|
||||||
|
"--share=network",
|
||||||
|
"--socket=wayland",
|
||||||
|
"--socket=x11",
|
||||||
|
"--socket=pulseaudio",
|
||||||
|
"--device=dri",
|
||||||
|
"--filesystem=home"
|
||||||
|
],
|
||||||
"bundleNamePattern": "maksit-uscheduler-{version}.flatpak",
|
"bundleNamePattern": "maksit-uscheduler-{version}.flatpak",
|
||||||
"buildBundle": true,
|
"buildBundle": true,
|
||||||
"attachSourceZip": false,
|
"attachSourceZip": false,
|
||||||
|
|||||||
@ -283,13 +283,7 @@ function New-WixPackageXml {
|
|||||||
[string]$Architecture = 'x64',
|
[string]$Architecture = 'x64',
|
||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
[Parameter(Mandatory = $false)]
|
||||||
[string]$IconPath,
|
[string]$IconPath
|
||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
|
||||||
[string]$PostInstallFileName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
|
||||||
[string]$PostInstallArguments
|
|
||||||
)
|
)
|
||||||
|
|
||||||
$ns = 'http://wixtoolset.org/schemas/v4/wxs'
|
$ns = 'http://wixtoolset.org/schemas/v4/wxs'
|
||||||
@ -377,7 +371,6 @@ function New-WixPackageXml {
|
|||||||
'' = $installFolder
|
'' = $installFolder
|
||||||
}
|
}
|
||||||
$componentIds = [System.Collections.Generic.List[string]]::new()
|
$componentIds = [System.Collections.Generic.List[string]]::new()
|
||||||
$postInstallFileId = $null
|
|
||||||
|
|
||||||
$publishRoot = [System.IO.Path]::GetFullPath($PublishDirectory)
|
$publishRoot = [System.IO.Path]::GetFullPath($PublishDirectory)
|
||||||
$files = @(Get-ChildItem -LiteralPath $publishRoot -Recurse -File)
|
$files = @(Get-ChildItem -LiteralPath $publishRoot -Recurse -File)
|
||||||
@ -422,12 +415,6 @@ function New-WixPackageXml {
|
|||||||
$null = $fileNode.SetAttribute('KeyPath', 'yes')
|
$null = $fileNode.SetAttribute('KeyPath', 'yes')
|
||||||
$null = $component.AppendChild($fileNode)
|
$null = $component.AppendChild($fileNode)
|
||||||
$null = $parent.AppendChild($component)
|
$null = $parent.AppendChild($component)
|
||||||
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace($PostInstallFileName) `
|
|
||||||
-and [string]::IsNullOrWhiteSpace($relativeDir) `
|
|
||||||
-and [string]::Equals($file.Name, $PostInstallFileName, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
||||||
$postInstallFileId = $fileNode.GetAttribute('Id')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$exeName = [System.IO.Path]::GetFileName($ExecutablePath)
|
$exeName = [System.IO.Path]::GetFileName($ExecutablePath)
|
||||||
@ -502,34 +489,6 @@ function New-WixPackageXml {
|
|||||||
|
|
||||||
$null = $package.AppendChild($feature)
|
$null = $package.AppendChild($feature)
|
||||||
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace($PostInstallFileName)) {
|
|
||||||
if ([string]::IsNullOrWhiteSpace($postInstallFileId)) {
|
|
||||||
throw "WindowsInstaller postInstallExecutableName '$PostInstallFileName' was not harvested from $publishRoot."
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($PostInstallArguments)) {
|
|
||||||
throw "WindowsInstaller postInstallArguments is required when postInstallExecutableName is set."
|
|
||||||
}
|
|
||||||
|
|
||||||
$ca = $xml.CreateElement('CustomAction', $ns)
|
|
||||||
$null = $ca.SetAttribute('Id', 'PostInstallPrepareData')
|
|
||||||
$null = $ca.SetAttribute('FileRef', $postInstallFileId)
|
|
||||||
$arguments = if ($null -eq $PostInstallArguments) { '' } else { $PostInstallArguments }
|
|
||||||
$null = $ca.SetAttribute('ExeCommand', $arguments)
|
|
||||||
$null = $ca.SetAttribute('Execute', 'deferred')
|
|
||||||
$null = $ca.SetAttribute('Impersonate', 'no')
|
|
||||||
$null = $ca.SetAttribute('Return', 'check')
|
|
||||||
$null = $package.AppendChild($ca)
|
|
||||||
|
|
||||||
$seq = $xml.CreateElement('InstallExecuteSequence', $ns)
|
|
||||||
$custom = $xml.CreateElement('Custom', $ns)
|
|
||||||
$null = $custom.SetAttribute('Action', 'PostInstallPrepareData')
|
|
||||||
$null = $custom.SetAttribute('After', 'InstallFiles')
|
|
||||||
$null = $custom.SetAttribute('Condition', 'NOT REMOVE')
|
|
||||||
$null = $seq.AppendChild($custom)
|
|
||||||
$null = $package.AppendChild($seq)
|
|
||||||
}
|
|
||||||
|
|
||||||
return $xml
|
return $xml
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -537,9 +496,10 @@ function Get-DefaultFlatpakFinishArgs {
|
|||||||
return @(
|
return @(
|
||||||
'--share=ipc',
|
'--share=ipc',
|
||||||
'--share=network',
|
'--share=network',
|
||||||
'--socket=fallback-x11',
|
'--socket=x11',
|
||||||
'--socket=wayland',
|
'--socket=wayland',
|
||||||
'--device=dri',
|
'--device=dri',
|
||||||
|
'--socket=pulseaudio',
|
||||||
'--filesystem=home'
|
'--filesystem=home'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -565,12 +525,118 @@ function New-FlatpakDesktopEntry {
|
|||||||
"Name=$AppName",
|
"Name=$AppName",
|
||||||
"Exec=$Command",
|
"Exec=$Command",
|
||||||
"Icon=$AppId",
|
"Icon=$AppId",
|
||||||
|
"StartupWMClass=$AppId",
|
||||||
|
'StartupNotify=true',
|
||||||
'Terminal=false',
|
'Terminal=false',
|
||||||
"Categories=$Categories"
|
"Categories=$Categories"
|
||||||
)
|
)
|
||||||
return (($lines -join "`n") + "`n")
|
return (($lines -join "`n") + "`n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Copy-FlatpakHicolorIcons {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$AppId,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$HicolorRoot,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[string]$IconPath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[string]$SvgIconPath
|
||||||
|
)
|
||||||
|
|
||||||
|
$pngSource = $null
|
||||||
|
$svgSource = $null
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($IconPath) -and (Test-Path -LiteralPath $IconPath -PathType Leaf)) {
|
||||||
|
$ext = [System.IO.Path]::GetExtension($IconPath)
|
||||||
|
if ($ext -ieq '.png') {
|
||||||
|
$pngSource = $IconPath
|
||||||
|
}
|
||||||
|
elseif ($ext -ieq '.svg') {
|
||||||
|
$svgSource = $IconPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($SvgIconPath) -and (Test-Path -LiteralPath $SvgIconPath -PathType Leaf)) {
|
||||||
|
$svgSource = $SvgIconPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($svgSource) -and -not [string]::IsNullOrWhiteSpace($pngSource)) {
|
||||||
|
$siblingSvg = [System.IO.Path]::ChangeExtension($pngSource, '.svg')
|
||||||
|
if (Test-Path -LiteralPath $siblingSvg -PathType Leaf) {
|
||||||
|
$svgSource = $siblingSvg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($pngSource) -and -not [string]::IsNullOrWhiteSpace($svgSource)) {
|
||||||
|
$siblingPng = [System.IO.Path]::ChangeExtension($svgSource, '.png')
|
||||||
|
if (Test-Path -LiteralPath $siblingPng -PathType Leaf) {
|
||||||
|
$pngSource = $siblingPng
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$installed = $false
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($pngSource)) {
|
||||||
|
foreach ($size in @('48x48', '64x64', '128x128', '256x256')) {
|
||||||
|
$pngDir = Join-Path $HicolorRoot "$size\apps"
|
||||||
|
New-Item -ItemType Directory -Path $pngDir -Force | Out-Null
|
||||||
|
Copy-Item -LiteralPath $pngSource -Destination (Join-Path $pngDir "$AppId.png") -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
$installed = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($svgSource)) {
|
||||||
|
$svgDir = Join-Path $HicolorRoot 'scalable\apps'
|
||||||
|
New-Item -ItemType Directory -Path $svgDir -Force | Out-Null
|
||||||
|
Copy-Item -LiteralPath $svgSource -Destination (Join-Path $svgDir "$AppId.svg") -Force
|
||||||
|
$installed = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installed
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-FlatpakMetainfoDescriptionXml {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$AppName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[AllowEmptyString()]
|
||||||
|
[string]$Summary,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[AllowEmptyString()]
|
||||||
|
[string]$Description
|
||||||
|
)
|
||||||
|
|
||||||
|
$text = $Description
|
||||||
|
if ([string]::IsNullOrWhiteSpace($text)) {
|
||||||
|
$text = "$AppName is a desktop application. $Summary".Trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
$paragraphs = [System.Collections.Generic.List[string]]::new()
|
||||||
|
foreach ($chunk in @($text -split '(\r?\n){2,}')) {
|
||||||
|
$trimmed = $chunk.Trim()
|
||||||
|
if ([string]::IsNullOrWhiteSpace($trimmed)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$paragraphs.Add(' <p>' + [System.Security.SecurityElement]::Escape($trimmed) + '</p>')
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($paragraphs.Count -eq 0) {
|
||||||
|
$paragraphs.Add(' <p>' + [System.Security.SecurityElement]::Escape("$AppName is a desktop application.") + '</p>')
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($paragraphs -join "`n")
|
||||||
|
}
|
||||||
|
|
||||||
function New-FlatpakMetainfoXml {
|
function New-FlatpakMetainfoXml {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
@ -582,26 +648,124 @@ function New-FlatpakMetainfoXml {
|
|||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$Summary,
|
[string]$Summary,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[AllowEmptyString()]
|
||||||
|
[string]$Description = '',
|
||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
[Parameter(Mandatory = $false)]
|
||||||
[string]$ProjectLicense = 'MIT',
|
[string]$ProjectLicense = 'MIT',
|
||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
[Parameter(Mandatory = $false)]
|
||||||
[string]$MetadataLicense = 'CC0-1.0'
|
[string]$MetadataLicense = 'CC0-1.0',
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[string]$Version = ''
|
||||||
)
|
)
|
||||||
|
|
||||||
$escapedName = [System.Security.SecurityElement]::Escape($AppName)
|
$escapedName = [System.Security.SecurityElement]::Escape($AppName)
|
||||||
$escapedSummary = [System.Security.SecurityElement]::Escape($Summary)
|
$escapedSummary = [System.Security.SecurityElement]::Escape($Summary)
|
||||||
return @"
|
$descriptionXml = ConvertTo-FlatpakMetainfoDescriptionXml -AppName $AppName -Summary $Summary -Description $Description
|
||||||
|
$xml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<component type="desktop-application">
|
<component type="desktop-application">
|
||||||
<id>$AppId</id>
|
<id>$AppId</id>
|
||||||
<name>$escapedName</name>
|
<name>$escapedName</name>
|
||||||
<summary>$escapedSummary</summary>
|
<summary>$escapedSummary</summary>
|
||||||
|
<description>
|
||||||
|
$descriptionXml
|
||||||
|
</description>
|
||||||
<launchable type="desktop-id">$AppId.desktop</launchable>
|
<launchable type="desktop-id">$AppId.desktop</launchable>
|
||||||
<metadata_license>$MetadataLicense</metadata_license>
|
<metadata_license>$MetadataLicense</metadata_license>
|
||||||
<project_license>$ProjectLicense</project_license>
|
<project_license>$ProjectLicense</project_license>
|
||||||
</component>
|
</component>
|
||||||
"@
|
"@
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Version)) {
|
||||||
|
return $xml
|
||||||
|
}
|
||||||
|
|
||||||
|
return Set-FlatpakMetainfoRelease -XmlText $xml -Version $Version
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-FlatpakMetainfoRelease {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$XmlText,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Version,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[string]$Date = ''
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Version)) {
|
||||||
|
throw "Set-FlatpakMetainfoRelease requires Version."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Date)) {
|
||||||
|
$Date = [datetime]::UtcNow.ToString('yyyy-MM-dd')
|
||||||
|
}
|
||||||
|
|
||||||
|
$doc = New-Object System.Xml.XmlDocument
|
||||||
|
$doc.PreserveWhitespace = $false
|
||||||
|
$doc.LoadXml($XmlText)
|
||||||
|
$component = $doc.SelectSingleNode('/component')
|
||||||
|
if ($null -eq $component) {
|
||||||
|
throw "Flatpak metainfo has no <component> root."
|
||||||
|
}
|
||||||
|
|
||||||
|
$releases = $component.SelectSingleNode('releases')
|
||||||
|
if ($null -eq $releases) {
|
||||||
|
$releases = $doc.CreateElement('releases')
|
||||||
|
$null = $component.AppendChild($releases)
|
||||||
|
}
|
||||||
|
|
||||||
|
$match = $null
|
||||||
|
foreach ($node in @($releases.SelectNodes('release'))) {
|
||||||
|
if ([string]$node.GetAttribute('version') -eq $Version) {
|
||||||
|
$match = $node
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$first = $releases.SelectSingleNode('release')
|
||||||
|
if ($null -ne $match) {
|
||||||
|
$match.SetAttribute('date', $Date)
|
||||||
|
if (-not [object]::ReferenceEquals($match, $first)) {
|
||||||
|
$null = $releases.InsertBefore($match, $first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$release = $doc.CreateElement('release')
|
||||||
|
$release.SetAttribute('version', $Version)
|
||||||
|
$release.SetAttribute('date', $Date)
|
||||||
|
if ($null -eq $first) {
|
||||||
|
$null = $releases.AppendChild($release)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$null = $releases.InsertBefore($release, $first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = New-Object System.Xml.XmlWriterSettings
|
||||||
|
$settings.Indent = $true
|
||||||
|
$settings.OmitXmlDeclaration = $false
|
||||||
|
$settings.Encoding = [System.Text.UTF8Encoding]::new($false)
|
||||||
|
$stream = New-Object System.IO.MemoryStream
|
||||||
|
try {
|
||||||
|
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
||||||
|
try {
|
||||||
|
$doc.Save($writer)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$writer.Dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return [System.Text.Encoding]::UTF8.GetString($stream.ToArray())
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$stream.Dispose()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function New-FlatpakManifestObject {
|
function New-FlatpakManifestObject {
|
||||||
@ -638,8 +802,8 @@ function New-FlatpakManifestObject {
|
|||||||
if ($null -eq $BuildCommands -or $BuildCommands.Count -eq 0) {
|
if ($null -eq $BuildCommands -or $BuildCommands.Count -eq 0) {
|
||||||
$BuildCommands = @(
|
$BuildCommands = @(
|
||||||
'mkdir -p /app/lib /app/share',
|
'mkdir -p /app/lib /app/share',
|
||||||
'cp -a lib /app/lib',
|
'cp -a lib/. /app/lib/',
|
||||||
'cp -a share /app/share',
|
'cp -a share/. /app/share/',
|
||||||
"install -Dm755 bin/$Command /app/bin/$Command",
|
"install -Dm755 bin/$Command /app/bin/$Command",
|
||||||
"chmod +x /app/lib/$ModuleName/$Command"
|
"chmod +x /app/lib/$ModuleName/$Command"
|
||||||
)
|
)
|
||||||
@ -677,7 +841,15 @@ function New-FlatpakLaunchScript {
|
|||||||
[string]$ExecutableFileName
|
[string]$ExecutableFileName
|
||||||
)
|
)
|
||||||
|
|
||||||
return "#!/bin/sh`nexec /app/lib/$ModuleName/$ExecutableFileName `"`$@`"`n"
|
$lib = "/app/lib/$ModuleName"
|
||||||
|
return @"
|
||||||
|
#!/bin/sh
|
||||||
|
LIB='$lib'
|
||||||
|
if [ -f "`$LIB/flatpak-native.env" ]; then
|
||||||
|
. "`$LIB/flatpak-native.env"
|
||||||
|
fi
|
||||||
|
exec `$LIB/$ExecutableFileName "`$`@"
|
||||||
|
"@
|
||||||
}
|
}
|
||||||
|
|
||||||
function Assert-FlatpakAppId {
|
function Assert-FlatpakAppId {
|
||||||
@ -1061,7 +1233,9 @@ Export-ModuleMember -Function `
|
|||||||
New-WixBundleXml, `
|
New-WixBundleXml, `
|
||||||
Get-DefaultFlatpakFinishArgs, `
|
Get-DefaultFlatpakFinishArgs, `
|
||||||
New-FlatpakDesktopEntry, `
|
New-FlatpakDesktopEntry, `
|
||||||
|
Copy-FlatpakHicolorIcons, `
|
||||||
New-FlatpakMetainfoXml, `
|
New-FlatpakMetainfoXml, `
|
||||||
|
Set-FlatpakMetainfoRelease, `
|
||||||
New-FlatpakManifestObject, `
|
New-FlatpakManifestObject, `
|
||||||
New-FlatpakLaunchScript, `
|
New-FlatpakLaunchScript, `
|
||||||
Assert-FlatpakAppId, `
|
Assert-FlatpakAppId, `
|
||||||
|
|||||||
@ -13,6 +13,8 @@
|
|||||||
(default Debian) on the Linux filesystem, then copies the bundle back.
|
(default Debian) on the Linux filesystem, then copies the bundle back.
|
||||||
Docker Desktop WSL distros are never used. whenBuilderMissing skip|fail
|
Docker Desktop WSL distros are never used. whenBuilderMissing skip|fail
|
||||||
applies when neither native nor WSL builder is available.
|
applies when neither native nor WSL builder is available.
|
||||||
|
AppStream <release version> and the bundle file name use the same shared
|
||||||
|
release version as zip/MSI (DotNetReleaseVersion: csproj or Directory.Build.props).
|
||||||
#>
|
#>
|
||||||
|
|
||||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
@ -34,6 +36,37 @@ function Get-FlatpakWhenBuilderMissing {
|
|||||||
return $value
|
return $value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Resolve-FlatpakPackOptionalFile {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ScriptDir,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[AllowEmptyString()]
|
||||||
|
[string]$Setting,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Name
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Setting)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = if ([System.IO.Path]::IsPathRooted($Setting)) {
|
||||||
|
$Setting
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[System.IO.Path]::GetFullPath((Join-Path $ScriptDir $Setting))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
|
||||||
|
throw "FlatpakPack $Name not found: $path"
|
||||||
|
}
|
||||||
|
|
||||||
|
return $path
|
||||||
|
}
|
||||||
|
|
||||||
function Test-FlatpakBuilderMissingException {
|
function Test-FlatpakBuilderMissingException {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
@ -188,6 +221,9 @@ function Invoke-Plugin {
|
|||||||
$scriptDir = $sharedSettings.scriptDir
|
$scriptDir = $sharedSettings.scriptDir
|
||||||
$version = [string]$sharedSettings.version
|
$version = [string]$sharedSettings.version
|
||||||
$artifactsDirectory = $sharedSettings.artifactsDirectory
|
$artifactsDirectory = $sharedSettings.artifactsDirectory
|
||||||
|
if ([string]::IsNullOrWhiteSpace($version)) {
|
||||||
|
throw "FlatpakPack requires a release version in the shared context (same as zip/MSI: DotNetReleaseVersion from csproj / Directory.Build.props)."
|
||||||
|
}
|
||||||
|
|
||||||
$appId = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'appId')
|
$appId = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'appId')
|
||||||
$appName = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'appName')
|
$appName = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'appName')
|
||||||
@ -206,9 +242,13 @@ function Invoke-Plugin {
|
|||||||
$runtimeIdentifier = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'runtimeIdentifier' -Default 'linux-x64')
|
$runtimeIdentifier = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'runtimeIdentifier' -Default 'linux-x64')
|
||||||
$publishDirSetting = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'publishDir')
|
$publishDirSetting = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'publishDir')
|
||||||
$iconSetting = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'iconPath')
|
$iconSetting = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'iconPath')
|
||||||
|
$svgIconSetting = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'svgIconPath')
|
||||||
$summary = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'summary' -Default $appName)
|
$summary = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'summary' -Default $appName)
|
||||||
|
$description = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'description' -Default '')
|
||||||
$projectLicense = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'projectLicense' -Default 'MIT')
|
$projectLicense = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'projectLicense' -Default 'MIT')
|
||||||
$categories = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'categories' -Default 'Utility;')
|
$categories = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'categories' -Default 'Utility;')
|
||||||
|
$metainfoSetting = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'metainfoPath' -Default '')
|
||||||
|
$desktopFileSetting = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'desktopPath' -Default '')
|
||||||
$runtime = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'runtime' -Default 'org.freedesktop.Platform')
|
$runtime = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'runtime' -Default 'org.freedesktop.Platform')
|
||||||
$runtimeVersion = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'runtimeVersion' -Default '24.08')
|
$runtimeVersion = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'runtimeVersion' -Default '24.08')
|
||||||
$sdk = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'sdk' -Default 'org.freedesktop.Sdk')
|
$sdk = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'sdk' -Default 'org.freedesktop.Sdk')
|
||||||
@ -264,8 +304,8 @@ function Invoke-Plugin {
|
|||||||
$binDir = Join-Path $filesRoot 'bin'
|
$binDir = Join-Path $filesRoot 'bin'
|
||||||
$shareAppDir = Join-Path $filesRoot 'share\applications'
|
$shareAppDir = Join-Path $filesRoot 'share\applications'
|
||||||
$shareMetaDir = Join-Path $filesRoot 'share\metainfo'
|
$shareMetaDir = Join-Path $filesRoot 'share\metainfo'
|
||||||
$shareIconDir = Join-Path $filesRoot 'share\icons\hicolor\256x256\apps'
|
$hicolorRoot = Join-Path $filesRoot 'share\icons\hicolor'
|
||||||
New-Item -ItemType Directory -Path $libDir, $binDir, $shareAppDir, $shareMetaDir, $shareIconDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $libDir, $binDir, $shareAppDir, $shareMetaDir, $hicolorRoot -Force | Out-Null
|
||||||
|
|
||||||
Write-Log -Level "STEP" -Message "Staging Flatpak layout from $publishDirectory"
|
Write-Log -Level "STEP" -Message "Staging Flatpak layout from $publishDirectory"
|
||||||
Copy-Item -Path (Join-Path $publishDirectory '*') -Destination $libDir -Recurse -Force
|
Copy-Item -Path (Join-Path $publishDirectory '*') -Destination $libDir -Recurse -Force
|
||||||
@ -274,20 +314,56 @@ function Invoke-Plugin {
|
|||||||
$launchPath = Join-Path $binDir $command
|
$launchPath = Join-Path $binDir $command
|
||||||
[System.IO.File]::WriteAllText($launchPath, $launchScript.Replace("`r`n", "`n"), [System.Text.UTF8Encoding]::new($false))
|
[System.IO.File]::WriteAllText($launchPath, $launchScript.Replace("`r`n", "`n"), [System.Text.UTF8Encoding]::new($false))
|
||||||
|
|
||||||
$desktopPath = Join-Path $shareAppDir "$appId.desktop"
|
$desktopDest = Join-Path $shareAppDir "$appId.desktop"
|
||||||
[System.IO.File]::WriteAllText(
|
$upstreamDesktop = Resolve-FlatpakPackOptionalFile -ScriptDir $scriptDir -Setting $desktopFileSetting -Name 'desktopPath'
|
||||||
$desktopPath,
|
if ($null -ne $upstreamDesktop) {
|
||||||
(New-FlatpakDesktopEntry -AppId $appId -AppName $appName -Command $command -Categories $categories).Replace("`r`n", "`n"),
|
$desktopText = [System.IO.File]::ReadAllText($upstreamDesktop) -replace "`r`n", "`n" -replace "`r", "`n"
|
||||||
[System.Text.UTF8Encoding]::new($false)
|
if (-not $desktopText.EndsWith("`n")) {
|
||||||
)
|
$desktopText += "`n"
|
||||||
|
}
|
||||||
|
|
||||||
$metaPath = Join-Path $shareMetaDir "$appId.metainfo.xml"
|
[System.IO.File]::WriteAllText($desktopDest, $desktopText, [System.Text.UTF8Encoding]::new($false))
|
||||||
[System.IO.File]::WriteAllText(
|
}
|
||||||
$metaPath,
|
else {
|
||||||
(New-FlatpakMetainfoXml -AppId $appId -AppName $appName -Summary $summary -ProjectLicense $projectLicense).Replace("`r`n", "`n"),
|
[System.IO.File]::WriteAllText(
|
||||||
[System.Text.UTF8Encoding]::new($false)
|
$desktopDest,
|
||||||
)
|
(New-FlatpakDesktopEntry -AppId $appId -AppName $appName -Command $command -Categories $categories).Replace("`r`n", "`n"),
|
||||||
|
[System.Text.UTF8Encoding]::new($false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
$metaDest = Join-Path $shareMetaDir "$appId.metainfo.xml"
|
||||||
|
$upstreamMetainfo = Resolve-FlatpakPackOptionalFile -ScriptDir $scriptDir -Setting $metainfoSetting -Name 'metainfoPath'
|
||||||
|
if ($null -ne $upstreamMetainfo) {
|
||||||
|
$metaText = [System.IO.File]::ReadAllText($upstreamMetainfo) -replace "`r`n", "`n" -replace "`r", "`n"
|
||||||
|
if (-not $metaText.EndsWith("`n")) {
|
||||||
|
$metaText += "`n"
|
||||||
|
}
|
||||||
|
|
||||||
|
$metaText = $metaText.Replace('{version}', $version)
|
||||||
|
$metaText = Set-FlatpakMetainfoRelease -XmlText $metaText -Version $version
|
||||||
|
if (-not $metaText.EndsWith("`n")) {
|
||||||
|
$metaText += "`n"
|
||||||
|
}
|
||||||
|
|
||||||
|
[System.IO.File]::WriteAllText($metaDest, $metaText, [System.Text.UTF8Encoding]::new($false))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[System.IO.File]::WriteAllText(
|
||||||
|
$metaDest,
|
||||||
|
(New-FlatpakMetainfoXml -AppId $appId -AppName $appName -Summary $summary -Description $description -ProjectLicense $projectLicense -Version $version).Replace("`r`n", "`n"),
|
||||||
|
[System.Text.UTF8Encoding]::new($false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
$stagedMeta = [System.IO.File]::ReadAllText($metaDest)
|
||||||
|
if ($stagedMeta -notmatch [regex]::Escape("version=`"$version`"")) {
|
||||||
|
throw "FlatpakPack failed to stamp AppStream release version '$version' into $metaDest."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " AppStream release version: $version"
|
||||||
|
|
||||||
|
$iconPath = $null
|
||||||
if (-not [string]::IsNullOrWhiteSpace($iconSetting)) {
|
if (-not [string]::IsNullOrWhiteSpace($iconSetting)) {
|
||||||
$iconPath = if ([System.IO.Path]::IsPathRooted($iconSetting)) {
|
$iconPath = if ([System.IO.Path]::IsPathRooted($iconSetting)) {
|
||||||
$iconSetting
|
$iconSetting
|
||||||
@ -299,21 +375,39 @@ function Invoke-Plugin {
|
|||||||
if (-not (Test-Path -LiteralPath $iconPath -PathType Leaf)) {
|
if (-not (Test-Path -LiteralPath $iconPath -PathType Leaf)) {
|
||||||
throw "FlatpakPack iconPath not found: $iconPath"
|
throw "FlatpakPack iconPath not found: $iconPath"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ([System.IO.Path]::GetExtension($iconPath) -ine '.png') {
|
$svgIconPath = $null
|
||||||
Write-Log -Level "WARN" -Message " Flatpak icon should be PNG; skipping copy of $iconPath"
|
if (-not [string]::IsNullOrWhiteSpace($svgIconSetting)) {
|
||||||
|
$svgIconPath = if ([System.IO.Path]::IsPathRooted($svgIconSetting)) {
|
||||||
|
$svgIconSetting
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Copy-Item -LiteralPath $iconPath -Destination (Join-Path $shareIconDir "$appId.png") -Force
|
[System.IO.Path]::GetFullPath((Join-Path $scriptDir $svgIconSetting))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $svgIconPath -PathType Leaf)) {
|
||||||
|
throw "FlatpakPack svgIconPath not found: $svgIconPath"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$iconInstalled = Copy-FlatpakHicolorIcons `
|
||||||
|
-AppId $appId `
|
||||||
|
-HicolorRoot $hicolorRoot `
|
||||||
|
-IconPath $iconPath `
|
||||||
|
-SvgIconPath $svgIconPath
|
||||||
|
|
||||||
|
if (-not $iconInstalled -and -not [string]::IsNullOrWhiteSpace($iconPath)) {
|
||||||
|
Write-Log -Level "WARN" -Message " Flatpak icon should be PNG or SVG; skipping copy of $iconPath"
|
||||||
}
|
}
|
||||||
|
|
||||||
$buildCommands = @(
|
$buildCommands = @(
|
||||||
'mkdir -p /app/lib /app/share',
|
'mkdir -p /app/lib /app/share',
|
||||||
"cp -a lib/$moduleName /app/lib/$moduleName",
|
"cp -a lib/$moduleName /app/lib/$moduleName",
|
||||||
'cp -a share /app/share',
|
'cp -a share/. /app/share/',
|
||||||
"install -Dm755 bin/$command /app/bin/$command",
|
"install -Dm755 bin/$command /app/bin/$command",
|
||||||
"chmod +x /app/lib/$moduleName/$executableFileName"
|
"chmod +x /app/lib/$moduleName/$executableFileName",
|
||||||
|
'gtk-update-icon-cache -f /app/share/icons/hicolor >/dev/null 2>&1 || true'
|
||||||
)
|
)
|
||||||
|
|
||||||
$manifest = New-FlatpakManifestObject `
|
$manifest = New-FlatpakManifestObject `
|
||||||
|
|||||||
@ -9,8 +9,6 @@
|
|||||||
Harvests a win-* (or sole) DotNetPublish folder into a WiX MSI, then wraps
|
Harvests a win-* (or sole) DotNetPublish folder into a WiX MSI, then wraps
|
||||||
it in a Burn bootstrapper .exe. The .exe is the GitHub asset; the MSI/WXS
|
it in a Burn bootstrapper .exe. The .exe is the GitHub asset; the MSI/WXS
|
||||||
stay in a staging folder and are not added to the portable zip.
|
stay in a staging folder and are not added to the portable zip.
|
||||||
Optional postInstallExecutableName + postInstallArguments run a deferred
|
|
||||||
elevated custom action after InstallFiles (used to seed C:\MaksIT\Scripts).
|
|
||||||
`wix build -arch` follows `runtimeIdentifier` (default win-x64 → x64) so
|
`wix build -arch` follows `runtimeIdentifier` (default win-x64 → x64) so
|
||||||
per-machine installs go to `C:\Program Files`, not Program Files (x86).
|
per-machine installs go to `C:\Program Files`, not Program Files (x86).
|
||||||
Requires the WiX CLI (`dotnet tool install -g wix`). WiX v7: accept the
|
Requires the WiX CLI (`dotnet tool install -g wix`). WiX v7: accept the
|
||||||
@ -215,9 +213,6 @@ function Invoke-Plugin {
|
|||||||
$msiPath = Join-Path $stageDir ($safeName + '-' + $version + '.msi')
|
$msiPath = Join-Path $stageDir ($safeName + '-' + $version + '.msi')
|
||||||
$bundleWxsPath = Join-Path $stageDir ($safeName + '-' + $version + '-bundle.wxs')
|
$bundleWxsPath = Join-Path $stageDir ($safeName + '-' + $version + '-bundle.wxs')
|
||||||
|
|
||||||
$postInstallFileName = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'postInstallExecutableName' -Default '')
|
|
||||||
$postInstallArguments = [string](Get-PluginPropertyValue -PluginSettings $pluginSettings -Name 'postInstallArguments' -Default '')
|
|
||||||
|
|
||||||
Write-Log -Level "STEP" -Message "Generating WiX source for '$appName' from $publishDirectory ($wixArch)"
|
Write-Log -Level "STEP" -Message "Generating WiX source for '$appName' from $publishDirectory ($wixArch)"
|
||||||
$xml = New-WixPackageXml `
|
$xml = New-WixPackageXml `
|
||||||
-AppName $appName `
|
-AppName $appName `
|
||||||
@ -229,9 +224,7 @@ function Invoke-Plugin {
|
|||||||
-InstallScope $installScope `
|
-InstallScope $installScope `
|
||||||
-InstallFolderName $installFolderName `
|
-InstallFolderName $installFolderName `
|
||||||
-Architecture $wixArch `
|
-Architecture $wixArch `
|
||||||
-IconPath $iconPath `
|
-IconPath $iconPath
|
||||||
-PostInstallFileName $postInstallFileName `
|
|
||||||
-PostInstallArguments $postInstallArguments
|
|
||||||
|
|
||||||
$xml.Save($wxsPath)
|
$xml.Save($wxsPath)
|
||||||
Write-Log -Level "OK" -Message " WiX source: $wxsPath"
|
Write-Log -Level "OK" -Message " WiX source: $wxsPath"
|
||||||
|
|||||||
@ -26,17 +26,25 @@
|
|||||||
<Label X="185" Y="11" Width="-11" Height="32" FontId="1" DisablePrefix="yes">#(loc.Title)</Label>
|
<Label X="185" Y="11" Width="-11" Height="32" FontId="1" DisablePrefix="yes">#(loc.Title)</Label>
|
||||||
<ImageControl X="11" Y="11" Width="165" Height="400" ImageFile="logoside.png"/>
|
<ImageControl X="11" Y="11" Width="165" Height="400" ImageFile="logoside.png"/>
|
||||||
<Label X="185" Y="50" Width="-11" Height="32" FontId="2" DisablePrefix="yes">#(loc.InstallHeader)</Label>
|
<Label X="185" Y="50" Width="-11" Height="32" FontId="2" DisablePrefix="yes">#(loc.InstallHeader)</Label>
|
||||||
<Label X="185" Y="91" Width="-11" Height="40" FontId="3" DisablePrefix="yes">
|
<Label X="185" Y="91" Width="-11" Height="24" FontId="3" DisablePrefix="yes">
|
||||||
<Text Condition="WixStdBASuppressOptionsUI">#(loc.InstallMessage)</Text>
|
<Text Condition="WixStdBASuppressOptionsUI">#(loc.InstallMessage)</Text>
|
||||||
<Text Condition="NOT WixStdBASuppressOptionsUI">#(loc.InstallMessageOptions)</Text>
|
<Text Condition="NOT WixStdBASuppressOptionsUI">#(loc.InstallMessageOptions)</Text>
|
||||||
</Label>
|
</Label>
|
||||||
<Label X="185" Y="138" Width="-11" Height="17" FontId="3">#(loc.OptionsLocationLabel)</Label>
|
<RadioButtons Name="InstallLayout">
|
||||||
<Editbox Name="InstallFolder" X="185" Y="160" Width="-91" Height="21" TabStop="yes" FontId="3" FileSystemAutoComplete="yes" />
|
<RadioButton X="185" Y="116" Height="18" Width="-11" Value="standard" TabStop="yes" FontId="3">Standard — Program Files, data in C:\MaksIT</RadioButton>
|
||||||
<Button Name="InstallFolderBrowseButton" X="-11" Y="159" Width="75" Height="23" TabStop="yes" FontId="3">
|
<RadioButton X="185" Y="134" Height="18" Width="-11" Value="portable" TabStop="yes" FontId="3">Portable — everything in one folder</RadioButton>
|
||||||
|
</RadioButtons>
|
||||||
|
<Label X="185" Y="156" Width="-11" Height="17" FontId="3">
|
||||||
|
<Text Condition="InstallLayout = "portable"">Folder for program, scripts, logs, and settings</Text>
|
||||||
|
<Text Condition="InstallLayout <> "portable"">#(loc.OptionsLocationLabel)</Text>
|
||||||
|
</Label>
|
||||||
|
<Editbox Name="InstallFolder" X="185" Y="174" Width="-91" Height="21" TabStop="yes" FontId="3" FileSystemAutoComplete="yes" />
|
||||||
|
<Button Name="InstallFolderBrowseButton" X="-11" Y="173" Width="75" Height="23" TabStop="yes" FontId="3">
|
||||||
<Text>#(loc.OptionsBrowseButton)</Text>
|
<Text>#(loc.OptionsBrowseButton)</Text>
|
||||||
<BrowseDirectoryAction VariableName="InstallFolder" />
|
<BrowseDirectoryAction VariableName="InstallFolder" />
|
||||||
</Button>
|
</Button>
|
||||||
<Checkbox Name="InstallDesktopShortcut" X="185" Y="192" Width="-11" Height="17" TabStop="yes" FontId="3">Place a shortcut on the desktop</Checkbox>
|
<Checkbox Name="InstallDesktopShortcut" X="185" Y="200" Width="-11" Height="17" TabStop="yes" FontId="3">Place a shortcut on the desktop</Checkbox>
|
||||||
|
<Label X="185" Y="220" Width="-11" Height="32" FontId="3" DisablePrefix="yes" VisibleCondition="InstallLayout = "portable"">Scripts, logs, and settings stay in this folder (Scripts, Logs, and Data subfolders). Pick a writable location if you do not want Program Files.</Label>
|
||||||
<Hypertext Name="EulaHyperlink" X="185" Y="-111" Width="-11" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallLicenseLinkText)</Hypertext>
|
<Hypertext Name="EulaHyperlink" X="185" Y="-111" Width="-11" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallLicenseLinkText)</Hypertext>
|
||||||
<Label Name="InstallVersion" X="185" Y="-81" Width="-11" Height="17" FontId="3" DisablePrefix="yes" VisibleCondition="WixStdBAShowVersion">#(loc.InstallVersion)</Label>
|
<Label Name="InstallVersion" X="185" Y="-81" Width="-11" Height="17" FontId="3" DisablePrefix="yes" VisibleCondition="WixStdBAShowVersion">#(loc.InstallVersion)</Label>
|
||||||
<Checkbox Name="EulaAcceptCheckbox" X="185" Y="-51" Width="-11" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallAcceptCheckbox)</Checkbox>
|
<Checkbox Name="EulaAcceptCheckbox" X="185" Y="-51" Width="-11" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallAcceptCheckbox)</Checkbox>
|
||||||
|
|||||||
139
utils/plugins/Desktop/brand/brand/HyperlinkSidebarTheme.xml
Normal file
139
utils/plugins/Desktop/brand/brand/HyperlinkSidebarTheme.xml
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
|
||||||
|
|
||||||
|
|
||||||
|
<Theme xmlns="http://wixtoolset.org/schemas/v4/thmutil">
|
||||||
|
<Font Id="0" Height="-12" Weight="500" Foreground="windowtext" Background="window">Segoe UI</Font>
|
||||||
|
<Font Id="1" Height="-24" Weight="500" Foreground="windowtext">Segoe UI</Font>
|
||||||
|
<Font Id="2" Height="-22" Weight="500" Foreground="graytext">Segoe UI</Font>
|
||||||
|
<Font Id="3" Height="-12" Weight="500" Foreground="windowtext" Background="window">Segoe UI</Font>
|
||||||
|
|
||||||
|
<Window Width="600" Height="450" HexStyle="100a0000" FontId="0" Caption="#(loc.Caption)">
|
||||||
|
<Page Name="Help">
|
||||||
|
<Label X="80" Y="11" Width="-11" Height="32" FontId="1" DisablePrefix="yes">#(loc.Title)</Label>
|
||||||
|
<ImageControl X="11" Y="11" Width="64" Height="64" ImageFile="logo.png"/>
|
||||||
|
<Label X="11" Y="80" Width="-11" Height="32" FontId="2" DisablePrefix="yes">#(loc.HelpHeader)</Label>
|
||||||
|
<Label X="11" Y="121" Width="-11" Height="-35" FontId="3" DisablePrefix="yes">#(loc.HelpText)</Label>
|
||||||
|
<Button Name="HelpCloseButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">
|
||||||
|
<Text>#(loc.HelpCloseButton)</Text>
|
||||||
|
<CloseWindowAction />
|
||||||
|
</Button>
|
||||||
|
</Page>
|
||||||
|
<Page Name="Loading">
|
||||||
|
<Label X="185" Y="50" Width="-11" Height="30" FontId="2" DisablePrefix="yes" Visible="no" Name="CheckingForUpdatesLabel" />
|
||||||
|
</Page>
|
||||||
|
<Page Name="Install">
|
||||||
|
<Label X="185" Y="11" Width="-11" Height="32" FontId="1" DisablePrefix="yes">#(loc.Title)</Label>
|
||||||
|
<ImageControl X="11" Y="11" Width="165" Height="400" ImageFile="logoside.png"/>
|
||||||
|
<Label X="185" Y="50" Width="-11" Height="32" FontId="2" DisablePrefix="yes">#(loc.InstallHeader)</Label>
|
||||||
|
<Label X="185" Y="91" Width="-11" Height="40" FontId="3" DisablePrefix="yes">
|
||||||
|
<Text Condition="WixStdBASuppressOptionsUI">#(loc.InstallMessage)</Text>
|
||||||
|
<Text Condition="NOT WixStdBASuppressOptionsUI">#(loc.InstallMessageOptions)</Text>
|
||||||
|
</Label>
|
||||||
|
<Label X="185" Y="138" Width="-11" Height="17" FontId="3">#(loc.OptionsLocationLabel)</Label>
|
||||||
|
<Editbox Name="InstallFolder" X="185" Y="160" Width="-91" Height="21" TabStop="yes" FontId="3" FileSystemAutoComplete="yes" />
|
||||||
|
<Button Name="InstallFolderBrowseButton" X="-11" Y="159" Width="75" Height="23" TabStop="yes" FontId="3">
|
||||||
|
<Text>#(loc.OptionsBrowseButton)</Text>
|
||||||
|
<BrowseDirectoryAction VariableName="InstallFolder" />
|
||||||
|
</Button>
|
||||||
|
<Checkbox Name="InstallDesktopShortcut" X="185" Y="192" Width="-11" Height="17" TabStop="yes" FontId="3">Place a shortcut on the desktop</Checkbox>
|
||||||
|
<Hypertext Name="EulaHyperlink" X="185" Y="-111" Width="-11" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallLicenseLinkText)</Hypertext>
|
||||||
|
<Label Name="InstallVersion" X="185" Y="-81" Width="-11" Height="17" FontId="3" DisablePrefix="yes" VisibleCondition="WixStdBAShowVersion">#(loc.InstallVersion)</Label>
|
||||||
|
<Checkbox Name="EulaAcceptCheckbox" X="185" Y="-51" Width="-11" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallAcceptCheckbox)</Checkbox>
|
||||||
|
<Button Name="InstallUpdateButton" X="11" Y="-11" Width="200" Height="23" TabStop="yes" FontId="0" EnableCondition="WixStdBAUpdateAvailable" HideWhenDisabled="yes">#(loc.UpdateButton)</Button>
|
||||||
|
<Button Name="OptionsButton" X="-171" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" VisibleCondition="NOT WixStdBASuppressOptionsUI">
|
||||||
|
<Text>#(loc.InstallOptionsButton)</Text>
|
||||||
|
<ChangePageAction Page="Options" />
|
||||||
|
</Button>
|
||||||
|
<Button Name="InstallButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" ElevationShieldCondition='(WixBundleAuthoredScope = 1 AND NOT WixBundleElevated) OR ((WixBundleAuthoredScope = 2 OR WixBundleAuthoredScope = 3) AND WixStdBAScope = "PerMachine")'>#(loc.InstallInstallButton)</Button>
|
||||||
|
<Button Name="InstallCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">
|
||||||
|
<Text>#(loc.InstallCancelButton)</Text>
|
||||||
|
<CloseWindowAction />
|
||||||
|
</Button>
|
||||||
|
</Page>
|
||||||
|
<Page Name="Options">
|
||||||
|
<Label X="80" Y="11" Width="-11" Height="32" FontId="1" DisablePrefix="yes">#(loc.Title)</Label>
|
||||||
|
<ImageControl X="11" Y="11" Width="64" Height="64" ImageFile="logo.png"/>
|
||||||
|
<Label X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.OptionsHeader)</Label>
|
||||||
|
<RadioButtons Name="WixStdBAScope">
|
||||||
|
<RadioButton X="11" Y="180" Height="20" Width="-11" Value="PerUser" EnableCondition="WixBundleAuthoredScope = 2 OR WixBundleAuthoredScope = 3" HideWhenDisabled="yes" FontId="3">#(loc.OptionsPerUserScopeText)</RadioButton>
|
||||||
|
<RadioButton X="11" Y="200" Height="20" Width="-11" Value="PerMachine" EnableCondition="WixBundleAuthoredScope = 2 OR WixBundleAuthoredScope = 3" HideWhenDisabled="yes" FontId="3">#(loc.OptionsPerMachineScopeText)</RadioButton>
|
||||||
|
</RadioButtons>
|
||||||
|
<Button Name="OptionsOkButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">
|
||||||
|
<Text>#(loc.OptionsOkButton)</Text>
|
||||||
|
<ChangePageAction Page="Install" />
|
||||||
|
</Button>
|
||||||
|
<Button Name="OptionsCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">
|
||||||
|
<Text>#(loc.OptionsCancelButton)</Text>
|
||||||
|
<ChangePageAction Page="Install" Cancel="yes" />
|
||||||
|
</Button>
|
||||||
|
</Page>
|
||||||
|
<Page Name="Progress">
|
||||||
|
<Label X="80" Y="11" Width="-11" Height="32" FontId="1" DisablePrefix="yes">#(loc.Title)</Label>
|
||||||
|
<ImageControl X="11" Y="11" Width="64" Height="64" ImageFile="logo.png"/>
|
||||||
|
<Label X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.ProgressHeader)</Label>
|
||||||
|
<Label X="11" Y="141" Width="70" Height="17" FontId="3" DisablePrefix="yes">#(loc.ProgressLabel)</Label>
|
||||||
|
<Label Name="OverallProgressPackageText" X="85" Y="141" Width="-11" Height="17" FontId="3" DisablePrefix="yes">#(loc.OverallProgressPackageText)</Label>
|
||||||
|
<Progressbar Name="OverallCalculatedProgressbar" X="11" Y="163" Width="-11" Height="20" />
|
||||||
|
<Button Name="ProgressCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ProgressCancelButton)</Button>
|
||||||
|
</Page>
|
||||||
|
<Page Name="Modify">
|
||||||
|
<Label X="185" Y="11" Width="-11" Height="32" FontId="1" DisablePrefix="yes">#(loc.Title)</Label>
|
||||||
|
<ImageControl X="11" Y="11" Width="165" Height="400" ImageFile="logoside.png"/>
|
||||||
|
<Label X="185" Y="50" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.ModifyHeader)</Label>
|
||||||
|
<Button Name="ModifyUpdateButton" X="11" Y="-11" Width="200" Height="23" TabStop="yes" FontId="0" EnableCondition="WixStdBAUpdateAvailable" HideWhenDisabled="yes">#(loc.UpdateButton)</Button>
|
||||||
|
<Button Name="RepairButton" X="-171" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes" ElevationShieldCondition='(WixBundleAuthoredScope = 1 AND NOT WixBundleElevated) OR ((WixBundleAuthoredScope = 2 OR WixBundleAuthoredScope = 3) AND WixStdBAScope = "PerMachine")'>#(loc.ModifyRepairButton)</Button>
|
||||||
|
<Button Name="UninstallButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" ElevationShieldCondition='(WixBundleAuthoredScope = 1 AND NOT WixBundleElevated) OR ((WixBundleAuthoredScope = 2 OR WixBundleAuthoredScope = 3) AND WixStdBAScope = "PerMachine")'>#(loc.ModifyUninstallButton)</Button>
|
||||||
|
<Button Name="ModifyCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">
|
||||||
|
<Text>#(loc.ModifyCancelButton)</Text>
|
||||||
|
<CloseWindowAction />
|
||||||
|
</Button>
|
||||||
|
</Page>
|
||||||
|
<Page Name="Success">
|
||||||
|
<Label X="185" Y="11" Width="-11" Height="32" FontId="1" DisablePrefix="yes">#(loc.Title)</Label>
|
||||||
|
<ImageControl X="11" Y="11" Width="165" Height="400" ImageFile="logoside.png"/>
|
||||||
|
<Label X="185" Y="50" Width="-11" Height="30" FontId="2" DisablePrefix="yes">
|
||||||
|
<Text>#(loc.SuccessHeader)</Text>
|
||||||
|
<Text Condition="WixBundleAction = 2">#(loc.SuccessLayoutHeader)</Text>
|
||||||
|
<Text Condition="WixBundleAction = 3">#(loc.SuccessUnsafeUninstallHeader)</Text>
|
||||||
|
<Text Condition="WixBundleAction = 4">#(loc.SuccessUninstallHeader)</Text>
|
||||||
|
<Text Condition="WixBundleAction = 5">#(loc.SuccessCacheHeader)</Text>
|
||||||
|
<Text Condition="WixBundleAction = 6">#(loc.SuccessInstallHeader)</Text>
|
||||||
|
<Text Condition="WixBundleAction = 7">#(loc.SuccessModifyHeader)</Text>
|
||||||
|
<Text Condition="WixBundleAction = 8">#(loc.SuccessRepairHeader)</Text>
|
||||||
|
</Label>
|
||||||
|
<Button Name="LaunchButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.SuccessLaunchButton)</Button>
|
||||||
|
<Label X="185" Y="-51" Width="400" Height="34" FontId="3" DisablePrefix="yes" VisibleCondition="WixStdBARestartRequired">
|
||||||
|
<Text>#(loc.SuccessRestartText)</Text>
|
||||||
|
<Text Condition="WixBundleAction = 3">#(loc.SuccessUninstallRestartText)</Text>
|
||||||
|
</Label>
|
||||||
|
<Button Name="SuccessRestartButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.SuccessRestartButton)</Button>
|
||||||
|
<Button Name="SuccessCloseButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">
|
||||||
|
<Text>#(loc.SuccessCloseButton)</Text>
|
||||||
|
<CloseWindowAction />
|
||||||
|
</Button>
|
||||||
|
</Page>
|
||||||
|
<Page Name="Failure">
|
||||||
|
<Label X="185" Y="11" Width="-11" Height="32" FontId="1" DisablePrefix="yes">#(loc.Title)</Label>
|
||||||
|
<ImageControl X="11" Y="11" Width="165" Height="400" ImageFile="logoside.png"/>
|
||||||
|
<Label X="185" Y="50" Width="-11" Height="30" FontId="2" DisablePrefix="yes">
|
||||||
|
<Text>#(loc.FailureHeader)</Text>
|
||||||
|
<Text Condition="WixBundleAction = 2">#(loc.FailureLayoutHeader)</Text>
|
||||||
|
<Text Condition="WixBundleAction = 3">#(loc.FailureUnsafeUninstallHeader)</Text>
|
||||||
|
<Text Condition="WixBundleAction = 4">#(loc.FailureUninstallHeader)</Text>
|
||||||
|
<Text Condition="WixBundleAction = 5">#(loc.FailureCacheHeader)</Text>
|
||||||
|
<Text Condition="WixBundleAction = 6">#(loc.FailureInstallHeader)</Text>
|
||||||
|
<Text Condition="WixBundleAction = 7">#(loc.FailureModifyHeader)</Text>
|
||||||
|
<Text Condition="WixBundleAction = 8">#(loc.FailureRepairHeader)</Text>
|
||||||
|
</Label>
|
||||||
|
<Hypertext Name="FailureLogFileLink" X="185" Y="121" Width="-11" Height="68" FontId="3" TabStop="yes" HideWhenDisabled="yes">#(loc.FailureHyperlinkLogText)</Hypertext>
|
||||||
|
<Hypertext Name="FailureMessageText" X="185" Y="-115" Width="-11" Height="80" FontId="3" TabStop="yes" HideWhenDisabled="yes" />
|
||||||
|
<Label X="185" Y="-57" Width="-11" Height="80" FontId="3" DisablePrefix="yes" VisibleCondition="WixStdBARestartRequired">#(loc.FailureRestartText)</Label>
|
||||||
|
<Button Name="FailureRestartButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.FailureRestartButton)</Button>
|
||||||
|
<Button Name="FailureCloseButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">
|
||||||
|
<Text>#(loc.FailureCloseButton)</Text>
|
||||||
|
<CloseWindowAction />
|
||||||
|
</Button>
|
||||||
|
</Page>
|
||||||
|
</Window>
|
||||||
|
</Theme>
|
||||||
BIN
utils/plugins/Desktop/brand/brand/logo-64.png
Normal file
BIN
utils/plugins/Desktop/brand/brand/logo-64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
utils/plugins/Desktop/brand/brand/mark.ico
Normal file
BIN
utils/plugins/Desktop/brand/brand/mark.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
utils/plugins/Desktop/brand/brand/mark.png
Normal file
BIN
utils/plugins/Desktop/brand/brand/mark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
27
utils/plugins/Desktop/brand/brand/mark.svg
Normal file
27
utils/plugins/Desktop/brand/brand/mark.svg
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="MAKS.IT">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="hi" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0" stop-color="#33A5CF"/>
|
||||||
|
<stop offset="1" stop-color="#006199"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="mid" x1="0.2" y1="0" x2="0.9" y2="1">
|
||||||
|
<stop offset="0" stop-color="#0795C5"/>
|
||||||
|
<stop offset="1" stop-color="#003D7A"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="deep" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#00427E"/>
|
||||||
|
<stop offset="1" stop-color="#002A6A"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="navy" x1="1" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#00306F"/>
|
||||||
|
<stop offset="1" stop-color="#001428"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<!-- Origami M: sharp facets, light from upper-left. No wordmark. -->
|
||||||
|
<polygon fill="url(#hi)" points="28,40 78,40 62,216 12,216"/>
|
||||||
|
<polygon fill="url(#navy)" points="78,40 118,128 102,216 62,216"/>
|
||||||
|
<polygon fill="url(#mid)" points="118,128 158,40 174,216 102,216"/>
|
||||||
|
<polygon fill="url(#hi)" points="158,40 228,40 244,216 174,216"/>
|
||||||
|
<polygon fill="url(#deep)" points="28,40 78,40 70,128 20,128"/>
|
||||||
|
<polygon fill="url(#navy)" points="158,40 228,40 220,128 166,128"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
utils/plugins/Desktop/brand/brand/sidebar.png
Normal file
BIN
utils/plugins/Desktop/brand/brand/sidebar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Loading…
Reference in New Issue
Block a user