Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fde145f071
|
||
|
|
846fcc0af7
|
||
|
|
4601628a55
|
||
|
|
3a9e0b0358
|
||
|
|
c45e7fb47c
|
||
|
|
c7086fbc31
|
||
|
|
33a1e4e780
|
No files matched your search
@@ -37,13 +37,13 @@ tauri-plugin-http = "2.5.8"
|
||||
dashmap = "6.1.0"
|
||||
tauri-plugin-store = "2.4.2"
|
||||
hex = "0.4.3"
|
||||
window-vibrancy = "0.7.1"
|
||||
|
||||
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
|
||||
sysinfo = "0.38.4"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-single-instance = "2.4.1"
|
||||
tauri-plugin-updater = "2.10.1"
|
||||
tauri-plugin-window-state = "2.4.1"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
{
|
||||
"identifier": "desktop-capability",
|
||||
"platforms": [
|
||||
"macOS",
|
||||
"windows",
|
||||
"linux"
|
||||
],
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"window-state:default",
|
||||
"updater:default"
|
||||
]
|
||||
"platforms": ["macOS", "windows", "linux"],
|
||||
"windows": ["main"],
|
||||
"permissions": ["window-state:default"]
|
||||
}
|
||||
+169
-94
@@ -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},
|
||||
@@ -21,6 +21,8 @@ use tauri_plugin_os::platform;
|
||||
use tauri_plugin_prevent_default::Flags;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::{io::AsyncWriteExt, time::timeout};
|
||||
#[cfg(target_os = "macos")]
|
||||
use window_vibrancy::{NSVisualEffectMaterial, apply_vibrancy};
|
||||
use zip::ZipArchive;
|
||||
use zip::result::ZipError;
|
||||
|
||||
@@ -30,8 +32,18 @@ use sysinfo::System;
|
||||
#[cfg(unix)]
|
||||
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();
|
||||
|
||||
@@ -120,7 +132,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 +172,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 +421,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 +480,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 +534,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,
|
||||
@@ -559,7 +682,6 @@ fn open_new_window(
|
||||
pub fn run() {
|
||||
#[allow(unused_variables)]
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(
|
||||
@@ -597,11 +719,38 @@ 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 = "macos")]
|
||||
apply_vibrancy(&window, NSVisualEffectMaterial::HudWindow, None, None)
|
||||
.expect("Unable to apply window blur");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use tauri_plugin_decorum::WebviewWindowExt;
|
||||
@@ -609,83 +758,9 @@ pub fn run() {
|
||||
eprintln!("Failed to create overlay titlebar: {:?}", e);
|
||||
}
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
let handle = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
update(handle, window.clone()).await.unwrap();
|
||||
});
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let mut new_url = app.config().build.dev_url.clone().unwrap();
|
||||
new_url.set_path("/main");
|
||||
window.navigate(new_url).unwrap();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
async fn update(
|
||||
app: tauri::AppHandle,
|
||||
window: tauri::WebviewWindow,
|
||||
) -> tauri_plugin_updater::Result<()> {
|
||||
let target = {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if std::path::Path::new("/etc/debian_version").exists() {
|
||||
"linux-debian"
|
||||
} else if std::path::Path::new("/etc/redhat-release").exists() {
|
||||
"linux-fedora"
|
||||
} else {
|
||||
let mut new_url = window.url().unwrap();
|
||||
new_url.set_path("/update/outdated");
|
||||
window.navigate(new_url).unwrap();
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
tauri_plugin_updater::target().unwrap_or("unknown".to_string())
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(update) = app
|
||||
.updater_builder()
|
||||
.target(target)
|
||||
.build()?
|
||||
.check()
|
||||
.await?
|
||||
{
|
||||
let mut new_url = window.url().unwrap();
|
||||
new_url.set_path("/update/updating");
|
||||
window.navigate(new_url).unwrap();
|
||||
|
||||
let mut downloaded = 0;
|
||||
|
||||
update
|
||||
.download_and_install(
|
||||
|chunk_length, content_length| {
|
||||
downloaded += chunk_length;
|
||||
println!("downloaded {downloaded} from {content_length:?}");
|
||||
},
|
||||
|| {
|
||||
println!("download finished");
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("update installed");
|
||||
app.restart();
|
||||
}
|
||||
|
||||
let mut new_url = window.url().unwrap();
|
||||
new_url.set_path("/main");
|
||||
window.navigate(new_url).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
"windows": [
|
||||
{
|
||||
"title": "Lncvrt Games Launcher",
|
||||
"url": "/update/checking",
|
||||
"url": "/main",
|
||||
"width": 1000,
|
||||
"height": 600,
|
||||
"minWidth": 1000,
|
||||
@@ -27,6 +27,8 @@
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"category": "Game",
|
||||
"shortDescription": "A launcher for downloading, managing, and launching games with built-in mod support.",
|
||||
"longDescription": "Lncvrt Games Launcher lets you download and manage multiple versions of games, install mods, and launch them directly. Linux users can run Windows exclusive games via Wine (or proton via a custom command).",
|
||||
"targets": ["deb", "rpm"],
|
||||
"icon": ["icons/icon.png"],
|
||||
"createUpdaterArtifacts": true
|
||||
|
||||
@@ -14,13 +14,14 @@
|
||||
"windows": [
|
||||
{
|
||||
"title": "Lncvrt Games Launcher",
|
||||
"url": "/update/checking",
|
||||
"url": "/main",
|
||||
"width": 1000,
|
||||
"height": 600,
|
||||
"minWidth": 1000,
|
||||
"minHeight": 500,
|
||||
"titleBarStyle": "Overlay",
|
||||
"hiddenTitle": true
|
||||
"hiddenTitle": true,
|
||||
"transparent": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
@@ -30,6 +31,8 @@
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"category": "Game",
|
||||
"shortDescription": "A launcher for downloading, managing, and launching games with built-in mod support.",
|
||||
"longDescription": "Lncvrt Games Launcher lets you download and manage multiple versions of games, install mods, and launch them directly. Linux users can run Windows exclusive games via Wine (or proton via a custom command).",
|
||||
"targets": ["dmg", "app"],
|
||||
"icon": ["icons/icon.icns"],
|
||||
"createUpdaterArtifacts": true,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"windows": [
|
||||
{
|
||||
"title": "Lncvrt Games Launcher",
|
||||
"url": "/update/checking",
|
||||
"url": "/main",
|
||||
"width": 1000,
|
||||
"height": 632,
|
||||
"minWidth": 1000,
|
||||
@@ -30,6 +30,8 @@
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"category": "Game",
|
||||
"shortDescription": "A launcher for downloading, managing, and launching games with built-in mod support.",
|
||||
"longDescription": "Lncvrt Games Launcher lets you download and manage multiple versions of games, install mods, and launch them directly. Linux users can run Windows exclusive games via Wine (or proton via a custom command).",
|
||||
"targets": ["msi"],
|
||||
"icon": ["icons/icon.ico"],
|
||||
"createUpdaterArtifacts": true
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
}
|
||||
|
||||
.downloads-scroll {
|
||||
@apply bg-(--col1) border border-(--col3) rounded-lg overflow-y-auto w-full;
|
||||
@apply bg-(--col1)/50 border border-(--col3)/50 rounded-lg overflow-y-auto w-full;
|
||||
}
|
||||
|
||||
.downloads-entry {
|
||||
@apply flex justify-between items-center m-2 p-2 rounded-lg text-gray-200 text-lg transition-colors bg-(--col2) hover:bg-(--col3) border border-(--col4) hover:border-(--col5) cursor-pointer;
|
||||
@apply flex justify-between items-center m-2 p-2 rounded-lg text-gray-200 text-lg transition-colors bg-(--col2)/50 hover:bg-(--col3)/50 border border-(--col4)/50 hover:border-(--col5)/50 cursor-pointer;
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
.box {
|
||||
@apply bg-(--col1) border border-(--col3) rounded-lg w-auto p-1;
|
||||
@apply bg-(--col1)/50 border border-(--col3)/50 rounded-lg w-auto p-1;
|
||||
}
|
||||
|
||||
.leaderboard-button {
|
||||
@apply bg-(--col2) hover:bg-(--col4) border border-(--col4) hover:border-(--col6) rounded-lg px-4 py-2 inline-block transition-all duration-200 cursor-pointer;
|
||||
@apply bg-(--col2)/50 hover:bg-(--col4)/50 border border-(--col4)/50 hover:border-(--col6)/50 rounded-lg px-4 py-2 inline-block transition-all duration-200 cursor-pointer;
|
||||
}
|
||||
|
||||
.leaderboard-entry {
|
||||
@apply rounded-lg text-gray-200 text-lg px-1 py-0.5 transition-colors bg-(--col2) hover:bg-(--col3) border border-(--col4) hover:border-(--col5) cursor-pointer;
|
||||
@apply rounded-lg text-gray-200 text-lg px-1 py-0.5 transition-colors bg-(--col2)/50 hover:bg-(--col3)/50 border border-(--col4)/50 hover:border-(--col5)/50 cursor-pointer;
|
||||
}
|
||||
+22
-16
@@ -88,12 +88,13 @@ export default function RootLayout ({
|
||||
const [linuxUseWine, setLinuxUseWine] = useState<boolean>(false)
|
||||
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 +118,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 +313,8 @@ export default function RootLayout ({
|
||||
sidebarAlwaysShowGames: true,
|
||||
linuxUseWine: false,
|
||||
linuxWineCommand: 'wine %path%',
|
||||
theme: 'dark'
|
||||
theme: 'dark',
|
||||
customDataLocation: ''
|
||||
}
|
||||
})
|
||||
const versionsLocal = await load('versions.json', {
|
||||
@@ -623,15 +625,10 @@ 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
|
||||
})
|
||||
}
|
||||
}
|
||||
})()
|
||||
@@ -677,7 +674,6 @@ export default function RootLayout ({
|
||||
unlisteners.push(versions!.onKeyChange<T>(key, fn))
|
||||
}
|
||||
|
||||
watchSettings<string>('theme', v => setTheme(v ?? 'dark'))
|
||||
watchSettings<boolean>('notificationsAllowed', v =>
|
||||
setNotificationsAllowed(v ?? true)
|
||||
)
|
||||
@@ -688,6 +684,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 =>
|
||||
@@ -702,7 +701,7 @@ export default function RootLayout ({
|
||||
return (
|
||||
<>
|
||||
<html lang='en' className={roboto.className}>
|
||||
<body className={theme + '-theme'}>
|
||||
<body>
|
||||
{loading ? (
|
||||
<>
|
||||
<div
|
||||
@@ -755,9 +754,11 @@ export default function RootLayout ({
|
||||
sidebarAlwaysShowGames,
|
||||
linuxUseWine,
|
||||
linuxWineCommand,
|
||||
theme,
|
||||
customDataLocation,
|
||||
versionsList,
|
||||
modsList
|
||||
modsList,
|
||||
movingData,
|
||||
setMovingData
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -825,6 +826,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>
|
||||
)}
|
||||
|
||||
@@ -4,7 +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 Dropdown from '@/components/Dropdown'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
export default function Settings () {
|
||||
const {
|
||||
@@ -14,13 +15,15 @@ export default function Settings () {
|
||||
sidebarAlwaysShowGames,
|
||||
linuxUseWine,
|
||||
linuxWineCommand,
|
||||
theme
|
||||
customDataLocation,
|
||||
downloadProgress,
|
||||
setMovingData
|
||||
} = useGlobal()
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='text-3xl ml-4 mt-4'>Settings</p>
|
||||
<div className='ml-4 mt-4 bg-(--col1) border border-(--col3) rounded-lg p-4 w-fit h-fit'>
|
||||
<div className='ml-4 mt-4 bg-(--col1)/50 border border-(--col3)/50 rounded-lg p-4 w-fit h-fit'>
|
||||
<Setting
|
||||
label='Allow sending notifications'
|
||||
value={notificationsAllowed}
|
||||
@@ -58,26 +61,45 @@ export default function Settings () {
|
||||
className='input-field my-1'
|
||||
hidden={!(platform() == 'linux' && linuxUseWine)}
|
||||
></input>
|
||||
<div className='flex flex-row gap-2 items-center'>
|
||||
<p
|
||||
title='The theme you want the launcher to use.'
|
||||
className='text-lg'
|
||||
>
|
||||
Theme:
|
||||
<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>
|
||||
<Dropdown
|
||||
value={theme}
|
||||
options={[
|
||||
{ label: 'Dark', value: 'dark' },
|
||||
{ label: 'Red', value: 'red' },
|
||||
{ label: 'Blue', value: 'blue' },
|
||||
{ label: 'Purple', value: 'purple' }
|
||||
]}
|
||||
onChange={async val => {
|
||||
await settings?.set('theme', val)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p
|
||||
className='fixed bottom-1.5 right-1.5 rounded-md cursor-pointer px-1 border z-100 transition-colors btntheme1'
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
'use client'
|
||||
|
||||
export default function UpdateCheckingPage () {
|
||||
return <p className='text-5xl text-center'>Checking for updates</p>
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import '@/styles/globals.css'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { Roboto } from 'next/font/google'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
const roboto = Roboto({
|
||||
subsets: ['latin']
|
||||
})
|
||||
|
||||
export default function RootLayout ({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [platformName, setPlatformName] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => setPlatformName(platform()), 0)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<html lang='en' className={roboto.className}>
|
||||
<body className='dark-theme'>
|
||||
<div
|
||||
className='relative z-2 w-screen border-b border-b-(--col3) h-8.25 bg-(--col1)'
|
||||
hidden={platformName != 'windows'}
|
||||
/>
|
||||
<div
|
||||
className={`w-screen ${
|
||||
platformName == 'windows' ? 'h-[calc(100vh-64px)]' : 'h-screen'
|
||||
} flex items-center justify-center`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
|
||||
export default function UpdateCheckingPage () {
|
||||
return (
|
||||
<div className='text-center'>
|
||||
<p className='text-6xl mb-4'>Outdated Launcher!</p>
|
||||
<p className='text-2xl mb-4'>
|
||||
Please update to the latest version to continue.
|
||||
</p>
|
||||
<button
|
||||
className='button btntheme1'
|
||||
onClick={() => openUrl('https://games.lncvrt.xyz/download')}
|
||||
>
|
||||
Download latest version!
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
'use client'
|
||||
|
||||
export default function UpdateCheckingPage () {
|
||||
return <p className='text-5xl text-center'>Updating launcher</p>
|
||||
}
|
||||
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -56,9 +56,11 @@ type GlobalCtxType = {
|
||||
sidebarAlwaysShowGames: boolean
|
||||
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)
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
}
|
||||
|
||||
.dropdown-btn {
|
||||
@apply w-full flex items-center justify-between px-3 py-1.5 rounded-md cursor-pointer
|
||||
@apply w-full flex items-center justify-between px-2 py-1 rounded-md cursor-pointer
|
||||
bg-(--col2) hover:bg-(--col3)
|
||||
border border-(--col4) hover:border-(--col5)
|
||||
text-white;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
@apply w-4 h-4 transition-transform stroke-current stroke-2 fill-none;
|
||||
@apply w-3 h-3 transition-transform stroke-current stroke-2 fill-none;
|
||||
}
|
||||
|
||||
.dropdown-arrow.open {
|
||||
@@ -20,7 +20,7 @@
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
@apply absolute z-50 mt-1 w-full rounded-md shadow-lg overflow-hidden
|
||||
@apply absolute z-50 mt-1 w-full rounded-md shadow-2xl overflow-hidden
|
||||
bg-(--col2) border border-(--col4)
|
||||
origin-top
|
||||
opacity-0 scale-95 -translate-y-1 pointer-events-none
|
||||
@@ -32,7 +32,7 @@
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
@apply px-3 py-1.5 cursor-pointer text-white transition-colors;
|
||||
@apply px-2 py-1 cursor-pointer text-white transition-colors border-b border-(--col4) last:border-b-0;
|
||||
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
|
||||
+8
-49
@@ -5,10 +5,8 @@ html {
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-(--col0) text-white select-none;
|
||||
}
|
||||
@apply bg-transparent text-white select-none;
|
||||
|
||||
.dark-theme {
|
||||
--col0: rgb(8, 8, 8);
|
||||
--col1: rgb(16, 16, 16);
|
||||
--col2: rgb(32, 32, 32);
|
||||
@@ -21,63 +19,24 @@ body {
|
||||
--col9: rgb(144, 144, 144);
|
||||
}
|
||||
|
||||
.red-theme {
|
||||
--col0: rgb(32, 8, 8);
|
||||
--col1: rgb(40, 16, 16);
|
||||
--col2: rgb(56, 32, 32);
|
||||
--col3: rgb(72, 48, 48);
|
||||
--col4: rgb(88, 64, 64);
|
||||
--col5: rgb(104, 80, 80);
|
||||
--col6: rgb(120, 96, 96);
|
||||
--col7: rgb(136, 112, 112);
|
||||
--col8: rgb(152, 128, 128);
|
||||
--col9: rgb(168, 144, 144);
|
||||
}
|
||||
|
||||
.blue-theme {
|
||||
--col0: rgb(8, 8, 32);
|
||||
--col1: rgb(16, 16, 40);
|
||||
--col2: rgb(32, 32, 56);
|
||||
--col3: rgb(48, 48, 72);
|
||||
--col4: rgb(64, 64, 88);
|
||||
--col5: rgb(80, 80, 104);
|
||||
--col6: rgb(96, 96, 120);
|
||||
--col7: rgb(112, 112, 136);
|
||||
--col8: rgb(128, 128, 152);
|
||||
--col9: rgb(144, 144, 168);
|
||||
}
|
||||
|
||||
.purple-theme {
|
||||
--col0: rgb(20, 8, 32);
|
||||
--col1: rgb(28, 16, 40);
|
||||
--col2: rgb(44, 32, 56);
|
||||
--col3: rgb(60, 48, 72);
|
||||
--col4: rgb(76, 64, 88);
|
||||
--col5: rgb(92, 80, 104);
|
||||
--col6: rgb(108, 96, 120);
|
||||
--col7: rgb(124, 112, 136);
|
||||
--col8: rgb(140, 128, 152);
|
||||
--col9: rgb(156, 144, 168);
|
||||
}
|
||||
|
||||
.button {
|
||||
@apply rounded-md cursor-pointer text-[16px] py-1 px-2 transition-colors border;
|
||||
}
|
||||
|
||||
.btntheme1 {
|
||||
@apply bg-(--col2) border-(--col4) hover:bg-(--col4) hover:border-(--col6) disabled:text-gray-300 disabled:bg-(--col1) disabled:border-(--col5) disabled:hover:text-gray-100 disabled:hover:bg-(--col2) disabled:hover:border-(--col6);
|
||||
@apply bg-(--col2)/50 border-(--col4)/50 hover:bg-(--col4)/50 hover:border-(--col6)/50 disabled:text-gray-300 disabled:bg-(--col1)/50 disabled:border-(--col5)/50 disabled:hover:text-gray-100 disabled:hover:bg-(--col2) disabled:hover:border-(--col6);
|
||||
}
|
||||
|
||||
.btntheme2,
|
||||
.downloads-entry .entry-info-item,
|
||||
.downloads-entry .button {
|
||||
@apply bg-(--col3) border-(--col5) hover:bg-(--col5) hover:border-(--col7) disabled:text-gray-300 disabled:bg-(--col2) disabled:border-(--col6) disabled:hover:text-gray-100 disabled:hover:bg-(--col3) disabled:hover:border-(--col7);
|
||||
@apply bg-(--col3)/50 border-(--col5)/50 hover:bg-(--col5)/50 hover:border-(--col7)/50 disabled:text-gray-300 disabled:bg-(--col2)/50 disabled:border-(--col6)/50 disabled:hover:text-gray-100 disabled:hover:bg-(--col3) disabled:hover:border-(--col7);
|
||||
}
|
||||
|
||||
.btntheme3,
|
||||
.downloads-entry:hover .entry-info-item,
|
||||
.downloads-entry:hover .button {
|
||||
@apply bg-(--col4) border-(--col6) hover:bg-(--col6) hover:border-(--col8) disabled:text-gray-300 disabled:bg-(--col2) disabled:border-(--col7) disabled:hover:text-gray-100 disabled:hover:bg-(--col4) disabled:hover:border-(--col8);
|
||||
@apply bg-(--col4)/50 border-(--col6)/50 hover:bg-(--col6)/50 hover:border-(--col8)/50 disabled:text-gray-300 disabled:bg-(--col2)/50 disabled:border-(--col7)/50 disabled:hover:text-gray-100 disabled:hover:bg-(--col4) disabled:hover:border-(--col8);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@@ -119,15 +78,15 @@ body {
|
||||
}
|
||||
|
||||
.popup-box {
|
||||
@apply relative w-150 h-120 rounded-lg bg-(--col1) border border-(--col3) flex flex-col p-6;
|
||||
@apply relative w-150 h-120 rounded-lg bg-(--col1)/50 border border-(--col3)/50 flex flex-col p-6;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
@apply flex-1 overflow-auto bg-(--col2) border border-(--col4) rounded-lg mt-4;
|
||||
@apply flex-1 overflow-auto bg-(--col2)/50 border border-(--col4)/50 rounded-lg mt-4;
|
||||
}
|
||||
|
||||
.popup-entry {
|
||||
@apply relative h-fit bg-(--col3) m-2 p-2 rounded-lg border border-(--col5);
|
||||
@apply relative h-fit bg-(--col3)/50 m-2 p-2 rounded-lg border border-(--col5)/50;
|
||||
}
|
||||
|
||||
.popup-entry button {
|
||||
@@ -143,7 +102,7 @@ body {
|
||||
}
|
||||
|
||||
.input-field {
|
||||
@apply border-2 border-(--col4) rounded-md bg-(--col2) py-1 px-2 focus:bg-(--col3) focus:border-(--col6) transition-colors w-full;
|
||||
@apply border-2 border-(--col4)/50 rounded-md bg-(--col2)/50 py-1 px-2 focus:bg-(--col3)/50 focus:border-(--col6)/50 transition-colors w-full;
|
||||
}
|
||||
|
||||
.entry-info-item {
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
}
|
||||
|
||||
.setting-checkbox {
|
||||
@apply appearance-none w-full h-full border-2 border-(--col4) rounded-md bg-(--col2) transition-colors cursor-pointer;
|
||||
@apply appearance-none w-full h-full border-2 border-(--col4)/50 rounded-md bg-(--col2)/50 transition-colors cursor-pointer;
|
||||
}
|
||||
|
||||
.setting-checkbox:checked {
|
||||
@apply bg-(--col4) border-(--col6);
|
||||
@apply bg-(--col4)/50 border-(--col6)/50;
|
||||
}
|
||||
|
||||
.fa-check-icon {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
.sidebar {
|
||||
@apply fixed top-0 left-0 w-60 h-screen bg-(--col1) flex flex-col border-e border-(--col2) z-1;
|
||||
@apply fixed top-0 left-0 w-60 h-screen bg-(--col1)/50 flex flex-col border-e border-(--col2)/50 z-1;
|
||||
}
|
||||
|
||||
.sidebar-downloads {
|
||||
@apply text-[#bdbdbd] fixed bottom-2 left-2 bg-(--col2) rounded-lg border border-(--col3) px-4 py-3 text-xl cursor-pointer transition-colors duration-[0.25s] opacity-75;
|
||||
@apply text-[#bdbdbd] fixed bottom-2 left-2 bg-(--col2)/50 rounded-lg border border-(--col3)/50 px-4 py-3 text-xl cursor-pointer transition-colors duration-[0.25s] opacity-75;
|
||||
}
|
||||
|
||||
.sidebar-downloads:hover {
|
||||
@apply text-white opacity-100;
|
||||
@apply bg-(--col3) border-(--col4);
|
||||
@apply bg-(--col3)/50 border-(--col4)/50;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
@@ -22,7 +22,7 @@
|
||||
}
|
||||
|
||||
.link.active {
|
||||
@apply bg-(--col2) border-(--col3);
|
||||
@apply bg-(--col2)/50 border-(--col3)/50;
|
||||
}
|
||||
|
||||
.link.active,
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
|
||||
.link.active:hover {
|
||||
@apply bg-(--col3) border-(--col4);
|
||||
@apply bg-(--col3)/50 border-(--col4)/50;
|
||||
}
|
||||
|
||||
.sidebar ::-webkit-scrollbar {
|
||||
|
||||
@@ -4,4 +4,5 @@ export type SettingProps = {
|
||||
onChange: (val: boolean) => void
|
||||
className?: string
|
||||
title?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
Reference in new issue
Block a user