From c49f62e5392e03b32909807e6c20fd1d09577770 Mon Sep 17 00:00:00 2001 From: neoapps-dev Date: Sun, 12 Apr 2026 18:31:57 +0300 Subject: [PATCH] feat: screenshots tab! --- public/images/lmrp.png | Bin 8020 -> 0 bytes src-tauri/Cargo.lock | 171 +++++++++++++++++- src-tauri/Cargo.toml | 4 +- src-tauri/capabilities/default.json | 54 +++--- src-tauri/gen/schemas/capabilities.json | 2 +- src-tauri/src/lib.rs | 72 +++++++- src-tauri/tauri.conf.json | 2 +- src/components/common/SkinViewer.tsx | 23 ++- src/components/views/HomeView.tsx | 56 +++--- src/components/views/ScreenshotsView.tsx | 218 +++++++++++++++++++++++ src/pages/App.tsx | 4 + src/services/ScreenshotService.ts | 17 ++ 12 files changed, 561 insertions(+), 62 deletions(-) delete mode 100644 public/images/lmrp.png create mode 100644 src/components/views/ScreenshotsView.tsx create mode 100644 src/services/ScreenshotService.ts diff --git a/public/images/lmrp.png b/public/images/lmrp.png deleted file mode 100644 index 09a57edd535823e7a43db162345684102263e50f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8020 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 WKbJ+&6EqtM6b)SDW>> } pub struct GameState { pub child: Arc>> } @@ -1295,6 +1304,46 @@ async fn fetch_skin(username: String) -> Result<(String, String), String> { Ok((image_b64.to_string(), name_exact)) } +#[tauri::command] +fn get_screenshots(app: AppHandle) -> Vec { + let mut screenshots = Vec::new(); + let instances_dir = get_app_dir(&app).join("instances"); + let _ = fs::create_dir_all(&instances_dir); + if let Ok(entries) = fs::read_dir(instances_dir) { + for entry in entries.flatten() { + if entry.path().is_dir() { + let instance_id = entry.file_name().to_string_lossy().to_string(); + let screenshots_dir = entry.path().join("screenshots"); + if let Ok(files) = fs::read_dir(screenshots_dir) { + for file in files.flatten() { + let path = file.path(); + if path.extension().and_then(|s| s.to_str()) == Some("png") { + let name = path.file_name().unwrap_or_default().to_string_lossy().to_string(); + let date = path.metadata().and_then(|m| m.modified()).ok() + .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) + .map(|d| d.as_secs()) + .unwrap_or(0); + screenshots.push(ScreenshotInfo { + path: path.to_string_lossy().to_string(), + instance_id: instance_id.clone(), + name, + date, + }); + } + } + } + } + } + } + screenshots.sort_by(|a, b| b.date.cmp(&a.date)); + screenshots +} + +#[tauri::command] +fn delete_screenshot(path: String) -> Result<(), String> { + fs::remove_file(path).map_err(|e| e.to_string()) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() @@ -1303,7 +1352,28 @@ 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, sync_dlc, fetch_skin, workshop_install]) + .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 path = std::path::Path::new(&*decoded_path); + + match std::fs::read(path) { + Ok(data) => { + tauri::http::Response::builder() + .header("Content-Type", "image/png") + .header("Access-Control-Allow-Origin", "*") + .body(data) + .unwrap() + } + Err(_) => { + tauri::http::Response::builder() + .status(404) + .body(Vec::new()) + .unwrap() + } + } + }) + .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, get_screenshots, delete_screenshot]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 4772313..924085e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -26,7 +26,7 @@ } ], "security": { - "csp": null + "csp": "default-src 'self'; img-src 'self' screenshots: asset: https://asset.localhost http://asset.localhost; style-src 'self' 'unsafe-inline'; font-src 'self'; connect-src 'self' https://* http://* screenshots: asset:; script-src 'self' 'unsafe-inline' 'unsafe-eval';" } }, "bundle": { diff --git a/src/components/common/SkinViewer.tsx b/src/components/common/SkinViewer.tsx index 71c8235..fe924f5 100644 --- a/src/components/common/SkinViewer.tsx +++ b/src/components/common/SkinViewer.tsx @@ -211,16 +211,16 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo if (e.key === 'ArrowRight') { if (legacyMode) onNavigateRight(); - else if (focusIndex === 3) onNavigateRight(); - else if (focusIndex === 1 || focusIndex === 2) setFocusIndex(prev => prev + 1); + else if (focusIndex === 4) onNavigateRight(); + else if (focusIndex >= 1 && focusIndex < 4) setFocusIndex(prev => prev + 1); } else if (e.key === 'ArrowLeft') { if (legacyMode) return; - if (focusIndex === 2 || focusIndex === 3) setFocusIndex(prev => prev - 1); + if (focusIndex > 1 && focusIndex <= 4) setFocusIndex(prev => prev - 1); } else if (e.key === 'ArrowDown') { if (legacyMode) { - return; + setFocusIndex(prev => (prev < 4 ? prev + 1 : prev)); } else { - setFocusIndex(prev => (prev < 3 ? prev + 1 : prev)); + setFocusIndex(prev => (prev < 4 ? prev + 1 : prev)); } } else if (e.key === 'ArrowUp') { if (legacyMode) { @@ -240,6 +240,9 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo } else if (focusIndex === 3) { playPressSound(); setSkinUrl('/images/Default.png'); + } else if (focusIndex === 4) { + playPressSound(); + setActiveView('screenshots'); } } }; @@ -320,6 +323,16 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo Delete )} + ); diff --git a/src/components/views/HomeView.tsx b/src/components/views/HomeView.tsx index 96f263d..1d3bd25 100644 --- a/src/components/views/HomeView.tsx +++ b/src/components/views/HomeView.tsx @@ -47,7 +47,7 @@ const HomeView = memo(function HomeView() { : isGameRunning ? stopGame : isDownloading - ? () => {} + ? () => { } : isInstalled ? handleLaunch : () => toggleInstall(profile), @@ -105,35 +105,37 @@ const HomeView = memo(function HomeView() { animate={{ opacity: isFocusedSection ? 1 : 0.5, y: 0 }} exit={{ opacity: 0, y: 10 }} transition={{ duration: useConfig().animationsEnabled ? 0.3 : 0 }} - className="w-full max-w-[540px] flex flex-col space-y-3 outline-none" + className="relative w-full max-w-[540px] flex flex-col space-y-3 outline-none" > {buttons.map((btn: any, i: number) => ( - +

