Merge pull request #23 from jroweboy/merged

Installer v1.8
This commit is contained in:
bunnei 2019-12-08 21:52:21 -05:00 committed by GitHub
commit f848e8fb53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 644 additions and 199 deletions

View File

@ -1,2 +1,2 @@
name = "yuzu" name = "yuzu"
target_url = "https://raw.githubusercontent.com/yuzu-emu/liftinstall/master/config.windows.v9.toml" target_url = "https://raw.githubusercontent.com/yuzu-emu/liftinstall/master/config.windows.v10.toml"

58
config.windows.v10.toml Normal file
View File

@ -0,0 +1,58 @@
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
hide_advanced = true
[authentication]
# Base64 encoded version of the public key for validating the JWT token. Must be in DER format
pub_key_base64 = "MIIBCgKCAQEAs5K6s49JVV9LBMzDrkORsoPSYsv1sCXDtxjp4pn8p0uPSvJAsbNNmdIgCjfSULzbHLM28MblnI4zYP8ZgKtkjdg+Ic5WQbS5iBAkf18zMafpOrotTArLsgZSmUfNYt0SOiN17D+sq/Ov/CKXRM9CttKkEbanBTVqkx7sxsHVbkI6tDvkboSaNeVPHzHlfAbvGrUo5cbAFCB/KnRsoxr+g7jLKTxU1w4xb/pIs91h80AXV/yZPXL6ItPM3/0noIRXjmoeYWf2sFQaFALNB2Kef0p6/hoHYUQP04ZSIL3Q+v13z5X2YJIlI4eLg+iD25QYm9V8oP3+Xro4vd47a0/maQIDAQAB"
# URL to authenticate against. This must return a JWT token with their permissions and a custom claim patreonInfo with the following structure
# "patreonInfo": { "linked": false, "activeSubscription": false }
# If successful, the frontend will use this JWT token as a Bearer Authentication when requesting the binaries to download
auth_url = "https://api.yuzu-emu.org/jwt/installer/"
[authentication.validation]
iss = "citra-core"
aud = "installer"
[[packages]]
name = "yuzu Early Access"
description = "Preview release with the newest features for the supporters."
icon = "thicc_logo_installer__ea_shadow.png"
requires_authorization = true
# puts a "new" ribbon the package select
is_new = true
[packages.extended_description]
no_action_description = "Thank you for your support!"
# Displayed when the package has no authentication for the user
need_authentication_description = "Click here to sign in with your yuzu account for Early Access"
# Displayed when the package has an authentication, but the user has not linked their account
need_link_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
# Displayed when the package has an authentication, but the user has not linked their account
need_subscription_description = "You are signed in, but you need to link your Patreon account! Click here for more details"
# Displayed when the package has an authentication, but the user has not linked their account
need_reward_tier_description = "You are signed in, but are not backing an eligible reward tier! Click here for more details"
[packages.source]
name = "patreon"
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
[packages.source.config]
repo = "earlyaccess"
[[packages.shortcuts]]
name = "yuzu Early Access"
relative_path = "yuzu-windows-msvc-early-access/yuzu.exe"
description = "Launch yuzu Early Access"
[[packages]]
name = "yuzu"
description = "Includes frequent updates to yuzu with all the latest reviewed and tested features."
icon = "thicc_logo_installer_shadow.png"
default = true
[packages.source]
name = "github"
match = "^yuzu-windows-msvc-[0-9]*-[0-9a-f]*.tar.xz$"
[packages.source.config]
repo = "yuzu-emu/yuzu-mainline"
[[packages.shortcuts]]
name = "yuzu"
relative_path = "yuzu-windows-msvc/yuzu.exe"
description = "Launch yuzu"

View File

@ -1,5 +1,6 @@
installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!" installing_message = "Reminder: yuzu is an <b>experimental</b> emulator. Stuff will break!"
hide_advanced = true hide_advanced = true
new_tool = "https://github.com/yuzu-emu/liftinstall/releases/download/1.8/yuzu_install.exe"
[authentication] [authentication]
# Base64 encoded version of the public key for validating the JWT token. Must be in DER format # Base64 encoded version of the public key for validating the JWT token. Must be in DER format

View File

