1
0
Fork 0
mirror of https://github.com/jugeeya/UltimateTrainingModpack.git synced 2024-11-27 20:34:03 +00:00

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 <jugeeya@live.com>
This commit is contained in:
asimon-1 2022-04-09 18:10:44 -04:00 committed by GitHub
parent 6da6aa41b7
commit a6bed95de3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1137 additions and 1466 deletions

View file

@ -66,10 +66,10 @@ pub unsafe fn write_menu() {
const MENU_CONF_PATH: &str = "sd:/TrainingModpack/training_modpack_menu.conf"; const MENU_CONF_PATH: &str = "sd:/TrainingModpack/training_modpack_menu.conf";
pub fn set_menu_from_url(orig_last_url: &str) { pub fn set_menu_from_url(last_url: &str) {
let last_url = &orig_last_url.replace("&save_defaults=1", "");
unsafe { 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 MENU.quick_menu == OnOff::Off {
if is_emulator() { 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"); std::fs::write(MENU_CONF_PATH, last_url).expect("Failed to write menu conf file");
unsafe { unsafe {
EVENT_QUEUE.push(Event::menu_open(last_url.to_string())); EVENT_QUEUE.push(Event::menu_open(last_url.to_string()));
@ -115,19 +104,22 @@ pub fn spawn_menu() {
if !quick_menu { if !quick_menu {
let fname = "training_menu.html"; let fname = "training_menu.html";
let params = unsafe { MENU.to_url_params() }; unsafe {
let page_response = Webpage::new() let params = MENU.to_url_params(false);
.background(Background::BlurredScreenshot) let default_params = DEFAULTS_MENU.to_url_params(true);
.htdocs_dir("training_modpack") let page_response = Webpage::new()
.boot_display(BootDisplay::BlurredScreenshot) .background(Background::BlurredScreenshot)
.boot_icon(true) .htdocs_dir("training_modpack")
.start_page(&format!("{}{}", fname, params)) .boot_display(BootDisplay::BlurredScreenshot)
.open() .boot_icon(true)
.unwrap(); .start_page(&format!("{}?{}&{}", fname, params, default_params))
.open()
.unwrap();
let orig_last_url = page_response.get_last_url().unwrap(); let last_url = page_response.get_last_url().unwrap();
println!("Received URL from web menu: {}", last_url);
set_menu_from_url(orig_last_url); set_menu_from_url(last_url);
}
} else { } else {
unsafe { unsafe {
QUICK_MENU_ACTIVE = true; QUICK_MENU_ACTIVE = true;

View file

@ -1,138 +1,138 @@
pub mod consts; pub mod consts;
pub mod events; pub mod events;
pub mod menu; pub mod menu;
pub mod raygun_printer; pub mod raygun_printer;
pub mod release; pub mod release;
use crate::common::consts::*; use crate::common::consts::*;
use smash::app::{self, lua_bind::*}; use smash::app::{self, lua_bind::*};
use smash::lib::lua_const::*; use smash::lib::lua_const::*;
pub use crate::common::consts::MENU; pub use crate::common::consts::MENU;
pub static mut DEFAULT_MENU: TrainingModpackMenu = crate::common::consts::DEFAULT_MENU; pub static mut DEFAULTS_MENU: TrainingModpackMenu = crate::common::consts::DEFAULTS_MENU;
pub static mut BASE_MENU: TrainingModpackMenu = unsafe { DEFAULT_MENU }; pub static mut BASE_MENU: TrainingModpackMenu = unsafe { DEFAULTS_MENU };
pub static mut FIGHTER_MANAGER_ADDR: usize = 0; pub static mut FIGHTER_MANAGER_ADDR: usize = 0;
pub static mut STAGE_MANAGER_ADDR: usize = 0; pub static mut STAGE_MANAGER_ADDR: usize = 0;
#[cfg(not(feature = "outside_training_mode"))] #[cfg(not(feature = "outside_training_mode"))]
extern "C" { extern "C" {
#[link_name = "\u{1}_ZN3app9smashball16is_training_modeEv"] #[link_name = "\u{1}_ZN3app9smashball16is_training_modeEv"]
pub fn is_training_mode() -> bool; pub fn is_training_mode() -> bool;
} }
#[cfg(feature = "outside_training_mode")] #[cfg(feature = "outside_training_mode")]
pub fn is_training_mode() -> bool { pub fn is_training_mode() -> bool {
return true; return true;
} }
pub fn get_category(module_accessor: &mut app::BattleObjectModuleAccessor) -> i32 { pub fn get_category(module_accessor: &mut app::BattleObjectModuleAccessor) -> i32 {
(module_accessor.info >> 28) as u8 as i32 (module_accessor.info >> 28) as u8 as i32
} }
pub fn is_emulator() -> bool { pub fn is_emulator() -> bool {
unsafe { skyline::hooks::getRegionAddress(skyline::hooks::Region::Text) as u64 == 0x8004000 } unsafe { skyline::hooks::getRegionAddress(skyline::hooks::Region::Text) as u64 == 0x8004000 }
} }
pub fn get_module_accessor(fighter_id: FighterId) -> *mut app::BattleObjectModuleAccessor { pub fn get_module_accessor(fighter_id: FighterId) -> *mut app::BattleObjectModuleAccessor {
let entry_id_int = fighter_id as i32; let entry_id_int = fighter_id as i32;
let entry_id = app::FighterEntryID(entry_id_int); let entry_id = app::FighterEntryID(entry_id_int);
unsafe { unsafe {
let mgr = *(FIGHTER_MANAGER_ADDR as *mut *mut app::FighterManager); let mgr = *(FIGHTER_MANAGER_ADDR as *mut *mut app::FighterManager);
let fighter_entry = let fighter_entry =
FighterManager::get_fighter_entry(mgr, entry_id) as *mut app::FighterEntry; FighterManager::get_fighter_entry(mgr, entry_id) as *mut app::FighterEntry;
let current_fighter_id = FighterEntry::current_fighter_id(fighter_entry); let current_fighter_id = FighterEntry::current_fighter_id(fighter_entry);
app::sv_battle_object::module_accessor(current_fighter_id as u32) app::sv_battle_object::module_accessor(current_fighter_id as u32)
} }
} }
pub fn is_fighter(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { pub fn is_fighter(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool {
get_category(module_accessor) == BATTLE_OBJECT_CATEGORY_FIGHTER get_category(module_accessor) == BATTLE_OBJECT_CATEGORY_FIGHTER
} }
pub fn is_operation_cpu(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { pub fn is_operation_cpu(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool {
unsafe { unsafe {
if !is_fighter(module_accessor) { if !is_fighter(module_accessor) {
return false; return false;
} }
let entry_id_int = let entry_id_int =
WorkModule::get_int(module_accessor, *FIGHTER_INSTANCE_WORK_ID_INT_ENTRY_ID) as i32; WorkModule::get_int(module_accessor, *FIGHTER_INSTANCE_WORK_ID_INT_ENTRY_ID) as i32;
if entry_id_int == 0 { if entry_id_int == 0 {
return false; return false;
} }
let entry_id = app::FighterEntryID(entry_id_int); let entry_id = app::FighterEntryID(entry_id_int);
let mgr = *(FIGHTER_MANAGER_ADDR as *mut *mut app::FighterManager); let mgr = *(FIGHTER_MANAGER_ADDR as *mut *mut app::FighterManager);
let fighter_information = let fighter_information =
FighterManager::get_fighter_information(mgr, entry_id) as *mut app::FighterInformation; FighterManager::get_fighter_information(mgr, entry_id) as *mut app::FighterInformation;
FighterInformation::is_operation_cpu(fighter_information) FighterInformation::is_operation_cpu(fighter_information)
} }
} }
pub fn is_grounded(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { pub fn is_grounded(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool {
let situation_kind = unsafe { StatusModule::situation_kind(module_accessor) as i32 }; let situation_kind = unsafe { StatusModule::situation_kind(module_accessor) as i32 };
situation_kind == SITUATION_KIND_GROUND situation_kind == SITUATION_KIND_GROUND
} }
pub fn is_airborne(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { pub fn is_airborne(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool {
let situation_kind = unsafe { StatusModule::situation_kind(module_accessor) as i32 }; let situation_kind = unsafe { StatusModule::situation_kind(module_accessor) as i32 };
situation_kind == SITUATION_KIND_AIR situation_kind == SITUATION_KIND_AIR
} }
pub fn is_idle(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { pub fn is_idle(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool {
let status_kind = unsafe { StatusModule::status_kind(module_accessor) }; let status_kind = unsafe { StatusModule::status_kind(module_accessor) };
status_kind == FIGHTER_STATUS_KIND_WAIT status_kind == FIGHTER_STATUS_KIND_WAIT
} }
pub fn is_in_hitstun(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { pub fn is_in_hitstun(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool {
let status_kind = unsafe { StatusModule::status_kind(module_accessor) }; let status_kind = unsafe { StatusModule::status_kind(module_accessor) };
(*FIGHTER_STATUS_KIND_DAMAGE..*FIGHTER_STATUS_KIND_DAMAGE_FALL).contains(&status_kind) (*FIGHTER_STATUS_KIND_DAMAGE..*FIGHTER_STATUS_KIND_DAMAGE_FALL).contains(&status_kind)
} }
pub fn is_in_footstool(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { pub fn is_in_footstool(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool {
let status_kind = unsafe { StatusModule::status_kind(module_accessor) }; let status_kind = unsafe { StatusModule::status_kind(module_accessor) };
(*FIGHTER_STATUS_KIND_TREAD_DAMAGE..=*FIGHTER_STATUS_KIND_TREAD_FALL).contains(&status_kind) (*FIGHTER_STATUS_KIND_TREAD_DAMAGE..=*FIGHTER_STATUS_KIND_TREAD_FALL).contains(&status_kind)
} }
pub fn is_shielding(module_accessor: *mut app::BattleObjectModuleAccessor) -> bool { pub fn is_shielding(module_accessor: *mut app::BattleObjectModuleAccessor) -> bool {
let status_kind = unsafe { StatusModule::status_kind(module_accessor) as i32 }; let status_kind = unsafe { StatusModule::status_kind(module_accessor) as i32 };
(*FIGHTER_STATUS_KIND_GUARD_ON..=*FIGHTER_STATUS_KIND_GUARD_DAMAGE).contains(&status_kind) (*FIGHTER_STATUS_KIND_GUARD_ON..=*FIGHTER_STATUS_KIND_GUARD_DAMAGE).contains(&status_kind)
} }
pub fn is_in_shieldstun(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { pub fn is_in_shieldstun(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool {
let status_kind = unsafe { StatusModule::status_kind(module_accessor) }; let status_kind = unsafe { StatusModule::status_kind(module_accessor) };
let prev_status = unsafe { StatusModule::prev_status_kind(module_accessor, 0) }; 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 // 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 status_kind == FIGHTER_STATUS_KIND_GUARD_DAMAGE
|| (prev_status == FIGHTER_STATUS_KIND_GUARD_DAMAGE || (prev_status == FIGHTER_STATUS_KIND_GUARD_DAMAGE
&& status_kind == FIGHTER_STATUS_KIND_GUARD_OFF) && status_kind == FIGHTER_STATUS_KIND_GUARD_OFF)
} }
pub unsafe fn is_dead(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool { pub unsafe fn is_dead(module_accessor: &mut app::BattleObjectModuleAccessor) -> bool {
let fighter_kind = app::utility::get_kind(module_accessor); let fighter_kind = app::utility::get_kind(module_accessor);
let fighter_is_ptrainer = [ let fighter_is_ptrainer = [
*FIGHTER_KIND_PZENIGAME, *FIGHTER_KIND_PZENIGAME,
*FIGHTER_KIND_PFUSHIGISOU, *FIGHTER_KIND_PFUSHIGISOU,
*FIGHTER_KIND_PLIZARDON, *FIGHTER_KIND_PLIZARDON,
] ]
.contains(&fighter_kind); .contains(&fighter_kind);
let status_kind = StatusModule::status_kind(module_accessor) as i32; let status_kind = StatusModule::status_kind(module_accessor) as i32;
let prev_status_kind = StatusModule::prev_status_kind(module_accessor, 0); 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 // Pokemon trainer enters FIGHTER_STATUS_KIND_WAIT for one frame during their respawn animation
// And the previous status is FIGHTER_STATUS_NONE // And the previous status is FIGHTER_STATUS_NONE
if fighter_is_ptrainer { if fighter_is_ptrainer {
[*FIGHTER_STATUS_KIND_DEAD, *FIGHTER_STATUS_KIND_STANDBY].contains(&status_kind) [*FIGHTER_STATUS_KIND_DEAD, *FIGHTER_STATUS_KIND_STANDBY].contains(&status_kind)
|| (status_kind == FIGHTER_STATUS_KIND_WAIT || (status_kind == FIGHTER_STATUS_KIND_WAIT
&& prev_status_kind == FIGHTER_STATUS_KIND_NONE) && prev_status_kind == FIGHTER_STATUS_KIND_NONE)
} else { } else {
[*FIGHTER_STATUS_KIND_DEAD, *FIGHTER_STATUS_KIND_STANDBY].contains(&status_kind) [*FIGHTER_STATUS_KIND_DEAD, *FIGHTER_STATUS_KIND_STANDBY].contains(&status_kind)
} }
} }

View file

@ -71,7 +71,7 @@ pub fn render_text_to_screen(s: &str) {
pub fn main() { pub fn main() {
macro_rules! log { macro_rules! log {
($($arg:tt)*) => { ($($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") { if menu_conf.starts_with(b"http://localhost") {
log!("Previous menu found, loading from training_modpack_menu.conf"); log!("Previous menu found, loading from training_modpack_menu.conf");
unsafe { unsafe {
MENU = get_menu_from_url(MENU, std::str::from_utf8(&menu_conf).unwrap()); MENU = get_menu_from_url(MENU, std::str::from_utf8(&menu_conf).unwrap(), false);
if is_emulator() { DEFAULTS_MENU = get_menu_from_url(DEFAULTS_MENU, std::str::from_utf8(&menu_conf).unwrap(), true);
MENU.quick_menu = OnOff::On;
}
} }
} else { } else {
log!("Previous menu found but is invalid."); log!("Previous menu found but is invalid.");
@ -116,32 +114,10 @@ pub fn main() {
} else { } else {
log!("No previous menu file found."); 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() { if is_emulator() {
unsafe { unsafe {
DEFAULTS_MENU.quick_menu = OnOff::On;
MENU.quick_menu = OnOff::On; MENU.quick_menu = OnOff::On;
} }
} }
@ -195,6 +171,7 @@ pub fn main() {
// Leave menu. // Leave menu.
menu::QUICK_MENU_ACTIVE = false; menu::QUICK_MENU_ACTIVE = false;
crate::menu::set_menu_from_url(url.as_str()); crate::menu::set_menu_from_url(url.as_str());
println!("URL: {}", url.as_str());
} }
}); });
button_presses.zl.read_press().then(|| { button_presses.zl.read_press().then(|| {

View file

@ -20,337 +20,261 @@
} }
} }
.answer-border-outer { .tab-list-container {
margin-top: 5px; overflow: hidden;
} background-color: #555;
.button-icon {
height: 53px;
margin-left: 4px;
width: 53px;
}
.button-icon-wrapper {
align-items: center;
background: #000000;
display: flex; display: flex;
height: 58px; justify-content: flex-start;
margin-left: -1px; width: 100%;
width: 80px; align-items: center;
} }
.button-msg-wrapper { .tab-list-container p {
width: 259px; color: #fff;
width: 130px;
height: fit-content;
margin: 0px 10px 0px 10px;
padding: 0px;
} }
.checkbox-display {
margin: 10px 70px;
}
.checkbox-display::after { .tab-list {
/* Displayed Checkbox (unchecked) */ overflow: hidden;
color: white;
content: "\E14C";
}
.defaults-checkbox-container {
/* Save Defaults Container */
display: flex; display: flex;
flex-direction: column; justify-content: flex-start;
justify-content: center; width: 100%;
margin-top: 10px;
position: fixed;
right: 50px;
} }
.flex-button { .tab-list button {
align-items: center; background-color: inherit;
display: inline-flex; float: left;
flex: 1; border: none;
justify-content: center; outline: none;
text-align: center; cursor: pointer;
vertical-align: middle; padding: 14px 16px;
color: #fff;
margin: 5px 0px 0px 8px;
border-radius: 8px 8px 0px 0px;
font-size: large;
} }
.footer { .tab-list button:hover {
align-items: center; background: #797979;
background: #000000; }
.tab-list button.active {
color: #000;
background: #ccc;
}
.tab-content {
width: 100%;
display: flex; display: flex;
height: 73px; flex-wrap: wrap;
justify-content: center; justify-content: space-evenly;
position: fixed;
z-index: 10;
} }
.header { body {
align-items: center; background: none;
background: #000000; font-family: "nintendo_ext_003", "nintendo_udsgr_std_003";
border-bottom: 7px solid #000000; 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; display: flex;
height: 65px; justify-content: space-between;
} position: relative;
width: 100%;
.header-decoration { height: 80px;
align-items: center; z-index: 100;
background: #a80114; background: #000;
display: inline-flex; box-shadow: 0px 1px 1px #000;
height: 65px;
padding-left: 21px;
width: 101px;
}
.header-desc {
color: white;
} }
.header-title { .header-title {
color: #f46264; color: #f46264;
font-size: 26px; font-size: 26px;
line-height: 2.5; line-height: 2.5;
margin: 0px;
padding: 0px;
word-break: normal;
} }
.is-appear { .header-label {
opacity: 1; display: flex;
flex-direction: column;
justify-content: center;
align-items: end;
margin-right: 15px;
} }
.is-focused .question-border::before { .header-label > p {
background: #000000; color: #fff;
box-shadow: none; margin: 0;
padding: 0;
} }
.is-focused .question-message { .return-icon-container {
color: #FFFFFF; width: 101px;
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; height: 65px;
padding-left: 21px;
background: #a80114;
border-radius: 0px 0px 15px 0px;
} }
.is-focused .question.scuffle-thema { .return-icon {
background: #df1624; width: 58px;
height: 58px;
padding-left: 7px;
filter: drop-shadow(3px 5px 2px rgba(0, 0, 0, 0.8));
} }
.is-focused .scuffle-thema { /* Center Icons */
background: #df1624; .question::before {
width: 70px;
} }
.is-hidden { /* Footer */
display: none; .footer {
} position: fixed;
.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 {
bottom: 0px; bottom: 0px;
left: 0px; left: 0px;
width: 100%; background: #000;
}
.l-grid {
display: flex; display: flex;
flex-wrap: wrap; justify-content: center;
} align-items: center;
height: 73px;
.l-header {
box-shadow: 0px 1px 1px #000000;
display: flex;
left: 0px;
position: absolute;
top: 0px;
width: 100%; width: 100%;
color: #fff;
z-index: 100; z-index: 100;
} }
.l-header-title { /* Save Defaults Container */
align-items: center; .defaults-checkbox-container {
position: fixed;
right: 50px;
margin-top: 10px;
display: flex; display: flex;
height: 100%;
justify-content: center; justify-content: center;
left: 0px; flex-direction: column;
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;
} }
/* Checkbox element (hidden) */
#saveDefaults { #saveDefaults {
/* Checkbox element (hidden) */
left: -100vw;
position: absolute; 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 { #saveDefaults:checked~.checkbox-display::after {
/* Displayed Checkbox (checked) */
content: "\E14B"; content: "\E14B";
} }
a { .menu-item {
text-decoration: none; /* Container div for menu link and menu list */
width: 30%;
margin: 6px;
} }
body { .menu-button, .menu-item > div button {
background: none; /* Item styling */
width: 1280px; background-color: #d9e4ea;
} border: solid black;
border-width: 3px 20px 3px 3px;
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;
padding: 0px; padding: 0px;
word-break: normal; box-shadow: 3px 3px 3px #18181880;
font-family: "nintendo_ext_003", "nintendo_udsgr_std_003"; display: flex;
height: 60px;
width: 100%;
align-items: center;
align-content: center;
user-select: none;
} }
img, svg { .menu-item > div button {
opacity: 0; z-index: 1;
width: 22%;
margin: 4px 17px;
} }
img.question-icon:not(.toggle) { .modal p {
/* Fade icons slightly */ font-size: 18px !important;
opacity: 0.75;
} }
span { .menu-icon {
letter-spacing: 0.01px; 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;
}

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="80"
height="80"
viewBox="0 0 21.166666 21.166667"
version="1.1"
id="svg5"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<g
id="layer1">
<path
style="fill:none;stroke:#ffffff;stroke-width:2.11667;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 1.8552457,17.110952 8.469829,10.496369 1.8552457,3.8817854"
id="path1904" />
<path
style="fill:none;stroke:#ffffff;stroke-width:2.11667;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 7.22203,17.110952 13.836612,10.49637 7.22203,3.8817855"
id="path1904-3" />
<path
style="fill:none;stroke:#ffffff;stroke-width:2.11667;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 12.588812,17.110952 19.203396,10.496369 12.588812,3.8817854"
id="path1904-2" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,22 +1,23 @@
var isNx = (typeof window.nx !== 'undefined'); var isNx = (typeof window.nx !== 'undefined');
var prevQuestionMsg = null; var defaults_prefix = "__";
var prevFocusedElm = null;
if (isNx) { if (isNx) {
window.nx.footer.setAssign('B', '', goBackHook, {se: ''}); window.nx.footer.setAssign('B', '', close_or_exit, {se: ''});
window.nx.footer.setAssign('X', '', resetSubmenu, {se: ''}); window.nx.footer.setAssign('X', '', resetCurrentSubmenu, {se: ''});
window.nx.footer.setAssign('L', '', resetAllSubmenus, {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 { } else {
document.getElementById("body").addEventListener('keypress', (event) => { document.addEventListener('keypress', (event) => {
switch (event.key) { switch (event.key) {
case "b": case "b":
console.log("b"); console.log("b");
goBackHook(); close_or_exit();
break; break;
case "x": case "x":
console.log("x"); console.log("x");
resetSubmenu(); resetCurrentSubmenu();
break; break;
case "l": case "l":
console.log("l"); console.log("l");
@ -24,20 +25,87 @@ if (isNx) {
break; break;
case "r": case "r":
console.log("r"); console.log("r");
toggleSaveDefaults(); saveDefaults();
break;
case "p":
console.log("p");
cycleNextTab();
break;
case "o":
console.log("o");
cyclePrevTab();
break; break;
} }
}); });
} }
window.onload = setSettings; window.onload = onLoad;
var settings = new Map();
var lastFocusedItem = document.querySelector(".menu-item > button");
function isTextNode(node) { function onLoad() {
return node.nodeType == Node.TEXT_NODE // 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_) { 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) { if (!elem) {
// Reached the end of the DOM // Reached the end of the DOM
return null return null
@ -49,46 +117,6 @@ function closestClass(elem, class_) {
return closestClass(elem.parentElement, 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) { function playSound(label) {
// Valid labels: // Valid labels:
// SeToggleBtnFocus // 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 // If any submenus are open, close them
// Otherwise if all submenus are closed, exit the menu and return to the game // Otherwise if all submenus are closed, exit the menu and return to the game
if (document.querySelectorAll(".qa.is-opened").length == 0) { if (document.querySelector(".modal:not(.hide)")) {
// 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 {
// Close any open submenus // 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) { function setSettingsFromURL() {
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) {
var regex = /[?&]([^=#]+)=([^&#]*)/g, var regex = /[?&]([^=#]+)=([^&#]*)/g,
params = {},
match; match;
while (match = regex.exec(url)) { while (match = regex.exec(document.URL)) {
params[match[1]] = match[2]; settings.set(match[1], match[2]);
} }
return params;
} }
function setSettings() { function setSettingsFromMenu() {
// Get settings from the URL GET parameters var section;
const settings = getParams(document.URL); var mask;
[].forEach.call(document.querySelectorAll(".menu-item"), function (menuItem) {
// Set Toggles section = menuItem.id;
[].forEach.call(document.querySelectorAll("ul.l-grid"), function (toggle) { mask = getMaskFromSubmenu(menuItem);
var section = toggle.id; settings.set(section, mask);
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");
};
});
}); });
}
// Set OnOffs function buildURLFromSettings() {
[].forEach.call(document.querySelectorAll("div.onoff"), function (onOff) { var url = "http://localhost/";
var section = onOff.id; url += "?";
var section_setting = decodeURIComponent(settings[section]); settings.forEach((val, key) => { url += key + "=" + String(val) + "&" } );
var e = onOff.querySelector("img.toggle"); return url
if (section_setting == "1") { }
e.classList.add("is-appear");
e.classList.remove("is-hidden"); 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 { } else {
e.classList.remove("is-appear"); toggle.classList.add("hide");
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");
} }
}); });
[].forEach.call(document.querySelectorAll("[default*='is-hidden']"), function (item) { // If no setting for a Single Option is set, select the first one
if (isSubmenuFocused(item)) { var isSingleOption = menuItem.querySelectorAll(".modal.single-option").length != 0;
item.classList.remove("is-appear"); var isAllDeselected = menuItem.querySelectorAll(".modal .menu-icon img:not(.hide)").length == 0;
item.classList.add("is-hidden"); if (isSingleOption & isAllDeselected) {
} selectSingleOption(menuItem.querySelector(".modal button"));
}); }
} }
function isSubmenuFocused(elem) { function getMaskFromSubmenu(menuItem) {
// Return true if the element is in a submenu which is either focused or opened var val = 0;
return ( [].forEach.call(menuItem.querySelectorAll(".modal img:not(.hide)"), function (toggle) {
closestClass(elem, "l-qa").querySelectorAll(".is-opened, .is-focused").length val += parseInt(toggle.dataset.val);
|| closestClass(elem, "is-focused") });
) 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() { function resetAllSubmenus() {
// Resets all submenus to the default values // Resets all submenus to the default values
if (confirm("Are you sure that you want to reset all menu settings to the default?")) { if (confirm("Are you sure that you want to reset all menu settings to the default?")) {
playSound("SeToggleBtnOff"); [].forEach.call(document.querySelectorAll(".menu-item"), function (menuItem) {
[].forEach.call(document.querySelectorAll("[default*='is-appear']"), function (item) { var key = defaults_prefix + menuItem.id;
item.classList.add("is-appear"); var mask = decodeURIComponent(settings.get(key));
item.classList.remove("is-hidden"); setSubmenuByMask(menuItem, mask)
});
[].forEach.call(document.querySelectorAll("[default*='is-hidden']"), function (item) {
item.classList.remove("is-appear");
item.classList.add("is-hidden");
}); });
} }
} }
@ -296,9 +280,42 @@ function setHelpText(text) {
document.getElementById("help-text").innerText = text; document.getElementById("help-text").innerText = text;
} }
function toggleSaveDefaults() { function saveDefaults() {
// Change the status of the Save Defaults checkbox if (confirm("Are you sure that you want to change the default menu settings to the current selections?")) {
playSound("SeCheckboxOn"); var key;
var saveDefaultsCheckbox = document.getElementById("saveDefaults"); var mask;
saveDefaultsCheckbox.checked = !saveDefaultsCheckbox.checked; [].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);
} }

View file

@ -1,117 +1,59 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Document</title> <title>Modpack Menu</title>
<link rel="stylesheet" href="./css/training_modpack.css" /> <link rel="stylesheet" href="./css/training_modpack.css" />
</head> </head>
<body id="body"> <body>
<script defer src="./js/training_modpack.js"></script> <script defer src="./js/training_modpack.js"></script>
<div class="l-header"> <div class="l-header">
<div class="l-header-title"> <a id="ret-button" tabindex="-1" class="return-icon-container" href="javascript:goBackHook();">
<div class="header-title"><span>Ultimate Training Modpack Menu</span></div> <img class="return-icon" src="./img/m_retnormal.svg">
</div> </a>
<div class="header" style="flex-grow: 1;"> <p class="header-title">Ultimate Training Modpack Menu</p>
<a id="ret-button" tabindex="-1" class="header-decoration" href="javascript:goBackHook();"> <div class="header-label">
<div class="ret-icon-wrapper"> <p class="header-desc">Reset Current Menu: &#xE0A2;</p>
<img class="ret-icon is-appear" src="./img/m_retnormal.svg"> <p class="header-desc">Reset All Menus: &#xE0A4;</p>
</div> <p class="header-desc">Save Defaults: &#xE0A5;</p>
</a>
</div>
<div class="header" style="flex-direction: column; justify-content: center; align-items: end; padding-right: 10px;">
<p class="header-desc">Reset Current Menu: &#xE0A2;</p>
<p class="header-desc">Reset All Menus: &#xE0A4;</p>
</div>
</div> </div>
<br> </div>
<br> <div class="tab-list-container">
<br> <p>Prev Tab: &#xE0A6;</p>
<br> <div class="tab-list">
{{#tabs}}
<div class="l-grid"> <button class="tab-button active" id="{{tab_id}}_button" onclick="openTab(this)" tabindex="-1">{{tab_title}}</button>
{{/tabs}}
<!-- </div>
Script the part below via templating. Overall structure is perhaps <p>Next Tab: &#xE0A7;</p>
[ </div>
l-qa qa [id=qa-{{menuName}} tabindex="{{index}}"] { <div class="tab-content-container">
// make question for {{menuName}} {{#tabs}}
// make answer with l-grid : l-item list for options <div id="{{tab_id}}_tab" class="tab-content">
}, {{#tab_submenus}}
... <div class="menu-item" id="{{submenu_id}}">
] <button class="menu-button" onfocus="setHelpText('{{help_text}}')" onclick="openItem(this)">
<div class="menu-icon"><img src="./img/{{submenu_id}}.svg" /></div>
<p>{{submenu_title}}</p>
Remember to set make max keyword size greater than 23! </button>
--> <div class="hide modal{{#is_single_option}} single-option{{/is_single_option}}">
{{#sub_menus}} {{#toggles}}
<div class="l-qa"> <button class="menu-button" onclick="toggleOption(this)">
{{^onoffselector}} <div class="menu-icon"><img class="hide" src="./img/check.svg" data-val="{{toggle_value}}"/></div>
<a id="qa-{{id}}" class="qa" tabindex="{{index}}" href="javascript:void(0);" onfocus="focusQA(this);setHelpText({{help_text}})" onblur="defocusQA(this)" onclick="toggleAnswer(this)"> <p>{{toggle_title}}</p>
<div class="question-outer"> </button>
<div class="question-border"> {{/toggles}}
<div id="question-{{id}}" class="question scuffle-thema">
<img class="question-icon" src="./img/{{id}}.svg" />
<p class="question-message">
<span>{{title}}</span>
</p>
</div>
</div>
</div>
</a>
<div id="answer-border-{{id}}" class="answer-border-outer is-hidden">
<div class="l-main">
<ul class="l-grid" id="{{id}}">
{{#toggles}}
<li class="l-item" val="{{value}}">
<div class="keyword-button-outer">
<a tabindex="{{index}}" class="flex-button keyword-button scuffle-thema" href="javascript:void(0)" onclick="clickToggle(this);">
<div class="button-icon-wrapper">
<img class="button-icon toggle {{checked}} {{#is_single_option}}is-single-option{{/is_single_option}}" src="./img/check.svg" default="{{default}}">
</div>
<div class="button-msg-wrapper">
<div class="keyword-message">
{{title}}
</div>
</div>
</a>
</div>
</li>
{{/toggles}}
</ul>
</div>
</div>
{{/onoffselector}}
{{#onoffselector}}
<a id="qa-{{id}}" class="qa" tabindex="{{index}}" href="javascript:void(0);" onfocus="focusQA(this);setHelpText({{help_text}})" onblur="defocusQA(this)" onclick="clickToggle(this)">
<div class="question-outer">
<div class="question-border">
<div id="question-{{id}}" class="question scuffle-thema">
<div id="{{id}}" class="onoff">
<img class="question-icon" style="z-index: 1;" src="./img/{{id}}.svg" />
<div><img class="question-icon toggle {{checked}}" style="z-index: 2;" src="./img/check.svg" default="{{default}}"/></div>
<p class="question-message">
<span>{{title}}</span>
</p>
</div>
</div>
</div>
</div>
</a>
{{/onoffselector}}
</div> </div>
{{/sub_menus}}
</div>
<footer id="footer" class="footer l-footer">
<p id="help-text" class="header-desc"></p>
<div class="defaults-checkbox-container">
<label class="header-desc" for="saveDefaults">Save defaults: &#xE0A5;</label>
<input type="checkbox" id="saveDefaults">
<div class="checkbox-display"></div>
</div> </div>
</footer> {{/tab_submenus}}
</body> </div>
{{/tabs}}
</div>
<footer id="footer" class="footer">
<p id="help-text" class="header-desc"></p>
</footer>
</body>
</html> </html>

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
use training_mod_consts::{OnOffSelector, Slider, SubMenu, SubMenuType, Toggle}; use training_mod_consts::{Slider, SubMenu, SubMenuType, Toggle, UiMenu};
use tui::{ use tui::{
backend::{Backend}, backend::{Backend},
layout::{Constraint, Corner, Direction, Layout}, layout::{Constraint, Corner, Direction, Layout},
@ -22,81 +22,26 @@ pub struct App<'a> {
pub tabs: StatefulList<&'a str>, pub tabs: StatefulList<&'a str>,
pub menu_items: HashMap<&'a str, MultiStatefulList<SubMenu<'a>>>, pub menu_items: HashMap<&'a str, MultiStatefulList<SubMenu<'a>>>,
pub selected_sub_menu_toggles: MultiStatefulList<Toggle<'a>>, pub selected_sub_menu_toggles: MultiStatefulList<Toggle<'a>>,
pub selected_sub_menu_onoff_selectors: MultiStatefulList<OnOffSelector<'a>>,
pub selected_sub_menu_sliders: MultiStatefulList<Slider>, pub selected_sub_menu_sliders: MultiStatefulList<Slider>,
pub outer_list: bool pub outer_list: bool
} }
impl<'a> App<'a> { impl<'a> App<'a> {
pub fn new(menu: training_mod_consts::Menu<'a>) -> App<'a> { pub fn new(menu: UiMenu<'a>) -> App<'a> {
let tab_specifiers = vec![
("Mash Settings", vec![
"Mash Toggles",
"Followup Toggles",
"Attack Angle",
"Ledge Options",
"Ledge Delay",
"Tech Options",
"Miss Tech Options",
"Defensive Options",
"Aerial Delay",
"OoS Offset",
"Reaction Time",
]),
("Defensive Settings", vec![
"Fast Fall",
"Fast Fall Delay",
"Falling Aerials",
"Full Hop",
"Shield Tilt",
"DI Direction",
"SDI Direction",
"Airdodge Direction",
"SDI Strength",
"Shield Toggles",
"Mirroring",
"Throw Options",
"Throw Delay",
"Pummel Delay",
"Buff Options",
]),
("Other Settings", vec![
"Input Delay",
"Save States",
"Save Damage",
"Hitbox Visualization",
"Stage Hazards",
"Frame Advantage",
"Mash In Neutral",
"Quick Menu"
])
];
let mut tabs: std::collections::HashMap<&str, Vec<SubMenu>> = std::collections::HashMap::new();
tabs.insert("Mash Settings", vec![]);
tabs.insert("Defensive Settings", vec![]);
tabs.insert("Other Settings", vec![]);
for sub_menu in menu.sub_menus.iter() {
for tab_spec in tab_specifiers.iter() {
if tab_spec.1.contains(&sub_menu.title) {
tabs.get_mut(tab_spec.0).unwrap().push(sub_menu.clone());
}
}
};
let num_lists = 3; let num_lists = 3;
let mut menu_items_stateful = HashMap::new(); let mut menu_items_stateful = HashMap::new();
tabs.keys().for_each(|k| { menu.tabs.iter().for_each(|tab| {
menu_items_stateful.insert( menu_items_stateful.insert(
k.clone(), tab.tab_title,
MultiStatefulList::with_items(tabs.get(k).unwrap().clone(), num_lists) MultiStatefulList::with_items(tab.tab_submenus.clone(), num_lists)
); );
}); });
let mut app = App { let mut app = App {
tabs: StatefulList::with_items(tab_specifiers.iter().map(|(tab_title, _)| *tab_title).collect()), tabs: StatefulList::with_items(menu.tabs.iter().map(|tab| tab.tab_title).collect()),
menu_items: menu_items_stateful, menu_items: menu_items_stateful,
selected_sub_menu_toggles: MultiStatefulList::with_items(vec![], 0), selected_sub_menu_toggles: MultiStatefulList::with_items(vec![], 0),
selected_sub_menu_onoff_selectors: MultiStatefulList::with_items(vec![], 0),
selected_sub_menu_sliders: MultiStatefulList::with_items(vec![], 0), selected_sub_menu_sliders: MultiStatefulList::with_items(vec![], 0),
outer_list: true outer_list: true
}; };
@ -109,8 +54,7 @@ impl<'a> App<'a> {
let selected_sub_menu = &self.menu_items.get(self.tab_selected()).unwrap().lists[list_section].items.get(list_idx).unwrap(); let selected_sub_menu = &self.menu_items.get(self.tab_selected()).unwrap().lists[list_section].items.get(list_idx).unwrap();
let toggles = selected_sub_menu.toggles.clone(); let toggles = selected_sub_menu.toggles.clone();
let sliders = selected_sub_menu.sliders.clone(); // let sliders = selected_sub_menu.sliders.clone();
let onoffs = selected_sub_menu.onoffselector.clone();
match SubMenuType::from_str(self.sub_menu_selected()._type) { match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::TOGGLE => { SubMenuType::TOGGLE => {
self.selected_sub_menu_toggles = MultiStatefulList::with_items( self.selected_sub_menu_toggles = MultiStatefulList::with_items(
@ -118,14 +62,9 @@ impl<'a> App<'a> {
if selected_sub_menu.toggles.len() >= 3 { 3 } else { selected_sub_menu.toggles.len()} ) if selected_sub_menu.toggles.len() >= 3 { 3 } else { selected_sub_menu.toggles.len()} )
}, },
SubMenuType::SLIDER => { SubMenuType::SLIDER => {
self.selected_sub_menu_sliders = MultiStatefulList::with_items( // self.selected_sub_menu_sliders = MultiStatefulList::with_items(
sliders, // sliders,
if selected_sub_menu.sliders.len() >= 3 { 3 } else { selected_sub_menu.sliders.len()} ) // if selected_sub_menu.sliders.len() >= 3 { 3 } else { selected_sub_menu.sliders.len()} )
},
SubMenuType::ONOFF => {
self.selected_sub_menu_onoff_selectors = MultiStatefulList::with_items(
onoffs,
if selected_sub_menu.onoffselector.len() >= 3 { 3 } else { selected_sub_menu.onoffselector.len()} )
}, },
}; };
} }
@ -136,14 +75,13 @@ impl<'a> App<'a> {
fn sub_menu_selected(&self) -> &SubMenu { fn sub_menu_selected(&self) -> &SubMenu {
let (list_section, list_idx) = self.menu_items.get(self.tab_selected()).unwrap().idx_to_list_idx(self.menu_items.get(self.tab_selected()).unwrap().state); let (list_section, list_idx) = self.menu_items.get(self.tab_selected()).unwrap().idx_to_list_idx(self.menu_items.get(self.tab_selected()).unwrap().state);
&self.menu_items.get(self.tab_selected()).unwrap().lists[list_section].items.get(list_idx).unwrap() self.menu_items.get(self.tab_selected()).unwrap().lists[list_section].items.get(list_idx).unwrap()
} }
pub fn sub_menu_next(&mut self) { pub fn sub_menu_next(&mut self) {
match SubMenuType::from_str(self.sub_menu_selected()._type) { match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next(), SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next(),
SubMenuType::SLIDER => self.selected_sub_menu_sliders.next(), SubMenuType::SLIDER => self.selected_sub_menu_sliders.next(),
SubMenuType::ONOFF => self.selected_sub_menu_onoff_selectors.next(),
} }
} }
@ -151,7 +89,6 @@ impl<'a> App<'a> {
match SubMenuType::from_str(self.sub_menu_selected()._type) { match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next_list(), SubMenuType::TOGGLE => self.selected_sub_menu_toggles.next_list(),
SubMenuType::SLIDER => self.selected_sub_menu_sliders.next_list(), SubMenuType::SLIDER => self.selected_sub_menu_sliders.next_list(),
SubMenuType::ONOFF => self.selected_sub_menu_onoff_selectors.next_list(),
} }
} }
@ -159,7 +96,6 @@ impl<'a> App<'a> {
match SubMenuType::from_str(self.sub_menu_selected()._type) { match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous(), SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous(),
SubMenuType::SLIDER => self.selected_sub_menu_sliders.previous(), SubMenuType::SLIDER => self.selected_sub_menu_sliders.previous(),
SubMenuType::ONOFF => self.selected_sub_menu_onoff_selectors.previous(),
} }
} }
@ -167,30 +103,22 @@ impl<'a> App<'a> {
match SubMenuType::from_str(self.sub_menu_selected()._type) { match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous_list(), SubMenuType::TOGGLE => self.selected_sub_menu_toggles.previous_list(),
SubMenuType::SLIDER => self.selected_sub_menu_sliders.previous_list(), SubMenuType::SLIDER => self.selected_sub_menu_sliders.previous_list(),
SubMenuType::ONOFF => self.selected_sub_menu_onoff_selectors.previous_list(),
} }
} }
pub fn sub_menu_strs_and_states(&mut self) -> (&str, &str, Vec<(Vec<(&str, &str)>, ListState)>) { pub fn sub_menu_strs_and_states(&mut self) -> (&str, &str, Vec<(Vec<(bool, &str)>, ListState)>) {
(self.sub_menu_selected().title, self.sub_menu_selected().help_text, (self.sub_menu_selected().submenu_title, self.sub_menu_selected().help_text,
match SubMenuType::from_str(self.sub_menu_selected()._type) { match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::TOGGLE => { SubMenuType::TOGGLE => {
self.selected_sub_menu_toggles.lists.iter().map(|toggle_list| { self.selected_sub_menu_toggles.lists.iter().map(|toggle_list| {
(toggle_list.items.iter().map( (toggle_list.items.iter().map(
|toggle| (toggle.checked, toggle.title) |toggle| (toggle.checked, toggle.toggle_title)
).collect(), toggle_list.state.clone()) ).collect(), toggle_list.state.clone())
}).collect() }).collect()
}, },
SubMenuType::SLIDER => { SubMenuType::SLIDER => {
vec![(vec![], ListState::default())] vec![(vec![], ListState::default())]
}, },
SubMenuType::ONOFF => {
self.selected_sub_menu_onoff_selectors.lists.iter().map(|onoff_list| {
(onoff_list.items.iter().map(
|onoff| (onoff.checked, onoff.title)
).collect(), onoff_list.state.clone())
}).collect()
},
}) })
} }
@ -208,7 +136,7 @@ impl<'a> App<'a> {
.items.get_mut(list_idx).unwrap(); .items.get_mut(list_idx).unwrap();
match SubMenuType::from_str(selected_sub_menu._type) { match SubMenuType::from_str(selected_sub_menu._type) {
SubMenuType::TOGGLE => { SubMenuType::TOGGLE => {
let is_single_option = selected_sub_menu.is_single_option.is_some(); let is_single_option = selected_sub_menu.is_single_option;
let state = self.selected_sub_menu_toggles.state; let state = self.selected_sub_menu_toggles.state;
self.selected_sub_menu_toggles.lists.iter_mut() self.selected_sub_menu_toggles.lists.iter_mut()
.map(|list| (list.state.selected(), &mut list.items)) .map(|list| (list.state.selected(), &mut list.items))
@ -216,42 +144,31 @@ impl<'a> App<'a> {
.enumerate() .enumerate()
.for_each(|(i, o)| .for_each(|(i, o)|
if state.is_some() && i == state.unwrap() { if state.is_some() && i == state.unwrap() {
if o.checked != "is-appear" { if !o.checked {
o.checked = "is-appear"; o.checked = true;
} else { } else {
o.checked = "is-hidden"; o.checked = false;
} }
} else if is_single_option { } else if is_single_option {
o.checked = "is-hidden"; o.checked = false;
} }
)); ));
selected_sub_menu.toggles.iter_mut() selected_sub_menu.toggles.iter_mut()
.enumerate() .enumerate()
.for_each(|(i, o)| { .for_each(|(i, o)| {
if i == state { if i == state {
if o.checked != "is-appear" { if !o.checked {
o.checked = "is-appear"; o.checked = true;
} else { } else {
o.checked = "is-hidden"; o.checked = false;
} }
} else if is_single_option { } else if is_single_option {
o.checked = "is-hidden"; o.checked = false;
} }
}); });
}, },
SubMenuType::ONOFF => {
let onoff = self.selected_sub_menu_onoff_selectors.selected_list_item();
if onoff.checked != "is-appear" {
onoff.checked = "is-appear";
} else {
onoff.checked = "is-hidden";
}
selected_sub_menu.onoffselector.iter_mut()
.filter(|o| o.title == onoff.title)
.for_each(|o| o.checked = onoff.checked);
},
SubMenuType::SLIDER => { SubMenuType::SLIDER => {
// self.selected_sub_menu_sliders.selected_list_item().checked = "is-appear"; // self.selected_sub_menu_sliders.selected_list_item().checked = true;
} }
} }
} }
@ -352,17 +269,16 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
if app.outer_list { if app.outer_list {
let tab_selected = app.tab_selected(); let tab_selected = app.tab_selected();
let mut item_help = None; let mut item_help = None;
for list_section in 0..app.menu_items.get(tab_selected).unwrap().lists.len() { for (list_section, stateful_list) in app.menu_items.get(tab_selected).unwrap().lists.iter().enumerate() {
let stateful_list = &app.menu_items.get(tab_selected).unwrap().lists[list_section];
let items: Vec<ListItem> = stateful_list let items: Vec<ListItem> = stateful_list
.items .items
.iter() .iter()
.map(|i| { .map(|i| {
let lines = vec![Spans::from( let lines = vec![Spans::from(
if stateful_list.state.selected().is_some() { if stateful_list.state.selected().is_some() {
i.title.to_owned() i.submenu_title.to_owned()
} else { } else {
" ".to_owned() + i.title " ".to_owned() + i.submenu_title
})]; })];
ListItem::new(lines).style(Style::default().fg(Color::White)) ListItem::new(lines).style(Style::default().fg(Color::White))
}) })
@ -389,7 +305,7 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
// TODO: Add Save Defaults // TODO: Add Save Defaults
let help_paragraph = Paragraph::new( let help_paragraph = Paragraph::new(
item_help.unwrap_or("").replace("\"", "") + item_help.unwrap_or("").replace('\"', "") +
"\nA: Enter sub-menu | B: Exit menu | ZL/ZR: Next tab" "\nA: Enter sub-menu | B: Exit menu | ZL/ZR: Next tab"
).style(Style::default().fg(Color::Cyan)); ).style(Style::default().fg(Color::Cyan));
f.render_widget(help_paragraph, vertical_chunks[2]); f.render_widget(help_paragraph, vertical_chunks[2]);
@ -401,7 +317,7 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
let values_items: Vec<ListItem> = sub_menu_str.iter().map(|s| { let values_items: Vec<ListItem> = sub_menu_str.iter().map(|s| {
ListItem::new( ListItem::new(
vec![ vec![
Spans::from((if s.0 == "is-appear" { "X " } else { " " }).to_owned() + s.1) Spans::from((if s.0 { "X " } else { " " }).to_owned() + s.1)
] ]
) )
}).collect(); }).collect();
@ -419,7 +335,7 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
} }
let help_paragraph = Paragraph::new( let help_paragraph = Paragraph::new(
help_text.replace("\"", "") + help_text.replace('\"', "") +
"\nA: Select toggle | B: Exit submenu" "\nA: Select toggle | B: Exit submenu"
).style(Style::default().fg(Color::Cyan)); ).style(Style::default().fg(Color::Cyan));
f.render_widget(help_paragraph, vertical_chunks[2]); f.render_widget(help_paragraph, vertical_chunks[2]);
@ -433,22 +349,17 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
for key in app.menu_items.keys() { for key in app.menu_items.keys() {
for list in &app.menu_items.get(key).unwrap().lists { for list in &app.menu_items.get(key).unwrap().lists {
for sub_menu in &list.items { for sub_menu in &list.items {
let mut val = String::new(); let val : usize = sub_menu.toggles.iter()
sub_menu.toggles.iter() .filter(|t| t.checked)
.filter(|t| t.checked == "is-appear") .map(|t| t.toggle_value)
.for_each(|t| val.push_str(format!("{},", t.value).as_str())); .sum();
sub_menu.onoffselector.iter() settings.insert(sub_menu.submenu_id, val);
.for_each(|o| {
val.push_str(
format!("{}", if o.checked == "is-appear" { 1 } else { 0 }).as_str())
});
settings.insert(sub_menu.id, val);
} }
} }
} }
url.push_str("?"); url.push('?');
settings.iter() settings.iter()
.for_each(|(section, val)| url.push_str(format!("{}={}&", section, val).as_str())); .for_each(|(section, val)| url.push_str(format!("{}={}&", section, val).as_str()));
url url

View file

@ -49,14 +49,14 @@ impl<T: Clone> MultiStatefulList<T> {
state.select(Some(0)); state.select(Some(0));
} }
StatefulList { StatefulList {
state: state, state,
items: items[list_section_min_idx..list_section_max_idx].to_vec(), items: items[list_section_min_idx..list_section_max_idx].to_vec(),
} }
}).collect(); }).collect();
let total_len = items.len(); let total_len = items.len();
MultiStatefulList { MultiStatefulList {
lists: lists, lists,
total_len: total_len, total_len,
state: 0 state: 0
} }
} }
@ -68,12 +68,11 @@ impl<T: Clone> MultiStatefulList<T> {
if list_section != next_list_section { if list_section != next_list_section {
self.lists[list_section].unselect(); self.lists[list_section].unselect();
} }
let state; let state= if self.state + 1 >= self.total_len {
if self.state + 1 >= self.total_len { (0, 0)
state = (0, 0);
} else { } else {
state = (next_list_section, next_list_idx); (next_list_section, next_list_idx)
} };
self.lists[state.0].state.select(Some(state.1)); self.lists[state.0].state.select(Some(state.1));
self.state = self.list_idx_to_idx(state); self.state = self.list_idx_to_idx(state);
@ -84,13 +83,12 @@ impl<T: Clone> MultiStatefulList<T> {
let (last_list_section, last_list_idx) = (self.lists.len() - 1, self.lists[self.lists.len() - 1].items.len() - 1); let (last_list_section, last_list_idx) = (self.lists.len() - 1, self.lists[self.lists.len() - 1].items.len() - 1);
self.lists[list_section].unselect(); self.lists[list_section].unselect();
let state; let state= if self.state == 0 {
if self.state == 0 { (last_list_section, last_list_idx)
state = (last_list_section, last_list_idx);
} else { } else {
let (prev_list_section, prev_list_idx) = self.idx_to_list_idx(self.state - 1); let (prev_list_section, prev_list_idx) = self.idx_to_list_idx(self.state - 1);
state = (prev_list_section, prev_list_idx); (prev_list_section, prev_list_idx)
} };
self.lists[state.0].state.select(Some(state.1)); self.lists[state.0].state.select(Some(state.1));
self.state = self.list_idx_to_idx(state); self.state = self.list_idx_to_idx(state);
@ -99,12 +97,11 @@ impl<T: Clone> MultiStatefulList<T> {
pub fn next_list(&mut self) { pub fn next_list(&mut self) {
let (list_section, list_idx) = self.idx_to_list_idx(self.state); let (list_section, list_idx) = self.idx_to_list_idx(self.state);
let next_list_section = (list_section + 1) % self.lists.len(); let next_list_section = (list_section + 1) % self.lists.len();
let next_list_idx; let next_list_idx = if list_idx > self.lists[next_list_section].items.len() - 1 {
if list_idx > self.lists[next_list_section].items.len() - 1 { self.lists[next_list_section].items.len() - 1
next_list_idx = self.lists[next_list_section].items.len() - 1;
} else { } else {
next_list_idx = list_idx; list_idx
} };
if list_section != next_list_section { if list_section != next_list_section {
self.lists[list_section].unselect(); self.lists[list_section].unselect();
@ -117,19 +114,17 @@ impl<T: Clone> MultiStatefulList<T> {
pub fn previous_list(&mut self) { pub fn previous_list(&mut self) {
let (list_section, list_idx) = self.idx_to_list_idx(self.state); let (list_section, list_idx) = self.idx_to_list_idx(self.state);
let prev_list_section; let prev_list_section = if list_section == 0 {
if list_section == 0 { self.lists.len() - 1
prev_list_section = self.lists.len() - 1;
} else { } else {
prev_list_section = list_section - 1; list_section - 1
} };
let prev_list_idx; let prev_list_idx= if list_idx > self.lists[prev_list_section].items.len() - 1 {
if list_idx > self.lists[prev_list_section].items.len() - 1 { self.lists[prev_list_section].items.len() - 1
prev_list_idx = self.lists[prev_list_section].items.len() - 1;
} else { } else {
prev_list_idx = list_idx; list_idx
} };
if list_section != prev_list_section { if list_section != prev_list_section {
self.lists[list_section].unselect(); self.lists[list_section].unselect();
@ -152,7 +147,7 @@ impl<T> StatefulList<T> {
// Enforce state as first of list // Enforce state as first of list
state.select(Some(0)); state.select(Some(0));
StatefulList { StatefulList {
state: state, state,
items, items,
} }
} }

View file

@ -32,10 +32,10 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut url = String::new(); let mut url = String::new();
let frame_res = terminal.draw(|f| url = training_mod_tui::ui(f, &mut app))?; let frame_res = terminal.draw(|f| url = training_mod_tui::ui(f, &mut app))?;
for (i, cell) in frame_res.buffer.content().into_iter().enumerate() { for (i, cell) in frame_res.buffer.content().iter().enumerate() {
print!("{}", cell.symbol); print!("{}", cell.symbol);
if i % frame_res.area.width as usize == frame_res.area.width as usize - 1 { if i % frame_res.area.width as usize == frame_res.area.width as usize - 1 {
print!("\n"); println!();
} }
} }
println!(); println!();