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";
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;

View file

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

View file

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

View file

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

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 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);
}

View file

@ -1,117 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Document</title>
<link rel="stylesheet" href="./css/training_modpack.css" />
</head>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Modpack Menu</title>
<link rel="stylesheet" href="./css/training_modpack.css" />
</head>
<body id="body">
<script defer src="./js/training_modpack.js"></script>
<div class="l-header">
<div class="l-header-title">
<div class="header-title"><span>Ultimate Training Modpack Menu</span></div>
</div>
<div class="header" style="flex-grow: 1;">
<a id="ret-button" tabindex="-1" class="header-decoration" href="javascript:goBackHook();">
<div class="ret-icon-wrapper">
<img class="ret-icon is-appear" src="./img/m_retnormal.svg">
</div>
</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>
<body>
<script defer src="./js/training_modpack.js"></script>
<div class="l-header">
<a id="ret-button" tabindex="-1" class="return-icon-container" href="javascript:goBackHook();">
<img class="return-icon" src="./img/m_retnormal.svg">
</a>
<p class="header-title">Ultimate Training Modpack Menu</p>
<div class="header-label">
<p class="header-desc">Reset Current Menu: &#xE0A2;</p>
<p class="header-desc">Reset All Menus: &#xE0A4;</p>
<p class="header-desc">Save Defaults: &#xE0A5;</p>
</div>
<br>
<br>
<br>
<br>
<div class="l-grid">
<!--
Script the part below via templating. Overall structure is perhaps
[
l-qa qa [id=qa-{{menuName}} tabindex="{{index}}"] {
// make question for {{menuName}}
// make answer with l-grid : l-item list for options
},
...
]
Remember to set make max keyword size greater than 23!
-->
{{#sub_menus}}
<div class="l-qa">
{{^onoffselector}}
<a id="qa-{{id}}" class="qa" tabindex="{{index}}" href="javascript:void(0);" onfocus="focusQA(this);setHelpText({{help_text}})" onblur="defocusQA(this)" onclick="toggleAnswer(this)">
<div class="question-outer">
<div class="question-border">
<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 class="tab-list-container">
<p>Prev Tab: &#xE0A6;</p>
<div class="tab-list">
{{#tabs}}
<button class="tab-button active" id="{{tab_id}}_button" onclick="openTab(this)" tabindex="-1">{{tab_title}}</button>
{{/tabs}}
</div>
<p>Next Tab: &#xE0A7;</p>
</div>
<div class="tab-content-container">
{{#tabs}}
<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>
</button>
<div class="hide modal{{#is_single_option}} single-option{{/is_single_option}}">
{{#toggles}}
<button class="menu-button" onclick="toggleOption(this)">
<div class="menu-icon"><img class="hide" src="./img/check.svg" data-val="{{toggle_value}}"/></div>
<p>{{toggle_title}}</p>
</button>
{{/toggles}}
</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>
</footer>
</body>
{{/tab_submenus}}
</div>
{{/tabs}}
</div>
<footer id="footer" class="footer">
<p id="help-text" class="header-desc"></p>
</footer>
</body>
</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::{
backend::{Backend},
layout::{Constraint, Corner, Direction, Layout},
@ -22,81 +22,26 @@ pub struct App<'a> {
pub tabs: StatefulList<&'a str>,
pub menu_items: HashMap<&'a str, MultiStatefulList<SubMenu<'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 outer_list: bool
}
impl<'a> App<'a> {
pub fn new(menu: training_mod_consts::Menu<'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());
}
}
};
pub fn new(menu: UiMenu<'a>) -> App<'a> {
let num_lists = 3;
let mut menu_items_stateful = HashMap::new();
tabs.keys().for_each(|k| {
menu.tabs.iter().for_each(|tab| {
menu_items_stateful.insert(
k.clone(),
MultiStatefulList::with_items(tabs.get(k).unwrap().clone(), num_lists)
tab.tab_title,
MultiStatefulList::with_items(tab.tab_submenus.clone(), num_lists)
);
});
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,
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),
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 toggles = selected_sub_menu.toggles.clone();
let sliders = selected_sub_menu.sliders.clone();
let onoffs = selected_sub_menu.onoffselector.clone();
// let sliders = selected_sub_menu.sliders.clone();
match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::TOGGLE => {
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()} )
},
SubMenuType::SLIDER => {
self.selected_sub_menu_sliders = MultiStatefulList::with_items(
sliders,
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()} )
// self.selected_sub_menu_sliders = MultiStatefulList::with_items(
// sliders,
// if selected_sub_menu.sliders.len() >= 3 { 3 } else { selected_sub_menu.sliders.len()} )
},
};
}
@ -136,14 +75,13 @@ impl<'a> App<'a> {
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);
&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) {
match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.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) {
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.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) {
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.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) {
SubMenuType::TOGGLE => self.selected_sub_menu_toggles.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)>) {
(self.sub_menu_selected().title, self.sub_menu_selected().help_text,
pub fn sub_menu_strs_and_states(&mut self) -> (&str, &str, Vec<(Vec<(bool, &str)>, ListState)>) {
(self.sub_menu_selected().submenu_title, self.sub_menu_selected().help_text,
match SubMenuType::from_str(self.sub_menu_selected()._type) {
SubMenuType::TOGGLE => {
self.selected_sub_menu_toggles.lists.iter().map(|toggle_list| {
(toggle_list.items.iter().map(
|toggle| (toggle.checked, toggle.title)
|toggle| (toggle.checked, toggle.toggle_title)
).collect(), toggle_list.state.clone())
}).collect()
},
SubMenuType::SLIDER => {
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();
match SubMenuType::from_str(selected_sub_menu._type) {
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;
self.selected_sub_menu_toggles.lists.iter_mut()
.map(|list| (list.state.selected(), &mut list.items))
@ -216,42 +144,31 @@ impl<'a> App<'a> {
.enumerate()
.for_each(|(i, o)|
if state.is_some() && i == state.unwrap() {
if o.checked != "is-appear" {
o.checked = "is-appear";
if !o.checked {
o.checked = true;
} else {
o.checked = "is-hidden";
o.checked = false;
}
} else if is_single_option {
o.checked = "is-hidden";
o.checked = false;
}
));
selected_sub_menu.toggles.iter_mut()
.enumerate()
.for_each(|(i, o)| {
if i == state {
if o.checked != "is-appear" {
o.checked = "is-appear";
if !o.checked {
o.checked = true;
} else {
o.checked = "is-hidden";
o.checked = false;
}
} 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 => {
// 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 {
let tab_selected = app.tab_selected();
let mut item_help = None;
for list_section in 0..app.menu_items.get(tab_selected).unwrap().lists.len() {
let stateful_list = &app.menu_items.get(tab_selected).unwrap().lists[list_section];
for (list_section, stateful_list) in app.menu_items.get(tab_selected).unwrap().lists.iter().enumerate() {
let items: Vec<ListItem> = stateful_list
.items
.iter()
.map(|i| {
let lines = vec![Spans::from(
if stateful_list.state.selected().is_some() {
i.title.to_owned()
i.submenu_title.to_owned()
} else {
" ".to_owned() + i.title
" ".to_owned() + i.submenu_title
})];
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
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"
).style(Style::default().fg(Color::Cyan));
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| {
ListItem::new(
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();
@ -419,7 +335,7 @@ pub fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) -> String {
}
let help_paragraph = Paragraph::new(
help_text.replace("\"", "") +
help_text.replace('\"', "") +
"\nA: Select toggle | B: Exit submenu"
).style(Style::default().fg(Color::Cyan));
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 list in &app.menu_items.get(key).unwrap().lists {
for sub_menu in &list.items {
let mut val = String::new();
sub_menu.toggles.iter()
.filter(|t| t.checked == "is-appear")
.for_each(|t| val.push_str(format!("{},", t.value).as_str()));
let val : usize = sub_menu.toggles.iter()
.filter(|t| t.checked)
.map(|t| t.toggle_value)
.sum();
sub_menu.onoffselector.iter()
.for_each(|o| {
val.push_str(
format!("{}", if o.checked == "is-appear" { 1 } else { 0 }).as_str())
});
settings.insert(sub_menu.id, val);
settings.insert(sub_menu.submenu_id, val);
}
}
}
url.push_str("?");
url.push('?');
settings.iter()
.for_each(|(section, val)| url.push_str(format!("{}={}&", section, val).as_str()));
url

View file

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

View file

@ -32,10 +32,10 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut url = String::new();
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);
if i % frame_res.area.width as usize == frame_res.area.width as usize - 1 {
print!("\n");
println!();
}
}
println!();