+ +
))} + {!legacyMode && (
diff --git a/src/components/views/ScreenshotsView.tsx b/src/components/views/ScreenshotsView.tsx new file mode 100644 index 0000000..b4eaf94 --- /dev/null +++ b/src/components/views/ScreenshotsView.tsx @@ -0,0 +1,218 @@ +import { useState, useEffect, memo } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { useUI, useAudio, useGame, useConfig } from "../../context/LauncherContext"; +import { ScreenshotService, ScreenshotInfo } from "../../services/ScreenshotService"; +const ScreenshotsView = memo(function ScreenshotsView() { + const { setActiveView } = useUI(); + const { playPressSound, playBackSound } = useAudio(); + const { editions } = useGame(); + const { animationsEnabled } = useConfig(); + const [screenshots, setScreenshots] = useState([]); + const [selectedScreenshot, setSelectedScreenshot] = useState(null); + const [loading, setLoading] = useState(true); + const [gridFocusIndex, setGridFocusIndex] = useState(0); + const [modalFocusIndex, setModalFocusIndex] = useState(0); + + useEffect(() => { + ScreenshotService.getScreenshots().then((data) => { + setScreenshots(data); + setLoading(false); + }); + }, []); + + const handleBack = () => { + playBackSound(); + setActiveView("main"); + }; + + const handleDelete = async (screenshot: ScreenshotInfo) => { + if (confirm("Are you sure you want to delete this screenshot?")) { + playPressSound(); + await ScreenshotService.deleteScreenshot(screenshot.path); + setScreenshots((prev) => prev.filter((s) => s.path !== screenshot.path)); + setSelectedScreenshot(null); + } + }; + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (loading) return; + + if (selectedScreenshot) { + if (e.key === "Escape") { + playBackSound(); + setSelectedScreenshot(null); + } else if (e.key === "ArrowLeft") { + setModalFocusIndex((prev) => (prev > 0 ? prev - 1 : prev)); + } else if (e.key === "ArrowRight") { + setModalFocusIndex((prev) => (prev < 2 ? prev + 1 : prev)); + } else if (e.key === "Enter") { + if (modalFocusIndex === 0) handleDelete(selectedScreenshot); + else if (modalFocusIndex === 1) { + playBackSound(); + setSelectedScreenshot(null); + } + } + return; + } + + const cols = window.innerWidth >= 1024 ? 4 : window.innerWidth >= 768 ? 3 : 2; + + if (e.key === "Escape") { + handleBack(); + } else if (e.key === "ArrowLeft") { + setGridFocusIndex((prev) => (prev > 0 ? prev - 1 : prev)); + } else if (e.key === "ArrowRight") { + setGridFocusIndex((prev) => (prev < screenshots.length - 1 ? prev + 1 : prev)); + } else if (e.key === "ArrowUp") { + setGridFocusIndex((prev) => (prev >= cols ? prev - cols : prev)); + } else if (e.key === "ArrowDown") { + setGridFocusIndex((prev) => (prev <= screenshots.length - 1 - cols ? prev + cols : prev)); + } else if (e.key === "Enter") { + if (screenshots[gridFocusIndex]) { + playPressSound(); + setModalFocusIndex(0); + setSelectedScreenshot(screenshots[gridFocusIndex]); + } + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [loading, selectedScreenshot, gridFocusIndex, modalFocusIndex, screenshots]); + + useEffect(() => { + if (!selectedScreenshot) { + const element = document.getElementById(`ss-${gridFocusIndex}`); + element?.scrollIntoView({ block: "nearest", behavior: "smooth" }); + } + }, [gridFocusIndex, selectedScreenshot]); + + const getEditionLogo = (instanceId: string) => { + const edition = editions.find((e: any) => e.id === instanceId); + return edition?.logo || edition?.titleImage; + }; + + return ( + +
+

+ Screenshots +

+ +
+ +
+ {loading ? ( +
+ Scanning for screenshots... +
+ ) : screenshots.length === 0 ? ( +
+ No screenshots found. + Take some in-game with F2! +
+ ) : ( +
+ {screenshots.map((ss, index) => ( + { + setGridFocusIndex(index); + setSelectedScreenshot(ss); + }} + className={`relative aspect-video bg-black/40 border-2 transition-all cursor-pointer overflow-hidden group shadow-lg ${gridFocusIndex === index ? "border-[#FFFF55] scale-105" : "border-transparent"}`} + > + {ss.name} { + (e.target as HTMLImageElement).src = "/images/Folder_Icon.png"; + }} + /> +
+ {getEditionLogo(ss.instanceId) && ( + + )} +
+ {new Date(ss.date * 1000).toLocaleDateString()} +
+ + ))} +
+ )} +
+ + + {selectedScreenshot && ( + setSelectedScreenshot(null)} + > + e.stopPropagation()} + > + { + (e.target as HTMLImageElement).src = "/images/Pack_Icon.png"; + }} + /> + +
+ + +
+ +
+ {selectedScreenshot.name} - {new Date(selectedScreenshot.date * 1000).toLocaleString()} +
+
+
+ )} +
+ + ); +}); + +export default ScreenshotsView; diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 250d334..432e66c 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -9,6 +9,7 @@ import WorkshopView from "../components/views/WorkshopView"; import SetupView from "../components/views/SetupView"; import PckEditorView from "../components/views/PckEditorView"; import { ArcEditorView } from "../components/views/ArcEditorView"; +import ScreenshotsView from "../components/views/ScreenshotsView"; import SkinViewer from "../components/common/SkinViewer"; import TeamModal from "../components/modals/TeamModal"; import PanoramaBackground from "../components/common/PanoramaBackground"; @@ -348,6 +349,9 @@ export default function App() { )} {activeView === "skins" && } + {activeView === "screenshots" && ( + + )}
diff --git a/src/services/ScreenshotService.ts b/src/services/ScreenshotService.ts new file mode 100644 index 0000000..8ef405d --- /dev/null +++ b/src/services/ScreenshotService.ts @@ -0,0 +1,17 @@ +import { invoke } from "@tauri-apps/api/core"; +export interface ScreenshotInfo { + path: string; + instanceId: string; + name: string; + date: number; +} + +export class ScreenshotService { + static async getScreenshots(): Promise { + return invoke("get_screenshots"); + } + + static async deleteScreenshot(path: string): Promise { + return invoke("delete_screenshot", { path }); + } +}