From a6bed95de36ed41354a8794d824c29af27c19ffa Mon Sep 17 00:00:00 2001 From: asimon-1 <40246417+asimon-1@users.noreply.github.com> Date: Sat, 9 Apr 2022 18:10:44 -0400 Subject: [PATCH] Tabbed Web Menu (#333) * Web menu refactor * Fix some menu items * Fixes for quick_menu, general clippy fixes * Revert small testing change * Add quick menu SVG * Fix defaults saving/loading * Log the last URL from the web menu Co-authored-by: jugeeya --- src/common/menu.rs | 44 +- src/common/mod.rs | 276 ++++---- src/lib.rs | 35 +- src/static/css/training_modpack.css | 466 ++++++------- src/static/img/quick_menu.svg | 29 + src/static/js/training_modpack.js | 399 ++++++------ src/templates/menu.html | 160 ++--- training_mod_consts/src/lib.rs | 974 ++++++++++++---------------- training_mod_tui/src/lib.rs | 163 ++--- training_mod_tui/src/list.rs | 53 +- training_mod_tui/src/main.rs | 4 +- 11 files changed, 1137 insertions(+), 1466 deletions(-) create mode 100644 src/static/img/quick_menu.svg diff --git a/src/common/menu.rs b/src/common/menu.rs index ee8f787..bc074e6 100644 --- a/src/common/menu.rs +++ b/src/common/menu.rs @@ -66,10 +66,10 @@ pub unsafe fn write_menu() { const MENU_CONF_PATH: &str = "sd:/TrainingModpack/training_modpack_menu.conf"; -pub fn set_menu_from_url(orig_last_url: &str) { - let last_url = &orig_last_url.replace("&save_defaults=1", ""); +pub fn set_menu_from_url(last_url: &str) { unsafe { - MENU = get_menu_from_url(MENU, last_url); + MENU = get_menu_from_url(MENU, last_url, false); + DEFAULTS_MENU = get_menu_from_url(MENU, last_url, true); if MENU.quick_menu == OnOff::Off { if is_emulator() { @@ -83,17 +83,6 @@ pub fn set_menu_from_url(orig_last_url: &str) { } } - if last_url.len() != orig_last_url.len() { - // Save as default - unsafe { - DEFAULT_MENU = MENU; - write_menu(); - } - let menu_defaults_conf_path = "sd:/TrainingModpack/training_modpack_menu_defaults.conf"; - std::fs::write(menu_defaults_conf_path, last_url) - .expect("Failed to write default menu conf file"); - } - std::fs::write(MENU_CONF_PATH, last_url).expect("Failed to write menu conf file"); unsafe { EVENT_QUEUE.push(Event::menu_open(last_url.to_string())); @@ -115,19 +104,22 @@ pub fn spawn_menu() { if !quick_menu { let fname = "training_menu.html"; - let params = unsafe { MENU.to_url_params() }; - let page_response = Webpage::new() - .background(Background::BlurredScreenshot) - .htdocs_dir("training_modpack") - .boot_display(BootDisplay::BlurredScreenshot) - .boot_icon(true) - .start_page(&format!("{}{}", fname, params)) - .open() - .unwrap(); + unsafe { + let params = MENU.to_url_params(false); + let default_params = DEFAULTS_MENU.to_url_params(true); + let page_response = Webpage::new() + .background(Background::BlurredScreenshot) + .htdocs_dir("training_modpack") + .boot_display(BootDisplay::BlurredScreenshot) + .boot_icon(true) + .start_page(&format!("{}?{}&{}", fname, params, default_params)) + .open() + .unwrap(); - let orig_last_url = page_response.get_last_url().unwrap(); - - set_menu_from_url(orig_last_url); + let last_url = page_response.get_last_url().unwrap(); + println!("Received URL from web menu: {}", last_url); + set_menu_from_url(last_url); + } } else { unsafe { QUICK_MENU_ACTIVE = true; diff --git a/src/common/mod.rs b/src/common/mod.rs index 1673bc5..be0c7ea 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,138 +1,138 @@ -pub mod consts; -pub mod events; -pub mod menu; -pub mod raygun_printer; -pub mod release; - -use crate::common::consts::*; -use smash::app::{self, lua_bind::*}; -use smash::lib::lua_const::*; - -pub use crate::common::consts::MENU; -pub static mut DEFAULT_MENU: TrainingModpackMenu = crate::common::consts::DEFAULT_MENU; -pub static mut BASE_MENU: TrainingModpackMenu = unsafe { DEFAULT_MENU }; -pub static mut FIGHTER_MANAGER_ADDR: usize = 0; -pub static mut STAGE_MANAGER_ADDR: usize = 0; - -#[cfg(not(feature = "outside_training_mode"))] -extern "C" { - #[link_name = "\u{1}_ZN3app9smashball16is_training_modeEv"] - pub fn is_training_mode() -> bool; -} - -#[cfg(feature = "outside_training_mode")] -pub fn is_training_mode() -> bool { - return true; -} - -pub fn get_category(module_accessor: &mut app::BattleObjectModuleAccessor) -> i32 { - (module_accessor.info >> 28) as u8 as i32 -} - -pub fn is_emulator() -> bool { - unsafe { skyline::hooks::getRegionAddress(skyline::hooks::Region::Text) as u64 == 0x8004000 } -} - -pub fn get_module_accessor(fighter_id: FighterId) -> *mut app::BattleObjectModuleAccessor { - let entry_id_int = fighter_id as i32; - let entry_id = app::FighterEntryID(entry_id_int); - unsafe { - let mgr = *(FIGHTER_MANAGER_ADDR as *mut *mut app::FighterManager); - let fighter_entry = - FighterManager::get_fighter_entry(mgr, entry_id) as *mut app::FighterEntry; - let current_fighter_id = FighterEntry::current_fighter_id(fighter_entry); - app::sv_battle_object::module_accessor(current_fighter_id as u32) - } -} - -pub fn is_fighter(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { - get_category(module_accessor) == BATTLE_OBJECT_CATEGORY_FIGHTER -} - -pub fn is_operation_cpu(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { - unsafe { - if !is_fighter(module_accessor) { - return false; - } - - let entry_id_int = - WorkModule::get_int(module_accessor, *FIGHTER_INSTANCE_WORK_ID_INT_ENTRY_ID) as i32; - - if entry_id_int == 0 { - return false; - } - - let entry_id = app::FighterEntryID(entry_id_int); - let mgr = *(FIGHTER_MANAGER_ADDR as *mut *mut app::FighterManager); - let fighter_information = - FighterManager::get_fighter_information(mgr, entry_id) as *mut app::FighterInformation; - - FighterInformation::is_operation_cpu(fighter_information) - } -} - -pub fn is_grounded(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { - let situation_kind = unsafe { StatusModule::situation_kind(module_accessor) as i32 }; - - situation_kind == SITUATION_KIND_GROUND -} - -pub fn is_airborne(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { - let situation_kind = unsafe { StatusModule::situation_kind(module_accessor) as i32 }; - - situation_kind == SITUATION_KIND_AIR -} - -pub fn is_idle(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { - let status_kind = unsafe { StatusModule::status_kind(module_accessor) }; - - status_kind == FIGHTER_STATUS_KIND_WAIT -} - -pub fn is_in_hitstun(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { - let status_kind = unsafe { StatusModule::status_kind(module_accessor) }; - - (*FIGHTER_STATUS_KIND_DAMAGE..*FIGHTER_STATUS_KIND_DAMAGE_FALL).contains(&status_kind) -} -pub fn is_in_footstool(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { - let status_kind = unsafe { StatusModule::status_kind(module_accessor) }; - - (*FIGHTER_STATUS_KIND_TREAD_DAMAGE..=*FIGHTER_STATUS_KIND_TREAD_FALL).contains(&status_kind) -} - -pub fn is_shielding(module_accessor: *mut app::BattleObjectModuleAccessor) -> bool { - let status_kind = unsafe { StatusModule::status_kind(module_accessor) as i32 }; - - (*FIGHTER_STATUS_KIND_GUARD_ON..=*FIGHTER_STATUS_KIND_GUARD_DAMAGE).contains(&status_kind) -} - -pub fn is_in_shieldstun(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { - let status_kind = unsafe { StatusModule::status_kind(module_accessor) }; - let prev_status = unsafe { StatusModule::prev_status_kind(module_accessor, 0) }; - - // If we are taking shield damage or we are droping shield from taking shield damage we are in hitstun - status_kind == FIGHTER_STATUS_KIND_GUARD_DAMAGE - || (prev_status == FIGHTER_STATUS_KIND_GUARD_DAMAGE - && status_kind == FIGHTER_STATUS_KIND_GUARD_OFF) -} - -pub unsafe fn is_dead(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { - let fighter_kind = app::utility::get_kind(module_accessor); - let fighter_is_ptrainer = [ - *FIGHTER_KIND_PZENIGAME, - *FIGHTER_KIND_PFUSHIGISOU, - *FIGHTER_KIND_PLIZARDON, - ] - .contains(&fighter_kind); - let status_kind = StatusModule::status_kind(module_accessor) as i32; - let prev_status_kind = StatusModule::prev_status_kind(module_accessor, 0); - // Pokemon trainer enters FIGHTER_STATUS_KIND_WAIT for one frame during their respawn animation - // And the previous status is FIGHTER_STATUS_NONE - if fighter_is_ptrainer { - [*FIGHTER_STATUS_KIND_DEAD, *FIGHTER_STATUS_KIND_STANDBY].contains(&status_kind) - || (status_kind == FIGHTER_STATUS_KIND_WAIT - && prev_status_kind == FIGHTER_STATUS_KIND_NONE) - } else { - [*FIGHTER_STATUS_KIND_DEAD, *FIGHTER_STATUS_KIND_STANDBY].contains(&status_kind) - } -} +pub mod consts; +pub mod events; +pub mod menu; +pub mod raygun_printer; +pub mod release; + +use crate::common::consts::*; +use smash::app::{self, lua_bind::*}; +use smash::lib::lua_const::*; + +pub use crate::common::consts::MENU; +pub static mut DEFAULTS_MENU: TrainingModpackMenu = crate::common::consts::DEFAULTS_MENU; +pub static mut BASE_MENU: TrainingModpackMenu = unsafe { DEFAULTS_MENU }; +pub static mut FIGHTER_MANAGER_ADDR: usize = 0; +pub static mut STAGE_MANAGER_ADDR: usize = 0; + +#[cfg(not(feature = "outside_training_mode"))] +extern "C" { + #[link_name = "\u{1}_ZN3app9smashball16is_training_modeEv"] + pub fn is_training_mode() -> bool; +} + +#[cfg(feature = "outside_training_mode")] +pub fn is_training_mode() -> bool { + return true; +} + +pub fn get_category(module_accessor: &mut app::BattleObjectModuleAccessor) -> i32 { + (module_accessor.info >> 28) as u8 as i32 +} + +pub fn is_emulator() -> bool { + unsafe { skyline::hooks::getRegionAddress(skyline::hooks::Region::Text) as u64 == 0x8004000 } +} + +pub fn get_module_accessor(fighter_id: FighterId) -> *mut app::BattleObjectModuleAccessor { + let entry_id_int = fighter_id as i32; + let entry_id = app::FighterEntryID(entry_id_int); + unsafe { + let mgr = *(FIGHTER_MANAGER_ADDR as *mut *mut app::FighterManager); + let fighter_entry = + FighterManager::get_fighter_entry(mgr, entry_id) as *mut app::FighterEntry; + let current_fighter_id = FighterEntry::current_fighter_id(fighter_entry); + app::sv_battle_object::module_accessor(current_fighter_id as u32) + } +} + +pub fn is_fighter(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { + get_category(module_accessor) == BATTLE_OBJECT_CATEGORY_FIGHTER +} + +pub fn is_operation_cpu(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { + unsafe { + if !is_fighter(module_accessor) { + return false; + } + + let entry_id_int = + WorkModule::get_int(module_accessor, *FIGHTER_INSTANCE_WORK_ID_INT_ENTRY_ID) as i32; + + if entry_id_int == 0 { + return false; + } + + let entry_id = app::FighterEntryID(entry_id_int); + let mgr = *(FIGHTER_MANAGER_ADDR as *mut *mut app::FighterManager); + let fighter_information = + FighterManager::get_fighter_information(mgr, entry_id) as *mut app::FighterInformation; + + FighterInformation::is_operation_cpu(fighter_information) + } +} + +pub fn is_grounded(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { + let situation_kind = unsafe { StatusModule::situation_kind(module_accessor) as i32 }; + + situation_kind == SITUATION_KIND_GROUND +} + +pub fn is_airborne(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { + let situation_kind = unsafe { StatusModule::situation_kind(module_accessor) as i32 }; + + situation_kind == SITUATION_KIND_AIR +} + +pub fn is_idle(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { + let status_kind = unsafe { StatusModule::status_kind(module_accessor) }; + + status_kind == FIGHTER_STATUS_KIND_WAIT +} + +pub fn is_in_hitstun(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { + let status_kind = unsafe { StatusModule::status_kind(module_accessor) }; + + (*FIGHTER_STATUS_KIND_DAMAGE..*FIGHTER_STATUS_KIND_DAMAGE_FALL).contains(&status_kind) +} +pub fn is_in_footstool(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { + let status_kind = unsafe { StatusModule::status_kind(module_accessor) }; + + (*FIGHTER_STATUS_KIND_TREAD_DAMAGE..=*FIGHTER_STATUS_KIND_TREAD_FALL).contains(&status_kind) +} + +pub fn is_shielding(module_accessor: *mut app::BattleObjectModuleAccessor) -> bool { + let status_kind = unsafe { StatusModule::status_kind(module_accessor) as i32 }; + + (*FIGHTER_STATUS_KIND_GUARD_ON..=*FIGHTER_STATUS_KIND_GUARD_DAMAGE).contains(&status_kind) +} + +pub fn is_in_shieldstun(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { + let status_kind = unsafe { StatusModule::status_kind(module_accessor) }; + let prev_status = unsafe { StatusModule::prev_status_kind(module_accessor, 0) }; + + // If we are taking shield damage or we are droping shield from taking shield damage we are in hitstun + status_kind == FIGHTER_STATUS_KIND_GUARD_DAMAGE + || (prev_status == FIGHTER_STATUS_KIND_GUARD_DAMAGE + && status_kind == FIGHTER_STATUS_KIND_GUARD_OFF) +} + +pub unsafe fn is_dead(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { + let fighter_kind = app::utility::get_kind(module_accessor); + let fighter_is_ptrainer = [ + *FIGHTER_KIND_PZENIGAME, + *FIGHTER_KIND_PFUSHIGISOU, + *FIGHTER_KIND_PLIZARDON, + ] + .contains(&fighter_kind); + let status_kind = StatusModule::status_kind(module_accessor) as i32; + let prev_status_kind = StatusModule::prev_status_kind(module_accessor, 0); + // Pokemon trainer enters FIGHTER_STATUS_KIND_WAIT for one frame during their respawn animation + // And the previous status is FIGHTER_STATUS_NONE + if fighter_is_ptrainer { + [*FIGHTER_STATUS_KIND_DEAD, *FIGHTER_STATUS_KIND_STANDBY].contains(&status_kind) + || (status_kind == FIGHTER_STATUS_KIND_WAIT + && prev_status_kind == FIGHTER_STATUS_KIND_NONE) + } else { + [*FIGHTER_STATUS_KIND_DEAD, *FIGHTER_STATUS_KIND_STANDBY].contains(&status_kind) + } +} diff --git a/src/lib.rs b/src/lib.rs index d110a66..9fa5a17 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,7 +71,7 @@ pub fn render_text_to_screen(s: &str) { pub fn main() { macro_rules! log { ($($arg:tt)*) => { - print!("{}{}", "[Training Modpack] ".green(), format!($($arg)*)); + println!("{}{}", "[Training Modpack] ".green(), format!($($arg)*)); }; } @@ -105,10 +105,8 @@ pub fn main() { if menu_conf.starts_with(b"http://localhost") { log!("Previous menu found, loading from training_modpack_menu.conf"); unsafe { - MENU = get_menu_from_url(MENU, std::str::from_utf8(&menu_conf).unwrap()); - if is_emulator() { - MENU.quick_menu = OnOff::On; - } + MENU = get_menu_from_url(MENU, std::str::from_utf8(&menu_conf).unwrap(), false); + DEFAULTS_MENU = get_menu_from_url(DEFAULTS_MENU, std::str::from_utf8(&menu_conf).unwrap(), true); } } else { log!("Previous menu found but is invalid."); @@ -116,32 +114,10 @@ pub fn main() { } else { log!("No previous menu file found."); } - - let menu_defaults_conf_path = "sd:/TrainingModpack/training_modpack_menu_defaults.conf"; - log!("Checking for previous menu defaults in training_modpack_menu_defaults.conf..."); - if fs::metadata(menu_defaults_conf_path).is_ok() { - let menu_defaults_conf = fs::read(menu_defaults_conf_path).unwrap(); - if menu_defaults_conf.starts_with(b"http://localhost") { - log!("Menu defaults found, loading from training_modpack_menu_defaults.conf"); - unsafe { - DEFAULT_MENU = get_menu_from_url( - DEFAULT_MENU, - std::str::from_utf8(&menu_defaults_conf).unwrap(), - ); - if is_emulator() { - DEFAULT_MENU.quick_menu = OnOff::On; - } - crate::menu::write_menu(); - } - } else { - log!("Previous menu defaults found but are invalid."); - } - } else { - log!("No previous menu defaults found."); - } - + if is_emulator() { unsafe { + DEFAULTS_MENU.quick_menu = OnOff::On; MENU.quick_menu = OnOff::On; } } @@ -195,6 +171,7 @@ pub fn main() { // Leave menu. menu::QUICK_MENU_ACTIVE = false; crate::menu::set_menu_from_url(url.as_str()); + println!("URL: {}", url.as_str()); } }); button_presses.zl.read_press().then(|| { diff --git a/src/static/css/training_modpack.css b/src/static/css/training_modpack.css index aa57bb5..46286ae 100644 --- a/src/static/css/training_modpack.css +++ b/src/static/css/training_modpack.css @@ -20,337 +20,261 @@ } } -.answer-border-outer { - margin-top: 5px; -} - -.button-icon { - height: 53px; - margin-left: 4px; - width: 53px; -} - -.button-icon-wrapper { - align-items: center; - background: #000000; +.tab-list-container { + overflow: hidden; + background-color: #555; display: flex; - height: 58px; - margin-left: -1px; - width: 80px; + justify-content: flex-start; + width: 100%; + align-items: center; } -.button-msg-wrapper { - width: 259px; +.tab-list-container p { + color: #fff; + width: 130px; + height: fit-content; + margin: 0px 10px 0px 10px; + padding: 0px; } -.checkbox-display { - margin: 10px 70px; -} -.checkbox-display::after { - /* Displayed Checkbox (unchecked) */ - color: white; - content: "\E14C"; -} - -.defaults-checkbox-container { - /* Save Defaults Container */ +.tab-list { + overflow: hidden; display: flex; - flex-direction: column; - justify-content: center; - margin-top: 10px; - position: fixed; - right: 50px; + justify-content: flex-start; + width: 100%; } -.flex-button { - align-items: center; - display: inline-flex; - flex: 1; - justify-content: center; - text-align: center; - vertical-align: middle; +.tab-list button { + background-color: inherit; + float: left; + border: none; + outline: none; + cursor: pointer; + padding: 14px 16px; + color: #fff; + margin: 5px 0px 0px 8px; + border-radius: 8px 8px 0px 0px; + font-size: large; } -.footer { - align-items: center; - background: #000000; +.tab-list button:hover { + background: #797979; +} + +.tab-list button.active { + color: #000; + background: #ccc; +} + +.tab-content { + width: 100%; display: flex; - height: 73px; - justify-content: center; - position: fixed; - z-index: 10; + flex-wrap: wrap; + justify-content: space-evenly; } -.header { - align-items: center; - background: #000000; - border-bottom: 7px solid #000000; +body { + background: none; + font-family: "nintendo_ext_003", "nintendo_udsgr_std_003"; + margin: 0; +} + +/* Overwrite padding from keyword stuff. */ +.l-main-content { + padding: 0px 0px 0px; +} + +/* Handle alignment of items in the header */ +.l-header { display: flex; - height: 65px; -} - -.header-decoration { - align-items: center; - background: #a80114; - display: inline-flex; - height: 65px; - padding-left: 21px; - width: 101px; -} - -.header-desc { - color: white; + justify-content: space-between; + position: relative; + width: 100%; + height: 80px; + z-index: 100; + background: #000; + box-shadow: 0px 1px 1px #000; } .header-title { color: #f46264; font-size: 26px; line-height: 2.5; + margin: 0px; + padding: 0px; + word-break: normal; } -.is-appear { - opacity: 1; +.header-label { + display: flex; + flex-direction: column; + justify-content: center; + align-items: end; + margin-right: 15px; } -.is-focused .question-border::before { - background: #000000; - box-shadow: none; +.header-label > p { + color: #fff; + margin: 0; + padding: 0; } -.is-focused .question-message { - color: #FFFFFF; - text-shadow: 2px 0 0 #000, -2px 0 0 #000, 0 2px 0 #000, 0 -2px 0 #000, 1px 1px #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000; +.return-icon-container { + width: 101px; + height: 65px; + padding-left: 21px; + background: #a80114; + border-radius: 0px 0px 15px 0px; } -.is-focused .question.scuffle-thema { - background: #df1624; +.return-icon { + width: 58px; + height: 58px; + padding-left: 7px; + filter: drop-shadow(3px 5px 2px rgba(0, 0, 0, 0.8)); } -.is-focused .scuffle-thema { - background: #df1624; +/* Center Icons */ +.question::before { + width: 70px; } -.is-hidden { - display: none; -} - -.is-opened .question { - bottom: 11px; -} - -.is-opened .question-border::before { - background: #000000; - bottom: 5px; -} - -.is-opened .question-outer { - height: 86px; - width: 100%; -} - -.keyword-button { - background: #d9e4ea; - border: solid 4px #2e3c45; - box-shadow: 1px 1px 6px rgba(24, 24, 24, 0.6); - box-sizing: border-box; - height: 66px; - justify-content: flex-start; - position: relative; - z-index: 0; -} - -.keyword-button-outer { - border: 5px solid transparent; - will-change: transform; -} - -.keyword-message { - color: #2b3940; - font-size: 22px; - padding: 0px 5px; -} - -.l-footer { +/* Footer */ +.footer { + position: fixed; bottom: 0px; left: 0px; - width: 100%; -} - -.l-grid { + background: #000; display: flex; - flex-wrap: wrap; -} - -.l-header { - box-shadow: 0px 1px 1px #000000; - display: flex; - left: 0px; - position: absolute; - top: 0px; + justify-content: center; + align-items: center; + height: 73px; width: 100%; + color: #fff; z-index: 100; } -.l-header-title { - align-items: center; +/* Save Defaults Container */ +.defaults-checkbox-container { + position: fixed; + right: 50px; + margin-top: 10px; display: flex; - height: 100%; justify-content: center; - left: 0px; - position: absolute; - top: 0px; - width: 1280px; -} - -.l-item { - margin: 0px 13px; -} - -.l-main-content { - /* Overwrite padding from keyword stuff. */ - padding: 0px 0px 0px; -} - -.l-qa { - /* Column size */ - flex-basis: 33%; -} - -.l-qa:last-child .qa { - /* Overwrite margin on the last child to avoid overlap with footer */ - margin-bottom: 75px; -} - -.l-qa:last-child .qa.is-opened { - margin-bottom: 0px; -} - -.qa { - display: block; - will-change: scroll-position; -} - -.is-focused { - background: linear-gradient(90deg, rgb(255, 109, 0) 0%, rgb(255, 255, 0) 65%, rgb(255, 109, 0) 70%); - will-change: animation; - animation: background-slide 650ms linear infinite normal; -} - -.question { - align-items: center; - background: #d9e4ea; - bottom: 11px; - display: flex; - left: 11px; - padding: 0px 30px 0px 94px; - position: absolute; - right: 28px; - top: 11px; -} - -.question-border::before { - background: #2e3c45; - bottom: 6px; - box-shadow: 3px 3px 3px rgba(24, 24, 24, 0.5); - content: ''; - left: 6px; - position: absolute; - right: 6px; - top: 6px; -} - -.question-icon { - bottom: 0px; - height: 60px; - left: 2px; - position: absolute; - top: 2px; - transition: opacity 0.2s ease; - width: 60px; -} - -.question-message { - color: #2b3940; - font-size: 23px; - width: 100%; - z-index: 0; -} - -.question-message span { - display: block; - letter-spacing: normal; -} - -.question-outer { - height: 86px; - position: relative; -} - -.question::before { - width: 70px; - background: #000000; - bottom: 0px; - content: ''; - left: 0px; - position: absolute; - top: 0px; -} - -.ret-icon { - display: inline-block; - height: 58px; - transition: opacity 0.2s ease; - width: 58px; -} - -.ret-icon-wrapper { - margin-left: -4px; - position: relative; - will-change: transform; + flex-direction: column; } +/* Checkbox element (hidden) */ #saveDefaults { - /* Checkbox element (hidden) */ - left: -100vw; position: absolute; + left: -100vw; } +.checkbox-display { + margin: 10px 70px; +} + +/* Displayed Checkbox (unchecked) */ +.checkbox-display::after { + content: "\E14C"; + color: white; +} + +/* Displayed Checkbox (checked) */ #saveDefaults:checked~.checkbox-display::after { - /* Displayed Checkbox (checked) */ content: "\E14B"; } -a { - text-decoration: none; +.menu-item { + /* Container div for menu link and menu list */ + width: 30%; + margin: 6px; } -body { - background: none; - width: 1280px; -} - -body, div, th, td, p, ul, ol, dl, dt, dd, img, h1, h2, h3, h4, h5, h6, footer, header, nav, p, section, span, figure { - margin: 0px; - overflow-wrap: break-word; +.menu-button, .menu-item > div button { + /* Item styling */ + background-color: #d9e4ea; + border: solid black; + border-width: 3px 20px 3px 3px; padding: 0px; - word-break: normal; - font-family: "nintendo_ext_003", "nintendo_udsgr_std_003"; + box-shadow: 3px 3px 3px #18181880; + display: flex; + height: 60px; + width: 100%; + align-items: center; + align-content: center; + user-select: none; } -img, svg { - opacity: 0; +.menu-item > div button { + z-index: 1; + width: 22%; + margin: 4px 17px; } -img.question-icon:not(.toggle) { - /* Fade icons slightly */ - opacity: 0.75; +.modal p { + font-size: 18px !important; } -span { - letter-spacing: 0.01px; +.menu-icon { + background-color: black; + flex-basis: auto; + width: 60px; + height: 48px; + border: solid black; + border-right-width: 5px; +} + +.menu-icon img { + width: 100%; + height: 100%; +} + +.menu-item p { + font-size: 23px; + color: #2b3940; + width: 100%; + margin: 10px 0px 0px 20px; +} + +.hide { + display: none !important; +} + +.menu-item > div { + display: flex; + flex-wrap: wrap; + position: fixed; + justify-content: flex-start; + align-content: flex-start; + top: 80px; + bottom: 73px; + left: 0px; + right: 0px; + margin-top: 0px; + background-color: rgba(100, 100, 100, 0.9); +} + +:focus { + background: rgb(255,70,2); + background: linear-gradient(45deg, rgba(255,70,2,1) 20%, rgba(255,215,0,1) 46%, rgba(255,215,0,1) 54%, rgba(255,70,2,1) 80%); + background-size: 500% 100%; + will-change: animation; + animation: translate-anim 5s infinite linear; +} + +:focus > p { + color: #fff; + text-shadow: -1px -1px 1px #000, 1px -1px 1px #000, -1px 1px 1px #000, 1px 1px 1px #000; +} + +@keyframes translate-anim { + 0% { + background-position: 0% 0%; + } + 100% { + background-position: 100% 0%; + } } -ul, ol { - list-style-type: none; -} \ No newline at end of file diff --git a/src/static/img/quick_menu.svg b/src/static/img/quick_menu.svg new file mode 100644 index 0000000..14cfdc8 --- /dev/null +++ b/src/static/img/quick_menu.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + diff --git a/src/static/js/training_modpack.js b/src/static/js/training_modpack.js index 8cc210f..8776bff 100644 --- a/src/static/js/training_modpack.js +++ b/src/static/js/training_modpack.js @@ -1,22 +1,23 @@ var isNx = (typeof window.nx !== 'undefined'); -var prevQuestionMsg = null; -var prevFocusedElm = null; +var defaults_prefix = "__"; if (isNx) { - window.nx.footer.setAssign('B', '', goBackHook, {se: ''}); - window.nx.footer.setAssign('X', '', resetSubmenu, {se: ''}); + window.nx.footer.setAssign('B', '', close_or_exit, {se: ''}); + window.nx.footer.setAssign('X', '', resetCurrentSubmenu, {se: ''}); window.nx.footer.setAssign('L', '', resetAllSubmenus, {se: ''}); - window.nx.footer.setAssign('R', '', toggleSaveDefaults, {se: ''}); + window.nx.footer.setAssign('R', '', saveDefaults, {se: ''}); + window.nx.footer.setAssign('ZR', '', cycleNextTab, {se: ''}); + window.nx.footer.setAssign('ZL', '', cyclePrevTab, {se: ''}); } else { - document.getElementById("body").addEventListener('keypress', (event) => { + document.addEventListener('keypress', (event) => { switch (event.key) { case "b": console.log("b"); - goBackHook(); + close_or_exit(); break; case "x": console.log("x"); - resetSubmenu(); + resetCurrentSubmenu(); break; case "l": console.log("l"); @@ -24,20 +25,87 @@ if (isNx) { break; case "r": console.log("r"); - toggleSaveDefaults(); + saveDefaults(); + break; + case "p": + console.log("p"); + cycleNextTab(); + break; + case "o": + console.log("o"); + cyclePrevTab(); break; } }); } -window.onload = setSettings; +window.onload = onLoad; +var settings = new Map(); +var lastFocusedItem = document.querySelector(".menu-item > button"); -function isTextNode(node) { - return node.nodeType == Node.TEXT_NODE +function onLoad() { + // Activate the first tab + openTab(document.querySelector("button.tab-button")); + + // Extract URL params and set appropriate settings + setSettingsFromURL(); + setSubmenusFromSettings(); +} + +function openTab(e) { + var tab_id = e.id.replace("button", "tab"); + var selected_tab = document.getElementById(tab_id); + + + // Hide all content for all tabs + closeAllItems(); + tabcontent = document.getElementsByClassName("tab-content"); + Array.from(tabcontent).forEach(element => { + element.classList.add("hide"); + }); + + + // Get all elements with class="tablinks" and remove the class "active" + tablinks = document.getElementsByClassName("tab-button"); + Array.from(tablinks).forEach(element => { + element.classList.remove("active"); + }); + + // Show the current tab, and add an "active" class to the button that opened the tab + e.classList.add("active"); + selected_tab.classList.remove("hide"); + selected_tab.querySelector("button").focus(); +} + +function openItem(e) { + playSound("SeWebMenuListOpen"); + var modal = e.parentElement.querySelector(".modal"); + modal.classList.toggle("hide"); + modal.querySelector("button").focus(); + lastFocusedItem = e; +} + +function closeAllItems() { + var modals = document.querySelectorAll(".modal"); + Array.from(modals).forEach(element => { + element.classList.add("hide"); + }); + lastFocusedItem.focus(); +} + +function toggleOption(e) { + playSound("SeSelectCheck"); + if (e.parentElement.classList.contains("single-option")) { + selectSingleOption(e); + } else { + var img = e.querySelector("img"); + img.classList.toggle("hide"); + } } function closestClass(elem, class_) { - // Returns the closest anscestor (including self) with the given class + // Returns the closest ancestor (including self) with the given class + // TODO: Consider removing if (!elem) { // Reached the end of the DOM return null @@ -49,46 +117,6 @@ function closestClass(elem, class_) { return closestClass(elem.parentElement, class_); } } - -function getElementByXpath(path) { - return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; -} - -function focusQA(e) { - playSound("SeSelectUncheck"); - prevFocusedElm = e; - e.classList.add("is-focused"); -} - -function defocusQA(e) { - if (prevFocusedElm) { - prevFocusedElm.classList.remove('is-focused'); - - } - if (prevQuestionMsg) { - prevQuestionMsg.remove(); - prevQuestionMsg = null; - } -} - -function toggleAnswer(e) { - playSound("SeToggleBtnOn"); - e.classList.toggle("is-opened"); - - // Toggle visibility of child answers - [].forEach.call(e.childNodes, function (child) { - if (!isTextNode(child) && child.classList.contains("answer-border-outer")) { - child.classList.toggle("is-hidden"); - } - }); - - // Toggle visibility of sibling answers - var sibling = e.nextElementSibling; - if (sibling.classList.contains("answer-border-outer")) { - sibling.classList.toggle("is-hidden"); - } -} - function playSound(label) { // Valid labels: // SeToggleBtnFocus @@ -129,164 +157,120 @@ function playSound(label) { } } -function goBackHook() { +function exit() { + playSound("SeFooterDecideBack"); + setSettingsFromMenu(); + var url = buildURLFromSettings(); + + if (isNx) { + window.location.href = url; + } else { + console.log(url); + } +} + +function close_or_exit() { // If any submenus are open, close them // Otherwise if all submenus are closed, exit the menu and return to the game - if (document.querySelectorAll(".qa.is-opened").length == 0) { - // If all submenus are closed, exit and return through localhost - playSound("SeFooterDecideBack"); - var url = "http://localhost/"; - - var settings = new Map(); - - // Collect settings for toggles - - [].forEach.call(document.querySelectorAll("ul.l-grid"), function (toggle) { - var section = toggle.id; - var val = ""; - - [].forEach.call(toggle.childNodes, function (child) { - if (!isTextNode(child) && child.querySelectorAll(".is-appear").length) { - val += child.getAttribute("val") + ","; - }; - }); - - settings.set(section,val); - }); - - // Collect settings for OnOffs - [].forEach.call(document.querySelectorAll("div.onoff"), function (onoff) { - var section = onoff.id; - var val = ""; - if (onoff.querySelectorAll(".is-appear").length) { - val = "1"; - } else { - val = "0"; - } - settings.set(section,val); - }); - - url += "?"; - settings.forEach((val, section) => { url += section + "=" + val + "&" } ); - - if (document.getElementById("saveDefaults").checked) { - url += "save_defaults=1"; - } else { - url = url.slice(0, -1); - } - - if (isNx) { - window.location.href = url; - } else { - console.log(url); - } - } else { + if (document.querySelector(".modal:not(.hide)")) { // Close any open submenus - [].forEach.call(document.querySelectorAll(".qa.is-opened"), function (submenu) { toggleAnswer(submenu); }); + console.log("Closing Items"); + closeAllItems(); + } else { + // If all submenus are closed, exit and return through localhost + console.log("Exiting"); + exit(); } } -function clickToggle(e) { - playSound("SeCheckboxOn"); - var toggleOptions = e.querySelector(".toggle"); - if (e.querySelector(".is-single-option")) { // Single-option submenu - // Deselect all submenu options - closestClass(e, "l-qa").querySelector(".toggle").classList.remove("is-appear"); - closestClass(e, "l-qa").querySelector(".toggle").classList.add("is-hidden"); - // Then set the current one as the active setting - toggleOptions.classList.add("is-appear"); - toggleOptions.classList.remove("is-hidden"); - } else { // Multi-option submenu - toggleOptions.classList.toggle("is-appear"); - toggleOptions.classList.toggle("is-hidden"); - } -} - -function getParams(url) { +function setSettingsFromURL() { var regex = /[?&]([^=#]+)=([^&#]*)/g, - params = {}, match; - while (match = regex.exec(url)) { - params[match[1]] = match[2]; + while (match = regex.exec(document.URL)) { + settings.set(match[1], match[2]); } - return params; } -function setSettings() { - // Get settings from the URL GET parameters - const settings = getParams(document.URL); - - // Set Toggles - [].forEach.call(document.querySelectorAll("ul.l-grid"), function (toggle) { - var section = toggle.id; - var section_setting = decodeURIComponent(settings[section]); - - [].forEach.call(toggle.querySelectorAll("li"), function (child) { - var e = child.querySelector("img.toggle"); - if (section_setting.split(",").includes(child.getAttribute("val"))) { - e.classList.add("is-appear"); - e.classList.remove("is-hidden"); - } else { - e.classList.remove("is-appear"); - e.classList.add("is-hidden"); - }; - }); +function setSettingsFromMenu() { + var section; + var mask; + [].forEach.call(document.querySelectorAll(".menu-item"), function (menuItem) { + section = menuItem.id; + mask = getMaskFromSubmenu(menuItem); + settings.set(section, mask); }); +} - // Set OnOffs - [].forEach.call(document.querySelectorAll("div.onoff"), function (onOff) { - var section = onOff.id; - var section_setting = decodeURIComponent(settings[section]); - var e = onOff.querySelector("img.toggle"); - if (section_setting == "1") { - e.classList.add("is-appear"); - e.classList.remove("is-hidden"); +function buildURLFromSettings() { + var url = "http://localhost/"; + url += "?"; + settings.forEach((val, key) => { url += key + "=" + String(val) + "&" } ); + return url +} + +function selectSingleOption(e) { + // Deselect all options in the submenu + parent = closestClass(e, "single-option"); + siblings = parent.querySelectorAll(".menu-icon img"); + [].forEach.call(siblings, function (sibling) { + sibling.classList.add("hide"); + }); + e.querySelector(".menu-icon img").classList.remove("hide"); +} + +function setSubmenusFromSettings() { + [].forEach.call(document.querySelectorAll(".menu-item"), function (menuItem) { + var section = menuItem.id; + var section_mask = decodeURIComponent(settings.get(section)); + setSubmenuByMask(menuItem, section_mask) + }); +} + +function setSubmenuByMask(menuItem, mask) { + [].forEach.call(menuItem.querySelectorAll(".modal .menu-icon img"), function (toggle) { + if (isInBitmask(toggle.dataset.val, mask)) { + toggle.classList.remove("hide"); } else { - e.classList.remove("is-appear"); - e.classList.add("is-hidden"); - }; - }); -} - -function resetSubmenu() { - // Resets any open or focused submenus to the default values - playSound("SeToggleBtnOff"); - [].forEach.call(document.querySelectorAll("[default*='is-appear']"), function (item) { - if (isSubmenuFocused(item)) { - item.classList.add("is-appear"); - item.classList.remove("is-hidden"); + toggle.classList.add("hide"); } }); - [].forEach.call(document.querySelectorAll("[default*='is-hidden']"), function (item) { - if (isSubmenuFocused(item)) { - item.classList.remove("is-appear"); - item.classList.add("is-hidden"); - } - }); + // If no setting for a Single Option is set, select the first one + var isSingleOption = menuItem.querySelectorAll(".modal.single-option").length != 0; + var isAllDeselected = menuItem.querySelectorAll(".modal .menu-icon img:not(.hide)").length == 0; + if (isSingleOption & isAllDeselected) { + selectSingleOption(menuItem.querySelector(".modal button")); + } } -function isSubmenuFocused(elem) { - // Return true if the element is in a submenu which is either focused or opened - return ( - closestClass(elem, "l-qa").querySelectorAll(".is-opened, .is-focused").length - || closestClass(elem, "is-focused") - ) +function getMaskFromSubmenu(menuItem) { + var val = 0; + [].forEach.call(menuItem.querySelectorAll(".modal img:not(.hide)"), function (toggle) { + val += parseInt(toggle.dataset.val); + }); + return val +} + +function resetCurrentSubmenu() { + var focus = document.querySelector(".menu-item .modal:not(.hide)"); + if (!focus) { + focus = document.querySelector(":focus"); + } + var menuItem = closestClass(focus, "menu-item"); + + var key = defaults_prefix + menuItem.id; + var section_mask = decodeURIComponent(settings.get(key)); + setSubmenuByMask(menuItem, section_mask); } function resetAllSubmenus() { // Resets all submenus to the default values if (confirm("Are you sure that you want to reset all menu settings to the default?")) { - playSound("SeToggleBtnOff"); - [].forEach.call(document.querySelectorAll("[default*='is-appear']"), function (item) { - item.classList.add("is-appear"); - item.classList.remove("is-hidden"); - }); - - [].forEach.call(document.querySelectorAll("[default*='is-hidden']"), function (item) { - item.classList.remove("is-appear"); - item.classList.add("is-hidden"); + [].forEach.call(document.querySelectorAll(".menu-item"), function (menuItem) { + var key = defaults_prefix + menuItem.id; + var mask = decodeURIComponent(settings.get(key)); + setSubmenuByMask(menuItem, mask) }); } } @@ -296,9 +280,42 @@ function setHelpText(text) { document.getElementById("help-text").innerText = text; } -function toggleSaveDefaults() { - // Change the status of the Save Defaults checkbox - playSound("SeCheckboxOn"); - var saveDefaultsCheckbox = document.getElementById("saveDefaults"); - saveDefaultsCheckbox.checked = !saveDefaultsCheckbox.checked; +function saveDefaults() { + if (confirm("Are you sure that you want to change the default menu settings to the current selections?")) { + var key; + var mask; + [].forEach.call(document.querySelectorAll(".menu-item"), function (menuItem) { + key = defaults_prefix + menuItem.id; + mask = getMaskFromSubmenu(menuItem); + settings.set(key, mask); + }); + } +} + +function isInBitmask(val, mask) { + // Return true if the value is in the bitmask + return (mask & val) != 0 +} + +function cycleNextTab() { + // Cycle to the next tab + var activeTab = document.querySelector(".tab-button.active"); + var nextTab = activeTab.nextElementSibling; + if (!nextTab) { + // On the last tab - set the next tab as the first tab in the list + nextTab = document.querySelector(".tab-button"); + } + openTab(nextTab); +} + +function cyclePrevTab() { + // Cycle to the previous tab + var activeTab = document.querySelector(".tab-button.active"); + var prevTab = activeTab.previousElementSibling; + if (!prevTab) { + // On the first tab - set the next tab as the last tab in the list + tabs = document.querySelectorAll(".tab-button"); + prevTab = tabs[tabs.length - 1]; + } + openTab(prevTab); } \ No newline at end of file diff --git a/src/templates/menu.html b/src/templates/menu.html index 37b52e5..4041aa8 100644 --- a/src/templates/menu.html +++ b/src/templates/menu.html @@ -1,117 +1,59 @@ - - - - Document - - + + + + Modpack Menu + + - - -