@ -25,6 +25,23 @@ pub struct PackageShortcut {
pub name: String, pub name: String,
pub relative_path: String, pub relative_path: String,
pub description: String, pub description: String,
#[serde(default)]
pub has_desktop_shortcut: bool,
}
/// Extra description for authentication and authorization state for a package
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct PackageExtendedDescription {
#[serde(default)]
pub no_action_description: Option<String>,
#[serde(default)]
pub need_authentication_description: Option<String>,
#[serde(default)]
pub need_link_description: Option<String>,
#[serde(default)]
pub need_subscription_description: Option<String>,
#[serde(default)]
pub need_reward_tier_description: Option<String>,
} }
/// Describes a overview of a individual package. /// Describes a overview of a individual package.
@ -32,6 +49,8 @@ pub struct PackageShortcut {
pub struct PackageDescription { pub struct PackageDescription {
pub name: String, pub name: String,
pub description: String, pub description: String,
#[serde(default)]
pub icon: Option<String>,
pub default: Option<bool>, pub default: Option<bool>,
pub source: PackageSource, pub source: PackageSource,
#[serde(default)] #[serde(default)]
@ -41,13 +60,7 @@ pub struct PackageDescription {
#[serde(default)] #[serde(default)]
pub is_new: Option<bool>, pub is_new: Option<bool>,
#[serde(default)] #[serde(default)]
pub need_authentication_description: Option<String>, pub extended_description: Option<PackageExtendedDescription>,
#[serde(default)]
pub need_link_description: Option<String>,
#[serde(default)]
pub need_subscription_description: Option<String>,
#[serde(default)]
pub need_reward_tier_description: Option<String>,
} }
/// Configuration for validating the JWT token /// Configuration for validating the JWT token

View File

@ -34,7 +34,10 @@ pub fn file_from_string(file_path: &str) -> Option<(String, &'static [u8])> {
file_path, file_path,
"/index.html", "/index.html",
"/favicon.ico", "/favicon.ico",
"/img/logo.png", "/img/light_mode_installer_logo.png",
"/img/dark_mode_installer_logo.png",
"/thicc_logo_installer__ea_shadow.png",
"/thicc_logo_installer_shadow.png",
"/img/how-to-open.png", "/img/how-to-open.png",
"/css/app.css", "/css/app.css",
"/css/chunk-vendors.css", "/css/chunk-vendors.css",

View File

@ -138,7 +138,7 @@ pub fn validate_token(
}; };
// Configure validation for audience and issuer if the configuration provides it // Configure validation for audience and issuer if the configuration provides it
let validation = match validation { let mut validation = match validation {
Some(v) => { Some(v) => {
let mut valid = Validation::new(Algorithm::RS256); let mut valid = Validation::new(Algorithm::RS256);
valid.iss = v.iss; valid.iss = v.iss;
@ -149,7 +149,8 @@ pub fn validate_token(
} }
None => Validation::default(), None => Validation::default(),
}; };
validation.validate_exp = false;
validation.validate_nbf = false;
// Verify the JWT token // Verify the JWT token
decode::<JWTClaims>(&body, pub_key.as_slice(), &validation) decode::<JWTClaims>(&body, pub_key.as_slice(), &validation)
.map(|tok| tok.claims) .map(|tok| tok.claims)

View File

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

View File

@ -28,12 +28,17 @@ 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 install_desktop_shortcut= false;
// Transform results into just an array of stuff to install // Transform results into just an array of stuff to install
for (key, value) in &results { for (key, value) in &results {
if key == "path" { if key == "path" {
path = Some(value.to_owned()); path = Some(value.to_owned());
continue; continue;
} else if key == "installDesktopShortcut" {
info!("Found installDesktopShortcut {:?}", value);
install_desktop_shortcut = value == "true";
continue;
} }
if value == "true" { if value == "true" {
@ -55,7 +60,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) { if let Err(v) = framework.install(to_install, &sender, new_install, install_desktop_shortcut) {
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

@ -136,7 +136,7 @@ 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::Get, "/api/exit") => exit::handle(self, req), (Method::Post, "/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::Post, "/api/check-auth") => authentication::handle(self, req), (Method::Post, "/api/check-auth") => authentication::handle(self, req),

View File

@ -179,6 +179,7 @@ impl InstallerFramework {
items: Vec<String>, items: Vec<String>,
messages: &Sender<InstallMessage>, messages: &Sender<InstallMessage>,
fresh_install: bool, fresh_install: bool,
create_desktop_shortcuts: bool,
) -> Result<(), String> { ) -> Result<(), String> {
info!( info!(
"Framework: Installing {:?} to {:?}", "Framework: Installing {:?} to {:?}",
@ -207,6 +208,7 @@ impl InstallerFramework {
items, items,
uninstall_items, uninstall_items,
fresh_install, fresh_install,
create_desktop_shortcuts,
}); });
let mut tree = DependencyTree::build(task); let mut tree = DependencyTree::build(task);

View File

@ -65,11 +65,13 @@ mod tasks;
use installer::InstallerFramework; use installer::InstallerFramework;
use logging::LoggingErrors; use logging::LoggingErrors;
use std::path::PathBuf;
use clap::App; use clap::App;
use clap::Arg; use clap::Arg;
use config::BaseAttributes; use config::BaseAttributes;
use std::process::{Command, Stdio, exit};
static RAW_CONFIG: &'static str = include_str!(concat!(env!("OUT_DIR"), "/bootstrap.toml")); static RAW_CONFIG: &'static str = include_str!(concat!(env!("OUT_DIR"), "/bootstrap.toml"));
@ -106,12 +108,12 @@ 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()
.log_expect("Parent directory of executable could not be found"); .log_expect("Parent directory of executable could not be found");
// Handle self-updating if needed
self_update::perform_swap(&current_exe, matches.value_of("swap")); self_update::perform_swap(&current_exe, matches.value_of("swap"));
if let Some(new_matches) = self_update::check_args(reinterpret_app, current_path) { if let Some(new_matches) = self_update::check_args(reinterpret_app, current_path) {
matches = new_matches; matches = new_matches;
@ -119,15 +121,42 @@ fn main() {
self_update::cleanup(current_path); self_update::cleanup(current_path);
// Load in metadata + setup the installer framework // Load in metadata + setup the installer framework
let mut fresh_install = false;
let metadata_file = current_path.join("metadata.json"); let metadata_file = current_path.join("metadata.json");
let mut framework = if metadata_file.exists() { let mut framework = if metadata_file.exists() {
info!("Using pre-existing metadata file: {:?}", metadata_file); info!("Using pre-existing metadata file: {:?}", metadata_file);
InstallerFramework::new_with_db(config, current_path).log_expect("Unable to parse metadata") InstallerFramework::new_with_db(config, current_path).log_expect("Unable to parse metadata")
} else { } else {
info!("Starting fresh install"); info!("Starting fresh install");
fresh_install = true;
InstallerFramework::new(config) InstallerFramework::new(config)
}; };
// check for existing installs if we are running as a fresh install
let installed_path = PathBuf::from(framework.get_default_path().unwrap());
if fresh_install && installed_path.join("metadata.json").exists() {
info!("Existing install detected! Trying to launch this install instead");
// Generate installer path
let platform_extension = if cfg!(windows) {
"maintenancetool.exe"
} else {
"maintenancetool"
};
let existing = installed_path.join(platform_extension).into_os_string().into_string();
if existing.is_ok() {
info!("Launching {:?}", existing);
let success = Command::new(existing.unwrap())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
if success.is_ok() {
exit(0);
} else {
error!("Unable to start existing yuzu maintenance tool. Launching old one instead");
}
}
}
let is_launcher = if let Some(string) = matches.value_of("launcher") { let is_launcher = if let Some(string) = matches.value_of("launcher") {
framework.is_launcher = true; framework.is_launcher = true;
framework.launcher_path = Some(string.to_string()); framework.launcher_path = Some(string.to_string());

View File

@ -43,7 +43,8 @@ extern "C" int saveShortcut(
const wchar_t *description, const wchar_t *description,
const wchar_t *path, const wchar_t *path,
const wchar_t *args, const wchar_t *args,
const wchar_t *workingDir) const wchar_t *workingDir,
const wchar_t *exePath)
{ {
const char *errStr = NULL; const char *errStr = NULL;
HRESULT h; HRESULT h;
@ -82,6 +83,9 @@ extern "C" int saveShortcut(
shellLink->SetDescription(description); shellLink->SetDescription(description);
if (path != NULL) if (path != NULL)
shellLink->SetPath(path); shellLink->SetPath(path);
// default to using the first icon in the exe (usually correct)
if (exePath != NULL)
shellLink->SetIconLocation(exePath, 0);
if (args != NULL) if (args != NULL)
shellLink->SetArguments(args); shellLink->SetArguments(args);
if (workingDir != NULL) if (workingDir != NULL)
@ -95,6 +99,10 @@ extern "C" int saveShortcut(
goto err; goto err;
} }
// Notify that a new shortcut was created using the shell api
SHChangeNotify(SHCNE_CREATE, SHCNF_PATH, shortcutPath, NULL);
SHChangeNotify(SHCNE_UPDATEITEM, SHCNF_PATH, shortcutPath, NULL);
persistFile->Release(); persistFile->Release();
shellLink->Release(); shellLink->Release();
CoUninitialize(); CoUninitialize();
@ -160,3 +168,15 @@ 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

@ -30,7 +30,7 @@ mod natives {
HANDLE, PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE, PROCESS_VM_READ, HANDLE, PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE, PROCESS_VM_READ,
}; };
use widestring::U16CString; use widestring::{U16CString};
extern "C" { extern "C" {
pub fn saveShortcut( pub fn saveShortcut(
@ -39,6 +39,7 @@ mod natives {
path: *const winapi::ctypes::wchar_t, path: *const winapi::ctypes::wchar_t,
args: *const winapi::ctypes::wchar_t, args: *const winapi::ctypes::wchar_t,
workingDir: *const winapi::ctypes::wchar_t, workingDir: *const winapi::ctypes::wchar_t,
exePath: *const winapi::ctypes::wchar_t,
) -> ::std::os::raw::c_int; ) -> ::std::os::raw::c_int;
pub fn isDarkThemeActive() -> ::std::os::raw::c_uint; pub fn isDarkThemeActive() -> ::std::os::raw::c_uint;
@ -49,6 +50,28 @@ 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
@ -59,12 +82,27 @@ 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> {
let source_file = format!( let source_file = format!(
"{}\\Microsoft\\Windows\\Start Menu\\Programs\\{}.lnk", "{}\\Microsoft\\Windows\\Start Menu\\Programs\\{}.lnk",
env::var("APPDATA").log_expect("APPDATA is bad, apparently"), env::var("APPDATA").log_expect("APPDATA is bad, apparently"),
name name
); );
create_shortcut_inner(source_file, name, description, target, args, working_dir, exe_path)
}
// Needed here for Windows interop
#[allow(unsafe_code)]
fn create_shortcut_inner(
source_file: String,
_name: &str,
description: &str,
target: &str,
args: &str,
working_dir: &str,
exe_path: &str,
) -> Result<String, String> {
info!("Generating shortcut @ {:?}", source_file); info!("Generating shortcut @ {:?}", source_file);
@ -78,6 +116,8 @@ mod natives {
U16CString::from_str(args).log_expect("Error while converting to wchar_t"); U16CString::from_str(args).log_expect("Error while converting to wchar_t");
let native_working_dir = let native_working_dir =
U16CString::from_str(working_dir).log_expect("Error while converting to wchar_t"); U16CString::from_str(working_dir).log_expect("Error while converting to wchar_t");
let native_exe_path =
U16CString::from_str(exe_path).log_expect("Error while converting to wchar_t");
let shortcutResult = unsafe { let shortcutResult = unsafe {
saveShortcut( saveShortcut(
@ -86,6 +126,7 @@ mod natives {
native_target.as_ptr(), native_target.as_ptr(),
native_args.as_ptr(), native_args.as_ptr(),
native_working_dir.as_ptr(), native_working_dir.as_ptr(),
native_exe_path.as_ptr(),
) )
}; };
@ -98,6 +139,17 @@ mod natives {
} }
} }
fn count_u16(u16str: &[u16]) -> usize {
let mut pos = 0;
for x in u16str.iter() {
if *x == 0 {
break;
}
pos += 1;
}
pos
}
/// Cleans up the installer /// 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");
@ -127,20 +179,13 @@ 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[..pos])).as_str(), format!("{}\\cmd.exe", String::from_utf16_lossy(&cmd_path[..count_u16(&cmd_path)])).as_str(),
) )
.log_expect("Unable to convert string to wchar_t") .log_expect("Unable to convert string to wchar_t")
.as_ptr(), .as_ptr(),

View File

@ -22,7 +22,7 @@ impl Task for CheckAuthorizationTask {
) -> Result<TaskParamType, String> { ) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 1); assert_eq!(input.len(), 1);
let params = input.pop().log_expect("Should have input from resolver!"); let params = input.pop().log_expect("Check Authorization Task should have input from resolver!");
let (version, file) = match params { let (version, file) = match params {
TaskParamType::File(v, f) => Ok((v, f)), TaskParamType::File(v, f) => Ok((v, f)),
_ => Err("Unexpected TaskParamType in CheckAuthorization: {:?}"), _ => Err("Unexpected TaskParamType in CheckAuthorization: {:?}"),

View File

@ -24,7 +24,7 @@ impl Task for DownloadPackageTask {
) -> Result<TaskParamType, String> { ) -> Result<TaskParamType, String> {
assert_eq!(input.len(), 1); assert_eq!(input.len(), 1);
let file = input.pop().log_expect("Should have input from resolver!"); let file = input.pop().log_expect("Download Package Task should have input from resolver!");
let (version, file, auth) = match file { let (version, file, auth) = match file {
TaskParamType::Authentication(v, f, auth) => (v, f, auth), TaskParamType::Authentication(v, f, auth) => (v, f, auth),
_ => return Err("Unexpected param type to download package".to_string()), _ => return Err("Unexpected param type to download package".to_string()),

View File

@ -8,6 +8,7 @@ use tasks::install_global_shortcut::InstallGlobalShortcutsTask;
use tasks::install_pkg::InstallPackageTask; use tasks::install_pkg::InstallPackageTask;
use tasks::save_executable::SaveExecutableTask; use tasks::save_executable::SaveExecutableTask;
use tasks::uninstall_pkg::UninstallPackageTask; use tasks::uninstall_pkg::UninstallPackageTask;
use tasks::launch_installed_on_exit::LaunchOnExitTask;
use tasks::Task; use tasks::Task;
use tasks::TaskDependency; use tasks::TaskDependency;
@ -19,6 +20,7 @@ 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,
} }
impl Task for InstallTask { impl Task for InstallTask {
@ -60,7 +62,7 @@ impl Task for InstallTask {
for item in &self.items { for item in &self.items {
elements.push(TaskDependency::build( elements.push(TaskDependency::build(
TaskOrdering::Pre, TaskOrdering::Pre,
Box::new(InstallPackageTask { name: item.clone() }), Box::new(InstallPackageTask { name: item.clone(), create_desktop_shortcuts: self.create_desktop_shortcuts }),
)); ));
} }
@ -74,6 +76,11 @@ impl Task for InstallTask {
TaskOrdering::Pre, TaskOrdering::Pre,
Box::new(InstallGlobalShortcutsTask {}), Box::new(InstallGlobalShortcutsTask {}),
)); ));
elements.push(TaskDependency::build(
TaskOrdering::Post,
Box::new(LaunchOnExitTask {})
))
} }
elements elements

View File

@ -0,0 +1,113 @@
//! Generates shortcuts for a specified file.
use installer::InstallerFramework;
use tasks::Task;
use tasks::TaskDependency;
use tasks::TaskMessage;
use tasks::TaskParamType;
use config::PackageDescription;
use logging::LoggingErrors;
use native::create_desktop_shortcut;
pub struct InstallDesktopShortcutTask {
pub name: String,
pub should_run: bool,
}
impl Task for InstallDesktopShortcutTask {
fn execute(
&mut self,
_: Vec<TaskParamType>,
context: &mut InstallerFramework,
messenger: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
if !self.should_run {
return Ok(TaskParamType::GeneratedShortcuts(Vec::new()));
}
messenger(&TaskMessage::DisplayMessage(
&format!("Generating desktop shortcuts for package {:?}...", self.name),
0.0,
));
let path = context
.install_path
.as_ref()
.log_expect("No install path specified");
let starting_dir = path
.to_str()
.log_expect("Unable to build shortcut metadata (startingdir)");
let mut installed_files = Vec::new();
let mut metadata: Option<PackageDescription> = None;
for description in &context
.config
.as_ref()
.log_expect("Should have packages by now")
.packages
{
if self.name == description.name {
metadata = Some(description.clone());
break;
}
}
let package = match metadata {
Some(v) => v,
None => return Err(format!("Package {:?} could not be found.", self.name)),
};
// Generate installer path
let platform_extension = if cfg!(windows) {
"maintenancetool.exe"
} else {
"maintenancetool"
};
for shortcut in package.shortcuts {
let tool_path = path.join(platform_extension);
let tool_path = tool_path
.to_str()
.log_expect("Unable to build shortcut metadata (tool)");
let exe_path = path.join(shortcut.relative_path);
let exe_path = exe_path
.to_str()
.log_expect("Unable to build shortcut metadata (exe)");
installed_files.push(create_desktop_shortcut(
&shortcut.name,
&shortcut.description,
tool_path,
// TODO: Send by list
&format!("--launcher \"{}\"", exe_path),
&starting_dir,
exe_path,
)?);
}
// Update the installed packages shortcuts information in the database
let packages = &mut context.database.packages;
for pack in packages {
if pack.name == self.name {
pack.shortcuts.append(&mut installed_files);
}
}
Ok(TaskParamType::GeneratedShortcuts(installed_files))
}
fn dependencies(&self) -> Vec<TaskDependency> {
vec![]
}
fn name(&self) -> String {
format!("InstallDesktopShortcutTask (for {:?}, should_run = {:?})", self.name, self.should_run)
}
}

View File

@ -58,6 +58,7 @@ impl Task for InstallGlobalShortcutsTask {
// TODO: Send by list // TODO: Send by list
"", "",
&starting_dir, &starting_dir,
"",
)?; )?;
if !shortcut_file.is_empty() { if !shortcut_file.is_empty() {

View File

@ -24,9 +24,11 @@ use archives;
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::path::Path; use std::path::Path;
use tasks::install_desktop_shortcut::InstallDesktopShortcutTask;
pub struct InstallPackageTask { pub struct InstallPackageTask {
pub name: String, pub name: String,
pub create_desktop_shortcuts: bool,
} }
impl Task for InstallPackageTask { impl Task for InstallPackageTask {
@ -66,20 +68,15 @@ impl Task for InstallPackageTask {
None => return Err(format!("Package {:?} could not be found.", self.name)), None => return Err(format!("Package {:?} could not be found.", self.name)),
}; };
// Grab data from the shortcut generator // Ignore input from the uninstaller - no useful information passed
let shortcuts = input.pop().log_expect("Should have input from resolver!"); // If a previous task Breaks, then just early exit
let shortcuts = match shortcuts { match input.pop().log_expect("Install Package Task should have guaranteed output!") {
TaskParamType::GeneratedShortcuts(files) => files,
// If the resolver returned early, we need to unwind
TaskParamType::Break => return Ok(TaskParamType::None), TaskParamType::Break => return Ok(TaskParamType::None),
_ => return Err("Unexpected shortcuts param type to install package".to_string()), _ => (),
}; };
// Ignore input from the uninstaller - no useful information passed
input.pop();
// Grab data from the resolver // Grab data from the resolver
let data = input.pop().log_expect("Should have input from resolver!"); let data = input.pop().log_expect("Install Package Task should have input from resolver!");
let (version, file, data) = match data { let (version, file, data) = match data {
TaskParamType::FileContents(version, file, data) => (version, file, data), TaskParamType::FileContents(version, file, data) => (version, file, data),
_ => return Err("Unexpected file contents param type to install package".to_string()), _ => return Err("Unexpected file contents param type to install package".to_string()),
@ -170,7 +167,7 @@ impl Task for InstallPackageTask {
context.database.packages.push(LocalInstallation { context.database.packages.push(LocalInstallation {
name: package.name.to_owned(), name: package.name.to_owned(),
version, version,
shortcuts, shortcuts: Vec::new(),
files: installed_files, files: installed_files,
}); });
@ -195,11 +192,18 @@ impl Task for InstallPackageTask {
}), }),
), ),
TaskDependency::build( TaskDependency::build(
TaskOrdering::Pre, TaskOrdering::Post,
Box::new(InstallShortcutsTask { Box::new(InstallShortcutsTask {
name: self.name.clone(), name: self.name.clone(),
}), }),
), ),
TaskDependency::build(
TaskOrdering::Post,
Box::new(InstallDesktopShortcutTask {
name: self.name.clone(),
should_run: self.create_desktop_shortcuts
}),
),
TaskDependency::build(TaskOrdering::Post, Box::new(SaveDatabaseTask {})), TaskDependency::build(TaskOrdering::Post, Box::new(SaveDatabaseTask {})),
] ]
} }

View File

@ -83,9 +83,18 @@ impl Task for InstallShortcutsTask {
// TODO: Send by list // TODO: Send by list
&format!("--launcher \"{}\"", exe_path), &format!("--launcher \"{}\"", exe_path),
&starting_dir, &starting_dir,
exe_path,
)?); )?);
} }
// Update the installed packages shortcuts information in the database
let packages = &mut context.database.packages;
for pack in packages {
if pack.name == self.name {
pack.shortcuts.append(&mut installed_files);
}
}
Ok(TaskParamType::GeneratedShortcuts(installed_files)) Ok(TaskParamType::GeneratedShortcuts(installed_files))
} }

View File

@ -0,0 +1,74 @@
//! Configures lift to launch the new package on fresh install after its closed
//! If theres multiple launchable packages, then choose the first listed in config
//! If there are multiple shortcuts for the first package, then launch the first.
use installer::InstallerFramework;
use tasks::Task;
use tasks::TaskDependency;
use tasks::TaskMessage;
use tasks::TaskParamType;
use config::PackageDescription;
use logging::LoggingErrors;
pub struct LaunchOnExitTask {}
impl Task for LaunchOnExitTask {
fn execute(
&mut self,
_: Vec<TaskParamType>,
context: &mut InstallerFramework,
_: &dyn Fn(&TaskMessage),
) -> Result<TaskParamType, String> {
let pkg = &context.database.packages.first();
if pkg.is_none() {
return Ok(TaskParamType::None)
}
let pkg = pkg.unwrap();
// look up the first shortcut for the first listed package in the database
let path = context
.install_path
.as_ref()
.log_expect("No install path specified");
let mut metadata: Option<PackageDescription> = None;
for description in &context
.config
.as_ref()
.log_expect("Should have packages by now")
.packages
{
if pkg.name == description.name {
metadata = Some(description.clone());
break;
}
}
let package_desc = match metadata {
Some(v) => v,
// Package metadata is missing. Dunno what went wrong but we can skip this then
None => return Ok(TaskParamType::None),
};
let shortcut = package_desc.shortcuts.first();
// copy the path to the actual exe into launcher_path so it'll load it on exit
context.launcher_path = shortcut.map(|s| {
path.join(s.relative_path.clone()).to_str().map(|t| { t.to_string() }).unwrap()
});
Ok(TaskParamType::None)
}
fn dependencies(&self) -> Vec<TaskDependency> {
vec![]
}
fn name(&self) -> String {
"LaunchOnExitTask".to_string()
}
}

View File

@ -13,10 +13,12 @@ pub mod check_authorization;
pub mod download_pkg; pub mod download_pkg;
pub mod ensure_only_instance; pub mod ensure_only_instance;
pub mod install; pub mod install;
pub mod install_desktop_shortcut;
pub mod install_dir; pub mod install_dir;
pub mod install_global_shortcut; pub mod install_global_shortcut;
pub mod install_pkg; pub mod install_pkg;
pub mod install_shortcuts; pub mod install_shortcuts;
pub mod launch_installed_on_exit;
pub mod resolver; pub mod resolver;
pub mod save_database; pub mod save_database;
pub mod save_executable; pub mod save_executable;

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -4,7 +4,7 @@
<div class="container is-max-height"> <div class="container is-max-height">
<div class="columns is-max-height"> <div class="columns is-max-height">
<div class="column is-one-third has-padding" v-if="!$root.$data.metadata.is_launcher"> <div class="column is-one-third has-padding" v-if="!$root.$data.metadata.is_launcher">
<img src="./assets/logo.png" width="60%" alt="Application icon" /> <img src="./assets/light_mode_installer_logo.png" id="applicationIcon" alt="Application icon" />
<br /> <br />
<br /> <br />
@ -52,6 +52,30 @@ body, div, span, h1, h2, h3, h4, h5, h6 {
cursor: default; cursor: default;
} }
#applicationIcon {
width:0px; height: 0px;
padding: 50px 60% 0px 0px;
background: url("./assets/light_mode_installer_logo.png") left top no-repeat;
background-size: contain;
}
body.has-background-black-ter #applicationIcon {
background: url("./assets/dark_mode_installer_logo.png") left top no-repeat;
background-size: contain;
}
.package-icon {
width: 3rem;
height: 3rem;
float: left;
padding-right: 10px;
padding-top: 10px;
}
.package-description {
overflow: hidden;
}
pre { pre {
-webkit-user-select: text; -webkit-user-select: text;
-moz-user-select: text; -moz-user-select: text;
@ -124,7 +148,7 @@ pre {
} }
/* Dark mode */ /* Dark mode */
body.has-background-black-ter .subtitle, body.has-background-black-ter .column > div { body.has-background-black-ter .subtitle, body.has-background-black-ter .column > div, body.has-background-black-ter section {
color: hsl(0, 0%, 96%); color: hsl(0, 0%, 96%);
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -115,7 +115,8 @@ var app = new Vue({
'\n\nPlease upload the log file (in ' + search_location + ') to ' + '\n\nPlease upload the log file (in ' + search_location + ') to ' +
'the ' + app.attrs.name + ' team' 'the ' + app.attrs.name + ' team'
}}); }});
} },
{} // pass in nothing to cause `ajax` to post instead of get
) )
}, },
check_authentication: function (success, error) { check_authentication: function (success, error) {

View File

@ -30,7 +30,7 @@ export default new Router({
component: SelectPackages component: SelectPackages
}, },
{ {
path: '/install/:kind', path: '/install/:kind/:desktop_shortcut',
name: 'install', name: 'install',
component: InstallPackages component: InstallPackages
}, },

View File

@ -1,5 +1,6 @@
<template> <template>
<div class="column has-padding"> <div class="column has-padding">
<section>
<b-message type="is-info" :active.sync="browser_opened"> <b-message type="is-info" :active.sync="browser_opened">
Page opened! Check your default browser for the page, and follow the instructions there to link your patreon account. Page opened! Check your default browser for the page, and follow the instructions there to link your patreon account.
When you are done, enter the token below. When you are done, enter the token below.
@ -10,19 +11,28 @@
To be an Early Access member, you must be a Patreon Early Access Subscriber. To be an Early Access member, you must be a Patreon Early Access Subscriber.
</b-message> </b-message>
<div> <div>
If you are a subscriber, <a v-on:click="launch_browser('https://profile.yuzu-emu.org/')">click here to link your yuzu-emu.org account</a> If you are a subscriber, <a v-on:click="launch_browser('https://profile.yuzu-emu.org/')">click here to link your yuzu-emu.org account</a>
<br> <br>
If you are not already a subscriber, <a v-on:click="launch_browser('https://www.patreon.com/join/yuzuteam/checkout?rid=2822069')">click here to become one</a> If you are not already a subscriber, <a v-on:click="launch_browser('https://www.patreon.com/join/yuzuteam/checkout?rid=2822069')">click here to become one</a>
</div>
<br>
<div class="control">
<label for="token">Token</label>
<input class="input" type="text" v-model="combined_token" placeholder="Token" id="token">
</div> </div>
</section>
<br> <br>
<section>
<p>Token</p>
<b-field>
<b-input type="text" v-model="combined_token" placeholder="Token" id="token" style='width: 50em;'></b-input>
<p class="control">
<button class="button is-info" v-on:click="paste">Paste</button>
</p>
</b-field>
</section>
<br>
<section>
<b-message type="is-danger" :active.sync="invalid_token"> <b-message type="is-danger" :active.sync="invalid_token">
Login failed! Login failed!
Double check that your token is correct and try again Double check that your token is correct and try again
@ -47,6 +57,7 @@
Your patreon is linked, and you are supporting the project, but you must first join the Early Access reward tier! Your patreon is linked, and you are supporting the project, but you must first join the Early Access reward tier!
<a v-on:click="launch_browser('https://www.patreon.com/join/yuzuteam/checkout?rid=2822069')">Log into your patreon account</a> and choose to back the Early Access reward tier. <a v-on:click="launch_browser('https://www.patreon.com/join/yuzuteam/checkout?rid=2822069')">Log into your patreon account</a> and choose to back the Early Access reward tier.
</b-message> </b-message>
</section>
<div class="is-left-floating is-bottom-floating"> <div class="is-left-floating is-bottom-floating">
<p class="control"> <p class="control">
@ -63,6 +74,8 @@
</template> </template>
<script> <script>
export default { export default {
name: 'AuthenticationView', name: 'AuthenticationView',
created: function() { created: function() {
@ -120,6 +133,11 @@ export default {
go_back: function () { go_back: function () {
this.$router.go(-1) this.$router.go(-1)
}, },
paste: function () {
document.getElementById("token").focus();
document.execCommand("paste");
},
launch_browser: function(url) { launch_browser: function(url) {
const that = this; const that = this;
let app = this.$root; let app = this.$root;

View File

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

@ -31,7 +31,7 @@ export default {
// Update the updater if needed // Update the updater if needed
if (that.$root.config.new_tool) { if (that.$root.config.new_tool) {
this.$router.push('/install/updater') this.$router.push('/install/updater/false')
return return
} }
@ -70,7 +70,7 @@ export default {
} }
this.$router.replace({ name: 'migrate', this.$router.replace({ name: 'migrate',
params: { next: app.metadata.is_launcher ? '/install/regular' : '/modify' } }) params: { next: app.metadata.is_launcher ? '/install/regular/false' : '/modify' } })
} else { } else {
for (var x = 0; x < app.config.packages.length; x++) { for (var x = 0; x < app.config.packages.length; x++) {
app.config.packages[x].installed = false app.config.packages[x].installed = false

View File

@ -3,7 +3,7 @@
<b-message title="An error occurred" type="is-danger" :closable="false"> <b-message title="An error occurred" 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" v-bind:class="{ 'is-bottom-floating': !$root.$data.metadata.is_launcher, 'is-top-floating': $root.$data.metadata.is_launcher }"> <div class="field is-grouped is-right-floating is-bottom-floating">
<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> <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> <a class="button is-primary is-medium" v-if="$root.$data.metadata.is_launcher" v-on:click="exit">Exit</a>

View File

@ -24,6 +24,7 @@ export default {
is_uninstall: false, is_uninstall: false,
is_updater_update: false, is_updater_update: false,
is_update: false, is_update: false,
install_desktop_shortcut: false,
failed_with_error: false, failed_with_error: false,
authorization_required: false, authorization_required: false,
packages_installed: 0 packages_installed: 0
@ -33,7 +34,9 @@ export default {
this.is_uninstall = this.$route.params.kind === 'uninstall' this.is_uninstall = this.$route.params.kind === 'uninstall'
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'
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)
this.install() this.install()
}, },
methods: { methods: {
@ -53,6 +56,7 @@ export default {
} }
results['path'] = app.install_location results['path'] = app.install_location
results['installDesktopShortcut'] = that.install_desktop_shortcut
var targetUrl = '/api/start-install' var targetUrl = '/api/start-install'
if (this.is_uninstall) { if (this.is_uninstall) {
@ -88,7 +92,7 @@ export default {
if (that.is_updater_update) { if (that.is_updater_update) {
// Continue with what we were doing // Continue with what we were doing
if (app.metadata.is_launcher) { if (app.metadata.is_launcher) {
that.$router.replace('/install/regular') that.$router.replace('/install/regular/' + that.install_desktop_shortcut.toString())
} else { } else {
if (app.metadata.preexisting_install) { if (app.metadata.preexisting_install) {
that.$router.replace('/modify') that.$router.replace('/modify')

View File

@ -43,7 +43,7 @@ export default {
}, },
methods: { methods: {
update: function () { update: function () {
this.$router.push('/install/update') this.$router.push('/install/update/false')
}, },
modify_packages: function () { modify_packages: function () {
this.$router.push('/packages') this.$router.push('/packages')
@ -55,7 +55,7 @@ export default {
this.show_uninstall = false this.show_uninstall = false
}, },
uninstall: function () { uninstall: function () {
this.$router.push('/install/uninstall') this.$router.push('/install/uninstall/false')
} }
} }
} }

View File

@ -1,144 +1,158 @@
<template> <template>
<div class="column has-padding"> <div class="column has-padding">
<h4 class="subtitle">Select which packages you want to install:</h4> <!-- Build options -->
<div class="tile is-ancestor">
<!-- Build options --> <div class="tile is-parent is-vertical">
<div class="tile is-ancestor"> <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="ribbon" v-if="Lpackage.is_new"><span>New!</span></div>
<div class="tile is-child"> <label class="checkbox">
<div class="box clickable-box" v-if="!Lpackage.requires_authorization || (Lpackage.requires_authorization && $root.$data.has_reward_tier)" v-on:click.capture.stop="Lpackage.default = !Lpackage.default"> <b-checkbox v-model="Lpackage.default">
<div class="ribbon" v-if="Lpackage.is_new"><span>New!</span></div> <span v-if="!Lpackage.installed">Install</span> {{ Lpackage.name }}
<label class="checkbox"> </b-checkbox>
<b-checkbox v-model="Lpackage.default"> <span v-if="Lpackage.installed"><i>(installed)</i></span>
{{ Lpackage.name }} </label>
</b-checkbox> <div>
<span v-if="Lpackage.installed"><i>(installed)</i></span> <img class="package-icon" :src="`${publicPath + Lpackage.icon}`"/>
</label> <p style="padding-top: 4px;" class="package-description">
<p> {{ Lpackage.description }}
{{ Lpackage.description }} </p>
</p> <p class="package-description">
</div> {{ get_extended_description(Lpackage) }}
<div class="box clickable-box" v-else-if="Lpackage.requires_authorization && !$root.$data.is_authenticated" v-on:click="show_authentication"> </p>
<div class="ribbon" v-if="Lpackage.is_new"><span>New!</span></div>
<b-checkbox>
{{ Lpackage.name }}
</b-checkbox>
<p>
{{Lpackage.need_authentication_description}}
</p>
</div>
<div class="box clickable-box" v-else-if="Lpackage.requires_authorization && !$root.$data.is_linked" v-on:click="show_authorization">
<div class="ribbon" v-if="Lpackage.is_new"><span>New!</span></div>
<b-checkbox>
{{ Lpackage.name }}
</b-checkbox>
<p>
{{Lpackage.need_link_description}}
</p>
</div>
<div class="box clickable-box" v-else-if="Lpackage.requires_authorization && !$root.$data.is_subscribed" v-on:click="show_authorization">
<div class="ribbon" v-if="Lpackage.is_new"><span>New!</span></div>
<b-checkbox>
{{ Lpackage.name }}
</b-checkbox>
<p>
{{Lpackage.need_subscription_description}}
</p>
</div>
<div class="box clickable-box" v-else v-on:click="show_authorization">
<div class="ribbon" v-if="Lpackage.is_new"><span>New!</span></div>
<b-checkbox>
{{ Lpackage.name }}
</b-checkbox>
<p>
{{Lpackage.need_reward_tier_description}}
</p>
</div>
</div>
</div>
</div>
<div class="subtitle is-6" v-if="!$root.$data.metadata.preexisting_install && advanced">Install 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">
</div>
<div class="control">
<a class="button is-dark" v-on:click="select_file">
Select
</a>
</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>
</p>
<p class="control">
<a class="button is-dark is-medium" v-if="!$root.$data.metadata.preexisting_install"
v-on:click="install">Install</a>
</p>
<p class="control">
<a class="button is-dark is-medium" v-if="$root.$data.metadata.preexisting_install"
v-on:click="install">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>
</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">
Create Desktop Shortcut
</b-checkbox>
</div>
</div> </div>
</div>
<div class="subtitle is-6" v-if="!$root.$data.metadata.preexisting_install && advanced">Install 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">
</div>
<div class="control">
<a class="button is-dark" v-on:click="select_file">
Select
</a>
</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>
</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>
</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>
</p>
</div>
</div>
</template> </template>
<script> <script>
export default { export default {
name: 'SelectPackages', name: 'SelectPackages',
created: function() { created: function() {
if (this.$root.$data.has_reward_tier) { // If they are authorized, make the packages that require authorization default
for (let package_index = 0; package_index < this.$root.config.packages.length; package_index++) { // and also deselect any packages that don't use authorization
let current_package = this.$root.config.packages[package_index]; if (this.$root.$data.has_reward_tier) {
// If they are authorized, make the packages that require authorization default for (let package_index = 0; package_index < this.$root.config.packages.length; package_index++) {
if (current_package.requires_authorization) { let current_package = this.$root.config.packages[package_index];
current_package.default = true; current_package.default = current_package.requires_authorization;
} else { }
// And unselect any other packages }
current_package.default = false; },
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({
SelectInstallDir: {
callback_name: 'selectFileCallback'
}
}))
},
install: function () {
this.$router.push('/install/regular/' + this.installDesktopShortcut.toString())
},
go_back: function () {
this.$router.go(-1)
},
show_authentication: function () {
this.$router.push('/authentication')
},
show_authorization: function () {
this.$router.push('/authentication')
},
installable: function (pkg) {
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;
} else if (pkg.requires_authorization && !this.$root.$data.is_authenticated) {
this.show_authentication()
} else if (pkg.requires_authorization && !this.$root.$data.is_linked) {
this.show_authorization()
} else if (pkg.requires_authorization && !this.$root.$data.is_subscribed) {
this.show_authorization()
} else { // need_reward_tier_description
this.show_authorization()
}
},
get_extended_description: function(pkg) {
if (!pkg.extended_description) {
return "";
}
if (this.installable(pkg)) {
return pkg.extended_description.no_action_description;
} else if (pkg.requires_authorization && !this.$root.$data.is_authenticated) {
return pkg.extended_description.need_authentication_description;
} else if (pkg.requires_authorization && !this.$root.$data.is_linked) {
return pkg.extended_description.need_link_description;
} else if (pkg.requires_authorization && !this.$root.$data.is_subscribed) {
return pkg.extended_description.need_subscription_description;
} else { // need_reward_tier_description
return pkg.extended_description.need_reward_tier_description;
} }
} }
} }
},
data: function () {
return {
advanced: false
}
},
methods: {
select_file: function () {
window.external.invoke(JSON.stringify({
SelectInstallDir: {
callback_name: 'selectFileCallback'
}
}))
},
install: function () {
this.$router.push('/install/regular')
},
go_back: function () {
this.$router.go(-1)
},
show_authentication: function () {
this.$router.push('/authentication')
},
show_authorization: function () {
this.$router.push('/authentication')
},
} }
}
</script> </script>