diff --git a/src/device.rs b/src/device.rs index 9a2c07a..830f3da 100644 --- a/src/device.rs +++ b/src/device.rs @@ -140,7 +140,11 @@ fn swap_rom(contents: Vec) -> Option> { pub fn get_rom_contents(file_path: &std::path::Path) -> Option> { let mut contents = vec![]; - if file_path.extension().unwrap().eq_ignore_ascii_case("zip") { + if file_path + .extension() + .unwrap_or_default() + .eq_ignore_ascii_case("zip") + { let zip_file = fs::File::open(file_path).unwrap(); let mut archive = zip::ZipArchive::new(zip_file).unwrap(); for i in 0..archive.len() { @@ -157,7 +161,11 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Option> { break; } } - } else if file_path.extension().unwrap().eq_ignore_ascii_case("7z") { + } else if file_path + .extension() + .unwrap_or_default() + .eq_ignore_ascii_case("7z") + { let mut archive = sevenz_rust2::ArchiveReader::open(file_path, sevenz_rust2::Password::empty()).unwrap(); @@ -182,7 +190,8 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Option> { ) .expect("ok"); } else { - contents = fs::read(file_path).expect("Should have been able to read the file"); + contents = fs::read(file_path) + .unwrap_or_else(|_| panic!("Could not read ROM file: {}", file_path.display())); } if contents.is_empty() { diff --git a/src/main.rs b/src/main.rs index ccc95cc..4a25bd4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -138,6 +138,11 @@ async fn main() -> std::io::Result<()> { } let mut device = device::Device::new(); + + device.ui.config.recent_roms.retain(|x| *x != game); + device.ui.config.recent_roms.insert(0, game); + device.ui.config.recent_roms.truncate(5); + if args.fullscreen { device.ui.video.fullscreen = true; } else { diff --git a/src/ui/config.rs b/src/ui/config.rs index 1338d4c..26a5c06 100644 --- a/src/ui/config.rs +++ b/src/ui/config.rs @@ -73,6 +73,7 @@ pub struct Config { pub video: Video, pub emulation: Emulation, pub rom_dir: std::path::PathBuf, + pub recent_roms: Vec, } impl Drop for Cheats { @@ -161,6 +162,7 @@ impl Config { usb: false, }, rom_dir: std::path::PathBuf::new(), + recent_roms: Vec::new(), } } } diff --git a/src/ui/gui.rs b/src/ui/gui.rs index daa0614..0bca1c6 100644 --- a/src/ui/gui.rs +++ b/src/ui/gui.rs @@ -54,47 +54,88 @@ fn check_latest_version(weak: slint::Weak) { }); } +fn run_with_path( + weak: slint::Weak, + path: std::path::PathBuf, + controller_paths: &[Option], +) { + let controller_paths = controller_paths.to_owned(); + let weak2 = weak.clone(); + weak.upgrade_in_event_loop(move |handle| { + if handle.get_game_running() { + return; + } + save_settings(&handle, &controller_paths); + + run_rom( + path, + ui::GameSettings { + overclock: handle.get_overclock_n64_cpu(), + disable_expansion_pak: handle.get_disable_expansion_pak(), + cheats: std::collections::HashMap::new(), // will be filled in later + load_savestate_slot: None, + }, + None, + RASettings { + enabled: handle.get_ra_enabled(), + hardcore: handle.get_ra_hardcore(), + challenge: handle.get_ra_challenge(), + leaderboard: handle.get_ra_leaderboard(), + }, + weak2, + ); + }) + .unwrap(); +} + fn file_dropped(app: &AppWindow, controller_paths: &[Option]) { let weak = app.as_weak(); - let owned_controller_paths = controller_paths.to_owned(); + let controller_paths = controller_paths.to_owned(); app.window() .on_winit_window_event(move |_winit_window, event| { if let slint::winit_030::winit::event::WindowEvent::DroppedFile(path) = event { - let controller_paths = owned_controller_paths.clone(); - let path = path.clone(); - let weak2 = weak.clone(); - weak.upgrade_in_event_loop(move |handle| { - if handle.get_game_running() { - return; - } - save_settings(&handle, &controller_paths); - - run_rom( - path, - ui::GameSettings { - overclock: handle.get_overclock_n64_cpu(), - disable_expansion_pak: handle.get_disable_expansion_pak(), - cheats: std::collections::HashMap::new(), // will be filled in later - load_savestate_slot: None, - }, - None, - RASettings { - enabled: handle.get_ra_enabled(), - hardcore: handle.get_ra_hardcore(), - challenge: handle.get_ra_challenge(), - leaderboard: handle.get_ra_leaderboard(), - }, - weak2, - ); - }) - .unwrap(); + run_with_path(weak.clone(), path.to_path_buf(), &controller_paths); } slint::winit_030::EventResult::Propagate }); } -fn local_game_window(app: &AppWindow, controller_paths: &[Option]) { +fn local_game_window( + app: &AppWindow, + config: &ui::config::Config, + controller_paths: &[Option], +) { let dirs = ui::get_dirs(); + + app.set_recent_roms(slint::ModelRc::from(std::rc::Rc::new( + slint::VecModel::from( + config + .recent_roms + .iter() + .filter(|x| { + if let Ok(exists) = std::fs::exists(x) + && exists + { + true + } else { + false + } + }) + .map(|x| { + ( + x.into(), + std::path::Path::new(x) + .file_name() + .unwrap() + .to_str() + .unwrap() + .into(), + ) + }) + .collect::>(), + ), + ))); + let weak = app.as_weak(); let owned_controller_paths = controller_paths.to_owned(); app.on_open_rom_button_clicked(move || { @@ -106,6 +147,20 @@ fn local_game_window(app: &AppWindow, controller_paths: &[Option]) { .unwrap(); }); + let weak = app.as_weak(); + let owned_controller_paths = controller_paths.to_owned(); + app.on_recent_rom_button_clicked(move |rom| { + let controller_paths = owned_controller_paths.clone(); + weak.upgrade_in_event_loop(move |handle| { + run_with_path( + handle.as_weak(), + std::path::PathBuf::from(rom.to_string()), + &controller_paths, + ); + }) + .unwrap(); + }); + let saves_path = dirs.data_dir.join("saves"); app.on_saves_folder_button_clicked(move || { open::that_detached(saves_path.clone()).unwrap(); @@ -162,11 +217,14 @@ fn update_input_profiles(weak: &slint::Weak, config: &ui::config::Con profile_bindings.push(position.unwrap() as i32); } - let input_profiles = slint::VecModel::default(); - for profile in profiles { - input_profiles.push(profile.into()); - } - handle.set_input_profiles(slint::ModelRc::from(std::rc::Rc::new(input_profiles))); + handle.set_input_profiles(slint::ModelRc::from(std::rc::Rc::new( + slint::VecModel::from( + profiles + .iter() + .map(|x| x.into()) + .collect::>(), + ), + ))); handle .set_selected_profile_binding(slint::ModelRc::from(std::rc::Rc::new(profile_bindings))); @@ -187,7 +245,7 @@ fn update_input_profiles(weak: &slint::Weak, config: &ui::config::Con fn controller_window( app: &AppWindow, config: &ui::config::Config, - controller_names: &Vec, + controller_names: &[String], controller_paths: &[Option], ) { app.set_emulate_vru(config.input.emulate_vru); @@ -200,25 +258,38 @@ fn controller_window( slint::VecModel::from(config.input.transfer_pak.to_vec()), ))); - let gb_rom_paths = slint::VecModel::default(); - for gb_rom_path in config.input.gb_rom_path.iter() { - gb_rom_paths.push(gb_rom_path.into()); - } - app.set_gb_rom_paths(slint::ModelRc::from(std::rc::Rc::new(gb_rom_paths))); + app.set_gb_rom_paths(slint::ModelRc::from(std::rc::Rc::new( + slint::VecModel::from( + config + .input + .gb_rom_path + .iter() + .map(|x| x.into()) + .collect::>(), + ), + ))); - let gb_ram_paths = slint::VecModel::default(); - for gb_ram_path in config.input.gb_ram_path.iter() { - gb_ram_paths.push(gb_ram_path.into()); - } - app.set_gb_ram_paths(slint::ModelRc::from(std::rc::Rc::new(gb_ram_paths))); + app.set_gb_ram_paths(slint::ModelRc::from(std::rc::Rc::new( + slint::VecModel::from( + config + .input + .gb_ram_path + .iter() + .map(|x| x.into()) + .collect::>(), + ), + ))); update_input_profiles(&app.as_weak(), config); - let controllers = slint::VecModel::default(); - for controller in controller_names { - controllers.push(controller.into()); - } - app.set_controller_names(slint::ModelRc::from(std::rc::Rc::new(controllers))); + app.set_controller_names(slint::ModelRc::from(std::rc::Rc::new( + slint::VecModel::from( + controller_names + .iter() + .map(|x| x.into()) + .collect::>(), + ), + ))); let controller_changed = slint::VecModel::default(); let selected_controllers = slint::VecModel::default(); @@ -395,8 +466,8 @@ pub fn app_window() { controller_paths.insert(0, None); settings_window(&app, &game_ui.config); controller_window(&app, &game_ui.config, &controller_names, &controller_paths); + local_game_window(&app, &game_ui.config, &controller_paths); } - local_game_window(&app, &controller_paths); ui::netplay::netplay_window(&app, &controller_paths); ui::cheats::cheats_window(&app); app.run().unwrap(); @@ -453,13 +524,14 @@ pub fn run_rom( } } - if !command + let success = command .arg(file_path.to_str().unwrap()) .status() .await .unwrap() - .success() - { + .success(); + + if !success { eprintln!("Failed to run game"); } @@ -469,6 +541,24 @@ pub fn run_rom( if let Some(rom_dir) = file_path.parent().unwrap().to_str() { handle.set_rom_dir(rom_dir.into()); } + if success { + let recent_roms = slint::VecModel::default(); + recent_roms.push(( + file_path.to_str().unwrap().into(), + file_path.file_name().unwrap().to_str().unwrap().into(), + )); + + for rom in handle.get_recent_roms().iter() { + if rom.0 != file_path.to_str().unwrap() + && recent_roms.row_count() < 5 + && let Ok(exists) = std::fs::exists(&rom.0) + && exists + { + recent_roms.push(rom); + } + } + handle.set_recent_roms(slint::ModelRc::from(std::rc::Rc::new(recent_roms))); + } handle.set_game_running(false); }) .unwrap(); diff --git a/src/ui/gui/appwindow.slint b/src/ui/gui/appwindow.slint index 881cffa..d49ec45 100644 --- a/src/ui/gui/appwindow.slint +++ b/src/ui/gui/appwindow.slint @@ -22,6 +22,7 @@ export component AppWindow inherits Window { callback wiki_button_clicked; callback discord_button_clicked; callback newversion_button_clicked; + callback recent_rom_button_clicked(string); callback patreon_button_clicked; callback github_sponsors_button_clicked; callback source_code_button_clicked; @@ -72,6 +73,7 @@ export component AppWindow inherits Window { in-out property controller_names <=> ControllerData.controller_names; in-out property selected_controller <=> ControllerData.selected_controller; in-out property game_running <=> State.game_running; + in-out property recent_roms <=> State.recent_roms; in-out property blank_profiles <=> ControllerData.blank_profiles; in-out property cheat_game_name <=> CheatsData.cheat_game_name; in-out property cheat_game_crc <=> CheatsData.cheat_game_crc; @@ -105,6 +107,9 @@ export component AppWindow inherits Window { newversion_button_clicked => { newversion_button_clicked(); } + recent_rom_button_clicked(rom) => { + recent_rom_button_clicked(rom); + } } if(side-bar.current-item == 1): ControllerConfig { input_profile_button_clicked => { diff --git a/src/ui/gui/cheat_page.slint b/src/ui/gui/cheat_page.slint index 5df273a..04cf9bb 100644 --- a/src/ui/gui/cheat_page.slint +++ b/src/ui/gui/cheat_page.slint @@ -69,7 +69,7 @@ export component Cheats inherits Page { cheat_toggled(CheatsData.cheat_game_crc,data.cheat_name,"",self.checked); } } - for item[index] in data.options: CheckBox { + for item in data.options: CheckBox { text: item.option; enabled: !data.enabled || (data.enabled && item.enabled); checked: item.enabled; diff --git a/src/ui/gui/localgame_page.slint b/src/ui/gui/localgame_page.slint index 556fb1f..4ade531 100644 --- a/src/ui/gui/localgame_page.slint +++ b/src/ui/gui/localgame_page.slint @@ -1,18 +1,20 @@ // Copyright © SixtyFPS GmbH // SPDX-License-Identifier: MIT -import { Button, HorizontalBox, VerticalBox } from "std-widgets.slint"; +import { Button, HorizontalBox, VerticalBox, GroupBox } from "std-widgets.slint"; import { Page } from "page.slint"; import { AboutData, NewVersionButton } from "about_page.slint"; export global State { in-out property game_running; + in-out property <[{a:string, b:string}]> recent_roms; } export component LocalGame inherits Page { callback open_rom_button_clicked; callback saves_folder_button_clicked; callback newversion_button_clicked; + callback recent_rom_button_clicked(string); title: @tr("Local Game"); VerticalBox { @@ -44,5 +46,22 @@ export component LocalGame inherits Page { } } Rectangle { } + + if State.recent_roms.length>0:HorizontalBox { + alignment: center; + GroupBox { + title: @tr("Recent ROMs"); + VerticalLayout { + alignment: space-between; + for rom in State.recent_roms: Button { + text: rom.b; + enabled: !State.game_running; + clicked => { + recent_rom_button_clicked(rom.a); + } + } + } + } + } } } diff --git a/src/ui/netplay.rs b/src/ui/netplay.rs index 9b61c0a..d90e074 100644 --- a/src/ui/netplay.rs +++ b/src/ui/netplay.rs @@ -1051,13 +1051,13 @@ fn setup_wait_window( handle.set_can_start(false); } - let players_vec: slint::VecModel = - slint::VecModel::default(); - for player in player_names { - players_vec.push(player.into()); - } handle.set_players(slint::ModelRc::from(std::rc::Rc::new( - players_vec, + slint::VecModel::from( + player_names + .iter() + .map(|x| x.into()) + .collect::>(), + ), ))); } }) diff --git a/src/ui/vru.rs b/src/ui/vru.rs index aaea147..ee649ac 100644 --- a/src/ui/vru.rs +++ b/src/ui/vru.rs @@ -17,11 +17,14 @@ pub fn prompt_for_match(words: &[String], frame_time: f64) -> u16 { vru_dialog_weak.unwrap().window().hide().unwrap(); }); - let words_vec = slint::VecModel::default(); - for word in dedup_words { - words_vec.push(word.into()); - } - vru_dialog.set_words(slint::ModelRc::from(std::rc::Rc::new(words_vec))); + vru_dialog.set_words(slint::ModelRc::from(std::rc::Rc::new( + slint::VecModel::from( + dedup_words + .iter() + .map(|x| x.into()) + .collect::>(), + ), + ))); let timer = slint::Timer::default(); timer.start(