Merge pull request #29 from liushuyu/yuzu

Installer Overhaul
This commit is contained in:
liushuyu 2023-01-03 18:32:25 -07:00 committed by GitHub
commit f46fed17b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
101 changed files with 10929 additions and 8638 deletions

58
.github/workflows/test-build.yml vendored Normal file
View 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
View File

@ -5,3 +5,5 @@
**/*.rs.bk **/*.rs.bk
*.log *.log
*.exe

View File

@ -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

8
.tx/config Executable file
View 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

File diff suppressed because it is too large Load Diff

View File

@ -1,64 +1,71 @@
[package] [package]
name = "liftinstall" name = "liftinstall"
version = "0.1.0" version = "0.2.0"
edition = "2018"
authors = ["James <jselby@jselby.net>"] authors = ["James <jselby@jselby.net>"]
repository = "https://github.com/j-selby/liftinstall.git" repository = "https://github.com/j-selby/liftinstall.git"
documentation = "https://liftinstall.jselby.net" documentation = "https://liftinstall.jselby.net"
description = "An adaptable installer for your application." description = "An adaptable installer for your application."
build = "build.rs" build = "build.rs"
resolver = "2"
[dependencies] [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" hyper = "0.11.27"
futures = "0.1.25" futures = "0.1.29"
mime_guess = "1.8.6" mime_guess = "2.0"
url = "1.7.2" url = "2.2"
reqwest = "0.9.21" reqwest = "0.9.22"
number_prefix = "0.3.0" number_prefix = "0.4"
serde = "1.0.89" serde = "1.0"
serde_derive = "1.0.89" serde_derive = "1.0"
serde_json = "1.0.39" serde_json = "1.0"
toml = "0.5.0" toml = "0.5"
semver = {version = "0.9.0", features = ["serde"]} semver = {version = "1.0", features = ["serde"]}
regex = "1.1.5" regex = "1.4"
dirs = "1.0.5" dirs = "^4"
zip = "0.5.1" zip = "0.6"
xz2 = "0.1.6" xz2 = "0.1"
tar = "0.4" tar = "0.4"
log = "0.4" log = "0.4"
fern = "0.5" fern = "0.6"
chrono = "0.4.6" chrono = "0.4"
clap = "2.32.0" clap = "2.33"
# used to open a link to the users default browser # used to open a link to the users default browser
webbrowser = "0.5.2" webbrowser = "0.8"
# used in JWT based package authentication # used in JWT based package authentication
jsonwebtoken = "6" jsonwebtoken = "^8"
# used to decode the public key for verifying JWT tokens # used to decode the public key for verifying JWT tokens
base64 = "0.10.1" base64 = "0.13"
[build-dependencies] [build-dependencies]
walkdir = "2.2.7" walkdir = "2.3"
serde = "1.0.89" serde = "1.0"
serde_derive = "1.0.89" serde_derive = "1.0"
toml = "0.5.0" toml = "0.5"
which = "2.0.1" which = "4.0"
image = { version = "0.24", default-features = false, features = ["ico"] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["psapi", "winbase", "winioctl", "winnt"] } 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] [target.'cfg(not(windows))'.dependencies]
sysinfo = "0.8.2" sysinfo = "0.26"
slug = "0.1.4" slug = "0.1"
[target.'cfg(windows)'.build-dependencies] [target.'cfg(windows)'.build-dependencies]
winres = "0.1" winres = "0.1"

18
Justfile Normal file
View 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
View 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.

View File

@ -12,6 +12,7 @@ extern crate toml;
extern crate which; extern crate which;
use std::env; use std::env;
use std::io::Write;
use std::path::PathBuf; use std::path::PathBuf;
use std::fs::copy; use std::fs::copy;
@ -22,6 +23,8 @@ use std::process::Command;
use std::env::consts::OS; use std::env::consts::OS;
use image::imageops::FilterType;
/// Describes the application itself. /// Describes the application itself.
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct BaseAttributes { pub struct BaseAttributes {
@ -46,6 +49,8 @@ fn handle_binary(config: &BaseAttributes) {
cc::Build::new() cc::Build::new()
.cpp(true) .cpp(true)
.define("_WIN32_WINNT", Some("0x0600"))
.define("WINVER", Some("0x0600"))
.file("src/native/interop.cpp") .file("src/native/interop.cpp")
.compile("interop"); .compile("interop");
} }
@ -60,6 +65,13 @@ fn main() {
let os = OS.to_lowercase(); 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 // Find target config
let target_config = PathBuf::from(format!("bootstrap.{}.toml", os)); let target_config = PathBuf::from(format!("bootstrap.{}.toml", os));
@ -90,6 +102,14 @@ fn main() {
let yarn_binary = let yarn_binary =
which::which("yarn").expect("Failed to find yarn - please go ahead and install it!"); 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 // Build and deploy frontend files
Command::new(&yarn_binary) Command::new(&yarn_binary)
.arg("--version") .arg("--version")
@ -102,7 +122,7 @@ fn main() {
.unwrap() .unwrap()
.wait() .wait()
.expect("Unable to install Node.JS dependencies using Yarn"); .expect("Unable to install Node.JS dependencies using Yarn");
Command::new(&yarn_binary) let return_code = Command::new(&yarn_binary)
.args(&[ .args(&[
"--cwd", "--cwd",
ui_dir.to_str().expect("Unable to covert path"), ui_dir.to_str().expect("Unable to covert path"),
@ -114,8 +134,7 @@ fn main() {
.to_str() .to_str()
.expect("Unable to convert path"), .expect("Unable to convert path"),
]) ])
.spawn() .status()
.unwrap()
.wait()
.expect("Unable to build frontend assets using Webpack"); .expect("Unable to build frontend assets using Webpack");
assert!(return_code.success());
} }

View File

@ -41,7 +41,7 @@ impl<'a> Archive<'a> for ZipArchive<'a> {
continue; continue;
} }
func(i, Some(max), archive.sanitized_name(), &mut archive)?; func(i, Some(max), archive.mangled_name(), &mut archive)?;
} }
Ok(()) Ok(())

View File

