mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-09-23 08:32:51 +00:00
feat: local custom TUs support
This commit is contained in:
+63
-6
@@ -35,6 +35,7 @@ pub struct CustomEdition {
|
||||
pub name: String,
|
||||
pub desc: String,
|
||||
pub url: String,
|
||||
pub path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
@@ -146,10 +147,10 @@ fn find_executable_recursive(root: &PathBuf, file_name: &str) -> Option<PathBuf>
|
||||
None
|
||||
}
|
||||
|
||||
fn is_macos_runtime_installed(app: &AppHandle) -> bool {
|
||||
fn is_macos_runtime_installed(_app: &AppHandle) -> bool {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let runtime_dir = get_macos_runtime_dir(app);
|
||||
let runtime_dir = get_macos_runtime_dir(_app);
|
||||
let toolkit_dir = runtime_dir.join("toolkit");
|
||||
if !toolkit_dir.exists() {
|
||||
return false;
|
||||
@@ -280,6 +281,19 @@ fn import_theme(app: AppHandle) -> Result<String, String> {
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn pick_folder() -> Result<String, String> {
|
||||
let folder = rfd::FileDialog::new()
|
||||
.set_title("Select Custom TU Folder")
|
||||
.pick_folder();
|
||||
|
||||
if let Some(path) = folder {
|
||||
Ok(path.to_string_lossy().to_string())
|
||||
} else {
|
||||
Err("CANCELED".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_available_runners(app: AppHandle) -> Vec<Runner> {
|
||||
let mut runners = Vec::new();
|
||||
@@ -456,13 +470,36 @@ async fn download_runner(app: AppHandle, state: State<'_, DownloadState>, name:
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
fn check_game_installed(app: AppHandle, instance_id: String) -> bool {
|
||||
let config = load_config(app.clone());
|
||||
if let Some(ref editions) = config.custom_editions {
|
||||
if let Some(edition) = editions.iter().find(|e| e.id == instance_id) {
|
||||
if let Some(ref path) = edition.path {
|
||||
return PathBuf::from(path).join("Minecraft.Client.exe").exists();
|
||||
}
|
||||
}
|
||||
}
|
||||
get_app_dir(&app).join("instances").join(&instance_id).join("Minecraft.Client.exe").exists()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
fn open_instance_folder(app: AppHandle, instance_id: String) {
|
||||
let dir = get_app_dir(&app).join("instances").join(&instance_id);
|
||||
let config = load_config(app.clone());
|
||||
let dir = if let Some(ref editions) = config.custom_editions {
|
||||
if let Some(edition) = editions.iter().find(|e| e.id == instance_id) {
|
||||
if let Some(ref path) = edition.path {
|
||||
Some(PathBuf::from(path))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let dir = dir.unwrap_or_else(|| get_app_dir(&app).join("instances").join(&instance_id));
|
||||
if dir.exists() {
|
||||
let _ = app.opener().open_path(dir.to_str().unwrap(), None::<&str>);
|
||||
}
|
||||
@@ -471,6 +508,15 @@ fn open_instance_folder(app: AppHandle, instance_id: String) {
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
fn delete_instance(app: AppHandle, instance_id: String) -> Result<(), String> {
|
||||
let config = load_config(app.clone());
|
||||
if let Some(ref editions) = config.custom_editions {
|
||||
if let Some(edition) = editions.iter().find(|e| e.id == instance_id) {
|
||||
if edition.path.is_some() {
|
||||
// Do not delete files for custom path instances
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
let dir = get_app_dir(&app).join("instances").join(&instance_id);
|
||||
if dir.exists() {
|
||||
let _ = fs::remove_dir_all(dir);
|
||||
@@ -1187,7 +1233,18 @@ async fn launch_game(app: AppHandle, state: State<'_, GameState>, instance_id: S
|
||||
}
|
||||
|
||||
let _ = perform_dlc_sync(&app, &instance_dir)?;
|
||||
let game_exe = instance_dir.join("Minecraft.Client.exe");
|
||||
|
||||
let mut custom_path = None;
|
||||
if let Some(ref editions) = config.custom_editions {
|
||||
if let Some(edition) = editions.iter().find(|e| e.id == instance_id) {
|
||||
if let Some(ref path) = edition.path {
|
||||
custom_path = Some(PathBuf::from(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let working_dir = custom_path.unwrap_or_else(|| instance_dir.clone());
|
||||
let game_exe = working_dir.join("Minecraft.Client.exe");
|
||||
if !game_exe.exists() {
|
||||
return Err("Game executable not found in instance folder.".into());
|
||||
}
|
||||
@@ -1226,7 +1283,7 @@ async fn launch_game(app: AppHandle, state: State<'_, GameState>, instance_id: S
|
||||
}
|
||||
|
||||
cmd.arg(&game_exe)
|
||||
.current_dir(&instance_dir);
|
||||
.current_dir(&working_dir);
|
||||
|
||||
let child = cmd.spawn().map_err(|e| e.to_string())?;
|
||||
{
|
||||
@@ -1532,7 +1589,7 @@ 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, 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])
|
||||
.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])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
@@ -18,9 +18,8 @@ const ModalButton = memo(function ModalButton({
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className={`flex-1 h-12 flex items-center justify-center text-xl mc-text-shadow transition-colors outline-none border-none bg-transparent ${
|
||||
isDanger ? "text-red-500" : "text-white"
|
||||
} ${isHovered ? (isDanger ? "text-red-400" : "text-[#FFFF55]") : ""}`}
|
||||
className={`flex-1 h-12 flex items-center justify-center text-xl mc-text-shadow transition-colors outline-none border-none bg-transparent ${isDanger ? "text-red-500" : "text-white"
|
||||
} ${isHovered ? (isDanger ? "text-red-400" : "text-[#FFFF55]") : ""}`}
|
||||
style={{
|
||||
backgroundImage: isHovered
|
||||
? "url('/images/button_highlighted.png')"
|
||||
@@ -41,10 +40,12 @@ export default function CustomTUModal({
|
||||
playPressSound,
|
||||
playBackSound,
|
||||
editingEdition = null,
|
||||
initialPath = "",
|
||||
}: any) {
|
||||
const [name, setName] = useState("");
|
||||
const [desc, setDesc] = useState("");
|
||||
const [url, setUrl] = useState("");
|
||||
const [path, setPath] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [focusIndex, setFocusIndex] = useState(0);
|
||||
|
||||
@@ -53,13 +54,17 @@ export default function CustomTUModal({
|
||||
setName(editingEdition.name);
|
||||
setDesc(editingEdition.desc);
|
||||
setUrl(editingEdition.url);
|
||||
setPath(editingEdition.path || "");
|
||||
} else if (isOpen && initialPath) {
|
||||
setPath(initialPath);
|
||||
} else if (!isOpen) {
|
||||
setName("");
|
||||
setDesc("");
|
||||
setUrl("");
|
||||
setPath("");
|
||||
setError("");
|
||||
}
|
||||
}, [editingEdition, isOpen]);
|
||||
}, [editingEdition, isOpen, initialPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
@@ -88,25 +93,30 @@ export default function CustomTUModal({
|
||||
};
|
||||
window.addEventListener("keydown", handleKey);
|
||||
return () => window.removeEventListener("keydown", handleKey);
|
||||
}, [isOpen, focusIndex, name, desc, url]);
|
||||
}, [isOpen, focusIndex, name, desc, url, path]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleImport = () => {
|
||||
if (!name || !url) {
|
||||
setError("Name and URL are required");
|
||||
if (!name) {
|
||||
setError("Name is required");
|
||||
return;
|
||||
}
|
||||
if (!url.startsWith("http")) {
|
||||
if (!url && !path) {
|
||||
setError("URL or Path is required");
|
||||
return;
|
||||
}
|
||||
if (url && !url.startsWith("http")) {
|
||||
setError("Invalid URL");
|
||||
return;
|
||||
}
|
||||
setError("");
|
||||
onImport({ name, desc: desc || "Custom imported TU", url });
|
||||
onImport({ name, desc: desc || "Custom imported TU", url, path: path || undefined });
|
||||
onClose();
|
||||
setName("");
|
||||
setDesc("");
|
||||
setUrl("");
|
||||
setPath("");
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -114,19 +124,19 @@ export default function CustomTUModal({
|
||||
<div
|
||||
className="relative w-[400px] p-6 flex flex-col items-center"
|
||||
style={{
|
||||
backgroundImage: "url('/images/Download_Background.png')",
|
||||
backgroundImage: "url('/images/background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
backgroundRepeat: "no-repeat",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
<h2 className="text-xl text-white mc-text-shadow mb-4 text-center">
|
||||
<h2 className="text-xl text-black mc-text-shadow mb-4 text-center">
|
||||
{editingEdition ? "Edit Custom TU" : "Import Custom TU"}
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-white text-sm mc-text-shadow uppercase tracking-widest">
|
||||
<label className="text-gray text-sm mc-text-shadow uppercase tracking-widest">
|
||||
TU Name
|
||||
</label>
|
||||
<input
|
||||
@@ -142,7 +152,7 @@ export default function CustomTUModal({
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-white text-sm mc-text-shadow uppercase tracking-widest">
|
||||
<label className="text-gray text-sm mc-text-shadow uppercase tracking-widest">
|
||||
Description (Optional)
|
||||
</label>
|
||||
<input
|
||||
@@ -157,7 +167,7 @@ export default function CustomTUModal({
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-white text-sm mc-text-shadow uppercase tracking-widest">
|
||||
<label className="text-gray text-sm mc-text-shadow uppercase tracking-widest">
|
||||
Download URL (.zip)
|
||||
</label>
|
||||
<input
|
||||
@@ -165,12 +175,27 @@ export default function CustomTUModal({
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
onFocus={() => setFocusIndex(2)}
|
||||
placeholder="https://example.com/mod.zip"
|
||||
placeholder="optional if path is set"
|
||||
className="w-full h-10 px-3 bg-black/40 border-2 border-[#373737] text-white text-base outline-none font-['Mojangles']"
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{path && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-gray text-sm mc-text-shadow uppercase tracking-widest">
|
||||
Local Path
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
readOnly
|
||||
value={path}
|
||||
className="w-full h-10 px-3 bg-black/20 border-2 border-[#222] text-black text-xs outline-none font-['Mojangles'] cursor-not-allowed"
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="text-red-500 text-center mc-text-shadow uppercase text-xs tracking-widest">
|
||||
{error}
|
||||
|
||||
@@ -22,9 +22,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')"
|
||||
@@ -43,22 +42,20 @@ const VersionsView = memo(function VersionsView() {
|
||||
const { profile: selectedProfile, setProfile: setSelectedProfile, animationsEnabled } = useConfig();
|
||||
const { playPressSound, playBackSound } = useAudio();
|
||||
const { editions, installs: installedVersions, toggleInstall, handleUninstall, handleCancelDownload, deleteCustomEdition: onDeleteEdition, addCustomEdition: onAddEdition, updateCustomEdition: onUpdateEdition, downloadingId, downloadProgress } = useGame();
|
||||
|
||||
const [focusIndex, setFocusIndex] = useState<number>(0);
|
||||
const [focusBtn, setFocusBtn] = useState<number>(0);
|
||||
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
|
||||
const [editingEdition, setEditingEdition] = useState<any>(null);
|
||||
const [hoveredBtn, setHoveredBtn] = useState<{row: number, btn: string} | null>(null);
|
||||
const [initialPath, setInitialPath] = useState<string>("");
|
||||
const [hoveredBtn, setHoveredBtn] = useState<{ row: number, btn: string } | null>(null);
|
||||
const [deleteConfirmEdition, setDeleteConfirmEdition] = useState<any>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const ITEM_COUNT = editions.length + 2; // +1 for "+" button, +1 for Done button
|
||||
|
||||
const ITEM_COUNT = editions.length + 3;
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (document.activeElement?.tagName === "INPUT") return;
|
||||
|
||||
|
||||
if (e.key === "Escape" || e.key === "Backspace") {
|
||||
playBackSound();
|
||||
setActiveView("main");
|
||||
@@ -139,6 +136,9 @@ const VersionsView = memo(function VersionsView() {
|
||||
} else if (focusIndex === editions.length) {
|
||||
playPressSound();
|
||||
setIsImportModalOpen(true);
|
||||
} else if (focusIndex === editions.length + 1) {
|
||||
playPressSound();
|
||||
handleImportFolder();
|
||||
} else {
|
||||
playBackSound();
|
||||
setActiveView("main");
|
||||
@@ -174,7 +174,7 @@ const VersionsView = memo(function VersionsView() {
|
||||
|
||||
const handleEditionClick = (edition: any, index: number) => {
|
||||
const isInstalled = installedVersions.includes(edition.id);
|
||||
|
||||
|
||||
if (isInstalled) {
|
||||
playPressSound();
|
||||
setSelectedProfile(edition.id);
|
||||
@@ -182,6 +182,18 @@ const VersionsView = memo(function VersionsView() {
|
||||
setFocusIndex(index);
|
||||
};
|
||||
|
||||
const handleImportFolder = async () => {
|
||||
try {
|
||||
const folder = await TauriService.pickFolder();
|
||||
if (folder) {
|
||||
setInitialPath(folder);
|
||||
setIsImportModalOpen(true);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e !== "CANCELED") console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={containerRef}
|
||||
@@ -195,7 +207,7 @@ const VersionsView = memo(function VersionsView() {
|
||||
Versions
|
||||
</h2>
|
||||
|
||||
<div
|
||||
<div
|
||||
className="w-full min-w-[480px] p-6 mb-4"
|
||||
style={{
|
||||
backgroundImage: "url('/images/background.png')",
|
||||
@@ -222,11 +234,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">
|
||||
@@ -261,13 +271,12 @@ const VersionsView = memo(function VersionsView() {
|
||||
<button
|
||||
onClick={() => !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
|
||||
<img
|
||||
src={edition.logo}
|
||||
alt=""
|
||||
className="w-5 h-5 object-contain flex-shrink-0"
|
||||
@@ -275,9 +284,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}
|
||||
@@ -288,9 +296,8 @@ const VersionsView = memo(function VersionsView() {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className={`text-base font-medium leading-tight ${
|
||||
isSelected ? "text-[#DDDDDD]" : "text-[#666666]"
|
||||
}`}>
|
||||
<p className={`text-base font-medium leading-tight ${isSelected ? "text-[#DDDDDD]" : "text-[#666666]"
|
||||
}`}>
|
||||
{edition.desc}
|
||||
</p>
|
||||
</button>
|
||||
@@ -303,7 +310,7 @@ const VersionsView = memo(function VersionsView() {
|
||||
e.stopPropagation();
|
||||
handleCancelDownload();
|
||||
}}
|
||||
onMouseEnter={() => setHoveredBtn({row: i, btn: 'cancel'})}
|
||||
onMouseEnter={() => setHoveredBtn({ row: i, btn: 'cancel' })}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className="w-8 h-8 flex items-center justify-center text-red-600"
|
||||
style={{
|
||||
@@ -315,7 +322,7 @@ const VersionsView = memo(function VersionsView() {
|
||||
}}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="square">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
) : edition.comingSoon ? (
|
||||
@@ -326,7 +333,7 @@ const VersionsView = memo(function VersionsView() {
|
||||
e.stopPropagation();
|
||||
if (!downloadingId) toggleInstall(edition.id);
|
||||
}}
|
||||
onMouseEnter={() => setHoveredBtn({row: i, btn: 'download'})}
|
||||
onMouseEnter={() => setHoveredBtn({ row: i, btn: 'download' })}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className={`w-8 h-8 flex items-center justify-center ${downloadingId ? "text-gray-400 cursor-not-allowed" : "text-[#3a3a3a]"}`}
|
||||
style={{
|
||||
@@ -355,7 +362,7 @@ const VersionsView = memo(function VersionsView() {
|
||||
e.stopPropagation();
|
||||
handleCancelDownload();
|
||||
}}
|
||||
onMouseEnter={() => setHoveredBtn({row: i, btn: 'cancel'})}
|
||||
onMouseEnter={() => setHoveredBtn({ row: i, btn: 'cancel' })}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className="w-8 h-8 flex items-center justify-center text-red-600"
|
||||
style={{
|
||||
@@ -367,7 +374,7 @@ const VersionsView = memo(function VersionsView() {
|
||||
}}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="square">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
) : (
|
||||
@@ -377,7 +384,7 @@ const VersionsView = memo(function VersionsView() {
|
||||
e.stopPropagation();
|
||||
if (!downloadingId) toggleInstall(edition.id);
|
||||
}}
|
||||
onMouseEnter={() => setHoveredBtn({row: i, btn: 'update'})}
|
||||
onMouseEnter={() => setHoveredBtn({ row: i, btn: 'update' })}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className={`w-8 h-8 flex items-center justify-center ${downloadingId ? "text-gray-400 cursor-not-allowed" : "text-[#3a3a3a]"}`}
|
||||
style={{
|
||||
@@ -403,82 +410,33 @@ const VersionsView = memo(function VersionsView() {
|
||||
playPressSound();
|
||||
TauriService.openInstanceFolder(edition.id);
|
||||
}}
|
||||
onMouseEnter={() => setHoveredBtn({row: i, btn: 'folder'})}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className="w-8 h-8 flex items-center justify-center text-[#3a3a3a]"
|
||||
style={{
|
||||
backgroundImage: (hoveredBtn?.row === i && hoveredBtn?.btn === 'folder') || (focusIndex === i && focusBtn === 2)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/images/Folder_Icon.png"
|
||||
alt="Folder"
|
||||
className="w-6 h-6 object-contain"
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
playBackSound();
|
||||
setDeleteConfirmEdition(edition);
|
||||
}}
|
||||
onMouseEnter={() => setHoveredBtn({row: i, btn: 'delete'})}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className="w-8 h-8 flex items-center justify-center text-[#3a3a3a]"
|
||||
style={{
|
||||
backgroundImage: (hoveredBtn?.row === i && hoveredBtn?.btn === 'delete') || (focusIndex === i && focusBtn === 3)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/images/Trash_Bin_Icon.png"
|
||||
alt="Delete"
|
||||
className="w-6 h-6 object-contain"
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
</button>
|
||||
{isCustom && (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
playPressSound();
|
||||
setEditingEdition(edition);
|
||||
setIsImportModalOpen(true);
|
||||
}}
|
||||
onMouseEnter={() => setHoveredBtn({row: i, btn: 'edit'})}
|
||||
onMouseEnter={() => setHoveredBtn({ row: i, btn: 'folder' })}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className="w-8 h-8 flex items-center justify-center text-[#3a3a3a]"
|
||||
style={{
|
||||
backgroundImage: (hoveredBtn?.row === i && hoveredBtn?.btn === 'edit') || (focusIndex === i && focusBtn === 4)
|
||||
backgroundImage: (hoveredBtn?.row === i && hoveredBtn?.btn === 'folder') || (focusIndex === i && focusBtn === 2)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="square">
|
||||
<path d="M12 20h9"/>
|
||||
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
|
||||
</svg>
|
||||
<img
|
||||
src="/images/Folder_Icon.png"
|
||||
alt="Folder"
|
||||
className="w-6 h-6 object-contain"
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
playBackSound();
|
||||
onDeleteEdition(edition.id);
|
||||
setDeleteConfirmEdition(edition);
|
||||
}}
|
||||
onMouseEnter={() => setHoveredBtn({row: i, btn: 'delete'})}
|
||||
onMouseEnter={() => setHoveredBtn({ row: i, btn: 'delete' })}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className="w-8 h-8 flex items-center justify-center text-red-600"
|
||||
className="w-8 h-8 flex items-center justify-center text-[#3a3a3a]"
|
||||
style={{
|
||||
backgroundImage: (hoveredBtn?.row === i && hoveredBtn?.btn === 'delete') || (focusIndex === i && focusBtn === 3)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
@@ -487,29 +445,79 @@ const VersionsView = memo(function VersionsView() {
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="square">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
</svg>
|
||||
<img
|
||||
src="/images/Trash_Bin_Icon.png"
|
||||
alt="Delete"
|
||||
className="w-6 h-6 object-contain"
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
</button>
|
||||
{isCustom && (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
playPressSound();
|
||||
setEditingEdition(edition);
|
||||
setIsImportModalOpen(true);
|
||||
}}
|
||||
onMouseEnter={() => setHoveredBtn({ row: i, btn: 'edit' })}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className="w-8 h-8 flex items-center justify-center text-[#3a3a3a]"
|
||||
style={{
|
||||
backgroundImage: (hoveredBtn?.row === i && hoveredBtn?.btn === 'edit') || (focusIndex === i && focusBtn === 4)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="square">
|
||||
<path d="M12 20h9" />
|
||||
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
playBackSound();
|
||||
onDeleteEdition(edition.id);
|
||||
}}
|
||||
onMouseEnter={() => setHoveredBtn({ row: i, btn: 'delete' })}
|
||||
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)
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="square">
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="w-full flex items-center justify-center p-2 mt-1">
|
||||
<div className="w-full flex items-center justify-center gap-4 p-2 mt-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
playPressSound();
|
||||
setInitialPath("");
|
||||
setIsImportModalOpen(true);
|
||||
}}
|
||||
onMouseEnter={() => setHoveredBtn({row: editions.length, btn: 'add'})}
|
||||
onMouseEnter={() => setFocusIndex(editions.length)}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
className="w-8 h-8 flex items-center justify-center text-[#3a3a3a]"
|
||||
style={{
|
||||
@@ -521,9 +529,34 @@ const VersionsView = memo(function VersionsView() {
|
||||
}}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="square">
|
||||
<path d="M12 5v14M5 12h14"/>
|
||||
<path d="M12 5v14M5 12h14" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
playPressSound();
|
||||
handleImportFolder();
|
||||
}}
|
||||
onMouseEnter={() => setFocusIndex(editions.length + 1)}
|
||||
onMouseLeave={() => setHoveredBtn(null)}
|
||||
title="Import Custom TU"
|
||||
className="w-8 h-8 flex items-center justify-center text-[#3a3a3a]"
|
||||
style={{
|
||||
backgroundImage: (hoveredBtn?.row === editions.length && hoveredBtn?.btn === 'folder_import') || focusIndex === editions.length + 1
|
||||
? "url('/images/Button_Square_Highlighted.png')"
|
||||
: "url('/images/Button_Square.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/images/Folder_Icon.png"
|
||||
alt="Import Custom TU"
|
||||
className="w-5 h-5 object-contain"
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -531,15 +564,15 @@ const VersionsView = memo(function VersionsView() {
|
||||
|
||||
<div className="flex justify-center">
|
||||
<button
|
||||
data-index={editions.length + 1}
|
||||
onMouseEnter={() => setFocusIndex(editions.length + 1)}
|
||||
data-index={editions.length + 2}
|
||||
onMouseEnter={() => setFocusIndex(editions.length + 2)}
|
||||
onClick={() => {
|
||||
playBackSound();
|
||||
setActiveView("main");
|
||||
}}
|
||||
className="w-48 h-10 flex items-center justify-center text-xl mc-text-shadow outline-none border-none text-white"
|
||||
style={{
|
||||
backgroundImage: focusIndex === editions.length + 1
|
||||
backgroundImage: focusIndex === editions.length + 2
|
||||
? "url('/images/button_highlighted.png')"
|
||||
: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
@@ -555,6 +588,7 @@ const VersionsView = memo(function VersionsView() {
|
||||
onClose={() => {
|
||||
setIsImportModalOpen(false);
|
||||
setEditingEdition(null);
|
||||
setInitialPath("");
|
||||
}}
|
||||
onImport={(ed: any) => {
|
||||
if (editingEdition) {
|
||||
@@ -567,6 +601,7 @@ const VersionsView = memo(function VersionsView() {
|
||||
playPressSound={playPressSound}
|
||||
playBackSound={playBackSound}
|
||||
editingEdition={editingEdition}
|
||||
initialPath={initialPath}
|
||||
/>
|
||||
|
||||
{deleteConfirmEdition && (
|
||||
|
||||
@@ -218,7 +218,7 @@ export function useGameManager({
|
||||
}, [profile]);
|
||||
|
||||
const addCustomEdition = useCallback(
|
||||
(edition: { name: string; desc: string; url: string }) => {
|
||||
(edition: { name: string; desc: string; url: string; path?: string }) => {
|
||||
const id = `custom_${Date.now()}`;
|
||||
const newEdition = {
|
||||
...edition,
|
||||
@@ -240,7 +240,7 @@ export function useGameManager({
|
||||
);
|
||||
|
||||
const updateCustomEdition = useCallback(
|
||||
(id: string, updated: { name: string; desc: string; url: string }) => {
|
||||
(id: string, updated: { name: string; desc: string; url: string; path?: string }) => {
|
||||
setCustomEditions(
|
||||
customEditions.map((e) => (e.id === id ? { ...e, ...updated } : e)),
|
||||
);
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface CustomEdition {
|
||||
name: string;
|
||||
desc: string;
|
||||
url: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
@@ -198,4 +199,8 @@ export class TauriService {
|
||||
static async checkGameUpdate(instanceId: string, url: string): Promise<boolean> {
|
||||
return invoke("check_game_update", { instanceId, url });
|
||||
}
|
||||
|
||||
static async pickFolder(): Promise<string> {
|
||||
return invoke("pick_folder");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user