feat(title-image): allow removing a custom title image

Previously once a title image was set on a custom edition there was no way to fall back to the default MenuTitle.png short of deleting the instance. Add a round-trip remove path:

- Rust: remove_instance_title_image command deletes title_image.png from the instance stub dir if present, registered in the invoke_handler.
- TauriService.removeInstanceTitleImage wraps the command.
- useGameManager.removeTitleImage calls the command and resets the edition's titleImage back to /images/MenuTitle.png.
- The Versions-view title-image button now detects a custom image (titleImage starts with data:) and swaps the icon, tooltip, and click handler between set/remove, for both keyboard and mouse paths.
This commit is contained in:
itsRevela
2026-04-17 14:55:34 -05:00
parent f289e6568c
commit 0489737b5c
4 changed files with 85 additions and 22 deletions

View File

@@ -565,6 +565,18 @@ fn set_instance_title_image(app: AppHandle, instance_id: String) -> Result<Strin
Ok(format!("data:image/png;base64,{}", b64))
}
#[tauri::command]
#[allow(non_snake_case)]
fn remove_instance_title_image(app: AppHandle, instance_id: String) -> Result<(), String> {
let safe_id = sanitize_path_component(&instance_id)?;
let stub_dir = get_app_dir(&app).join("instances").join(&safe_id);
let title_path = stub_dir.join("title_image.png");
if title_path.exists() {
fs::remove_file(&title_path).map_err(|e| e.to_string())?;
}
Ok(())
}
#[tauri::command]
#[allow(non_snake_case)]
fn get_instance_title_image(app: AppHandle, instance_id: String) -> Result<String, String> {
@@ -1609,7 +1621,7 @@ pub fn run() {
.plugin(tauri_plugin_gamepad::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_drpc::init())
.invoke_handler(tauri::generate_handler![setup_macos_runtime, launch_game, stop_game, check_game_installed, save_config, load_config, download_and_install, open_instance_folder, cancel_download, get_available_runners, get_external_palettes, import_theme, download_runner, delete_instance, update_tray_icon, sync_dlc, fetch_skin, check_for_game_update, open_skins_folder, save_skin_file, import_instance_folder, set_instance_title_image, get_instance_title_image])
.invoke_handler(tauri::generate_handler![setup_macos_runtime, launch_game, stop_game, check_game_installed, save_config, load_config, download_and_install, open_instance_folder, cancel_download, get_available_runners, get_external_palettes, import_theme, download_runner, delete_instance, update_tray_icon, sync_dlc, fetch_skin, check_for_game_update, open_skins_folder, save_skin_file, import_instance_folder, set_instance_title_image, get_instance_title_image, remove_instance_title_image])
.setup(|app| {
let config = load_config(app.handle().clone());
let visible = config.enable_tray_icon.unwrap_or(true);

View File

@@ -9,7 +9,7 @@ const VersionsView = memo(function VersionsView() {
const { setActiveView } = useUI();
const { profile: selectedProfile, setProfile: setSelectedProfile } = useConfig();
const { playClickSound, playBackSound, playSfx } = useAudio();
const { editions, installs: installedVersions, toggleInstall, handleUninstall: onUninstall, deleteCustomEdition: onDeleteEdition, addCustomEdition: onAddEdition, updateCustomEdition: onUpdateEdition, downloadingId, updateAvailable, importInstance, setTitleImage } = useGame();
const { editions, installs: installedVersions, toggleInstall, handleUninstall: onUninstall, deleteCustomEdition: onDeleteEdition, addCustomEdition: onAddEdition, updateCustomEdition: onUpdateEdition, downloadingId, updateAvailable, importInstance, setTitleImage, removeTitleImage } = useGame();
const [focusRow, setFocusRow] = useState<number>(0);
const [focusCol, setFocusCol] = useState<number>(0);
@@ -101,8 +101,14 @@ const VersionsView = memo(function VersionsView() {
}
} else if (focusCol === 4) {
if (isCustom) {
playClickSound();
setTitleImage(edition.id);
const hasCustomImage = typeof edition.titleImage === "string" && edition.titleImage.startsWith("data:");
if (hasCustomImage) {
playBackSound();
removeTitleImage(edition.id);
} else {
playClickSound();
setTitleImage(edition.id);
}
}
} else if (focusCol === 5) {
if (isCustom) {
@@ -437,6 +443,9 @@ const VersionsView = memo(function VersionsView() {
)}
{isCustom && (
<>
{(() => {
const hasCustomImage = typeof edition.titleImage === "string" && edition.titleImage.startsWith("data:");
return (
<button
data-row={i}
data-col={isInstalled ? 4 : 2}
@@ -447,8 +456,13 @@ const VersionsView = memo(function VersionsView() {
}}
onClick={(e) => {
e.stopPropagation();
playClickSound();
setTitleImage(edition.id);
if (hasCustomImage) {
playBackSound();
removeTitleImage(edition.id);
} else {
playClickSound();
setTitleImage(edition.id);
}
}}
className="w-8 h-8 flex items-center justify-center outline-none border-none transition-all"
style={{
@@ -459,24 +473,45 @@ const VersionsView = memo(function VersionsView() {
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
title="Set title image"
title={hasCustomImage ? "Remove title image" : "Set title image"}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="square"
className="text-white drop-shadow-md"
>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
{hasCustomImage ? (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="square"
className="text-white drop-shadow-md"
>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
<line x1="4" y1="4" x2="20" y2="20" stroke="#ff5555" strokeWidth="3"></line>
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="square"
className="text-white drop-shadow-md"
>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21 15 16 10 5 21"></polyline>
</svg>
)}
</button>
);
})()}
<button
data-row={i}
data-col={isInstalled ? 5 : 3}

View File

@@ -192,6 +192,17 @@ export function useGameManager({ profile, setProfile, customEditions, setCustomE
}
}, [customEditions, setCustomEditions]);
const removeTitleImage = useCallback(async (id: string) => {
try {
await TauriService.removeInstanceTitleImage(id);
setCustomEditions(customEditions.map((e) =>
e.id === id ? { ...e, titleImage: "/images/MenuTitle.png" } : e
));
} catch {
// failed
}
}, [customEditions, setCustomEditions]);
// Load custom title images on startup
useEffect(() => {
const loadTitleImages = async () => {
@@ -233,5 +244,6 @@ export function useGameManager({ profile, setProfile, customEditions, setCustomE
updateAvailable,
importInstance,
setTitleImage,
removeTitleImage,
};
}

View File

@@ -173,6 +173,10 @@ export class TauriService {
return invoke('get_instance_title_image', { instanceId });
}
static async removeInstanceTitleImage(instanceId: string): Promise<void> {
return invoke('remove_instance_title_image', { instanceId });
}
static async importInstanceFolder(instanceId: string, url: string): Promise<string> {
return invoke('import_instance_folder', { instanceId, url });
}