Use claude to fix bugs & add a custom data location setting

This commit is contained in:
Lncvrt committed 2026-04-14 15:58:54 -07:00
1 parent 33a1e4e780
commit c7086fbc31
8 files changed
+242 -59

No files matched your search

+1
View File
@@ -21,6 +21,7 @@
"decorum:allow-show-snap-overlay",
"fs:default",
"dialog:default",
"dialog:allow-open",
"notification:default",
"clipboard-manager:default",
"clipboard-manager:allow-write-text",
+164 -17
View File
@@ -6,7 +6,7 @@ use openssl::sign::Verifier;
use sha2::{Digest, Sha512};
use std::fs;
use std::path::Path;
use std::sync::OnceLock;
use std::sync::{OnceLock, RwLock};
use std::time::Instant;
use std::{
fs::{File, create_dir_all},
@@ -33,6 +33,19 @@ use std::os::unix::fs::PermissionsExt;
#[cfg(not(debug_assertions))]
use tauri_plugin_updater::UpdaterExt;
struct AppState {
custom_data_dir: RwLock<Option<PathBuf>>,
}
fn data_dir(app: &AppHandle) -> Result<PathBuf, String> {
let state = app.state::<AppState>();
let guard = state.custom_data_dir.read().unwrap();
match &*guard {
Some(p) => Ok(p.clone()),
None => app.path().app_local_data_dir().map_err(|e| e.to_string()),
}
}
static CANCEL_MAP: OnceLock<DashMap<String, bool>> = OnceLock::new();
fn cancel_map() -> &'static DashMap<String, bool> {
@@ -120,7 +133,7 @@ async fn unzip_to_dir(app: AppHandle, zip_path: PathBuf, out_dir: PathBuf, name:
#[tauri::command]
fn folder_size(app: AppHandle, version: String) -> String {
let path = match app.path().app_local_data_dir() {
let path = match data_dir(&app) {
Ok(p) => p.join("game").join(&version),
Err(_) => return "-1".to_string(),
};
@@ -160,20 +173,15 @@ async fn download(
let _ = app.emit("download-start", format!("{}", name));
let window = app.get_webview_window("main").expect("main window missing");
let downloads_path = match app.path().app_local_data_dir() {
Ok(p) => p.join("downloads"),
Err(_) => {
let _ = &app.emit("download-stop", format!("{}", name));
return "-1".to_string();
}
};
let game_path = match app.path().app_local_data_dir() {
Ok(p) => p.join("game"),
let base_dir = match data_dir(&app) {
Ok(p) => p,
Err(_) => {
let _ = &app.emit("download-stop", format!("{}", name));
return "-1".to_string();
}
};
let downloads_path = base_dir.join("downloads");
let game_path = base_dir.join("game");
let download_part_path = downloads_path.join(format!("{}.part", name));
let download_zip_path = downloads_path.join(format!("{}.zip", name));
@@ -414,7 +422,7 @@ fn launch_game(
use_wine: bool,
wine_command: String,
) {
let game_folder = match app.path().app_local_data_dir() {
let game_folder = match data_dir(&app) {
Ok(p) => p.join("game").join(&name),
Err(_) => return,
};
@@ -473,10 +481,7 @@ fn launch_game(
}
}
let bepinex_folder = match app.path().app_local_data_dir() {
Ok(p) => p.join("game").join(&name).join("BepInEx"),
Err(_) => return,
};
let bepinex_folder = game_folder.join("BepInEx");
if platform() == "macos" && !bepinex_folder.exists() && executable.ends_with(".app") {
if let Err(_) = Command::new("open")
@@ -530,6 +535,125 @@ async fn cancel_download(name: String) {
cancel_map().insert(name.clone(), true);
}
fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if src_path.is_dir() {
copy_dir_recursive(&src_path, &dst_path)?;
} else {
fs::copy(&src_path, &dst_path)?;
}
}
Ok(())
}
fn move_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
if !src.exists() {
return Ok(());
}
if let Some(parent) = dst.parent() {
fs::create_dir_all(parent)?;
}
if dst.exists() {
fs::remove_dir_all(dst)?;
}
match fs::rename(src, dst) {
Ok(()) => Ok(()),
Err(_) => {
copy_dir_recursive(src, dst)?;
fs::remove_dir_all(src)?;
Ok(())
}
}
}
#[tauri::command]
async fn move_game_data(app: AppHandle, destination: String) -> Result<(), String> {
let default_dir = app.path().app_local_data_dir().map_err(|e| e.to_string())?;
let current_dir = data_dir(&app)?;
let new_dir = if destination.is_empty() {
default_dir.clone()
} else {
PathBuf::from(&destination)
};
if current_dir == new_dir {
return Ok(());
}
let src_game = current_dir.join("game");
let dst_game = new_dir.join("game");
let src_downloads = current_dir.join("downloads");
let dst_downloads = new_dir.join("downloads");
tauri::async_runtime::spawn_blocking(move || {
fs::create_dir_all(&new_dir).map_err(|e| e.to_string())?;
move_dir(&src_game, &dst_game).map_err(|e| e.to_string())?;
move_dir(&src_downloads, &dst_downloads).map_err(|e| e.to_string())?;
Ok::<(), String>(())
})
.await
.map_err(|e| e.to_string())??;
let state = app.state::<AppState>();
let mut guard = state.custom_data_dir.write().unwrap();
if destination.is_empty() {
*guard = None;
} else {
*guard = Some(PathBuf::from(&destination));
}
Ok(())
}
#[tauri::command]
fn restart_app(app: AppHandle) {
app.restart();
}
#[tauri::command]
fn open_game_folder(app: AppHandle, version: String) -> Result<(), String> {
let dir = data_dir(&app)?;
let path = dir.join("game").join(&version);
if !path.exists() || !path.is_dir() {
return Err(format!("Game folder \"{}\" not found.", path.display()));
}
#[cfg(target_os = "macos")]
Command::new("open")
.arg(&path)
.spawn()
.map_err(|e| e.to_string())?;
#[cfg(target_os = "windows")]
Command::new("explorer")
.arg(&path)
.spawn()
.map_err(|e| e.to_string())?;
#[cfg(target_os = "linux")]
Command::new("xdg-open")
.arg(&path)
.spawn()
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
fn remove_stale_executable(app: AppHandle, version: String, executable: String) -> bool {
let dir = match data_dir(&app) {
Ok(d) => d,
Err(_) => return false,
};
let path = dir.join("game").join(&version).join(&executable);
if path.exists() {
fs::remove_file(&path).is_ok()
} else {
false
}
}
#[tauri::command]
fn open_new_window(
app: tauri::AppHandle,
@@ -597,9 +721,32 @@ pub fn run() {
folder_size,
verify_signature,
cancel_download,
open_new_window
open_new_window,
move_game_data,
restart_app,
open_game_folder,
remove_stale_executable
])
.setup(|app| {
let custom_dir = app
.path()
.app_config_dir()
.ok()
.map(|d| d.join("settings.json"))
.filter(|p| p.exists())
.and_then(|p| fs::read_to_string(p).ok())
.and_then(|c| serde_json::from_str::<serde_json::Value>(&c).ok())
.and_then(|v| {
v.get("customDataLocation")
.and_then(|s| s.as_str())
.filter(|s| !s.is_empty())
.map(PathBuf::from)
});
app.manage(AppState {
custom_data_dir: RwLock::new(custom_dir),
});
let window = app.get_webview_window("main").unwrap();
#[cfg(target_os = "windows")]
+20 -11
View File
@@ -89,11 +89,13 @@ export default function RootLayout ({
const [linuxWineCommand, setLinuxWineCommand] =
useState<string>('wine %path%')
const [theme, setTheme] = useState<string>('dark')
const [customDataLocation, setCustomDataLocation] = useState<string>('')
const [versionsList, setVersionsList] = useState<Record<string, number>>({})
const [modsList, setModsList] = useState<
Record<string, Record<string, number>>
>({})
const [movingData, setMovingData] = useState(false)
function getSpecialVersionsList (game?: number): GameVersion[] {
if (!serverVersionList) return []
@@ -117,7 +119,7 @@ export default function RootLayout ({
}
const closePopup = useCallback(() => {
if (popupMode == 0 && selectedGame && pathname === '/') {
if (popupMode == 0 && selectedGame && pathname === '/main') {
setSelectedGame(null)
setSelectedVersionList([])
} else if (viewingInfoFromDownloads) {
@@ -312,7 +314,8 @@ export default function RootLayout ({
sidebarAlwaysShowGames: true,
linuxUseWine: false,
linuxWineCommand: 'wine %path%',
theme: 'dark'
theme: 'dark',
customDataLocation: ''
}
})
const versionsLocal = await load('versions.json', {
@@ -623,14 +626,9 @@ export default function RootLayout ({
verInfo.lastRevision > 0 &&
value / 1000 <= verInfo.lastRevision
) {
if (
await exists('game/' + key + '/' + verInfo.executable, {
baseDir: BaseDirectory.AppLocalData
})
)
await remove('game/' + key + '/' + verInfo.executable, {
baseDir: BaseDirectory.AppLocalData,
recursive: true
await invoke('remove_stale_executable', {
version: key,
executable: verInfo.executable
})
}
}
@@ -688,6 +686,9 @@ export default function RootLayout ({
watchSettings<string>('linuxWineCommand', v =>
setLinuxWineCommand(v ?? 'wine %path%')
)
watchSettings<string>('customDataLocation', v =>
setCustomDataLocation(v ?? '')
)
watchVersions<Record<string, number>>('list', v => setVersionsList(v ?? {}))
watchVersions<Record<string, Record<string, number>>>('mods', v =>
@@ -756,8 +757,11 @@ export default function RootLayout ({
linuxUseWine,
linuxWineCommand,
theme,
customDataLocation,
versionsList,
modsList
modsList,
movingData,
setMovingData
}}
>
<div
@@ -825,6 +829,11 @@ export default function RootLayout ({
</div>
</div>
)}
{movingData && (
<div className='fixed inset-0 z-999999 bg-(--col0) flex items-center justify-center'>
<p className='text-5xl text-center'>Moving data...</p>
</div>
)}
</div>
</GlobalProvider>
)}
+45 -1
View File
@@ -4,6 +4,8 @@ import { Setting } from '@/components/Setting'
import { useGlobal } from '@/providers/GlobalProvider'
import { copyToClipboard } from '@/lib/clipboard'
import { platform } from '@tauri-apps/plugin-os'
import { open } from '@tauri-apps/plugin-dialog'
import { invoke } from '@tauri-apps/api/core'
import Dropdown from '@/components/Dropdown'
export default function Settings () {
@@ -14,7 +16,10 @@ export default function Settings () {
sidebarAlwaysShowGames,
linuxUseWine,
linuxWineCommand,
theme
theme,
customDataLocation,
downloadProgress,
setMovingData
} = useGlobal()
return (
@@ -58,6 +63,45 @@ export default function Settings () {
className='input-field my-1'
hidden={!(platform() == 'linux' && linuxUseWine)}
></input>
<Setting
label='Use custom data location'
value={!!customDataLocation}
disabled={downloadProgress.length > 0}
onChange={async () => {
if (customDataLocation) {
try {
setMovingData(true)
await invoke('move_game_data', { destination: '' })
await settings?.set('customDataLocation', '')
await settings?.save()
await invoke('restart_app')
} catch {
setMovingData(false)
}
} else {
const selected = await open({
directory: true,
title: 'Select data location'
})
if (!selected || typeof selected !== 'string') return
try {
setMovingData(true)
await invoke('move_game_data', { destination: selected })
await settings?.set('customDataLocation', selected)
await settings?.save()
await invoke('restart_app')
} catch {
setMovingData(false)
}
}
}}
title='Move game data to a custom folder location. Disabled during downloads.'
/>
{customDataLocation && (
<p className='text-sm opacity-50 ml-6 -mt-1 mb-2'>
{customDataLocation}
</p>
)}
<div className='flex flex-row gap-2 items-center'>
<p
title='The theme you want the launcher to use.'
+4 -2
View File
@@ -8,16 +8,18 @@ export function Setting ({
value,
onChange,
className,
title
title,
disabled
}: SettingProps) {
return (
<div className={`flex items-center gap-2 mb-2 ${className}`} title={title}>
<div className={`flex items-center gap-2 mb-2 ${disabled ? 'opacity-50 pointer-events-none' : ''} ${className}`} title={title}>
<label className='text-white text-lg'>{label}</label>
<div className='setting-checkbox-wrapper'>
<input
type='checkbox'
className='setting-checkbox'
checked={value}
disabled={disabled}
onChange={() => onChange(!value)}
/>
{value && <FontAwesomeIcon icon={faCheck} className='fa-check-icon' />}
+4 -28
View File
@@ -1,39 +1,15 @@
import { message } from '@tauri-apps/plugin-dialog'
import { BaseDirectory, exists, stat } from '@tauri-apps/plugin-fs'
import { openPath } from '@tauri-apps/plugin-opener'
import { join, appLocalDataDir } from '@tauri-apps/api/path'
import { invoke } from '@tauri-apps/api/core'
export const openFolder = async (name: string) => {
const relativePath = await join('game', name)
const basePath = await appLocalDataDir()
const absolutePath = await join(basePath, relativePath)
const folderExists = await exists(relativePath, {
baseDir: BaseDirectory.AppLocalData
})
if (!folderExists) {
await message(`Game folder "${absolutePath}" not found.`, {
try {
await invoke('open_game_folder', { version: name })
} catch (e) {
await message(String(e), {
title: 'Folder not found',
kind: 'error'
})
return
}
const folderStat = await stat(relativePath, {
baseDir: BaseDirectory.AppLocalData
})
if (folderStat.isFile) {
await message(`Game folder "${absolutePath}" not found.`, {
title: 'Folder not found',
kind: 'error'
})
return
}
await openPath(absolutePath)
}
export const formatEtaSmart = (seconds: number) => {
+3
View File
@@ -57,8 +57,11 @@ type GlobalCtxType = {
linuxUseWine: boolean
linuxWineCommand: string
theme: string
customDataLocation: string
versionsList: Record<string, number>
modsList: Record<string, Record<string, number>>
movingData: boolean
setMovingData: Dispatch<SetStateAction<boolean>>
}
const GlobalCtx = createContext<GlobalCtxType | null>(null)
+1
View File
@@ -4,4 +4,5 @@ export type SettingProps = {
onChange: (val: boolean) => void
className?: string
title?: string
disabled?: boolean
}