mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-07-23 10:47:10 +00:00
feat: initial support for steam integration
This commit is contained in:
parent
eddba38829
commit
7414efce47
BIN
public/images/steam.png
Normal file
BIN
public/images/steam.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3 KiB |
52
src-tauri/Cargo.lock
generated
52
src-tauri/Cargo.lock
generated
|
|
@ -47,6 +47,12 @@ version = "1.0.102"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "ascii"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
|
||||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.11.1"
|
||||
|
|
@ -363,6 +369,12 @@ version = "3.20.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "bytecount"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.0"
|
||||
|
|
@ -987,6 +999,7 @@ dependencies = [
|
|||
"rfd",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"steam_shortcuts_util",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-drpc",
|
||||
|
|
@ -2505,6 +2518,12 @@ version = "0.3.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
|
|
@ -2628,6 +2647,27 @@ version = "0.1.14"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom_locate"
|
||||
version = "4.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e3c83c053b0713da60c5b8de47fe8e494fe3ece5267b2f23090a07a053ba8f3"
|
||||
dependencies = [
|
||||
"bytecount",
|
||||
"memchr",
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.1"
|
||||
|
|
@ -4202,6 +4242,18 @@ version = "1.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "steam_shortcuts_util"
|
||||
version = "1.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0543ebdb23a93b196aceebc53f70cc5a573bb74248a974b3f5fa3883e6a89b6"
|
||||
dependencies = [
|
||||
"ascii",
|
||||
"crc32fast",
|
||||
"nom",
|
||||
"nom_locate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "string_cache"
|
||||
version = "0.8.9"
|
||||
|
|
|
|||
|
|
@ -28,3 +28,4 @@ rfd = "0.15"
|
|||
libc = "0.2"
|
||||
image = "0.24"
|
||||
percent-encoding = "2.3"
|
||||
steam_shortcuts_util = "1.1.8"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use std::fs;
|
|||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use steam_shortcuts_util::{Shortcut, parse_shortcuts, shortcuts_to_bytes};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::process::Stdio;
|
||||
|
|
@ -1680,6 +1681,124 @@ fn delete_screenshot(path: String) -> Result<(), String> {
|
|||
fs::remove_file(path).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn add_to_steam(
|
||||
app: AppHandle,
|
||||
instance_id: String,
|
||||
name: String,
|
||||
title_image: String,
|
||||
panorama_image: String,
|
||||
) -> Result<(), String> {
|
||||
let exe_path = std::env::current_exe().map_err(|e| e.to_string())?;
|
||||
let exe_str = exe_path.to_string_lossy().to_string();
|
||||
let launch_options = format!("\"{}\"", instance_id);
|
||||
let start_dir = exe_path.parent().map(|p| p.to_string_lossy().to_string()).unwrap_or_default();
|
||||
let app_id_32 = steam_shortcuts_util::app_id_generator::calculate_app_id(&exe_str, &name);
|
||||
let app_id_64 = ((app_id_32 as u64) << 32) | 0x02000000;
|
||||
let mut userdata_dirs = Vec::new();
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
let h = PathBuf::from(home);
|
||||
userdata_dirs.push(h.join(".steam/steam/userdata"));
|
||||
userdata_dirs.push(h.join(".local/share/Steam/userdata"));
|
||||
userdata_dirs.push(h.join(".var/app/com.valvesoftware.Steam/.local/share/Steam/userdata"));
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
userdata_dirs.push(PathBuf::from("C:\\Program Files (x86)\\Steam\\userdata"));
|
||||
}
|
||||
|
||||
let valid_userdata_dirs: Vec<PathBuf> = userdata_dirs.into_iter().filter(|d| d.exists()).collect();
|
||||
if valid_userdata_dirs.is_empty() {
|
||||
return Err("Steam userdata directory not found.".into());
|
||||
}
|
||||
|
||||
for userdata_root in valid_userdata_dirs {
|
||||
let entries = fs::read_dir(&userdata_root).map_err(|e| e.to_string())?;
|
||||
for entry in entries.flatten() {
|
||||
if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
|
||||
let user_config_dir = entry.path().join("config");
|
||||
let shortcuts_path = user_config_dir.join("shortcuts.vdf");
|
||||
if !user_config_dir.exists() { continue; }
|
||||
|
||||
let content = if shortcuts_path.exists() {
|
||||
fs::read(&shortcuts_path).unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let shortcuts = if !content.is_empty() {
|
||||
parse_shortcuts(&content).map_err(|e| e.to_string())?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let mut owned_shortcuts: Vec<steam_shortcuts_util::shortcut::ShortcutOwned> = shortcuts.iter().map(|s| s.to_owned()).collect();
|
||||
|
||||
if owned_shortcuts.iter().any(|s| s.app_name == name && s.exe == exe_str) {
|
||||
continue;
|
||||
}
|
||||
|
||||
owned_shortcuts.push(steam_shortcuts_util::shortcut::ShortcutOwned {
|
||||
order: owned_shortcuts.len().to_string(),
|
||||
app_id: app_id_32,
|
||||
app_name: name.clone(),
|
||||
exe: exe_str.clone(),
|
||||
start_dir: start_dir.clone(),
|
||||
icon: "".to_string(),
|
||||
shortcut_path: "".to_string(),
|
||||
launch_options: launch_options.clone(),
|
||||
is_hidden: false,
|
||||
allow_desktop_config: true,
|
||||
allow_overlay: true,
|
||||
open_vr: 0,
|
||||
dev_kit: 0,
|
||||
dev_kit_game_id: "".to_string(),
|
||||
dev_kit_overrite_app_id: 0,
|
||||
last_play_time: 0,
|
||||
tags: Vec::new(),
|
||||
});
|
||||
|
||||
let final_shortcuts: Vec<Shortcut> = owned_shortcuts.iter().map(|s| s.borrow()).collect();
|
||||
let new_content = shortcuts_to_bytes(&final_shortcuts);
|
||||
fs::write(&shortcuts_path, new_content).map_err(|e| e.to_string())?;
|
||||
let grid_dir = user_config_dir.join("grid");
|
||||
if !grid_dir.exists() {
|
||||
let _ = fs::create_dir_all(&grid_dir);
|
||||
}
|
||||
|
||||
if grid_dir.exists() {
|
||||
let resource_dir = app.path().resource_dir().unwrap_or_default();
|
||||
let resolve_image_path = |p: &str| {
|
||||
if p.starts_with('/') {
|
||||
resource_dir.join(&p[1..])
|
||||
} else {
|
||||
PathBuf::from(p)
|
||||
}
|
||||
};
|
||||
|
||||
let title_path = resolve_image_path(&title_image);
|
||||
let panorama_path = resolve_image_path(&panorama_image);
|
||||
if let Ok(img_data) = fs::read(&title_path) {
|
||||
let _ = fs::write(grid_dir.join(format!("{}p.png", app_id_64)), &img_data);
|
||||
}
|
||||
if let Ok(img_data) = fs::read(&panorama_path) {
|
||||
let _ = fs::write(grid_dir.join(format!("{}_hero.png", app_id_64)), &img_data);
|
||||
let _ = fs::write(grid_dir.join(format!("{}.png", app_id_64)), &img_data);
|
||||
}
|
||||
if let Ok(img_data) = fs::read(&title_path) {
|
||||
let _ = fs::write(grid_dir.join(format!("{}_logo.png", app_id_64)), &img_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn open_screenshot_folder(app: AppHandle, path: String) {
|
||||
let p = std::path::Path::new(&path);
|
||||
|
|
@ -1708,7 +1827,7 @@ pub fn run() {
|
|||
.register_uri_scheme_protocol("screenshots", |_app, request| {
|
||||
let uri = request.uri().path();
|
||||
let decoded_path = percent_encoding::percent_decode_str(uri).decode_utf8_lossy();
|
||||
let mut path_str = decoded_path.to_string();
|
||||
let path_str = decoded_path.to_string();
|
||||
#[cfg(target_os = "windows")]
|
||||
if path_str.starts_with('/') {
|
||||
path_str = path_str[1..].to_string();
|
||||
|
|
@ -1731,7 +1850,28 @@ pub fn run() {
|
|||
}
|
||||
}
|
||||
})
|
||||
.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, pick_folder, download_runner, delete_instance, sync_dlc, fetch_skin, workshop_install, workshop_uninstall, workshop_list_installed, get_screenshots, delete_screenshot, open_screenshot_folder, save_global_skin_pck, check_game_update, check_macos_runtime_installed, check_macos_runtime_installed_fast, download_logo, pick_file, save_file_dialog, write_binary_file, read_binary_file])
|
||||
.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, pick_folder, download_runner, delete_instance, sync_dlc, fetch_skin, workshop_install, workshop_uninstall, workshop_list_installed, get_screenshots, delete_screenshot, open_screenshot_folder, save_global_skin_pck, check_game_update, check_macos_runtime_installed, check_macos_runtime_installed_fast, download_logo, pick_file, save_file_dialog, write_binary_file, read_binary_file, add_to_steam])
|
||||
.setup(|app| {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() > 1 && !args[1].starts_with('-') {
|
||||
let instance_id = args[1].clone();
|
||||
let app_handle = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let _ = window.hide();
|
||||
}
|
||||
let state = app_handle.state::<GameState>();
|
||||
match launch_game(app_handle.clone(), state, instance_id, Vec::new()).await {
|
||||
Ok(_) => { app_handle.exit(0); }
|
||||
Err(e) => {
|
||||
eprintln!("Auto-launch error: {}", e);
|
||||
app_handle.exit(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ fn main() {
|
|||
let stage = env::var("EMERALD_LAUNCH_STAGE").unwrap_or_else(|_| "0".to_string());
|
||||
if stage == "0" {
|
||||
let mut cmd = Command::new(env::current_exe().unwrap());
|
||||
cmd.args(env::args().skip(1));
|
||||
cmd.env("EMERALD_LAUNCH_STAGE", "1");
|
||||
|
||||
let wayland_libs = ["/usr/lib64/libwayland-client.so.0", "/usr/lib/libwayland-client.so.0"];
|
||||
|
|
@ -76,6 +77,7 @@ fn main() {
|
|||
println!("Emerald: Automatic recovery triggered for graphics crash/invisible launch.");
|
||||
}
|
||||
let mut retry_cmd = Command::new(env::current_exe().unwrap());
|
||||
retry_cmd.args(env::args().skip(1));
|
||||
retry_cmd.env("EMERALD_LAUNCH_STAGE", "2")
|
||||
.env("GDK_BACKEND", "x11")
|
||||
.env("WEBKIT_DISABLE_DMABUF_RENDERER", "1")
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ interface AchievementToastProps {
|
|||
onClose: () => void;
|
||||
onClick?: () => void;
|
||||
title?: string;
|
||||
variant?: "error" | "update";
|
||||
variant?: "error" | "update" | "steam";
|
||||
}
|
||||
|
||||
export function AchievementToast({
|
||||
|
|
@ -43,6 +43,11 @@ export function AchievementToast({
|
|||
</svg>
|
||||
);
|
||||
}
|
||||
if (variant === "steam") {
|
||||
return (
|
||||
<img src="/images/steam.png" alt="Steam" className="w-8 h-8 object-contain" style={{ imageRendering: "pixelated", filter: "brightness(0) invert(1)" }} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -72,9 +77,9 @@ export function AchievementToast({
|
|||
onClick={
|
||||
onClick
|
||||
? () => {
|
||||
onClick();
|
||||
onClose();
|
||||
}
|
||||
onClick();
|
||||
onClose();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
className={`fixed top-6 right-6 z-[9999] ${onClick ? "cursor-pointer" : ""}`}
|
||||
|
|
|
|||
|
|
@ -26,9 +26,8 @@ const DeleteConfirmButton = memo(function DeleteConfirmButton({
|
|||
onClick={onClick}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className={`w-24 h-10 flex items-center justify-center mc-text-shadow transition-colors ${
|
||||
isDanger ? "text-red-500" : "text-white"
|
||||
} ${isHovered ? (isDanger ? "text-red-400" : "text-[#FFFF55]") : ""}`}
|
||||
className={`w-24 h-10 flex items-center justify-center mc-text-shadow transition-colors ${isDanger ? "text-red-500" : "text-white"
|
||||
} ${isHovered ? (isDanger ? "text-red-400" : "text-[#FFFF55]") : ""}`}
|
||||
style={{
|
||||
backgroundImage: isHovered
|
||||
? "url('/images/button_highlighted.png')"
|
||||
|
|
@ -62,7 +61,9 @@ const VersionsView = memo(function VersionsView() {
|
|||
downloadingId,
|
||||
downloadProgress,
|
||||
updatesAvailable,
|
||||
addToSteam,
|
||||
} = useGame();
|
||||
const { isDayTime } = useConfig();
|
||||
const [focusIndex, setFocusIndex] = useState<number>(0);
|
||||
const [focusBtn, setFocusBtn] = useState<number>(0);
|
||||
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
|
||||
|
|
@ -100,7 +101,7 @@ const VersionsView = memo(function VersionsView() {
|
|||
const edition = editions[focusIndex];
|
||||
const isInstalled = installedVersions.includes(edition.id);
|
||||
const isCustom = edition.id.startsWith("custom_");
|
||||
const maxBtn = isInstalled ? (isCustom ? 4 : 3) : 1;
|
||||
const maxBtn = isInstalled ? (isCustom ? 6 : 4) : 1;
|
||||
setFocusBtn((prev) => (prev <= 0 ? maxBtn : prev - 1));
|
||||
}
|
||||
} else if (e.key === "ArrowRight") {
|
||||
|
|
@ -109,7 +110,7 @@ const VersionsView = memo(function VersionsView() {
|
|||
const edition = editions[focusIndex];
|
||||
const isInstalled = installedVersions.includes(edition.id);
|
||||
const isCustom = edition.id.startsWith("custom_");
|
||||
const maxBtn = isInstalled ? (isCustom ? 4 : 3) : 1;
|
||||
const maxBtn = isInstalled ? (isCustom ? 6 : 4) : 1;
|
||||
setFocusBtn((prev) => (prev >= maxBtn ? 0 : prev + 1));
|
||||
}
|
||||
} else if (e.key === "Enter") {
|
||||
|
|
@ -142,10 +143,26 @@ const VersionsView = memo(function VersionsView() {
|
|||
playPressSound();
|
||||
setDeleteConfirmEdition(edition);
|
||||
}
|
||||
} else if (focusBtn === 4 && isCustom) {
|
||||
} else if (focusBtn === 4) {
|
||||
playPressSound();
|
||||
const PANORAMA_PROFILES = ["legacy_evolved", "360revived"];
|
||||
const panoId = PANORAMA_PROFILES.includes(edition.id)
|
||||
? edition.id
|
||||
: "legacy_evolved";
|
||||
const panoramaUrl = `/panorama/${panoId}_Panorama_Background_${isDayTime ? "Day" : "Night"}.png`;
|
||||
addToSteam(
|
||||
edition.id,
|
||||
edition.name,
|
||||
edition.titleImage,
|
||||
panoramaUrl,
|
||||
);
|
||||
} else if (focusBtn === 5 && isCustom) {
|
||||
playPressSound();
|
||||
setEditingEdition(edition);
|
||||
setIsImportModalOpen(true);
|
||||
} else if (focusBtn === 6 && isCustom) {
|
||||
playBackSound();
|
||||
onDeleteEdition(edition.id);
|
||||
}
|
||||
} else {
|
||||
if (focusBtn === 1) {
|
||||
|
|
@ -185,6 +202,8 @@ const VersionsView = memo(function VersionsView() {
|
|||
setActiveView,
|
||||
toggleInstall,
|
||||
handleCancelDownload,
|
||||
addToSteam,
|
||||
isDayTime
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -261,11 +280,9 @@ const VersionsView = memo(function VersionsView() {
|
|||
<div
|
||||
key={edition.id}
|
||||
data-index={i}
|
||||
className={`w-[calc(100%-16px)] mx-2 flex items-center gap-3 p-2 rounded-sm ${
|
||||
isSelected && !isComingSoon ? "bg-[#404040]/50" : ""
|
||||
} ${isFocused && !isComingSoon ? "ring-2 ring-white" : ""} ${
|
||||
isComingSoon ? "opacity-50 cursor-not-allowed" : ""
|
||||
}`}
|
||||
className={`w-[calc(100%-16px)] mx-2 flex items-center gap-3 p-2 rounded-sm ${isSelected && !isComingSoon ? "bg-[#404040]/50" : ""
|
||||
} ${isFocused && !isComingSoon ? "ring-2 ring-white" : ""} ${isComingSoon ? "opacity-50 cursor-not-allowed" : ""
|
||||
}`}
|
||||
onMouseEnter={() => !isComingSoon && setFocusIndex(i)}
|
||||
>
|
||||
<div className="w-6 flex items-center justify-center flex-shrink-0">
|
||||
|
|
@ -302,18 +319,17 @@ const VersionsView = memo(function VersionsView() {
|
|||
!isComingSoon && handleEditionClick(edition, i)
|
||||
}
|
||||
disabled={isComingSoon}
|
||||
className={`flex-1 text-left min-w-0 outline-none rounded ${
|
||||
focusIndex === i && focusBtn === 0 && !isComingSoon
|
||||
? "ring-2 ring-white"
|
||||
: ""
|
||||
} ${isComingSoon ? "cursor-not-allowed" : ""}`}
|
||||
className={`flex-1 text-left min-w-0 outline-none rounded ${focusIndex === i && focusBtn === 0 && !isComingSoon
|
||||
? "ring-2 ring-white"
|
||||
: ""
|
||||
} ${isComingSoon ? "cursor-not-allowed" : ""}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{edition.logo && (
|
||||
<img
|
||||
src={
|
||||
edition.logo.startsWith("http") ||
|
||||
edition.logo.startsWith("/images")
|
||||
edition.logo.startsWith("/images")
|
||||
? edition.logo
|
||||
: `screenshots://localhost/${edition.logo.replace(/\\/g, "/")}`
|
||||
}
|
||||
|
|
@ -323,9 +339,8 @@ const VersionsView = memo(function VersionsView() {
|
|||
/>
|
||||
)}
|
||||
<span
|
||||
className={`text-xl tracking-wide truncate ${
|
||||
isSelected ? "text-white" : "text-black"
|
||||
}`}
|
||||
className={`text-xl tracking-wide truncate ${isSelected ? "text-white" : "text-black"
|
||||
}`}
|
||||
style={{ textShadow: "none" }}
|
||||
>
|
||||
{edition.name}
|
||||
|
|
@ -346,9 +361,8 @@ const VersionsView = memo(function VersionsView() {
|
|||
)}
|
||||
</div>
|
||||
<p
|
||||
className={`text-base font-medium leading-tight ${
|
||||
isSelected ? "text-[#DDDDDD]" : "text-[#666666]"
|
||||
}`}
|
||||
className={`text-base font-medium leading-tight ${isSelected ? "text-[#DDDDDD]" : "text-[#666666]"
|
||||
}`}
|
||||
>
|
||||
{edition.desc}
|
||||
</p>
|
||||
|
|
@ -371,7 +385,7 @@ const VersionsView = memo(function VersionsView() {
|
|||
backgroundImage:
|
||||
(hoveredBtn?.row === i &&
|
||||
hoveredBtn?.btn === "cancel") ||
|
||||
(focusIndex === i && focusBtn === 1)
|
||||
(focusIndex === i && focusBtn === 1)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
|
|
@ -407,7 +421,7 @@ const VersionsView = memo(function VersionsView() {
|
|||
backgroundImage:
|
||||
(hoveredBtn?.row === i &&
|
||||
hoveredBtn?.btn === "download") ||
|
||||
(focusIndex === i && focusBtn === 1)
|
||||
(focusIndex === i && focusBtn === 1)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
|
|
@ -441,7 +455,7 @@ const VersionsView = memo(function VersionsView() {
|
|||
backgroundImage:
|
||||
(hoveredBtn?.row === i &&
|
||||
hoveredBtn?.btn === "cancel") ||
|
||||
(focusIndex === i && focusBtn === 1)
|
||||
(focusIndex === i && focusBtn === 1)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
|
|
@ -476,7 +490,7 @@ const VersionsView = memo(function VersionsView() {
|
|||
backgroundImage:
|
||||
(hoveredBtn?.row === i &&
|
||||
hoveredBtn?.btn === "update") ||
|
||||
(focusIndex === i && focusBtn === 1)
|
||||
(focusIndex === i && focusBtn === 1)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
|
|
@ -510,7 +524,7 @@ const VersionsView = memo(function VersionsView() {
|
|||
backgroundImage:
|
||||
(hoveredBtn?.row === i &&
|
||||
hoveredBtn?.btn === "folder") ||
|
||||
(focusIndex === i && focusBtn === 2)
|
||||
(focusIndex === i && focusBtn === 2)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
|
|
@ -539,7 +553,7 @@ const VersionsView = memo(function VersionsView() {
|
|||
backgroundImage:
|
||||
(hoveredBtn?.row === i &&
|
||||
hoveredBtn?.btn === "delete") ||
|
||||
(focusIndex === i && focusBtn === 3)
|
||||
(focusIndex === i && focusBtn === 3)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
|
|
@ -553,6 +567,33 @@ const VersionsView = memo(function VersionsView() {
|
|||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
playPressSound();
|
||||
const PANORAMA_PROFILES = ['legacy_evolved', '360revived'];
|
||||
const panoId = PANORAMA_PROFILES.includes(edition.id) ? edition.id : 'legacy_evolved';
|
||||
const panoramaUrl = `/panorama/${panoId}_Panorama_Background_${isDayTime ? 'Day' : 'Night'}.png`;
|
||||
addToSteam(edition.id, edition.name, edition.titleImage, panoramaUrl);
|
||||
}}
|
||||
onMouseEnter={() =>
|
||||
setHoveredBtn({ row: i, btn: "steam" })
|
||||
}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className="w-8 h-8 flex items-center justify-center text-[#3a3a3a]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
(hoveredBtn?.row === i &&
|
||||
hoveredBtn?.btn === "steam") ||
|
||||
(focusIndex === i && focusBtn === 4)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
<img src="/images/steam.png" alt="Add To Steam" className="w-6 h-6 object-contain" style={{ imageRendering: "pixelated", filter: "brightness(0) invert(1)" }} />
|
||||
</button>
|
||||
{isCustom && (
|
||||
<>
|
||||
<button
|
||||
|
|
@ -571,7 +612,7 @@ const VersionsView = memo(function VersionsView() {
|
|||
backgroundImage:
|
||||
(hoveredBtn?.row === i &&
|
||||
hoveredBtn?.btn === "edit") ||
|
||||
(focusIndex === i && focusBtn === 4)
|
||||
(focusIndex === i && focusBtn === 5)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
|
|
@ -598,15 +639,15 @@ const VersionsView = memo(function VersionsView() {
|
|||
onDeleteEdition(edition.id);
|
||||
}}
|
||||
onMouseEnter={() =>
|
||||
setHoveredBtn({ row: i, btn: "delete" })
|
||||
setHoveredBtn({ row: i, btn: "delete_custom" })
|
||||
}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className="w-8 h-8 flex items-center justify-center text-red-600"
|
||||
style={{
|
||||
backgroundImage:
|
||||
(hoveredBtn?.row === i &&
|
||||
hoveredBtn?.btn === "delete") ||
|
||||
(focusIndex === i && focusBtn === 3)
|
||||
hoveredBtn?.btn === "delete_custom") ||
|
||||
(focusIndex === i && focusBtn === 6)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
|
|
@ -651,7 +692,7 @@ const VersionsView = memo(function VersionsView() {
|
|||
backgroundImage:
|
||||
(hoveredBtn?.row === editions.length &&
|
||||
hoveredBtn?.btn === "add") ||
|
||||
focusIndex === editions.length
|
||||
focusIndex === editions.length
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
|
|
@ -684,7 +725,7 @@ const VersionsView = memo(function VersionsView() {
|
|||
backgroundImage:
|
||||
(hoveredBtn?.row === editions.length &&
|
||||
hoveredBtn?.btn === "folder_import") ||
|
||||
focusIndex === editions.length + 1
|
||||
focusIndex === editions.length + 1
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
|||
gameRaw.downloadingId, gameRaw.editions, gameRaw.isRunnerDownloading,
|
||||
gameRaw.runnerDownloadProgress, gameRaw.error, gameRaw.updateCustomEdition,
|
||||
gameRaw.handleUninstall, gameRaw.handleCancelDownload, gameRaw.gameUpdateMessage, configRaw.profile,
|
||||
gameRaw.updatesAvailable
|
||||
gameRaw.updatesAvailable, gameRaw.addToSteam, gameRaw.steamSuccessMessage
|
||||
]);
|
||||
|
||||
const audio = useMemo(() => audioRaw, [
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ export function useGameManager({
|
|||
>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [gameUpdateMessage, setGameUpdateMessage] = useState<string | null>(null);
|
||||
const [steamSuccessMessage, setSteamSuccessMessage] = useState<string | null>(null);
|
||||
|
||||
const editions = useMemo(
|
||||
() => [...BASE_EDITIONS, ...customEditions],
|
||||
|
|
@ -264,6 +265,19 @@ export function useGameManager({
|
|||
[customEditions, setCustomEditions],
|
||||
);
|
||||
|
||||
const addToSteam = useCallback(
|
||||
async (id: string, name: string, titleImage: string, panoramaImage: string) => {
|
||||
try {
|
||||
await TauriService.addToSteam(id, name, titleImage, panoramaImage);
|
||||
setSteamSuccessMessage(`Added ${name} to Steam! (Restart Steam to see it)`);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
setError(typeof e === "string" ? e : e.message || "Failed to add to Steam");
|
||||
}
|
||||
},
|
||||
[setError, setSteamSuccessMessage]
|
||||
);
|
||||
|
||||
return {
|
||||
installs,
|
||||
isGameRunning,
|
||||
|
|
@ -286,6 +300,9 @@ export function useGameManager({
|
|||
checkInstalls,
|
||||
gameUpdateMessage,
|
||||
setGameUpdateMessage,
|
||||
steamSuccessMessage,
|
||||
setSteamSuccessMessage,
|
||||
updatesAvailable,
|
||||
addToSteam,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,6 +177,13 @@ export default function App() {
|
|||
variant="update"
|
||||
/>
|
||||
|
||||
<AchievementToast
|
||||
message={game.steamSuccessMessage}
|
||||
onClose={() => game.setSteamSuccessMessage(null)}
|
||||
title="Steam Integration"
|
||||
variant="steam"
|
||||
/>
|
||||
|
||||
<AnimatePresence>
|
||||
{showSetup ? (
|
||||
<SetupView
|
||||
|
|
|
|||
|
|
@ -226,4 +226,13 @@ export class TauriService {
|
|||
static async downloadLogo(id: string, url: string): Promise<string> {
|
||||
return invoke("download_logo", { id, url });
|
||||
}
|
||||
|
||||
static async addToSteam(
|
||||
instanceId: string,
|
||||
name: string,
|
||||
titleImage: string,
|
||||
panoramaImage: string
|
||||
): Promise<void> {
|
||||
return invoke("add_to_steam", { instanceId, name, titleImage, panoramaImage });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue