Track recent roms (#786)

* track recent roms

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
This commit is contained in:
Logan McNaughton
2026-04-20 08:53:40 -06:00
committed by GitHub
parent cf3a27cecf
commit 048d1d662f
9 changed files with 204 additions and 71 deletions
+12 -3
View File
@@ -140,7 +140,11 @@ fn swap_rom(contents: Vec<u8>) -> Option<Vec<u8>> {
pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
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<Vec<u8>> {
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<Vec<u8>> {
)
.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() {
+5
View File
@@ -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 {
+2
View File
@@ -73,6 +73,7 @@ pub struct Config {
pub video: Video,
pub emulation: Emulation,
pub rom_dir: std::path::PathBuf,
pub recent_roms: Vec<String>,
}
impl Drop for Cheats {
@@ -161,6 +162,7 @@ impl Config {
usb: false,
},
rom_dir: std::path::PathBuf::new(),
recent_roms: Vec::new(),
}
}
}
+145 -55
View File
@@ -54,47 +54,88 @@ fn check_latest_version(weak: slint::Weak<AppWindow>) {
});
}
fn run_with_path(
weak: slint::Weak<AppWindow>,
path: std::path::PathBuf,
controller_paths: &[Option<String>],
) {
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<String>]) {
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<String>]) {
fn local_game_window(
app: &AppWindow,
config: &ui::config::Config,
controller_paths: &[Option<String>],
) {
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::<Vec<(slint::SharedString, slint::SharedString)>>(),
),
)));
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<String>]) {
.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<AppWindow>, 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::<Vec<slint::SharedString>>(),
),
)));
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<AppWindow>, config: &ui::config::Con
fn controller_window(
app: &AppWindow,
config: &ui::config::Config,
controller_names: &Vec<String>,
controller_names: &[String],
controller_paths: &[Option<String>],
) {
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::<Vec<slint::SharedString>>(),
),
)));
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::<Vec<slint::SharedString>>(),
),
)));
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::<Vec<slint::SharedString>>(),
),
)));
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();
+5
View File
@@ -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 => {
+1 -1
View File
@@ -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;
+20 -1
View File
@@ -1,18 +1,20 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// 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 <bool> 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);
}
}
}
}
}
}
}
+6 -6
View File
@@ -1051,13 +1051,13 @@ fn setup_wait_window(
handle.set_can_start(false);
}
let players_vec: slint::VecModel<slint::SharedString> =
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::<Vec<slint::SharedString>>(),
),
)));
}
})
+8 -5
View File
@@ -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::<Vec<slint::SharedString>>(),
),
)));
let timer = slint::Timer::default();
timer.start(