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]
name = "liftinstall"
version = "0.1.0"
version = "0.2.0"
edition = "2018"
authors = ["James <jselby@jselby.net>"]
repository = "https://github.com/j-selby/liftinstall.git"
documentation = "https://liftinstall.jselby.net"
@ -8,35 +9,36 @@ description = "An adaptable installer for your application."
build = "build.rs"
[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"
futures = "0.1.25"
mime_guess = "1.8.6"
url = "1.7.2"
futures = "0.1.29"
mime_guess = "2.0"
url = "2.2"
reqwest = "0.9.21"
number_prefix = "0.3.0"
reqwest = "0.9.22"
number_prefix = "0.4"
serde = "1.0.89"
serde_derive = "1.0.89"
serde_json = "1.0.39"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
toml = "0.5.0"
toml = "0.5"
semver = {version = "0.9.0", features = ["serde"]}
regex = "1.1.5"
semver = {version = "1.0", features = ["serde"]}
regex = "1.4"
dirs = "1.0.5"
zip = "0.5.1"
xz2 = "0.1.6"
dirs = "3.0"
zip = "0.5"
xz2 = "0.1"
tar = "0.4"
log = "0.4"
fern = "0.5"
chrono = "0.4.6"
fern = "0.6"
chrono = "0.4"
clap = "2.32.0"
clap = "2.33"
# used to open a link to the users default browser
webbrowser = "0.5.2"
@ -46,19 +48,19 @@ jsonwebtoken = "6"
base64 = "0.10.1"
[build-dependencies]
walkdir = "2.2.7"
serde = "1.0.89"
serde_derive = "1.0.89"
toml = "0.5.0"
which = "2.0.1"
walkdir = "2.3"
serde = "1.0"
serde_derive = "1.0"
toml = "0.5"
which = "4.0"
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["psapi", "winbase", "winioctl", "winnt"] }
widestring = "0.4.0"
widestring = "0.4"
[target.'cfg(not(windows))'.dependencies]
sysinfo = "0.8.2"
slug = "0.1.4"
sysinfo = "0.18"
slug = "0.1"
[target.'cfg(windows)'.build-dependencies]
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()
.cpp(true)
.define("_WIN32_WINNT", Some("0x0600"))
.define("WINVER", Some("0x0600"))
.file("src/native/interop.cpp")
.compile("interop");
}
@ -102,7 +104,7 @@ fn main() {
.unwrap()
.wait()
.expect("Unable to install Node.JS dependencies using Yarn");
Command::new(&yarn_binary)
let return_code = Command::new(&yarn_binary)
.args(&[
"--cwd",
ui_dir.to_str().expect("Unable to covert path"),
@ -114,8 +116,7 @@ fn main() {
.to_str()
.expect("Unable to convert path"),
])
.spawn()
.unwrap()
.wait()
.status()
.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;
}
func(i, Some(max), archive.sanitized_name(), &mut archive)?;
func(i, Some(max), archive.mangled_name(), &mut archive)?;
}
Ok(())

View file

@ -7,8 +7,8 @@ use toml::de::Error as TomlError;
use serde_json::{self, Error as SerdeError};
use sources::get_by_name;
use sources::types::Release;
use crate::sources::get_by_name;
use crate::sources::types::Release;
/// Description of the source of a package.
#[derive(Debug, Deserialize, Serialize, Clone)]

View file

@ -4,8 +4,8 @@
use std::sync::{Arc, RwLock};
use installer::InstallerFramework;
use logging::LoggingErrors;
use crate::installer::InstallerFramework;
use crate::logging::LoggingErrors;
pub mod rest;
mod ui;

View file