@ -7,8 +7,8 @@ use toml::de::Error as TomlError;
use serde_json::{self, Error as SerdeError}; use serde_json::{self, Error as SerdeError};
use sources::get_by_name; use crate::sources::get_by_name;
use sources::types::Release; use crate::sources::types::Release;
/// Description of the source of a package. /// Description of the source of a package.
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
@ -84,6 +84,8 @@ pub struct AuthenticationConfig {
pub struct BaseAttributes { pub struct BaseAttributes {
pub name: String, pub name: String,
pub target_url: String, pub target_url: String,
#[serde(default)]
pub recovery: bool,
} }
impl BaseAttributes { impl BaseAttributes {

View File

@ -4,8 +4,8 @@
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use installer::InstallerFramework; use crate::installer::InstallerFramework;
use logging::LoggingErrors; use crate::logging::LoggingErrors;
pub mod rest; pub mod rest;
mod ui; 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()); 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. // Explicitly hint that we want the servers instance until here.
drop(servers); drop(servers);

View File

@ -2,7 +2,8 @@
extern crate mime_guess; 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 { macro_rules! include_files_as_assets {
( $target_match:expr, $( $file_name:expr ),* ) => { ( $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) => { Some(ext_ptr) => {
let ext = &file_path[ext_ptr + 1..]; 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(); let string_mime = guessed_mime.to_string();
@ -44,10 +45,11 @@ pub fn file_from_string(file_path: &str) -> Option<(String, &'static [u8])> {
"/fonts/roboto-v18-latin-regular.eot", "/fonts/roboto-v18-latin-regular.eot",
"/fonts/roboto-v18-latin-regular.woff", "/fonts/roboto-v18-latin-regular.woff",
"/fonts/roboto-v18-latin-regular.woff2", "/fonts/roboto-v18-latin-regular.woff2",
"/fonts/materialdesignicons-webfont.eot",
"/fonts/materialdesignicons-webfont.woff",
"/fonts/materialdesignicons-webfont.woff2",
"/js/chunk-vendors.js", "/js/chunk-vendors.js",
"/js/chunk-vendors.js.map", "/js/app.js"
"/js/app.js",
"/js/app.js.map"
)?; )?;
Some((string_mime, contents)) Some((string_mime, contents))

View File

@ -2,11 +2,11 @@
//! //!
//! Contains the over-arching server object + methods to manipulate it. //! 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; use hyper::server::Http;

View File

@ -2,27 +2,23 @@
//! //!
//! The /api/attr call returns an executable script containing session variables. //! The /api/attr call returns an executable script containing session variables.
use frontend::rest::services::default_future; use crate::frontend::rest::services::default_future;
use frontend::rest::services::encapsulate_json; use crate::frontend::rest::services::Future;
use frontend::rest::services::Future; use crate::frontend::rest::services::Request;
use frontend::rest::services::Request; use crate::frontend::rest::services::Response;
use frontend::rest::services::Response; use crate::frontend::rest::services::WebService;
use frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType}; use hyper::header::{ContentLength, ContentType};
use logging::LoggingErrors; use crate::logging::LoggingErrors;
pub fn handle(service: &WebService, _req: Request) -> Future { pub fn handle(service: &WebService, _req: Request) -> Future {
let framework = service.get_framework_read(); let framework = service.get_framework_read();
let file = encapsulate_json( let file = framework
"base_attributes",
&framework
.base_attributes .base_attributes
.to_json_str() .to_json_str()
.log_expect("Failed to render JSON representation of config"), .log_expect("Failed to render JSON representation of config");
);
default_future( default_future(
Response::new() Response::new()

View File

@ -2,27 +2,26 @@
//! //!
//! Provides mechanisms to authenticate users using JWT. //! Provides mechanisms to authenticate users using JWT.
use std::collections::HashMap; use std::collections::HashSet;
use std::sync::Arc; use std::sync::Arc;
use futures::{Future, Stream}; use futures::{Future, Stream};
use hyper::header::{ContentLength, ContentType}; use hyper::header::{ContentLength, ContentType};
use jsonwebtoken::DecodingKey;
use jwt::{decode, Algorithm, Validation}; use jwt::{decode, Algorithm, Validation};
use reqwest::header::USER_AGENT; use reqwest::header::USER_AGENT;
use url::form_urlencoded; use crate::frontend::rest::services::Future as InternalFuture;
use crate::frontend::rest::services::{default_future, Request, Response, WebService};
use frontend::rest::services::Future as InternalFuture; use crate::http::{build_async_client, build_client};
use frontend::rest::services::{default_future, Request, Response, WebService};
use http::{build_async_client, build_client}; use crate::config::JWTValidation;
use config::JWTValidation; use crate::logging::LoggingErrors;
use logging::LoggingErrors;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
struct Auth { struct Auth {
@ -48,6 +47,12 @@ pub struct JWTClaims {
pub is_subscribed: bool, 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 /// Calls the given server to obtain a JWT token and returns a Future<String> with the response
pub fn authenticate_async( pub fn authenticate_async(
url: String, url: String,
@ -126,24 +131,25 @@ pub fn validate_token(
let pub_key = if pub_key_base64.is_empty() { let pub_key = if pub_key_base64.is_empty() {
vec![] vec![]
} else { } else {
match base64::decode(&pub_key_base64) { base64::decode(&pub_key_base64).map_err(|e| {
Ok(v) => v, format!(
Err(err) => {
return Err(format!(
"Configured public key was not empty and did not decode as base64 {:?}", "Configured public key was not empty and did not decode as base64 {:?}",
err e
)); )
} })?
}
}; };
// Configure validation for audience and issuer if the configuration provides it // Configure validation for audience and issuer if the configuration provides it
let mut validation = match validation { let mut validation = match validation {
Some(v) => { Some(v) => {
let mut valid = Validation::new(Algorithm::RS256); let mut valid = Validation::new(Algorithm::RS256);
valid.iss = v.iss; valid.iss = v.iss.map(|iss| {
let mut issuer = HashSet::new();
issuer.insert(iss);
issuer
});
if let &Some(ref v) = &v.aud { if let &Some(ref v) = &v.aud {
valid.set_audience(v); valid.set_audience(&[v]);
} }
valid valid
} }
@ -152,7 +158,7 @@ pub fn validate_token(
validation.validate_exp = false; validation.validate_exp = false;
validation.validate_nbf = false; validation.validate_nbf = false;
// Verify the JWT token // Verify the JWT token
decode::<JWTClaims>(&body, pub_key.as_slice(), &validation) decode::<JWTClaims>(&body, &DecodingKey::from_rsa_der(&pub_key), &validation)
.map(|tok| tok.claims) .map(|tok| tok.claims)
.map_err(|err| { .map_err(|err| {
format!( format!(
@ -163,6 +169,7 @@ pub fn validate_token(
} }
pub fn handle(service: &WebService, _req: Request) -> InternalFuture { pub fn handle(service: &WebService, _req: Request) -> InternalFuture {
info!("Handling authentication");
let framework = service let framework = service
.framework .framework
.read() .read()
@ -185,14 +192,19 @@ pub fn handle(service: &WebService, _req: Request) -> InternalFuture {
_req.body() _req.body()
.concat2() .concat2()
.map(move |body| { .map(move |body| {
let req = form_urlencoded::parse(body.as_ref()) let req = serde_json::from_slice::<AuthRequest>(&body);
.into_owned() if req.is_err() {
.collect::<HashMap<String, String>>(); 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 // Determine which credentials we should use
let (username, token) = { let (username, token) = {
let req_username = req.get("username").log_expect("No username in request"); let req_username = req.username;
let req_token = req.get("token").log_expect("No token in request"); let req_token = req.token;
// if the user didn't provide credentials, and theres nothing stored in the // if the user didn't provide credentials, and theres nothing stored in the
// database, return an early error // database, return an early error

View File

@ -2,20 +2,21 @@
//! //!
//! Launches the user's web browser on request from the frontend. //! Launches the user's web browser on request from the frontend.
use frontend::rest::services::Future as InternalFuture; use crate::frontend::rest::services::Future as InternalFuture;
use frontend::rest::services::{Request, Response, WebService}; use crate::frontend::rest::services::{Request, Response, WebService};
use crate::logging::LoggingErrors;
use futures::{Future, Stream}; use futures::{Future, Stream};
use hyper::header::ContentType; use hyper::header::ContentType;
use logging::LoggingErrors;
use std::collections::HashMap; #[derive(Debug, Serialize, Deserialize, Clone)]
use url::form_urlencoded; struct OpenRequest {
url: String,
}
pub fn handle(_service: &WebService, req: Request) -> InternalFuture { pub fn handle(_service: &WebService, req: Request) -> InternalFuture {
Box::new(req.body().concat2().map(move |body| { Box::new(req.body().concat2().map(move |body| {
let req = form_urlencoded::parse(body.as_ref()) let req: OpenRequest = serde_json::from_slice(&body).log_expect("Malformed request");
.into_owned() if webbrowser::open(&req.url).is_ok() {
.collect::<HashMap<String, String>>();
if webbrowser::open(req.get("url").log_expect("No URL to launch")).is_ok() {
Response::new() Response::new()
.with_status(hyper::Ok) .with_status(hyper::Ok)
.with_header(ContentType::json()) .with_header(ContentType::json())

View File

@ -4,18 +4,18 @@
//! //!
//! This endpoint should be usable directly from a <script> tag during loading. //! This endpoint should be usable directly from a <script> tag during loading.
use frontend::rest::services::Future; use crate::frontend::rest::services::Future;
use frontend::rest::services::Request; use crate::frontend::rest::services::Request;
use frontend::rest::services::Response; use crate::frontend::rest::services::Response;
use frontend::rest::services::WebService; use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType}; 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::stream::Stream;
use futures::Future as _; use futures::Future as _;

View File

@ -2,17 +2,17 @@
//! //!
//! This call returns if dark mode is enabled on the system currently. //! This call returns if dark mode is enabled on the system currently.
use frontend::rest::services::default_future; use crate::frontend::rest::services::default_future;
use frontend::rest::services::Future; use crate::frontend::rest::services::Future;
use frontend::rest::services::Request; use crate::frontend::rest::services::Request;
use frontend::rest::services::Response; use crate::frontend::rest::services::Response;
use frontend::rest::services::WebService; use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType}; 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 { pub fn handle(_service: &WebService, _req: Request) -> Future {
let file = serde_json::to_string(&is_dark_mode_active()) let file = serde_json::to_string(&is_dark_mode_active())

View File

@ -2,15 +2,15 @@
//! //!
//! The /api/default-path returns the default path for the application to install into. //! The /api/default-path returns the default path for the application to install into.
use frontend::rest::services::default_future; use crate::frontend::rest::services::default_future;
use frontend::rest::services::Future; use crate::frontend::rest::services::Future;
use frontend::rest::services::Request; use crate::frontend::rest::services::Request;
use frontend::rest::services::Response; use crate::frontend::rest::services::Response;
use frontend::rest::services::WebService; use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType}; 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. /// Struct used by serde to send a JSON payload to the client containing an optional value.
#[derive(Serialize)] #[derive(Serialize)]

View File

@ -2,15 +2,18 @@
//! //!
//! The /api/exit closes down the application. //! The /api/exit closes down the application.
use frontend::rest::services::Future as InternalFuture; use crate::frontend::rest::services::default_future;
use frontend::rest::services::{default_future, Request, Response, 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::ContentType; use hyper::header::ContentType;
use hyper::StatusCode; use hyper::StatusCode;
use std::process::exit; use std::process::exit;
pub fn handle(service: &WebService, _req: Request) -> InternalFuture { pub fn handle(service: &WebService, _req: Request) -> Future {
match service.get_framework_write().shutdown() { match service.get_framework_write().shutdown() {
Ok(_) => { Ok(_) => {
exit(0); exit(0);

View File

@ -2,14 +2,14 @@
//! //!
//! The /api/install call installs a set of packages dictated by a POST request. //! The /api/install call installs a set of packages dictated by a POST request.
use frontend::rest::services::stream_progress; use crate::frontend::rest::services::stream_progress;
use frontend::rest::services::Future; use crate::frontend::rest::services::Future;
use frontend::rest::services::Request; use crate::frontend::rest::services::Request;
use frontend::rest::services::WebService; 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::future::Future as _;
use futures::stream::Stream; use futures::stream::Stream;
@ -28,6 +28,7 @@ pub fn handle(service: &WebService, req: Request) -> Future {
let mut to_install = Vec::new(); let mut to_install = Vec::new();
let mut path: Option<String> = None; let mut path: Option<String> = None;
let mut force_install = false;
let mut install_desktop_shortcut = false; let mut install_desktop_shortcut = false;
// Transform results into just an array of stuff to install // Transform results into just an array of stuff to install
@ -41,11 +42,29 @@ pub fn handle(service: &WebService, req: Request) -> Future {
continue; continue;
} }
if key == "mode" && value == "force" {
force_install = true;
continue;
}
if value == "true" { if value == "true" {
to_install.push(key.to_owned()); to_install.push(key.to_owned());
} }
} }
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 // The frontend always provides this
let path = let path =
path.log_expect("No path specified by frontend when one should have already existed"); path.log_expect("No path specified by frontend when one should have already existed");
@ -60,7 +79,13 @@ pub fn handle(service: &WebService, req: Request) -> Future {
framework.set_install_dir(&path); framework.set_install_dir(&path);
} }
if let Err(v) = framework.install(to_install, &sender, new_install, install_desktop_shortcut) { if let Err(v) = framework.install(
to_install,
&sender,
new_install,
install_desktop_shortcut,
force_install,
) {
error!("Install error occurred: {:?}", v); error!("Install error occurred: {:?}", v);
if let Err(v) = sender.send(InstallMessage::Error(v)) { if let Err(v) = sender.send(InstallMessage::Error(v)) {
error!("Failed to send install error: {:?}", v); error!("Failed to send install error: {:?}", v);

View File

@ -5,15 +5,15 @@
//! //!
//! e.g. if the application is in maintenance mode //! e.g. if the application is in maintenance mode
use frontend::rest::services::default_future; use crate::frontend::rest::services::default_future;
use frontend::rest::services::Future; use crate::frontend::rest::services::Future;
use frontend::rest::services::Request; use crate::frontend::rest::services::Request;
use frontend::rest::services::Response; use crate::frontend::rest::services::Response;
use frontend::rest::services::WebService; use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType}; use hyper::header::{ContentLength, ContentType};
use logging::LoggingErrors; use crate::logging::LoggingErrors;
pub fn handle(service: &WebService, _req: Request) -> Future { pub fn handle(service: &WebService, _req: Request) -> Future {
let framework = service.get_framework_read(); let framework = service.get_framework_read();

View File

@ -4,12 +4,12 @@
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
use installer::{InstallMessage, InstallerFramework}; use crate::installer::{InstallMessage, InstallerFramework};
use hyper::server::Service; use hyper::server::Service;
use hyper::{Method, StatusCode}; use hyper::{Method, StatusCode};
use logging::LoggingErrors; use crate::logging::LoggingErrors;
use std::sync::mpsc::{channel, Sender}; use std::sync::mpsc::{channel, Sender};
@ -33,6 +33,8 @@ mod packages;
mod static_files; mod static_files;
mod uninstall; mod uninstall;
mod update_updater; mod update_updater;
mod verify_path;
mod view_folder;
/// Expected incoming Request format from Hyper. /// Expected incoming Request format from Hyper.
pub type Request = hyper::server::Request; pub type Request = hyper::server::Request;
@ -136,14 +138,16 @@ impl Service for WebService {
(Method::Get, "/api/config") => config::handle(self, req), (Method::Get, "/api/config") => config::handle(self, req),
(Method::Get, "/api/dark-mode") => dark_mode::handle(self, req), (Method::Get, "/api/dark-mode") => dark_mode::handle(self, req),
(Method::Get, "/api/default-path") => default_path::handle(self, req), (Method::Get, "/api/default-path") => default_path::handle(self, req),
(Method::Post, "/api/exit") => exit::handle(self, req), (Method::Get, "/api/exit") => exit::handle(self, req),
(Method::Get, "/api/packages") => packages::handle(self, req), (Method::Get, "/api/packages") => packages::handle(self, req),
(Method::Get, "/api/installation-status") => installation_status::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/check-auth") => authentication::handle(self, req),
(Method::Post, "/api/start-install") => install::handle(self, req), (Method::Post, "/api/start-install") => install::handle(self, req),
(Method::Post, "/api/open-browser") => browser::handle(self, req), (Method::Post, "/api/open-browser") => browser::handle(self, req),
(Method::Post, "/api/uninstall") => uninstall::handle(self, req), (Method::Post, "/api/uninstall") => uninstall::handle(self, req),
(Method::Post, "/api/update-updater") => update_updater::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), (Method::Get, _) => static_files::handle(self, req),
e => { e => {
info!("Returned 404 for {:?}", e); info!("Returned 404 for {:?}", e);

View File

@ -2,16 +2,16 @@
//! //!
//! The /api/packages call returns all the currently installed packages. //! The /api/packages call returns all the currently installed packages.
use frontend::rest::services::default_future; use crate::frontend::rest::services::default_future;
use frontend::rest::services::encapsulate_json; use crate::frontend::rest::services::encapsulate_json;
use frontend::rest::services::Future; use crate::frontend::rest::services::Future;
use frontend::rest::services::Request; use crate::frontend::rest::services::Request;
use frontend::rest::services::Response; use crate::frontend::rest::services::Response;
use frontend::rest::services::WebService; use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType}; use hyper::header::{ContentLength, ContentType};
use logging::LoggingErrors; use crate::logging::LoggingErrors;
pub fn handle(service: &WebService, _req: Request) -> Future { pub fn handle(service: &WebService, _req: Request) -> Future {
let framework = service.get_framework_read(); let framework = service.get_framework_read();

View File

@ -4,18 +4,18 @@
//! //!
//! e.g. index.html, main.js, ... //! e.g. index.html, main.js, ...
use frontend::rest::assets; use crate::frontend::rest::assets;
use frontend::rest::services::default_future; use crate::frontend::rest::services::default_future;
use frontend::rest::services::Future; use crate::frontend::rest::services::Future;
use frontend::rest::services::Request; use crate::frontend::rest::services::Request;
use frontend::rest::services::Response; use crate::frontend::rest::services::Response;
use frontend::rest::services::WebService; use crate::frontend::rest::services::WebService;
use hyper::header::{ContentLength, ContentType}; use hyper::header::{ContentLength, ContentType};
use hyper::StatusCode; use hyper::StatusCode;
use logging::LoggingErrors; use crate::logging::LoggingErrors;
pub fn handle(_service: &WebService, req: Request) -> Future { pub fn handle(_service: &WebService, req: Request) -> Future {
// At this point, we have a web browser client. Search for a index page // At this point, we have a web browser client. Search for a index page

View File

@ -2,15 +2,15 @@
//! //!
//! The /api/uninstall call uninstalls all packages. //! The /api/uninstall call uninstalls all packages.
use frontend::rest::services::default_future; use crate::frontend::rest::services::default_future;
use frontend::rest::services::stream_progress; use crate::frontend::rest::services::stream_progress;
use frontend::rest::services::Future; use crate::frontend::rest::services::Future;
use frontend::rest::services::Request; use crate::frontend::rest::services::Request;
use frontend::rest::services::WebService; 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 { pub fn handle(service: &WebService, _req: Request) -> Future {
let framework = service.framework.clone(); let framework = service.framework.clone();

View File

@ -2,15 +2,15 @@
//! //!
//! The /api/update-updater call attempts to update the currently running updater. //! The /api/update-updater call attempts to update the currently running updater.
use frontend::rest::services::default_future; use crate::frontend::rest::services::default_future;
use frontend::rest::services::stream_progress; use crate::frontend::rest::services::stream_progress;
use frontend::rest::services::Future; use crate::frontend::rest::services::Future;
use frontend::rest::services::Request; use crate::frontend::rest::services::Request;
use frontend::rest::services::WebService; 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 { pub fn handle(service: &WebService, _req: Request) -> Future {
let framework = service.framework.clone(); let framework = service.framework.clone();

View 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)
}))
}

View 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),
)
}

View File

@ -2,70 +2,85 @@
//! //!
//! Provides a web-view UI. //! Provides a web-view UI.
use web_view::Content; use anyhow::Result;
use wry::{
use logging::LoggingErrors; application::{
dpi::LogicalSize,
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::{Icon, WindowBuilder},
},
webview::{RpcResponse, WebViewBuilder},
};
use log::Level; use log::Level;
#[derive(Deserialize, Debug)] use crate::logging::LoggingErrors;
enum CallbackType {
SelectInstallDir { callback_name: String }, const ICON_DATA: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/icon-data.bin"));
Log { msg: String, kind: String },
Test {},
}
/// Starts the main web UI. Will return when UI is closed. /// Starts the main web UI. Will return when UI is closed.
pub fn start_ui(app_name: &str, http_address: &str, _is_launcher: bool) { pub fn start_ui(app_name: &str, http_address: &str, is_launcher: bool) -> Result<()> {
let size = (1024, 550); #[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"); info!("Spawning web view instance");
web_view::builder() let window_icon =
.title(&format!("{} Installer", app_name)) Icon::from_rgba(ICON_DATA.to_vec(), 48, 48).log_expect("Unable to construct window icon");
.content(Content::Url(http_address)) let event_loop = EventLoop::new();
.size(size.0, size.1) let window = WindowBuilder::new()
.resizable(false) .with_title(format!("{} Installer", app_name))
.debug(false) .with_window_icon(Some(window_icon))
.user_data(()) .with_inner_size(LogicalSize::new(size.0, size.1))
.invoke_handler(|wv, msg| { .with_resizable(false)
let mut cb_result = Ok(()); .build(&event_loop)?;
let command: CallbackType = let _webview = WebViewBuilder::new(window)?
serde_json::from_str(msg).log_expect(&format!("Unable to parse string: {:?}", msg)); .with_url(http_address)?
.with_rpc_handler(|_, mut event| {
debug!("Incoming payload: {:?}", command); debug!("Incoming payload: {:?}", event);
match event.method.as_str() {
match command { "Test" => (),
CallbackType::SelectInstallDir { callback_name } => { "Log" => {
let result = wv if let Some(msg) = event.params.take() {
.dialog() if let Ok(msg) = serde_json::from_value::<(String, String)>(msg) {
.choose_directory("Select a install directory...", ""); let kind = match msg.0.as_str() {
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);
}
}
}
CallbackType::Log { msg, kind } => {
let kind = match kind.as_ref() {
"info" | "log" => Level::Info, "info" | "log" => Level::Info,
"warn" => Level::Warn, "warn" => Level::Warn,
"error" => Level::Error,
_ => Level::Error, _ => Level::Error,
}; };
log!(target: "liftinstall::frontend::js", kind, "{}", msg.1);
log!(target: "liftinstall::frontend::js", kind, "{}", msg);
} }
CallbackType::Test {} => {}
} }
}
cb_result "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));
}
_ => warn!("Unknown RPC method: {}", event.method),
}
None
}) })
.run() .build()?;
.log_expect("Unable to launch Web UI!");
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,
_ => (),
}
});
} }

View File

@ -7,7 +7,7 @@ use reqwest::header::CONTENT_LENGTH;
use std::io::Read; use std::io::Read;
use std::time::Duration; use std::time::Duration;
use reqwest::async::Client as AsyncClient; use reqwest::r#async::Client as AsyncClient;
use reqwest::Client; use reqwest::Client;
use reqwest::StatusCode; use reqwest::StatusCode;

View File

@ -21,28 +21,29 @@ use std::io::Cursor;
use std::process::Command; use std::process::Command;
use std::process::{exit, Stdio}; use std::process::{exit, Stdio};
use config::BaseAttributes; use crate::config::BaseAttributes;
use config::Config; use crate::config::Config;
use sources::types::Version; use crate::sources::types::Version;
use tasks::install::InstallTask; use crate::tasks::install::InstallTask;
use tasks::uninstall::UninstallTask; use crate::tasks::uninstall::UninstallTask;
use tasks::uninstall_global_shortcut::UninstallGlobalShortcutsTask; use crate::tasks::uninstall_global_shortcut::UninstallGlobalShortcutsTask;
use tasks::DependencyTree; use crate::tasks::DependencyTree;
use tasks::TaskMessage; use crate::tasks::TaskMessage;
use logging::LoggingErrors; use crate::logging::LoggingErrors;
use dirs::home_dir; use dirs::home_dir;
use std::collections::HashSet;
use std::fs::remove_file; 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. /// A message thrown during the installation of packages.
#[derive(Serialize)] #[derive(Serialize)]
@ -118,7 +119,7 @@ pub struct LocalInstallation {
/// Relative paths to generated files /// Relative paths to generated files
pub files: Vec<String>, pub files: Vec<String>,
/// Absolute paths to generated shortcut files /// Absolute paths to generated shortcut files
pub shortcuts: Vec<String>, pub shortcuts: HashSet<String>,
} }
macro_rules! declare_messenger_callback { macro_rules! declare_messenger_callback {
@ -175,12 +176,14 @@ impl InstallerFramework {
/// items: Array of named packages to be installed/kept /// items: Array of named packages to be installed/kept
/// messages: Channel used to send progress messages /// messages: Channel used to send progress messages
/// fresh_install: If the install directory must be empty /// fresh_install: If the install directory must be empty
/// force_install: If the install directory should be erased first
pub fn install( pub fn install(
&mut self, &mut self,
items: Vec<String>, items: Vec<String>,
messages: &Sender<InstallMessage>, messages: &Sender<InstallMessage>,
fresh_install: bool, fresh_install: bool,
create_desktop_shortcuts: bool, create_desktop_shortcuts: bool,
force_install: bool,
) -> Result<(), String> { ) -> Result<(), String> {
info!( info!(
"Framework: Installing {:?} to {:?}", "Framework: Installing {:?} to {:?}",
@ -210,6 +213,7 @@ impl InstallerFramework {
uninstall_items, uninstall_items,
fresh_install, fresh_install,
create_desktop_shortcuts, create_desktop_shortcuts,
force_install,
}); });
let mut tree = DependencyTree::build(task); let mut tree = DependencyTree::build(task);
@ -458,6 +462,24 @@ impl InstallerFramework {
} }
} }
/// 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),
}
}
/// Creates a new instance of the Installer Framework with a specified Config, managing /// Creates a new instance of the Installer Framework with a specified Config, managing
/// a pre-existing installation. /// a pre-existing installation.
pub fn new_with_db(attrs: BaseAttributes, install_path: &Path) -> Result<Self, String> { pub fn new_with_db(attrs: BaseAttributes, install_path: &Path) -> Result<Self, String> {

View File

@ -7,7 +7,7 @@
#![deny(unsafe_code)] #![deny(unsafe_code)]
#![deny(missing_docs)] #![deny(missing_docs)]
extern crate web_view; extern crate wry;
extern crate futures; extern crate futures;
extern crate hyper; extern crate hyper;
@ -71,10 +71,10 @@ use clap::App;
use clap::Arg; use clap::Arg;
use config::BaseAttributes; use config::BaseAttributes;
use std::process::{Command, Stdio, exit};
use std::fs; 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() { fn main() {
let config = BaseAttributes::from_toml_str(RAW_CONFIG).expect("Config file could not be read"); let config = BaseAttributes::from_toml_str(RAW_CONFIG).expect("Config file could not be read");
@ -109,6 +109,7 @@ fn main() {
info!("{} installer", app_name); info!("{} installer", app_name);
// Handle self-updating if needed
let current_exe = std::env::current_exe().log_expect("Current executable could not be found"); let current_exe = std::env::current_exe().log_expect("Current executable could not be found");
let current_path = current_exe let current_path = current_exe
.parent() .parent()
@ -126,7 +127,11 @@ fn main() {
let metadata_file = current_path.join("metadata.json"); let metadata_file = current_path.join("metadata.json");
let mut framework = if metadata_file.exists() { let mut framework = if metadata_file.exists() {
info!("Using pre-existing metadata file: {:?}", metadata_file); 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 { } else {
info!("Starting fresh install"); info!("Starting fresh install");
fresh_install = true; fresh_install = true;
@ -171,12 +176,17 @@ fn replace_existing_install(current_exe: &PathBuf, installed_path: &PathBuf) ->
return Err(format!("Unable to copy installer binary: {:?}", v)); return Err(format!("Unable to copy installer binary: {:?}", v));
} }
let existing = installed_path.join(platform_extension).into_os_string().into_string(); let existing = installed_path
.join(platform_extension)
.into_os_string()
.into_string();
let new = installed_path.join(new_tool).into_os_string().into_string(); let new = installed_path.join(new_tool).into_os_string().into_string();
if existing.is_ok() && new.is_ok() { if existing.is_ok() && new.is_ok() {
// Remove NTFS alternate stream which tells the operating system that the updater was downloaded from the internet // Remove NTFS alternate stream which tells the operating system that the updater was downloaded from the internet
if cfg!(windows) { if cfg!(windows) {
let _ = fs::remove_file(installed_path.join("maintenancetool_new.exe:Zone.Identifier:$DATA")); let _ = fs::remove_file(
installed_path.join("maintenancetool_new.exe:Zone.Identifier:$DATA"),
);
} }
info!("Launching {:?}", existing); info!("Launching {:?}", existing);
let success = Command::new(new.unwrap()) let success = Command::new(new.unwrap())

View File

@ -46,7 +46,7 @@ extern "C" int saveShortcut(
const wchar_t *workingDir, const wchar_t *workingDir,
const wchar_t *exePath) const wchar_t *exePath)
{ {
const char *errStr = NULL; char *errStr = NULL;
HRESULT h; HRESULT h;
IShellLink *shellLink = NULL; IShellLink *shellLink = NULL;
IPersistFile *persistFile = NULL; IPersistFile *persistFile = NULL;

View File

@ -14,10 +14,14 @@ mod natives {
#![allow(non_snake_case)] #![allow(non_snake_case)]
const PROCESS_LEN: usize = 10192; 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::env;
use std::io::Write;
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use winapi::shared::minwindef::{DWORD, FALSE, MAX_PATH}; use winapi::shared::minwindef::{DWORD, FALSE, MAX_PATH};
@ -26,11 +30,17 @@ mod natives {
use winapi::um::psapi::{ use winapi::um::psapi::{
EnumProcessModulesEx, GetModuleFileNameExW, K32EnumProcesses, LIST_MODULES_ALL, EnumProcessModulesEx, GetModuleFileNameExW, K32EnumProcesses, LIST_MODULES_ALL,
}; };
use winapi::um::shellapi::ShellExecuteW;
use winapi::um::winnt::{ use winapi::um::winnt::{
HANDLE, PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE, PROCESS_VM_READ, HANDLE, PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE, PROCESS_VM_READ,
}; };
use winapi::um::winuser::SW_SHOWDEFAULT;
use widestring::{U16CString}; use std::process::Command;
use tempfile::Builder;
use tinyfiledialogs::{message_box_yes_no, MessageBoxIcon, YesNo};
use webview2::EnvironmentBuilder;
use widestring::U16CString;
extern "C" { extern "C" {
pub fn saveShortcut( pub fn saveShortcut(
@ -54,24 +64,32 @@ mod natives {
pub fn getDesktopFolder(out_path: *mut ::std::os::raw::c_ushort) -> HRESULT; pub fn getDesktopFolder(out_path: *mut ::std::os::raw::c_ushort) -> HRESULT;
} }
// Needed here for Windows interop pub fn prepare_install_webview2(name: &str) -> Result<(), String> {
#[allow(unsafe_code)] if EnvironmentBuilder::default()
pub fn create_desktop_shortcut( .get_available_browser_version_string()
name: &str, .is_ok()
description: &str, {
target: &str, return Ok(());
args: &str, }
working_dir: &str, 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 {
exe_path: &str, std::process::exit(1);
) -> Result<String, String> { }
let mut cmd_path = [0u16; MAX_PATH + 1]; let mut installer_file = Builder::new()
let _result = unsafe { getDesktopFolder(cmd_path.as_mut_ptr()) }; .suffix(".exe")
let source_path = format!( .tempfile()
"{}\\{}.lnk", .log_expect("Unable to open the webview2 installer file");
String::from_utf16_lossy(&cmd_path[..count_u16(&cmd_path)]).as_str(), installer_file
name .write_all(&WV2_INSTALLER_DATA)
); .log_expect("Unable to write the webview2 installer file");
create_shortcut_inner(source_path, name, description, target, args, working_dir, exe_path) 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 // Needed here for Windows interop
@ -89,7 +107,43 @@ mod natives {
env::var("APPDATA").log_expect("APPDATA is bad, apparently"), env::var("APPDATA").log_expect("APPDATA is bad, apparently"),
name name
); );
create_shortcut_inner(source_file, name, description, target, args, working_dir, exe_path) 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 // Needed here for Windows interop
@ -103,7 +157,6 @@ mod natives {
working_dir: &str, working_dir: &str,
exe_path: &str, exe_path: &str,
) -> Result<String, String> { ) -> Result<String, String> {
info!("Generating shortcut @ {:?}", source_file); info!("Generating shortcut @ {:?}", source_file);
let native_target_dir = U16CString::from_str(source_file.clone()) let native_target_dir = U16CString::from_str(source_file.clone())
@ -139,6 +192,26 @@ 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 { fn count_u16(u16str: &[u16]) -> usize {
let mut pos = 0; let mut pos = 0;
for x in u16str.iter() { for x in u16str.iter() {
@ -163,6 +236,7 @@ mod natives {
.to_str() .to_str()
.log_expect("Unable to convert tool path to string") .log_expect("Unable to convert tool path to string")
.replace(" ", "\\ "); .replace(" ", "\\ ");
let tool_wv = format!("{}.WebView2", tool);
let log = path.join(format!("{}_installer.log", app_name)); let log = path.join(format!("{}_installer.log", app_name));
let log = log let log = log
@ -170,7 +244,15 @@ mod natives {
.log_expect("Unable to convert log path to string") .log_expect("Unable to convert log path to string")
.replace(" ", "\\ "); .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); info!("Launching cmd with {:?}", target_arguments);
@ -179,13 +261,20 @@ mod natives {
let spawn_result: i32 = unsafe { let spawn_result: i32 = unsafe {
let mut cmd_path = [0u16; MAX_PATH + 1]; let mut cmd_path = [0u16; MAX_PATH + 1];
let result = getSystemFolder(cmd_path.as_mut_ptr()); let result = getSystemFolder(cmd_path.as_mut_ptr());
let mut pos = 0;
for x in cmd_path.iter() {
if *x == 0 {
break;
}
pos += 1;
}
if result != winapi::shared::winerror::S_OK { if result != winapi::shared::winerror::S_OK {
return; return;
} }
spawnDetached( spawnDetached(
U16CString::from_str( U16CString::from_str(
format!("{}\\cmd.exe", String::from_utf16_lossy(&cmd_path[..count_u16(&cmd_path)])).as_str(), format!("{}\\cmd.exe", String::from_utf16_lossy(&cmd_path[..pos])).as_str(),
) )
.log_expect("Unable to convert string to wchar_t") .log_expect("Unable to convert string to wchar_t")
.as_ptr(), .as_ptr(),
@ -293,19 +382,21 @@ mod natives {
#[cfg(not(windows))] #[cfg(not(windows))]
mod natives { mod natives {
use std::fs::remove_file; use std::fs::{remove_dir, remove_file};
use std::env; use std::env;
use logging::LoggingErrors; use crate::logging::LoggingErrors;
use sysinfo::{ProcessExt, SystemExt}; use sysinfo::{PidExt, ProcessExt, SystemExt};
use dirs; use dirs;
use slug::slugify; use slug::slugify;
use std::fs::{create_dir_all, File}; use std::fs::{create_dir_all, File};
use std::io::Write; use std::io::Write;
use std::path::Path;
use std::process::Command;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub fn create_shortcut( pub fn create_shortcut(
@ -323,7 +414,7 @@ mod natives {
let mut path = x; let mut path = x;
path.push("applications"); path.push("applications");
match create_dir_all(path.to_path_buf()) { match create_dir_all(path.to_path_buf()) {
Ok(_) => (()), Ok(_) => (),
Err(e) => { Err(e) => {
return Err(format!( return Err(format!(
"Local data directory does not exist and cannot be created: {}", "Local data directory does not exist and cannot be created: {}",
@ -341,7 +432,7 @@ mod natives {
Ok(file) => file, Ok(file) => file,
Err(e) => return Err(format!("Unable to create desktop file: {}", e)), 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 { match desktop_f {
Ok(_) => Ok("".to_string()), Ok(_) => Ok("".to_string()),
Err(e) => Err(format!("Unable to write desktop file: {}", e)), Err(e) => Err(format!("Unable to write desktop file: {}", e)),
@ -359,27 +450,44 @@ mod natives {
target: &str, target: &str,
args: &str, args: &str,
working_dir: &str, working_dir: &str,
_exe_path: &str,
) -> Result<String, String> { ) -> Result<String, String> {
warn!("STUB! Creating shortcut is not implemented on macOS"); warn!("STUB! Creating shortcut is not implemented on macOS");
Ok("".to_string()) 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 /// Cleans up the installer
pub fn burn_on_exit(app_name: &str) { pub fn burn_on_exit(app_name: &str) {
let current_exe = env::current_exe().log_expect("Current executable could not be found"); 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 // Thank god for *nix platforms
if let Err(e) = remove_file(&current_exe) { if let Err(e) = remove_file(&current_exe) {
// No regular logging now. // No regular logging now.
eprintln!("Failed to delete maintenancetool: {:?}", e); eprintln!("Failed to delete maintenancetool: {:?}", e);
}; };
// delete the directory if not empty and ignore errors (since we can't handle errors anymore)
let current_dir = env::current_dir().log_expect("Current directory cannot be found"); remove_dir(exe_dir).ok();
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);
};
} }
/// Returns a list of running processes /// Returns a list of running processes
@ -388,9 +496,9 @@ mod natives {
let mut processes: Vec<super::Process> = Vec::new(); let mut processes: Vec<super::Process> = Vec::new();
let mut system = sysinfo::System::new(); let mut system = sysinfo::System::new();
system.refresh_all(); system.refresh_all();
for (pid, procs) in system.get_process_list() { for (pid, procs) in system.processes() {
processes.push(super::Process { processes.push(super::Process {
pid: *pid as usize, pid: pid.as_u32() as usize,
name: procs.name().to_string(), name: procs.name().to_string(),
}); });
} }

View File

@ -9,7 +9,7 @@ use std::{thread, time};
use clap::{App, ArgMatches}; use clap::{App, ArgMatches};
use logging::LoggingErrors; use crate::logging::LoggingErrors;
/// Swaps around the main executable if needed. /// Swaps around the main executable if needed.
pub fn perform_swap(current_exe: &PathBuf, to_path: Option<&str>) { pub fn perform_swap(current_exe: &PathBuf, to_path: Option<&str>) {

View File

@ -7,9 +7,9 @@ use reqwest::StatusCode;
use serde_json; use serde_json;
use sources::types::*; use crate::sources::types::*;
use http::build_client; use crate::http::build_client;
pub struct GithubReleases {} pub struct GithubReleases {}

View File

@ -2,10 +2,10 @@
//! //!
//! Contains the yuzu-emu core API implementation of a release source. //! Contains the yuzu-emu core API implementation of a release source.
use http::build_client; use crate::http::build_client;
use crate::sources::types::*;
use reqwest::header::USER_AGENT; use reqwest::header::USER_AGENT;
use reqwest::StatusCode; use reqwest::StatusCode;
use sources::types::*;
pub struct PatreonReleases {} pub struct PatreonReleases {}

View File

@ -23,9 +23,7 @@ impl Version {
fn coarse_into_semver(&self) -> SemverVersion { fn coarse_into_semver(&self) -> SemverVersion {
match *self { match *self {
Version::Semver(ref version) => version.to_owned(), Version::Semver(ref version) => version.to_owned(),
Version::Integer(ref version) => { Version::Integer(ref version) => SemverVersion::new(version.to_owned(), 0u64, 0u64),
SemverVersion::from((version.to_owned(), 0 as u64, 0 as u64))
}
} }
} }

View File

@ -1,13 +1,13 @@
//! Validates that users have correct authorization to download packages. //! Validates that users have correct authorization to download packages.
use frontend::rest::services::authentication; use crate::frontend::rest::services::authentication;
use installer::InstallerFramework; use crate::installer::InstallerFramework;
use logging::LoggingErrors; use crate::logging::LoggingErrors;
use tasks::resolver::ResolvePackageTask; use crate::tasks::resolver::ResolvePackageTask;
use tasks::{Task, TaskDependency, TaskMessage, TaskOrdering, TaskParamType}; use crate::tasks::{Task, TaskDependency, TaskMessage, TaskOrdering, TaskParamType};
pub struct CheckAuthorizationTask { pub struct CheckAuthorizationTask {
pub name: String, pub name: String,
@ -22,7 +22,9 @@ impl Task for CheckAuthorizationTask {
) -> Result<TaskParamType, String> { ) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 1); assert_eq!(input.len(), 1);
let params = input.pop().log_expect("Check Authorization Task should have input from resolver!"); let params = input
.pop()
.log_expect("Check Authorization Task should have input from resolver!");
let (version, file) = match params { let (version, file) = match params {
TaskParamType::File(v, f) => Ok((v, f)), TaskParamType::File(v, f) => Ok((v, f)),
_ => Err("Unexpected TaskParamType in CheckAuthorization: {:?}"), _ => Err("Unexpected TaskParamType in CheckAuthorization: {:?}"),

View File

@ -1,15 +1,19 @@
//! Downloads a package into memory. //! Downloads a package into memory.
use installer::InstallerFramework; use crate::installer::InstallerFramework;
use tasks::check_authorization::CheckAuthorizationTask; use crate::tasks::check_authorization::CheckAuthorizationTask;
use tasks::{Task, TaskDependency, TaskMessage, TaskOrdering, TaskParamType}; use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskOrdering;
use crate::tasks::TaskParamType;
use http::stream_file; use crate::http::stream_file;
use number_prefix::{NumberPrefix, Prefixed, Standalone}; use number_prefix::NumberPrefix::{self, Prefixed, Standalone};
use logging::LoggingErrors; use crate::logging::LoggingErrors;
pub struct DownloadPackageTask { pub struct DownloadPackageTask {
pub name: String, pub name: String,
@ -24,7 +28,9 @@ impl Task for DownloadPackageTask {
) -> Result<TaskParamType, String> { ) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 1); assert_eq!(input.len(), 1);
let file = input.pop().log_expect("Download Package Task should have input from resolver!"); let file = input
.pop()
.log_expect("Download Package Task should have input from resolver!");
let (version, file, auth) = match file { let (version, file, auth) = match file {
TaskParamType::Authentication(v, f, auth) => (v, f, auth), TaskParamType::Authentication(v, f, auth) => (v, f, auth),
_ => return Err("Unexpected param type to download package".to_string()), _ => return Err("Unexpected param type to download package".to_string()),

View File

@ -1,14 +1,14 @@
//! Verifies that this is the only running instance of the installer, and that no application is running. //! 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 crate::tasks::Task;
use tasks::TaskDependency; use crate::tasks::TaskDependency;
use tasks::TaskMessage; use crate::tasks::TaskMessage;
use tasks::TaskParamType; use crate::tasks::TaskParamType;
use native::get_process_names; use crate::native::get_process_names;
use native::Process; use crate::native::Process;
use std::process; use std::process;

View File

@ -1,26 +1,29 @@
//! Overall hierarchy for installing a installation of the application. //! Overall hierarchy for installing a installation of the application.
use installer::InstallerFramework; use crate::installer::InstallerFramework;
use tasks::ensure_only_instance::EnsureOnlyInstanceTask; use crate::tasks::ensure_only_instance::EnsureOnlyInstanceTask;
use tasks::install_dir::VerifyInstallDirTask; use crate::tasks::install_dir::VerifyInstallDirTask;
use tasks::install_global_shortcut::InstallGlobalShortcutsTask; use crate::tasks::install_global_shortcut::InstallGlobalShortcutsTask;
use tasks::install_pkg::InstallPackageTask; use crate::tasks::install_pkg::InstallPackageTask;
use tasks::save_executable::SaveExecutableTask; use crate::tasks::launch_installed_on_exit::LaunchOnExitTask;
use tasks::uninstall_pkg::UninstallPackageTask; use crate::tasks::remove_target_dir::RemoveTargetDirTask;
use tasks::launch_installed_on_exit::LaunchOnExitTask; use crate::tasks::save_executable::SaveExecutableTask;
use crate::tasks::uninstall_pkg::UninstallPackageTask;
use tasks::Task; use crate::tasks::Task;
use tasks::TaskDependency; use crate::tasks::TaskDependency;
use tasks::TaskMessage; use crate::tasks::TaskMessage;
use tasks::TaskOrdering; use crate::tasks::TaskOrdering;
use tasks::TaskParamType; use crate::tasks::TaskParamType;
pub struct InstallTask { pub struct InstallTask {
pub items: Vec<String>, pub items: Vec<String>,
pub uninstall_items: Vec<String>, pub uninstall_items: Vec<String>,
pub fresh_install: bool, pub fresh_install: bool,
pub create_desktop_shortcuts: bool, pub create_desktop_shortcuts: bool,
// force_install: remove the target directory before installing
pub force_install: bool,
} }
impl Task for InstallTask { impl Task for InstallTask {
@ -42,6 +45,13 @@ impl Task for InstallTask {
Box::new(EnsureOnlyInstanceTask {}), Box::new(EnsureOnlyInstanceTask {}),
)); ));
if self.force_install {
elements.push(TaskDependency::build(
TaskOrdering::Pre,
Box::new(RemoveTargetDirTask {}),
));
}
elements.push(TaskDependency::build( elements.push(TaskDependency::build(
TaskOrdering::Pre, TaskOrdering::Pre,
Box::new(VerifyInstallDirTask { Box::new(VerifyInstallDirTask {
@ -62,7 +72,10 @@ impl Task for InstallTask {
for item in &self.items { for item in &self.items {
elements.push(TaskDependency::build( elements.push(TaskDependency::build(
TaskOrdering::Pre, TaskOrdering::Pre,
Box::new(InstallPackageTask { name: item.clone(), create_desktop_shortcuts: self.create_desktop_shortcuts }), Box::new(InstallPackageTask {
name: item.clone(),
create_desktop_shortcuts: self.create_desktop_shortcuts,
}),
)); ));
} }
@ -79,7 +92,7 @@ impl Task for InstallTask {
elements.push(TaskDependency::build( elements.push(TaskDependency::build(
TaskOrdering::Post, TaskOrdering::Post,
Box::new(LaunchOnExitTask {}) Box::new(LaunchOnExitTask {}),
)) ))
} }

View File

@ -1,20 +1,20 @@
//! Generates shortcuts for a specified file. //! Generates shortcuts for a specified file.
use installer::InstallerFramework; use crate::installer::InstallerFramework;
use tasks::Task; use crate::tasks::Task;
use tasks::TaskDependency; use crate::tasks::TaskDependency;
use tasks::TaskMessage; use crate::tasks::TaskMessage;
use tasks::TaskParamType; use crate::tasks::TaskParamType;
use config::PackageDescription; use crate::config::PackageDescription;
use logging::LoggingErrors; use crate::logging::LoggingErrors;
#[cfg(windows)] #[cfg(windows)]
use native::create_desktop_shortcut; use crate::native::create_desktop_shortcut;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use native::create_shortcut; use crate::native::create_shortcut;
pub struct InstallDesktopShortcutTask { pub struct InstallDesktopShortcutTask {
pub name: String, pub name: String,
@ -33,7 +33,10 @@ impl Task for InstallDesktopShortcutTask {
} }
messenger(&TaskMessage::DisplayMessage( messenger(&TaskMessage::DisplayMessage(
&format!("Generating desktop shortcuts for package {:?}...", self.name), &format!(
"Generating desktop shortcuts for package {:?}...",
self.name
),
0.0, 0.0,
)); ));
@ -110,7 +113,7 @@ impl Task for InstallDesktopShortcutTask {
let packages = &mut context.database.packages; let packages = &mut context.database.packages;
for pack in packages { for pack in packages {
if pack.name == self.name { if pack.name == self.name {
pack.shortcuts.append(&mut installed_files); pack.shortcuts.extend(installed_files.clone());
} }
} }
@ -122,6 +125,9 @@ impl Task for InstallDesktopShortcutTask {
} }
fn name(&self) -> String { fn name(&self) -> String {
format!("InstallDesktopShortcutTask (for {:?}, should_run = {:?})", self.name, self.should_run) format!(
"InstallDesktopShortcutTask (for {:?}, should_run = {:?})",
self.name, self.should_run
)
} }
} }

View File

@ -1,16 +1,16 @@
//! Verifies properties about the installation directory. //! Verifies properties about the installation directory.
use installer::InstallerFramework; use crate::installer::InstallerFramework;
use tasks::Task; use crate::tasks::Task;
use tasks::TaskDependency; use crate::tasks::TaskDependency;
use tasks::TaskMessage; use crate::tasks::TaskMessage;
use tasks::TaskParamType; use crate::tasks::TaskParamType;
use std::fs::create_dir_all; use std::fs::create_dir_all;
use std::fs::read_dir; use std::fs::read_dir;
use logging::LoggingErrors; use crate::logging::LoggingErrors;
pub struct VerifyInstallDirTask { pub struct VerifyInstallDirTask {
pub clean_install: bool, pub clean_install: bool,

View File

@ -1,17 +1,17 @@
//! Generates the global shortcut for this application. //! Generates the global shortcut for this application.
use installer::InstallerFramework; use crate::installer::InstallerFramework;
use tasks::Task; use crate::tasks::Task;
use tasks::TaskDependency; use crate::tasks::TaskDependency;
use tasks::TaskMessage; use crate::tasks::TaskMessage;
use tasks::TaskParamType; use crate::tasks::TaskParamType;
use logging::LoggingErrors; use crate::logging::LoggingErrors;
use native::create_shortcut; use crate::native::create_shortcut;
use tasks::save_database::SaveDatabaseTask; use crate::tasks::save_database::SaveDatabaseTask;
use tasks::TaskOrdering; use crate::tasks::TaskOrdering;
pub struct InstallGlobalShortcutsTask {} pub struct InstallGlobalShortcutsTask {}
@ -61,7 +61,7 @@ impl Task for InstallGlobalShortcutsTask {
"", "",
)?; )?;
if !shortcut_file.is_empty() { if !shortcut_file.is_empty() && !context.database.shortcuts.contains(&shortcut_file) {
context.database.shortcuts.push(shortcut_file); context.database.shortcuts.push(shortcut_file);
} }

View File

@ -1,30 +1,31 @@
//! Installs a specific package. //! Installs a specific package.
use installer::InstallerFramework; use crate::installer::InstallerFramework;
use tasks::download_pkg::DownloadPackageTask; use crate::tasks::download_pkg::DownloadPackageTask;
use tasks::install_shortcuts::InstallShortcutsTask; use crate::tasks::install_shortcuts::InstallShortcutsTask;
use tasks::save_database::SaveDatabaseTask; use crate::tasks::save_database::SaveDatabaseTask;
use tasks::uninstall_pkg::UninstallPackageTask; use crate::tasks::uninstall_pkg::UninstallPackageTask;
use tasks::Task; use crate::tasks::Task;
use tasks::TaskDependency; use crate::tasks::TaskDependency;
use tasks::TaskMessage; use crate::tasks::TaskMessage;
use tasks::TaskOrdering; use crate::tasks::TaskOrdering;
use tasks::TaskParamType; use crate::tasks::TaskParamType;
use config::PackageDescription; use crate::config::PackageDescription;
use installer::LocalInstallation; use crate::installer::LocalInstallation;
use std::fs::create_dir_all; use std::fs::create_dir_all;
use std::io::copy; 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::fs::OpenOptions;
use std::path::Path; use std::path::Path;
use tasks::install_desktop_shortcut::InstallDesktopShortcutTask;
pub struct InstallPackageTask { pub struct InstallPackageTask {
pub name: String, pub name: String,
@ -70,13 +71,18 @@ impl Task for InstallPackageTask {
// Ignore input from the uninstaller - no useful information passed // Ignore input from the uninstaller - no useful information passed
// If a previous task Breaks, then just early exit // If a previous task Breaks, then just early exit
match input.pop().log_expect("Install Package Task should have guaranteed output!") { match input
.pop()
.log_expect("Install Package Task should have guaranteed output!")
{
TaskParamType::Break => return Ok(TaskParamType::None), TaskParamType::Break => return Ok(TaskParamType::None),
_ => (), _ => (),
}; };
// Grab data from the resolver // Grab data from the resolver
let data = input.pop().log_expect("Install Package Task 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 { let (version, file, data) = match data {
TaskParamType::FileContents(version, file, data) => (version, file, data), TaskParamType::FileContents(version, file, data) => (version, file, data),
_ => return Err("Unexpected file contents param type to install package".to_string()), _ => return Err("Unexpected file contents param type to install package".to_string()),
@ -136,7 +142,7 @@ impl Task for InstallPackageTask {
info!("Creating file: {:?}", string_name); info!("Creating file: {:?}", string_name);
if !installed_files.contains(&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(); let mut file_metadata = OpenOptions::new();
@ -165,9 +171,9 @@ impl Task for InstallPackageTask {
// Save metadata about this package // Save metadata about this package
context.database.packages.push(LocalInstallation { context.database.packages.push(LocalInstallation {
name: package.name.to_owned(), name: package.name,
version, version,
shortcuts: Vec::new(), shortcuts: HashSet::new(),
files: installed_files, files: installed_files,
}); });
@ -201,7 +207,7 @@ impl Task for InstallPackageTask {
TaskOrdering::Post, TaskOrdering::Post,
Box::new(InstallDesktopShortcutTask { Box::new(InstallDesktopShortcutTask {
name: self.name.clone(), name: self.name.clone(),
should_run: self.create_desktop_shortcuts should_run: self.create_desktop_shortcuts,
}), }),
), ),
TaskDependency::build(TaskOrdering::Post, Box::new(SaveDatabaseTask {})), TaskDependency::build(TaskOrdering::Post, Box::new(SaveDatabaseTask {})),

View File

@ -1,17 +1,17 @@
//! Generates shortcuts for a specified file. //! Generates shortcuts for a specified file.
use installer::InstallerFramework; use crate::installer::InstallerFramework;
use tasks::Task; use crate::tasks::Task;
use tasks::TaskDependency; use crate::tasks::TaskDependency;
use tasks::TaskMessage; use crate::tasks::TaskMessage;
use tasks::TaskParamType; 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 struct InstallShortcutsTask {
pub name: String, pub name: String,
@ -91,7 +91,7 @@ impl Task for InstallShortcutsTask {
let packages = &mut context.database.packages; let packages = &mut context.database.packages;
for pack in packages { for pack in packages {
if pack.name == self.name { if pack.name == self.name {
pack.shortcuts.append(&mut installed_files); pack.shortcuts.extend(installed_files.clone());
} }
} }

View File

@ -2,21 +2,20 @@
//! If theres multiple launchable packages, then choose the first listed in config //! 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. //! If there are multiple shortcuts for the first package, then launch the first.
use installer::InstallerFramework; use crate::installer::InstallerFramework;
use tasks::Task; use crate::tasks::Task;
use tasks::TaskDependency; use crate::tasks::TaskDependency;
use tasks::TaskMessage; use crate::tasks::TaskMessage;
use tasks::TaskParamType; use crate::tasks::TaskParamType;
use config::PackageDescription; use crate::config::PackageDescription;
use logging::LoggingErrors; use crate::logging::LoggingErrors;
pub struct LaunchOnExitTask {} pub struct LaunchOnExitTask {}
impl Task for LaunchOnExitTask { impl Task for LaunchOnExitTask {
fn execute( fn execute(
&mut self, &mut self,
_: Vec<TaskParamType>, _: Vec<TaskParamType>,
@ -25,7 +24,7 @@ impl Task for LaunchOnExitTask {
) -> Result<TaskParamType, String> { ) -> Result<TaskParamType, String> {
let pkg = &context.database.packages.first(); let pkg = &context.database.packages.first();
if pkg.is_none() { if pkg.is_none() {
return Ok(TaskParamType::None) return Ok(TaskParamType::None);
} }
let pkg = pkg.unwrap(); let pkg = pkg.unwrap();
@ -58,7 +57,10 @@ impl Task for LaunchOnExitTask {
// copy the path to the actual exe into launcher_path so it'll load it on exit // copy the path to the actual exe into launcher_path so it'll load it on exit
context.launcher_path = shortcut.map(|s| { context.launcher_path = shortcut.map(|s| {
path.join(s.relative_path.clone()).to_str().map(|t| { t.to_string() }).unwrap() path.join(s.relative_path.clone())
.to_str()
.map(|t| t.to_string())
.unwrap()
}); });
Ok(TaskParamType::None) Ok(TaskParamType::None)

View File

@ -4,10 +4,10 @@
use std::fmt; use std::fmt;
use std::fmt::Display; use std::fmt::Display;
use installer::InstallerFramework; use crate::installer::InstallerFramework;
use sources::types::File; use crate::sources::types::File;
use sources::types::Version; use crate::sources::types::Version;
pub mod check_authorization; pub mod check_authorization;
pub mod download_pkg; pub mod download_pkg;
@ -19,6 +19,7 @@ pub mod install_global_shortcut;
pub mod install_pkg; pub mod install_pkg;
pub mod install_shortcuts; pub mod install_shortcuts;
pub mod launch_installed_on_exit; pub mod launch_installed_on_exit;
pub mod remove_target_dir;
pub mod resolver; pub mod resolver;
pub mod save_database; pub mod save_database;
pub mod save_executable; pub mod save_executable;

View 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()
}
}

View File

@ -2,18 +2,18 @@
use std::env::consts::OS; use std::env::consts::OS;
use installer::InstallerFramework; use crate::installer::InstallerFramework;
use tasks::Task; use crate::tasks::Task;
use tasks::TaskDependency; use crate::tasks::TaskDependency;
use tasks::TaskMessage; use crate::tasks::TaskMessage;
use tasks::TaskParamType; use crate::tasks::TaskParamType;
use config::PackageDescription; use crate::config::PackageDescription;
use regex::Regex; use regex::Regex;
use logging::LoggingErrors; use crate::logging::LoggingErrors;
pub struct ResolvePackageTask { pub struct ResolvePackageTask {
pub name: String, pub name: String,

View File

@ -1,11 +1,11 @@
//! Saves the main database into the installation directory. //! Saves the main database into the installation directory.
use installer::InstallerFramework; use crate::installer::InstallerFramework;
use tasks::Task; use crate::tasks::Task;
use tasks::TaskDependency; use crate::tasks::TaskDependency;
use tasks::TaskMessage; use crate::tasks::TaskMessage;
use tasks::TaskParamType; use crate::tasks::TaskParamType;
pub struct SaveDatabaseTask {} pub struct SaveDatabaseTask {}

View File

@ -1,11 +1,11 @@
//! Saves the installer executable into the install directory. //! Saves the installer executable into the install directory.
use installer::InstallerFramework; use crate::installer::InstallerFramework;
use tasks::Task; use crate::tasks::Task;
use tasks::TaskDependency; use crate::tasks::TaskDependency;
use tasks::TaskMessage; use crate::tasks::TaskMessage;
use tasks::TaskParamType; use crate::tasks::TaskParamType;
use std::fs::File; use std::fs::File;
use std::fs::OpenOptions; use std::fs::OpenOptions;
@ -14,7 +14,7 @@ use std::io::copy;
use std::env::current_exe; use std::env::current_exe;
use logging::LoggingErrors; use crate::logging::LoggingErrors;
pub struct SaveExecutableTask {} pub struct SaveExecutableTask {}

View File

@ -1,14 +1,14 @@
//! Uninstalls a set of packages. //! Uninstalls a set of packages.
use installer::InstallerFramework; use crate::installer::InstallerFramework;
use tasks::Task; use crate::tasks::Task;
use tasks::TaskParamType; use crate::tasks::TaskParamType;
use tasks::uninstall_pkg::UninstallPackageTask; use crate::tasks::uninstall_pkg::UninstallPackageTask;
use tasks::TaskDependency; use crate::tasks::TaskDependency;
use tasks::TaskMessage; use crate::tasks::TaskMessage;
use tasks::TaskOrdering; use crate::tasks::TaskOrdering;
pub struct UninstallTask { pub struct UninstallTask {
pub items: Vec<String>, pub items: Vec<String>,

View File

@ -1,15 +1,15 @@
//! Uninstalls a specific package. //! Uninstalls a specific package.
use installer::InstallerFramework; use crate::installer::InstallerFramework;
use tasks::Task; use crate::tasks::Task;
use tasks::TaskDependency; use crate::tasks::TaskDependency;
use tasks::TaskMessage; use crate::tasks::TaskMessage;
use tasks::TaskParamType; use crate::tasks::TaskParamType;
use crate::tasks::save_database::SaveDatabaseTask;
use crate::tasks::TaskOrdering;
use std::fs::remove_file; use std::fs::remove_file;
use tasks::save_database::SaveDatabaseTask;
use tasks::TaskOrdering;
pub struct UninstallGlobalShortcutsTask {} pub struct UninstallGlobalShortcutsTask {}

View File

@ -1,21 +1,21 @@
//! Uninstalls a specific package. //! Uninstalls a specific package.
use installer::InstallerFramework; use crate::installer::InstallerFramework;
use tasks::save_database::SaveDatabaseTask; use crate::tasks::save_database::SaveDatabaseTask;
use tasks::Task; use crate::tasks::Task;
use tasks::TaskDependency; use crate::tasks::TaskDependency;
use tasks::TaskMessage; use crate::tasks::TaskMessage;
use tasks::TaskOrdering; use crate::tasks::TaskOrdering;
use tasks::TaskParamType; use crate::tasks::TaskParamType;
use installer::LocalInstallation; use crate::installer::LocalInstallation;
use std::fs::remove_dir; use std::fs::remove_dir;
use std::fs::remove_file; use std::fs::remove_file;
use logging::LoggingErrors; use crate::logging::LoggingErrors;
use tasks::uninstall_shortcuts::UninstallShortcutsTask; use crate::tasks::uninstall_shortcuts::UninstallShortcutsTask;
pub struct UninstallPackageTask { pub struct UninstallPackageTask {
pub name: String, pub name: String,
@ -44,7 +44,7 @@ impl Task for UninstallPackageTask {
} }
} }
let mut package = match metadata { let package = match metadata {
Some(v) => v, Some(v) => v,
None => { None => {
if self.optional { if self.optional {
@ -63,8 +63,7 @@ impl Task for UninstallPackageTask {
0.0, 0.0,
)); ));
// Reverse, as to delete directories last let mut directories = Vec::new();
package.files.reverse();
let max = package.files.len(); let max = package.files.len();
for (i, file) in package.files.iter().enumerate() { for (i, file) in package.files.iter().enumerate() {
@ -78,7 +77,9 @@ impl Task for UninstallPackageTask {
)); ));
let result = if file.is_dir() { let result = if file.is_dir() {
remove_dir(file) // we don't delete directory just yet
directories.push(file);
Ok(())
} else { } else {
remove_file(file) 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) Ok(TaskParamType::None)
} }

View File

@ -1,18 +1,18 @@
//! Uninstalls a specific package. //! Uninstalls a specific package.
use installer::InstallerFramework; use crate::installer::InstallerFramework;
use tasks::Task; use crate::tasks::Task;
use tasks::TaskDependency; use crate::tasks::TaskDependency;
use tasks::TaskMessage; use crate::tasks::TaskMessage;
use tasks::TaskParamType; use crate::tasks::TaskParamType;
use installer::LocalInstallation; use crate::installer::LocalInstallation;
use std::fs::remove_dir; use std::fs::remove_dir;
use std::fs::remove_file; use std::fs::remove_file;
use logging::LoggingErrors; use crate::logging::LoggingErrors;
pub struct UninstallShortcutsTask { pub struct UninstallShortcutsTask {
pub name: String, pub name: String,

View File

@ -1,3 +1 @@
> 1% > 1%
last 2 versions
not ie <= 11

View File

@ -3,15 +3,14 @@ module.exports = {
env: { env: {
node: true node: true
}, },
'extends': [ extends: [
'plugin:vue/essential', 'plugin:vue/recommended',
'@vue/standard' '@vue/standard'
], ],
rules: { rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 'no-console': 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
}, 'no-redeclare': 'off',
parserOptions: { camelcase: 'off'
parser: 'babel-eslint'
} }
} }

20
ui/merge-strings.js Executable file
View 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), {})
})

View File

@ -4,20 +4,38 @@ const express = require('express')
const app = express() const app = express()
const port = 3000 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) { function progressSimulation (res) {
var progress = 0.0 if (showError) {
var timer = setInterval(() => { const resp = JSON.stringify({ Error: 'Simulated error.' }) + '\n'
var resp = JSON.stringify({ Status: ['Processing...', progress] }) + '\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 progress += 0.1
res.write(resp) res.write(resp)
if (progress >= 1) { if (progress >= 1) {
res.status(200).end() res.status(200).end()
clearInterval(timer) clearInterval(timer)
} }
}, 1500) }, 500)
} }
function returnConfig (res) { function returnConfig (res) {
if (showConfigError) {
res.status(500).json({})
return
}
res.json({ res.json({
installing_message: installing_message:
'Test Banner <strong>Bold</strong>&nbsp;<pre>Code block</pre>&nbsp;<i>Italic</i>&nbsp;<del>Strike</del>', 'Test Banner <strong>Bold</strong>&nbsp;<pre>Code block</pre>&nbsp;<i>Italic</i>&nbsp;<del>Strike</del>',
@ -52,21 +70,22 @@ function returnConfig (res) {
} }
app.get('/api/attrs', (req, res) => { app.get('/api/attrs', (req, res) => {
console.log('-- Get attrs')
res.send( 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) => { app.get('/api/dark-mode', (req, res) => {
res.json(false) res.json(darkMode)
}) })
app.get('/api/installation-status', (req, res) => { app.get('/api/installation-status', (req, res) => {
res.json({ res.json({
database: { packages: [], shortcuts: [] }, database: { packages: [], shortcuts: [] },
install_path: null, install_path: null,
preexisting_install: false, preexisting_install: maintenance,
is_launcher: false, is_launcher: launcher,
launcher_path: null launcher_path: null
}) })
}) })
@ -82,13 +101,72 @@ app.get('/api/config', (req, res) => {
}) })
app.post('/api/start-install', (req, res) => { app.post('/api/start-install', (req, res) => {
console.log(`-- Install: ${req.body}`) console.log('-- Install:')
console.log(req.body)
progressSimulation(res) progressSimulation(res)
}) })
app.get('/api/exit', (req, res) => { app.get('/api/exit', (req, res) => {
console.log('-- Exit') 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}...`) console.log(`Listening on ${port}...`)

View File

@ -5,23 +5,32 @@
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build": "vue-cli-service build",
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint",
"postinstall": "node merge-strings.js"
}, },
"dependencies": { "dependencies": {
"buefy": "^0.7.7", "@mdi/font": "^7.1.96",
"vue": "^2.6.6", "axios": "^1.2.1",
"vue-router": "^3.0.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": { "devDependencies": {
"@vue/cli-plugin-babel": "^3.5.0", "@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-plugin-eslint": "^3.5.0", "@vue/cli-plugin-eslint": "^5.0.8",
"@vue/cli-service": "^3.5.0", "@vue/cli-service": "^5.0.8",
"@vue/eslint-config-standard": "^4.0.0", "@vue/eslint-config-standard": "^6.1.0",
"babel-eslint": "^10.0.1", "eslint": "^8.30.0",
"eslint": "^5.8.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-vue": "^5.0.0", "eslint-plugin-node": "^11.1.0",
"express": "^4.17.1", "eslint-plugin-promise": "^6.1.1",
"http-proxy-middleware": "^0.19.1", "eslint-plugin-standard": "^4.1.0",
"vue-template-compiler": "^2.5.21" "eslint-plugin-vue": "^9.8.0",
"express": "^4.18.2",
"http-proxy-middleware": "^2.0.6",
"vue-template-compiler": "^2.7.14"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -4,7 +4,6 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=11"> <meta http-equiv="X-UA-Compatible" content="IE=11">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <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"> <link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title id="window-title">... Installer</title> <title id="window-title">... Installer</title>
</head> </head>

View File

@ -9,15 +9,23 @@
<br /> <br />
<h2 class="subtitle" v-if="!$root.$data.metadata.preexisting_install"> <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>
<h2 class="subtitle" v-if="!$root.$data.metadata.preexisting_install"> <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>
<h2 class="subtitle" v-if="$root.$data.metadata.preexisting_install"> <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> </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> </div>
<router-view /> <router-view />
@ -27,6 +35,33 @@
</div> </div>
</template> </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> <style>
/* roboto-regular - latin */ /* roboto-regular - latin */
@font-face { @font-face {
@ -147,8 +182,12 @@ pre {
background: #fff; background: #fff;
} }
.tile.box.clickable-box {
color: #4a4a4a;
}
/* Dark mode */ /* Dark mode */
body.has-background-black-ter .subtitle, body.has-background-black-ter .column > div, body.has-background-black-ter section { body.has-background-black-ter .subtitle, body.has-background-black-ter .column > div {
color: hsl(0, 0%, 96%); color: hsl(0, 0%, 96%);
} }
@ -195,4 +234,7 @@ body.has-background-black-ter .subtitle, body.has-background-black-ter .column >
border-bottom: 3px solid transparent; border-bottom: 3px solid transparent;
border-top: 3px solid #FF3C28; border-top: 3px solid #FF3C28;
} }
a:hover {
text-decoration: underline;
}
</style> </style>

BIN
ui/src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -4,60 +4,6 @@
* Additional state-less helper methods. * 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, * Makes a AJAX request, streaming each line as it arrives. Type should be text/plain,
* each line will be interpreted as JSON separately. * each line will be interpreted as JSON separately.
@ -69,7 +15,7 @@ export function ajax (path, successCallback, failCallback, data) {
* @param data POST data. Optional. * @param data POST data. Optional.
*/ */
export function stream_ajax (path, callback, successCallback, failCallback, data) { 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) console.log('Making streaming HTTP request to ' + path)
@ -82,23 +28,23 @@ export function stream_ajax (path, callback, successCallback, failCallback, data
} }
}) })
var buffer = '' let buffer = ''
var seenBytes = 0 let seenBytes = 0
req.onreadystatechange = function () { req.onreadystatechange = function () {
if (req.readyState > 2) { if (req.readyState > 2) {
buffer += req.responseText.substr(seenBytes) buffer += req.responseText.substr(seenBytes)
var pointer let pointer
while ((pointer = buffer.indexOf('\n')) >= 0) { 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) buffer = buffer.substring(pointer + 1)
if (line.length === 0) { if (line.length === 0) {
continue continue
} }
var contents = JSON.parse(line) const contents = JSON.parse(line)
callback(contents) callback(contents)
} }
@ -108,15 +54,15 @@ export function stream_ajax (path, callback, successCallback, failCallback, data
req.addEventListener('error', failCallback) 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. // Rocket only currently supports URL encoded forms.
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
if (data != null) { if (data != null) {
var form = '' let form = ''
for (var key in data) { for (const key in data) {
if (!data.hasOwnProperty(key)) { if (!data[key]) {
continue continue
} }
@ -132,13 +78,3 @@ export function stream_ajax (path, callback, successCallback, failCallback, data
req.send() 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
View File

@ -0,0 +1 @@
messages.json

78
ui/src/locales/ca.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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 Accesskanalen 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 Accessmedlem må du være en Patreon Early Accessabonnent.",
"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
View 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
View 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
View 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
View 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
View 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
View 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}"
}
}
}

78
ui/src/locales/vi_VN.json Normal file
View File

@ -0,0 +1,78 @@
{
"vi-VN": {
"locale": "Tiếng Anh",
"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": "Huỷ",
"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}"
}
}
}

78
ui/src/locales/zh_CN.json Normal file
View File

@ -0,0 +1,78 @@
{
"zh-CN": {
"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 用户,您需要成为 Early Access 的捐献者(通过 Patreon 平台)。",
"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/zh_TW.json Normal file
View File

@ -0,0 +1,78 @@
{
"zh-TW": {
"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": "Token",
"verify": "驗證 Token",
"page_header": "Early Access 通道允許您在最新的實驗性功能和修復檔合併到 yuzu 主要通道之前先行體驗。此通道包含 yuzu 每日更新及以上獨家功能。\n\n欲成為 Early Access 成員您需要至Patreon 平台贊助 Early Access。",
"page_opened": "已開啟網頁。請檢視網頁並依照指示連結您的 patreon 帳號。\n完成後請在下方輸入您的 token。",
"login_failed": "驗證失敗!\n\n請確定您的 token 正確後再重試。"
},
"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}"
}
}
}

View File

@ -1,60 +1,64 @@
import Vue from 'vue' import Vue from 'vue'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import { ajax, stream_ajax } from './helpers' import axios from 'axios'
import VueAxios from 'vue-axios'
import VueI18n from 'vue-i18n'
import { stream_ajax as streamAjax } from './helpers'
import Buefy from 'buefy' import Buefy from 'buefy'
import messages from './locales/messages.json'
import 'buefy/dist/buefy.css' import 'buefy/dist/buefy.css'
import '@mdi/font/css/materialdesignicons.min.css'
// HACK: disables context menu
document.addEventListener('contextmenu', event => event.preventDefault())
Vue.config.productionTip = false Vue.config.productionTip = false
Vue.use(Buefy) Vue.use(Buefy)
Vue.use(VueI18n)
Vue.use(VueAxios, axios)
export const i18n = new VueI18n({
locale: 'en', // set locale
fallbackLocale: 'en',
messages // set locale messages
})
// Borrowed from http://tobyho.com/2012/07/27/taking-over-console-log/ // Borrowed from http://tobyho.com/2012/07/27/taking-over-console-log/
function intercept (method) { function intercept (method) {
console[method] = function () { console[method] = function () {
var message = Array.prototype.slice.apply(arguments).join(' ') const message = Array.prototype.slice.apply(arguments).join(' ')
window.external.invoke( window.rpc.notify(
JSON.stringify({ 'Log', method, message
Log: {
kind: method,
msg: message
}
})
) )
} }
} }
// See if we have access to the JSON interface // See if we have access to the JSON interface
var has_external_interface = false; let hasExternalInterface = false
try { try {
window.external.invoke(JSON.stringify({ window.rpc.notify('Test')
Test: {} hasExternalInterface = true
}))
has_external_interface = true;
} catch (e) { } catch (e) {
console.warn("Running without JSON interface - unexpected behaviour may occur!") console.warn('Running without JSON interface - unexpected behaviour may occur!')
} }
// Overwrite loggers with the logging backend // Overwrite loggers with the logging backend
if (has_external_interface) { if (hasExternalInterface) {
window.onerror = function (msg, url, line) { window.onerror = function (msg, url, line) {
window.external.invoke( window.rpc.notify(
JSON.stringify({ 'Log', 'error', msg + ' @ ' + url + ':' + line
Log: {
kind: 'error',
msg: msg + ' @ ' + url + ':' + line
}
})
) )
} }
var methods = ['log', 'warn', 'error'] const methods = ['log', 'warn', 'error']
for (var i = 0; i < methods.length; i++) { for (let i = 0; i < methods.length; i++) {
intercept(methods[i]) intercept(methods[i])
} }
} }
// Disable F5 // Disable F5
function disable_shortcuts (e) { function disableShortcuts (e) {
switch (e.keyCode) { switch (e.keyCode) {
case 116: // F5 case 116: // F5
e.preventDefault() e.preventDefault()
@ -63,25 +67,26 @@ function disable_shortcuts (e) {
} }
// Check to see if we need to enable dark mode // Check to see if we need to enable dark mode
ajax('/api/dark-mode', function (enable) { axios.get('/api/dark-mode').then(function (resp) {
if (enable) { if (resp.data === true) {
document.body.classList.add('has-background-black-ter') document.body.classList.add('has-background-black-ter')
} }
}) })
window.addEventListener('keydown', disable_shortcuts) window.addEventListener('keydown', disableShortcuts)
axios.get('/api/attrs').then(function (resp) {
document.getElementById('window-title').innerText = document.getElementById('window-title').innerText =
base_attributes.name + ' Installer' i18n.t('app.window_title', { name: resp.data.name })
}).catch(function (err) {
console.error(err)
})
function selectFileCallback (name) { const app = new Vue({
app.install_location = name i18n: i18n,
}
var app = new Vue({
router: router, router: router,
data: { data: {
attrs: base_attributes, attrs: {},
config: {}, config: {},
install_location: '', install_location: '',
username: '', username: '',
@ -102,54 +107,61 @@ var app = new Vue({
render: function (caller) { render: function (caller) {
return caller(App) return caller(App)
}, },
methods: { mounted: function () {
exit: function () { axios.get('/api/attrs').then(function (resp) {
ajax( app.attrs = resp.data
'/api/exit', }).catch(function (err) {
function () {}, console.error(err)
function (msg) {
var search_location = app.metadata.install_path.length > 0 ? app.metadata.install_path :
"the location where this installer is";
app.$router.replace({ name: 'showerr', params: { msg: msg +
'\n\nPlease upload the log file (in ' + search_location + ') to ' +
'the ' + app.attrs.name + ' team'
}});
},
{} // pass in nothing to cause `ajax` to post instead of get
)
},
check_authentication: function (success, error) {
var that = this;
var app = this.$root;
app.ajax('/api/check-auth', function (auth) {
app.$data.username = auth.username;
app.$data.token = auth.token;
that.jwt_token = auth.jwt_token;
that.is_authenticated = Object.keys(that.jwt_token).length !== 0 && that.jwt_token.constructor === Object;
if (that.is_authenticated) {
// Give all permissions to vip roles
that.is_linked = that.jwt_token.isPatreonAccountLinked;
that.is_subscribed = that.jwt_token.isPatreonSubscriptionActive;
that.has_reward_tier = that.jwt_token.releaseChannels.indexOf("early-access") > -1;
}
if (success) {
success();
}
}, function (e) {
if (error) {
error();
}
}, {
"username": app.$data.username,
"token": app.$data.token
}) })
}, },
methods: {
exit: function () {
axios.get('/api/exit').catch(function (msg) {
const searchLocation = (app.metadata.install_path && app.metadata.install_path.length > 0)
? app.metadata.install_path
: i18n.t('error.location_unknown')
ajax: ajax, app.$router.replace({
stream_ajax: stream_ajax name: 'showerr',
params: {
msg: i18n.t('error.exit_error', {
name: app.attrs.name,
path: searchLocation,
msg: msg
})
}
})
})
},
check_authentication: function (success, error) {
const that = this
const app = this.$root
axios.post('/api/check-auth', {
username: app.$data.username,
token: app.$data.token
}).then(function (resp) {
app.$data.username = resp.data.username
app.$data.token = resp.data.token
that.jwt_token = resp.data.jwt_token
that.is_authenticated = Object.keys(that.jwt_token).length !== 0 && that.jwt_token.constructor === Object
if (that.is_authenticated) {
// Give all permissions to vip roles
that.is_linked = that.jwt_token.isPatreonAccountLinked
that.is_subscribed = that.jwt_token.isPatreonSubscriptionActive
that.has_reward_tier = that.jwt_token.releaseChannels.indexOf('early-access') > -1
}
if (success) {
success()
}
}).catch(function () {
if (error) {
error()
}
})
},
stream_ajax: streamAjax
} }
}).$mount('#app') }).$mount('#app')
console.log("Vue started") console.log('Vue started')

View File

@ -2,13 +2,10 @@
<div class="column has-padding"> <div class="column has-padding">
<section> <section>
<b-message type="is-info" :active.sync="browser_opened"> <b-message type="is-info" :active.sync="browser_opened">
Page opened! Check your default browser for the page, and follow the instructions there to link your patreon account. {{ $t('auth.page_opened') }}
When you are done, enter the token below.
</b-message> </b-message>
<b-message type="is-info" :active.sync="show_header"> <b-message type="is-info" :active.sync="show_header">
The <strong>Early Access</strong> 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. {{ $t('auth.page_header') }}
To be an Early Access member, you must be a Patreon Early Access Subscriber.
</b-message> </b-message>
<div> <div>
If you are a subscriber, <a v-on:click="launch_browser('https://profile.yuzu-emu.org/')">click here to link your yuzu-emu.org account</a> If you are a subscriber, <a v-on:click="launch_browser('https://profile.yuzu-emu.org/')">click here to link your yuzu-emu.org account</a>
@ -20,11 +17,11 @@
<br> <br>
<section> <section>
<p>Token</p> <p>{{ $t('auth.token') }}</p>
<b-field> <b-field>
<b-input type="text" v-model="combined_token" placeholder="Token" id="token" style='width: 50em;'></b-input> <b-input type="text" v-model="combined_token" :placeholder="$t('auth.token')" id="token" style='width: 80%;'></b-input>
<p class="control"> <p class="control">
<button class="button is-info" v-on:click="paste">Paste</button> <b-button type="is-info" v-on:click="paste">{{ $t('auth.paste') }}</b-button>
</p> </p>
</b-field> </b-field>
</section> </section>
@ -33,14 +30,8 @@
<section> <section>
<b-message type="is-danger" :active.sync="invalid_token"> <b-message id="invalid-token" type="is-danger" :active.sync="show_error">
Login failed! {{ $t('auth.login_failed') }}
Double check that your token is correct and try again
</b-message>
<b-message type="is-danger" :active.sync="invalid_login">
Login failed!
Double check that your token is correct and try again
</b-message> </b-message>
<b-message type="is-danger" :active.sync="unlinked_patreon"> <b-message type="is-danger" :active.sync="unlinked_patreon">
@ -61,28 +52,27 @@
<div class="is-left-floating is-bottom-floating"> <div class="is-left-floating is-bottom-floating">
<p class="control"> <p class="control">
<a class="button is-medium" v-on:click="go_back">Back</a> <b-button class="is-dark is-medium" v-on:click="go_back">{{ $t('back') }}</b-button>
</p> </p>
</div> </div>
<div class="is-right-floating is-bottom-floating" v-scroll> <div class="is-right-floating is-bottom-floating" v-scroll>
<p class="control"> <p class="control">
<a class="button is-dark is-medium" v-on:click="verify_token">Verify Token</a> <b-button type="is-primary is-medium" :loading="loading" v-on:click="verify_token">{{ $t('auth.verify') }}</b-button>
</p> </p>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
const confetti = require('canvas-confetti')
export default { export default {
name: 'AuthenticationView', name: 'AuthenticationView',
created: function () { created: function () {
// If they are already authenticated when this page is loaded, // If they are already authenticated when this page is loaded,
// then we can asssume they are "clicking here for more details" and should show the appropriate error message // then we can asssume they are "clicking here for more details" and should show the appropriate error message
if (this.$root.is_authenticated) { if (this.$root.is_authenticated) {
this.verification_opened = true; this.verification_opened = true
} }
}, },
data: function () { data: function () {
@ -90,41 +80,49 @@ export default {
browser_opened: false, browser_opened: false,
verification_opened: false, verification_opened: false,
invalid_token: false, invalid_token: false,
loading: false
} }
}, },
computed: { computed: {
show_header: function () { show_header: function () {
return !this.browser_opened && !this.verification_opened && !this.invalid_token; return !this.browser_opened && !this.verification_opened
},
show_error: function () {
return this.invalid_login || this.invalid_token
}, },
invalid_login: function () { invalid_login: function () {
return this.verification_opened && !this.$root.is_authenticated; return this.verification_opened && !this.$root.is_authenticated
}, },
unlinked_patreon: function () { unlinked_patreon: function () {
return this.verification_opened && this.$root.is_authenticated && !this.$root.is_linked; return this.verification_opened && this.$root.is_authenticated && !this.$root.is_linked
}, },
no_subscription: function () { no_subscription: function () {
return this.verification_opened && this.$root.is_linked && !this.$root.is_subscribed; return this.verification_opened && this.$root.is_linked && !this.$root.is_subscribed
}, },
tier_not_selected: function () { tier_not_selected: function () {
return this.verification_opened && this.$root.is_linked && this.$root.is_subscribed && !this.$root.has_reward_tier; return this.verification_opened && this.$root.is_linked && this.$root.is_subscribed && !this.$root.has_reward_tier
}, },
combined_token: { combined_token: {
// getter // getter
get: function () { get: function () {
if (this.$root.$data.username && this.$root.$data.token) { if (this.$root.$data.username && this.$root.$data.token) {
return btoa(this.$root.$data.username + ":" + this.$root.$data.token) return btoa(this.$root.$data.username + ':' + this.$root.$data.token)
} }
return ""; return ''
}, },
// setter // setter
set: function (newValue) { set: function (newValue) {
if (!newValue || !newValue.trim()) {
this.invalid_token = true
return
}
try { try {
var split = atob(newValue).split(':') const split = atob(newValue).split(':')
this.$root.$data.username = split[0]; this.$root.$data.username = split[0]
this.$root.$data.token = split[1]; this.$root.$data.token = split[1]
this.invalid_token = false; this.invalid_token = false
} catch (e) { } catch (e) {
this.invalid_token = true; this.invalid_token = true
} }
} }
} }
@ -134,37 +132,64 @@ export default {
this.$router.go(-1) this.$router.go(-1)
}, },
paste: function () { paste: function () {
document.getElementById("token").focus(); window.document.getElementById('token').focus()
document.execCommand("paste"); const that = this
if ('readText' in (window.navigator.clipboard ?? {})) {
window.navigator.clipboard.readText().then(function (v) {
that.combined_token = v.trim()
}).catch(() => {})
} else {
this.combined_token = ''
document.execCommand('paste')
}
}, },
launch_browser: function (url) { launch_browser: function (url) {
const that = this; const that = this
let app = this.$root; this.$http.post('/api/open-browser', {
app.ajax('/api/open-browser', function (e) { url: url
}).then(function () {
// only open the browser opened message if there isn't an error message currently // only open the browser opened message if there isn't an error message currently
if (!that.verification_opened) { if (!that.verification_opened) {
that.browser_opened = true; that.browser_opened = true
} }
}, function (e) {}, { }).catch(function () {})
"url": url, },
}); blink_error: function () {
const target = document.getElementById('invalid-token')
target.classList.add('blink-block')
setTimeout(function () {
target.classList.remove('blink-block')
}, 1200)
}, },
verify_token: function () { verify_token: function () {
this.browser_opened = false; if (this.invalid_token) {
this.$root.check_authentication(this.success, this.error); this.blink_error()
return
}
this.loading = true
this.browser_opened = false
this.$root.check_authentication(this.success, this.error)
}, },
success: function () { success: function () {
// if they are eligible, go back to the select package page
if (this.$root.has_reward_tier) { if (this.$root.has_reward_tier) {
this.$router.go(-1); // show a success animation
return; confetti.default({
particleCount: 200,
spread: 90,
origin: { x: 0.6 }
})
// if they are eligible, go back to the select package page
this.$router.go(-1)
return
} }
// They aren't currently eligible for the release, so display the error message // They aren't currently eligible for the release, so display the error message
this.verification_opened = true; this.verification_opened = true
this.loading = false
}, },
error: function () { error: function () {
this.verification_opened = true; this.loading = false
this.verification_opened = true
this.blink_error()
} }
}, },
directives: { directives: {
@ -176,3 +201,14 @@ export default {
} }
} }
</script> </script>
<style>
.blink-block {
animation: blink 0.3s linear infinite;
}
@keyframes blink {
50% {
opacity: 0
}
}
</style>

View File

@ -1,38 +1,45 @@
<template> <template>
<div class="column has-padding"> <div class="column has-padding">
<div v-if="was_migrate"> <div v-if="was_migrate">
<h4 class="subtitle">You have been moved to the new, single version of {{ $root.$data.attrs.name }}.</h4> <h4 class="subtitle">{{ $t('complete.migration_finished', {'name': $root.$data.attrs.name}) }}</h4>
<p>You can find your installed applications in your applications menu - if you were in the middle of something, just reattempt.</p> <p>{{ $t('complete.migration_where_to_find') }}</p>
<img src="../assets/how-to-open.png" alt="Where yuzu is installed"/> <img src="../assets/how-to-open.png" alt="Where yuzu is installed"/>
</div> </div>
<div v-else-if="was_update"> <div v-else-if="was_update">
<div v-if="has_installed"> <div v-if="has_installed">
<h4 class="subtitle">{{ $root.$data.attrs.name }} has been updated.</h4> <h4 class="subtitle">{{ $t('complete.updated', {'name': $root.$data.attrs.name}) }}</h4>
<p>You can find your installed applications in your applications menu.</p> <p>{{ $t('complete.where_to_find') }}</p>
</div> </div>
<div v-else> <div v-else>
<h4 class="subtitle">{{ $root.$data.attrs.name }} is already up to date!</h4> <h4 class="subtitle">{{ $t('complete.up_to_date', {'name': $root.$data.attrs.name}) }}</h4>
<p>You can find your installed applications in your applications menu.</p> <p>{{ $t('complete.where_to_find') }}</p>
</div> </div>
</div> </div>
<div v-else-if="was_install"> <div v-else-if="was_install">
<h4 class="subtitle">Thanks for installing {{ $root.$data.attrs.name }}!</h4> <h4 class="subtitle">{{ $t('complete.thanks', {'name': $root.$data.attrs.name}) }}</h4>
<p>You can find your installed applications in your applications menu.</p> <p>{{ $t('complete.where_to_find') }}</p>
<br> <br>
<img src="../assets/how-to-open.png" alt="Where yuzu is installed" v-if="$root.$data.metadata.is_windows"/> <img src="../assets/how-to-open.png" alt="Where yuzu is installed" v-if="$root.$data.metadata.is_windows"/>
</div> </div>
<div v-else> <div v-else>
<h4 class="subtitle">{{ $root.$data.attrs.name }} has been uninstalled.</h4> <h4 class="subtitle">{{ $t('complete.uninstalled', {'name': $root.$data.attrs.name}) }}</h4>
</div>
<!-- show the back button when the user was repairing/installing/updating -->
<div class="is-left-floating is-bottom-floating" v-if="$root.$data.metadata.preexisting_install && !this.$route.params.uninstall">
<p class="control">
<b-button class="is-dark is-medium" v-on:click="go_back">{{ $t('back') }}</b-button>
</p>
</div> </div>
<div class="field is-grouped is-right-floating is-bottom-floating"> <div class="field is-grouped is-right-floating is-bottom-floating">
<p class="control"> <p class="control">
<a class="button is-dark is-medium" v-on:click="exit">Exit</a> <b-button class="is-primary is-medium" v-on:click="exit">{{ $t('exit') }}</b-button>
</p> </p>
</div> </div>
</div> </div>
@ -46,12 +53,15 @@ export default {
was_install: !this.$route.params.uninstall, was_install: !this.$route.params.uninstall,
was_update: this.$route.params.update, was_update: this.$route.params.update,
was_migrate: this.$route.params.migrate, was_migrate: this.$route.params.migrate,
has_installed: this.$route.params.packages_installed > 0, has_installed: this.$route.params.packages_installed > 0
} }
}, },
methods: { methods: {
go_back: function () {
this.$router.replace('/modify')
},
exit: function () { exit: function () {
this.$root.exit(); this.$root.exit()
} }
} }
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="column has-padding"> <div class="column has-padding">
<h4 class="subtitle">Downloading config...</h4> <h4 class="subtitle">{{ $t('download_config.download_config') }}</h4>
<br /> <br />
<progress class="progress is-info is-medium" max="100"> <progress class="progress is-info is-medium" max="100">
@ -17,51 +17,54 @@ export default {
}, },
methods: { methods: {
download_install_status: function () { download_install_status: function () {
var that = this const that = this
this.$root.ajax('/api/installation-status', function (e) { this.$http.get('/api/installation-status').then(function (resp) {
that.$root.metadata = e that.$root.metadata = resp.data
that.download_config() that.download_config()
}) })
}, },
download_config: function () { download_config: function () {
var that = this const that = this
this.$root.ajax('/api/config', function (e) { this.$http.get('/api/config').then(function (resp) {
that.$root.config = e that.$root.config = resp.data
// Update the updater if needed
if (that.$root.config.new_tool) {
that.$router.push('/install/updater/false')
return
}
that.$root.check_authentication(that.choose_next_state, that.choose_next_state) that.$root.check_authentication(that.choose_next_state, that.choose_next_state)
}, function (e) { }).catch(function (e) {
console.error('Got error while downloading config: ' + e) console.error('Got error while downloading config: ' +
e)
if (that.$root.metadata.is_launcher) { if (that.$root.metadata.is_launcher) {
// Just launch the target application // Just launch the target application
that.$root.exit() that.$root.exit()
} else { } else {
that.$router.replace({ name: 'showerr', that.$router.replace({
params: { msg: 'Got error while downloading config: ' + e } }) name: 'showerr',
params: { msg: that.$i18n.t('download_config.error_download_config', { msg: e }) }
})
} }
}) })
}, },
choose_next_state: function () { choose_next_state: function () {
var app = this.$root const app = this.$root
// Update the updater if needed
if (app.config.new_tool) {
this.$router.push('/install/updater/false')
return
}
if (app.metadata.preexisting_install) { if (app.metadata.preexisting_install) {
app.install_location = app.metadata.install_path app.install_location = app.metadata.install_path
// Copy over installed packages // Copy over installed packages
for (var x = 0; x < app.config.packages.length; x++) { for (let x = 0; x < app.config.packages.length; x++) {
app.config.packages[x].default = false app.config.packages[x].default = false
app.config.packages[x].installed = false app.config.packages[x].installed = false
} }
for (var i = 0; i < app.metadata.database.packages.length; i++) { for (let i = 0; i < app.metadata.database.packages.length; i++) {
// Find this config package // Find this config package
for (var x = 0; x < app.config.packages.length; x++) { for (let x = 0; x < app.config.packages.length; x++) {
if (app.config.packages[x].name === app.metadata.database.packages[i].name) { if (app.config.packages[x].name === app.metadata.database.packages[i].name) {
app.config.packages[x].default = true app.config.packages[x].default = true
app.config.packages[x].installed = true app.config.packages[x].installed = true
@ -69,23 +72,27 @@ export default {
} }
} }
this.$router.replace({ name: 'migrate', this.$router.replace({
params: { next: app.metadata.is_launcher ? '/install/regular/false' : '/modify' } }) name: 'migrate',
params: { next: app.metadata.is_launcher ? '/install/regular/false' : '/modify' }
})
} else { } else {
for (var x = 0; x < app.config.packages.length; x++) { for (let x = 0; x < app.config.packages.length; x++) {
app.config.packages[x].installed = false app.config.packages[x].installed = false
} }
// Need to do a bit more digging to get at the // Need to do a bit more digging to get at the
// install location. // install location.
this.$root.ajax('/api/default-path', function (e) { this.$http.get('/api/default-path').then(function (resp) {
if (e.path != null) { if (resp.data.path != null) {
app.install_location = e.path app.install_location = resp.data.path
} }
}) })
this.$router.replace({ name: 'migrate', this.$router.replace({
params: { next: '/packages' } }) name: 'migrate',
params: { next: '/packages' }
})
} }
} }
} }

View File

@ -1,12 +1,12 @@
<template> <template>
<div class="column" v-bind:class="{ 'has-padding': !$root.$data.metadata.is_launcher }"> <div class="column" v-bind:class="{ 'has-padding': !$root.$data.metadata.is_launcher }">
<b-message title="An error occurred" type="is-danger" :closable="false"> <b-message :title="$t('error.title')" type="is-danger" :closable="false">
<div id="error_msg" v-html="msg"></div> <div id="error_msg" v-html="msg"></div>
</b-message> </b-message>
<div class="field is-grouped is-right-floating is-bottom-floating"> <div class="field is-grouped is-right-floating" v-bind:class="{ 'is-bottom-floating': !$root.$data.metadata.is_launcher, 'is-top-floating': $root.$data.metadata.is_launcher }">
<p class="control"> <p class="control">
<a class="button is-primary is-medium" v-if="remaining && !$root.$data.metadata.is_launcher" v-on:click="go_back">Back</a> <b-button class="is-primary is-medium" v-if="remaining && !$root.$data.metadata.is_launcher" v-on:click="go_back">{{ $t('back') }}</b-button>
<a class="button is-primary is-medium" v-if="$root.$data.metadata.is_launcher" v-on:click="exit">Exit</a> <b-button class="is-primary is-medium" v-if="$root.$data.metadata.is_launcher" v-on:click="exit">{{ $t('exit') }}</b-button>
</p> </p>
</div> </div>
</div> </div>
@ -38,12 +38,12 @@ export default {
return { return {
// https://stackoverflow.com/questions/6234773/can-i-escape-html-special-chars-in-javascript // https://stackoverflow.com/questions/6234773/can-i-escape-html-special-chars-in-javascript
msg: this.$route.params.msg msg: this.$route.params.msg
.replace(/&/g, "&amp;") .replace(/&/g, '&amp;')
.replace(/</g, "&lt;") .replace(/</g, '&lt;')
.replace(/>/g, "&gt;") .replace(/>/g, '&gt;')
.replace(/"/g, "&quot;") .replace(/"/g, '&quot;')
.replace(/'/g, "&#039;") .replace(/'/g, '&#039;')
.replace(/\n/g, "<br />"), .replace(/\n/g, '<br />'),
remaining: window.history.length > 1 remaining: window.history.length > 1
} }
}, },

View File

@ -1,9 +1,9 @@
<template> <template>
<div class="column has-padding"> <div class="column has-padding">
<h4 class="subtitle" v-if="$root.$data.metadata.is_launcher || is_update">Checking for updates...</h4> <h4 class="subtitle" v-if="$root.$data.metadata.is_launcher || is_update">{{ $t('install_packages.check_for_update') }}</h4>
<h4 class="subtitle" v-else-if="is_uninstall">Uninstalling...</h4> <h4 class="subtitle" v-else-if="is_uninstall">{{ $t('install_packages.uninstall') }}</h4>
<h4 class="subtitle" v-else-if="is_updater_update">Downloading self-update...</h4> <h4 class="subtitle" v-else-if="is_updater_update">{{ $t('install_packages.self_update') }}</h4>
<h4 class="subtitle" v-else>Installing...</h4> <h4 class="subtitle" v-else>{{ $t('install_packages.install') }}</h4>
<div v-html="$root.$data.config.installing_message"></div> <div v-html="$root.$data.config.installing_message"></div>
<br /> <br />
@ -20,10 +20,11 @@ export default {
data: function () { data: function () {
return { return {
progress: 0.0, progress: 0.0,
progress_message: 'Please wait...', progress_message: this.$i18n.t('install_packages.please_wait'),
is_uninstall: false, is_uninstall: false,
is_updater_update: false, is_updater_update: false,
is_update: false, is_update: false,
is_repair: false,
install_desktop_shortcut: false, install_desktop_shortcut: false,
failed_with_error: false, failed_with_error: false,
authorization_required: false, authorization_required: false,
@ -35,30 +36,33 @@ export default {
this.is_updater_update = this.$route.params.kind === 'updater' this.is_updater_update = this.$route.params.kind === 'updater'
this.is_update = this.$route.params.kind === 'update' this.is_update = this.$route.params.kind === 'update'
this.install_desktop_shortcut = this.$route.params.desktop_shortcut === 'true' this.install_desktop_shortcut = this.$route.params.desktop_shortcut === 'true'
this.is_repair = this.$route.params.kind === 'repair'
console.log('Installer kind: ' + this.$route.params.kind) console.log('Installer kind: ' + this.$route.params.kind)
console.log('Installing desktop shortcut: ' + this.$route.params.desktop_shortcut) console.log('Installing desktop shortcut: ' + this.$route.params.desktop_shortcut)
this.install() this.install()
}, },
methods: { methods: {
install: function () { install: function () {
var that = this const that = this
var app = this.$root const app = this.$root
var results = {} const results = {}
var requires_authorization = false;
for (var package_index = 0; package_index < app.config.packages.length; package_index++) { for (let package_index = 0; package_index < app.config.packages.length; package_index++) {
var current_package = app.config.packages[package_index] const current_package = app.config.packages[package_index]
if (current_package.default != null) { if (current_package.default != null) {
requires_authorization |= current_package.requires_authorization;
results[current_package.name] = current_package.default results[current_package.name] = current_package.default
} }
} }
results['path'] = app.install_location results.path = app.install_location
results['installDesktopShortcut'] = that.install_desktop_shortcut results.installDesktopShortcut = that.install_desktop_shortcut
var targetUrl = '/api/start-install' if (this.is_repair) {
results.mode = 'force'
}
let targetUrl = '/api/start-install'
if (this.is_uninstall) { if (this.is_uninstall) {
targetUrl = '/api/uninstall' targetUrl = '/api/uninstall'
} }
@ -69,20 +73,20 @@ export default {
this.$root.stream_ajax(targetUrl, function (line) { this.$root.stream_ajax(targetUrl, function (line) {
// On progress line received from server // On progress line received from server
if (line.hasOwnProperty('Status')) { if (line.Status) {
that.progress_message = line.Status[0] that.progress_message = line.Status[0]
that.progress = line.Status[1] * 100 that.progress = line.Status[1] * 100
} }
if (line.hasOwnProperty('PackageInstalled')) { if (line.PackageInstalled) {
that.packages_installed += 1 that.packages_installed += 1
} }
if (line.hasOwnProperty('AuthorizationRequired')) { if (line.AuthorizationRequired) {
that.authorization_required = true that.authorization_required = true
} }
if (line.hasOwnProperty('Error')) { if (line.Error) {
that.failed_with_error = true that.failed_with_error = true
that.$router.replace({ name: 'showerr', params: { msg: line.Error } }) that.$router.replace({ name: 'showerr', params: { msg: line.Error } })
} }
@ -107,21 +111,23 @@ export default {
app.exit() app.exit()
} else if (!that.failed_with_error) { } else if (!that.failed_with_error) {
if (that.is_uninstall) { if (that.is_uninstall) {
that.$router.replace({ name: 'complete', that.$router.replace({
name: 'complete',
params: { params: {
uninstall: true, uninstall: true,
update: that.is_update, update: that.is_update,
migrate: false,
installed: that.packages_installed installed: that.packages_installed
} }) }
})
} else { } else {
that.$router.replace({ name: 'complete', that.$router.replace({
name: 'complete',
params: { params: {
uninstall: false, uninstall: false,
update: that.is_update, update: that.is_update,
migrate: false,
installed: that.packages_installed installed: that.packages_installed
} }) }
})
} }
} }
} }

View File

@ -25,33 +25,33 @@ export default {
}, },
created: function () { created: function () {
// See if we need to migrate yuzu to mainline // See if we need to migrate yuzu to mainline
var need_migrate = false; let need_migrate = false
for (var package_id in this.$root.metadata.database.packages) { for (const package_id in this.$root.metadata.database.packages) {
var name = this.$root.metadata.database.packages[package_id].name const name = this.$root.metadata.database.packages[package_id].name
if ((name.indexOf("Nightly") !== -1 || name.indexOf("Canary") !== -1)) { if ((name.indexOf('Nightly') !== -1 || name.indexOf('Canary') !== -1)) {
console.log("Migration needed (found \"" + name + "\", move to mainline)") console.log('Migration needed (found "' + name + '", move to mainline)')
// Migration step: deactivate this package // Migration step: deactivate this package
if (this.$root.config.packages[package_id] !== undefined) { if (this.$root.config.packages[package_id] !== undefined) {
this.$root.config.packages[package_id].default = false; this.$root.config.packages[package_id].default = false
} }
// Migration step: enable mainline // Migration step: enable mainline
for (var sub_package_id in this.$root.config.packages) { for (const sub_package_id in this.$root.config.packages) {
var name = this.$root.config.packages[sub_package_id].name const name = this.$root.config.packages[sub_package_id].name
if (name === "yuzu") { if (name === 'yuzu') {
this.$root.config.packages[sub_package_id].default = true; this.$root.config.packages[sub_package_id].default = true
break; break
} }
} }
need_migrate = true; need_migrate = true
} }
} }
console.log("Next stop: " + JSON.stringify(this.next_stop)); console.log('Next stop: ' + JSON.stringify(this.next_stop))
if (need_migrate) { if (need_migrate) {
this.next_stop = "/complete/false/true/true/[]" this.next_stop = '/complete/false/true/true/[]'
this.install() this.install()
} else { } else {
this.$router.replace(this.next_stop) this.$router.replace(this.next_stop)
@ -59,37 +59,37 @@ export default {
}, },
methods: { methods: {
install: function () { install: function () {
var that = this const that = this
var app = this.$root const app = this.$root
var results = {} const results = {}
for (var package_index = 0; package_index < app.config.packages.length; package_index++) { for (let package_index = 0; package_index < app.config.packages.length; package_index++) {
var current_package = app.config.packages[package_index] const current_package = app.config.packages[package_index]
if (current_package.default != null) { if (current_package.default != null) {
results[current_package.name] = current_package.default results[current_package.name] = current_package.default
} }
} }
console.log("Install results: " + JSON.stringify(results)); console.log('Install results: ' + JSON.stringify(results))
results['path'] = app.install_location results.path = app.install_location
var targetUrl = '/api/start-install' const targetUrl = '/api/start-install'
this.$root.stream_ajax(targetUrl, function (line) { this.$root.stream_ajax(targetUrl, function (line) {
// On progress line received from server // On progress line received from server
if (line.hasOwnProperty('Status')) { if (line.Status) {
that.progress_message = line.Status[0] that.progress_message = line.Status[0]
that.progress = line.Status[1] * 100 that.progress = line.Status[1] * 100
} }
if (line.hasOwnProperty('PackageInstalled')) { if (line.PackageInstalled) {
that.packages_installed += 1 that.packages_installed += 1
} }
if (line.hasOwnProperty('Error')) { if (line.Error) {
that.failed_with_error = true that.failed_with_error = true
that.$router.replace({ name: 'showerr', params: { msg: line.Error } }) that.$router.replace({ name: 'showerr', params: { msg: line.Error } })
} }

View File

@ -1,35 +1,34 @@
<template> <template>
<div class="column has-padding"> <div class="column has-padding">
<h4 class="subtitle">Choose an option:</h4> <h4 class="subtitle">{{ $t('modify.title') }}</h4>
<a class="button is-dark is-medium" v-on:click="update"> <b-button icon-left="update" type="is-dark-green" size="is-medium" @click="update">
Update {{ $t('modify.update') }}
</a> </b-button>
<br /> <br />
<br /> <br />
<a class="button is-dark is-medium" v-on:click="modify_packages"> <b-button icon-left="pencil" type="is-info" size="is-medium" @click="modify_packages">
Modify {{ $t('modify.modify') }}
</a> </b-button>
<br /> <br />
<br /> <br />
<a class="button is-dark is-medium" v-on:click="prepare_uninstall"> <b-button icon-left="wrench" type="is-info" size="is-medium" @click="repair_packages">
Uninstall {{ $t('modify.repair') }}
</a> </b-button>
<br />
<br />
<div class="modal is-active" v-if="show_uninstall"> <b-button icon-left="delete" type="is-danger" size="is-medium" @click="prepare_uninstall">
<div class="modal-background"></div> {{ $t('modify.uninstall') }}
<div class="modal-card"> </b-button>
<header class="modal-card-head"> <br />
<p class="modal-card-title">Are you sure you want to uninstall {{ $root.$data.attrs.name }}?</p> <br />
</header>
<footer class="modal-card-foot"> <b-button icon-left="file-find" type="is-link" size="is-medium" @click="view_files">
<button class="button is-danger" v-on:click="uninstall">Yes</button> {{ $t('modify.view_local_files') }}
<button class="button" v-on:click="cancel_uninstall">No</button> </b-button>
</footer>
</div>
</div>
</div> </div>
</template> </template>
@ -37,26 +36,74 @@
export default { export default {
name: 'ModifyView', name: 'ModifyView',
data: function () { data: function () {
return { return {}
show_uninstall: false },
mounted: function () {
if (this.$root.$data.attrs.recovery) {
this.recovery()
} }
}, },
methods: { methods: {
recovery: function () {
this.$buefy.dialog.alert({
title: this.$t('modify.repair'),
message: this.$t('modify.prompt_recover', { name: this.$root.$data.attrs.name }),
confirmText: this.$t('continue'),
type: 'is-danger',
hasIcon: true,
onConfirm: this.repair_packages
})
},
update: function () { update: function () {
this.$router.push('/install/update/false') this.$router.push('/install/update/false')
}, },
modify_packages: function () { modify_packages: function () {
this.$router.push('/packages') this.$router.push('/packages')
}, },
prepare_uninstall: function () { repair_packages: function () {
this.show_uninstall = true this.$buefy.dialog.confirm({
title: this.$t('modify.repair'),
message: this.$t('modify.prompt_repair', { path: this.$root.$data.metadata.install_path }),
cancelText: this.$t('cancel'),
confirmText: this.$t('continue'),
type: 'is-danger',
hasIcon: true,
onConfirm: () => { this.$router.push({ name: 'packages', params: { repair: true } }) }
})
}, },
cancel_uninstall: function () { prepare_uninstall: function () {
this.show_uninstall = false this.$buefy.dialog.confirm({
title: this.$t('modify.uninstall'),
message: this.$t('modify.prompt', { name: this.$root.$data.attrs.name }),
cancelText: this.$t('cancel'),
confirmText: this.$t('modify.prompt_confirm', { name: this.$root.$data.attrs.name }),
type: 'is-danger',
hasIcon: true,
onConfirm: this.uninstall
})
}, },
uninstall: function () { uninstall: function () {
this.$router.push('/install/uninstall/false') this.$router.push('/install/uninstall/false')
},
view_files: function () {
this.$http.get('/api/view-local-folder')
} }
} }
} }
</script> </script>
<style>
span {
cursor: unset !important;
}
.button.is-dark-green {
background-color: #00B245;
border-color: transparent;
color: #fff;
}
.button.is-dark-green:hover, .button.is-dark-green.is-hovered, .button.is-dark-green:focus {
background-color: #00a53f;
border-color: transparent;
color: #fff;
}
</style>

View File

@ -12,13 +12,13 @@
<div class="is-left-floating is-bottom-floating"> <div class="is-left-floating is-bottom-floating">
<p class="control"> <p class="control">
<a class="button is-medium" v-on:click="go_packages">Back</a> <a class="button is-medium" v-on:click="go_packages">{{ $t('back') }}</a>
</p> </p>
</div> </div>
<div class="is-right-floating is-bottom-floating"> <div class="is-right-floating is-bottom-floating">
<p class="control"> <p class="control">
<a class="button is-dark is-medium" v-on:click="launch_old_version">Launch Old Version</a> <a class="button is-info is-medium" v-on:click="launch_old_version">Launch Old Version</a>
</p> </p>
</div> </div>
</div> </div>
@ -27,7 +27,7 @@
<script> <script>
export default { export default {
name: "ReAuthenticationView", name: 'ReAuthenticationView',
methods: { methods: {
go_authenticate: function () { go_authenticate: function () {
this.$router.replace('/authentication') this.$router.replace('/authentication')

View File

@ -1,15 +1,19 @@
<template> <template>
<div class="column has-padding"> <div class="column has-padding">
<h4 class="subtitle" v-if="!repair">{{ $t('select_packages.title') }}</h4>
<h4 class="subtitle" v-if="repair">{{ $t('select_packages.title_repair') }}</h4>
<!-- Build options --> <!-- Build options -->
<div class="tile is-ancestor"> <div class="tile is-ancestor">
<div class="tile is-parent is-vertical"> <div class="tile is-parent selection-box" v-for="Lpackage in $root.$data.config.packages" :key="Lpackage.name" :index="Lpackage.name">
<div class="tile is-child is-12 box clickable-box" v-for="Lpackage in $root.$data.config.packages" :key="Lpackage.name" :index="Lpackage.name" v-on:click.capture.stop="clicked_box(Lpackage)"> <div class="tile is-child">
<div class="box clickable-box" v-on:click.capture.stop="clicked_box(Lpackage)">
<div class="ribbon" v-if="Lpackage.is_new"><span>New!</span></div> <div class="ribbon" v-if="Lpackage.is_new"><span>New!</span></div>
<label class="checkbox"> <label class="checkbox">
<b-checkbox v-model="Lpackage.default"> <b-checkbox v-model="Lpackage.default">
<span v-if="!Lpackage.installed">Install</span> {{ Lpackage.name }} {{ Lpackage.name }}
</b-checkbox> </b-checkbox>
<span v-if="Lpackage.installed"><i>(installed)</i></span> <span v-if="Lpackage.installed"><i>{{ $t('select_packages.installed') }}</i></span>
</label> </label>
<div> <div>
<img class="package-icon" :src="`${publicPath + Lpackage.icon}`"/> <img class="package-icon" :src="`${publicPath + Lpackage.icon}`"/>
@ -21,53 +25,53 @@
</p> </p>
</div> </div>
</div> </div>
</div>
</div>
</div>
<div class="tile is-child is-6 box clickable-box" v-if="!$root.$data.metadata.preexisting_install" v-on:click.capture.stop="installDesktopShortcut = !installDesktopShortcut"> <div class="tile is-child is-6 box clickable-box" v-if="!$root.$data.metadata.preexisting_install" v-on:click.capture.stop="installDesktopShortcut = !installDesktopShortcut">
<h4>Install Options</h4> <h4>{{ $t('select_packages.options') }}</h4>
<b-checkbox v-model="installDesktopShortcut" v-if="$root.$data.metadata.is_windows"> <b-checkbox v-model="installDesktopShortcut" v-if="$root.$data.metadata.is_windows">
Create Desktop Shortcut {{ $t('select_packages.option_shortcut') }}
</b-checkbox> </b-checkbox>
<b-checkbox v-model="installDesktopShortcut" v-if="!$root.$data.metadata.is_windows"> <b-checkbox v-model="installDesktopShortcut" v-if="!$root.$data.metadata.is_windows">
Create Shortcut Create Shortcut
</b-checkbox> </b-checkbox>
</div> </div>
</div>
</div>
<div class="subtitle is-6" v-if="!$root.$data.metadata.preexisting_install && advanced">Install Location</div> <div class="subtitle is-6" v-if="!$root.$data.metadata.preexisting_install && advanced">{{ $t('select_packages.location') }}</div>
<div class="field has-addons" v-if="!$root.$data.metadata.preexisting_install && advanced"> <div class="field has-addons" v-if="!$root.$data.metadata.preexisting_install && advanced">
<div class="control is-expanded"> <div class="control is-expanded">
<input class="input" type="text" v-model="$root.$data.install_location" <input class="input" type="text" v-model="$root.$data.install_location"
placeholder="Enter a install path here"> :placeholder="$t('select_packages.location_placeholder')">
</div> </div>
<div class="control"> <div class="control">
<a class="button is-dark" v-on:click="select_file"> <b-button class="is-link is-info" v-on:click="select_file">
Select {{ $t('select_packages.select') }}
</a> </b-button>
</div> </div>
</div> </div>
<div class="is-right-floating is-bottom-floating"> <div class="is-right-floating is-bottom-floating">
<div class="field is-grouped"> <div class="field is-grouped">
<p class="control"> <p class="control">
<a class="button is-medium" v-if="!$root.$data.config.hide_advanced && !$root.$data.metadata.preexisting_install && !advanced" <b-button class="is-primary is-medium" v-if="!$root.$data.config.hide_advanced && !$root.$data.metadata.preexisting_install && !advanced"
v-on:click="advanced = true">Advanced...</a> v-on:click="advanced = true">{{ $t('select_packages.advanced') }}</b-button>
</p> </p>
<p class="control"> <p class="control">
<!-- Disable the Install button on a fresh install with no packages selected --> <b-button class="is-primary is-medium" v-if="!$root.$data.metadata.preexisting_install"
<button v-if="$root.$data.metadata.preexisting_install" class="button is-medium is-dark" v-on:click="install"> v-on:click="install">{{ $t('select_packages.install') }}</b-button>
Modify </p>
</button> <p class="control">
<button v-else class="button is-medium is-dark" v-on:click="install" :disabled="!this.has_package_selected"> <a class="button is-primary is-medium" v-if="$root.$data.metadata.preexisting_install"
Install v-on:click="install">{{ repair ? $t('select_packages.repair') : $t('select_packages.modify') }}</a>
</button>
</p> </p>
</div> </div>
</div> </div>
<div class="field is-grouped is-left-floating is-bottom-floating"> <div class="field is-grouped is-left-floating is-bottom-floating">
<p class="control"> <p class="control">
<a class="button is-medium" v-if="$root.$data.metadata.preexisting_install" <b-button class="is-dark is-medium" v-if="$root.$data.metadata.preexisting_install"
v-on:click="go_back">Back</a> v-on:click="go_back">{{ $t('back') }}</b-button>
</p> </p>
</div> </div>
</div> </div>
@ -76,44 +80,81 @@
<script> <script>
export default { export default {
name: 'SelectPackages', name: 'SelectPackages',
created: function() { data: function () {
return {
publicPath: process.env.BASE_URL,
advanced: true,
repair: false,
installDesktopShortcut: true
}
},
mounted: function () {
this.repair = this.$route.params.repair
// EA
// If they are authorized, make the packages that require authorization default // If they are authorized, make the packages that require authorization default
// and also deselect any packages that don't use authorization // and also deselect any packages that don't use authorization
if (this.$root.$data.has_reward_tier) { if (this.$root.$data.has_reward_tier) {
for (let package_index = 0; package_index < this.$root.config.packages.length; package_index++) { for (let package_index = 0; package_index < this.$root.config.packages.length; package_index++) {
let current_package = this.$root.config.packages[package_index]; const current_package = this.$root.config.packages[package_index]
current_package.default = current_package.requires_authorization; current_package.default = current_package.requires_authorization
} }
} }
}, },
data: function () {
return {
publicPath: process.env.BASE_URL,
advanced: false,
installDesktopShortcut: true
}
},
computed: {
has_package_selected: function() {
for (let i=0; i < this.$root.config.packages.length; ++i) {
let pkg = this.$root.config.packages[i];
if (pkg.default) {
return true;
}
}
return false;
}
},
methods: { methods: {
select_file: function () { select_file: function () {
window.external.invoke(JSON.stringify({ const that = this
SelectInstallDir: { window.rpc.call('SelectInstallDir').then(function (name) {
callback_name: 'selectFileCallback' if (name) {
that.$root.$data.install_location = name
} }
})) })
},
show_overwrite_dialog: function (confirmCallback) {
this.$buefy.dialog.confirm({
title: this.$t('select_packages.overwriting'),
message: this.$t('select_packages.overwriting_warning', { path: this.$root.$data.install_location }),
confirmText: this.$t('continue'),
cancelText: this.$t('cancel'),
type: 'is-danger',
hasIcon: true,
onConfirm: confirmCallback
})
},
show_nothing_picked_dialog: function () {
this.$buefy.dialog.alert({
title: this.$t('select_packages.nothing_picked'),
message: this.$t('select_packages.nothing_picked_warning', { path: this.$root.$data.install_location }),
confirmText: this.$t('cancel'),
type: 'is-danger',
hasIcon: true
})
}, },
install: function () { install: function () {
if (!this.$root.config.packages.some(function (x) { return x.default })) {
this.show_nothing_picked_dialog()
return
}
// maintenance + repair
if (this.repair) {
this.$router.push('/install/repair/' + this.installDesktopShortcut.toString())
return
}
// maintenance + modify
if (this.$root.$data.metadata.preexisting_install) {
this.$router.push('/install/regular/' + this.installDesktopShortcut.toString()) this.$router.push('/install/regular/' + this.installDesktopShortcut.toString())
return
}
const my = this
this.$http.post('/api/verify-path', `path=${this.$root.$data.install_location}`).then(function (resp) {
const data = resp.data || {}
if (!data.exists) {
my.$router.push('/install/regular/' + my.installDesktopShortcut.toString())
} else {
my.show_overwrite_dialog(function () {
my.$router.push('/install/repair/' + my.installDesktopShortcut.toString())
})
}
})
}, },
go_back: function () { go_back: function () {
this.$router.go(-1) this.$router.go(-1)
@ -125,11 +166,11 @@
this.$router.push('/authentication') this.$router.push('/authentication')
}, },
installable: function (pkg) { installable: function (pkg) {
return !pkg.requires_authorization || (pkg.requires_authorization && this.$root.$data.has_reward_tier); return !pkg.requires_authorization || (pkg.requires_authorization && this.$root.$data.has_reward_tier)
}, },
clicked_box: function (pkg) { clicked_box: function (pkg) {
if (this.installable(pkg)) { if (this.installable(pkg)) {
pkg.default = !pkg.default; pkg.default = !pkg.default
} else if (pkg.requires_authorization && !this.$root.$data.is_authenticated) { } else if (pkg.requires_authorization && !this.$root.$data.is_authenticated) {
this.show_authentication() this.show_authentication()
} else if (pkg.requires_authorization && !this.$root.$data.is_linked) { } else if (pkg.requires_authorization && !this.$root.$data.is_linked) {
@ -142,20 +183,26 @@
}, },
get_extended_description: function (pkg) { get_extended_description: function (pkg) {
if (!pkg.extended_description) { if (!pkg.extended_description) {
return ""; return ''
} }
if (this.installable(pkg)) { if (this.installable(pkg)) {
return pkg.extended_description.no_action_description; return pkg.extended_description.no_action_description
} else if (pkg.requires_authorization && !this.$root.$data.is_authenticated) { } else if (pkg.requires_authorization && !this.$root.$data.is_authenticated) {
return pkg.extended_description.need_authentication_description; return pkg.extended_description.need_authentication_description
} else if (pkg.requires_authorization && !this.$root.$data.is_linked) { } else if (pkg.requires_authorization && !this.$root.$data.is_linked) {
return pkg.extended_description.need_link_description; return pkg.extended_description.need_link_description
} else if (pkg.requires_authorization && !this.$root.$data.is_subscribed) { } else if (pkg.requires_authorization && !this.$root.$data.is_subscribed) {
return pkg.extended_description.need_subscription_description; return pkg.extended_description.need_subscription_description
} else { // need_reward_tier_description } else { // need_reward_tier_description
return pkg.extended_description.need_reward_tier_description; return pkg.extended_description.need_reward_tier_description
} }
} }
} }
} }
</script> </script>
<style>
.selection-box {
cursor: pointer;
}
</style>

View File

@ -0,0 +1,30 @@
const path = require('path')
const fs = require('fs')
const input_file = process.argv[2]
const output_file = process.argv[3]
const mappings = {
select: 'select_packages',
install: 'install_packages',
download: 'download_packages'
}
console.info(`Fixing ${input_file} ...`)
const lang = path.basename(input_file).replace('.json', '').replace('_', '-')
const translations = require(path.resolve(input_file))
translations[lang] = translations.en
delete translations.en
translations[lang].modify.modify = translations[lang].select['modify en'].modify.modify
delete translations[lang].select['modify en']
translations[lang].modify.repair = translations[lang].select['repair en'].modify.repair
delete translations[lang].select['repair en']
for (const i of Object.keys(mappings)) {
translations[lang][mappings[i]] = translations[lang][i]
delete translations[lang][i]
}
fs.writeFileSync(output_file, JSON.stringify(translations, null, 2))

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