Merge remote-tracking branch 'fix-usability' into yuzu

This commit is contained in:
liushuyu 2021-07-28 18:18:38 -06:00
commit a816cbe767
69 changed files with 6866 additions and 5452 deletions

53
.github/workflows/test-build.yml vendored Normal file
View File

@ -0,0 +1,53 @@
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
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: '12.x'
- run: npm install -g yarn
- uses: actions/checkout@v2
- name: Build
run: cargo build --verbose

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

2566
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
[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"
@ -8,35 +9,36 @@ description = "An adaptable installer for your application."
build = "build.rs" build = "build.rs"
[dependencies] [dependencies]
web-view = {git = "https://github.com/j-selby/web-view.git", rev = "752106e4637356cbdb39a0bf1113ea3ae8a14243"} web-view = { version = "0.7", features = ["edge"] }
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 = "3.0"
zip = "0.5.1" zip = "0.5"
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.5.2"
@ -46,19 +48,19 @@ jsonwebtoken = "6"
base64 = "0.10.1" base64 = "0.10.1"
[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"
[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 = "0.4"
[target.'cfg(not(windows))'.dependencies] [target.'cfg(not(windows))'.dependencies]
sysinfo = "0.8.2" sysinfo = "0.18"
slug = "0.1.4" slug = "0.1"
[target.'cfg(windows)'.build-dependencies] [target.'cfg(windows)'.build-dependencies]
winres = "0.1" winres = "0.1"

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

@ -46,6 +46,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");
} }
@ -102,7 +104,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 +116,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)]

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;

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

@ -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
@ -38,6 +39,10 @@ pub fn handle(service: &WebService, req: Request) -> Future {
} else if key == "installDesktopShortcut" { } else if key == "installDesktopShortcut" {
info!("Found installDesktopShortcut {:?}", value); info!("Found installDesktopShortcut {:?}", value);
install_desktop_shortcut = value == "true"; install_desktop_shortcut = value == "true";
}
if key == "mode" && value == "force" {
force_install = true;
continue; continue;
} }
@ -60,7 +65,7 @@ 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,49 @@
//! 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

@ -4,7 +4,7 @@
use web_view::Content; use web_view::Content;
use logging::LoggingErrors; use crate::logging::LoggingErrors;
use log::Level; use log::Level;
@ -16,8 +16,8 @@ enum CallbackType {
} }
/// 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) {
let size = (1024, 550); let size = if is_launcher { (600, 300) } else { (1024, 500) };
info!("Spawning web view instance"); info!("Spawning web view instance");
@ -26,7 +26,7 @@ pub fn start_ui(app_name: &str, http_address: &str, _is_launcher: bool) {
.content(Content::Url(http_address)) .content(Content::Url(http_address))
.size(size.0, size.1) .size(size.0, size.1)
.resizable(false) .resizable(false)
.debug(false) .debug(cfg!(debug_assertions))
.user_data(()) .user_data(())
.invoke_handler(|wv, msg| { .invoke_handler(|wv, msg| {
let mut cb_result = Ok(()); let mut cb_result = Ok(());
@ -37,15 +37,14 @@ pub fn start_ui(app_name: &str, http_address: &str, _is_launcher: bool) {
match command { match command {
CallbackType::SelectInstallDir { callback_name } => { CallbackType::SelectInstallDir { callback_name } => {
let result = wv let result =
.dialog() tinyfiledialogs::select_folder_dialog("Select a install directory...", "");
.choose_directory("Select a install directory...", "");
if let Ok(Some(new_path)) = result { if let Some(new_path) = result {
if new_path.to_string_lossy().len() > 0 { if !new_path.is_empty() {
let result = serde_json::to_string(&new_path) let result = serde_json::to_string(&new_path)
.log_expect("Unable to serialize response"); .log_expect("Unable to serialize response");
let command = format!("{}({});", callback_name, result); let command = format!("window.{}({});", callback_name, result);
debug!("Injecting response: {}", command); debug!("Injecting response: {}", command);
cb_result = wv.eval(&command); cb_result = wv.eval(&command);
} }

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,28 @@ 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::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)]
@ -174,12 +174,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 {:?}",
@ -209,6 +211,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);

View File

@ -74,7 +74,7 @@ use config::BaseAttributes;
use std::process::{Command, Stdio, exit}; use std::process::{Command, Stdio, exit};
use std::fs; use std::fs;
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()

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;
@ -168,15 +168,3 @@ extern "C" HRESULT getSystemFolder(wchar_t *out_path)
} }
return result; return result;
} }
extern "C" HRESULT getDesktopFolder(wchar_t *out_path)
{
PWSTR path = NULL;
HRESULT result = SHGetKnownFolderPath(FOLDERID_Desktop, 0, NULL, &path);
if (result == S_OK)
{
wcscpy_s(out_path, MAX_PATH + 1, path);
CoTaskMemFree(path);
}
return result;
}

View File

@ -15,9 +15,12 @@ mod natives {
const PROCESS_LEN: usize = 10192; const PROCESS_LEN: usize = 10192;
use logging::LoggingErrors; use crate::logging::LoggingErrors;
use std::env; use std::env;
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use std::process::Command;
use winapi::shared::minwindef::{DWORD, FALSE, MAX_PATH}; use winapi::shared::minwindef::{DWORD, FALSE, MAX_PATH};
@ -26,11 +29,13 @@ 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 widestring::U16CString;
extern "C" { extern "C" {
pub fn saveShortcut( pub fn saveShortcut(
@ -50,28 +55,6 @@ mod natives {
) -> ::std::os::raw::c_int; ) -> ::std::os::raw::c_int;
pub fn getSystemFolder(out_path: *mut ::std::os::raw::c_ushort) -> HRESULT; pub fn getSystemFolder(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
#[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
@ -139,15 +122,23 @@ mod natives {
} }
} }
fn count_u16(u16str: &[u16]) -> usize { // Needed to call unsafe function `ShellExecuteW` from `winapi` crate
let mut pos = 0; #[allow(unsafe_code)]
for x in u16str.iter() { pub fn open_in_shell(path: &Path) {
if *x == 0 { let native_verb = U16CString::from_str("open").unwrap();
break; // 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,
);
} }
pos += 1;
}
pos
} }
/// Cleans up the installer /// Cleans up the installer
@ -179,13 +170,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(),
@ -297,7 +295,7 @@ mod natives {
use std::env; use std::env;
use logging::LoggingErrors; use crate::logging::LoggingErrors;
use sysinfo::{ProcessExt, SystemExt}; use sysinfo::{ProcessExt, SystemExt};
@ -306,6 +304,8 @@ mod natives {
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(
@ -314,6 +314,7 @@ 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> {
// FIXME: no icon will be shown since no icon is provided // FIXME: no icon will be shown since no icon is provided
let data_local_dir = dirs::data_local_dir(); let data_local_dir = dirs::data_local_dir();
@ -322,7 +323,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: {}",
@ -340,7 +341,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)),
@ -358,11 +359,25 @@ 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");
@ -387,7 +402,7 @@ 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.get_processes() {
processes.push(super::Process { processes.push(super::Process {
pid: *pid as usize, pid: *pid 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

@ -24,7 +24,7 @@ impl Version {
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::from((version.to_owned(), 0 as u64, 0 as u64)) SemverVersion::new(version.to_owned(), 0u64, 0u64)
} }
} }
} }

View File

@ -1,15 +1,21 @@
//! 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::tasks::resolver::ResolvePackageTask;
use number_prefix::{NumberPrefix, Prefixed, Standalone}; use crate::http::stream_file;
use logging::LoggingErrors; use number_prefix::NumberPrefix::{self, Prefixed, Standalone};
use crate::logging::LoggingErrors;
pub struct DownloadPackageTask { pub struct DownloadPackageTask {
pub name: String, pub name: 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::remove_target_dir::RemoveTargetDirTask;
use tasks::uninstall_pkg::UninstallPackageTask; use crate::tasks::save_executable::SaveExecutableTask;
use tasks::launch_installed_on_exit::LaunchOnExitTask; use crate::tasks::uninstall_pkg::UninstallPackageTask;
use crate::tasks::launch_installed_on_exit::LaunchOnExitTask;
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 {

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 {}

View File

@ -1,26 +1,26 @@
//! 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 std::fs::OpenOptions; use std::fs::OpenOptions;
use std::path::Path; use std::path::Path;
@ -136,7 +136,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,7 +165,7 @@ 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: Vec::new(),
files: installed_files, files: installed_files,

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,

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;
@ -26,6 +26,7 @@ pub mod uninstall;
pub mod uninstall_global_shortcut; pub mod uninstall_global_shortcut;
pub mod uninstall_pkg; pub mod uninstall_pkg;
pub mod uninstall_shortcuts; pub mod uninstall_shortcuts;
pub mod remove_target_dir;
/// An abstraction over the various parameters that can be passed around. /// An abstraction over the various parameters that can be passed around.
pub enum TaskParamType { pub enum TaskParamType {

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,

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

@ -3,13 +3,15 @@ module.exports = {
env: { env: {
node: true node: true
}, },
'extends': [ extends: [
'plugin:vue/essential', 'plugin:vue/essential',
'@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',
camelcase: 'off'
}, },
parserOptions: { parserOptions: {
parser: 'babel-eslint' 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,7 +4,20 @@ 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
function progressSimulation (res) { function progressSimulation (res) {
if (showError) {
var resp = JSON.stringify({ Error: 'Simulated error.' }) + '\n'
res.write(resp)
res.status(200).end()
return
}
var progress = 0.0 var progress = 0.0
var timer = setInterval(() => { var timer = setInterval(() => {
var resp = JSON.stringify({ Status: ['Processing...', progress] }) + '\n' var resp = JSON.stringify({ Status: ['Processing...', progress] }) + '\n'
@ -14,10 +27,14 @@ function progressSimulation (res) {
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 +69,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', 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 +100,54 @@ 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: ${req}`)
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
})
})
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
}
}) })
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": "^5.9.55",
"vue": "^2.6.6", "axios": "^0.21.1",
"vue-router": "^3.0.1" "buefy": "^0.9.7",
"vue": "^2.6.14",
"vue-axios": "^3.2.4",
"vue-i18n": "^8.24.4",
"vue-router": "^3.5.1"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^3.5.0", "@vue/cli-plugin-babel": "^4.5.13",
"@vue/cli-plugin-eslint": "^3.5.0", "@vue/cli-plugin-eslint": "^4.5.13",
"@vue/cli-service": "^3.5.0", "@vue/cli-service": "^4.5.13",
"@vue/eslint-config-standard": "^4.0.0", "@vue/eslint-config-standard": "^6.0.0",
"babel-eslint": "^10.0.1", "babel-eslint": "^10.1.0",
"eslint": "^5.8.0", "eslint": "^7.28.0",
"eslint-plugin-vue": "^5.0.0", "eslint-plugin-import": "^2.23.4",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-standard": "^4.1.0",
"eslint-plugin-vue": "^7.10.0",
"express": "^4.17.1", "express": "^4.17.1",
"http-proxy-middleware": "^0.19.1", "http-proxy-middleware": "^2.0.0",
"vue-template-compiler": "^2.5.21" "vue-template-compiler": "^2.6.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">
<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 {
@ -148,7 +183,7 @@ pre {
} }
/* 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%);
} }

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.
@ -108,7 +54,7 @@ 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')
@ -116,7 +62,7 @@ export function stream_ajax (path, callback, successCallback, failCallback, data
var form = '' var form = ''
for (var key in data) { for (var 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

66
ui/src/locales/en.json Normal file
View File

@ -0,0 +1,66 @@
{
"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 {path} 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!"
},
"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."
},
"modify":{
"title":"Choose an option:",
"update":"Update",
"modify":"Modify",
"repair": "Repair",
"uninstall":"Uninstall",
"view_local_files": "View local files",
"prompt":"Are you sure you want to uninstall {name}?",
"prompt_confirm":"Uninstall {name}"
},
"back":"Back",
"exit":"Exit",
"yes":"Yes",
"no":"No",
"continue": "Continue",
"cancel":"Cancel"
}
}

View File

@ -1,17 +1,30 @@
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'
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.external.invoke(
JSON.stringify({ JSON.stringify({
Log: { Log: {
@ -24,18 +37,18 @@ function intercept (method) {
} }
// 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.external.invoke(JSON.stringify({
Test: {} Test: {}
})) }))
has_external_interface = true; hasExternalInterface = 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.external.invoke(
JSON.stringify({ JSON.stringify({
@ -47,14 +60,14 @@ if (has_external_interface) {
) )
} }
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 +76,32 @@ 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)
document.getElementById('window-title').innerText = axios.get('/api/attrs').then(function (resp) {
base_attributes.name + ' Installer' document.getElementById('window-title').innerText =
i18n.t('app.window_title', { name: resp.data.name })
}).catch(function (err) {
console.error(err)
})
function selectFileCallback (name) { function selectFileCallback (name) {
app.install_location = name app.install_location = name
} }
var app = new Vue({ window.selectFileCallback = selectFileCallback
const app = new Vue({
i18n: i18n,
router: router, router: router,
data: { data: {
attrs: base_attributes, attrs: {},
config: {}, config: {},
install_location: '', install_location: '',
username: '', username: '',
@ -102,54 +122,61 @@ var app = new Vue({
render: function (caller) { render: function (caller) {
return caller(App) return caller(App)
}, },
mounted: function () {
axios.get('/api/attrs').then(function (resp) {
app.attrs = resp.data
}).catch(function (err) {
console.error(err)
})
},
methods: { methods: {
exit: function () { exit: function () {
ajax( axios.get('/api/exit').catch(function (msg) {
'/api/exit', const searchLocation = (app.metadata.install_path && app.metadata.install_path.length > 0)
function () {}, ? app.metadata.install_path
function (msg) { : i18n.t('error.location_unknown')
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 + app.$router.replace({
'\n\nPlease upload the log file (in ' + search_location + ') to ' + name: 'showerr',
'the ' + app.attrs.name + ' team' params: {
}}); msg: i18n.t('error.exit_error', {
}, name: app.attrs.name,
{} // pass in nothing to cause `ajax` to post instead of get path: searchLocation,
) msg: msg
})
}
})
})
}, },
check_authentication: function (success, error) { check_authentication: function (success, error) {
var that = this; const that = this
var app = this.$root; const app = this.$root
app.ajax('/api/check-auth', function (auth) { app.ajax('/api/check-auth', function (auth) {
app.$data.username = auth.username; app.$data.username = auth.username
app.$data.token = auth.token; app.$data.token = auth.token
that.jwt_token = auth.jwt_token; that.jwt_token = auth.jwt_token
that.is_authenticated = Object.keys(that.jwt_token).length !== 0 && that.jwt_token.constructor === Object; that.is_authenticated = Object.keys(that.jwt_token).length !== 0 && that.jwt_token.constructor === Object
if (that.is_authenticated) { if (that.is_authenticated) {
// Give all permissions to vip roles // Give all permissions to vip roles
that.is_linked = that.jwt_token.isPatreonAccountLinked; that.is_linked = that.jwt_token.isPatreonAccountLinked
that.is_subscribed = that.jwt_token.isPatreonSubscriptionActive; that.is_subscribed = that.jwt_token.isPatreonSubscriptionActive
that.has_reward_tier = that.jwt_token.releaseChannels.indexOf("early-access") > -1; that.has_reward_tier = that.jwt_token.releaseChannels.indexOf('early-access') > -1
} }
if (success) { if (success) {
success(); success()
} }
}, function (e) { }, function (e) {
if (error) { if (error) {
error(); error()
} }
}, { }, {
"username": app.$data.username, username: app.$data.username,
"token": app.$data.token token: app.$data.token
}) })
}, },
stream_ajax: streamAjax
ajax: ajax,
stream_ajax: stream_ajax
} }
}).$mount('#app') }).$mount('#app')
console.log("Vue started") console.log('Vue started')

View File

@ -9,30 +9,30 @@
</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 start 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 start 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 start menu.</p> <p>{{ $t('complete.where_to_find') }}</p>
<br> <br>
<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> <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> </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-dark is-medium" v-on:click="exit">{{ $t('exit') }}</b-button>
</p> </p>
</div> </div>
</div> </div>
@ -46,12 +46,12 @@ 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: {
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

@ -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,9 +36,7 @@
export default { export default {
name: 'ModifyView', name: 'ModifyView',
data: function () { data: function () {
return { return {}
show_uninstall: false
}
}, },
methods: { methods: {
update: function () { update: function () {
@ -48,15 +45,42 @@ export default {
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.$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

@ -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" 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="Lpackage.default = !Lpackage.default">
<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,6 +25,7 @@
</p> </p>
</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>Install Options</h4>
<b-checkbox v-model="installDesktopShortcut"> <b-checkbox v-model="installDesktopShortcut">
@ -30,77 +35,67 @@
</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-dark" 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-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-dark 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-dark 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-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>
</template> </template>
<script> <script>
export default { export default {
name: 'SelectPackages', name: 'SelectPackages',
created: function() { data: function () {
return {
advanced: false,
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({ window.external.invoke(JSON.stringify({
@ -109,8 +104,52 @@
} }
})) }))
}, },
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 () {
this.$router.push('/install/regular/' + this.installDesktopShortcut.toString()) 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')
return
}
// maintenance + modify
if (this.$root.$data.metadata.preexisting_install) {
this.$router.push('/install/regular')
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')
} else {
my.show_overwrite_dialog(function () {
my.$router.push('/install/repair')
})
}
})
}, },
go_back: function () { go_back: function () {
this.$router.go(-1) this.$router.go(-1)
@ -122,11 +161,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) {
@ -137,22 +176,22 @@
this.show_authorization() this.show_authorization()
} }
}, },
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>

File diff suppressed because it is too large Load Diff