@ -2,7 +2,8 @@
extern crate mime_guess;
use self::mime_guess::{get_mime_type, octet_stream};
use self::mime_guess::from_ext;
use self::mime_guess::mime::APPLICATION_OCTET_STREAM;
macro_rules! include_files_as_assets {
( $target_match:expr, $( $file_name:expr ),* ) => {
@ -23,9 +24,9 @@ pub fn file_from_string(file_path: &str) -> Option<(String, &'static [u8])> {
Some(ext_ptr) => {
let ext = &file_path[ext_ptr + 1..];
get_mime_type(ext)
from_ext(ext).first_or_octet_stream()
}
None => octet_stream(),
None => APPLICATION_OCTET_STREAM,
};
let string_mime = guessed_mime.to_string();
@ -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.woff",
"/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.map",
"/js/app.js",
"/js/app.js.map"
"/js/app.js"
)?;
Some((string_mime, contents))

View file

@ -2,11 +2,11 @@
//!
//! Contains the over-arching server object + methods to manipulate it.
use frontend::rest::services::WebService;
use crate::frontend::rest::services::WebService;
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use logging::LoggingErrors;
use crate::logging::LoggingErrors;
use hyper::server::Http;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,14 +2,14 @@
//!
//! The /api/install call installs a set of packages dictated by a POST request.
use frontend::rest::services::stream_progress;
use frontend::rest::services::Future;
use frontend::rest::services::Request;
use frontend::rest::services::WebService;
use crate::frontend::rest::services::stream_progress;
use crate::frontend::rest::services::Future;
use crate::frontend::rest::services::Request;
use crate::frontend::rest::services::WebService;
use logging::LoggingErrors;
use crate::logging::LoggingErrors;
use installer::InstallMessage;
use crate::installer::InstallMessage;
use futures::future::Future as _;
use futures::stream::Stream;
@ -28,6 +28,7 @@ pub fn handle(service: &WebService, req: Request) -> Future {
let mut to_install = Vec::new();
let mut path: Option<String> = None;
let mut force_install = false;
let mut install_desktop_shortcut= false;
// Transform results into just an array of stuff to install
@ -38,6 +39,10 @@ pub fn handle(service: &WebService, req: Request) -> Future {
} else if key == "installDesktopShortcut" {
info!("Found installDesktopShortcut {:?}", value);
install_desktop_shortcut = value == "true";
}
if key == "mode" && value == "force" {
force_install = true;
continue;
}
@ -60,7 +65,7 @@ pub fn handle(service: &WebService, req: Request) -> Future {
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);
if let Err(v) = sender.send(InstallMessage::Error(v)) {
error!("Failed to send install error: {:?}", v);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

@ -21,28 +21,28 @@ use std::io::Cursor;
use std::process::Command;
use std::process::{exit, Stdio};
use config::BaseAttributes;
use config::Config;
use crate::config::BaseAttributes;
use crate::config::Config;
use sources::types::Version;
use crate::sources::types::Version;
use tasks::install::InstallTask;
use tasks::uninstall::UninstallTask;
use tasks::uninstall_global_shortcut::UninstallGlobalShortcutsTask;
use tasks::DependencyTree;
use tasks::TaskMessage;
use crate::tasks::install::InstallTask;
use crate::tasks::uninstall::UninstallTask;
use crate::tasks::uninstall_global_shortcut::UninstallGlobalShortcutsTask;
use crate::tasks::DependencyTree;
use crate::tasks::TaskMessage;
use logging::LoggingErrors;
use crate::logging::LoggingErrors;
use dirs::home_dir;
use std::fs::remove_file;
use http;
use crate::http;
use number_prefix::{NumberPrefix, Prefixed, Standalone};
use number_prefix::NumberPrefix::{self, Prefixed, Standalone};
use native;
use crate::native;
/// A message thrown during the installation of packages.
#[derive(Serialize)]
@ -174,12 +174,14 @@ impl InstallerFramework {
/// items: Array of named packages to be installed/kept
/// messages: Channel used to send progress messages
/// fresh_install: If the install directory must be empty
/// force_install: If the install directory should be erased first
pub fn install(
&mut self,
items: Vec<String>,
messages: &Sender<InstallMessage>,
fresh_install: bool,
create_desktop_shortcuts: bool,
force_install: bool,
) -> Result<(), String> {
info!(
"Framework: Installing {:?} to {:?}",
@ -209,6 +211,7 @@ impl InstallerFramework {
uninstall_items,
fresh_install,
create_desktop_shortcuts,
force_install
});
let mut tree = DependencyTree::build(task);

View file

@ -74,7 +74,7 @@ use config::BaseAttributes;
use std::process::{Command, Stdio, exit};
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() {
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);
// Handle self-updating if needed
let current_exe = std::env::current_exe().log_expect("Current executable could not be found");
let current_path = current_exe
.parent()

View file

@ -46,7 +46,7 @@ extern "C" int saveShortcut(
const wchar_t *workingDir,
const wchar_t *exePath)
{
const char *errStr = NULL;
char *errStr = NULL;
HRESULT h;
IShellLink *shellLink = NULL;
IPersistFile *persistFile = NULL;
@ -168,15 +168,3 @@ extern "C" HRESULT getSystemFolder(wchar_t *out_path)
}
return result;
}
extern "C" HRESULT getDesktopFolder(wchar_t *out_path)
{
PWSTR path = NULL;
HRESULT result = SHGetKnownFolderPath(FOLDERID_Desktop, 0, NULL, &path);
if (result == S_OK)
{
wcscpy_s(out_path, MAX_PATH + 1, path);
CoTaskMemFree(path);
}
return result;
}

View file

@ -15,9 +15,12 @@ mod natives {
const PROCESS_LEN: usize = 10192;
use logging::LoggingErrors;
use crate::logging::LoggingErrors;
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};
@ -26,11 +29,13 @@ mod natives {
use winapi::um::psapi::{
EnumProcessModulesEx, GetModuleFileNameExW, K32EnumProcesses, LIST_MODULES_ALL,
};
use winapi::um::shellapi::ShellExecuteW;
use winapi::um::winnt::{
HANDLE, PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE, PROCESS_VM_READ,
};
use winapi::um::winuser::SW_SHOWDEFAULT;
use widestring::{U16CString};
use widestring::U16CString;
extern "C" {
pub fn saveShortcut(
@ -50,28 +55,6 @@ mod natives {
) -> ::std::os::raw::c_int;
pub fn getSystemFolder(out_path: *mut ::std::os::raw::c_ushort) -> HRESULT;
pub fn getDesktopFolder(out_path: *mut ::std::os::raw::c_ushort) -> HRESULT;
}
// 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
@ -139,15 +122,23 @@ mod natives {
}
}
fn count_u16(u16str: &[u16]) -> usize {
let mut pos = 0;
for x in u16str.iter() {
if *x == 0 {
break;
// Needed to call unsafe function `ShellExecuteW` from `winapi` crate
#[allow(unsafe_code)]
pub fn open_in_shell(path: &Path) {
let native_verb = U16CString::from_str("open").unwrap();
// https://doc.rust-lang.org/std/os/windows/ffi/trait.OsStrExt.html#tymethod.encode_wide
let mut native_path: Vec<u16> = path.as_os_str().encode_wide().collect();
native_path.push(0); // NULL terminator
unsafe {
ShellExecuteW(
std::ptr::null_mut(),
native_verb.as_ptr(),
native_path.as_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
SW_SHOWDEFAULT,
);
}
pos += 1;
}
pos
}
/// Cleans up the installer
@ -179,13 +170,20 @@ mod natives {
let spawn_result: i32 = unsafe {
let mut cmd_path = [0u16; MAX_PATH + 1];
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 {
return;
}
spawnDetached(
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")
.as_ptr(),
@ -297,7 +295,7 @@ mod natives {
use std::env;
use logging::LoggingErrors;
use crate::logging::LoggingErrors;
use sysinfo::{ProcessExt, SystemExt};
@ -306,6 +304,8 @@ mod natives {
use slug::slugify;
use std::fs::{create_dir_all, File};
use std::io::Write;
use std::path::Path;
use std::process::Command;
#[cfg(target_os = "linux")]
pub fn create_shortcut(
@ -314,6 +314,7 @@ mod natives {
target: &str,
args: &str,
working_dir: &str,
_exe_path: &str,
) -> Result<String, String> {
// FIXME: no icon will be shown since no icon is provided
let data_local_dir = dirs::data_local_dir();
@ -322,7 +323,7 @@ mod natives {
let mut path = x;
path.push("applications");
match create_dir_all(path.to_path_buf()) {
Ok(_) => (()),
Ok(_) => (),
Err(e) => {
return Err(format!(
"Local data directory does not exist and cannot be created: {}",
@ -340,7 +341,7 @@ mod natives {
Ok(file) => file,
Err(e) => return Err(format!("Unable to create desktop file: {}", e)),
};
let mut desktop_f = desktop_f.write_all(desktop_file.as_bytes());
let desktop_f = desktop_f.write_all(desktop_file.as_bytes());
match desktop_f {
Ok(_) => Ok("".to_string()),
Err(e) => Err(format!("Unable to write desktop file: {}", e)),
@ -358,11 +359,25 @@ mod natives {
target: &str,
args: &str,
working_dir: &str,
_exe_path: &str,
) -> Result<String, String> {
warn!("STUB! Creating shortcut is not implemented on macOS");
Ok("".to_string())
}
pub fn open_in_shell(path: &Path) {
let shell: &str;
if cfg!(target_os = "linux") {
shell = "xdg-open";
} else if cfg!(target_os = "macos") {
shell = "open";
} else {
warn!("Unsupported platform");
return;
}
Command::new(shell).arg(path).spawn().ok();
}
/// Cleans up the installer
pub fn burn_on_exit(app_name: &str) {
let current_exe = env::current_exe().log_expect("Current executable could not be found");
@ -387,7 +402,7 @@ mod natives {
let mut processes: Vec<super::Process> = Vec::new();
let mut system = sysinfo::System::new();
system.refresh_all();
for (pid, procs) in system.get_process_list() {
for (pid, procs) in system.get_processes() {
processes.push(super::Process {
pid: *pid as usize,
name: procs.name().to_string(),

View file

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

View file

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

View file

@ -24,7 +24,7 @@ impl Version {
match *self {
Version::Semver(ref version) => version.to_owned(),
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.
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use tasks::check_authorization::CheckAuthorizationTask;
use tasks::{Task, TaskDependency, TaskMessage, TaskOrdering, TaskParamType};
use crate::tasks::check_authorization::CheckAuthorizationTask;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskOrdering;
use crate::tasks::TaskParamType;
use 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 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.
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use tasks::Task;
use tasks::TaskDependency;
use tasks::TaskMessage;
use tasks::TaskParamType;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskParamType;
use native::get_process_names;
use native::Process;
use crate::native::get_process_names;
use crate::native::Process;
use std::process;

View file

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

View file

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

View file

@ -1,17 +1,17 @@
//! Generates the global shortcut for this application.
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use tasks::Task;
use tasks::TaskDependency;
use tasks::TaskMessage;
use tasks::TaskParamType;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskParamType;
use logging::LoggingErrors;
use crate::logging::LoggingErrors;
use native::create_shortcut;
use tasks::save_database::SaveDatabaseTask;
use tasks::TaskOrdering;
use crate::native::create_shortcut;
use crate::tasks::save_database::SaveDatabaseTask;
use crate::tasks::TaskOrdering;
pub struct InstallGlobalShortcutsTask {}

View file

@ -1,26 +1,26 @@
//! Installs a specific package.
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use tasks::download_pkg::DownloadPackageTask;
use tasks::install_shortcuts::InstallShortcutsTask;
use tasks::save_database::SaveDatabaseTask;
use tasks::uninstall_pkg::UninstallPackageTask;
use tasks::Task;
use tasks::TaskDependency;
use tasks::TaskMessage;
use tasks::TaskOrdering;
use tasks::TaskParamType;
use crate::tasks::download_pkg::DownloadPackageTask;
use crate::tasks::install_shortcuts::InstallShortcutsTask;
use crate::tasks::save_database::SaveDatabaseTask;
use crate::tasks::uninstall_pkg::UninstallPackageTask;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskOrdering;
use crate::tasks::TaskParamType;
use config::PackageDescription;
use installer::LocalInstallation;
use crate::config::PackageDescription;
use crate::installer::LocalInstallation;
use std::fs::create_dir_all;
use std::io::copy;
use logging::LoggingErrors;
use crate::logging::LoggingErrors;
use archives;
use crate::archives;
use std::fs::OpenOptions;
use std::path::Path;
@ -136,7 +136,7 @@ impl Task for InstallPackageTask {
info!("Creating file: {:?}", string_name);
if !installed_files.contains(&string_name) {
installed_files.push(string_name.to_string());
installed_files.push(string_name);
}
let mut file_metadata = OpenOptions::new();
@ -165,7 +165,7 @@ impl Task for InstallPackageTask {
// Save metadata about this package
context.database.packages.push(LocalInstallation {
name: package.name.to_owned(),
name: package.name,
version,
shortcuts: Vec::new(),
files: installed_files,

View file

@ -1,17 +1,17 @@
//! Generates shortcuts for a specified file.
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use tasks::Task;
use tasks::TaskDependency;
use tasks::TaskMessage;
use tasks::TaskParamType;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskParamType;
use config::PackageDescription;
use crate::config::PackageDescription;
use logging::LoggingErrors;
use crate::logging::LoggingErrors;
use native::create_shortcut;
use crate::native::create_shortcut;
pub struct InstallShortcutsTask {
pub name: String,

View file

@ -4,10 +4,10 @@
use std::fmt;
use std::fmt::Display;
use installer::InstallerFramework;
use crate::installer::InstallerFramework;
use sources::types::File;
use sources::types::Version;
use crate::sources::types::File;
use crate::sources::types::Version;
pub mod check_authorization;
pub mod download_pkg;
@ -26,6 +26,7 @@ pub mod uninstall;
pub mod uninstall_global_shortcut;
pub mod uninstall_pkg;
pub mod uninstall_shortcuts;
pub mod remove_target_dir;
/// An abstraction over the various parameters that can be passed around.
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 installer::InstallerFramework;
use crate::installer::InstallerFramework;
use tasks::Task;
use tasks::TaskDependency;
use tasks::TaskMessage;
use tasks::TaskParamType;
use crate::tasks::Task;
use crate::tasks::TaskDependency;
use crate::tasks::TaskMessage;
use crate::tasks::TaskParamType;
use config::PackageDescription;
use crate::config::PackageDescription;
use regex::Regex;
use logging::LoggingErrors;
use crate::logging::LoggingErrors;
pub struct ResolvePackageTask {
pub name: String,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,13 +3,15 @@ module.exports = {
env: {
node: true
},
'extends': [
extends: [
'plugin:vue/essential',
'@vue/standard'
],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
'no-console': 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-redeclare': 'off',
camelcase: 'off'
},
parserOptions: {
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 port = 3000
let showError = false
let showConfigError = false
let maintenance = false
let launcher = false
let fileExists = false
let darkMode = false
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 timer = setInterval(() => {
var resp = JSON.stringify({ Status: ['Processing...', progress] }) + '\n'
@ -14,10 +27,14 @@ function progressSimulation (res) {
res.status(200).end()
clearInterval(timer)
}
}, 1500)
}, 500)
}
function returnConfig (res) {
if (showConfigError) {
res.status(500).json({})
return
}
res.json({
installing_message:
'Test Banner <strong>Bold</strong>&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) => {
console.log('-- Get attrs')
res.send(
`var base_attributes = {"name":"yuzu","target_url":"https://raw.githubusercontent.com/j-selby/test-installer/master/config.linux.v2.toml"};`
{ name: 'yuzu', target_url: 'https://raw.githubusercontent.com/j-selby/test-installer/master/config.linux.v2.toml' }
)
})
app.get('/api/dark-mode', (req, res) => {
res.json(false)
res.json(darkMode)
})
app.get('/api/installation-status', (req, res) => {
res.json({
database: { packages: [], shortcuts: [] },
install_path: null,
preexisting_install: false,
is_launcher: false,
preexisting_install: maintenance,
is_launcher: launcher,
launcher_path: null
})
})
@ -82,13 +100,54 @@ app.get('/api/config', (req, res) => {
})
app.post('/api/start-install', (req, res) => {
console.log(`-- Install: ${req.body}`)
console.log(`-- Install: ${req}`)
progressSimulation(res)
})
app.get('/api/exit', (req, res) => {
console.log('-- Exit')
res.status(204)
if (showError) {
res.status(500).send('Simulated error: Nothing to see here.')
return
}
res.status(204).send()
})
app.post('/api/verify-path', (req, res) => {
console.log('-- Verify Path')
res.send({
exists: fileExists
})
})
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}...`)

View file

@ -5,23 +5,32 @@
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
"lint": "vue-cli-service lint",
"postinstall": "node merge-strings.js"
},
"dependencies": {
"buefy": "^0.7.7",
"vue": "^2.6.6",
"vue-router": "^3.0.1"
"@mdi/font": "^5.9.55",
"axios": "^0.21.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": {
"@vue/cli-plugin-babel": "^3.5.0",
"@vue/cli-plugin-eslint": "^3.5.0",
"@vue/cli-service": "^3.5.0",
"@vue/eslint-config-standard": "^4.0.0",
"babel-eslint": "^10.0.1",
"eslint": "^5.8.0",
"eslint-plugin-vue": "^5.0.0",
"@vue/cli-plugin-babel": "^4.5.13",
"@vue/cli-plugin-eslint": "^4.5.13",
"@vue/cli-service": "^4.5.13",
"@vue/eslint-config-standard": "^6.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^7.28.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",
"http-proxy-middleware": "^0.19.1",
"vue-template-compiler": "^2.5.21"
"http-proxy-middleware": "^2.0.0",
"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 http-equiv="X-UA-Compatible" content="IE=11">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<script src="/api/attrs" type="text/javascript"></script>
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title id="window-title">... Installer</title>
</head>

View file

@ -9,15 +9,23 @@
<br />
<h2 class="subtitle" v-if="!$root.$data.metadata.preexisting_install">
Welcome to the {{ $root.$data.attrs.name }} installer!
{{ $t('app.installer_title', {'name': $root.$data.attrs.name}) }}
</h2>
<h2 class="subtitle" v-if="!$root.$data.metadata.preexisting_install">
We will have you up and running in just a few moments.
{{ $t('app.installer_subtitle') }}
</h2>
<h2 class="subtitle" v-if="$root.$data.metadata.preexisting_install">
Welcome to the {{ $root.$data.attrs.name }} Maintenance Tool.
{{ $t('app.maintenance_title', {'name': $root.$data.attrs.name}) }}
</h2>
<b-dropdown hoverable @change="selectLocale" aria-role="list">
<button class="button" slot="trigger">
<span>{{ $t('locale') }}</span>
<b-icon icon="menu-down"></b-icon>
</button>
<b-dropdown-item v-for="(locale, index) in this.$i18n.messages" v-bind:key="index" :value="index" aria-role="listitem">{{locale.locale}}</b-dropdown-item>
</b-dropdown>
</div>
<router-view />
@ -27,6 +35,33 @@
</div>
</template>
<script>
export default {
mounted: function () {
// detect languages
const languages = window.navigator.languages
if (languages) {
// standard-compliant browsers
for (let index = 0; index < languages.length; index++) {
const lang = languages[index]
// Find the most preferred language that we support
if (Object.prototype.hasOwnProperty.call(this.$i18n.messages, lang)) {
this.$i18n.locale = lang
return
}
}
}
// IE9+ support
this.$i18n.locale = window.navigator.browserLanguage
},
methods: {
selectLocale: function (locale) {
this.$i18n.locale = locale
}
}
}
</script>
<style>
/* roboto-regular - latin */
@font-face {
@ -148,7 +183,7 @@ pre {
}
/* 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%);
}

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.
*/
var request_id = 0
/**
* Makes a AJAX request.
*
* @param path The path to connect to.
* @param successCallback A callback with a JSON payload.
* @param failCallback A fail callback. Optional.
* @param data POST data. Optional.
*/
export function ajax (path, successCallback, failCallback, data) {
if (failCallback === undefined) {
failCallback = defaultFailHandler
}
console.log('Making HTTP request to ' + path)
var req = new XMLHttpRequest()
req.addEventListener('load', function () {
// The server can sometimes return a string error. Make sure we handle this.
if (this.status === 200 && this.getResponseHeader('Content-Type').indexOf('application/json') !== -1) {
successCallback(JSON.parse(this.responseText))
} else {
failCallback(this.responseText)
}
})
req.addEventListener('error', failCallback)
req.open(data == null ? 'GET' : 'POST', path + '?nocache=' + request_id++, true)
// Rocket only currently supports URL encoded forms.
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
if (data != null) {
var form = ''
for (var key in data) {
if (!data.hasOwnProperty(key)) {
continue
}
if (form !== '') {
form += '&'
}
form += encodeURIComponent(key) + '=' + encodeURIComponent(data[key])
}
req.send(form)
} else {
req.send()
}
}
/**
* Makes a AJAX request, streaming each line as it arrives. Type should be text/plain,
* each line will be interpreted as JSON separately.
@ -108,7 +54,7 @@ export function stream_ajax (path, callback, successCallback, failCallback, data
req.addEventListener('error', failCallback)
req.open(data == null ? 'GET' : 'POST', path + '?nocache=' + request_id++, true)
req.open(data == null ? 'GET' : 'POST', path + '?nocache=' + Date.now(), true)
// Rocket only currently supports URL encoded forms.
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
@ -116,7 +62,7 @@ export function stream_ajax (path, callback, successCallback, failCallback, data
var form = ''
for (var key in data) {
if (!data.hasOwnProperty(key)) {
if (!data[key]) {
continue
}
@ -132,13 +78,3 @@ export function stream_ajax (path, callback, successCallback, failCallback, data
req.send()
}
}
/**
* The default handler if a AJAX request fails. Not to be used directly.
*
* @param e The XMLHttpRequest that failed.
*/
function defaultFailHandler (e) {
console.error('A AJAX request failed, and was not caught:')
console.error(e)
}

1
ui/src/locales/.gitignore vendored Normal file
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 App from './App.vue'
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 messages from './locales/messages.json'
import 'buefy/dist/buefy.css'
import '@mdi/font/css/materialdesignicons.min.css'
Vue.config.productionTip = false
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/
function intercept (method) {
console[method] = function () {
var message = Array.prototype.slice.apply(arguments).join(' ')
const message = Array.prototype.slice.apply(arguments).join(' ')
window.external.invoke(
JSON.stringify({
Log: {
@ -24,18 +37,18 @@ function intercept (method) {
}
// See if we have access to the JSON interface
var has_external_interface = false;
let hasExternalInterface = false
try {
window.external.invoke(JSON.stringify({
Test: {}
}))
has_external_interface = true;
hasExternalInterface = true
} 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
if (has_external_interface) {
if (hasExternalInterface) {
window.onerror = function (msg, url, line) {
window.external.invoke(
JSON.stringify({
@ -47,14 +60,14 @@ if (has_external_interface) {
)
}
var methods = ['log', 'warn', 'error']
for (var i = 0; i < methods.length; i++) {
const methods = ['log', 'warn', 'error']
for (let i = 0; i < methods.length; i++) {
intercept(methods[i])
}
}
// Disable F5
function disable_shortcuts (e) {
function disableShortcuts (e) {
switch (e.keyCode) {
case 116: // F5
e.preventDefault()
@ -63,25 +76,32 @@ function disable_shortcuts (e) {
}
// Check to see if we need to enable dark mode
ajax('/api/dark-mode', function (enable) {
if (enable) {
axios.get('/api/dark-mode').then(function (resp) {
if (resp.data === true) {
document.body.classList.add('has-background-black-ter')
}
})
window.addEventListener('keydown', disable_shortcuts)
window.addEventListener('keydown', disableShortcuts)
axios.get('/api/attrs').then(function (resp) {
document.getElementById('window-title').innerText =
base_attributes.name + ' Installer'
i18n.t('app.window_title', { name: resp.data.name })
}).catch(function (err) {
console.error(err)
})
function selectFileCallback (name) {
app.install_location = name
}
var app = new Vue({
window.selectFileCallback = selectFileCallback
const app = new Vue({
i18n: i18n,
router: router,
data: {
attrs: base_attributes,
attrs: {},
config: {},
install_location: '',
username: '',
@ -102,54 +122,61 @@ var app = new Vue({
render: function (caller) {
return caller(App)
},
mounted: function () {
axios.get('/api/attrs').then(function (resp) {
app.attrs = resp.data
}).catch(function (err) {
console.error(err)
})
},
methods: {
exit: function () {
ajax(
'/api/exit',
function () {},
function (msg) {
var search_location = app.metadata.install_path.length > 0 ? app.metadata.install_path :
"the location where this installer is";
axios.get('/api/exit').catch(function (msg) {
const searchLocation = (app.metadata.install_path && app.metadata.install_path.length > 0)
? app.metadata.install_path
: i18n.t('error.location_unknown')
app.$router.replace({ name: 'showerr', params: { msg: msg +
'\n\nPlease upload the log file (in ' + search_location + ') to ' +
'the ' + app.attrs.name + ' team'
}});
},
{} // pass in nothing to cause `ajax` to post instead of get
)
app.$router.replace({
name: 'showerr',
params: {
msg: i18n.t('error.exit_error', {
name: app.attrs.name,
path: searchLocation,
msg: msg
})
}
})
})
},
check_authentication: function (success, error) {
var that = this;
var app = this.$root;
const that = this
const app = this.$root
app.ajax('/api/check-auth', function (auth) {
app.$data.username = auth.username;
app.$data.token = auth.token;
that.jwt_token = auth.jwt_token;
that.is_authenticated = Object.keys(that.jwt_token).length !== 0 && that.jwt_token.constructor === Object;
app.$data.username = auth.username
app.$data.token = auth.token
that.jwt_token = auth.jwt_token
that.is_authenticated = Object.keys(that.jwt_token).length !== 0 && that.jwt_token.constructor === Object
if (that.is_authenticated) {
// Give all permissions to vip roles
that.is_linked = that.jwt_token.isPatreonAccountLinked;
that.is_subscribed = that.jwt_token.isPatreonSubscriptionActive;
that.has_reward_tier = that.jwt_token.releaseChannels.indexOf("early-access") > -1;
that.is_linked = that.jwt_token.isPatreonAccountLinked
that.is_subscribed = that.jwt_token.isPatreonSubscriptionActive
that.has_reward_tier = that.jwt_token.releaseChannels.indexOf('early-access') > -1
}
if (success) {
success();
success()
}
}, function (e) {
if (error) {
error();
error()
}
}, {
"username": app.$data.username,
"token": app.$data.token
username: app.$data.username,
token: app.$data.token
})
},
ajax: ajax,
stream_ajax: stream_ajax
stream_ajax: streamAjax
}
}).$mount('#app')
console.log("Vue started")
console.log('Vue started')

View file

@ -9,30 +9,30 @@
</div>
<div v-else-if="was_update">
<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 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 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>
<img src="../assets/how-to-open.png" alt="Where yuzu is installed"/>
</div>
<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 class="field is-grouped is-right-floating is-bottom-floating">
<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>
</div>
</div>
@ -46,12 +46,12 @@ export default {
was_install: !this.$route.params.uninstall,
was_update: this.$route.params.update,
was_migrate: this.$route.params.migrate,
has_installed: this.$route.params.packages_installed > 0,
has_installed: this.$route.params.packages_installed > 0
}
},
methods: {
exit: function () {
this.$root.exit();
this.$root.exit()
}
}
}

View file

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

View file

@ -1,12 +1,12 @@
<template>
<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>
</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">
<a class="button is-primary is-medium" v-if="remaining && !$root.$data.metadata.is_launcher" v-on:click="go_back">Back</a>
<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="remaining && !$root.$data.metadata.is_launcher" v-on:click="go_back">{{ $t('back') }}</b-button>
<b-button class="is-primary is-medium" v-if="$root.$data.metadata.is_launcher" v-on:click="exit">{{ $t('exit') }}</b-button>
</p>
</div>
</div>
@ -38,12 +38,12 @@ export default {
return {
// https://stackoverflow.com/questions/6234773/can-i-escape-html-special-chars-in-javascript
msg: this.$route.params.msg
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;")
.replace(/\n/g, "<br />"),
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
.replace(/\n/g, '<br />'),
remaining: window.history.length > 1
}
},

View file

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

View file

@ -1,35 +1,34 @@
<template>
<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">
Update
</a>
<b-button icon-left="update" type="is-dark-green" size="is-medium" @click="update">
{{ $t('modify.update') }}
</b-button>
<br />
<br />
<a class="button is-dark is-medium" v-on:click="modify_packages">
Modify
</a>
<b-button icon-left="pencil" type="is-info" size="is-medium" @click="modify_packages">
{{ $t('modify.modify') }}
</b-button>
<br />
<br />
<a class="button is-dark is-medium" v-on:click="prepare_uninstall">
Uninstall
</a>
<b-button icon-left="wrench" type="is-info" size="is-medium" @click="repair_packages">
{{ $t('modify.repair') }}
</b-button>
<br />
<br />
<div class="modal is-active" v-if="show_uninstall">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Are you sure you want to uninstall {{ $root.$data.attrs.name }}?</p>
</header>
<footer class="modal-card-foot">
<button class="button is-danger" v-on:click="uninstall">Yes</button>
<button class="button" v-on:click="cancel_uninstall">No</button>
</footer>
</div>
</div>
<b-button icon-left="delete" type="is-danger" size="is-medium" @click="prepare_uninstall">
{{ $t('modify.uninstall') }}
</b-button>
<br />
<br />
<b-button icon-left="file-find" type="is-link" size="is-medium" @click="view_files">
{{ $t('modify.view_local_files') }}
</b-button>
</div>
</template>
@ -37,9 +36,7 @@
export default {
name: 'ModifyView',
data: function () {
return {
show_uninstall: false
}
return {}
},
methods: {
update: function () {
@ -48,15 +45,42 @@ export default {
modify_packages: function () {
this.$router.push('/packages')
},
prepare_uninstall: function () {
this.show_uninstall = true
repair_packages: function () {
this.$router.push({ name: 'packages', params: { repair: true } })
},
cancel_uninstall: function () {
this.show_uninstall = false
prepare_uninstall: function () {
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 () {
this.$router.push('/install/uninstall/false')
},
view_files: function () {
this.$http.get('/api/view-local-folder')
}
}
}
</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>
<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 -->
<div class="tile is-ancestor">
<div class="tile is-parent is-vertical">
<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-parent" v-for="Lpackage in $root.$data.config.packages" :key="Lpackage.name" :index="Lpackage.name">
<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>
<label class="checkbox">
<b-checkbox v-model="Lpackage.default">
<span v-if="!Lpackage.installed">Install</span> {{ Lpackage.name }}
{{ Lpackage.name }}
</b-checkbox>
<span v-if="Lpackage.installed"><i>(installed)</i></span>
<span v-if="Lpackage.installed"><i>{{ $t('select_packages.installed') }}</i></span>
</label>
<div>
<img class="package-icon" :src="`${publicPath + Lpackage.icon}`"/>
@ -21,6 +25,7 @@
</p>
</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">
<h4>Install Options</h4>
<b-checkbox v-model="installDesktopShortcut">
@ -30,41 +35,40 @@
</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="control is-expanded">
<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 class="control">
<a class="button is-dark" v-on:click="select_file">
Select
</a>
<b-button class="is-dark" v-on:click="select_file">
{{ $t('select_packages.select') }}
</b-button>
</div>
</div>
<div class="is-right-floating is-bottom-floating">
<div class="field is-grouped">
<p class="control">
<a class="button is-medium" v-if="!$root.$data.config.hide_advanced && !$root.$data.metadata.preexisting_install && !advanced"
v-on:click="advanced = true">Advanced...</a>
<b-button class="is-medium" v-if="!$root.$data.config.hide_advanced && !$root.$data.metadata.preexisting_install && !advanced"
v-on:click="advanced = true">{{ $t('select_packages.advanced') }}</b-button>
</p>
<p class="control">
<!-- Disable the Install button on a fresh install with no packages selected -->
<button v-if="$root.$data.metadata.preexisting_install" class="button is-medium is-dark" v-on:click="install">
Modify
</button>
<button v-else class="button is-medium is-dark" v-on:click="install" :disabled="!this.has_package_selected">
Install
</button>
<b-button class="is-dark is-medium" v-if="!$root.$data.metadata.preexisting_install"
v-on:click="install">{{ $t('select_packages.install') }}</b-button>
</p>
<p class="control">
<a class="button is-dark is-medium" v-if="$root.$data.metadata.preexisting_install"
v-on:click="install">{{ repair ? $t('select_packages.repair') : $t('select_packages.modify') }}</a>
</p>
</div>
</div>
<div class="field is-grouped is-left-floating is-bottom-floating">
<p class="control">
<a class="button is-medium" v-if="$root.$data.metadata.preexisting_install"
v-on:click="go_back">Back</a>
<b-button class="is-medium" v-if="$root.$data.metadata.preexisting_install"
v-on:click="go_back">{{ $t('back') }}</b-button>
</p>
</div>
</div>
@ -73,34 +77,25 @@
<script>
export default {
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
// and also deselect any packages that don't use authorization
if (this.$root.$data.has_reward_tier) {
for (let package_index = 0; package_index < this.$root.config.packages.length; package_index++) {
let current_package = this.$root.config.packages[package_index];
current_package.default = current_package.requires_authorization;
const current_package = this.$root.config.packages[package_index]
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: {
select_file: function () {
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 () {
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 () {
this.$router.go(-1)
@ -122,11 +161,11 @@
this.$router.push('/authentication')
},
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) {
if (this.installable(pkg)) {
pkg.default = !pkg.default;
pkg.default = !pkg.default
} else if (pkg.requires_authorization && !this.$root.$data.is_authenticated) {
this.show_authentication()
} else if (pkg.requires_authorization && !this.$root.$data.is_linked) {
@ -139,18 +178,18 @@
},
get_extended_description: function (pkg) {
if (!pkg.extended_description) {
return "";
return ''
}
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) {
return pkg.extended_description.need_authentication_description;
return pkg.extended_description.need_authentication_description
} 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) {
return pkg.extended_description.need_subscription_description;
return pkg.extended_description.need_subscription_description
} else { // need_reward_tier_description
return pkg.extended_description.need_reward_tier_description;
return pkg.extended_description.need_reward_tier_description
}
}
}

File diff suppressed because it is too large Load diff