mirror of
https://github.com/yuzu-emu/liftinstall.git
synced 2025-11-04 23:24:51 +00:00
Compare commits
190 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f46fed17b7 | ||
|
|
31b3e7e3a6 | ||
|
|
fec5587b87 | ||
|
|
d28b19c25a | ||
|
|
08ab3c369e | ||
|
|
d8df3b3114 | ||
|
|
61a7db2005 | ||
|
|
7b8bf579f2 | ||
|
|
3ea6aa9852 | ||
|
|
faeb4885bf | ||
|
|
85ed6275f8 | ||
|
|
f3d0d06a09 | ||
|
|
8c795396eb | ||
|
|
e6600e3b17 | ||
|
|
89be1c0d84 | ||
|
|
42e092f54d | ||
|
|
278af40b37 | ||
|
|
8917ba88ca | ||
|
|
b87dab83d8 | ||
|
|
79b799f655 | ||
|
|
c61c068ed0 | ||
|
|
3727e4185b | ||
|
|
d6cb916a9c | ||
|
|
d269677b2c | ||
|
|
9a27b24f05 | ||
|
|
3fc8583646 | ||
|
|
a3f0d0f999 | ||
|
|
d194ed5dd5 | ||
|
|
0decda8232 | ||
|
|
2103a8ec15 | ||
|
|
a86bd209a8 | ||
|
|
eff81e6d99 | ||
|
|
d63473ec9c | ||
|
|
1fd97b6e42 | ||
|
|
0cfa44330d | ||
|
|
679312f101 | ||
|
|
2120abf299 | ||
|
|
a8db5ff8c4 | ||
|
|
faba49c025 | ||
|
|
3196736d36 | ||
|
|
f809e6cb23 | ||
|
|
77a26c1496 | ||
|
|
e990138200 | ||
|
|
6e7d045794 | ||
|
|
0d4022d348 | ||
|
|
109322836b | ||
|
|
fbf7640657 | ||
|
|
2d42189e5e | ||
|
|
dde96db57c | ||
|
|
a816cbe767 | ||
|
|
95ee7a1739 | ||
|
|
061944079b | ||
|
|
e54199ad6f | ||
|
|
4ed1ffb5c3 | ||
|
|
2958c583af | ||
|
|
825e9cc1c3 | ||
|
|
810ef5fb25 | ||
|
|
89e1b2f91f | ||
|
|
f13b2fe93d | ||
|
|
df0414b26e | ||
|
|
a9de893cca | ||
|
|
bdda585f12 | ||
|
|
27aa9924f3 | ||
|
|
322f72609f | ||
|
|
ca994e49d3 | ||
|
|
41918c709c | ||
|
|
48fa172169 | ||
|
|
928661db77 | ||
|
|
45c562d723 | ||
|
|
c7628c1474 | ||
|
|
34fd140a9e | ||
|
|
cd7fb8de28 | ||
|
|
01419e5da4 | ||
|
|
3ce4504a5b | ||
|
|
5003edd43d | ||
|
|
87efd394a1 | ||
|
|
8e212460d8 | ||
|
|
4bb84d98b3 | ||
|
|
1dbf078728 | ||
|
|
8e8d729019 | ||
|
|
9fcfe0c77b | ||
|
|
74cefc277e | ||
|
|
adbd4a304d | ||
|
|
351be36f05 | ||
|
|
9866a32c10 | ||
|
|
3537b5823f | ||
|
|
5ff1486f69 | ||
|
|
7acefbc8cb | ||
|
|
ef71b707cb | ||
|
|
c68ebcb61e | ||
|
|
93e24ea06a | ||
|
|
d9d8b92cc4 | ||
|
|
76a77d3caf | ||
|
|
ea8b631aa2 | ||
|
|
f848e8fb53 | ||
|
|
d9e4e5ecc2 | ||
|
|
6210a2668f | ||
|
|
d2399d97e4 | ||
|
|
b9e825faa5 | ||
|
|
630f2231ab | ||
|
|
91fb88aa98 | ||
|
|
713b85b59a | ||
|
|
ca6ac320c2 | ||
|
|
9999c52ea8 | ||
|
|
6cae746192 | ||
|
|
732e344605 | ||
|
|
9b58c273d1 | ||
|
|
b356f0057f | ||
|
|
d339816695 | ||
|
|
d2ad619d87 | ||
|
|
128c1b1f41 | ||
|
|
37d27a82ba | ||
|
|
eb556c8cab | ||
|
|
6af46ec703 | ||
|
|
db2176763d | ||
|
|
fccd1c9bd2 | ||
|
|
0e190ecdc6 | ||
|
|
f89cb19602 | ||
|
|
7392e1ef91 | ||
|
|
9cf5e745d4 | ||
|
|
de4246536e | ||
|
|
30f817e4fa | ||
|
|
6845ed9ad7 | ||
|
|
1639e74b98 | ||
|
|
c176658e28 | ||
|
|
d79fd3e6e1 | ||
|
|
d3c3b77e6b | ||
|
|
561f0071bd | ||
|
|
e72a5f0420 | ||
|
|
2b4b59320e | ||
|
|
004a49587c | ||
|
|
d20c17964e | ||
|
|
288518cd78 | ||
|
|
fc40f6691c | ||
|
|
74cecab186 | ||
|
|
9bec77a2db | ||
|
|
5409b32bf0 | ||
|
|
a7057dfed3 | ||
|
|
c4b4c597fa | ||
|
|
26997ba229 | ||
|
|
3bd85bac8d | ||
|
|
474fb71efd | ||
|
|
548daa1b2b | ||
|
|
bdbab4dc4d | ||
|
|
ca8defda7e | ||
|
|
6853ade29c | ||
|
|
b6122349d6 | ||
|
|
12081db009 | ||
|
|
3abc0a1b11 | ||
|
|
56cdaabbae | ||
|
|
a02e8a1624 | ||
|
|
4b158036da | ||
|
|
eb6475bac6 | ||
|
|
c7cef0b49d | ||
|
|
2ee02bbf46 | ||
|
|
6d443805fc | ||
|
|
c4139f7e37 | ||
|
|
6272c294c8 | ||
|
|
5d31fd0129 | ||
|
|
e69443c22e | ||
|
|
c8699b6e62 | ||
|
|
e83cf6cf4e | ||
|
|
9a28807423 | ||
|
|
b3b686ed53 | ||
|
|
f80db92188 | ||
|
|
4578450bff | ||
|
|
5603981af1 | ||
|
|
341a6a6537 | ||
|
|
27d0a05ade | ||
|
|
6c19b8b0d1 | ||
|
|
ff574c9d73 | ||
|
|
761ce91299 | ||
|
|
44e0ebdab4 | ||
|
|
270a17cd86 | ||
|
|
f24d1112dd | ||
|
|
3109d48dce | ||
|
|
5d53ef7a2e | ||
|
|
4d50a0f8f8 | ||
|
|
a447ef25b6 | ||
|
|
9d1f4c2576 | ||
|
|
30bb49e1fb | ||
|
|
8b6c2c1708 | ||
|
|
0d63e0ab21 | ||
|
|
269b083ec8 | ||
|
|
ae63bc7dab | ||
|
|
d3fb463f20 | ||
|
|
137d2ec539 | ||
|
|
fed2d28aa8 | ||
|
|
66e2473a40 | ||
|
|
d236eeec0c |
58
.github/workflows/test-build.yml
vendored
Normal file
58
.github/workflows/test-build.yml
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
name: Rust
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
|
||||
steps:
|
||||
- uses: hecrj/setup-rust-action@master
|
||||
with:
|
||||
rust-version: stable
|
||||
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.0-dev libssl-dev libappindicator3-dev
|
||||
if: runner.os == 'Linux'
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Get cargo cache directory path
|
||||
id: cargo-cache-dir-path
|
||||
run: echo "::set-output name=dir::$HOME/.cargo/"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: cargo-cache
|
||||
with:
|
||||
path: ${{ steps.cargo-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '16.x'
|
||||
- run: npm install -g yarn
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Download Webview2
|
||||
run: Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile 'MicrosoftEdgeWebview2Setup.exe'
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -5,3 +5,5 @@
|
|||
**/*.rs.bk
|
||||
|
||||
*.log
|
||||
|
||||
*.exe
|
||||
|
|
|
|||
27
.travis.yml
27
.travis.yml
|
|
@ -1,27 +0,0 @@
|
|||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
language: cpp
|
||||
sudo: required
|
||||
dist: trusty
|
||||
services: docker
|
||||
install: docker pull rust:1
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cargo
|
||||
- $TRAVIS_BUILD_DIR/ui/node_modules
|
||||
script: docker run -v $HOME/.cargo:/root/.cargo -v $(pwd):/liftinstall rust:1 /bin/bash -ex /liftinstall/.travis/build.sh
|
||||
|
||||
- os: osx
|
||||
language: rust
|
||||
cache: cargo
|
||||
osx_image: xcode10
|
||||
script: brew install yarn && cargo build
|
||||
|
||||
- os: windows
|
||||
language: rust
|
||||
cache: cargo
|
||||
script:
|
||||
- choco install nodejs yarn
|
||||
- export PATH="$PROGRAMFILES/nodejs/:$PROGRAMFILES (x86)/Yarn/bin/:$PATH"
|
||||
- cargo build
|
||||
|
|
@ -1,15 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
cd /liftinstall || exit 1
|
||||
|
||||
# setup NodeJS
|
||||
curl -sL https://deb.nodesource.com/setup_12.x | bash -
|
||||
# setup Yarn
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list
|
||||
|
||||
apt-get update
|
||||
apt-get install -y libwebkit2gtk-4.0-dev libssl-dev nodejs yarn
|
||||
|
||||
yarn --cwd ui
|
||||
|
||||
cargo build
|
||||
cargo build --release
|
||||
|
|
|
|||
4
.travis/exec.sh
Normal file
4
.travis/exec.sh
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash -ex
|
||||
|
||||
# the UID for the container yuzu user is 1027
|
||||
docker run -u root -v $(pwd):/liftinstall -t yuzuemu/build-environments:linux-liftinstall /bin/bash /liftinstall/.travis/build.sh
|
||||
8
.tx/config
Executable file
8
.tx/config
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[o:yuzu-emulator:p:yuzu:r:installer]
|
||||
file_filter = ui/translations/<lang>.po
|
||||
source_file = ui/translations/en.po
|
||||
source_lang = en
|
||||
type = PO
|
||||
4039
Cargo.lock
generated
4039
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
80
Cargo.toml
80
Cargo.toml
|
|
@ -1,58 +1,84 @@
|
|||
[package]
|
||||
name = "liftinstall"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2018"
|
||||
authors = ["James <jselby@jselby.net>"]
|
||||
repository = "https://github.com/j-selby/liftinstall.git"
|
||||
documentation = "https://liftinstall.jselby.net"
|
||||
description = "An adaptable installer for your application."
|
||||
build = "build.rs"
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
web-view = {git = "https://github.com/j-selby/web-view.git", rev = "752106e4637356cbdb39a0bf1113ea3ae8a14243"}
|
||||
anyhow = "^1"
|
||||
wry = "0.12"
|
||||
tinyfiledialogs = "3.8"
|
||||
|
||||
hyper = "0.11.27"
|
||||
futures = "0.1.25"
|
||||
mime_guess = "1.8.6"
|
||||
url = "1.7.2"
|
||||
futures = "0.1.29"
|
||||
mime_guess = "2.0"
|
||||
url = "2.2"
|
||||
|
||||
reqwest = "0.9.12"
|
||||
number_prefix = "0.3.0"
|
||||
reqwest = "0.9.22"
|
||||
number_prefix = "0.4"
|
||||
|
||||
serde = "1.0.89"
|
||||
serde_derive = "1.0.89"
|
||||
serde_json = "1.0.39"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
|
||||
toml = "0.5.0"
|
||||
toml = "0.5"
|
||||
|
||||
semver = {version = "0.9.0", features = ["serde"]}
|
||||
regex = "1.1.5"
|
||||
semver = {version = "1.0", features = ["serde"]}
|
||||
regex = "1.4"
|
||||
|
||||
dirs = "1.0.5"
|
||||
zip = "0.5.1"
|
||||
xz2 = "0.1.6"
|
||||
dirs = "^4"
|
||||
zip = "0.6"
|
||||
xz2 = "0.1"
|
||||
tar = "0.4"
|
||||
|
||||
log = "0.4"
|
||||
fern = "0.5"
|
||||
chrono = "0.4.6"
|
||||
fern = "0.6"
|
||||
chrono = "0.4"
|
||||
|
||||
clap = "2.32.0"
|
||||
clap = "2.33"
|
||||
|
||||
# used to open a link to the users default browser
|
||||
webbrowser = "0.8"
|
||||
# used in JWT based package authentication
|
||||
jsonwebtoken = "^8"
|
||||
# used to decode the public key for verifying JWT tokens
|
||||
base64 = "0.13"
|
||||
|
||||
[build-dependencies]
|
||||
walkdir = "2.2.7"
|
||||
serde = "1.0.89"
|
||||
serde_derive = "1.0.89"
|
||||
toml = "0.5.0"
|
||||
which = "2.0.1"
|
||||
walkdir = "2.3"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
toml = "0.5"
|
||||
which = "4.0"
|
||||
image = { version = "0.24", default-features = false, features = ["ico"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winapi = { version = "0.3", features = ["psapi", "winbase", "winioctl", "winnt"] }
|
||||
widestring = "0.4.0"
|
||||
widestring = "^1"
|
||||
webview2 = "0.1"
|
||||
tempfile = "3"
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
sysinfo = "0.8.2"
|
||||
slug = "0.1.4"
|
||||
sysinfo = "0.26"
|
||||
slug = "0.1"
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
winres = "0.1"
|
||||
cc = "1.0"
|
||||
|
||||
[profile.release]
|
||||
#panic = "abort"
|
||||
lto = true
|
||||
opt-level = "z"
|
||||
codegen-units = 1
|
||||
incremental = false
|
||||
|
||||
#[profile.release.overrides."*"] # +
|
||||
#opt-level = "z"
|
||||
#codegen-units = 1
|
||||
#incremental = false
|
||||
|
|
|
|||
18
Justfile
Normal file
18
Justfile
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
ui-build:
|
||||
yarn --cwd {{ justfile_directory() }}/ui/ install
|
||||
yarn --cwd {{ justfile_directory() }}/ui/ build
|
||||
|
||||
ui-test:
|
||||
cd {{ justfile_directory() }}/ui/ && node mock-server.js &
|
||||
yarn --cwd {{ justfile_directory() }}/ui/ serve
|
||||
|
||||
update-i18n:
|
||||
#!/bin/bash -e
|
||||
[ -z "${TX_PULL}" ] || tx pull -a --minimum-perc 85
|
||||
for i in {{ justfile_directory() }}/ui/translations/*.po; do
|
||||
TARGET_FILE="$(basename $i)"
|
||||
TARGET_LANG="${TARGET_FILE/.po/}"
|
||||
OUTPUT="{{ justfile_directory() }}/ui/src/locales/${TARGET_LANG}.json"
|
||||
i18next-conv -l en -s "$i" -t "$OUTPUT" -K
|
||||
node {{ justfile_directory() }}/ui/unbreak-translations.js "$OUTPUT" "$OUTPUT"
|
||||
done
|
||||
13
SECURITY.md
Normal file
13
SECURITY.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
As `liftinstall` is a template for your project, no specific versioning is
|
||||
provided at this time, though a rough version is given in the Cargo file.
|
||||
|
||||
Only the latest version from this file will be supported.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
For any specific security concerns/vulnerabilities, please email me directly
|
||||
at security *at* jselby.net.
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
name = "yuzu"
|
||||
target_url = "https://raw.githubusercontent.com/yuzu-emu/liftinstall/master/config.linux.v1.toml"
|
||||
target_url = "https://raw.githubusercontent.com/yuzu-emu/liftinstall/master/config.linux.v3.toml"
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
name = "yuzu"
|
||||
target_url = "https://raw.githubusercontent.com/yuzu-emu/liftinstall/master/config.windows.v7.toml"
|
||||
target_url = "https://raw.githubusercontent.com/yuzu-emu/liftinstall/master/config.windows.v10.toml"
|
||||
|
|
|
|||
35
build.rs
35
build.rs
|
|
@ -12,6 +12,7 @@ extern crate toml;
|
|||
extern crate which;
|
||||
|
||||
use std::env;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use std::fs::copy;
|
||||
|
|
@ -22,6 +23,8 @@ use std::process::Command;
|
|||
|
||||
use std::env::consts::OS;
|
||||
|
||||
use image::imageops::FilterType;
|
||||
|
||||
/// Describes the application itself.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct BaseAttributes {
|
||||
|
|
@ -46,6 +49,8 @@ fn handle_binary(config: &BaseAttributes) {
|
|||
|
||||
cc::Build::new()
|
||||
.cpp(true)
|
||||
.define("_WIN32_WINNT", Some("0x0600"))
|
||||
.define("WINVER", Some("0x0600"))
|
||||
.file("src/native/interop.cpp")
|
||||
.compile("interop");
|
||||
}
|
||||
|
|
@ -60,6 +65,13 @@ fn main() {
|
|||
|
||||
let os = OS.to_lowercase();
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if std::fs::metadata("MicrosoftEdgeWebview2Setup.exe").is_err() {
|
||||
panic!("Please download MicrosoftEdgeWebview2Setup.exe from https://go.microsoft.com/fwlink/p/?LinkId=2124703 and put the file at the workspace root!");
|
||||
}
|
||||
}
|
||||
|
||||
// Find target config
|
||||
let target_config = PathBuf::from(format!("bootstrap.{}.toml", os));
|
||||
|
||||
|
|
@ -87,8 +99,16 @@ fn main() {
|
|||
// Copy for the main build
|
||||
copy(&target_config, output_dir.join("bootstrap.toml")).expect("Unable to copy config file");
|
||||
|
||||
let yarn_binary = which::which("yarn")
|
||||
.expect("Failed to find yarn - please go ahead and install it!");
|
||||
let yarn_binary =
|
||||
which::which("yarn").expect("Failed to find yarn - please go ahead and install it!");
|
||||
|
||||
// bundle the icon
|
||||
let mut f = File::create(output_dir.join("icon-data.bin")).unwrap();
|
||||
let icon_file = image::open("ui/public/favicon.ico").expect("Unable to read the icon file");
|
||||
let icon_data = icon_file
|
||||
.resize_exact(48, 48, FilterType::Triangle)
|
||||
.to_rgba8();
|
||||
f.write_all(&icon_data.into_vec()).unwrap();
|
||||
|
||||
// Build and deploy frontend files
|
||||
Command::new(&yarn_binary)
|
||||
|
|
@ -100,8 +120,9 @@ fn main() {
|
|||
.arg(ui_dir.to_str().expect("Unable to covert path"))
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait().expect("Unable to install Node.JS dependencies using Yarn");
|
||||
Command::new(&yarn_binary)
|
||||
.wait()
|
||||
.expect("Unable to install Node.JS dependencies using Yarn");
|
||||
let return_code = Command::new(&yarn_binary)
|
||||
.args(&[
|
||||
"--cwd",
|
||||
ui_dir.to_str().expect("Unable to covert path"),
|
||||
|
|
@ -113,7 +134,7 @@ fn main() {
|
|||
.to_str()
|
||||
.expect("Unable to convert path"),
|
||||
])
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait().expect("Unable to build frontend assets using Webpack");
|
||||
.status()
|
||||
.expect("Unable to build frontend assets using Webpack");
|
||||
assert!(return_code.success());
|
||||
}
|
||||
|
|
|
|||
52
config.linux.v2.toml
Normal file
52
config.linux.v2.toml
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
|
||||
hide_advanced = true
|
||||
|
||||
[authentication]
|
||||
# Base64 encoded version of the public key for validating the JWT token. Must be in DER format
|
||||
pub_key_base64 = "MIIBCgKCAQEAs5K6s49JVV9LBMzDrkORsoPSYsv1sCXDtxjp4pn8p0uPSvJAsbNNmdIgCjfSULzbHLM28MblnI4zYP8ZgKtkjdg+Ic5WQbS5iBAkf18zMafpOrotTArLsgZSmUfNYt0SOiN17D+sq/Ov/CKXRM9CttKkEbanBTVqkx7sxsHVbkI6tDvkboSaNeVPHzHlfAbvGrUo5cbAFCB/KnRsoxr+g7jLKTxU1w4xb/pIs91h80AXV/yZPXL6ItPM3/0noIRXjmoeYWf2sFQaFALNB2Kef0p6/hoHYUQP04ZSIL3Q+v13z5X2YJIlI4eLg+iD25QYm9V8oP3+Xro4vd47a0/maQIDAQAB"
|
||||
# URL to authenticate against. This must return a JWT token with their permissions and a custom claim patreonInfo with the following structure
|
||||
# "patreonInfo": { "linked": false, "activeSubscription": false }
|
||||
# If successful, the frontend will use this JWT token as a Bearer Authentication when requesting the binaries to download
|
||||
auth_url = "https://api.yuzu-emu.org/jwt/installer/"
|
||||
[authentication.validation]
|
||||
iss = "citra-core"
|
||||
aud = "installer"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu"
|
||||
description = "Includes frequent updates to yuzu with all the latest reviewed and tested features."
|
||||
default = true
|
||||
[packages.source]
|
||||
name = "github"
|
||||
match = "^yuzu-linux-[0-9]*-[0-9a-f]*.tar.xz$"
|
||||
[packages.source.config]
|
||||
repo = "yuzu-emu/yuzu-mainline"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu"
|
||||
relative_path = "yuzu-windows-msvc/yuzu.exe"
|
||||
description = "Launch yuzu"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu Early Access"
|
||||
description = "Bonus preview release for project supporters. Thanks for your support!"
|
||||
# Displayed when the package has no authentication for the user
|
||||
need_authentication_description = "Click here to sign in with your yuzu account for Early Access"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_link_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_subscription_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_reward_tier_description = "You are signed in, but are not backing an eligible reward tier! Click here for more details"
|
||||
requires_authorization = true
|
||||
# puts a "new" ribbon the package select
|
||||
is_new = true
|
||||
[packages.source]
|
||||
name = "patreon"
|
||||
match = "^yuzu-linux-[0-9]*-[0-9a-f]*.tar.xz$"
|
||||
[packages.source.config]
|
||||
repo = "earlyaccess"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu Early Access"
|
||||
relative_path = "yuzu-linux-earlyaccess/yuzu.exe"
|
||||
description = "Launch yuzu Early Access"
|
||||
|
||||
58
config.linux.v3.toml
Normal file
58
config.linux.v3.toml
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
|
||||
hide_advanced = true
|
||||
|
||||
[authentication]
|
||||
# Base64 encoded version of the public key for validating the JWT token. Must be in DER format
|
||||
pub_key_base64 = "MIIBCgKCAQEAs5K6s49JVV9LBMzDrkORsoPSYsv1sCXDtxjp4pn8p0uPSvJAsbNNmdIgCjfSULzbHLM28MblnI4zYP8ZgKtkjdg+Ic5WQbS5iBAkf18zMafpOrotTArLsgZSmUfNYt0SOiN17D+sq/Ov/CKXRM9CttKkEbanBTVqkx7sxsHVbkI6tDvkboSaNeVPHzHlfAbvGrUo5cbAFCB/KnRsoxr+g7jLKTxU1w4xb/pIs91h80AXV/yZPXL6ItPM3/0noIRXjmoeYWf2sFQaFALNB2Kef0p6/hoHYUQP04ZSIL3Q+v13z5X2YJIlI4eLg+iD25QYm9V8oP3+Xro4vd47a0/maQIDAQAB"
|
||||
# URL to authenticate against. This must return a JWT token with their permissions and a custom claim patreonInfo with the following structure
|
||||
# "patreonInfo": { "linked": false, "activeSubscription": false }
|
||||
# If successful, the frontend will use this JWT token as a Bearer Authentication when requesting the binaries to download
|
||||
auth_url = "https://api.yuzu-emu.org/jwt/installer/"
|
||||
[authentication.validation]
|
||||
iss = "citra-core"
|
||||
aud = "installer"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu Early Access"
|
||||
description = "Preview release with the newest features for the supporters."
|
||||
icon = "thicc_logo_installer__ea_shadow.png"
|
||||
requires_authorization = true
|
||||
# puts a "new" ribbon the package select
|
||||
is_new = true
|
||||
[packages.extended_description]
|
||||
no_action_description = "Thank you for your support!"
|
||||
# Displayed when the package has no authentication for the user
|
||||
need_authentication_description = "Click here to sign in with your yuzu account for Early Access"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_link_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_subscription_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_reward_tier_description = "You are signed in, but are not backing an eligible reward tier! Click here for more details"
|
||||
|
||||
[packages.source]
|
||||
name = "patreon"
|
||||
match = "^yuzu-linux-[0-9]*-[0-9a-f]*.tar.xz$"
|
||||
[packages.source.config]
|
||||
repo = "earlyaccess"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu Early Access"
|
||||
relative_path = "yuzu-linux-early-access/yuzu-early-access.AppImage"
|
||||
description = "Launch yuzu Early Access"
|
||||
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu"
|
||||
description = "Includes frequent updates to yuzu with all the latest reviewed and tested features."
|
||||
icon = "thicc_logo_installer_shadow.png"
|
||||
default = true
|
||||
[packages.source]
|
||||
name = "github"
|
||||
match = "^yuzu-linux-[0-9]*-[0-9a-f]*.tar.xz$"
|
||||
[packages.source.config]
|
||||
repo = "yuzu-emu/yuzu-mainline"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu"
|
||||
relative_path = "yuzu-linux-mainline/yuzu-mainline.AppImage"
|
||||
description = "Launch yuzu"
|
||||
|
||||
58
config.windows.v10.toml
Normal file
58
config.windows.v10.toml
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
|
||||
hide_advanced = true
|
||||
|
||||
[authentication]
|
||||
# Base64 encoded version of the public key for validating the JWT token. Must be in DER format
|
||||
pub_key_base64 = "MIIBCgKCAQEAs5K6s49JVV9LBMzDrkORsoPSYsv1sCXDtxjp4pn8p0uPSvJAsbNNmdIgCjfSULzbHLM28MblnI4zYP8ZgKtkjdg+Ic5WQbS5iBAkf18zMafpOrotTArLsgZSmUfNYt0SOiN17D+sq/Ov/CKXRM9CttKkEbanBTVqkx7sxsHVbkI6tDvkboSaNeVPHzHlfAbvGrUo5cbAFCB/KnRsoxr+g7jLKTxU1w4xb/pIs91h80AXV/yZPXL6ItPM3/0noIRXjmoeYWf2sFQaFALNB2Kef0p6/hoHYUQP04ZSIL3Q+v13z5X2YJIlI4eLg+iD25QYm9V8oP3+Xro4vd47a0/maQIDAQAB"
|
||||
# URL to authenticate against. This must return a JWT token with their permissions and a custom claim patreonInfo with the following structure
|
||||
# "patreonInfo": { "linked": false, "activeSubscription": false }
|
||||
# If successful, the frontend will use this JWT token as a Bearer Authentication when requesting the binaries to download
|
||||
auth_url = "https://api.yuzu-emu.org/jwt/installer/"
|
||||
[authentication.validation]
|
||||
iss = "citra-core"
|
||||
aud = "installer"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu Early Access"
|
||||
description = "Preview release with the newest features for the supporters."
|
||||
icon = "thicc_logo_installer__ea_shadow.png"
|
||||
requires_authorization = true
|
||||
# puts a "new" ribbon the package select
|
||||
is_new = true
|
||||
[packages.extended_description]
|
||||
no_action_description = "Thank you for your support!"
|
||||
# Displayed when the package has no authentication for the user
|
||||
need_authentication_description = "Click here to sign in with your yuzu account for Early Access"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_link_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_subscription_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_reward_tier_description = "You are signed in, but are not backing an eligible reward tier! Click here for more details"
|
||||
|
||||
[packages.source]
|
||||
name = "patreon"
|
||||
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
|
||||
[packages.source.config]
|
||||
repo = "earlyaccess"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu Early Access"
|
||||
relative_path = "yuzu-windows-msvc-early-access/yuzu.exe"
|
||||
description = "Launch yuzu Early Access"
|
||||
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu"
|
||||
description = "Includes frequent updates to yuzu with all the latest reviewed and tested features."
|
||||
icon = "thicc_logo_installer_shadow.png"
|
||||
default = true
|
||||
[packages.source]
|
||||
name = "github"
|
||||
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
|
||||
[packages.source.config]
|
||||
repo = "yuzu-emu/yuzu-mainline"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu"
|
||||
relative_path = "yuzu-windows-msvc/yuzu.exe"
|
||||
description = "Launch yuzu"
|
||||
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
|
||||
hide_advanced = true
|
||||
new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.6/yuzu_install.exe"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu Nightly"
|
||||
|
|
|
|||
17
config.windows.v8.toml
Normal file
17
config.windows.v8.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
|
||||
hide_advanced = true
|
||||
new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.7/yuzu_install.exe"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu"
|
||||
description = "Includes frequent updates to yuzu with all the latest reviewed and tested features."
|
||||
default = true
|
||||
[packages.source]
|
||||
name = "github"
|
||||
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
|
||||
[packages.source.config]
|
||||
repo = "yuzu-emu/yuzu-mainline"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu"
|
||||
relative_path = "yuzu-windows-msvc/yuzu.exe"
|
||||
description = "Launch yuzu"
|
||||
52
config.windows.v9.toml
Normal file
52
config.windows.v9.toml
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
|
||||
hide_advanced = true
|
||||
|
||||
[authentication]
|
||||
# Base64 encoded version of the public key for validating the JWT token. Must be in DER format
|
||||
pub_key_base64 = "MIIBCgKCAQEAs5K6s49JVV9LBMzDrkORsoPSYsv1sCXDtxjp4pn8p0uPSvJAsbNNmdIgCjfSULzbHLM28MblnI4zYP8ZgKtkjdg+Ic5WQbS5iBAkf18zMafpOrotTArLsgZSmUfNYt0SOiN17D+sq/Ov/CKXRM9CttKkEbanBTVqkx7sxsHVbkI6tDvkboSaNeVPHzHlfAbvGrUo5cbAFCB/KnRsoxr+g7jLKTxU1w4xb/pIs91h80AXV/yZPXL6ItPM3/0noIRXjmoeYWf2sFQaFALNB2Kef0p6/hoHYUQP04ZSIL3Q+v13z5X2YJIlI4eLg+iD25QYm9V8oP3+Xro4vd47a0/maQIDAQAB"
|
||||
# URL to authenticate against. This must return a JWT token with their permissions and a custom claim patreonInfo with the following structure
|
||||
# "patreonInfo": { "linked": false, "activeSubscription": false }
|
||||
# If successful, the frontend will use this JWT token as a Bearer Authentication when requesting the binaries to download
|
||||
auth_url = "https://api.yuzu-emu.org/jwt/installer/"
|
||||
[authentication.validation]
|
||||
iss = "citra-core"
|
||||
aud = "installer"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu"
|
||||
description = "Includes frequent updates to yuzu with all the latest reviewed and tested features."
|
||||
default = true
|
||||
[packages.source]
|
||||
name = "github"
|
||||
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
|
||||
[packages.source.config]
|
||||
repo = "yuzu-emu/yuzu-mainline"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu"
|
||||
relative_path = "yuzu-windows-msvc/yuzu.exe"
|
||||
description = "Launch yuzu"
|
||||
|
||||
[[packages]]
|
||||
name = "yuzu Early Access"
|
||||
description = "Bonus preview release for project supporters. Thanks for your support!"
|
||||
# Displayed when the package has no authentication for the user
|
||||
need_authentication_description = "Click here to sign in with your yuzu account for Early Access"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_link_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_subscription_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
|
||||
# Displayed when the package has an authentication, but the user has not linked their account
|
||||
need_reward_tier_description = "You are signed in, but are not backing an eligible reward tier! Click here for more details"
|
||||
requires_authorization = true
|
||||
# puts a "new" ribbon the package select
|
||||
is_new = true
|
||||
[packages.source]
|
||||
name = "patreon"
|
||||
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
|
||||
[packages.source.config]
|
||||
repo = "earlyaccess"
|
||||
[[packages.shortcuts]]
|
||||
name = "yuzu Early Access"
|
||||
relative_path = "yuzu-windows-msvc-early-access/yuzu.exe"
|
||||
description = "Launch yuzu Early Access"
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ impl<'a> Archive<'a> for ZipArchive<'a> {
|
|||
continue;
|
||||
}
|
||||
|
||||
func(i, Some(max), archive.sanitized_name(), &mut archive)?;
|
||||
func(i, Some(max), archive.mangled_name(), &mut archive)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ use toml::de::Error as TomlError;
|
|||
|
||||
use serde_json::{self, Error as SerdeError};
|
||||
|
||||
use sources::get_by_name;
|
||||
use sources::types::Release;
|
||||
use crate::sources::get_by_name;
|
||||
use crate::sources::types::Release;
|
||||
|
||||
/// Description of the source of a package.
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
|
|
@ -25,6 +25,23 @@ pub struct PackageShortcut {
|
|||
pub name: String,
|
||||
pub relative_path: String,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub has_desktop_shortcut: bool,
|
||||
}
|
||||
|
||||
/// Extra description for authentication and authorization state for a package
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct PackageExtendedDescription {
|
||||
#[serde(default)]
|
||||
pub no_action_description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub need_authentication_description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub need_link_description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub need_subscription_description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub need_reward_tier_description: Option<String>,
|
||||
}
|
||||
|
||||
/// Describes a overview of a individual package.
|
||||
|
|
@ -32,10 +49,34 @@ pub struct PackageShortcut {
|
|||
pub struct PackageDescription {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub icon: Option<String>,
|
||||
pub default: Option<bool>,
|
||||
pub source: PackageSource,
|
||||
#[serde(default)]
|
||||
pub shortcuts: Vec<PackageShortcut>,
|
||||
#[serde(default)]
|
||||
pub requires_authorization: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub is_new: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub extended_description: Option<PackageExtendedDescription>,
|
||||
}
|
||||
|
||||
/// Configuration for validating the JWT token
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct JWTValidation {
|
||||
pub iss: Option<String>,
|
||||
// This can technically be a Vec as well, but thats a pain to support atm
|
||||
pub aud: Option<String>,
|
||||
}
|
||||
|
||||
/// The configuration for this release.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct AuthenticationConfig {
|
||||
pub pub_key_base64: String,
|
||||
pub auth_url: String,
|
||||
pub validation: Option<JWTValidation>,
|
||||
}
|
||||
|
||||
/// Describes the application itself.
|
||||
|
|
@ -43,6 +84,8 @@ pub struct PackageDescription {
|
|||
pub struct BaseAttributes {
|
||||
pub name: String,
|
||||
pub target_url: String,
|
||||
#[serde(default)]
|
||||
pub recovery: bool,
|
||||
}
|
||||
|
||||
impl BaseAttributes {
|
||||
|
|
@ -66,6 +109,8 @@ pub struct Config {
|
|||
pub packages: Vec<PackageDescription>,
|
||||
#[serde(default)]
|
||||
pub hide_advanced: bool,
|
||||
#[serde(default)]
|
||||
pub authentication: Option<AuthenticationConfig>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use logging::LoggingErrors;
|
||||
use crate::installer::InstallerFramework;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub mod rest;
|
||||
mod ui;
|
||||
|
|
@ -16,7 +16,7 @@ pub fn launch(app_name: &str, is_launcher: bool, framework: InstallerFramework)
|
|||
|
||||
let (servers, address) = rest::server::spawn_servers(framework.clone());
|
||||
|
||||
ui::start_ui(app_name, &address, is_launcher);
|
||||
ui::start_ui(app_name, &address, is_launcher).log_expect("Failed to start UI");
|
||||
|
||||
// Explicitly hint that we want the servers instance until here.
|
||||
drop(servers);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
extern crate mime_guess;
|
||||
|
||||
use self::mime_guess::{get_mime_type, octet_stream};
|
||||
use self::mime_guess::from_ext;
|
||||
use self::mime_guess::mime::APPLICATION_OCTET_STREAM;
|
||||
|
||||
macro_rules! include_files_as_assets {
|
||||
( $target_match:expr, $( $file_name:expr ),* ) => {
|
||||
|
|
@ -23,9 +24,9 @@ pub fn file_from_string(file_path: &str) -> Option<(String, &'static [u8])> {
|
|||
Some(ext_ptr) => {
|
||||
let ext = &file_path[ext_ptr + 1..];
|
||||
|
||||
get_mime_type(ext)
|
||||
from_ext(ext).first_or_octet_stream()
|
||||
}
|
||||
None => octet_stream(),
|
||||
None => APPLICATION_OCTET_STREAM,
|
||||
};
|
||||
|
||||
let string_mime = guessed_mime.to_string();
|
||||
|
|
@ -34,13 +35,19 @@ pub fn file_from_string(file_path: &str) -> Option<(String, &'static [u8])> {
|
|||
file_path,
|
||||
"/index.html",
|
||||
"/favicon.ico",
|
||||
"/img/logo.png",
|
||||
"/img/light_mode_installer_logo.png",
|
||||
"/img/dark_mode_installer_logo.png",
|
||||
"/thicc_logo_installer__ea_shadow.png",
|
||||
"/thicc_logo_installer_shadow.png",
|
||||
"/img/how-to-open.png",
|
||||
"/css/app.css",
|
||||
"/css/chunk-vendors.css",
|
||||
"/fonts/roboto-v18-latin-regular.eot",
|
||||
"/fonts/roboto-v18-latin-regular.woff",
|
||||
"/fonts/roboto-v18-latin-regular.woff2",
|
||||
"/fonts/materialdesignicons-webfont.eot",
|
||||
"/fonts/materialdesignicons-webfont.woff",
|
||||
"/fonts/materialdesignicons-webfont.woff2",
|
||||
"/js/chunk-vendors.js",
|
||||
"/js/app.js"
|
||||
)?;
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
//!
|
||||
//! Contains the over-arching server object + methods to manipulate it.
|
||||
|
||||
use frontend::rest::services::WebService;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use hyper::server::Http;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,27 +2,23 @@
|
|||
//!
|
||||
//! The /api/attr call returns an executable script containing session variables.
|
||||
|
||||
use frontend::rest::services::default_future;
|
||||
use frontend::rest::services::encapsulate_json;
|
||||
use frontend::rest::services::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::Response;
|
||||
use frontend::rest::services::WebService;
|
||||
use crate::frontend::rest::services::default_future;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::Response;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> Future {
|
||||
let framework = service.get_framework_read();
|
||||
|
||||
let file = encapsulate_json(
|
||||
"base_attributes",
|
||||
&framework
|
||||
.base_attributes
|
||||
.to_json_str()
|
||||
.log_expect("Failed to render JSON representation of config"),
|
||||
);
|
||||
let file = framework
|
||||
.base_attributes
|
||||
.to_json_str()
|
||||
.log_expect("Failed to render JSON representation of config");
|
||||
|
||||
default_future(
|
||||
Response::new()
|
||||
|
|
|
|||
293
src/frontend/rest/services/authentication.rs
Normal file
293
src/frontend/rest/services/authentication.rs
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
//! frontend/rest/services/authentication.rs
|
||||
//!
|
||||
//! Provides mechanisms to authenticate users using JWT.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::{Future, Stream};
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use jsonwebtoken::DecodingKey;
|
||||
use jwt::{decode, Algorithm, Validation};
|
||||
|
||||
use reqwest::header::USER_AGENT;
|
||||
|
||||
use crate::frontend::rest::services::Future as InternalFuture;
|
||||
use crate::frontend::rest::services::{default_future, Request, Response, WebService};
|
||||
|
||||
use crate::http::{build_async_client, build_client};
|
||||
|
||||
use crate::config::JWTValidation;
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Auth {
|
||||
username: String,
|
||||
token: String,
|
||||
jwt_token: Option<JWTClaims>,
|
||||
}
|
||||
|
||||
/// claims struct, it needs to derive `Serialize` and/or `Deserialize`
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct JWTClaims {
|
||||
pub sub: String,
|
||||
pub iss: String,
|
||||
pub aud: String,
|
||||
pub exp: usize,
|
||||
#[serde(default)]
|
||||
pub roles: Vec<String>,
|
||||
#[serde(rename = "releaseChannels", default)]
|
||||
pub channels: Vec<String>,
|
||||
#[serde(rename = "isPatreonAccountLinked")]
|
||||
pub is_linked: bool,
|
||||
#[serde(rename = "isPatreonSubscriptionActive")]
|
||||
pub is_subscribed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
struct AuthRequest {
|
||||
username: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
/// Calls the given server to obtain a JWT token and returns a Future<String> with the response
|
||||
pub fn authenticate_async(
|
||||
url: String,
|
||||
username: String,
|
||||
token: String,
|
||||
) -> Box<dyn futures::Future<Item = String, Error = String>> {
|
||||
// Build the HTTP client up
|
||||
let client = match build_async_client() {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
return Box::new(futures::future::err(
|
||||
"Unable to build async web client".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
Box::new(client.post(&url)
|
||||
.header(USER_AGENT, "liftinstall (j-selby)")
|
||||
.header("X-USERNAME", username.clone())
|
||||
.header("X-TOKEN", token.clone())
|
||||
.send()
|
||||
.map_err(|err| {
|
||||
format!("stream error {:?}, client: {:?}, http: {:?}, redirect: {:?}, serialization: {:?}, timeout: {:?}, server: {:?}",
|
||||
err, err.is_client_error(), err.is_http(), err.is_redirect(),
|
||||
err.is_serialization(), err.is_timeout(), err.is_server_error())
|
||||
})
|
||||
.map(|mut response| {
|
||||
match response.status() {
|
||||
reqwest::StatusCode::OK =>
|
||||
Ok(response.text()
|
||||
.map_err(|e| {
|
||||
format!("Error while converting the response to text {:?}", e)
|
||||
})),
|
||||
_ => {
|
||||
Err(format!("Error wrong response code from server {:?}", response.status()))
|
||||
}
|
||||
}
|
||||
})
|
||||
.and_then(|x| x)
|
||||
.flatten()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn authenticate_sync(url: String, username: String, token: String) -> Result<String, String> {
|
||||
// Build the HTTP client up
|
||||
let client = build_client()?;
|
||||
|
||||
let mut response = client.post(&url)
|
||||
.header(USER_AGENT, "liftinstall (j-selby)")
|
||||
.header("X-USERNAME", username.clone())
|
||||
.header("X-TOKEN", token.clone())
|
||||
.send()
|
||||
.map_err(|err| {
|
||||
format!("stream error {:?}, client: {:?}, http: {:?}, redirect: {:?}, serialization: {:?}, timeout: {:?}, server: {:?}",
|
||||
err, err.is_client_error(), err.is_http(), err.is_redirect(),
|
||||
err.is_serialization(), err.is_timeout(), err.is_server_error())
|
||||
})?;
|
||||
|
||||
match response.status() {
|
||||
reqwest::StatusCode::OK => Ok(response
|
||||
.text()
|
||||
.map_err(|e| format!("Error while converting the response to text {:?}", e))?),
|
||||
_ => Err(format!(
|
||||
"Error wrong response code from server {:?}",
|
||||
response.status()
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_token(
|
||||
body: String,
|
||||
pub_key_base64: String,
|
||||
validation: Option<JWTValidation>,
|
||||
) -> Result<JWTClaims, String> {
|
||||
// Get the public key for this authentication url
|
||||
let pub_key = if pub_key_base64.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
base64::decode(&pub_key_base64).map_err(|e| {
|
||||
format!(
|
||||
"Configured public key was not empty and did not decode as base64 {:?}",
|
||||
e
|
||||
)
|
||||
})?
|
||||
};
|
||||
|
||||
// Configure validation for audience and issuer if the configuration provides it
|
||||
let mut validation = match validation {
|
||||
Some(v) => {
|
||||
let mut valid = Validation::new(Algorithm::RS256);
|
||||
valid.iss = v.iss.map(|iss| {
|
||||
let mut issuer = HashSet::new();
|
||||
issuer.insert(iss);
|
||||
issuer
|
||||
});
|
||||
if let &Some(ref v) = &v.aud {
|
||||
valid.set_audience(&[v]);
|
||||
}
|
||||
valid
|
||||
}
|
||||
None => Validation::default(),
|
||||
};
|
||||
validation.validate_exp = false;
|
||||
validation.validate_nbf = false;
|
||||
// Verify the JWT token
|
||||
decode::<JWTClaims>(&body, &DecodingKey::from_rsa_der(&pub_key), &validation)
|
||||
.map(|tok| tok.claims)
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
"Error while decoding the JWT. error: {:?} jwt: {:?}",
|
||||
err, body
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> InternalFuture {
|
||||
info!("Handling authentication");
|
||||
let framework = service
|
||||
.framework
|
||||
.read()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
let credentials = framework.database.credentials.clone();
|
||||
let config = framework
|
||||
.config
|
||||
.clone()
|
||||
.log_expect("No in-memory configuration found");
|
||||
|
||||
// If authentication isn't configured, just return immediately
|
||||
if config.authentication.is_none() {
|
||||
return default_future(Response::new().with_status(hyper::Ok).with_body("{}"));
|
||||
}
|
||||
|
||||
// Create moveable framework references so that the lambdas can write to them later
|
||||
let write_cred_fw = Arc::clone(&service.framework);
|
||||
|
||||
Box::new(
|
||||
_req.body()
|
||||
.concat2()
|
||||
.map(move |body| {
|
||||
let req = serde_json::from_slice::<AuthRequest>(&body);
|
||||
if req.is_err() {
|
||||
warn!("Failed to parse auth request from the frontend");
|
||||
return default_future(
|
||||
Response::new().with_status(hyper::StatusCode::BadRequest),
|
||||
);
|
||||
}
|
||||
let req = req.unwrap();
|
||||
|
||||
// Determine which credentials we should use
|
||||
let (username, token) = {
|
||||
let req_username = req.username;
|
||||
let req_token = req.token;
|
||||
|
||||
// if the user didn't provide credentials, and theres nothing stored in the
|
||||
// database, return an early error
|
||||
let req_cred_valid = !req_username.is_empty() && !req_token.is_empty();
|
||||
let stored_cred_valid =
|
||||
!credentials.username.is_empty() && !credentials.token.is_empty();
|
||||
|
||||
if !req_cred_valid && !stored_cred_valid {
|
||||
info!("No passed in credential and no stored credentials to validate");
|
||||
return default_future(Response::new().with_status(hyper::BadRequest));
|
||||
}
|
||||
|
||||
if req_cred_valid {
|
||||
(req_username.clone(), req_token.clone())
|
||||
} else {
|
||||
(credentials.username.clone(), credentials.token.clone())
|
||||
}
|
||||
};
|
||||
|
||||
// second copy of the credentials so we can move them into a different closure
|
||||
let (username_clone, token_clone) = (username.clone(), token.clone());
|
||||
|
||||
let authentication = config
|
||||
.authentication
|
||||
.log_expect("No authentication configuration");
|
||||
let auth_url = authentication.auth_url.clone();
|
||||
let pub_key_base64 = authentication.pub_key_base64.clone();
|
||||
let validation = authentication.validation.clone();
|
||||
|
||||
// call the authentication URL to see if we are authenticated
|
||||
Box::new(
|
||||
authenticate_async(auth_url, username.clone(), token.clone())
|
||||
.map(|body| validate_token(body, pub_key_base64, validation))
|
||||
.and_then(|res| res)
|
||||
.map(move |claims| {
|
||||
let out = Auth {
|
||||
username: username_clone,
|
||||
token: token_clone,
|
||||
jwt_token: Some(claims.clone()),
|
||||
};
|
||||
// Convert the json to a string and return the json token
|
||||
match serde_json::to_string(&out) {
|
||||
Ok(v) => Ok(v),
|
||||
Err(e) => Err(format!(
|
||||
"Error while converting the claims to JSON string: {:?}",
|
||||
e
|
||||
)),
|
||||
}
|
||||
})
|
||||
.and_then(|res| res)
|
||||
.map(move |json| {
|
||||
{
|
||||
// Store the validated username and password into the installer database
|
||||
let mut framework = write_cred_fw
|
||||
.write()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
framework.database.credentials.username = username;
|
||||
framework.database.credentials.token = token;
|
||||
}
|
||||
|
||||
// Finally return the JSON with the response
|
||||
info!("successfully verified username and token");
|
||||
Response::new()
|
||||
.with_header(ContentLength(json.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_status(hyper::StatusCode::Ok)
|
||||
.with_body(json)
|
||||
})
|
||||
.map_err(|err| {
|
||||
error!(
|
||||
"Got an internal error while processing user token: {:?}",
|
||||
err
|
||||
);
|
||||
Response::new().with_status(hyper::StatusCode::InternalServerError)
|
||||
})
|
||||
.or_else(|err| {
|
||||
// Convert the Err value into an Ok value since the error code from
|
||||
// this HTTP request is an Ok(response)
|
||||
Ok(err)
|
||||
}),
|
||||
)
|
||||
})
|
||||
// Flatten the internal future into the output response future
|
||||
.flatten(),
|
||||
)
|
||||
}
|
||||
31
src/frontend/rest/services/browser.rs
Normal file
31
src/frontend/rest/services/browser.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
//! frontend/rest/services/browser.rs
|
||||
//!
|
||||
//! Launches the user's web browser on request from the frontend.
|
||||
|
||||
use crate::frontend::rest::services::Future as InternalFuture;
|
||||
use crate::frontend::rest::services::{Request, Response, WebService};
|
||||
use crate::logging::LoggingErrors;
|
||||
use futures::{Future, Stream};
|
||||
use hyper::header::ContentType;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
struct OpenRequest {
|
||||
url: String,
|
||||
}
|
||||
|
||||
pub fn handle(_service: &WebService, req: Request) -> InternalFuture {
|
||||
Box::new(req.body().concat2().map(move |body| {
|
||||
let req: OpenRequest = serde_json::from_slice(&body).log_expect("Malformed request");
|
||||
if webbrowser::open(&req.url).is_ok() {
|
||||
Response::new()
|
||||
.with_status(hyper::Ok)
|
||||
.with_header(ContentType::json())
|
||||
.with_body("{}")
|
||||
} else {
|
||||
Response::new()
|
||||
.with_status(hyper::BadRequest)
|
||||
.with_header(ContentType::json())
|
||||
.with_body("{}")
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
|
@ -4,18 +4,18 @@
|
|||
//!
|
||||
//! This endpoint should be usable directly from a <script> tag during loading.
|
||||
|
||||
use frontend::rest::services::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::Response;
|
||||
use frontend::rest::services::WebService;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::Response;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use config::Config;
|
||||
use crate::config::Config;
|
||||
|
||||
use http::build_async_client;
|
||||
use crate::http::build_async_client;
|
||||
|
||||
use futures::stream::Stream;
|
||||
use futures::Future as _;
|
||||
|
|
|
|||
|
|
@ -2,17 +2,17 @@
|
|||
//!
|
||||
//! This call returns if dark mode is enabled on the system currently.
|
||||
|
||||
use frontend::rest::services::default_future;
|
||||
use frontend::rest::services::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::Response;
|
||||
use frontend::rest::services::WebService;
|
||||
use crate::frontend::rest::services::default_future;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::Response;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use native::is_dark_mode_active;
|
||||
use crate::native::is_dark_mode_active;
|
||||
|
||||
pub fn handle(_service: &WebService, _req: Request) -> Future {
|
||||
let file = serde_json::to_string(&is_dark_mode_active())
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@
|
|||
//!
|
||||
//! The /api/default-path returns the default path for the application to install into.
|
||||
|
||||
use frontend::rest::services::default_future;
|
||||
use frontend::rest::services::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::Response;
|
||||
use frontend::rest::services::WebService;
|
||||
use crate::frontend::rest::services::default_future;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::Response;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
/// Struct used by serde to send a JSON payload to the client containing an optional value.
|
||||
#[derive(Serialize)]
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
//!
|
||||
//! The /api/exit closes down the application.
|
||||
|
||||
use frontend::rest::services::default_future;
|
||||
use frontend::rest::services::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::Response;
|
||||
use frontend::rest::services::WebService;
|
||||
use crate::frontend::rest::services::default_future;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::Response;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::ContentType;
|
||||
use hyper::StatusCode;
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@
|
|||
//!
|
||||
//! The /api/install call installs a set of packages dictated by a POST request.
|
||||
|
||||
use frontend::rest::services::stream_progress;
|
||||
use frontend::rest::services::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::WebService;
|
||||
use crate::frontend::rest::services::stream_progress;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use installer::InstallMessage;
|
||||
use crate::installer::InstallMessage;
|
||||
|
||||
use futures::future::Future as _;
|
||||
use futures::stream::Stream;
|
||||
|
|
@ -28,12 +28,23 @@ pub fn handle(service: &WebService, req: Request) -> Future {
|
|||
|
||||
let mut to_install = Vec::new();
|
||||
let mut path: Option<String> = None;
|
||||
let mut force_install = false;
|
||||
let mut install_desktop_shortcut = false;
|
||||
|
||||
// Transform results into just an array of stuff to install
|
||||
for (key, value) in &results {
|
||||
if key == "path" {
|
||||
path = Some(value.to_owned());
|
||||
continue;
|
||||
} else if key == "installDesktopShortcut" {
|
||||
info!("Found installDesktopShortcut {:?}", value);
|
||||
install_desktop_shortcut = value == "true";
|
||||
continue;
|
||||
}
|
||||
|
||||
if key == "mode" && value == "force" {
|
||||
force_install = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if value == "true" {
|
||||
|
|
@ -41,6 +52,19 @@ pub fn handle(service: &WebService, req: Request) -> Future {
|
|||
}
|
||||
}
|
||||
|
||||
if !install_desktop_shortcut {
|
||||
let framework_ref = framework
|
||||
.read()
|
||||
.log_expect("InstallerFramework has been dirtied");
|
||||
install_desktop_shortcut = framework_ref.preexisting_install
|
||||
&& framework_ref
|
||||
.database
|
||||
.packages
|
||||
.first()
|
||||
.and_then(|x| Some(x.shortcuts.len() > 1))
|
||||
.unwrap_or(false);
|
||||
}
|
||||
|
||||
// The frontend always provides this
|
||||
let path =
|
||||
path.log_expect("No path specified by frontend when one should have already existed");
|
||||
|
|
@ -55,7 +79,13 @@ pub fn handle(service: &WebService, req: Request) -> Future {
|
|||
framework.set_install_dir(&path);
|
||||
}
|
||||
|
||||
if let Err(v) = framework.install(to_install, &sender, new_install) {
|
||||
if let Err(v) = framework.install(
|
||||
to_install,
|
||||
&sender,
|
||||
new_install,
|
||||
install_desktop_shortcut,
|
||||
force_install,
|
||||
) {
|
||||
error!("Install error occurred: {:?}", v);
|
||||
if let Err(v) = sender.send(InstallMessage::Error(v)) {
|
||||
error!("Failed to send install error: {:?}", v);
|
||||
|
|
|
|||
|
|
@ -5,15 +5,15 @@
|
|||
//!
|
||||
//! e.g. if the application is in maintenance mode
|
||||
|
||||
use frontend::rest::services::default_future;
|
||||
use frontend::rest::services::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::Response;
|
||||
use frontend::rest::services::WebService;
|
||||
use crate::frontend::rest::services::default_future;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::Response;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> Future {
|
||||
let framework = service.get_framework_read();
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@
|
|||
|
||||
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
|
||||
use installer::{InstallMessage, InstallerFramework};
|
||||
use crate::installer::{InstallMessage, InstallerFramework};
|
||||
|
||||
use hyper::server::Service;
|
||||
use hyper::{Method, StatusCode};
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use std::sync::mpsc::{channel, Sender};
|
||||
|
||||
|
|
@ -21,9 +21,11 @@ use futures::future::Future as _;
|
|||
use futures::sink::Sink;
|
||||
|
||||
mod attributes;
|
||||
pub mod authentication;
|
||||
mod browser;
|
||||
mod config;
|
||||
mod default_path;
|
||||
mod dark_mode;
|
||||
mod default_path;
|
||||
mod exit;
|
||||
mod install;
|
||||
mod installation_status;
|
||||
|
|
@ -31,6 +33,8 @@ mod packages;
|
|||
mod static_files;
|
||||
mod uninstall;
|
||||
mod update_updater;
|
||||
mod verify_path;
|
||||
mod view_folder;
|
||||
|
||||
/// Expected incoming Request format from Hyper.
|
||||
pub type Request = hyper::server::Request;
|
||||
|
|
@ -137,9 +141,13 @@ impl Service for WebService {
|
|||
(Method::Get, "/api/exit") => exit::handle(self, req),
|
||||
(Method::Get, "/api/packages") => packages::handle(self, req),
|
||||
(Method::Get, "/api/installation-status") => installation_status::handle(self, req),
|
||||
(Method::Get, "/api/view-local-folder") => view_folder::handle(self, req),
|
||||
(Method::Post, "/api/check-auth") => authentication::handle(self, req),
|
||||
(Method::Post, "/api/start-install") => install::handle(self, req),
|
||||
(Method::Post, "/api/open-browser") => browser::handle(self, req),
|
||||
(Method::Post, "/api/uninstall") => uninstall::handle(self, req),
|
||||
(Method::Post, "/api/update-updater") => update_updater::handle(self, req),
|
||||
(Method::Post, "/api/verify-path") => verify_path::handle(self, req),
|
||||
(Method::Get, _) => static_files::handle(self, req),
|
||||
e => {
|
||||
info!("Returned 404 for {:?}", e);
|
||||
|
|
|
|||
|
|
@ -2,16 +2,16 @@
|
|||
//!
|
||||
//! The /api/packages call returns all the currently installed packages.
|
||||
|
||||
use frontend::rest::services::default_future;
|
||||
use frontend::rest::services::encapsulate_json;
|
||||
use frontend::rest::services::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::Response;
|
||||
use frontend::rest::services::WebService;
|
||||
use crate::frontend::rest::services::default_future;
|
||||
use crate::frontend::rest::services::encapsulate_json;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::Response;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> Future {
|
||||
let framework = service.get_framework_read();
|
||||
|
|
|
|||
|
|
@ -4,18 +4,18 @@
|
|||
//!
|
||||
//! e.g. index.html, main.js, ...
|
||||
|
||||
use frontend::rest::assets;
|
||||
use crate::frontend::rest::assets;
|
||||
|
||||
use frontend::rest::services::default_future;
|
||||
use frontend::rest::services::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::Response;
|
||||
use frontend::rest::services::WebService;
|
||||
use crate::frontend::rest::services::default_future;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::Response;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
use hyper::StatusCode;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub fn handle(_service: &WebService, req: Request) -> Future {
|
||||
// At this point, we have a web browser client. Search for a index page
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@
|
|||
//!
|
||||
//! The /api/uninstall call uninstalls all packages.
|
||||
|
||||
use frontend::rest::services::default_future;
|
||||
use frontend::rest::services::stream_progress;
|
||||
use frontend::rest::services::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::WebService;
|
||||
use crate::frontend::rest::services::default_future;
|
||||
use crate::frontend::rest::services::stream_progress;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use installer::InstallMessage;
|
||||
use crate::installer::InstallMessage;
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> Future {
|
||||
let framework = service.framework.clone();
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@
|
|||
//!
|
||||
//! The /api/update-updater call attempts to update the currently running updater.
|
||||
|
||||
use frontend::rest::services::default_future;
|
||||
use frontend::rest::services::stream_progress;
|
||||
use frontend::rest::services::Future;
|
||||
use frontend::rest::services::Request;
|
||||
use frontend::rest::services::WebService;
|
||||
use crate::frontend::rest::services::default_future;
|
||||
use crate::frontend::rest::services::stream_progress;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use installer::InstallMessage;
|
||||
use crate::installer::InstallMessage;
|
||||
|
||||
pub fn handle(service: &WebService, _req: Request) -> Future {
|
||||
let framework = service.framework.clone();
|
||||
|
|
|
|||
48
src/frontend/rest/services/verify_path.rs
Normal file
48
src/frontend/rest/services/verify_path.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
//! frontend/rest/services/verify_path.rs
|
||||
//!
|
||||
//! The /api/verify-path returns whether the path exists or not.
|
||||
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::Response;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
use url::form_urlencoded;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use futures::future::Future as _;
|
||||
use futures::stream::Stream;
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Struct used by serde to send a JSON payload to the client containing an optional value.
|
||||
#[derive(Serialize)]
|
||||
struct VerifyResponse {
|
||||
exists: bool,
|
||||
}
|
||||
|
||||
pub fn handle(_service: &WebService, req: Request) -> Future {
|
||||
Box::new(req.body().concat2().map(move |b| {
|
||||
let results = form_urlencoded::parse(b.as_ref())
|
||||
.into_owned()
|
||||
.collect::<HashMap<String, String>>();
|
||||
let mut exists = false;
|
||||
if let Some(path) = results.get("path") {
|
||||
let path = PathBuf::from(path);
|
||||
exists = path.is_dir();
|
||||
}
|
||||
|
||||
let response = VerifyResponse { exists };
|
||||
|
||||
let file = serde_json::to_string(&response)
|
||||
.log_expect("Failed to render JSON payload of default path object");
|
||||
|
||||
Response::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file)
|
||||
}))
|
||||
}
|
||||
35
src/frontend/rest/services/view_folder.rs
Normal file
35
src/frontend/rest/services/view_folder.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
//! frontend/rest/services/view_folder.rs
|
||||
//!
|
||||
//! The /api/view-local-folder returns whether the path exists or not.
|
||||
//! Side-effect: will open the folder in the default file manager if it exists.
|
||||
|
||||
use super::default_future;
|
||||
use crate::frontend::rest::services::Future;
|
||||
use crate::frontend::rest::services::Request;
|
||||
use crate::frontend::rest::services::Response;
|
||||
use crate::frontend::rest::services::WebService;
|
||||
|
||||
use hyper::header::{ContentLength, ContentType};
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
use crate::native::open_in_shell;
|
||||
|
||||
pub fn handle(service: &WebService, _: Request) -> Future {
|
||||
let framework = service.get_framework_read();
|
||||
let mut response = false;
|
||||
let path = framework.install_path.clone();
|
||||
if let Some(path) = path {
|
||||
response = true;
|
||||
open_in_shell(path.as_path());
|
||||
}
|
||||
|
||||
let file = serde_json::to_string(&response)
|
||||
.log_expect("Failed to render JSON payload of installation status object");
|
||||
|
||||
default_future(
|
||||
Response::new()
|
||||
.with_header(ContentLength(file.len() as u64))
|
||||
.with_header(ContentType::json())
|
||||
.with_body(file),
|
||||
)
|
||||
}
|
||||
|
|
@ -2,70 +2,85 @@
|
|||
//!
|
||||
//! Provides a web-view UI.
|
||||
|
||||
use web_view::Content;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use anyhow::Result;
|
||||
use wry::{
|
||||
application::{
|
||||
dpi::LogicalSize,
|
||||
event::{Event, StartCause, WindowEvent},
|
||||
event_loop::{ControlFlow, EventLoop},
|
||||
window::{Icon, WindowBuilder},
|
||||
},
|
||||
webview::{RpcResponse, WebViewBuilder},
|
||||
};
|
||||
|
||||
use log::Level;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
enum CallbackType {
|
||||
SelectInstallDir { callback_name: String },
|
||||
Log { msg: String, kind: String },
|
||||
Test {}
|
||||
}
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
const ICON_DATA: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/icon-data.bin"));
|
||||
|
||||
/// Starts the main web UI. Will return when UI is closed.
|
||||
pub fn start_ui(app_name: &str, http_address: &str, is_launcher: bool) {
|
||||
let size = if is_launcher { (600, 300) } else { (1024, 500) };
|
||||
|
||||
pub fn start_ui(app_name: &str, http_address: &str, is_launcher: bool) -> Result<()> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
crate::native::prepare_install_webview2(app_name).log_expect("Unable to install webview2");
|
||||
}
|
||||
let size = if is_launcher {
|
||||
(600.0, 300.0)
|
||||
} else {
|
||||
(1024.0, 600.0)
|
||||
};
|
||||
info!("Spawning web view instance");
|
||||
|
||||
web_view::builder()
|
||||
.title(&format!("{} Installer", app_name))
|
||||
.content(Content::Url(http_address))
|
||||
.size(size.0, size.1)
|
||||
.resizable(false)
|
||||
.debug(false)
|
||||
.user_data(())
|
||||
.invoke_handler(|wv, msg| {
|
||||
let mut cb_result = Ok(());
|
||||
let command: CallbackType =
|
||||
serde_json::from_str(msg).log_expect(&format!("Unable to parse string: {:?}", msg));
|
||||
|
||||
debug!("Incoming payload: {:?}", command);
|
||||
|
||||
match command {
|
||||
CallbackType::SelectInstallDir { callback_name } => {
|
||||
let result = wv
|
||||
.dialog()
|
||||
.choose_directory("Select a install directory...", "");
|
||||
|
||||
if let Ok(Some(new_path)) = result {
|
||||
if new_path.to_string_lossy().len() > 0 {
|
||||
let result = serde_json::to_string(&new_path)
|
||||
.log_expect("Unable to serialize response");
|
||||
let command = format!("{}({});", callback_name, result);
|
||||
debug!("Injecting response: {}", command);
|
||||
cb_result = wv.eval(&command);
|
||||
let window_icon =
|
||||
Icon::from_rgba(ICON_DATA.to_vec(), 48, 48).log_expect("Unable to construct window icon");
|
||||
let event_loop = EventLoop::new();
|
||||
let window = WindowBuilder::new()
|
||||
.with_title(format!("{} Installer", app_name))
|
||||
.with_window_icon(Some(window_icon))
|
||||
.with_inner_size(LogicalSize::new(size.0, size.1))
|
||||
.with_resizable(false)
|
||||
.build(&event_loop)?;
|
||||
let _webview = WebViewBuilder::new(window)?
|
||||
.with_url(http_address)?
|
||||
.with_rpc_handler(|_, mut event| {
|
||||
debug!("Incoming payload: {:?}", event);
|
||||
match event.method.as_str() {
|
||||
"Test" => (),
|
||||
"Log" => {
|
||||
if let Some(msg) = event.params.take() {
|
||||
if let Ok(msg) = serde_json::from_value::<(String, String)>(msg) {
|
||||
let kind = match msg.0.as_str() {
|
||||
"info" | "log" => Level::Info,
|
||||
"warn" => Level::Warn,
|
||||
_ => Level::Error,
|
||||
};
|
||||
log!(target: "liftinstall::frontend::js", kind, "{}", msg.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
CallbackType::Log { msg, kind } => {
|
||||
let kind = match kind.as_ref() {
|
||||
"info" | "log" => Level::Info,
|
||||
"warn" => Level::Warn,
|
||||
"error" => Level::Error,
|
||||
_ => Level::Error,
|
||||
};
|
||||
|
||||
log!(target: "liftinstall::frontend::js", kind, "{}", msg);
|
||||
"SelectInstallDir" => {
|
||||
let result =
|
||||
tinyfiledialogs::select_folder_dialog("Select a install directory...", "")
|
||||
.and_then(|v| serde_json::to_value(v).ok());
|
||||
return Some(RpcResponse::new_result(event.id, result));
|
||||
}
|
||||
CallbackType::Test {} => {}
|
||||
_ => warn!("Unknown RPC method: {}", event.method),
|
||||
}
|
||||
|
||||
cb_result
|
||||
None
|
||||
})
|
||||
.run()
|
||||
.log_expect("Unable to launch Web UI!");
|
||||
.build()?;
|
||||
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
|
||||
match event {
|
||||
Event::NewEvents(StartCause::Init) => info!("Webview started"),
|
||||
Event::WindowEvent {
|
||||
event: WindowEvent::CloseRequested,
|
||||
..
|
||||
} => *control_flow = ControlFlow::Exit,
|
||||
_ => (),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
32
src/http.rs
32
src/http.rs
|
|
@ -7,8 +7,9 @@ use reqwest::header::CONTENT_LENGTH;
|
|||
use std::io::Read;
|
||||
use std::time::Duration;
|
||||
|
||||
use reqwest::async::Client as AsyncClient;
|
||||
use reqwest::r#async::Client as AsyncClient;
|
||||
use reqwest::Client;
|
||||
use reqwest::StatusCode;
|
||||
|
||||
/// Asserts that a URL is valid HTTPS, else returns an error.
|
||||
pub fn assert_ssl(url: &str) -> Result<(), String> {
|
||||
|
|
@ -36,17 +37,40 @@ pub fn build_async_client() -> Result<AsyncClient, String> {
|
|||
}
|
||||
|
||||
/// Streams a file from a HTTP server.
|
||||
pub fn stream_file<F>(url: &str, mut callback: F) -> Result<(), String>
|
||||
pub fn stream_file<F>(
|
||||
url: &str,
|
||||
authorization: Option<String>,
|
||||
mut callback: F,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
F: FnMut(Vec<u8>, u64) -> (),
|
||||
{
|
||||
assert_ssl(url)?;
|
||||
|
||||
let mut client = build_client()?
|
||||
.get(url)
|
||||
let mut client = build_client()?.get(url);
|
||||
|
||||
if let Some(auth) = authorization {
|
||||
client = client.header("Authorization", format!("Bearer {}", auth));
|
||||
}
|
||||
|
||||
let mut client = client
|
||||
.send()
|
||||
.map_err(|x| format!("Failed to GET resource: {:?}", x))?;
|
||||
|
||||
match client.status() {
|
||||
StatusCode::OK => {}
|
||||
StatusCode::TOO_MANY_REQUESTS => {
|
||||
return Err(
|
||||
"Your token has exceeded the number of daily allowable IP addresses. \
|
||||
Please wait 24 hours and try again."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
x => {
|
||||
return Err(format!("Bad status code: {:?}.", x));
|
||||
}
|
||||
}
|
||||
|
||||
let size = match client.headers().get(CONTENT_LENGTH) {
|
||||
Some(ref v) => v
|
||||
.to_str()
|
||||
|
|
|
|||
|
|
@ -21,28 +21,29 @@ use std::io::Cursor;
|
|||
use std::process::Command;
|
||||
use std::process::{exit, Stdio};
|
||||
|
||||
use config::BaseAttributes;
|
||||
use config::Config;
|
||||
use crate::config::BaseAttributes;
|
||||
use crate::config::Config;
|
||||
|
||||
use sources::types::Version;
|
||||
use crate::sources::types::Version;
|
||||
|
||||
use tasks::install::InstallTask;
|
||||
use tasks::uninstall::UninstallTask;
|
||||
use tasks::uninstall_global_shortcut::UninstallGlobalShortcutsTask;
|
||||
use tasks::DependencyTree;
|
||||
use tasks::TaskMessage;
|
||||
use crate::tasks::install::InstallTask;
|
||||
use crate::tasks::uninstall::UninstallTask;
|
||||
use crate::tasks::uninstall_global_shortcut::UninstallGlobalShortcutsTask;
|
||||
use crate::tasks::DependencyTree;
|
||||
use crate::tasks::TaskMessage;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use dirs::home_dir;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::fs::remove_file;
|
||||
|
||||
use http;
|
||||
use crate::http;
|
||||
|
||||
use number_prefix::{NumberPrefix, Prefixed, Standalone};
|
||||
use number_prefix::NumberPrefix::{self, Prefixed, Standalone};
|
||||
|
||||
use native;
|
||||
use crate::native;
|
||||
|
||||
/// A message thrown during the installation of packages.
|
||||
#[derive(Serialize)]
|
||||
|
|
@ -50,14 +51,25 @@ pub enum InstallMessage {
|
|||
Status(String, f64),
|
||||
PackageInstalled,
|
||||
Error(String),
|
||||
AuthorizationRequired(String),
|
||||
EOF,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Clone)]
|
||||
pub struct Credentials {
|
||||
#[serde(default)]
|
||||
pub username: String,
|
||||
#[serde(default)]
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
/// Metadata about the current installation itself.
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct InstallationDatabase {
|
||||
pub packages: Vec<LocalInstallation>,
|
||||
pub shortcuts: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub credentials: Credentials,
|
||||
}
|
||||
|
||||
impl InstallationDatabase {
|
||||
|
|
@ -66,6 +78,10 @@ impl InstallationDatabase {
|
|||
InstallationDatabase {
|
||||
packages: Vec::new(),
|
||||
shortcuts: Vec::new(),
|
||||
credentials: Credentials {
|
||||
username: String::new(),
|
||||
token: String::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -82,6 +98,7 @@ pub struct InstallerFramework {
|
|||
// If we just completed an uninstall, and we should clean up after ourselves.
|
||||
pub burn_after_exit: bool,
|
||||
pub launcher_path: Option<String>,
|
||||
pub is_windows: bool,
|
||||
}
|
||||
|
||||
/// Contains basic properties on the status of the session. Subset of InstallationFramework.
|
||||
|
|
@ -102,7 +119,7 @@ pub struct LocalInstallation {
|
|||
/// Relative paths to generated files
|
||||
pub files: Vec<String>,
|
||||
/// Absolute paths to generated shortcut files
|
||||
pub shortcuts: Vec<String>,
|
||||
pub shortcuts: HashSet<String>,
|
||||
}
|
||||
|
||||
macro_rules! declare_messenger_callback {
|
||||
|
|
@ -114,6 +131,12 @@ macro_rules! declare_messenger_callback {
|
|||
error!("Failed to submit queue message: {:?}", v);
|
||||
}
|
||||
}
|
||||
TaskMessage::AuthorizationRequired(msg) => {
|
||||
if let Err(v) = $target.send(InstallMessage::AuthorizationRequired(msg.to_string()))
|
||||
{
|
||||
error!("Failed to submit queue message: {:?}", v);
|
||||
}
|
||||
}
|
||||
TaskMessage::PackageInstalled => {
|
||||
if let Err(v) = $target.send(InstallMessage::PackageInstalled) {
|
||||
error!("Failed to submit queue message: {:?}", v);
|
||||
|
|
@ -153,11 +176,14 @@ impl InstallerFramework {
|
|||
/// items: Array of named packages to be installed/kept
|
||||
/// messages: Channel used to send progress messages
|
||||
/// fresh_install: If the install directory must be empty
|
||||
/// force_install: If the install directory should be erased first
|
||||
pub fn install(
|
||||
&mut self,
|
||||
items: Vec<String>,
|
||||
messages: &Sender<InstallMessage>,
|
||||
fresh_install: bool,
|
||||
create_desktop_shortcuts: bool,
|
||||
force_install: bool,
|
||||
) -> Result<(), String> {
|
||||
info!(
|
||||
"Framework: Installing {:?} to {:?}",
|
||||
|
|
@ -186,6 +212,8 @@ impl InstallerFramework {
|
|||
items,
|
||||
uninstall_items,
|
||||
fresh_install,
|
||||
create_desktop_shortcuts,
|
||||
force_install,
|
||||
});
|
||||
|
||||
let mut tree = DependencyTree::build(task);
|
||||
|
|
@ -252,7 +280,7 @@ impl InstallerFramework {
|
|||
let mut downloaded = 0;
|
||||
let mut data_storage: Vec<u8> = Vec::new();
|
||||
|
||||
http::stream_file(tool, |data, size| {
|
||||
http::stream_file(tool, None, |data, size| {
|
||||
{
|
||||
data_storage.extend_from_slice(&data);
|
||||
}
|
||||
|
|
@ -430,6 +458,25 @@ impl InstallerFramework {
|
|||
is_launcher: false,
|
||||
burn_after_exit: false,
|
||||
launcher_path: None,
|
||||
is_windows: cfg!(windows),
|
||||
}
|
||||
}
|
||||
|
||||
/// The special recovery mode for the Installer Framework.
|
||||
pub fn new_recovery_mode(attrs: BaseAttributes, install_path: &Path) -> Self {
|
||||
InstallerFramework {
|
||||
base_attributes: BaseAttributes {
|
||||
recovery: true,
|
||||
..attrs
|
||||
},
|
||||
config: None,
|
||||
database: InstallationDatabase::new(),
|
||||
install_path: Some(install_path.to_path_buf()),
|
||||
preexisting_install: true,
|
||||
is_launcher: false,
|
||||
burn_after_exit: false,
|
||||
launcher_path: None,
|
||||
is_windows: cfg!(windows),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -457,6 +504,7 @@ impl InstallerFramework {
|
|||
is_launcher: false,
|
||||
burn_after_exit: false,
|
||||
launcher_path: None,
|
||||
is_windows: cfg!(windows),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
79
src/main.rs
79
src/main.rs
|
|
@ -7,7 +7,7 @@
|
|||
#![deny(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
extern crate web_view;
|
||||
extern crate wry;
|
||||
|
||||
extern crate futures;
|
||||
extern crate hyper;
|
||||
|
|
@ -38,15 +38,19 @@ extern crate chrono;
|
|||
|
||||
extern crate clap;
|
||||
#[cfg(windows)]
|
||||
extern crate winapi;
|
||||
#[cfg(windows)]
|
||||
extern crate widestring;
|
||||
#[cfg(windows)]
|
||||
extern crate winapi;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
extern crate slug;
|
||||
#[cfg(not(windows))]
|
||||
extern crate sysinfo;
|
||||
|
||||
extern crate jsonwebtoken as jwt;
|
||||
|
||||
extern crate base64;
|
||||
|
||||
mod archives;
|
||||
mod config;
|
||||
mod frontend;
|
||||
|
|
@ -61,13 +65,16 @@ mod tasks;
|
|||
use installer::InstallerFramework;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::App;
|
||||
use clap::Arg;
|
||||
|
||||
use config::BaseAttributes;
|
||||
use std::fs;
|
||||
use std::process::{exit, Command, Stdio};
|
||||
|
||||
static RAW_CONFIG: &'static str = include_str!(concat!(env!("OUT_DIR"), "/bootstrap.toml"));
|
||||
const RAW_CONFIG: &str = include_str!(concat!(env!("OUT_DIR"), "/bootstrap.toml"));
|
||||
|
||||
fn main() {
|
||||
let config = BaseAttributes::from_toml_str(RAW_CONFIG).expect("Config file could not be read");
|
||||
|
|
@ -108,6 +115,7 @@ fn main() {
|
|||
.parent()
|
||||
.log_expect("Parent directory of executable could not be found");
|
||||
|
||||
// Handle self-updating if needed
|
||||
self_update::perform_swap(¤t_exe, matches.value_of("swap"));
|
||||
if let Some(new_matches) = self_update::check_args(reinterpret_app, current_path) {
|
||||
matches = new_matches;
|
||||
|
|
@ -115,15 +123,29 @@ fn main() {
|
|||
self_update::cleanup(current_path);
|
||||
|
||||
// Load in metadata + setup the installer framework
|
||||
let mut fresh_install = false;
|
||||
let metadata_file = current_path.join("metadata.json");
|
||||
let mut framework = if metadata_file.exists() {
|
||||
info!("Using pre-existing metadata file: {:?}", metadata_file);
|
||||
InstallerFramework::new_with_db(config, current_path).log_expect("Unable to parse metadata")
|
||||
InstallerFramework::new_with_db(config.clone(), current_path).unwrap_or_else(|e| {
|
||||
error!("Failed to load metadata: {:?}", e);
|
||||
warn!("Entering recovery mode");
|
||||
InstallerFramework::new_recovery_mode(config, current_path)
|
||||
})
|
||||
} else {
|
||||
info!("Starting fresh install");
|
||||
fresh_install = true;
|
||||
InstallerFramework::new(config)
|
||||
};
|
||||
|
||||
// check for existing installs if we are running as a fresh install
|
||||
let installed_path = PathBuf::from(framework.get_default_path().unwrap());
|
||||
if fresh_install && installed_path.join("metadata.json").exists() {
|
||||
info!("Existing install detected! Copying Trying to launch this install instead");
|
||||
// Ignore the return value from this since it should exit the application if its successful
|
||||
let _ = replace_existing_install(¤t_exe, &installed_path);
|
||||
}
|
||||
|
||||
let is_launcher = if let Some(string) = matches.value_of("launcher") {
|
||||
framework.is_launcher = true;
|
||||
framework.launcher_path = Some(string.to_string());
|
||||
|
|
@ -135,3 +157,50 @@ fn main() {
|
|||
// Start up the UI
|
||||
frontend::launch(&app_name, is_launcher, framework);
|
||||
}
|
||||
|
||||
fn replace_existing_install(current_exe: &PathBuf, installed_path: &PathBuf) -> Result<(), String> {
|
||||
// Generate installer path
|
||||
let platform_extension = if cfg!(windows) {
|
||||
"maintenancetool.exe"
|
||||
} else {
|
||||
"maintenancetool"
|
||||
};
|
||||
|
||||
let new_tool = if cfg!(windows) {
|
||||
"maintenancetool_new.exe"
|
||||
} else {
|
||||
"maintenancetool_new"
|
||||
};
|
||||
|
||||
if let Err(v) = fs::copy(current_exe, installed_path.join(new_tool)) {
|
||||
return Err(format!("Unable to copy installer binary: {:?}", v));
|
||||
}
|
||||
|
||||
let existing = installed_path
|
||||
.join(platform_extension)
|
||||
.into_os_string()
|
||||
.into_string();
|
||||
let new = installed_path.join(new_tool).into_os_string().into_string();
|
||||
if existing.is_ok() && new.is_ok() {
|
||||
// Remove NTFS alternate stream which tells the operating system that the updater was downloaded from the internet
|
||||
if cfg!(windows) {
|
||||
let _ = fs::remove_file(
|
||||
installed_path.join("maintenancetool_new.exe:Zone.Identifier:$DATA"),
|
||||
);
|
||||
}
|
||||
info!("Launching {:?}", existing);
|
||||
let success = Command::new(new.unwrap())
|
||||
.arg("--swap")
|
||||
.arg(existing.unwrap())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn();
|
||||
if success.is_ok() {
|
||||
exit(0);
|
||||
} else {
|
||||
error!("Unable to start existing yuzu maintenance tool. Launching old one instead");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,8 @@ extern "C" int saveShortcut(
|
|||
const wchar_t *description,
|
||||
const wchar_t *path,
|
||||
const wchar_t *args,
|
||||
const wchar_t *workingDir)
|
||||
const wchar_t *workingDir,
|
||||
const wchar_t *exePath)
|
||||
{
|
||||
char *errStr = NULL;
|
||||
HRESULT h;
|
||||
|
|
@ -82,6 +83,9 @@ extern "C" int saveShortcut(
|
|||
shellLink->SetDescription(description);
|
||||
if (path != NULL)
|
||||
shellLink->SetPath(path);
|
||||
// default to using the first icon in the exe (usually correct)
|
||||
if (exePath != NULL)
|
||||
shellLink->SetIconLocation(exePath, 0);
|
||||
if (args != NULL)
|
||||
shellLink->SetArguments(args);
|
||||
if (workingDir != NULL)
|
||||
|
|
@ -95,6 +99,10 @@ extern "C" int saveShortcut(
|
|||
goto err;
|
||||
}
|
||||
|
||||
// Notify that a new shortcut was created using the shell api
|
||||
SHChangeNotify(SHCNE_CREATE, SHCNF_PATH, shortcutPath, NULL);
|
||||
SHChangeNotify(SHCNE_UPDATEITEM, SHCNF_PATH, shortcutPath, NULL);
|
||||
|
||||
persistFile->Release();
|
||||
shellLink->Release();
|
||||
CoUninitialize();
|
||||
|
|
@ -160,3 +168,15 @@ extern "C" HRESULT getSystemFolder(wchar_t *out_path)
|
|||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
extern "C" HRESULT getDesktopFolder(wchar_t *out_path)
|
||||
{
|
||||
PWSTR path = NULL;
|
||||
HRESULT result = SHGetKnownFolderPath(FOLDERID_Desktop, 0, NULL, &path);
|
||||
if (result == S_OK)
|
||||
{
|
||||
wcscpy_s(out_path, MAX_PATH + 1, path);
|
||||
CoTaskMemFree(path);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,14 @@ mod natives {
|
|||
#![allow(non_snake_case)]
|
||||
|
||||
const PROCESS_LEN: usize = 10192;
|
||||
const WV2_INSTALLER_DATA: &[u8] = include_bytes!("../../MicrosoftEdgeWebview2Setup.exe");
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use std::env;
|
||||
use std::io::Write;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use std::path::Path;
|
||||
|
||||
use winapi::shared::minwindef::{DWORD, FALSE, MAX_PATH};
|
||||
|
||||
|
|
@ -26,10 +30,16 @@ mod natives {
|
|||
use winapi::um::psapi::{
|
||||
EnumProcessModulesEx, GetModuleFileNameExW, K32EnumProcesses, LIST_MODULES_ALL,
|
||||
};
|
||||
use winapi::um::shellapi::ShellExecuteW;
|
||||
use winapi::um::winnt::{
|
||||
HANDLE, PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE, PROCESS_VM_READ,
|
||||
};
|
||||
use winapi::um::winuser::SW_SHOWDEFAULT;
|
||||
|
||||
use std::process::Command;
|
||||
use tempfile::Builder;
|
||||
use tinyfiledialogs::{message_box_yes_no, MessageBoxIcon, YesNo};
|
||||
use webview2::EnvironmentBuilder;
|
||||
use widestring::U16CString;
|
||||
|
||||
extern "C" {
|
||||
|
|
@ -39,6 +49,7 @@ mod natives {
|
|||
path: *const winapi::ctypes::wchar_t,
|
||||
args: *const winapi::ctypes::wchar_t,
|
||||
workingDir: *const winapi::ctypes::wchar_t,
|
||||
exePath: *const winapi::ctypes::wchar_t,
|
||||
) -> ::std::os::raw::c_int;
|
||||
|
||||
pub fn isDarkThemeActive() -> ::std::os::raw::c_uint;
|
||||
|
|
@ -49,6 +60,36 @@ mod natives {
|
|||
) -> ::std::os::raw::c_int;
|
||||
|
||||
pub fn getSystemFolder(out_path: *mut ::std::os::raw::c_ushort) -> HRESULT;
|
||||
|
||||
pub fn getDesktopFolder(out_path: *mut ::std::os::raw::c_ushort) -> HRESULT;
|
||||
}
|
||||
|
||||
pub fn prepare_install_webview2(name: &str) -> Result<(), String> {
|
||||
if EnvironmentBuilder::default()
|
||||
.get_available_browser_version_string()
|
||||
.is_ok()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
if message_box_yes_no(&format!("{} installer", name), &format!("{} installer now requires Webview2 runtime to function properly.\nDo you wish to install it now?", name), MessageBoxIcon::Question, YesNo::Yes) == YesNo::No {
|
||||
std::process::exit(1);
|
||||
}
|
||||
let mut installer_file = Builder::new()
|
||||
.suffix(".exe")
|
||||
.tempfile()
|
||||
.log_expect("Unable to open the webview2 installer file");
|
||||
installer_file
|
||||
.write_all(&WV2_INSTALLER_DATA)
|
||||
.log_expect("Unable to write the webview2 installer file");
|
||||
let path = installer_file.path().to_owned();
|
||||
installer_file.keep().log_unwrap();
|
||||
Command::new(&path)
|
||||
.arg("/install")
|
||||
.spawn()
|
||||
.log_expect("Unable to run the webview2 installer")
|
||||
.wait()
|
||||
.log_unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Needed here for Windows interop
|
||||
|
|
@ -59,13 +100,63 @@ mod natives {
|
|||
target: &str,
|
||||
args: &str,
|
||||
working_dir: &str,
|
||||
exe_path: &str,
|
||||
) -> Result<String, String> {
|
||||
let source_file = format!(
|
||||
"{}\\Microsoft\\Windows\\Start Menu\\Programs\\{}.lnk",
|
||||
env::var("APPDATA").log_expect("APPDATA is bad, apparently"),
|
||||
name
|
||||
);
|
||||
create_shortcut_inner(
|
||||
source_file,
|
||||
name,
|
||||
description,
|
||||
target,
|
||||
args,
|
||||
working_dir,
|
||||
exe_path,
|
||||
)
|
||||
}
|
||||
|
||||
// Needed here for Windows interop
|
||||
#[allow(unsafe_code)]
|
||||
pub fn create_desktop_shortcut(
|
||||
name: &str,
|
||||
description: &str,
|
||||
target: &str,
|
||||
args: &str,
|
||||
working_dir: &str,
|
||||
exe_path: &str,
|
||||
) -> Result<String, String> {
|
||||
let mut cmd_path = [0u16; MAX_PATH + 1];
|
||||
let _result = unsafe { getDesktopFolder(cmd_path.as_mut_ptr()) };
|
||||
let source_path = format!(
|
||||
"{}\\{}.lnk",
|
||||
String::from_utf16_lossy(&cmd_path[..count_u16(&cmd_path)]).as_str(),
|
||||
name
|
||||
);
|
||||
create_shortcut_inner(
|
||||
source_path,
|
||||
name,
|
||||
description,
|
||||
target,
|
||||
args,
|
||||
working_dir,
|
||||
exe_path,
|
||||
)
|
||||
}
|
||||
|
||||
// Needed here for Windows interop
|
||||
#[allow(unsafe_code)]
|
||||
fn create_shortcut_inner(
|
||||
source_file: String,
|
||||
_name: &str,
|
||||
description: &str,
|
||||
target: &str,
|
||||
args: &str,
|
||||
working_dir: &str,
|
||||
exe_path: &str,
|
||||
) -> Result<String, String> {
|
||||
info!("Generating shortcut @ {:?}", source_file);
|
||||
|
||||
let native_target_dir = U16CString::from_str(source_file.clone())
|
||||
|
|
@ -78,6 +169,8 @@ mod natives {
|
|||
U16CString::from_str(args).log_expect("Error while converting to wchar_t");
|
||||
let native_working_dir =
|
||||
U16CString::from_str(working_dir).log_expect("Error while converting to wchar_t");
|
||||
let native_exe_path =
|
||||
U16CString::from_str(exe_path).log_expect("Error while converting to wchar_t");
|
||||
|
||||
let shortcutResult = unsafe {
|
||||
saveShortcut(
|
||||
|
|
@ -86,6 +179,7 @@ mod natives {
|
|||
native_target.as_ptr(),
|
||||
native_args.as_ptr(),
|
||||
native_working_dir.as_ptr(),
|
||||
native_exe_path.as_ptr(),
|
||||
)
|
||||
};
|
||||
|
||||
|
|
@ -98,6 +192,37 @@ mod natives {
|
|||
}
|
||||
}
|
||||
|
||||
// Needed to call unsafe function `ShellExecuteW` from `winapi` crate
|
||||
#[allow(unsafe_code)]
|
||||
pub fn open_in_shell(path: &Path) {
|
||||
let native_verb = U16CString::from_str("open").unwrap();
|
||||
// https://doc.rust-lang.org/std/os/windows/ffi/trait.OsStrExt.html#tymethod.encode_wide
|
||||
let mut native_path: Vec<u16> = path.as_os_str().encode_wide().collect();
|
||||
native_path.push(0); // NULL terminator
|
||||
unsafe {
|
||||
ShellExecuteW(
|
||||
std::ptr::null_mut(),
|
||||
native_verb.as_ptr(),
|
||||
native_path.as_ptr(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
SW_SHOWDEFAULT,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn count_u16(u16str: &[u16]) -> usize {
|
||||
let mut pos = 0;
|
||||
for x in u16str.iter() {
|
||||
if *x == 0 {
|
||||
break;
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
pos
|
||||
}
|
||||
|
||||
/// Cleans up the installer
|
||||
pub fn burn_on_exit(app_name: &str) {
|
||||
let current_exe = env::current_exe().log_expect("Current executable could not be found");
|
||||
|
|
@ -111,6 +236,7 @@ mod natives {
|
|||
.to_str()
|
||||
.log_expect("Unable to convert tool path to string")
|
||||
.replace(" ", "\\ ");
|
||||
let tool_wv = format!("{}.WebView2", tool);
|
||||
|
||||
let log = path.join(format!("{}_installer.log", app_name));
|
||||
let log = log
|
||||
|
|
@ -118,7 +244,15 @@ mod natives {
|
|||
.log_expect("Unable to convert log path to string")
|
||||
.replace(" ", "\\ ");
|
||||
|
||||
let target_arguments = format!("/C choice /C Y /N /D Y /T 2 & del {} {}", tool, log);
|
||||
let install_path = path
|
||||
.to_str()
|
||||
.log_expect("Unable to convert path to string")
|
||||
.replace(" ", "\\ ");
|
||||
|
||||
let target_arguments = format!(
|
||||
"/C choice /C Y /N /D Y /T 2 & del {} {} & rmdir /Q /S {} & rmdir {}",
|
||||
tool, log, tool_wv, install_path
|
||||
);
|
||||
|
||||
info!("Launching cmd with {:?}", target_arguments);
|
||||
|
||||
|
|
@ -248,19 +382,21 @@ mod natives {
|
|||
|
||||
#[cfg(not(windows))]
|
||||
mod natives {
|
||||
use std::fs::remove_file;
|
||||
use std::fs::{remove_dir, remove_file};
|
||||
|
||||
use std::env;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use sysinfo::{ProcessExt, SystemExt};
|
||||
use sysinfo::{PidExt, ProcessExt, SystemExt};
|
||||
|
||||
use dirs;
|
||||
|
||||
use slug::slugify;
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn create_shortcut(
|
||||
|
|
@ -269,6 +405,7 @@ mod natives {
|
|||
target: &str,
|
||||
args: &str,
|
||||
working_dir: &str,
|
||||
exe_path: &str,
|
||||
) -> Result<String, String> {
|
||||
// FIXME: no icon will be shown since no icon is provided
|
||||
let data_local_dir = dirs::data_local_dir();
|
||||
|
|
@ -277,7 +414,7 @@ mod natives {
|
|||
let mut path = x;
|
||||
path.push("applications");
|
||||
match create_dir_all(path.to_path_buf()) {
|
||||
Ok(_) => (()),
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
return Err(format!(
|
||||
"Local data directory does not exist and cannot be created: {}",
|
||||
|
|
@ -285,17 +422,17 @@ mod natives {
|
|||
));
|
||||
}
|
||||
};
|
||||
path.push(format!("{}.desktop", slugify(name))); // file name
|
||||
path.push(format!("yuzu-maintenance-tool_{}.desktop", slugify(name))); // file name
|
||||
let desktop_file = format!(
|
||||
"[Desktop Entry]\nName={}\nExec=\"{}\" {}\nComment={}\nPath={}\n",
|
||||
name, target, args, description, working_dir
|
||||
"[Desktop Entry]\nType=Application\nName={}\nExec=\"{}\" {}\nComment={}\nPath={}\nIcon=yuzu\n",
|
||||
name, target, args, description, working_dir
|
||||
);
|
||||
let desktop_f = File::create(path);
|
||||
let mut desktop_f = match desktop_f {
|
||||
Ok(file) => file,
|
||||
Err(e) => return Err(format!("Unable to create desktop file: {}", e)),
|
||||
};
|
||||
let mut desktop_f = desktop_f.write_all(desktop_file.as_bytes());
|
||||
let desktop_f = desktop_f.write_all(desktop_file.as_bytes());
|
||||
match desktop_f {
|
||||
Ok(_) => Ok("".to_string()),
|
||||
Err(e) => Err(format!("Unable to write desktop file: {}", e)),
|
||||
|
|
@ -313,27 +450,44 @@ mod natives {
|
|||
target: &str,
|
||||
args: &str,
|
||||
working_dir: &str,
|
||||
_exe_path: &str,
|
||||
) -> Result<String, String> {
|
||||
warn!("STUB! Creating shortcut is not implemented on macOS");
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
pub fn open_in_shell(path: &Path) {
|
||||
let shell: &str;
|
||||
if cfg!(target_os = "linux") {
|
||||
shell = "xdg-open";
|
||||
} else if cfg!(target_os = "macos") {
|
||||
shell = "open";
|
||||
} else {
|
||||
warn!("Unsupported platform");
|
||||
return;
|
||||
}
|
||||
Command::new(shell).arg(path).spawn().ok();
|
||||
}
|
||||
|
||||
/// Cleans up the installer
|
||||
pub fn burn_on_exit(app_name: &str) {
|
||||
let current_exe = env::current_exe().log_expect("Current executable could not be found");
|
||||
let exe_dir = current_exe
|
||||
.parent()
|
||||
.log_expect("Current executable directory cannot be found");
|
||||
|
||||
if let Err(e) = remove_file(exe_dir.join(format!("{}_installer.log", app_name))) {
|
||||
// No regular logging now.
|
||||
eprintln!("Failed to delete maintenance log: {:?}", e);
|
||||
};
|
||||
|
||||
// Thank god for *nix platforms
|
||||
if let Err(e) = remove_file(¤t_exe) {
|
||||
// No regular logging now.
|
||||
eprintln!("Failed to delete maintenancetool: {:?}", e);
|
||||
};
|
||||
|
||||
let current_dir = env::current_dir().log_expect("Current directory cannot be found");
|
||||
|
||||
if let Err(e) = remove_file(current_dir.join(format!("{}_installer.log", app_name))) {
|
||||
// No regular logging now.
|
||||
eprintln!("Failed to delete installer log: {:?}", e);
|
||||
};
|
||||
// delete the directory if not empty and ignore errors (since we can't handle errors anymore)
|
||||
remove_dir(exe_dir).ok();
|
||||
}
|
||||
|
||||
/// Returns a list of running processes
|
||||
|
|
@ -342,9 +496,9 @@ mod natives {
|
|||
let mut processes: Vec<super::Process> = Vec::new();
|
||||
let mut system = sysinfo::System::new();
|
||||
system.refresh_all();
|
||||
for (pid, procs) in system.get_process_list() {
|
||||
for (pid, procs) in system.processes() {
|
||||
processes.push(super::Process {
|
||||
pid: *pid as usize,
|
||||
pid: pid.as_u32() as usize,
|
||||
name: procs.name().to_string(),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use std::{thread, time};
|
|||
|
||||
use clap::{App, ArgMatches};
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
/// Swaps around the main executable if needed.
|
||||
pub fn perform_swap(current_exe: &PathBuf, to_path: Option<&str>) {
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ use reqwest::StatusCode;
|
|||
|
||||
use serde_json;
|
||||
|
||||
use sources::types::*;
|
||||
use crate::sources::types::*;
|
||||
|
||||
use http::build_client;
|
||||
use crate::http::build_client;
|
||||
|
||||
pub struct GithubReleases {}
|
||||
|
||||
|
|
@ -107,6 +107,7 @@ impl ReleaseSource for GithubReleases {
|
|||
files.push(File {
|
||||
name: string.to_string(),
|
||||
url: url.to_string(),
|
||||
requires_authorization: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,12 +6,15 @@ pub mod types;
|
|||
|
||||
pub mod github;
|
||||
|
||||
pub mod patreon;
|
||||
|
||||
use self::types::ReleaseSource;
|
||||
|
||||
/// Returns a ReleaseSource by a name, if possible
|
||||
pub fn get_by_name(name: &str) -> Option<Box<dyn ReleaseSource>> {
|
||||
match name {
|
||||
"github" => Some(Box::new(github::GithubReleases::new())),
|
||||
"patreon" => Some(Box::new(patreon::PatreonReleases::new())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
102
src/sources/patreon.rs
Normal file
102
src/sources/patreon.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
//! patreon.rs
|
||||
//!
|
||||
//! Contains the yuzu-emu core API implementation of a release source.
|
||||
|
||||
use crate::http::build_client;
|
||||
use crate::sources::types::*;
|
||||
use reqwest::header::USER_AGENT;
|
||||
use reqwest::StatusCode;
|
||||
|
||||
pub struct PatreonReleases {}
|
||||
|
||||
/// The configuration for this release.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct PatreonConfig {
|
||||
repo: String,
|
||||
}
|
||||
|
||||
impl PatreonReleases {
|
||||
pub fn new() -> Self {
|
||||
PatreonReleases {}
|
||||
}
|
||||
}
|
||||
|
||||
impl ReleaseSource for PatreonReleases {
|
||||
fn get_current_releases(&self, _config: &TomlValue) -> Result<Vec<Release>, String> {
|
||||
let config: PatreonConfig = match _config.clone().try_into() {
|
||||
Ok(v) => v,
|
||||
Err(v) => return Err(format!("Failed to parse release config: {:?}", v)),
|
||||
};
|
||||
|
||||
let mut results: Vec<Release> = Vec::new();
|
||||
|
||||
// Build the HTTP client up
|
||||
let client = build_client()?;
|
||||
let mut response = client
|
||||
.get(&format!(
|
||||
"https://api.yuzu-emu.org/downloads/{}/",
|
||||
config.repo
|
||||
))
|
||||
.header(USER_AGENT, "liftinstall (j-selby)")
|
||||
.send()
|
||||
.map_err(|x| format!("Error while sending HTTP request: {:?}", x))?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => {}
|
||||
StatusCode::FORBIDDEN => {
|
||||
return Err("You are not eligible to download this release".to_string());
|
||||
}
|
||||
_ => {
|
||||
return Err(format!("Bad status code: {:?}.", response.status()));
|
||||
}
|
||||
}
|
||||
|
||||
let body = response
|
||||
.text()
|
||||
.map_err(|x| format!("Failed to decode HTTP response body: {:?}", x))?;
|
||||
|
||||
let result: serde_json::Value = serde_json::from_str(&body)
|
||||
.map_err(|x| format!("Failed to parse response: {:?}", x))?;
|
||||
|
||||
// Parse JSON from server
|
||||
let mut files = Vec::new();
|
||||
|
||||
let id: u64 = match result["version"].as_u64() {
|
||||
Some(v) => v,
|
||||
None => return Err("JSON payload missing information about ID".to_string()),
|
||||
};
|
||||
|
||||
let downloads = match result["files"].as_array() {
|
||||
Some(v) => v,
|
||||
None => return Err("JSON payload not an array".to_string()),
|
||||
};
|
||||
|
||||
for file in downloads.iter() {
|
||||
let string = match file["name"].as_str() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return Err("JSON payload missing information about release name".to_string());
|
||||
}
|
||||
};
|
||||
|
||||
let url = match file["url"].as_str() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return Err("JSON payload missing information about release URL".to_string());
|
||||
}
|
||||
};
|
||||
|
||||
files.push(File {
|
||||
name: string.to_string(),
|
||||
url: url.to_string(),
|
||||
requires_authorization: true,
|
||||
});
|
||||
}
|
||||
|
||||
results.push(Release {
|
||||
version: Version::new_number(id),
|
||||
files,
|
||||
});
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
|
|
@ -23,9 +23,7 @@ impl Version {
|
|||
fn coarse_into_semver(&self) -> SemverVersion {
|
||||
match *self {
|
||||
Version::Semver(ref version) => version.to_owned(),
|
||||
Version::Integer(ref version) => {
|
||||
SemverVersion::from((version.to_owned(), 0 as u64, 0 as u64))
|
||||
}
|
||||
Version::Integer(ref version) => SemverVersion::new(version.to_owned(), 0u64, 0u64),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,6 +64,7 @@ impl Ord for Version {
|
|||
pub struct File {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub requires_authorization: bool,
|
||||
}
|
||||
|
||||
impl File {}
|
||||
|
|
|
|||
88
src/tasks/check_authorization.rs
Normal file
88
src/tasks/check_authorization.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
//! Validates that users have correct authorization to download packages.
|
||||
|
||||
use crate::frontend::rest::services::authentication;
|
||||
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use crate::tasks::resolver::ResolvePackageTask;
|
||||
use crate::tasks::{Task, TaskDependency, TaskMessage, TaskOrdering, TaskParamType};
|
||||
|
||||
pub struct CheckAuthorizationTask {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Task for CheckAuthorizationTask {
|
||||
fn execute(
|
||||
&mut self,
|
||||
mut input: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
_messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
assert_eq!(input.len(), 1);
|
||||
|
||||
let params = input
|
||||
.pop()
|
||||
.log_expect("Check Authorization Task should have input from resolver!");
|
||||
let (version, file) = match params {
|
||||
TaskParamType::File(v, f) => Ok((v, f)),
|
||||
_ => Err("Unexpected TaskParamType in CheckAuthorization: {:?}"),
|
||||
}?;
|
||||
|
||||
if !file.requires_authorization {
|
||||
return Ok(TaskParamType::Authentication(version, file, None));
|
||||
}
|
||||
|
||||
let username = context.database.credentials.username.clone();
|
||||
let token = context.database.credentials.token.clone();
|
||||
let authentication = context
|
||||
.config
|
||||
.clone()
|
||||
.log_expect("In-memory configuration doesn't exist")
|
||||
.authentication
|
||||
.log_expect("No authentication configuration exists while checking authorization");
|
||||
|
||||
let auth_url = authentication.auth_url.clone();
|
||||
let pub_key_base64 = authentication.pub_key_base64.clone();
|
||||
let validation = authentication.validation.clone();
|
||||
|
||||
// Authorizaion is required for this package so post the username and token and get a jwt_token response
|
||||
let jwt_token = match authentication::authenticate_sync(auth_url, username, token) {
|
||||
Ok(jwt) => jwt,
|
||||
Err(_) => return Ok(TaskParamType::Authentication(version, file, None)),
|
||||
};
|
||||
|
||||
let claims =
|
||||
match authentication::validate_token(jwt_token.clone(), pub_key_base64, validation) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return Ok(TaskParamType::Authentication(version, file, None)),
|
||||
};
|
||||
|
||||
// Validate that they are authorized
|
||||
if !claims.roles.contains(&"vip".to_string())
|
||||
&& !claims.channels.contains(&"early-access".to_string())
|
||||
{
|
||||
return Ok(TaskParamType::Authentication(version, file, None));
|
||||
}
|
||||
|
||||
Ok(TaskParamType::Authentication(
|
||||
version,
|
||||
file,
|
||||
Some(jwt_token),
|
||||
))
|
||||
}
|
||||
|
||||
fn dependencies(&self) -> Vec<TaskDependency> {
|
||||
vec![TaskDependency::build(
|
||||
TaskOrdering::Pre,
|
||||
Box::new(ResolvePackageTask {
|
||||
name: self.name.clone(),
|
||||
}),
|
||||
)]
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
format!("CheckAuthorizationTask (for {:?})", self.name)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,19 @@
|
|||
//! Downloads a package into memory.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskOrdering;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::check_authorization::CheckAuthorizationTask;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskOrdering;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use tasks::resolver::ResolvePackageTask;
|
||||
use crate::http::stream_file;
|
||||
|
||||
use http::stream_file;
|
||||
use number_prefix::NumberPrefix::{self, Prefixed, Standalone};
|
||||
|
||||
use number_prefix::{NumberPrefix, Prefixed, Standalone};
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub struct DownloadPackageTask {
|
||||
pub name: String,
|
||||
|
|
@ -29,12 +28,21 @@ impl Task for DownloadPackageTask {
|
|||
) -> Result<TaskParamType, String> {
|
||||
assert_eq!(input.len(), 1);
|
||||
|
||||
let file = input.pop().log_expect("Should have input from resolver!");
|
||||
let (version, file) = match file {
|
||||
TaskParamType::File(v, f) => (v, f),
|
||||
let file = input
|
||||
.pop()
|
||||
.log_expect("Download Package Task should have input from resolver!");
|
||||
let (version, file, auth) = match file {
|
||||
TaskParamType::Authentication(v, f, auth) => (v, f, auth),
|
||||
_ => return Err("Unexpected param type to download package".to_string()),
|
||||
};
|
||||
|
||||
// TODO: move this back below checking for latest version after testing is done
|
||||
if file.requires_authorization && auth.is_none() {
|
||||
info!("Authorization required to update this package!");
|
||||
messenger(&TaskMessage::AuthorizationRequired("AuthorizationRequired"));
|
||||
return Ok(TaskParamType::Break);
|
||||
}
|
||||
|
||||
// Check to see if this is the newest file available already
|
||||
for element in &context.database.packages {
|
||||
if element.name == self.name {
|
||||
|
|
@ -54,7 +62,7 @@ impl Task for DownloadPackageTask {
|
|||
let mut downloaded = 0;
|
||||
let mut data_storage: Vec<u8> = Vec::new();
|
||||
|
||||
stream_file(&file.url, |data, size| {
|
||||
stream_file(&file.url, auth, |data, size| {
|
||||
{
|
||||
data_storage.extend_from_slice(&data);
|
||||
}
|
||||
|
|
@ -92,7 +100,7 @@ impl Task for DownloadPackageTask {
|
|||
fn dependencies(&self) -> Vec<TaskDependency> {
|
||||
vec![TaskDependency::build(
|
||||
TaskOrdering::Pre,
|
||||
Box::new(ResolvePackageTask {
|
||||
Box::new(CheckAuthorizationTask {
|
||||
name: self.name.clone(),
|
||||
}),
|
||||
)]
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
//! Verifies that this is the only running instance of the installer, and that no application is running.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use native::get_process_names;
|
||||
use native::Process;
|
||||
use crate::native::get_process_names;
|
||||
use crate::native::Process;
|
||||
|
||||
use std::process;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,29 @@
|
|||
//! Overall hierarchy for installing a installation of the application.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::ensure_only_instance::EnsureOnlyInstanceTask;
|
||||
use tasks::install_dir::VerifyInstallDirTask;
|
||||
use tasks::install_global_shortcut::InstallGlobalShortcutsTask;
|
||||
use tasks::install_pkg::InstallPackageTask;
|
||||
use tasks::save_executable::SaveExecutableTask;
|
||||
use tasks::uninstall_pkg::UninstallPackageTask;
|
||||
use crate::tasks::ensure_only_instance::EnsureOnlyInstanceTask;
|
||||
use crate::tasks::install_dir::VerifyInstallDirTask;
|
||||
use crate::tasks::install_global_shortcut::InstallGlobalShortcutsTask;
|
||||
use crate::tasks::install_pkg::InstallPackageTask;
|
||||
use crate::tasks::launch_installed_on_exit::LaunchOnExitTask;
|
||||
use crate::tasks::remove_target_dir::RemoveTargetDirTask;
|
||||
use crate::tasks::save_executable::SaveExecutableTask;
|
||||
use crate::tasks::uninstall_pkg::UninstallPackageTask;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskOrdering;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskOrdering;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
pub struct InstallTask {
|
||||
pub items: Vec<String>,
|
||||
pub uninstall_items: Vec<String>,
|
||||
pub fresh_install: bool,
|
||||
pub create_desktop_shortcuts: bool,
|
||||
// force_install: remove the target directory before installing
|
||||
pub force_install: bool,
|
||||
}
|
||||
|
||||
impl Task for InstallTask {
|
||||
|
|
@ -40,6 +45,13 @@ impl Task for InstallTask {
|
|||
Box::new(EnsureOnlyInstanceTask {}),
|
||||
));
|
||||
|
||||
if self.force_install {
|
||||
elements.push(TaskDependency::build(
|
||||
TaskOrdering::Pre,
|
||||
Box::new(RemoveTargetDirTask {}),
|
||||
));
|
||||
}
|
||||
|
||||
elements.push(TaskDependency::build(
|
||||
TaskOrdering::Pre,
|
||||
Box::new(VerifyInstallDirTask {
|
||||
|
|
@ -47,13 +59,6 @@ impl Task for InstallTask {
|
|||
}),
|
||||
));
|
||||
|
||||
for item in &self.items {
|
||||
elements.push(TaskDependency::build(
|
||||
TaskOrdering::Pre,
|
||||
Box::new(InstallPackageTask { name: item.clone() }),
|
||||
));
|
||||
}
|
||||
|
||||
for item in &self.uninstall_items {
|
||||
elements.push(TaskDependency::build(
|
||||
TaskOrdering::Pre,
|
||||
|
|
@ -64,6 +69,16 @@ impl Task for InstallTask {
|
|||
));
|
||||
}
|
||||
|
||||
for item in &self.items {
|
||||
elements.push(TaskDependency::build(
|
||||
TaskOrdering::Pre,
|
||||
Box::new(InstallPackageTask {
|
||||
name: item.clone(),
|
||||
create_desktop_shortcuts: self.create_desktop_shortcuts,
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
if self.fresh_install {
|
||||
elements.push(TaskDependency::build(
|
||||
TaskOrdering::Pre,
|
||||
|
|
@ -74,6 +89,11 @@ impl Task for InstallTask {
|
|||
TaskOrdering::Pre,
|
||||
Box::new(InstallGlobalShortcutsTask {}),
|
||||
));
|
||||
|
||||
elements.push(TaskDependency::build(
|
||||
TaskOrdering::Post,
|
||||
Box::new(LaunchOnExitTask {}),
|
||||
))
|
||||
}
|
||||
|
||||
elements
|
||||
|
|
|
|||
133
src/tasks/install_desktop_shortcut.rs
Normal file
133
src/tasks/install_desktop_shortcut.rs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
//! Generates shortcuts for a specified file.
|
||||
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use crate::config::PackageDescription;
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
#[cfg(windows)]
|
||||
use crate::native::create_desktop_shortcut;
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::native::create_shortcut;
|
||||
|
||||
pub struct InstallDesktopShortcutTask {
|
||||
pub name: String,
|
||||
pub should_run: bool,
|
||||
}
|
||||
|
||||
impl Task for InstallDesktopShortcutTask {
|
||||
fn execute(
|
||||
&mut self,
|
||||
_: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
if !self.should_run {
|
||||
return Ok(TaskParamType::GeneratedShortcuts(Vec::new()));
|
||||
}
|
||||
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
&format!(
|
||||
"Generating desktop shortcuts for package {:?}...",
|
||||
self.name
|
||||
),
|
||||
0.0,
|
||||
));
|
||||
|
||||
let path = context
|
||||
.install_path
|
||||
.as_ref()
|
||||
.log_expect("No install path specified");
|
||||
|
||||
let starting_dir = path
|
||||
.to_str()
|
||||
.log_expect("Unable to build shortcut metadata (startingdir)");
|
||||
|
||||
let mut installed_files = Vec::new();
|
||||
|
||||
let mut metadata: Option<PackageDescription> = None;
|
||||
for description in &context
|
||||
.config
|
||||
.as_ref()
|
||||
.log_expect("Should have packages by now")
|
||||
.packages
|
||||
{
|
||||
if self.name == description.name {
|
||||
metadata = Some(description.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let package = match metadata {
|
||||
Some(v) => v,
|
||||
None => return Err(format!("Package {:?} could not be found.", self.name)),
|
||||
};
|
||||
|
||||
// Generate installer path
|
||||
let platform_extension = if cfg!(windows) {
|
||||
"maintenancetool.exe"
|
||||
} else {
|
||||
"maintenancetool"
|
||||
};
|
||||
|
||||
for shortcut in package.shortcuts {
|
||||
let tool_path = path.join(platform_extension);
|
||||
let tool_path = tool_path
|
||||
.to_str()
|
||||
.log_expect("Unable to build shortcut metadata (tool)");
|
||||
|
||||
let exe_path = path.join(shortcut.relative_path);
|
||||
let exe_path = exe_path
|
||||
.to_str()
|
||||
.log_expect("Unable to build shortcut metadata (exe)");
|
||||
|
||||
#[cfg(windows)]
|
||||
installed_files.push(create_desktop_shortcut(
|
||||
&shortcut.name,
|
||||
&shortcut.description,
|
||||
tool_path,
|
||||
// TODO: Send by list
|
||||
&format!("--launcher \"{}\"", exe_path),
|
||||
&starting_dir,
|
||||
exe_path,
|
||||
)?);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
installed_files.push(create_shortcut(
|
||||
&shortcut.name,
|
||||
&shortcut.description,
|
||||
tool_path,
|
||||
&format!("--launcher \"{}\"", exe_path),
|
||||
&starting_dir,
|
||||
exe_path,
|
||||
)?);
|
||||
}
|
||||
|
||||
// Update the installed packages shortcuts information in the database
|
||||
let packages = &mut context.database.packages;
|
||||
for pack in packages {
|
||||
if pack.name == self.name {
|
||||
pack.shortcuts.extend(installed_files.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(TaskParamType::GeneratedShortcuts(installed_files))
|
||||
}
|
||||
|
||||
fn dependencies(&self) -> Vec<TaskDependency> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
format!(
|
||||
"InstallDesktopShortcutTask (for {:?}, should_run = {:?})",
|
||||
self.name, self.should_run
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
//! Verifies properties about the installation directory.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use std::fs::create_dir_all;
|
||||
use std::fs::read_dir;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub struct VerifyInstallDirTask {
|
||||
pub clean_install: bool,
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
//! Generates the global shortcut for this application.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use native::create_shortcut;
|
||||
use tasks::save_database::SaveDatabaseTask;
|
||||
use tasks::TaskOrdering;
|
||||
use crate::native::create_shortcut;
|
||||
use crate::tasks::save_database::SaveDatabaseTask;
|
||||
use crate::tasks::TaskOrdering;
|
||||
|
||||
pub struct InstallGlobalShortcutsTask {}
|
||||
|
||||
|
|
@ -58,9 +58,10 @@ impl Task for InstallGlobalShortcutsTask {
|
|||
// TODO: Send by list
|
||||
"",
|
||||
&starting_dir,
|
||||
"",
|
||||
)?;
|
||||
|
||||
if !shortcut_file.is_empty() {
|
||||
if !shortcut_file.is_empty() && !context.database.shortcuts.contains(&shortcut_file) {
|
||||
context.database.shortcuts.push(shortcut_file);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +1,35 @@
|
|||
//! Installs a specific package.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::download_pkg::DownloadPackageTask;
|
||||
use tasks::install_shortcuts::InstallShortcutsTask;
|
||||
use tasks::save_database::SaveDatabaseTask;
|
||||
use tasks::uninstall_pkg::UninstallPackageTask;
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskOrdering;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::download_pkg::DownloadPackageTask;
|
||||
use crate::tasks::install_shortcuts::InstallShortcutsTask;
|
||||
use crate::tasks::save_database::SaveDatabaseTask;
|
||||
use crate::tasks::uninstall_pkg::UninstallPackageTask;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskOrdering;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use config::PackageDescription;
|
||||
use installer::LocalInstallation;
|
||||
use crate::config::PackageDescription;
|
||||
use crate::installer::LocalInstallation;
|
||||
|
||||
use std::fs::create_dir_all;
|
||||
use std::io::copy;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use archives;
|
||||
use crate::archives;
|
||||
|
||||
use crate::tasks::install_desktop_shortcut::InstallDesktopShortcutTask;
|
||||
use std::collections::HashSet;
|
||||
use std::fs::OpenOptions;
|
||||
use std::path::Path;
|
||||
|
||||
pub struct InstallPackageTask {
|
||||
pub name: String,
|
||||
pub create_desktop_shortcuts: bool,
|
||||
}
|
||||
|
||||
impl Task for InstallPackageTask {
|
||||
|
|
@ -66,20 +69,20 @@ impl Task for InstallPackageTask {
|
|||
None => return Err(format!("Package {:?} could not be found.", self.name)),
|
||||
};
|
||||
|
||||
// Grab data from the shortcut generator
|
||||
let shortcuts = input.pop().log_expect("Should have input from resolver!");
|
||||
let shortcuts = match shortcuts {
|
||||
TaskParamType::GeneratedShortcuts(files) => files,
|
||||
// If the resolver returned early, we need to unwind
|
||||
// Ignore input from the uninstaller - no useful information passed
|
||||
// If a previous task Breaks, then just early exit
|
||||
match input
|
||||
.pop()
|
||||
.log_expect("Install Package Task should have guaranteed output!")
|
||||
{
|
||||
TaskParamType::Break => return Ok(TaskParamType::None),
|
||||
_ => return Err("Unexpected shortcuts param type to install package".to_string()),
|
||||
_ => (),
|
||||
};
|
||||
|
||||
// Ignore input from the uninstaller - no useful information passed
|
||||
input.pop();
|
||||
|
||||
// Grab data from the resolver
|
||||
let data = input.pop().log_expect("Should have input from resolver!");
|
||||
let data = input
|
||||
.pop()
|
||||
.log_expect("Install Package Task should have input from resolver!");
|
||||
let (version, file, data) = match data {
|
||||
TaskParamType::FileContents(version, file, data) => (version, file, data),
|
||||
_ => return Err("Unexpected file contents param type to install package".to_string()),
|
||||
|
|
@ -139,7 +142,7 @@ impl Task for InstallPackageTask {
|
|||
info!("Creating file: {:?}", string_name);
|
||||
|
||||
if !installed_files.contains(&string_name) {
|
||||
installed_files.push(string_name.to_string());
|
||||
installed_files.push(string_name);
|
||||
}
|
||||
|
||||
let mut file_metadata = OpenOptions::new();
|
||||
|
|
@ -168,9 +171,9 @@ impl Task for InstallPackageTask {
|
|||
|
||||
// Save metadata about this package
|
||||
context.database.packages.push(LocalInstallation {
|
||||
name: package.name.to_owned(),
|
||||
name: package.name,
|
||||
version,
|
||||
shortcuts,
|
||||
shortcuts: HashSet::new(),
|
||||
files: installed_files,
|
||||
});
|
||||
|
||||
|
|
@ -195,11 +198,18 @@ impl Task for InstallPackageTask {
|
|||
}),
|
||||
),
|
||||
TaskDependency::build(
|
||||
TaskOrdering::Pre,
|
||||
TaskOrdering::Post,
|
||||
Box::new(InstallShortcutsTask {
|
||||
name: self.name.clone(),
|
||||
}),
|
||||
),
|
||||
TaskDependency::build(
|
||||
TaskOrdering::Post,
|
||||
Box::new(InstallDesktopShortcutTask {
|
||||
name: self.name.clone(),
|
||||
should_run: self.create_desktop_shortcuts,
|
||||
}),
|
||||
),
|
||||
TaskDependency::build(TaskOrdering::Post, Box::new(SaveDatabaseTask {})),
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
//! Generates shortcuts for a specified file.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use config::PackageDescription;
|
||||
use crate::config::PackageDescription;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
use native::create_shortcut;
|
||||
use crate::native::create_shortcut;
|
||||
|
||||
pub struct InstallShortcutsTask {
|
||||
pub name: String,
|
||||
|
|
@ -83,9 +83,18 @@ impl Task for InstallShortcutsTask {
|
|||
// TODO: Send by list
|
||||
&format!("--launcher \"{}\"", exe_path),
|
||||
&starting_dir,
|
||||
exe_path,
|
||||
)?);
|
||||
}
|
||||
|
||||
// Update the installed packages shortcuts information in the database
|
||||
let packages = &mut context.database.packages;
|
||||
for pack in packages {
|
||||
if pack.name == self.name {
|
||||
pack.shortcuts.extend(installed_files.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(TaskParamType::GeneratedShortcuts(installed_files))
|
||||
}
|
||||
|
||||
|
|
|
|||
76
src/tasks/launch_installed_on_exit.rs
Normal file
76
src/tasks/launch_installed_on_exit.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
//! Configures lift to launch the new package on fresh install after its closed
|
||||
//! If theres multiple launchable packages, then choose the first listed in config
|
||||
//! If there are multiple shortcuts for the first package, then launch the first.
|
||||
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use crate::config::PackageDescription;
|
||||
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub struct LaunchOnExitTask {}
|
||||
|
||||
impl Task for LaunchOnExitTask {
|
||||
fn execute(
|
||||
&mut self,
|
||||
_: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
_: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
let pkg = &context.database.packages.first();
|
||||
if pkg.is_none() {
|
||||
return Ok(TaskParamType::None);
|
||||
}
|
||||
let pkg = pkg.unwrap();
|
||||
|
||||
// look up the first shortcut for the first listed package in the database
|
||||
let path = context
|
||||
.install_path
|
||||
.as_ref()
|
||||
.log_expect("No install path specified");
|
||||
|
||||
let mut metadata: Option<PackageDescription> = None;
|
||||
for description in &context
|
||||
.config
|
||||
.as_ref()
|
||||
.log_expect("Should have packages by now")
|
||||
.packages
|
||||
{
|
||||
if pkg.name == description.name {
|
||||
metadata = Some(description.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let package_desc = match metadata {
|
||||
Some(v) => v,
|
||||
// Package metadata is missing. Dunno what went wrong but we can skip this then
|
||||
None => return Ok(TaskParamType::None),
|
||||
};
|
||||
|
||||
let shortcut = package_desc.shortcuts.first();
|
||||
|
||||
// copy the path to the actual exe into launcher_path so it'll load it on exit
|
||||
context.launcher_path = shortcut.map(|s| {
|
||||
path.join(s.relative_path.clone())
|
||||
.to_str()
|
||||
.map(|t| t.to_string())
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
Ok(TaskParamType::None)
|
||||
}
|
||||
|
||||
fn dependencies(&self) -> Vec<TaskDependency> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
"LaunchOnExitTask".to_string()
|
||||
}
|
||||
}
|
||||
|
|
@ -4,18 +4,22 @@
|
|||
use std::fmt;
|
||||
use std::fmt::Display;
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use sources::types::File;
|
||||
use sources::types::Version;
|
||||
use crate::sources::types::File;
|
||||
use crate::sources::types::Version;
|
||||
|
||||
pub mod check_authorization;
|
||||
pub mod download_pkg;
|
||||
pub mod ensure_only_instance;
|
||||
pub mod install;
|
||||
pub mod install_desktop_shortcut;
|
||||
pub mod install_dir;
|
||||
pub mod install_global_shortcut;
|
||||
pub mod install_pkg;
|
||||
pub mod install_shortcuts;
|
||||
pub mod launch_installed_on_exit;
|
||||
pub mod remove_target_dir;
|
||||
pub mod resolver;
|
||||
pub mod save_database;
|
||||
pub mod save_executable;
|
||||
|
|
@ -29,6 +33,8 @@ pub enum TaskParamType {
|
|||
None,
|
||||
/// Metadata about a file
|
||||
File(Version, File),
|
||||
/// Authentication token for a package
|
||||
Authentication(Version, File, Option<String>),
|
||||
/// Downloaded contents of a file
|
||||
FileContents(Version, File, Vec<u8>),
|
||||
/// List of shortcuts that have been generated
|
||||
|
|
@ -62,6 +68,7 @@ impl TaskDependency {
|
|||
/// A message from a task.
|
||||
pub enum TaskMessage<'a> {
|
||||
DisplayMessage(&'a str, f64),
|
||||
AuthorizationRequired(&'a str),
|
||||
PackageInstalled,
|
||||
}
|
||||
|
||||
|
|
|
|||
64
src/tasks/remove_target_dir.rs
Normal file
64
src/tasks/remove_target_dir.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
//! remove the whole target directory from the existence
|
||||
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
pub struct RemoveTargetDirTask {}
|
||||
|
||||
impl Task for RemoveTargetDirTask {
|
||||
fn execute(
|
||||
&mut self,
|
||||
_: Vec<TaskParamType>,
|
||||
context: &mut InstallerFramework,
|
||||
messenger: &dyn Fn(&TaskMessage),
|
||||
) -> Result<TaskParamType, String> {
|
||||
messenger(&TaskMessage::DisplayMessage(
|
||||
"Removing previous install...",
|
||||
0.1,
|
||||
));
|
||||
// erase the database as well
|
||||
context.database.packages = Vec::new();
|
||||
if let Some(path) = context.install_path.as_ref() {
|
||||
let entries = std::fs::read_dir(path)
|
||||
.map_err(|e| format!("Error reading {}: {}", path.to_string_lossy(), e))?;
|
||||
// remove everything under the path
|
||||
if !context.preexisting_install {
|
||||
std::fs::remove_dir_all(path)
|
||||
.map_err(|e| format!("Error removing {}: {}", path.to_string_lossy(), e))?;
|
||||
return Ok(TaskParamType::None);
|
||||
}
|
||||
// remove everything except the maintenancetool if repairing
|
||||
for entry in entries {
|
||||
let path = entry
|
||||
.map_err(|e| format!("Error reading file: {}", e))?
|
||||
.path();
|
||||
if let Some(filename) = path.file_name() {
|
||||
if filename.to_string_lossy().starts_with("maintenancetool") {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if path.is_dir() {
|
||||
std::fs::remove_dir_all(&path)
|
||||
.map_err(|e| format!("Error removing {}: {}", path.to_string_lossy(), e))?;
|
||||
} else {
|
||||
std::fs::remove_file(&path)
|
||||
.map_err(|e| format!("Error removing {}: {}", path.to_string_lossy(), e))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(TaskParamType::None)
|
||||
}
|
||||
|
||||
fn dependencies(&self) -> Vec<TaskDependency> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
"RemoveTargetDirTask".to_string()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,18 +2,18 @@
|
|||
|
||||
use std::env::consts::OS;
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use config::PackageDescription;
|
||||
use crate::config::PackageDescription;
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub struct ResolvePackageTask {
|
||||
pub name: String,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
//! Saves the main database into the installation directory.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
pub struct SaveDatabaseTask {}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
//! Saves the installer executable into the install directory.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use std::fs::File;
|
||||
use std::fs::OpenOptions;
|
||||
|
|
@ -14,7 +14,7 @@ use std::io::copy;
|
|||
|
||||
use std::env::current_exe;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub struct SaveExecutableTask {}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
//! Uninstalls a set of packages.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use tasks::uninstall_pkg::UninstallPackageTask;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskOrdering;
|
||||
use crate::tasks::uninstall_pkg::UninstallPackageTask;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskOrdering;
|
||||
|
||||
pub struct UninstallTask {
|
||||
pub items: Vec<String>,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
//! Uninstalls a specific package.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use crate::tasks::save_database::SaveDatabaseTask;
|
||||
use crate::tasks::TaskOrdering;
|
||||
use std::fs::remove_file;
|
||||
use tasks::save_database::SaveDatabaseTask;
|
||||
use tasks::TaskOrdering;
|
||||
|
||||
pub struct UninstallGlobalShortcutsTask {}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
//! Uninstalls a specific package.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::save_database::SaveDatabaseTask;
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskOrdering;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::save_database::SaveDatabaseTask;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskOrdering;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use installer::LocalInstallation;
|
||||
use crate::installer::LocalInstallation;
|
||||
|
||||
use std::fs::remove_dir;
|
||||
use std::fs::remove_file;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use tasks::uninstall_shortcuts::UninstallShortcutsTask;
|
||||
use crate::logging::LoggingErrors;
|
||||
use crate::tasks::uninstall_shortcuts::UninstallShortcutsTask;
|
||||
|
||||
pub struct UninstallPackageTask {
|
||||
pub name: String,
|
||||
|
|
@ -44,7 +44,7 @@ impl Task for UninstallPackageTask {
|
|||
}
|
||||
}
|
||||
|
||||
let mut package = match metadata {
|
||||
let package = match metadata {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
if self.optional {
|
||||
|
|
@ -63,8 +63,7 @@ impl Task for UninstallPackageTask {
|
|||
0.0,
|
||||
));
|
||||
|
||||
// Reverse, as to delete directories last
|
||||
package.files.reverse();
|
||||
let mut directories = Vec::new();
|
||||
|
||||
let max = package.files.len();
|
||||
for (i, file) in package.files.iter().enumerate() {
|
||||
|
|
@ -78,7 +77,9 @@ impl Task for UninstallPackageTask {
|
|||
));
|
||||
|
||||
let result = if file.is_dir() {
|
||||
remove_dir(file)
|
||||
// we don't delete directory just yet
|
||||
directories.push(file);
|
||||
Ok(())
|
||||
} else {
|
||||
remove_file(file)
|
||||
};
|
||||
|
|
@ -88,6 +89,17 @@ impl Task for UninstallPackageTask {
|
|||
}
|
||||
}
|
||||
|
||||
// sort directories by reverse depth order
|
||||
directories.sort_by(|a, b| {
|
||||
let depth_a = a.components().fold(0usize, |acc, _| acc + 1);
|
||||
let depth_b = b.components().fold(0usize, |acc, _| acc + 1);
|
||||
depth_b.cmp(&depth_a)
|
||||
});
|
||||
for i in directories.iter() {
|
||||
info!("Deleting directory: {:?}", i);
|
||||
remove_dir(i).ok();
|
||||
}
|
||||
|
||||
Ok(TaskParamType::None)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
//! Uninstalls a specific package.
|
||||
|
||||
use installer::InstallerFramework;
|
||||
use crate::installer::InstallerFramework;
|
||||
|
||||
use tasks::Task;
|
||||
use tasks::TaskDependency;
|
||||
use tasks::TaskMessage;
|
||||
use tasks::TaskParamType;
|
||||
use crate::tasks::Task;
|
||||
use crate::tasks::TaskDependency;
|
||||
use crate::tasks::TaskMessage;
|
||||
use crate::tasks::TaskParamType;
|
||||
|
||||
use installer::LocalInstallation;
|
||||
use crate::installer::LocalInstallation;
|
||||
|
||||
use std::fs::remove_dir;
|
||||
use std::fs::remove_file;
|
||||
|
||||
use logging::LoggingErrors;
|
||||
use crate::logging::LoggingErrors;
|
||||
|
||||
pub struct UninstallShortcutsTask {
|
||||
pub name: String,
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
> 1%
|
||||
last 2 versions
|
||||
not ie <= 11
|
||||
|
|
|
|||
|
|
@ -3,15 +3,14 @@ module.exports = {
|
|||
env: {
|
||||
node: true
|
||||
},
|
||||
'extends': [
|
||||
'plugin:vue/essential',
|
||||
extends: [
|
||||
'plugin:vue/recommended',
|
||||
'@vue/standard'
|
||||
],
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
|
||||
},
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
'no-console': 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-redeclare': 'off',
|
||||
camelcase: 'off'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
20
ui/merge-strings.js
Executable file
20
ui/merge-strings.js
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
#!/bin/env node
|
||||
const fs = require('fs')
|
||||
const merge = require('deepmerge')
|
||||
const glob = require('glob')
|
||||
|
||||
glob('src/locales/!(messages).json', {}, (e, files) => {
|
||||
let messages = []
|
||||
for (const file of files) {
|
||||
console.log(`Loading ${file}...`)
|
||||
const locale_messages = require(`./${file}`)
|
||||
messages.push(locale_messages)
|
||||
}
|
||||
console.log('Merging messages...')
|
||||
if (messages && messages.length > 1) {
|
||||
messages = merge.all(messages)
|
||||
} else {
|
||||
messages = messages[0] // single locale mode
|
||||
}
|
||||
fs.writeFileSync('src/locales/messages.json', JSON.stringify(messages), {})
|
||||
})
|
||||
|
|
@ -4,20 +4,38 @@ const express = require('express')
|
|||
const app = express()
|
||||
const port = 3000
|
||||
|
||||
let showError = false
|
||||
let showConfigError = false
|
||||
let maintenance = false
|
||||
let launcher = false
|
||||
let fileExists = false
|
||||
let darkMode = false
|
||||
let recoveryMode = false
|
||||
|
||||
function progressSimulation (res) {
|
||||
var progress = 0.0
|
||||
var timer = setInterval(() => {
|
||||
var resp = JSON.stringify({ Status: ['Processing...', progress] }) + '\n'
|
||||
if (showError) {
|
||||
const resp = JSON.stringify({ Error: 'Simulated error.' }) + '\n'
|
||||
res.write(resp)
|
||||
res.status(200).end()
|
||||
return
|
||||
}
|
||||
let progress = 0.0
|
||||
const timer = setInterval(() => {
|
||||
const resp = JSON.stringify({ Status: ['Processing...', progress] }) + '\n'
|
||||
progress += 0.1
|
||||
res.write(resp)
|
||||
if (progress >= 1) {
|
||||
res.status(200).end()
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, 1500)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
function returnConfig (res) {
|
||||
if (showConfigError) {
|
||||
res.status(500).json({})
|
||||
return
|
||||
}
|
||||
res.json({
|
||||
installing_message:
|
||||
'Test Banner <strong>Bold</strong> <pre>Code block</pre> <i>Italic</i> <del>Strike</del>',
|
||||
|
|
@ -52,21 +70,22 @@ function returnConfig (res) {
|
|||
}
|
||||
|
||||
app.get('/api/attrs', (req, res) => {
|
||||
console.log('-- Get attrs')
|
||||
res.send(
|
||||
`var base_attributes = {"name":"yuzu","target_url":"https://raw.githubusercontent.com/j-selby/test-installer/master/config.linux.v2.toml"};`
|
||||
{ name: 'yuzu', recovery: recoveryMode, target_url: 'https://raw.githubusercontent.com/j-selby/test-installer/master/config.linux.v2.toml' }
|
||||
)
|
||||
})
|
||||
|
||||
app.get('/api/dark-mode', (req, res) => {
|
||||
res.json(false)
|
||||
res.json(darkMode)
|
||||
})
|
||||
|
||||
app.get('/api/installation-status', (req, res) => {
|
||||
res.json({
|
||||
database: { packages: [], shortcuts: [] },
|
||||
install_path: null,
|
||||
preexisting_install: false,
|
||||
is_launcher: false,
|
||||
preexisting_install: maintenance,
|
||||
is_launcher: launcher,
|
||||
launcher_path: null
|
||||
})
|
||||
})
|
||||
|
|
@ -82,13 +101,72 @@ app.get('/api/config', (req, res) => {
|
|||
})
|
||||
|
||||
app.post('/api/start-install', (req, res) => {
|
||||
console.log(`-- Install: ${req.body}`)
|
||||
console.log('-- Install:')
|
||||
console.log(req.body)
|
||||
progressSimulation(res)
|
||||
})
|
||||
|
||||
app.get('/api/exit', (req, res) => {
|
||||
console.log('-- Exit')
|
||||
res.status(204)
|
||||
if (showError) {
|
||||
res.status(500).send('Simulated error: Nothing to see here.')
|
||||
return
|
||||
}
|
||||
res.status(204).send()
|
||||
})
|
||||
|
||||
app.post('/api/verify-path', (req, res) => {
|
||||
console.log('-- Verify Path')
|
||||
res.send({
|
||||
exists: fileExists
|
||||
})
|
||||
})
|
||||
|
||||
app.post('/api/check-auth', (req, res) => {
|
||||
console.log('-- Check Authorization')
|
||||
res.send({
|
||||
username: 'test1',
|
||||
token: 'token',
|
||||
jwt_token: {
|
||||
isPatreonAccountLinked: true,
|
||||
isPatreonSubscriptionActive: true,
|
||||
releaseChannels: ['early-access']
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
process.argv.forEach((val, index) => {
|
||||
switch (val) {
|
||||
case 'maintenance':
|
||||
maintenance = true
|
||||
console.log('Simulating maintenance mode')
|
||||
break
|
||||
case 'launcher':
|
||||
maintenance = true
|
||||
launcher = true
|
||||
console.log('Simulating launcher mode')
|
||||
break
|
||||
case 'exists':
|
||||
fileExists = true
|
||||
console.log('Simulating file exists situation')
|
||||
break
|
||||
case 'dark':
|
||||
darkMode = true
|
||||
console.log('Simulating dark mode')
|
||||
break
|
||||
case 'config-error':
|
||||
showConfigError = true
|
||||
console.log('Simulating configuration errors')
|
||||
break
|
||||
case 'error':
|
||||
showError = true
|
||||
console.log('Simulating errors')
|
||||
break
|
||||
case 'recovery':
|
||||
recoveryMode = true
|
||||
console.log('Simulating recovery mode')
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`Listening on ${port}...`)
|
||||
|
|
|
|||
|
|
@ -5,23 +5,32 @@
|
|||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
"lint": "vue-cli-service lint",
|
||||
"postinstall": "node merge-strings.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"buefy": "^0.7.7",
|
||||
"vue": "^2.6.6",
|
||||
"vue-router": "^3.0.1"
|
||||
"@mdi/font": "^7.1.96",
|
||||
"axios": "^1.2.1",
|
||||
"buefy": "^0.9.22",
|
||||
"canvas-confetti": "^1.6.0",
|
||||
"vue": "^2.6.14",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-i18n": "^8.26.5",
|
||||
"vue-router": "^3.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.5.0",
|
||||
"@vue/cli-plugin-eslint": "^3.5.0",
|
||||
"@vue/cli-service": "^3.5.0",
|
||||
"@vue/eslint-config-standard": "^4.0.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"eslint": "^5.8.0",
|
||||
"eslint-plugin-vue": "^5.0.0",
|
||||
"express": "^4.17.1",
|
||||
"http-proxy-middleware": "^0.19.1",
|
||||
"vue-template-compiler": "^2.5.21"
|
||||
"@vue/cli-plugin-babel": "^5.0.8",
|
||||
"@vue/cli-plugin-eslint": "^5.0.8",
|
||||
"@vue/cli-service": "^5.0.8",
|
||||
"@vue/eslint-config-standard": "^6.1.0",
|
||||
"eslint": "^8.30.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-standard": "^4.1.0",
|
||||
"eslint-plugin-vue": "^9.8.0",
|
||||
"express": "^4.18.2",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"vue-template-compiler": "^2.7.14"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=11">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<script src="/api/attrs" type="text/javascript"></script>
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title id="window-title">... Installer</title>
|
||||
</head>
|
||||
|
|
|
|||
BIN
ui/public/thicc_logo_installer__ea_shadow.png
Normal file
BIN
ui/public/thicc_logo_installer__ea_shadow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
ui/public/thicc_logo_installer_shadow.png
Normal file
BIN
ui/public/thicc_logo_installer_shadow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
119
ui/src/App.vue
119
ui/src/App.vue
|
|
@ -4,20 +4,28 @@
|
|||
<div class="container is-max-height">
|
||||
<div class="columns is-max-height">
|
||||
<div class="column is-one-third has-padding" v-if="!$root.$data.metadata.is_launcher">
|
||||
<img src="./assets/logo.png" width="60%" alt="Application icon" />
|
||||
<img src="./assets/light_mode_installer_logo.png" id="applicationIcon" alt="Application icon" />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<h2 class="subtitle" v-if="!$root.$data.metadata.preexisting_install">
|
||||
Welcome to the {{ $root.$data.attrs.name }} installer!
|
||||
{{ $t('app.installer_title', {'name': $root.$data.attrs.name}) }}
|
||||
</h2>
|
||||
<h2 class="subtitle" v-if="!$root.$data.metadata.preexisting_install">
|
||||
We will have you up and running in just a few moments.
|
||||
{{ $t('app.installer_subtitle') }}
|
||||
</h2>
|
||||
|
||||
<h2 class="subtitle" v-if="$root.$data.metadata.preexisting_install">
|
||||
Welcome to the {{ $root.$data.attrs.name }} Maintenance Tool.
|
||||
{{ $t('app.maintenance_title', {'name': $root.$data.attrs.name}) }}
|
||||
</h2>
|
||||
<b-dropdown hoverable @change="selectLocale" aria-role="list" scrollable>
|
||||
<button class="button" slot="trigger">
|
||||
<span>{{ $t('locale') }}</span>
|
||||
<b-icon icon="menu-down"></b-icon>
|
||||
</button>
|
||||
|
||||
<b-dropdown-item v-for="(locale, index) in this.$i18n.messages" v-bind:key="index" :value="index" aria-role="listitem">{{locale.locale}}</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
|
||||
<router-view />
|
||||
|
|
@ -27,6 +35,33 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
mounted: function () {
|
||||
// detect languages
|
||||
const languages = window.navigator.languages
|
||||
if (languages) {
|
||||
// standard-compliant browsers
|
||||
for (let index = 0; index < languages.length; index++) {
|
||||
const lang = languages[index]
|
||||
// Find the most preferred language that we support
|
||||
if (Object.prototype.hasOwnProperty.call(this.$i18n.messages, lang)) {
|
||||
this.$i18n.locale = lang
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// IE9+ support
|
||||
this.$i18n.locale = window.navigator.browserLanguage
|
||||
},
|
||||
methods: {
|
||||
selectLocale: function (locale) {
|
||||
this.$i18n.locale = locale
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* roboto-regular - latin */
|
||||
@font-face {
|
||||
|
|
@ -52,6 +87,30 @@ body, div, span, h1, h2, h3, h4, h5, h6 {
|
|||
cursor: default;
|
||||
}
|
||||
|
||||
#applicationIcon {
|
||||
width:0px; height: 0px;
|
||||
padding: 50px 60% 0px 0px;
|
||||
background: url("./assets/light_mode_installer_logo.png") left top no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
body.has-background-black-ter #applicationIcon {
|
||||
background: url("./assets/dark_mode_installer_logo.png") left top no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.package-icon {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
float: left;
|
||||
padding-right: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.package-description {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
pre {
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
|
|
@ -71,6 +130,7 @@ pre {
|
|||
|
||||
.clickable-box {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.clickable-box label {
|
||||
|
|
@ -122,8 +182,59 @@ pre {
|
|||
background: #fff;
|
||||
}
|
||||
|
||||
.tile.box.clickable-box {
|
||||
color: #4a4a4a;
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
body.has-background-black-ter .subtitle, body.has-background-black-ter .column > div {
|
||||
color: hsl(0, 0%, 96%);
|
||||
}
|
||||
|
||||
.ribbon {
|
||||
position: absolute;
|
||||
right: -5px; top: -5px;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
width: 75px; height: 75px;
|
||||
text-align: right;
|
||||
}
|
||||
.ribbon span {
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
color: #FFF;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
transform: rotate(45deg);
|
||||
-webkit-transform: rotate(45deg);
|
||||
width: 100px;
|
||||
display: block;
|
||||
background: #79A70A;
|
||||
background: linear-gradient(#FF3C28 0%, #FF3C28 100%);
|
||||
box-shadow: 0 3px 10px -5px rgba(0, 0, 0, 1);
|
||||
position: absolute;
|
||||
top: 19px; right: -21px;
|
||||
}
|
||||
.ribbon span::before {
|
||||
content: "";
|
||||
position: absolute; left: 0px; top: 100%;
|
||||
z-index: -1;
|
||||
border-left: 3px solid #FF3C28;
|
||||
border-right: 3px solid transparent;
|
||||
border-bottom: 3px solid transparent;
|
||||
border-top: 3px solid #FF3C28;
|
||||
}
|
||||
.ribbon span::after {
|
||||
content: "";
|
||||
position: absolute; right: 0px; top: 100%;
|
||||
z-index: -1;
|
||||
border-left: 3px solid transparent;
|
||||
border-right: 3px solid #FF3C28;
|
||||
border-bottom: 3px solid transparent;
|
||||
border-top: 3px solid #FF3C28;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
BIN
ui/src/assets/dark_mode_installer_logo.png
Normal file
BIN
ui/src/assets/dark_mode_installer_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 29 KiB |
BIN
ui/src/assets/light_mode_installer_logo.png
Normal file
BIN
ui/src/assets/light_mode_installer_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.3 KiB |
|
|
@ -4,60 +4,6 @@
|
|||
* Additional state-less helper methods.
|
||||
*/
|
||||
|
||||
var request_id = 0
|
||||
|
||||
/**
|
||||
* Makes a AJAX request.
|
||||
*
|
||||
* @param path The path to connect to.
|
||||
* @param successCallback A callback with a JSON payload.
|
||||
* @param failCallback A fail callback. Optional.
|
||||
* @param data POST data. Optional.
|
||||
*/
|
||||
export function ajax (path, successCallback, failCallback, data) {
|
||||
if (failCallback === undefined) {
|
||||
failCallback = defaultFailHandler
|
||||
}
|
||||
|
||||
console.log('Making HTTP request to ' + path)
|
||||
|
||||
var req = new XMLHttpRequest()
|
||||
|
||||
req.addEventListener('load', function () {
|
||||
// The server can sometimes return a string error. Make sure we handle this.
|
||||
if (this.status === 200 && this.getResponseHeader('Content-Type').indexOf('application/json') !== -1) {
|
||||
successCallback(JSON.parse(this.responseText))
|
||||
} else {
|
||||
failCallback(this.responseText)
|
||||
}
|
||||
})
|
||||
req.addEventListener('error', failCallback)
|
||||
|
||||
req.open(data == null ? 'GET' : 'POST', path + '?nocache=' + request_id++, true)
|
||||
// Rocket only currently supports URL encoded forms.
|
||||
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
|
||||
|
||||
if (data != null) {
|
||||
var form = ''
|
||||
|
||||
for (var key in data) {
|
||||
if (!data.hasOwnProperty(key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (form !== '') {
|
||||
form += '&'
|
||||
}
|
||||
|
||||
form += encodeURIComponent(key) + '=' + encodeURIComponent(data[key])
|
||||
}
|
||||
|
||||
req.send(form)
|
||||
} else {
|
||||
req.send()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a AJAX request, streaming each line as it arrives. Type should be text/plain,
|
||||
* each line will be interpreted as JSON separately.
|
||||
|
|
@ -69,7 +15,7 @@ export function ajax (path, successCallback, failCallback, data) {
|
|||
* @param data POST data. Optional.
|
||||
*/
|
||||
export function stream_ajax (path, callback, successCallback, failCallback, data) {
|
||||
var req = new XMLHttpRequest()
|
||||
const req = new XMLHttpRequest()
|
||||
|
||||
console.log('Making streaming HTTP request to ' + path)
|
||||
|
||||
|
|
@ -82,23 +28,23 @@ export function stream_ajax (path, callback, successCallback, failCallback, data
|
|||
}
|
||||
})
|
||||
|
||||
var buffer = ''
|
||||
var seenBytes = 0
|
||||
let buffer = ''
|
||||
let seenBytes = 0
|
||||
|
||||
req.onreadystatechange = function () {
|
||||
if (req.readyState > 2) {
|
||||
buffer += req.responseText.substr(seenBytes)
|
||||
|
||||
var pointer
|
||||
let pointer
|
||||
while ((pointer = buffer.indexOf('\n')) >= 0) {
|
||||
var line = buffer.substring(0, pointer).trim()
|
||||
const line = buffer.substring(0, pointer).trim()
|
||||
buffer = buffer.substring(pointer + 1)
|
||||
|
||||
if (line.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
var contents = JSON.parse(line)
|
||||
const contents = JSON.parse(line)
|
||||
callback(contents)
|
||||
}
|
||||
|
||||
|
|
@ -108,15 +54,15 @@ export function stream_ajax (path, callback, successCallback, failCallback, data
|
|||
|
||||
req.addEventListener('error', failCallback)
|
||||
|
||||
req.open(data == null ? 'GET' : 'POST', path + '?nocache=' + request_id++, true)
|
||||
req.open(data == null ? 'GET' : 'POST', path + '?nocache=' + Date.now(), true)
|
||||
// Rocket only currently supports URL encoded forms.
|
||||
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
|
||||
|
||||
if (data != null) {
|
||||
var form = ''
|
||||
let form = ''
|
||||
|
||||
for (var key in data) {
|
||||
if (!data.hasOwnProperty(key)) {
|
||||
for (const key in data) {
|
||||
if (!data[key]) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -132,13 +78,3 @@ export function stream_ajax (path, callback, successCallback, failCallback, data
|
|||
req.send()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The default handler if a AJAX request fails. Not to be used directly.
|
||||
*
|
||||
* @param e The XMLHttpRequest that failed.
|
||||
*/
|
||||
function defaultFailHandler (e) {
|
||||
console.error('A AJAX request failed, and was not caught:')
|
||||
console.error(e)
|
||||
}
|
||||
|
|
|
|||
1
ui/src/locales/.gitignore
vendored
Normal file
1
ui/src/locales/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
messages.json
|
||||
78
ui/src/locales/ca.json
Normal file
78
ui/src/locales/ca.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"ca": {
|
||||
"locale": "Català",
|
||||
"error": {
|
||||
"title": "S'ha produït un error",
|
||||
"exit_error": "{msg}\n\nSi us plau, carregui l'arxiu de registre (a {path}) a l'equip de {name}",
|
||||
"location_unknown": "la ubicació on està aquest instal·lador"
|
||||
},
|
||||
"complete": {
|
||||
"thanks": "Gràcies per instal·lar {name}!",
|
||||
"updated": "{name} s'ha actualitzat.",
|
||||
"uninstalled": "{name} s'ha desinstal·lat.",
|
||||
"up_to_date": "{name} ja està actualitzat!",
|
||||
"where_to_find": "Pot trobar les aplicacions instal·lades al seu menú d'inici. ",
|
||||
"migration_where_to_find": "Pot trobar les seves aplicacions instal·lades al seu menú d'inici; si es troba al mig d'alguna cosa, torni-ho a provar.",
|
||||
"migration_finished": "L'hem traslladat a la nova i única versió de {name}."
|
||||
},
|
||||
"modify": {
|
||||
"title": "Esculli una opció:",
|
||||
"update": "Actualitzar",
|
||||
"uninstall": "Desinstal·lar",
|
||||
"prompt": "Està segur de que vol desinstal·lar {name}?",
|
||||
"prompt_repair": "<b>WARNING:</b> All the files under <code>{path}</code> will be <b>deleted</b> as part of the repair process!<br>Do you wish to proceed?",
|
||||
"view_local_files": "Veure arxius locals",
|
||||
"prompt_recover": "Les dades de l'instal·lador per a {name} estan malmeses. <br>Cal una reparació per restaurar la instal·lació.",
|
||||
"prompt_confirm": "Desinstal·lar {name}",
|
||||
"modify": "Modificar",
|
||||
"repair": "Reparar"
|
||||
},
|
||||
"auth": {
|
||||
"paste": "Enganxar",
|
||||
"token": "Token",
|
||||
"verify": "Verificar token",
|
||||
"page_header": "El canal de llançament d'accés anticipat li permet provar les últimes funcions experimentals i correccions abans que es fusionin al yuzu. Aquest canal inclou totes les actualitzacions diàries de yuzu, a més d'aquestes funcions exclusives.\n\nPer ser membre d'accés anticipat, ha de ser subscriptor d'accés anticipat a Patreon.",
|
||||
"page_opened": "Pàgina oberta! Comprovi el seu navegador predeterminat per a la pàgina i segueixi les instruccions que hi ha per enllaçar el seu compte de Patreon.\nQuan hagi acabat, introdueixi el token a continuació.",
|
||||
"login_failed": "Error a l'iniciar sessió!\n\nComprovi que el teu token sigui correcte i torni-ho a provar"
|
||||
},
|
||||
"back": "Enrere",
|
||||
"exit": "Sortir",
|
||||
"yes": "Sí",
|
||||
"no": "No",
|
||||
"continue": "Continuar",
|
||||
"cancel": "Cancel·lar",
|
||||
"app": {
|
||||
"installer_title": "Benvingut a l'instal·lador de {name}!",
|
||||
"maintenance_title": "Benvingut a l'eina de manteniment de {name}.",
|
||||
"window_title": "Instal·lador de {name}",
|
||||
"installer_subtitle": "Estarem llestos en uns instants."
|
||||
},
|
||||
"select_packages": {
|
||||
"title": "Seleccioni quins paquets vol instal·lar:",
|
||||
"installed": "(Instal·lat)",
|
||||
"advanced": "Avançat...",
|
||||
"install": "Instal·lar",
|
||||
"location": "Ubicació d'instal·lació",
|
||||
"select": "Seleccionar",
|
||||
"overwriting": "Sobreescrivint",
|
||||
"options": "Opcions d'instal·lació",
|
||||
"title_repair": "Seleccioni quins paquets vol reparar:",
|
||||
"location_placeholder": "Introdueixi una ruta d'instal·lació aquí",
|
||||
"overwriting_warning": "Directory <code>{path}</code> already exists.<br>Are you sure you want to <b>overwrite</b> the contents inside?",
|
||||
"nothing_picked_warning": "Si us plau, seleccioni almenys un paquet a instal·lar!",
|
||||
"nothing_picked": "Res seleccionat",
|
||||
"option_shortcut": "Crear un accés directe al escriptori"
|
||||
},
|
||||
"install_packages": {
|
||||
"uninstall": "Desinstal·lant...",
|
||||
"install": "Instal·lant...",
|
||||
"check_for_update": "Comprovant actualitzacions...",
|
||||
"self_update": "Descarregant una auto-actualització...",
|
||||
"please_wait": "Si us plau, esperi..."
|
||||
},
|
||||
"download_packages": {
|
||||
"download_config": "Descarregant configuració...",
|
||||
"error_download_config": "S'ha produït un error mentre descarregàvem la configuració: {msg}"
|
||||
}
|
||||
}
|
||||
}
|
||||
78
ui/src/locales/cs.json
Normal file
78
ui/src/locales/cs.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"cs": {
|
||||
"locale": "Česky",
|
||||
"error": {
|
||||
"title": "Objevil se error",
|
||||
"exit_error": "{msg}\n\nProsím nahrajte záznamoví soubor (do {path}) pro {name} tým",
|
||||
"location_unknown": "místo kde se instalátor nachází"
|
||||
},
|
||||
"complete": {
|
||||
"thanks": "Děkujeme za instalování {name}!",
|
||||
"updated": "{name} byl aktualizován.",
|
||||
"uninstalled": "{name} byl odinstalován.",
|
||||
"up_to_date": "{name} už je aktuální!",
|
||||
"where_to_find": "Nainstalované aplikace můžete najít ve vašem start menu.",
|
||||
"migration_where_to_find": "Nainstalované aplikace můžete najít ve vašem start menu - jestli jste byly uprostřed něčeho, prosím zkuste to znovu.",
|
||||
"migration_finished": "Byly jste přesunuti do nové, jediné verze {name}."
|
||||
},
|
||||
"modify": {
|
||||
"title": "Vyberte možnost:",
|
||||
"update": "Aktualizovat",
|
||||
"uninstall": "Odinstalovat",
|
||||
"prompt": "Jste si jistý že chcete odinstalovat {name}?",
|
||||
"prompt_repair": "<b>WARNING:</b> All the files under <code>{path}</code> will be <b>deleted</b> as part of the repair process!<br>Do you wish to proceed?",
|
||||
"view_local_files": "Zobrazit místní soubory",
|
||||
"prompt_recover": "Data instalátoru pro {name} jsou poškozený.<br>Oprava je potřeba pro obnovní instalace.",
|
||||
"prompt_confirm": "Odinstalovat {name}",
|
||||
"modify": "Modifikovat",
|
||||
"repair": "Opravit"
|
||||
},
|
||||
"auth": {
|
||||
"paste": "Vložit",
|
||||
"token": "Token",
|
||||
"verify": "Ověřit Token",
|
||||
"page_header": "Early Access kanál pro zveřejňovaní vám umožní si vyzkoušet nejnovější experimentální funkce and opravy, před tím než spojeny s yuzu. Tento kanál obsahuje všechny pravidelné denní yuzu aktualizace, k tomu ještě tyto exkluzivní funkce.\n\nAby jste se stal Early Access členem, musíte se jako první stát Patreon Early Access odběratelem.",
|
||||
"page_opened": "Stránka otevřena! Zkontrolujte váš výchozí prohlížeč pro stránku s instrukcemi a postupujte podle pokynů pro propojení patreon účtu.\nAž budete hotoví, vložte token níže.",
|
||||
"login_failed": "Přihlášení selhalo!\n\nUjistěte se, že jste zadali token správně a zkuste to znovu."
|
||||
},
|
||||
"back": "Zpět",
|
||||
"exit": "Opustit",
|
||||
"yes": "Ano",
|
||||
"no": "Ne",
|
||||
"continue": "Pokračovat",
|
||||
"cancel": "Zrušit",
|
||||
"app": {
|
||||
"installer_title": "Vítejte v {name} instalátoru!",
|
||||
"maintenance_title": "Vítejte v {name} Nástroji pro Údržbu.",
|
||||
"window_title": "{name} Instalátor",
|
||||
"installer_subtitle": "Za chvilku vás uvedeme zpátky do provozu."
|
||||
},
|
||||
"select_packages": {
|
||||
"title": "Vyberte balíček, který si chcete nainstalovat:",
|
||||
"installed": "(nainstalováno)",
|
||||
"advanced": "Pokročilé...",
|
||||
"install": "Nainstalovat",
|
||||
"location": "Místo Instalace",
|
||||
"select": "Vyberte",
|
||||
"overwriting": "Přepisování",
|
||||
"options": "Možnosti Instalace",
|
||||
"title_repair": "Vyberte balíček, který chcete opravit:",
|
||||
"location_placeholder": "Sem zadejte místo instalace",
|
||||
"overwriting_warning": "Directory <code>{path}</code> already exists.<br>Are you sure you want to <b>overwrite</b> the contents inside?",
|
||||
"nothing_picked_warning": "Prosím vyberte alespoň jeden balíček na instalování!",
|
||||
"nothing_picked": "Nic nevybráno",
|
||||
"option_shortcut": "Vytvořit Zástupce na Plochu"
|
||||
},
|
||||
"install_packages": {
|
||||
"uninstall": "Odinstalovávám...",
|
||||
"install": "Instaluji...",
|
||||
"check_for_update": "Hledám nové aktualizace...",
|
||||
"self_update": "Stahuji samo-aktualizaci...",
|
||||
"please_wait": "Prosím čekejte..."
|
||||
},
|
||||
"download_packages": {
|
||||
"download_config": "Stahuji konfigurace...",
|
||||
"error_download_config": "Error se objevil při stahování konfigurace: {msg}"
|
||||
}
|
||||
}
|
||||
}
|
||||
78
ui/src/locales/de.json
Normal file
78
ui/src/locales/de.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"de": {
|
||||
"locale": "Deutsch",
|
||||
"error": {
|
||||
"title": "Ein Fehler ist aufgetreten",
|
||||
"exit_error": "{msg}\n\nBitte laden sie die Log-Datei (in {path}) für das {name} Team hoch.",
|
||||
"location_unknown": "der Ort an dem sich der Installer befindet"
|
||||
},
|
||||
"complete": {
|
||||
"thanks": "Danke fürs installieren {name}!",
|
||||
"updated": "{name} wurde aktualisiert.",
|
||||
"uninstalled": "{name} wurde deinstalliert.",
|
||||
"up_to_date": "{name} ist bereits auf dem neusten Stand",
|
||||
"where_to_find": "Sie finden Ihre installierten Anwendungen im Startmenü.",
|
||||
"migration_where_to_find": "Sie finden Ihre installierten Anwendungen in Ihrem Startmenü - wenn Sie gerade mit etwas beschäftigt waren, versuchen Sie es einfach noch einmal.",
|
||||
"migration_finished": "Sie wurden in die neue, einfache Version von {Name} migriert."
|
||||
},
|
||||
"modify": {
|
||||
"title": "Wähle eine Option:",
|
||||
"update": "Aktualisieren",
|
||||
"uninstall": "Deinstallieren",
|
||||
"prompt": "Bist du sicher dass du {name} deinstallieren möchtest?",
|
||||
"prompt_repair": "<b>WARNING:</b> All the files under <code>{path}</code> will be <b>deleted</b> as part of the repair process!<br>Do you wish to proceed?",
|
||||
"view_local_files": "Lokale Dateien anzeigen",
|
||||
"prompt_recover": "Die Installationsdaten für {name} sind beschädigt.<br>Eine Reperatur wird benötigt um die Installation wiederherzustellen.",
|
||||
"prompt_confirm": "{name} deinstallieren",
|
||||
"modify": "Modifizieren",
|
||||
"repair": "Reparieren"
|
||||
},
|
||||
"auth": {
|
||||
"paste": "Einfügen",
|
||||
"token": "Token",
|
||||
"verify": "Token verifizieren",
|
||||
"page_header": "Im Early-Access-Channel können Sie die neuesten experimentellen Funktionen und Korrekturen ausprobieren, bevor sie in yuzu integriert werden. Dieser Kanal enthält alle regulären täglichen Updates von yuzu sowie diese exklusiven Funktionen.\n\nUm ein Early Access-Mitglied zu sein, müssen Sie ein Patreon Early Access-Abonnent sein.",
|
||||
"page_opened": "Seite geöffnet! Folgen Sie den Anweisungen in ihrem Browser, um Ihr Patreon-Konto zu verknüpfen.\nDanach geben Sie den Token unten ein.",
|
||||
"login_failed": "Login fehlgeschlagen!\n\nÜberprüfen Sie, ob Ihr Token korrekt ist und versuchen Sie es erneut."
|
||||
},
|
||||
"back": "Zurück",
|
||||
"exit": "Verlassen",
|
||||
"yes": "Ja",
|
||||
"no": "Nein",
|
||||
"continue": "Fortsetzen",
|
||||
"cancel": "Abbrechen",
|
||||
"app": {
|
||||
"installer_title": "Willkommen zum {name}-Installer!",
|
||||
"maintenance_title": "Willkommen zum {name}-Wartungstool!",
|
||||
"window_title": "{name} Installer",
|
||||
"installer_subtitle": "In nur wenigen Augenblicken sind wir startklar."
|
||||
},
|
||||
"select_packages": {
|
||||
"title": "Wähle die Pakete aus die du installieren möchtest:",
|
||||
"installed": "(Installieren)",
|
||||
"advanced": "Erweitert...",
|
||||
"install": "Installieren",
|
||||
"location": "Installationsort",
|
||||
"select": "Auswählen",
|
||||
"overwriting": "Überschreiben",
|
||||
"options": "Installationsoptionen",
|
||||
"title_repair": "Wählen Sie die Pakete aus, die Sie reparieren möchten:",
|
||||
"location_placeholder": "Gib hier den Installationspfad an",
|
||||
"overwriting_warning": "Directory <code>{path}</code> already exists.<br>Are you sure you want to <b>overwrite</b> the contents inside?",
|
||||
"nothing_picked_warning": "Bitte wähle mindestens ein Paket zum installieren aus!",
|
||||
"nothing_picked": "Nichts ausgewählt",
|
||||
"option_shortcut": "Desktopverknüpfung erstellen"
|
||||
},
|
||||
"install_packages": {
|
||||
"uninstall": "Deinstallieren...",
|
||||
"install": "Installieren...",
|
||||
"check_for_update": "Suche nach Updates...",
|
||||
"self_update": "Selbst-Update wird heruntergeladen...",
|
||||
"please_wait": "Bitte warten..."
|
||||
},
|
||||
"download_packages": {
|
||||
"download_config": "Konfiguration wird heruntergeladen...",
|
||||
"error_download_config": "Fehler beim Herunterladen der Konfiguration: {msg}"
|
||||
}
|
||||
}
|
||||
}
|
||||
80
ui/src/locales/en.json
Normal file
80
ui/src/locales/en.json
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
{
|
||||
"en":{
|
||||
"locale":"English",
|
||||
"app":{
|
||||
"installer_title":"Welcome to the {name} installer!",
|
||||
"installer_subtitle":"We will have you up and running in just a few moments.",
|
||||
"maintenance_title":"Welcome to the {name} Maintenance Tool.",
|
||||
"window_title":"{name} Installer"
|
||||
},
|
||||
"download_config":{
|
||||
"download_config":"Downloading config...",
|
||||
"error_download_config":"Got error while downloading config: {msg}"
|
||||
},
|
||||
"select_packages":{
|
||||
"title":"Select which packages you want to install:",
|
||||
"title_repair":"Select which packages you want to repair:",
|
||||
"installed":"(installed)",
|
||||
"advanced":"Advanced...",
|
||||
"install":"Install",
|
||||
"modify":"Modify",
|
||||
"repair": "Repair",
|
||||
"location":"Install Location",
|
||||
"location_placeholder":"Enter a install path here",
|
||||
"select":"Select",
|
||||
"overwriting": "Overwriting",
|
||||
"overwriting_warning": "Directory <code>{path}</code> already exists.<br>Are you sure you want to <b>overwrite</b> the contents inside?",
|
||||
"nothing_picked": "Nothing selected",
|
||||
"nothing_picked_warning": "Please select at least one package to install!",
|
||||
"options": "Install Options",
|
||||
"option_shortcut": "Create Desktop Shortcut"
|
||||
},
|
||||
"install_packages":{
|
||||
"check_for_update":"Checking for updates...",
|
||||
"uninstall":"Uninstalling...",
|
||||
"self_update":"Downloading self-update...",
|
||||
"install":"Installing...",
|
||||
"please_wait":"Please wait..."
|
||||
},
|
||||
"error":{
|
||||
"title":"An error occurred",
|
||||
"exit_error":"{msg}\n\nPlease upload the log file (in {path}) to the {name} team",
|
||||
"location_unknown":"the location where this installer is"
|
||||
},
|
||||
"complete":{
|
||||
"thanks":"Thanks for installing {name}!",
|
||||
"up_to_date":"{name} is already up to date!",
|
||||
"updated":"{name} has been updated.",
|
||||
"uninstalled":"{name} has been uninstalled.",
|
||||
"where_to_find":"You can find your installed applications in your start menu.",
|
||||
"migration_where_to_find": "You can find your installed applications in your start menu - if you were in the middle of something, just reattempt.",
|
||||
"migration_finished": "You have been moved to the new, single version of {name}."
|
||||
},
|
||||
"modify":{
|
||||
"title":"Choose an option:",
|
||||
"update":"Update",
|
||||
"modify":"Modify",
|
||||
"repair": "Repair",
|
||||
"uninstall":"Uninstall",
|
||||
"view_local_files": "View local files",
|
||||
"prompt_repair": "<b>WARNING:</b> All the files under <code>{path}</code> will be <b>deleted</b> as part of the repair process!<br>Do you wish to continue?",
|
||||
"prompt_recover": "Installer data for {name} is corrupted.<br>A repair is required to restore the installation.",
|
||||
"prompt":"Are you sure you want to uninstall {name}?",
|
||||
"prompt_confirm":"Uninstall {name}"
|
||||
},
|
||||
"auth":{
|
||||
"paste": "Paste",
|
||||
"token": "Token",
|
||||
"verify": "Verify Token",
|
||||
"page_header": "The Early Access release channel lets you try out the latest experimental features and fixes, before they are merged into yuzu. This channel includes all regular yuzu daily updates, plus these exclusive features.\n\nTo be an Early Access member, you must be a Patreon Early Access Subscriber.",
|
||||
"page_opened": "Page opened! Check your default browser for the page, and follow the instructions there to link your patreon account.\nWhen you are done, enter the token below.",
|
||||
"login_failed": "Login failed!\n\nDouble check that your token is correct and try again"
|
||||
},
|
||||
"back":"Back",
|
||||
"exit":"Exit",
|
||||
"yes":"Yes",
|
||||
"no":"No",
|
||||
"continue": "Continue",
|
||||
"cancel":"Cancel"
|
||||
}
|
||||
}
|
||||
78
ui/src/locales/es.json
Normal file
78
ui/src/locales/es.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"es": {
|
||||
"locale": "Español",
|
||||
"error": {
|
||||
"title": "Ha ocurrido un error",
|
||||
"exit_error": "{msg}\n\nPor favor, suba el archivo del registro (en {path}) al equipo de {name}",
|
||||
"location_unknown": "la ubicación donde se encuentra el instalador"
|
||||
},
|
||||
"complete": {
|
||||
"thanks": "¡Gracias por instalar {name}!",
|
||||
"updated": "{name} ha sido actualizado.",
|
||||
"uninstalled": "{name} ha sido desinstalado.",
|
||||
"up_to_date": "¡{name} ya está actualizado!",
|
||||
"where_to_find": "Puedes encontrar las aplicaciones instaladas en el menú de inicio.",
|
||||
"migration_where_to_find": "Puedes encontrar tus aplicaciones instaladas en el menú de inicio - si estabas en mitad de algo, reinténtalo.",
|
||||
"migration_finished": "Se ha actualizado a la nueva y única versión de {name}."
|
||||
},
|
||||
"modify": {
|
||||
"title": "Elige una opción:",
|
||||
"update": "Actualizar",
|
||||
"uninstall": "Desinstalar",
|
||||
"prompt": "¿Estás seguro que deseas desinstalar {name}?",
|
||||
"prompt_repair": "<b>WARNING:</b> All the files under <code>{path}</code> will be <b>deleted</b> as part of the repair process!<br>Do you wish to proceed?",
|
||||
"view_local_files": "Ver archivos locales",
|
||||
"prompt_recover": "Los datos de la instalación de {name} están corruptos.<br>Se necesita una reparación para restaurar la instalación.",
|
||||
"prompt_confirm": "Desinstalar {name}",
|
||||
"modify": "Modificar",
|
||||
"repair": "Reparar"
|
||||
},
|
||||
"auth": {
|
||||
"paste": "Pegar",
|
||||
"token": "Token",
|
||||
"verify": "Verificar Token",
|
||||
"page_header": "El canal de acceso Early Access te permitirá probar las últimas características experimentales y arreglos antes de que lleguen a yuzu. Este canal incluye todas las actualizaciones diarias de yuzu, además de las características exclusivas.\n\nPara unirte al Early Access, debes ser un Suscriptor del Early Access en Patreon.",
|
||||
"page_opened": "¡Página abierta! Comprueba la página en tu explorador por defecto, y sigue las instrucciones para vincular tu cuenta de Patreon.\nCuando termines, introduzca el token abajo.",
|
||||
"login_failed": "¡Acceso fallido!\n\nComprueba que tu token sea el correcto e inténtelo de nuevo"
|
||||
},
|
||||
"back": "Atrás",
|
||||
"exit": "Salir",
|
||||
"yes": "Sí",
|
||||
"no": "No",
|
||||
"continue": "Continuar",
|
||||
"cancel": "Cancelar",
|
||||
"app": {
|
||||
"installer_title": "¡Te damos la bienvenida al instalador de {name}!",
|
||||
"maintenance_title": "Te damos la bienvenida a la Herramienta de Mantenimiento de {name}.",
|
||||
"window_title": "Instalador de {name}",
|
||||
"installer_subtitle": "Tendremos todo listo y funcional en unos minutos."
|
||||
},
|
||||
"select_packages": {
|
||||
"title": "Seleccione los paquetes que desea instalar:",
|
||||
"installed": "(instalado)",
|
||||
"advanced": "Avanzado...",
|
||||
"install": "Instalar",
|
||||
"location": "Ubicación de la instalación",
|
||||
"select": "Elegir",
|
||||
"overwriting": "Sobreescribir",
|
||||
"options": "Opciones de instalación",
|
||||
"title_repair": "Seleccione los paquetes que desea reparar:",
|
||||
"location_placeholder": "Introduce una ruta de instalación aquí",
|
||||
"overwriting_warning": "Directory <code>{path}</code> already exists.<br>Are you sure you want to <b>overwrite</b> the contents inside?",
|
||||
"nothing_picked_warning": "¡Por favor, seleccione al menos un paquete para la instalación!",
|
||||
"nothing_picked": "Nada seleccionado",
|
||||
"option_shortcut": "Crear acceso directo en el escritorio"
|
||||
},
|
||||
"install_packages": {
|
||||
"uninstall": "Desinstalando...",
|
||||
"install": "Instalando...",
|
||||
"check_for_update": "Comprobando actualizaciones...",
|
||||
"self_update": "Descargando actualización automática...",
|
||||
"please_wait": "Por favor, espere..."
|
||||
},
|
||||
"download_packages": {
|
||||
"download_config": "Descargando config...",
|
||||
"error_download_config": "Se ha producido un error durante la descarga del config: {msg}"
|
||||
}
|
||||
}
|
||||
}
|
||||
78
ui/src/locales/fr.json
Normal file
78
ui/src/locales/fr.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"fr": {
|
||||
"locale": "Français",
|
||||
"error": {
|
||||
"title": "Une erreur s'est produite",
|
||||
"exit_error": "{msg}\n\nVeuillez téléverser votre fichier journal (dans {path}) à l'équipe {name}",
|
||||
"location_unknown": "l'emplacement où se trouve ce programme d'installation"
|
||||
},
|
||||
"complete": {
|
||||
"thanks": "Merci d'avoir installé {name}!",
|
||||
"updated": "{name} a été mis à jour.",
|
||||
"uninstalled": "{name} a été désinstallé.",
|
||||
"up_to_date": "{name} est déjà à jour!",
|
||||
"where_to_find": "Vous pouvez trouver vos applications installées dans votre menu démarrer.",
|
||||
"migration_where_to_find": "Vous pouvez trouver vos applications installées dans votre menu démarrer - si vous étiez au milieu de quelque chose, réessayez.",
|
||||
"migration_finished": "Vous avez été déplacé vers la nouvelle version unique de {name}."
|
||||
},
|
||||
"modify": {
|
||||
"title": "Choisissez une option:",
|
||||
"update": "Mettre à jour",
|
||||
"uninstall": "Désinstaller",
|
||||
"prompt": "Êtes-vous certain de vouloir désinstaller {name}?",
|
||||
"prompt_repair": "<b>WARNING:</b> All the files under <code>{path}</code> will be <b>deleted</b> as part of the repair process!<br>Do you wish to proceed?",
|
||||
"view_local_files": "Afficher les fichiers locaux",
|
||||
"prompt_recover": "Les données du programme d'installation pour {name} sont corrompues.<br> Une réparation est requise pour restaurer l'installation.",
|
||||
"prompt_confirm": "Désinstaller {name}",
|
||||
"modify": "Modifier",
|
||||
"repair": "Réparer"
|
||||
},
|
||||
"auth": {
|
||||
"paste": "Coller",
|
||||
"token": "Jeton",
|
||||
"verify": "Vérifier le jeton",
|
||||
"page_header": "Le canal de publication Early Access vous permet d'essayer les dernières fonctionnalités expérimentales et correctifs, avant qu'elles ne soient fusionnées dans yuzu. Cette chaîne comprend toutes les mises à jour quotidiennes régulières de yuzu, ainsi que des fonctionnalités exclusives.\n\nPour être un membre Early Access, vous devez être membre Patreon Early Access.",
|
||||
"page_opened": "Page ouverte! Vérifiez votre navigateur par défaut pour la page et suivez les instructions pour lier votre compte Patreon.\nLorsque vous avez terminé, entrez le jeton ci-dessous.",
|
||||
"login_failed": "Échec de la connexion!\n\nVérifiez que votre jeton est valide et réessayez"
|
||||
},
|
||||
"back": "Retour",
|
||||
"exit": "Quitter",
|
||||
"yes": "Oui",
|
||||
"no": "Non",
|
||||
"continue": "Continuer",
|
||||
"cancel": "Annuler",
|
||||
"app": {
|
||||
"installer_title": "Bienvenue dans le programme d'installation de {name}!",
|
||||
"maintenance_title": "Bienvenue dans l'outil de maintenance de {name}.",
|
||||
"window_title": "Programme d'installation de {name}",
|
||||
"installer_subtitle": "Vous serez prêt dans quelques instants."
|
||||
},
|
||||
"select_packages": {
|
||||
"title": "Sélectionner les paquets que vous désirez installer:",
|
||||
"installed": "(installé)",
|
||||
"advanced": "Avancé...",
|
||||
"install": "Installer",
|
||||
"location": "Emplacement d'installation",
|
||||
"select": "Sélectionner",
|
||||
"overwriting": "Écrasement",
|
||||
"options": "Options d'installation",
|
||||
"title_repair": "Sélectionnez les paquets que vous désirez réparer:",
|
||||
"location_placeholder": "Entrez un chemin d'installation ici",
|
||||
"overwriting_warning": "Directory <code>{path}</code> already exists.<br>Are you sure you want to <b>overwrite</b> the contents inside?",
|
||||
"nothing_picked_warning": "Veuillez sélectionner au moins un paquet à installer!",
|
||||
"nothing_picked": "Aucune sélection",
|
||||
"option_shortcut": "Créer un raccourci sur le bureau"
|
||||
},
|
||||
"install_packages": {
|
||||
"uninstall": "Désinstallation...",
|
||||
"install": "Installation...",
|
||||
"check_for_update": "Vérification des mises à jour...",
|
||||
"self_update": "Téléchargement de la mise à jour automatique...",
|
||||
"please_wait": "Veuillez patienter..."
|
||||
},
|
||||
"download_packages": {
|
||||
"download_config": "Téléchargement de la configuration...",
|
||||
"error_download_config": "Une erreur s'est produite lors du téléchargement de la configuration: {msg}"
|
||||
}
|
||||
}
|
||||
}
|
||||
78
ui/src/locales/id.json
Normal file
78
ui/src/locales/id.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"id": {
|
||||
"locale": "Bahasa Indonesia",
|
||||
"error": {
|
||||
"title": "Terjadi kesalahan",
|
||||
"exit_error": "{msg}\n\nHarap unggah file log (di {path}) kepada tim {name}",
|
||||
"location_unknown": "lokasi dimana penginstal ini berada"
|
||||
},
|
||||
"complete": {
|
||||
"thanks": "Terima kasih telah menginstal {name}!",
|
||||
"updated": "{name} telah diperbaharui.",
|
||||
"uninstalled": "{name} telah di hapus.",
|
||||
"up_to_date": "{name} sudah up-to-date!",
|
||||
"where_to_find": "Anda dapat menemukan aplikasi yang terinstal di start menu anda.",
|
||||
"migration_where_to_find": "Anda dapat menemukan aplikasi yang terinstal di start menu anda - jika anda sedang mengerjakan sesuatu yang lain, coba lagi.",
|
||||
"migration_finished": "Anda telah dipindahkan ke versi tunggal yang baru dari {name}."
|
||||
},
|
||||
"modify": {
|
||||
"title": "Pilih opsi:",
|
||||
"update": "Perbaharui",
|
||||
"uninstall": "Copot Instalasi",
|
||||
"prompt": "Apakah anda yakin untuk menghapus {name}?",
|
||||
"prompt_repair": "<b>WARNING:</b> All the files under <code>{path}</code> will be <b>deleted</b> as part of the repair process!<br>Do you wish to proceed?",
|
||||
"view_local_files": "Lihat file lokal",
|
||||
"prompt_recover": "Data instalatur untuk {name} rusak.<br>Diperlukan perbaikan untuk memulihkan instalasi.",
|
||||
"prompt_confirm": "Hapus {name}",
|
||||
"modify": "Modifikasi",
|
||||
"repair": "Perbaiki"
|
||||
},
|
||||
"auth": {
|
||||
"paste": "Tempel",
|
||||
"token": "Token",
|
||||
"verify": "Verifikasi Token",
|
||||
"page_header": "Saluran rilis Early Access memperbolehkan anda untuk mencoba fitur eksperimental dan perbaikan terbaru sebelum mereka disatukan menjadi yuzu. Saluran ini mencakup semua pembaruan harian yuzu reguler, ditambah fitur-fitur ekslusif ini.\n\nUntuk menjadi anggota Early Access, anda harus menjadi pelanggan Early Access Patreon.",
|
||||
"page_opened": "Halaman dibuka! Periksa peramban anda untuk halamannya, dan ikuti instruksi yang ada untuk menghubungkan akun patreon anda.\nJika anda sudah selesai, masukkan token di bawah.",
|
||||
"login_failed": "Login gagal!\n\nPeriksa kembali token anda dan coba lagi"
|
||||
},
|
||||
"back": "Kembali",
|
||||
"exit": "Keluar",
|
||||
"yes": "Ya",
|
||||
"no": "Tidak",
|
||||
"continue": "Lanjut",
|
||||
"cancel": "Batalkan",
|
||||
"app": {
|
||||
"installer_title": "Selamat datang di instalatur {name}!",
|
||||
"maintenance_title": "Selamat datang di Maintenance Tool {name}.",
|
||||
"window_title": "{name} Instalatur",
|
||||
"installer_subtitle": "Kami akan membuat anda siap dan berjalan hanya dalam beberapa saat."
|
||||
},
|
||||
"select_packages": {
|
||||
"title": "Pilih paket mana yang ingin Anda install:",
|
||||
"installed": "(terinstal)",
|
||||
"advanced": "Lanjutan...",
|
||||
"install": "Instal",
|
||||
"location": "Lokasi Instal",
|
||||
"select": "Pilih",
|
||||
"overwriting": "Menimpa",
|
||||
"options": "Opsi Instal",
|
||||
"title_repair": "Pilih paket mana yang ingin anda perbaiki:",
|
||||
"location_placeholder": "Masukkan lokasi instal di sini",
|
||||
"overwriting_warning": "Directory <code>{path}</code> already exists.<br>Are you sure you want to <b>overwrite</b> the contents inside?",
|
||||
"nothing_picked_warning": "Silakan pilih setidaknya satu paket untuk diinstal!",
|
||||
"nothing_picked": "Tidak ada yang dipilih",
|
||||
"option_shortcut": "Buat Pintasan Desktop"
|
||||
},
|
||||
"install_packages": {
|
||||
"uninstall": "Mencopot Instalan...",
|
||||
"install": "Menginstal...",
|
||||
"check_for_update": "Memeriksa pembaruan...",
|
||||
"self_update": "Mengunduh pembaruan diri...",
|
||||
"please_wait": "Mohon tunggu..."
|
||||
},
|
||||
"download_packages": {
|
||||
"download_config": "Mengunduh konfigurasi...",
|
||||
"error_download_config": "Mendapatkan kesalahan saat mengunduh konfigurasi: {msg}"
|
||||
}
|
||||
}
|
||||
}
|
||||
78
ui/src/locales/it.json
Normal file
78
ui/src/locales/it.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"it": {
|
||||
"locale": "Italiano",
|
||||
"error": {
|
||||
"title": "Si è verificato un errore",
|
||||
"exit_error": "{msg}\n\nPer favore carica il file di registro (in {path}) nel {name}",
|
||||
"location_unknown": "la posizione in cui si trova il programma di installazione è"
|
||||
},
|
||||
"complete": {
|
||||
"thanks": "Grazie per aver installato {name}!",
|
||||
"updated": "{name} è stato aggiornato.",
|
||||
"uninstalled": "{name} è stato disinstallato.",
|
||||
"up_to_date": "{name} è già aggiornato!",
|
||||
"where_to_find": "Puoi trovare le applicazioni installate nel tuo menu di avvio.",
|
||||
"migration_where_to_find": "Puoi trovare le applicazioni installate nel tuo menu di avvio",
|
||||
"migration_finished": "Sei stato spostato alla nuova versione di {name}."
|
||||
},
|
||||
"modify": {
|
||||
"title": "Scegli un'opzione:",
|
||||
"update": "Aggiorna",
|
||||
"uninstall": "Disinstalla",
|
||||
"prompt": "Sei sicuro di voler disinstallare {name}?",
|
||||
"prompt_repair": "<b>WARNING:</b> All the files under <code>{path}</code> will be <b>deleted</b> as part of the repair process!<br>Do you wish to proceed?",
|
||||
"view_local_files": "Visualizza i file locali",
|
||||
"prompt_recover": "I dati di installazione del programma {name} sono danneggiati. <br>È necessaria la riparazione per ripristinare l'installazione.",
|
||||
"prompt_confirm": "Disinstalla {name}",
|
||||
"modify": "Modifica",
|
||||
"repair": "Ripara"
|
||||
},
|
||||
"auth": {
|
||||
"paste": "Incolla",
|
||||
"token": "Token",
|
||||
"verify": "Verifica token",
|
||||
"page_header": "Il canale di rilascio dell'accesso anticipato ti consente di provare le ultime funzionalità e correzioni sperimentali, prima che vengano rilasciate in yuzu. Questo canale include tutti gli aggiornamenti quotidiani regolari di yuzu, oltre ad altre funzionalità esclusive.\nPer essere un membro Early Access, devi essere abbonato a Patreon Early Access.",
|
||||
"page_opened": "Pagina aperta! Controlla il tuo browser e segui le istruzioni per collegare il tuo account patreon.\nQuando hai finito, inserisci il token qui sotto.",
|
||||
"login_failed": "Accesso fallito!\n\nRicontrolla che il tuo token sia corretto e riprova"
|
||||
},
|
||||
"back": "Indietro",
|
||||
"exit": "Esci",
|
||||
"yes": "Si",
|
||||
"no": "No",
|
||||
"continue": "Continua",
|
||||
"cancel": "Annulla",
|
||||
"app": {
|
||||
"installer_title": "Benvenuto nell'installer di {name}!",
|
||||
"maintenance_title": "Benvenuto nello strumento di manutenzione di {name}.",
|
||||
"window_title": "Installer di {name}",
|
||||
"installer_subtitle": "Sarà pronto in pochi istanti."
|
||||
},
|
||||
"select_packages": {
|
||||
"title": "Seleziona i pacchetti che vuoi installare:",
|
||||
"installed": "(installato)",
|
||||
"advanced": "Avanzate",
|
||||
"install": "Installa",
|
||||
"location": "Percorso di installazione",
|
||||
"select": "Seleziona",
|
||||
"overwriting": "Sovrascrivendo",
|
||||
"options": "Opzioni di installazione",
|
||||
"title_repair": "Seleziona i pacchetti da riparare:",
|
||||
"location_placeholder": "Inserisci un percorso di installazione",
|
||||
"overwriting_warning": "Directory <code>{path}</code> already exists.<br>Are you sure you want to <b>overwrite</b> the contents inside?",
|
||||
"nothing_picked_warning": "Seleziona almeno un pacchetto da installare!",
|
||||
"nothing_picked": "Niente di selezionato",
|
||||
"option_shortcut": "Crea una scorciatoia sul desktop"
|
||||
},
|
||||
"install_packages": {
|
||||
"uninstall": "Sto disinstallando...",
|
||||
"install": "Installazione in corso...",
|
||||
"check_for_update": "Ricerca di aggiornamenti in corso...",
|
||||
"self_update": "Download dell'aggiornamento in corso...",
|
||||
"please_wait": "Attendere prego..."
|
||||
},
|
||||
"download_packages": {
|
||||
"download_config": "Scaricamento della configurazione....",
|
||||
"error_download_config": "Si è verificato un errore durante il download della configurazione: {msg}"
|
||||
}
|
||||
}
|
||||
}
|
||||
78
ui/src/locales/ko_KR.json
Normal file
78
ui/src/locales/ko_KR.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"ko-KR": {
|
||||
"locale": "한국어",
|
||||
"error": {
|
||||
"title": "오류가 발생했습니다",
|
||||
"exit_error": "{msg}\n\n{path}에 있는 로그 파일을 {name} 에 업로드하세요",
|
||||
"location_unknown": "설치 프로그램이 있는 위치"
|
||||
},
|
||||
"complete": {
|
||||
"thanks": "{name} 을 설치해 주셔서 감사합니다!",
|
||||
"updated": "{name} 이 업데이트 되었습니다",
|
||||
"uninstalled": "{name} 이 제거 되었습니다",
|
||||
"up_to_date": "{name} 은(는) 이미 최신 버전입니다!",
|
||||
"where_to_find": "시작 메뉴에서 설치한 프로그램을 찾을 수 있습니다",
|
||||
"migration_where_to_find": "시작 메뉴에서 설치된 응용 프로그램을 찾을 수 있습니다. 설치중이었다면 다시 시도하십시오.",
|
||||
"migration_finished": "{name}의 새로운 단일 버전으로 이동되었습니다."
|
||||
},
|
||||
"modify": {
|
||||
"title": "옵션 선택:",
|
||||
"update": "업데이트",
|
||||
"uninstall": "제거",
|
||||
"prompt": "정말 {name} 을 제거하시겠습니까?",
|
||||
"prompt_repair": "<b>WARNING:</b> All the files under <code>{path}</code> will be <b>deleted</b> as part of the repair process!<br>Do you wish to proceed?",
|
||||
"view_local_files": "로컬 파일 보기",
|
||||
"prompt_recover": "{name}의 설치 프로그램 데이터가 손상되었습니다.<br>설치를 복원하려면 복구가 필요합니다.",
|
||||
"prompt_confirm": "{name} 삭제",
|
||||
"modify": "수정",
|
||||
"repair": "복구"
|
||||
},
|
||||
"auth": {
|
||||
"paste": "붙여넣기",
|
||||
"token": "토큰",
|
||||
"verify": "검증 토큰",
|
||||
"page_header": "얼리 엑세스 릴리스 채널을 통해 yuzu에 병합되기 전에 최신 실험 기능 및 수정 사항을 시험해 볼 수 있습니다. 이 채널에는 모든 정기 yuzu 일일 업데이트와 이러한 독점 기능이 포함됩니다.\n\nEarly Access 회원이 되려면 Patreon Early Access 가입자여야 합니다.",
|
||||
"page_opened": "페이지가 열렸습니다! 페이지의 기본 브라우저를 확인하고 해당 페이지의 지침에 따라 Patreon 계정을 연결하세요.\n완료되면 아래 토큰을 입력하십시오.",
|
||||
"login_failed": "로그인 실패!\n\n토큰이 올바른지 다시 확인하고 다시 시도하세요"
|
||||
},
|
||||
"back": "뒤로",
|
||||
"exit": "종료",
|
||||
"yes": "예",
|
||||
"no": "아니요",
|
||||
"continue": "계속",
|
||||
"cancel": "취소",
|
||||
"app": {
|
||||
"installer_title": "{name} 설치 프로그램에 오신 것을 환영합니다!",
|
||||
"maintenance_title": "{name} 관리 도구에 오신 것을 환영합니다!",
|
||||
"window_title": "{name} 설치 프로그램",
|
||||
"installer_subtitle": "몇 분 안에 준비하여 실행할 수 있습니다"
|
||||
},
|
||||
"select_packages": {
|
||||
"title": "설치할 패키지를 선택하세요:",
|
||||
"installed": "(설치됨)",
|
||||
"advanced": "고급...",
|
||||
"install": "설치",
|
||||
"location": "설치 위치",
|
||||
"select": "선택",
|
||||
"overwriting": "덮어쓰기",
|
||||
"options": "설치 옵션",
|
||||
"title_repair": "복구할 패키지 선택:",
|
||||
"location_placeholder": "여기에 설치 경로를 입력하세요",
|
||||
"overwriting_warning": "Directory <code>{path}</code> already exists.<br>Are you sure you want to <b>overwrite</b> the contents inside?",
|
||||
"nothing_picked_warning": "설치할 패키지를 하나 이상 선택하세요!",
|
||||
"nothing_picked": "아무것도 선택되지 않았습니다",
|
||||
"option_shortcut": "바탕화면 바로가기 만들기"
|
||||
},
|
||||
"install_packages": {
|
||||
"uninstall": "제거중...",
|
||||
"install": "설치중...",
|
||||
"check_for_update": "업데이트 확인 중...",
|
||||
"self_update": "수동 업데이트 다운로드중...",
|
||||
"please_wait": "잠시 기다려 주세요..."
|
||||
},
|
||||
"download_packages": {
|
||||
"download_config": "설정 파일 다운로드중...",
|
||||
"error_download_config": "설정 파일 다운로드 중 오류 발생: {msg}"
|
||||
}
|
||||
}
|
||||
}
|
||||
78
ui/src/locales/nb.json
Normal file
78
ui/src/locales/nb.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"nb": {
|
||||
"locale": "Bokmål",
|
||||
"error": {
|
||||
"title": "En feil oppsto",
|
||||
"exit_error": "{msg}\n\nVennlist last opp loggfilen (i {path} til {name}-teamet",
|
||||
"location_unknown": "plasseringen til installasjonsprogrammet"
|
||||
},
|
||||
"complete": {
|
||||
"thanks": "Takk for at du installerte {name}!",
|
||||
"updated": "{name} har blitt oppdatert.",
|
||||
"uninstalled": "{name} har blitt avinstallert.",
|
||||
"up_to_date": "{name} er allerede oppdatert!",
|
||||
"where_to_find": "Du kan finne de installerte programmene i startmenyen.",
|
||||
"migration_where_to_find": "Du kan finne de installerte programmene i startmenyen – hvis du var midt oppi noe, bare prøv på nytt.",
|
||||
"migration_finished": "Du har blitt flyttet til den nye, forente versjonen av {name}."
|
||||
},
|
||||
"modify": {
|
||||
"title": "Velg en handling:",
|
||||
"update": "Oppdatering",
|
||||
"uninstall": "Avinstaller",
|
||||
"prompt": "Er du sikker på at du vil avinstallere {navn}?",
|
||||
"prompt_repair": "<b>WARNING:</b> All the files under <code>{path}</code> will be <b>deleted</b> as part of the repair process!<br>Do you wish to proceed?",
|
||||
"view_local_files": "Vis lokale filer",
|
||||
"prompt_recover": "Installasjonsdata for {name} er korrupt.<br>Reparasjon er nødvendig for å gjenopprette installasjonen.",
|
||||
"prompt_confirm": "Avinstaller {name}",
|
||||
"modify": "Modifiser",
|
||||
"repair": "Reparer"
|
||||
},
|
||||
"auth": {
|
||||
"paste": "Lim inn",
|
||||
"token": "Token",
|
||||
"verify": "Verifiser token",
|
||||
"page_header": "Early Access–kanalen lar deg prøve den siste eksperimentelle funksjonaliteten og fiksene, før de kommer inn i yuzu. Denne kanalen inkluderer alle de vanlige daglige yuzu-oppdateringene, i tillegg til denne eksklusive funksjonaliteten.\n\nFor å bli et Early Access–medlem må du være en Patreon Early Access–abonnent.",
|
||||
"page_opened": "Side åpnet! Se etter siden i standardnettleseren din, og følg instruksjonene der for å koble til Patreon-kontoen din.\nNår du er ferdig, skriv inn token-en under.",
|
||||
"login_failed": "Innlogging feilet!\n\nDobbeltsjekk at token-en din er korrekt og prøv på nytt"
|
||||
},
|
||||
"back": "Tilbake",
|
||||
"exit": "Avslutt",
|
||||
"yes": "Ja",
|
||||
"no": "Nei",
|
||||
"continue": "Fortsett",
|
||||
"cancel": "Avbryt",
|
||||
"app": {
|
||||
"installer_title": "Velkommen til installasjonsprogrammet for {navn}!",
|
||||
"maintenance_title": "Velkommen til vedlikeholdsverktøyet for {navn}.",
|
||||
"window_title": "Installasjonsprogram for {navn}",
|
||||
"installer_subtitle": "Vi er klar til å kjøre om noen få øyeblikk."
|
||||
},
|
||||
"select_packages": {
|
||||
"title": "Velg hvilke pakker du ønsker å installere:",
|
||||
"installed": "(installert)",
|
||||
"advanced": "Avansert...",
|
||||
"install": "Installer",
|
||||
"location": "Installasjonsdestinasjon",
|
||||
"select": "Velg",
|
||||
"overwriting": "Overskriver",
|
||||
"options": "Installasjonsinnstillinger",
|
||||
"title_repair": "Velg hvilke pakker du ønsker å reparere:",
|
||||
"location_placeholder": "Skriv inn en installasjonssti her",
|
||||
"overwriting_warning": "Directory <code>{path}</code> already exists.<br>Are you sure you want to <b>overwrite</b> the contents inside?",
|
||||
"nothing_picked_warning": "Vennligst velg minst én pakke å installere!",
|
||||
"nothing_picked": "Ingen ting valgt",
|
||||
"option_shortcut": "Lag skrivebordssnarvei"
|
||||
},
|
||||
"install_packages": {
|
||||
"uninstall": "Avinstallerer...",
|
||||
"install": "Installerer...",
|
||||
"check_for_update": "Ser etter oppdateringer...",
|
||||
"self_update": "Laster ned selvoppdatering...",
|
||||
"please_wait": "Vennligst vent..."
|
||||
},
|
||||
"download_packages": {
|
||||
"download_config": "Laster ned konfigurasjon...",
|
||||
"error_download_config": "Mottok feilmelding under nedlasting av konfigurasjon: {msg}"
|
||||
}
|
||||
}
|
||||
}
|
||||
78
ui/src/locales/pl.json
Normal file
78
ui/src/locales/pl.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"pl": {
|
||||
"locale": "Polski",
|
||||
"error": {
|
||||
"title": "Wystąpił błąd",
|
||||
"exit_error": "{msg}\n\nPrześlij plik dziennika (w {path}) do zespołu {name}",
|
||||
"location_unknown": "lokalizacja, w której znajduje się ten instalator"
|
||||
},
|
||||
"complete": {
|
||||
"thanks": "Dziękujemy za zainstalowanie {name}!",
|
||||
"updated": "{name} zostało zaktualizowane.",
|
||||
"uninstalled": "{name} zostało odinstalowane.",
|
||||
"up_to_date": "{name} jest już aktualne!",
|
||||
"where_to_find": "Zainstalowane aplikacje można znaleźć w menu Start.",
|
||||
"migration_where_to_find": "Zainstalowane aplikacje możesz znaleźć w menu Start - jeśli byłeś w trakcie czegoś, po prostu spróbuj ponownie.",
|
||||
"migration_finished": "Zostałeś przeniesiony do nowej, pojedynczej wersji {name}."
|
||||
},
|
||||
"modify": {
|
||||
"title": "Wybierz opcję:",
|
||||
"update": "Zaktualizuj",
|
||||
"uninstall": "Odinstaluj",
|
||||
"prompt": "Czy na pewno chcesz odinstalować {name}?",
|
||||
"prompt_repair": "<b>WARNING:</b> All the files under <code>{path}</code> will be <b>deleted</b> as part of the repair process!<br>Do you wish to proceed?",
|
||||
"view_local_files": "Wyświetl lokalne pliki",
|
||||
"prompt_recover": "Dane instalatora dla {name} są uszkodzone. <br>Do przywrócenia instalacji wymagana jest naprawa.",
|
||||
"prompt_confirm": "Odinstaluj {name}",
|
||||
"modify": "Modyfikuj",
|
||||
"repair": "Napraw"
|
||||
},
|
||||
"auth": {
|
||||
"paste": "Wklej",
|
||||
"token": "Token",
|
||||
"verify": "Zweryfikuj Token",
|
||||
"page_header": "Kanał wersji Early Access pozwala wypróbować najnowsze eksperymentalne funkcje i poprawki, zanim zostaną one połączone z yuzu. Ten kanał zawiera wszystkie regularne codzienne aktualizacje yuzu oraz te ekskluzywne funkcje.\n\nAby zostać członkiem Early Access, musisz być subskrybentem Patreon Early Access.",
|
||||
"page_opened": "Strona otwarta! Sprawdź domyślną przeglądarkę dla strony i postępuj zgodnie z instrukcjami, aby połączyć swoje konto patreon.\nGdy skończysz, wprowadź poniższy token.",
|
||||
"login_failed": "Logowanie nie powiodło się!\n\nSprawdź dokładnie, czy Twój Token jest poprawny i spróbuj ponownie."
|
||||
},
|
||||
"back": "Cofnij",
|
||||
"exit": "Wyjście",
|
||||
"yes": "Tak",
|
||||
"no": "Nie",
|
||||
"continue": "Kontynuuj",
|
||||
"cancel": "Anuluj",
|
||||
"app": {
|
||||
"installer_title": "Witaj w instalatorze {name}!",
|
||||
"maintenance_title": "Witaj w narzędziu do konserwacji {name}!",
|
||||
"window_title": "Instalator {name}",
|
||||
"installer_subtitle": "Już za chwilę będziesz mógł pracować."
|
||||
},
|
||||
"select_packages": {
|
||||
"title": "Wybierz które pakiety chcesz zainstalować:",
|
||||
"installed": "(zainstalowano)",
|
||||
"advanced": "Zaawansowane...",
|
||||
"install": "Zainstaluj",
|
||||
"location": "Lokacja instalacji",
|
||||
"select": "Wybierz",
|
||||
"overwriting": "Nadpisywanie",
|
||||
"options": "Opcje instalacji",
|
||||
"title_repair": "Wybierz które pakiety chcesz naprawić:",
|
||||
"location_placeholder": "Wybierz miejsce instalacji",
|
||||
"overwriting_warning": "Directory <code>{path}</code> already exists.<br>Are you sure you want to <b>overwrite</b> the contents inside?",
|
||||
"nothing_picked_warning": "Wybierz co najmniej jeden pakiet do zainstalowania!",
|
||||
"nothing_picked": "Nic nie wybrano",
|
||||
"option_shortcut": "Utwórz skrót na pulpicie"
|
||||
},
|
||||
"install_packages": {
|
||||
"uninstall": "Odinstalowywanie...",
|
||||
"install": "Instalowanie...",
|
||||
"check_for_update": "Sprawdzanie aktualizacji...",
|
||||
"self_update": "Pobieranie samoaktualizacji...",
|
||||
"please_wait": "Proszę czekać..."
|
||||
},
|
||||
"download_packages": {
|
||||
"download_config": "Pobieranie konfiguracji...",
|
||||
"error_download_config": "Wystąpił błąd podczas pobierania konfiguracji: {msg}"
|
||||
}
|
||||
}
|
||||
}
|
||||
78
ui/src/locales/pt_BR.json
Normal file
78
ui/src/locales/pt_BR.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"pt-BR": {
|
||||
"locale": "Português do Brasil",
|
||||
"error": {
|
||||
"title": "Ocorreu um erro",
|
||||
"exit_error": "{msg}\n\nPor favor, carregue o arquivo de registro (em {path}) para a equipe do {name}.",
|
||||
"location_unknown": "o local onde se encontra este instalador"
|
||||
},
|
||||
"complete": {
|
||||
"thanks": "Obrigado por instalar o {name}!",
|
||||
"updated": "O {name} foi atualizado.",
|
||||
"uninstalled": "O {name} foi desinstalado.",
|
||||
"up_to_date": "O {name} já está atualizado!",
|
||||
"where_to_find": "Você pode encontrar suas aplicações instaladas no seu menu inicial.",
|
||||
"migration_where_to_find": "Você pode encontrar suas aplicações instaladas no seu menu inicial - se estiver no meio de alguma coisa, é só tentar novamente.",
|
||||
"migration_finished": "Você foi movido para a nova versão única do {name}."
|
||||
},
|
||||
"modify": {
|
||||
"title": "Escolha uma opção:",
|
||||
"update": "Atualizar",
|
||||
"uninstall": "Desinstalar",
|
||||
"prompt": "Você tem certeza de que deseja desinstalar o {name}?",
|
||||
"prompt_repair": "<b>WARNING:</b> All the files under <code>{path}</code> will be <b>deleted</b> as part of the repair process!<br>Do you wish to proceed?",
|
||||
"view_local_files": "Ver arquivos locais",
|
||||
"prompt_recover": "Os dados do instalador do {name} estão corrompidos.<br>Um reparo é necessário para restaurar a instalação.",
|
||||
"prompt_confirm": "Desinstalar o {name}",
|
||||
"modify": "Modificar",
|
||||
"repair": "Reparar"
|
||||
},
|
||||
"auth": {
|
||||
"paste": "Colar",
|
||||
"token": "Token",
|
||||
"verify": "Verificar o Token",
|
||||
"page_header": "O canal da versão de Acesso Antecipado permite que você experimente as últimas características experimentais e correções, antes que elas sejam fundidas no yuzu. Este canal inclui todas as atualizações diárias regulares do yuzu, além destas características exclusivas.\n\nPara ser um membro do Acesso Antecipado, você deve ser um assinante do Patreon de Acesso Antecipado.",
|
||||
"page_opened": "Página aberta! Verifique seu navegador padrão com a página, e siga as instruções lá para vincular sua conta do patreon.\nQuando terminar, digite o token abaixo.",
|
||||
"login_failed": "Falha no login!\n\nVerifique duas vezes se seu token está correto e tente novamente"
|
||||
},
|
||||
"back": "Voltar",
|
||||
"exit": "Sair",
|
||||
"yes": "Sim",
|
||||
"no": "Não",
|
||||
"continue": "Continuar",
|
||||
"cancel": "Cancelar",
|
||||
"app": {
|
||||
"installer_title": "Bem-vindo ao instalador do {name}!",
|
||||
"maintenance_title": "Bem-vindo à Ferramenta de Manutenção do {name}.",
|
||||
"window_title": "Instalador do {name}",
|
||||
"installer_subtitle": "Em poucos instantes, estaremos prontos para começar."
|
||||
},
|
||||
"select_packages": {
|
||||
"title": "Selecione quais pacotes você deseja instalar:",
|
||||
"installed": "(instalado)",
|
||||
"advanced": "Avançado...",
|
||||
"install": "Instalar",
|
||||
"location": "Local de instalação",
|
||||
"select": "Selecionar",
|
||||
"overwriting": "Sobrescrever",
|
||||
"options": "Opções de instalação",
|
||||
"title_repair": "Selecione quais pacotes você deseja reparar:",
|
||||
"location_placeholder": "Insira aqui um caminho de instalação",
|
||||
"overwriting_warning": "Directory <code>{path}</code> already exists.<br>Are you sure you want to <b>overwrite</b> the contents inside?",
|
||||
"nothing_picked_warning": "Por favor, selecione pelo menos um pacote para instalar!",
|
||||
"nothing_picked": "Nada selecionado",
|
||||
"option_shortcut": "Criar atalho na Área de Trabalho"
|
||||
},
|
||||
"install_packages": {
|
||||
"uninstall": "Desinstalando...",
|
||||
"install": "Instalando...",
|
||||
"check_for_update": "Verificando se há atualizações...",
|
||||
"self_update": "Baixando a auto-atualização...",
|
||||
"please_wait": "Por favor, aguarde..."
|
||||
},
|
||||
"download_packages": {
|
||||
"download_config": "Baixando configuração...",
|
||||
"error_download_config": "Houve um erro durante o download da configuração: {msg}"
|
||||
}
|
||||
}
|
||||
}
|
||||
78
ui/src/locales/pt_PT.json
Normal file
78
ui/src/locales/pt_PT.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"pt-PT": {
|
||||
"locale": "Português",
|
||||
"error": {
|
||||
"title": "Ocorreu um erro",
|
||||
"exit_error": "{msg}\n\nPor favor, carregue o arquivo de registro (em {path}) para a equipe do {name}.",
|
||||
"location_unknown": "o local onde se encontra este instalador"
|
||||
},
|
||||
"complete": {
|
||||
"thanks": "Obrigado por instalar o {name}!",
|
||||
"updated": "O {name} foi atualizado.",
|
||||
"uninstalled": "O {name} foi desinstalado.",
|
||||
"up_to_date": "O {name} já está atualizado!",
|
||||
"where_to_find": "Você pode encontrar suas aplicações instaladas no seu menu inicial.",
|
||||
"migration_where_to_find": "Você pode encontrar suas aplicações instaladas no seu menu inicial - se estiver no meio de alguma coisa, é só tentar novamente.",
|
||||
"migration_finished": "Você foi movido para a nova versão única do {name}."
|
||||
},
|
||||
"modify": {
|
||||
"title": "Escolha uma opção:",
|
||||
"update": "Actualização",
|
||||
"uninstall": "Desinstalar",
|
||||
"prompt": "Você tem certeza de que deseja desinstalar o {name}?",
|
||||
"prompt_repair": "<b>WARNING:</b> All the files under <code>{path}</code> will be <b>deleted</b> as part of the repair process!<br>Do you wish to proceed?",
|
||||
"view_local_files": "Ver arquivos locais",
|
||||
"prompt_recover": "Os dados do instalador do {name} estão corrompidos.<br>Um reparo é necessário para restaurar a instalação.",
|
||||
"prompt_confirm": "Desinstalar o {name}",
|
||||
"modify": "Modificar",
|
||||
"repair": "Reparar"
|
||||
},
|
||||
"auth": {
|
||||
"paste": "Colar",
|
||||
"token": "Token",
|
||||
"verify": "Verificar o Token",
|
||||
"page_header": "O canal da versão de Acesso Antecipado permite que você experimente as últimas características experimentais e correções, antes que elas sejam fundidas no yuzu. Este canal inclui todas as atualizações diárias regulares do yuzu, além destas características exclusivas.\n\nPara ser um membro do Acesso Antecipado, você deve ser um assinante do Patreon de Acesso Antecipado.",
|
||||
"page_opened": "Página aberta! Verifique seu navegador padrão com a página, e siga as instruções lá para vincular sua conta do patreon.\nQuando terminar, digite o token abaixo.",
|
||||
"login_failed": "Falha no login!\n\nVerifique duas vezes se seu token está correto e tente novamente"
|
||||
},
|
||||
"back": "Voltar",
|
||||
"exit": "Sair",
|
||||
"yes": "Sim",
|
||||
"no": "Não",
|
||||
"continue": "Continuar",
|
||||
"cancel": "Cancelar",
|
||||
"app": {
|
||||
"installer_title": "Bem-vindo ao instalador do {name}!",
|
||||
"maintenance_title": "Bem-vindo à Ferramenta de Manutenção do {name}.",
|
||||
"window_title": "Instalador do {name}",
|
||||
"installer_subtitle": "Em poucos instantes, estaremos prontos para começar."
|
||||
},
|
||||
"select_packages": {
|
||||
"title": "Selecione quais pacotes você deseja instalar:",
|
||||
"installed": "(instalado)",
|
||||
"advanced": "Avançado...",
|
||||
"install": "Instalar",
|
||||
"location": "Local de instalação",
|
||||
"select": "Selecionar",
|
||||
"overwriting": "Sobrescrever",
|
||||
"options": "Opções de instalação",
|
||||
"title_repair": "Selecione quais pacotes você deseja reparar:",
|
||||
"location_placeholder": "Insira aqui um caminho de instalação",
|
||||
"overwriting_warning": "Directory <code>{path}</code> already exists.<br>Are you sure you want to <b>overwrite</b> the contents inside?",
|
||||
"nothing_picked_warning": "Por favor, selecione pelo menos um pacote para instalar!",
|
||||
"nothing_picked": "Nada selecionado",
|
||||
"option_shortcut": "Criar atalho na Área de Trabalho"
|
||||
},
|
||||
"install_packages": {
|
||||
"uninstall": "Desinstalando...",
|
||||
"install": "Instalando...",
|
||||
"check_for_update": "Verificando se há atualizações...",
|
||||
"self_update": "Baixando a auto-atualização...",
|
||||
"please_wait": "Por favor, aguarde..."
|
||||
},
|
||||
"download_packages": {
|
||||
"download_config": "Baixando configuração...",
|
||||
"error_download_config": "Houve um erro durante o download da configuração: {msg}"
|
||||
}
|
||||
}
|
||||
}
|
||||
78
ui/src/locales/ru_RU.json
Normal file
78
ui/src/locales/ru_RU.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"ru-RU": {
|
||||
"locale": "Русский",
|
||||
"error": {
|
||||
"title": "Произошла ошибка",
|
||||
"exit_error": "{msg}\n\nПожалуйста, загрузите файл журнала (в {path}) и отправьте его команде {name}",
|
||||
"location_unknown": "расположение, в котором находится установщик"
|
||||
},
|
||||
"complete": {
|
||||
"thanks": "Спасибо за установку {name}!",
|
||||
"updated": "{name} был обновлён.",
|
||||
"uninstalled": "{name} был удалён.",
|
||||
"up_to_date": "{name} уже обновлён!",
|
||||
"where_to_find": "Установленные приложения можно найти в меню \"Пуск\".",
|
||||
"migration_where_to_find": "Установленные приложения можно найти в меню \"Пуск\" - если вы были заняты чем-то, просто повторите попытку.",
|
||||
"migration_finished": "Вы были переведены на новую, единую версию {name}."
|
||||
},
|
||||
"modify": {
|
||||
"title": "Выберите вариант:",
|
||||
"update": "Обновить",
|
||||
"uninstall": "Удалить",
|
||||
"prompt": "Вы уверены, что хотите удалить {name}?",
|
||||
"prompt_repair": "<b>WARNING:</b> All the files under <code>{path}</code> will be <b>deleted</b> as part of the repair process!<br>Do you wish to proceed?",
|
||||
"view_local_files": "Просмотреть локальные файлы",
|
||||
"prompt_recover": "Файлы установщика {name} повреждены.<br>Для восстановления установки требуется починка.",
|
||||
"prompt_confirm": "Удалить {name}",
|
||||
"modify": "Изменить",
|
||||
"repair": "Исправить"
|
||||
},
|
||||
"auth": {
|
||||
"paste": "Вставить",
|
||||
"token": "Токен",
|
||||
"verify": "Проверить токен",
|
||||
"page_header": "Канал выпуска Early Access позволяет вам опробовать последние экспериментальные функции и исправления, прежде чем они будут внедрены в yuzu. Этот канал включает все обычные ежедневные обновления yuzu, а также эти эксклюзивные возможности.\n\nЧтобы стать участником Early Access, вы должны быть подписчиком Patreon Early Access.",
|
||||
"page_opened": "Страница открыта! Проверьте страницу на вашем браузере по умолчанию, и следуйте инструкциям, чтобы связать свой аккаунт Patreon.\nКогда вы закончите, введите токен ниже.",
|
||||
"login_failed": "Вход не удался!\n\nПроверьте правильность вашего токена и повторите попытку"
|
||||
},
|
||||
"back": "Назад",
|
||||
"exit": "Выход",
|
||||
"yes": "Да",
|
||||
"no": "Нет",
|
||||
"continue": "Продолжить",
|
||||
"cancel": "Отмена",
|
||||
"app": {
|
||||
"installer_title": "Добро пожаловать в установщик {name}!",
|
||||
"maintenance_title": "Добро пожаловать в Инструмент Обслуживания {name}.",
|
||||
"window_title": "Установщик {name}",
|
||||
"installer_subtitle": "Мы сейчас всё для вас подготовим."
|
||||
},
|
||||
"select_packages": {
|
||||
"title": "Выберите пакеты для установки:",
|
||||
"installed": "(установлено)",
|
||||
"advanced": "Дополнительно...",
|
||||
"install": "Установить",
|
||||
"location": "Расположение установки",
|
||||
"select": "Выбрать",
|
||||
"overwriting": "Перезапись",
|
||||
"options": "Параметры установки",
|
||||
"title_repair": "Выберите пакеты для починки:",
|
||||
"location_placeholder": "Введите путь установки здесь",
|
||||
"overwriting_warning": "Directory <code>{path}</code> already exists.<br>Are you sure you want to <b>overwrite</b> the contents inside?",
|
||||
"nothing_picked_warning": "Пожалуйста, выберите хотя-бы один пакет для установки!",
|
||||
"nothing_picked": "Ничего не выбрано",
|
||||
"option_shortcut": "Создать ярлык на Рабочем Столе"
|
||||
},
|
||||
"install_packages": {
|
||||
"uninstall": "Удаление...",
|
||||
"install": "Установка...",
|
||||
"check_for_update": "Проверка обновлений...",
|
||||
"self_update": "Загрузка самообновления...",
|
||||
"please_wait": "Пожалуйста, подождите..."
|
||||
},
|
||||
"download_packages": {
|
||||
"download_config": "Загрузка конфигурации...",
|
||||
"error_download_config": "Произошла ошибка при загрузке конфигурации: {msg}"
|
||||
}
|
||||
}
|
||||
}
|
||||
78
ui/src/locales/tr_TR.json
Normal file
78
ui/src/locales/tr_TR.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"tr-TR": {
|
||||
"locale": "Türkçe",
|
||||
"error": {
|
||||
"title": "Bir hata oluştu",
|
||||
"exit_error": "{msg}\n\nLütfen ({path}'deki) log dosyasını {name} takımına gönderin",
|
||||
"location_unknown": "bu yükleyicinin bulunduğu konum"
|
||||
},
|
||||
"complete": {
|
||||
"thanks": "Yüklediğiniz için teşekkür ederiz, {name}!",
|
||||
"updated": "{name} güncellendi.",
|
||||
"uninstalled": "{name} kaldırıldı",
|
||||
"up_to_date": "{name} zaten güncel!",
|
||||
"where_to_find": "Yüklü uygulamalarınızı start menüsünde bulabilirsiniz.",
|
||||
"migration_where_to_find": "Yüklü uygulamalarınızı start menüsünde bulabilirsiniz - eğer bir şeyle uğraşıyorduysanız sadece yeniden deneyin.",
|
||||
"migration_finished": "{name}'in en yeni versiyonuna taşındınız."
|
||||
},
|
||||
"modify": {
|
||||
"title": "Birini seçin:",
|
||||
"update": "Güncelle",
|
||||
"uninstall": "Kaldır",
|
||||
"prompt": "{name}'i kaldırmak istediğinizden emin misiniz?",
|
||||
"prompt_repair": "<b>WARNING:</b> All the files under <code>{path}</code> will be <b>deleted</b> as part of the repair process!<br>Do you wish to proceed?",
|
||||
"view_local_files": "Yerel dosyaları görüntüle",
|
||||
"prompt_recover": "{name} yükleyici dosyaları bozuk.<br>Yüklemeyi düzeltmek için onarım gerekli.",
|
||||
"prompt_confirm": "{name}'i kaldır",
|
||||
"modify": "Düzenle",
|
||||
"repair": "Onar"
|
||||
},
|
||||
"auth": {
|
||||
"paste": "Yapıştır",
|
||||
"token": "Token",
|
||||
"verify": "Token'i Doğrula",
|
||||
"page_header": "Erken Erişim Kanalı, en son deneysel özellik ve düzeltmeleri yuzuya eklenmeden denemenizi sağlar. Bu kanal tüm günlük yuzu güncellemelerini ve üstüne bütün özel özellikleri içerir.\n\nBir Erken Erişim üyesi olmak için Patreon Erken Erişim üyesi olmalısınız.",
|
||||
"page_opened": "Sayfa açıldı! Patreon hesabınızı bağlamak için tarayıcınızda açılan sayfadaki adımları takip edin.\nİşiniz bittiğinde tokeninizi aşağıya girin.",
|
||||
"login_failed": "Giriş başarısız oldu!\n\nToken'inizin doğruluğunu kontrol edip yeniden deneyin"
|
||||
},
|
||||
"back": "Geri",
|
||||
"exit": "Çık",
|
||||
"yes": "Evet",
|
||||
"no": "Hayır",
|
||||
"continue": "Devam Et",
|
||||
"cancel": "İptal",
|
||||
"app": {
|
||||
"installer_title": "{name} yükleyicisine hoş geldiniz!",
|
||||
"maintenance_title": "{name} Bakım Aracına Hoş Geldiniz.",
|
||||
"window_title": "{name} Yükleyicisi",
|
||||
"installer_subtitle": "Birazdan hepsi hallolacak."
|
||||
},
|
||||
"select_packages": {
|
||||
"title": "Yüklemek istediğiniz paketleri seçin:",
|
||||
"installed": "(Yüklendi)",
|
||||
"advanced": "Gelişmiş...",
|
||||
"install": "Yükle",
|
||||
"location": "Yükleme Konumu",
|
||||
"select": "Seç",
|
||||
"overwriting": "Üstüne yazma",
|
||||
"options": "Yükleme Seçenekleri",
|
||||
"title_repair": "Onarmak istediğiniz paketleri seçin:",
|
||||
"location_placeholder": "Bir yükleme konumu belirleyin",
|
||||
"overwriting_warning": "Directory <code>{path}</code> already exists.<br>Are you sure you want to <b>overwrite</b> the contents inside?",
|
||||
"nothing_picked_warning": "Lütfen yüklemek için en az bir paket seçin!",
|
||||
"nothing_picked": "Hiçbir şey seçilmedi",
|
||||
"option_shortcut": "Masaüstü Kısayolu Oluştur"
|
||||
},
|
||||
"install_packages": {
|
||||
"uninstall": "Kaldırılıyor...",
|
||||
"install": "Yükleniyor...",
|
||||
"check_for_update": "Güncellemeler kontrol ediliyor...",
|
||||
"self_update": "Güncelleme indiriliyor...",
|
||||
"please_wait": "Lütfen bekleyin..."
|
||||
},
|
||||
"download_packages": {
|
||||
"download_config": "Yapılandırma indiriliyor...",
|
||||
"error_download_config": "Yapılandırma indirilirken bir hata oluştu: {msg}"
|
||||
}
|
||||
}
|
||||
}
|
||||
78
ui/src/locales/vi.json
Normal file
78
ui/src/locales/vi.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"vi": {
|
||||
"locale": "Tiếng Việt",
|
||||
"error": {
|
||||
"title": "Có lỗi xảy ra",
|
||||
"exit_error": "{msg}\n\nVui lòng tải lên bản báo cáo (in {path}) cho đội {name}",
|
||||
"location_unknown": "nơi trình cài đặt được đặt"
|
||||
},
|
||||
"complete": {
|
||||
"thanks": "Cảm ơn bạn đã cài {name}!",
|
||||
"updated": "{name} đã được cập nhật.",
|
||||
"uninstalled": "{name} đã được gỡ cài đặt.",
|
||||
"up_to_date": "{name} đã ở phiên bản mới nhất!",
|
||||
"where_to_find": "Bạn có thể tìm thấy các ứng dụng đã cài trong menu Start",
|
||||
"migration_where_to_find": "Bạn có thể tìm thấy các ứng dụng đã cài trong menu Start - nếu đang làm dở việc gì thì cứ thử lại.",
|
||||
"migration_finished": "Bạn đã được chuyển tới bản mới của {name}"
|
||||
},
|
||||
"modify": {
|
||||
"title": "Chọn một sự lựa chọn:",
|
||||
"update": "Cập nhật",
|
||||
"uninstall": "Gỡ cài đặt",
|
||||
"prompt": "Bạn có chắc bạn muốn gỡ cài đặt {name}?",
|
||||
"prompt_repair": "<b>WARNING:</b> All the files under <code>{path}</code> will be <b>deleted</b> as part of the repair process!<br>Do you wish to proceed?",
|
||||
"view_local_files": "Xem các tập tin trên máy",
|
||||
"prompt_recover": "Dữ liệu cài đặt cho {name} đã bị lỗi.<br>Bạn cần thực hiện vá lỗi để khôi phục lại bản cài đặt.",
|
||||
"prompt_confirm": "Gỡ cài đặt {name}",
|
||||
"modify": "Chỉnh sửa",
|
||||
"repair": "Sửa chữa"
|
||||
},
|
||||
"auth": {
|
||||
"paste": "Dán",
|
||||
"token": "Token",
|
||||
"verify": "Kiểm tra Token",
|
||||
"page_header": "Kênh Truy Cập Sớm cho phép bạn thử một số chức năng và vá lỗi trong giai đoạn thử nghiệm trước khi nó được đưa vào yuzu chính thức. Kênh này bao gồm tất cả bản cập nhật yuzu hằng ngày bình thường và một số lợi ích đặc biệt trên.\n\nĐể trở thành một thành viên của kênh Truy Cập Sớm thì bạn phải là một người người ủng hộ và đăng ký kênh Truy Cập Sớm trên Patreon.",
|
||||
"page_opened": "Trang đã được mở! Kiểm tra trình duyệt mặc định cho trang này, và làm theo hướng dẫn ở đó để liên kết tài khoản Patreon của bạn.\nKhi bạn hoàn thành, hãy nhập token vào bên dưới.",
|
||||
"login_failed": "Đăng nhập thất bại!\n\nHãy kiểm tra lại token của bạn đã chính xác chưa và thử lại"
|
||||
},
|
||||
"back": "Trở lại",
|
||||
"exit": "Thoát",
|
||||
"yes": "Có",
|
||||
"no": "Không",
|
||||
"continue": "Tiếp tục",
|
||||
"cancel": "Hủy bỏ",
|
||||
"app": {
|
||||
"installer_title": "Chào mừng tới trình cài đặt {name}!",
|
||||
"maintenance_title": "Chào mừng tới trình bảo trì {name}.",
|
||||
"window_title": "Trình cài đặt {name}",
|
||||
"installer_subtitle": "Chúng ta sẽ hoàn thành trong một vài giây lát."
|
||||
},
|
||||
"select_packages": {
|
||||
"title": "Chọn các gói mà bạn muốn cài:",
|
||||
"installed": "(đã cài đặt)",
|
||||
"advanced": "Nâng cao...",
|
||||
"install": "Cài đặt",
|
||||
"location": "Nơi cài đặt",
|
||||
"select": "Lựa chọn",
|
||||
"overwriting": "Đang ghi đè",
|
||||
"options": "Các thiết lập cài đặt",
|
||||
"title_repair": "Chọn các gói mà bạn muốn vá lỗir:",
|
||||
"location_placeholder": "Nhập đường dẫn tới nơi bạn muốn cài vào đây",
|
||||
"overwriting_warning": "Directory <code>{path}</code> already exists.<br>Are you sure you want to <b>overwrite</b> the contents inside?",
|
||||
"nothing_picked_warning": "Vui lòng chọn ít nhất một gói để cài!",
|
||||
"nothing_picked": "Chưa chọn gì",
|
||||
"option_shortcut": "Tạo đường dẫn trên màn hình chính"
|
||||
},
|
||||
"install_packages": {
|
||||
"uninstall": "Đang gỡ cài đặt...",
|
||||
"install": "Đang cài đặt...",
|
||||
"check_for_update": "Đang tìm bản cập nhật...",
|
||||
"self_update": "Đang tải chương trình tự cập nhật...",
|
||||
"please_wait": "Vui lòng đợi..."
|
||||
},
|
||||
"download_packages": {
|
||||
"download_config": "Đang tải cấu hình",
|
||||
"error_download_config": "Có lỗi trong khi tải về cấu hình: {msg}"
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue