Compare commits

..
17 Commits
Author SHA1 Message Date
Lncvrt 2777e6dac0 1.5.0 2026-01-07 19:56:34 -07:00
Lncvrt a551d5fb52 Change the colors of popup close button 2026-01-07 19:39:15 -07:00
Lncvrt 08915ad2ae Remove this case, I want to eventually make it close the popup but I haven't gotten that to work for some reason 2026-01-07 19:36:36 -07:00
Lncvrt 1dd75f8ccf Improve download versions/games button size 2026-01-07 19:14:54 -07:00
Lncvrt 3dc24ab3d7 Use proper way of detecting when a user hovers for games/installs page 2026-01-07 19:01:58 -07:00
Lncvrt a3e9067ba0 Change rounding for popup close button 2026-01-07 19:01:39 -07:00
Lncvrt a0a763ae57 Remove the manage button and make it a right-click instead. And also change the button height/width. Along with more re-arranging in layout.tsx 2026-01-07 18:29:17 -07:00
Lncvrt c56a903d55 Moove stuff around to make more sense 2026-01-07 18:14:33 -07:00
Lncvrt c07828f244 Improve the logic for uninstalling a version, it was incredibly bad and stupid 2026-01-07 18:10:14 -07:00
Lncvrt cf4f1c15ea Launching works as intended now 2026-01-07 18:07:11 -07:00
Lncvrt 76797c6120 Bring back this, either works or doesn't on linux and windows. I will have to test 2026-01-07 17:45:07 -07:00
Lncvrt 36ed3e0095 Rename this to make more sense 2026-01-07 17:43:26 -07:00
Lncvrt e0ba25da77 Improve parts of launch/download logic 2026-01-07 17:29:14 -07:00
Lncvrt e89b0fac8a Fix a memory leak that doesn't fix until another version is downloaded (can take up to 400mb of ram) 2026-01-07 17:28:42 -07:00
Lncvrt fa256dd004 Update dependencies 2026-01-07 17:28:01 -07:00
Lncvrt b5c6598722 Well that's stupid but it's fixed now 2026-01-07 15:09:55 -07:00
Lncvrt db111aaa2e Fix stupidness here 2026-01-07 15:01:44 -07:00
17 changed files with 413 additions and 430 deletions

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "lncvrt-games-launcher",
"private": true,
"version": "1.4.0",
"version": "1.5.0",
"type": "module",
"scripts": {
"dev": "next dev",
+4 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "lncvrt-games-launcher"
version = "1.4.0"
version = "1.5.0"
authors = ["Lncvrt"]
edition = "2024"
@@ -15,15 +15,15 @@ tauri-build = { version = "2.5.3", features = [] }
tauri = { version = "2.9.5", features = ["macos-private-api"] }
tauri-plugin-opener = "2.5.2"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.148"
serde_json = "1.0.149"
tauri-plugin-os = "2.3.2"
reqwest = { version = "0.13.1", default-features = false, features = ["stream", "rustls"] }
tokio = "1.48.0"
tokio = "1.49.0"
futures-util = { version = "0.3.31", features = ["io"] }
tauri-plugin-decorum = "1.1.1"
tauri-plugin-fs = "2.4.4"
zip = "7.0.0"
libc = "0.2.178"
libc = "0.2.179"
tauri-plugin-dialog = "2.4.2"
tauri-plugin-notification = "2.3.3"
sysinfo = "0.37.2"
+9 -3
View File
@@ -21,11 +21,17 @@
"core:window:allow-toggle-maximize",
"decorum:allow-show-snap-overlay",
"fs:default",
"fs:allow-applocaldata-read",
"fs:allow-applocaldata-write",
"dialog:default",
"notification:default",
"clipboard-manager:default",
"clipboard-manager:allow-write-text"
"clipboard-manager:allow-write-text",
"fs:allow-applocaldata-read",
"fs:allow-applocaldata-write",
{
"identifier": "fs:scope",
"allow": [
{ "path": "$APPLOCALDATA/game/**" }
]
}
]
}
+70 -76
View File
@@ -15,22 +15,25 @@ use tauri::{AppHandle, Emitter, Manager};
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
use tauri_plugin_opener::OpenerExt;
use tauri_plugin_os::platform;
use tokio::io::AsyncReadExt;
use tokio::{io::AsyncWriteExt, time::timeout};
use zip::ZipArchive;
#[cfg(target_os = "linux")]
use std::os::unix::fs::PermissionsExt;
#[cfg(target_os = "windows")]
use tauri_plugin_decorum::WebviewWindowExt;
fn is_running_by_path(path: &PathBuf) -> bool {
#[allow(unused)]
fn is_running_by_path(path: &Path) -> bool {
let sys = System::new_all();
let target = path.canonicalize().ok();
if target.is_none() {
return false;
}
let target = target.unwrap();
sys.processes().values().any(|proc| {
if let Some(exe) = proc.exe() {
exe == path
} else {
false
}
proc.exe()
.and_then(|exe| exe.canonicalize().ok())
.map_or(false, |exe| exe == target)
})
}
@@ -182,9 +185,26 @@ async fn download(
app.emit("download-hash-checking", format!("{}", &name))
.unwrap();
let mut hasher = Sha512::new();
hasher.update(&tokio::fs::read(download_part_path.clone()).await.unwrap());
let download_hash = format!("{:x}", hasher.finalize());
let download_hash = {
let mut file = tokio::fs::File::open(download_part_path.clone())
.await
.unwrap();
let mut hasher = Sha512::new();
{
let mut buffer = [0; 8192];
loop {
let bytes_read = file.read(&mut buffer).await.unwrap();
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
}
drop(file);
format!("{:x}", hasher.finalize())
};
if hash != download_hash {
tokio::fs::remove_file(download_part_path.clone())
.await
@@ -205,102 +225,77 @@ async fn download(
return "-1".to_string();
}
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
let executable_path = game_path.join(&name).join(&executable);
use std::{fs, os::unix::fs::PermissionsExt};
let executable_path = if cfg!(target_os = "linux") {
game_path.join(&name).join(&executable)
} else {
game_path
.join(&name)
.join(&executable)
.join("Contents")
.join("MacOS")
.join(
&executable
.chars()
.take(executable.chars().count() - 4)
.collect::<String>(),
)
};
let mut perms = fs::metadata(&executable_path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(executable_path, perms).unwrap();
fs::set_permissions(&executable_path, perms).unwrap();
}
#[cfg(target_os = "macos")]
{
use std::fs;
use std::os::unix::fs::PermissionsExt;
let macos_app_path = game_path
.join(&name)
.join(&executable)
.join("Contents")
.join("MacOS")
.join(
&executable
.chars()
.take(executable.chars().count() - 4)
.collect::<String>(),
);
let mut perms = fs::metadata(&macos_app_path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&macos_app_path, perms).unwrap();
}
return "1".to_string();
}
#[allow(unused_variables)]
#[tauri::command]
fn launch_game(app: AppHandle, name: String, executable: String) {
fn launch_game(app: AppHandle, name: String, executable: String, display_name: String) {
let game_folder = app
.path()
.app_local_data_dir()
.unwrap()
.join("game")
.join(&name);
let game_path = game_folder.join(&executable);
if !game_path.exists() {
app.dialog()
.message(
format!(
"Executable \"{}\" not found.\n\nTry reinstalling the game or make a support request in the Community link on the sidebar.",
game_path.display().to_string()
)
)
.kind(MessageDialogKind::Error)
.title("Game not found")
.show(|_| {});
if !game_folder.exists() {
return;
}
if is_running_by_path(&game_path) {
app.dialog()
.message(format!("The version {} is already running.", name))
.kind(MessageDialogKind::Error)
.title("Game already running")
.show(|_| {});
return;
//if already running on macos, it'll auto take the user to that proccess
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
if is_running_by_path(&game_folder.join(&executable)) {
app.dialog()
.message(format!(
"{} is already running, if this doesn't seem true, try to kill the proccess.",
display_name
))
.kind(MessageDialogKind::Error)
.title("Game already running")
.show(|_| {});
return;
}
}
if platform() == "macos" {
Command::new("open")
.arg(&game_path)
.arg(&executable)
.current_dir(&game_folder)
.spawn()
.unwrap();
} else {
Command::new(&game_path)
Command::new(&game_folder.join(&executable))
.current_dir(&game_folder)
.spawn()
.unwrap();
}
}
#[tauri::command]
async fn uninstall_version(app: AppHandle, name: String) {
let game_path = app
.path()
.app_local_data_dir()
.unwrap()
.join("game")
.join(&name);
if game_path.exists() {
if let Err(_) = tokio::fs::remove_dir_all(&game_path).await {
app.emit("version-failed", &name).unwrap();
} else {
app.emit("version-uninstalled", &name).unwrap();
}
} else {
app.emit("version-uninstalled", &name).unwrap();
}
}
#[tauri::command]
async fn open_folder(app: AppHandle, name: String) {
let game_path = app
@@ -345,7 +340,6 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![
download,
launch_game,
uninstall_version,
open_folder,
folder_size
])
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Lncvrt Games Launcher",
"version": "1.4.0",
"version": "1.5.0",
"identifier": "xyz.lncvrt.games-launcher",
"build": {
"beforeDevCommand": "next dev",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Lncvrt Games Launcher",
"version": "1.4.0",
"version": "1.5.0",
"identifier": "xyz.lncvrt.games-launcher",
"build": {
"beforeDevCommand": "next dev",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Lncvrt Games Launcher",
"version": "1.4.0",
"version": "1.5.0",
"identifier": "xyz.lncvrt.games-launcher",
"build": {
"beforeDevCommand": "next dev",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Lncvrt Games Launcher",
"version": "1.4.0",
"version": "1.5.0",
"identifier": "xyz.lncvrt.games-launcher",
"build": {
"beforeDevCommand": "next dev",
+1 -1
View File
@@ -28,7 +28,7 @@ type GlobalCtxType = {
setManagingVersion: (v: string | null) => void
setSelectedGame: (v: number | null) => void
getVersionInfo: (id: string | undefined) => GameVersion | undefined
getVersionGame: (id: number | undefined) => Game | undefined
getGameInfo: (id: number | undefined) => Game | undefined
getListOfGames: () => Game[]
getVersionsAmountData: (gameId: number) => {
installed: number
+2 -2
View File
@@ -77,7 +77,7 @@ body {
}
.button {
@apply rounded-md cursor-pointer text-[16px] py-1.5 px-3 transition-colors border;
@apply rounded-md cursor-pointer text-[16px] py-1 px-2 transition-colors border;
}
.btntheme1 {
@@ -147,7 +147,7 @@ body {
}
.close-button {
@apply flex justify-center items-center absolute bg-(--col3) hover:bg-(--col4) text-2xl cursor-pointer text-gray-300 hover:text-white h-12 w-12 p-3 rounded-xl left-2 top-2 transition-colors border border-(--col4) hover:border-(--col5);
@apply flex justify-center items-center absolute text-2xl text-gray-200 hover:text-white cursor-pointer h-12 w-12 p-3 rounded-lg left-2 top-2 transition-colors border;
}
*:focus {
+10
View File
@@ -11,3 +11,13 @@
.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);
}
.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);
}
.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);
}
+2 -2
View File
@@ -4,7 +4,7 @@ import { useGlobal } from '../GlobalProvider'
export default function VersionChangelog () {
const {
getVersionGame,
getGameInfo,
getVersionInfo,
managingVersion,
downloadedVersionsConfig
@@ -12,7 +12,7 @@ export default function VersionChangelog () {
if (!managingVersion || !downloadedVersionsConfig) return <></>
const versionInfo = getVersionInfo(managingVersion)
const gameInfo = getVersionGame(versionInfo?.game)
const gameInfo = getGameInfo(versionInfo?.game)
return (
<>
+2 -2
View File
@@ -17,7 +17,7 @@ import prettyBytes from 'pretty-bytes'
export default function VersionInfo () {
const {
getVersionGame,
getGameInfo,
getVersionInfo,
managingVersion,
downloadedVersionsConfig,
@@ -27,7 +27,7 @@ export default function VersionInfo () {
if (!managingVersion || !downloadedVersionsConfig) return <></>
const versionInfo = getVersionInfo(managingVersion)
const gameInfo = getVersionGame(versionInfo?.game)
const gameInfo = getGameInfo(versionInfo?.game)
const [versionSize, setVersionSize] = useState<number | null>(null)
useEffect(() => {
+68 -64
View File
@@ -19,12 +19,11 @@ export default function Installs () {
normalConfig,
setManagingVersion,
getVersionInfo,
getVersionGame,
getGameInfo,
setSelectedGame
} = useGlobal()
const params = useSearchParams()
const [hoveredIds, setHoveredIds] = useState<string[]>([])
useEffect(() => {
if (!showPopup) return
@@ -36,7 +35,7 @@ export default function Installs () {
<div className='flex justify-between items-center mb-4'>
<p className='text-3xl'>Installs</p>
<button
className='button btntheme1 text-3xl'
className='button btntheme1'
onClick={() => {
setSelectedGame(Number(params.get('id') || 0))
setPopupMode(0)
@@ -85,31 +84,36 @@ export default function Installs () {
title={
normalConfig?.settings.useLegacyInteractButtons
? ''
: 'Click to launch game'
: 'Click to launch game. Right-click to manage this version install'
}
onClick={async () => {
if (normalConfig?.settings.useLegacyInteractButtons) return
const verInfo = getVersionInfo(entry)
if (verInfo == undefined) return
const gameInfo = getGameInfo(verInfo.game)
if (gameInfo == undefined) return
invoke('launch_game', {
name: verInfo.id,
executable: verInfo.executable
executable: verInfo.executable,
displayName: `${gameInfo.name} v${verInfo.versionName}`
})
}}
onMouseEnter={() => setHoveredIds(prev => [...prev, entry])}
onMouseLeave={() =>
setHoveredIds(prev => prev.filter(i => i !== i))
}
onContextMenu={e => {
e.preventDefault()
setManagingVersion(entry)
setPopupMode(2)
setShowPopup(true)
setFadeOut(false)
}}
>
<div className='flex flex-col'>
<div className='h-18 w-screen relative'>
<p className='text-2xl'>
{getVersionGame(getVersionInfo(entry)?.game)?.name} v
{getGameInfo(getVersionInfo(entry)?.game)?.name} v
{getVersionInfo(entry)?.versionName}
</p>
<div
className={`entry-info-item mt-2 ${
hoveredIds.includes(entry) ? 'btntheme3' : 'btntheme2'
}`}
className='entry-info-item absolute left-0 bottom-0'
title='The date the game was installed in MM/dd/yyyy format'
>
<p>
@@ -120,56 +124,56 @@ export default function Installs () {
)}
</p>
</div>
</div>
<div className='flex flex-row items-center gap-2 mt-auto'>
<button
className={`button ${
hoveredIds.includes(entry) ? 'btntheme3' : 'btntheme2'
}`}
onClick={e => {
e.stopPropagation()
setManagingVersion(entry)
setPopupMode(3)
setShowPopup(true)
setFadeOut(false)
}}
title='Click to view version info'
>
View Info
</button>
<button
className={`button ${
hoveredIds.includes(entry) ? 'btntheme3' : 'btntheme2'
}`}
onClick={e => {
e.stopPropagation()
setManagingVersion(entry)
setPopupMode(2)
setShowPopup(true)
setFadeOut(false)
}}
title='Click to manage this version install'
>
Manage
</button>
<button
className={`button ${
hoveredIds.includes(entry) ? 'btntheme3' : 'btntheme2'
}`}
onClick={e => {
e.stopPropagation()
const verInfo = getVersionInfo(entry)
if (verInfo == undefined) return
invoke('launch_game', {
name: verInfo.id,
executable: verInfo.executable
})
}}
hidden={!normalConfig?.settings.useLegacyInteractButtons}
title='Click to launch game'
>
Launch
</button>
<div className='flex gap-2 absolute right-0 bottom-0'>
<button
className='button'
onClick={e => {
e.stopPropagation()
setManagingVersion(entry)
setPopupMode(3)
setShowPopup(true)
setFadeOut(false)
}}
title='Click to view version info'
>
View Info
</button>
<button
className='button'
hidden={
!normalConfig?.settings.useLegacyInteractButtons
}
onClick={e => {
e.stopPropagation()
setManagingVersion(entry)
setPopupMode(2)
setShowPopup(true)
setFadeOut(false)
}}
title='Click to manage this version install'
>
Manage
</button>
<button
className='button'
onClick={e => {
e.stopPropagation()
const verInfo = getVersionInfo(entry)
if (verInfo == undefined) return
invoke('launch_game', {
name: verInfo.id,
executable: verInfo.executable
})
}}
hidden={
!normalConfig?.settings.useLegacyInteractButtons
}
title='Click to launch game'
>
Launch
</button>
</div>
</div>
</div>
))
+227 -243
View File
@@ -45,6 +45,7 @@ import {
requestPermission
} from '@tauri-apps/plugin-notification'
import VersionChangelog from './componets/VersionChangelog'
import { BaseDirectory, exists, remove } from '@tauri-apps/plugin-fs'
const roboto = Roboto({
subsets: ['latin']
@@ -80,24 +81,100 @@ export default function RootLayout ({
useState<boolean>(false)
const [selectedGame, setSelectedGame] = useState<number | null>(null)
function handleOverlayClick (e: React.MouseEvent<HTMLDivElement>) {
if (e.target === e.currentTarget) {
if (viewingInfoFromDownloads) {
setPopupMode(0)
setViewingInfoFromDownloads(false)
setManagingVersion(null)
setSelectedGame(null)
}
const pathname = usePathname()
function getSpecialVersionsList (game?: number): GameVersion[] {
if (!normalConfig || !serverVersionList) return []
return serverVersionList.versions
.filter(v => !downloadedVersionsConfig?.list.includes(v.id))
.filter(v => {
if (game && v.game != game) return false
if (downloadProgress.length != 0) {
return !downloadProgress.some(d => d.version === v.id)
}
return true
})
.sort((a, b) => {
if (b.game !== a.game) return a.game - b.game
return 0
})
}
function getVersionInfo (id: string | undefined): GameVersion | undefined {
if (!id) return undefined
return serverVersionList?.versions.find(v => v.id === id)
}
function getGameInfo (game: number | undefined): Game | undefined {
if (!game) return undefined
return serverVersionList?.games.find(g => g.id === game)
}
function getListOfGames (): Game[] {
if (!downloadedVersionsConfig?.list) return []
const gamesMap = new Map<number, Game>()
downloadedVersionsConfig.list.forEach(i => {
const version = getVersionInfo(i)
if (!version) return
const game = getGameInfo(version.game)
if (!game) return
gamesMap.set(game.id, game)
})
return Array.from(gamesMap.values())
}
function getVersionsAmountData (gameId: number): {
installed: number
total: number
} | null {
if (!downloadedVersionsConfig || !serverVersionList) return null
const installed = downloadedVersionsConfig.list.filter(
v => getGameInfo(getVersionInfo(v)?.game)?.id === gameId
).length
const total = serverVersionList.versions.filter(
v => getGameInfo(v?.game)?.id === gameId
).length
return { installed, total }
}
function formatEtaSmart (seconds: number) {
if (seconds < 60) return `${Math.floor(seconds)}s`
if (seconds < 3600)
return `${Math.floor(seconds / 60)}m ${Math.floor(seconds % 60)}s`
if (seconds < 86400) {
const h = Math.floor(seconds / 3600)
const m = Math.floor((seconds % 3600) / 60)
return `${h}h ${m}m`
}
const d = Math.floor(seconds / 86400)
const h = Math.floor((seconds % 86400) / 3600)
return `${d}d ${h}h`
}
function closePopup () {
if (popupMode == 0 && selectedGame && pathname === '/') {
setSelectedGame(null)
setSelectedVersionList([])
} else if (viewingInfoFromDownloads) {
setViewingInfoFromDownloads(false)
setPopupMode(0)
} else if (popupMode == 4) {
setPopupMode(3)
} else {
setFadeOut(true)
setTimeout(() => setShowPopup(false), 200)
}
}
const pathname = usePathname()
useEffect(() => {
let unlistenProgress: (() => void) | null = null
let unlistenUninstalled: (() => void) | null = null
listen<string>('download-progress', event => {
const [versionName, progStr, totalSizeStr, speedStr, etaSecsStr] =
@@ -143,30 +220,8 @@ export default function RootLayout ({
})
}).then(f => (unlistenProgress = f))
listen<string>('version-uninstalled', event => {
const versionName = event.payload
setDownloadedVersionsConfig(prev => {
if (!prev) return prev
const updatedList = prev.list.filter(v => v !== versionName)
const updatedTimestamps = Object.fromEntries(
Object.entries(prev.timestamps).filter(([k]) => k !== versionName)
)
const updatedConfig = {
...prev,
list: updatedList,
timestamps: updatedTimestamps
}
writeVersionsConfig(updatedConfig)
setManagingVersion(null)
setFadeOut(true)
setTimeout(() => setShowPopup(false), 200)
return updatedConfig
})
}).then(f => (unlistenUninstalled = f))
return () => {
unlistenProgress?.()
unlistenUninstalled?.()
}
}, [])
@@ -219,50 +274,6 @@ export default function RootLayout ({
return () => document.removeEventListener('contextmenu', handler)
}, [])
function getSpecialVersionsList (game?: number): GameVersion[] {
if (!normalConfig || !serverVersionList) return []
return serverVersionList.versions
.filter(v => !downloadedVersionsConfig?.list.includes(v.id))
.filter(v => {
if (game && v.game != game) return false
if (downloadProgress.length != 0) {
return !downloadProgress.some(d => d.version === v.id)
}
return true
})
.sort((a, b) => {
if (b.game !== a.game) return a.game - b.game
return 0
})
}
function getVersionInfo (id: string | undefined): GameVersion | undefined {
if (!id) return undefined
return serverVersionList?.versions.find(v => v.id === id)
}
function getVersionGame (game: number | undefined): Game | undefined {
if (!game) return undefined
return serverVersionList?.games.find(g => g.id === game)
}
function getListOfGames (): Game[] {
if (!downloadedVersionsConfig?.list) return []
const gamesMap = new Map<number, Game>()
downloadedVersionsConfig.list.forEach(i => {
const version = getVersionInfo(i)
if (!version) return
const game = getVersionGame(version.game)
if (!game) return
gamesMap.set(game.id, game)
})
return Array.from(gamesMap.values())
}
async function downloadVersions (): Promise<void> {
const list = selectedVersionList
setSelectedVersionList([])
@@ -282,7 +293,7 @@ export default function RootLayout ({
)
return
}
const gameInfo = getVersionGame(info.game)
const gameInfo = getGameInfo(info.game)
if (!gameInfo) {
setDownloadProgress(prev =>
prev.filter(d => d.version !== download.version)
@@ -336,37 +347,6 @@ export default function RootLayout ({
await notifyUser('Downloads Finished', 'All downloads have finished.')
}
function getVersionsAmountData (gameId: number): {
installed: number
total: number
} | null {
if (!downloadedVersionsConfig || !serverVersionList) return null
const installed = downloadedVersionsConfig.list.filter(
v => getVersionGame(getVersionInfo(v)?.game)?.id === gameId
).length
const total = serverVersionList.versions.filter(
v => getVersionGame(v?.game)?.id === gameId
).length
return { installed, total }
}
function formatEtaSmart (seconds: number) {
if (seconds < 60) return `${Math.floor(seconds)}s`
if (seconds < 3600)
return `${Math.floor(seconds / 60)}m ${Math.floor(seconds % 60)}s`
if (seconds < 86400) {
const h = Math.floor(seconds / 3600)
const m = Math.floor((seconds % 3600) / 60)
return `${h}h ${m}m`
}
const d = Math.floor(seconds / 86400)
const h = Math.floor((seconds % 86400) / 3600)
return `${d}d ${h}h`
}
return (
<>
<html lang='en' className={roboto.className}>
@@ -427,7 +407,7 @@ export default function RootLayout ({
managingVersion,
setManagingVersion,
getVersionInfo,
getVersionGame,
getGameInfo,
getListOfGames,
setSelectedGame,
getVersionsAmountData,
@@ -438,20 +418,7 @@ export default function RootLayout ({
<div
tabIndex={0}
onKeyDown={e => {
if (showPopup && e.key === 'Escape') {
if (popupMode == 0 && selectedGame && pathname === '/') {
setSelectedGame(null)
setSelectedVersionList([])
} else if (viewingInfoFromDownloads) {
setViewingInfoFromDownloads(false)
setPopupMode(0)
} else if (popupMode == 4) {
setPopupMode(3)
} else {
setFadeOut(true)
setTimeout(() => setShowPopup(false), 200)
}
}
if (showPopup && e.key === 'Escape') closePopup()
}}
>
<Sidebar />
@@ -467,29 +434,23 @@ export default function RootLayout ({
{showPopup && (
<div
className={`popup-overlay ${fadeOut ? 'fade-out' : ''}`}
onClick={handleOverlayClick}
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) {
if (viewingInfoFromDownloads) {
setPopupMode(0)
setViewingInfoFromDownloads(false)
setManagingVersion(null)
setSelectedGame(null)
}
setFadeOut(true)
setTimeout(() => setShowPopup(false), 200)
}
}}
>
<div className='popup-box'>
<button
className='close-button'
onClick={() => {
if (
popupMode == 0 &&
selectedGame &&
pathname === '/'
) {
setSelectedGame(null)
setSelectedVersionList([])
} else if (viewingInfoFromDownloads) {
setViewingInfoFromDownloads(false)
setPopupMode(0)
} else if (popupMode == 4) {
setPopupMode(3)
} else {
setFadeOut(true)
setTimeout(() => setShowPopup(false), 200)
}
}}
className='close-button btntheme1'
onClick={() => closePopup()}
>
<FontAwesomeIcon
icon={
@@ -516,16 +477,16 @@ export default function RootLayout ({
<p
className={`text-2xl truncate ${
selectedVersionList.includes(v.id)
? 'max-w-80.5'
: 'max-w-87.5'
? 'max-w-84.5'
: 'max-w-91.5'
}`}
>
{getVersionGame(v.game)?.name} v
{getGameInfo(v.game)?.name} v
{v.versionName}
</p>
</div>
<button
className='button btntheme3 right-21.5 bottom-1.25'
className='button btntheme3 right-20.75 bottom-1.75'
onClick={() => {
setSelectedVersionList(prev =>
prev.includes(v.id)
@@ -551,7 +512,7 @@ export default function RootLayout ({
)}
</button>
<button
className='button btntheme3 right-1 bottom-1.25'
className='button btntheme3 right-1.5 bottom-1.75'
onClick={() => {
setManagingVersion(v.id)
setViewingInfoFromDownloads(true)
@@ -653,97 +614,89 @@ export default function RootLayout ({
<>
<p className='text-xl text-center'>Downloads</p>
<div className='popup-content'>
{downloadProgress.length === 0 ? (
<p className='text-center mt-6'>
No more downloads!
</p>
) : (
downloadProgress.map((v, i) => (
<div
key={i}
className='popup-entry flex flex-col justify-between'
>
<p className='text-2xl text-center'>
{
getVersionGame(
getVersionInfo(v.version)?.game
)?.name
}{' '}
v{getVersionInfo(v.version)?.versionName}
</p>
<div className='mt-6.25 flex items-center justify-between'>
{v.failed ? (
<>
<div className='flex items-center'>
<span className='text-red-500 inline-block w-full text-center'>
Download failed
</span>
<button
className='button btntheme3 ml-30 mb-2'
onClick={() => {
setDownloadProgress(prev =>
prev.filter(
d => d.version !== v.version
)
)
}}
title='Click to remove this version from this menu.'
>
Cancel
</button>
</div>
</>
) : v.queued ? (
<span className='text-yellow-500 inline-block w-full text-center'>
Queued…
</span>
) : v.queued ? (
<span className='text-yellow-500 inline-block w-full text-center'>
Queued…
</span>
) : v.hash_checking ? (
<span className='text-blue-500 inline-block w-full text-center'>
Checking hash...
</span>
) : v.finishing ? (
<span className='text-green-500 inline-block w-full text-center'>
Finishing...
</span>
) : (
<div className='flex flex-col gap-1 w-full'>
<span className='text-center'>
Downloaded{' '}
{prettyBytes(v.progressBytes, {
minimumFractionDigits: 1,
maximumFractionDigits: 1
})}{' '}
of{' '}
{prettyBytes(
getVersionInfo(v.version)?.size ??
0,
{
minimumFractionDigits: 1,
maximumFractionDigits: 1
}
)}{' '}
(ETA: {formatEtaSmart(v.etaSecs)}{' '}
&bull; Speed:{' '}
{prettyBytes(v.speed, {
minimumFractionDigits: 1,
maximumFractionDigits: 1
})}
/s)
{downloadProgress.map((v, i) => (
<div
key={i}
className='popup-entry flex flex-col justify-between'
>
<p className='text-2xl text-center'>
{
getGameInfo(getVersionInfo(v.version)?.game)
?.name
}{' '}
v{getVersionInfo(v.version)?.versionName}
</p>
<div className='mt-6.25 flex items-center justify-between'>
{v.failed ? (
<>
<div className='flex items-center'>
<span className='text-red-500 inline-block w-full text-center'>
Download failed
</span>
<ProgressBar
progress={v.progress}
className='w-full'
/>
<button
className='button btntheme3 ml-30 mb-2'
onClick={() => {
setDownloadProgress(prev =>
prev.filter(
d => d.version !== v.version
)
)
}}
title='Click to remove this version from this menu.'
>
Cancel
</button>
</div>
)}
</div>
</>
) : v.queued ? (
<span className='text-yellow-500 inline-block w-full text-center'>
Queued…
</span>
) : v.queued ? (
<span className='text-yellow-500 inline-block w-full text-center'>
Queued…
</span>
) : v.hash_checking ? (
<span className='text-blue-500 inline-block w-full text-center'>
Checking hash...
</span>
) : v.finishing ? (
<span className='text-green-500 inline-block w-full text-center'>
Finishing...
</span>
) : (
<div className='flex flex-col gap-1 w-full'>
<span className='text-center'>
Downloaded{' '}
{prettyBytes(v.progressBytes, {
minimumFractionDigits: 1,
maximumFractionDigits: 1
})}{' '}
of{' '}
{prettyBytes(
getVersionInfo(v.version)?.size ?? 0,
{
minimumFractionDigits: 1,
maximumFractionDigits: 1
}
)}{' '}
(ETA: {formatEtaSmart(v.etaSecs)} &bull;
Speed:{' '}
{prettyBytes(v.speed, {
minimumFractionDigits: 1,
maximumFractionDigits: 1
})}
/s)
</span>
<ProgressBar
progress={v.progress}
className='w-full'
/>
</div>
)}
</div>
))
)}
</div>
))}
</div>
</>
) : popupMode === 2 ? (
@@ -752,7 +705,7 @@ export default function RootLayout ({
<p className='text-xl text-center'>
Manage{' '}
{
getVersionGame(
getGameInfo(
getVersionInfo(managingVersion)?.game
)?.name
}{' '}
@@ -762,11 +715,42 @@ export default function RootLayout ({
<button
className='button btntheme2'
disabled={downloadProgress.length != 0}
onClick={() =>
invoke('uninstall_version', {
name: managingVersion
onClick={async () => {
closePopup()
setDownloadedVersionsConfig(prev => {
if (!prev) return prev
const updatedList = prev.list.filter(
v => v !== managingVersion
)
const updatedTimestamps =
Object.fromEntries(
Object.entries(prev.timestamps).filter(
([k]) => k !== managingVersion
)
)
const updatedConfig = {
...prev,
list: updatedList,
timestamps: updatedTimestamps
}
writeVersionsConfig(updatedConfig)
setManagingVersion(null)
setFadeOut(true)
setTimeout(() => setShowPopup(false), 200)
return updatedConfig
})
}
if (
await exists('game/' + managingVersion, {
baseDir: BaseDirectory.AppLocalData
})
)
await remove('game/' + managingVersion, {
baseDir: BaseDirectory.AppLocalData,
recursive: true
})
}}
title='Click to uninstall this game. This will NOT remove any progress or any save files.'
>
Uninstall
@@ -825,7 +809,7 @@ export default function RootLayout ({
selectedVersionList.length
} version${
selectedVersionList.length == 1 ? '' : 's'
} of ${getVersionGame(selectedGame)?.name}`
} of ${getGameInfo(selectedGame)?.name}`
}
>
Download {selectedVersionList.length} version
+11 -26
View File
@@ -28,7 +28,6 @@ export default function Installs () {
} = useGlobal()
const router = useRouter()
const [hoveredIds, setHoveredIds] = useState<number[]>([])
useEffect(() => {
if (!showPopup) return
@@ -40,7 +39,7 @@ export default function Installs () {
<div className='flex justify-between items-center mb-4'>
<p className='text-3xl'>Games</p>
<button
className='button btntheme1 text-3xl'
className='button btntheme1'
onClick={() => {
setSelectedGame(null)
setPopupMode(0)
@@ -82,18 +81,13 @@ export default function Installs () {
if (normalConfig?.settings.useLegacyInteractButtons) return
router.push('/game?id=' + i.id)
}}
onMouseEnter={() => setHoveredIds(prev => [...prev, i.id])}
onMouseLeave={() =>
setHoveredIds(prev => prev.filter(e => e !== i.id))
}
>
<div className='flex flex-col'>
<div className='h-18 w-screen relative'>
<p className='text-2xl'>{i.name}</p>
<div className='flex gap-2 mt-2'>
<div className='flex gap-2 absolute left-0 bottom-0'>
<div
className={`entry-info-item ${
hoveredIds.includes(i.id) ? 'btntheme3' : 'btntheme2'
}`}
className='entry-info-item'
title='The amount of versions installed of this game in installed/installable format.'
>
<p>
@@ -106,9 +100,7 @@ export default function Installs () {
</p>
</div>
<div
className={`entry-info-item ${
hoveredIds.includes(i.id) ? 'btntheme3' : 'btntheme2'
}`}
className='entry-info-item'
hidden={!i.official}
title='This game is official.'
>
@@ -116,9 +108,7 @@ export default function Installs () {
<p>Official</p>
</div>
<div
className={`entry-info-item ${
hoveredIds.includes(i.id) ? 'btntheme3' : 'btntheme2'
}`}
className='entry-info-item'
hidden={i.official}
title={
i.verified
@@ -133,17 +123,12 @@ export default function Installs () {
<p>{i.verified ? 'Verified' : 'Unverified'}</p>
</div>
</div>
</div>
<div
className='flex flex-row items-center gap-2 mt-auto'
hidden={!normalConfig?.settings.useLegacyInteractButtons}
title='Click to view game installs'
>
<Link
className={`button ${
hoveredIds.includes(i.id) ? 'btntheme3' : 'btntheme2'
}`}
className='button absolute right-0 bottom-0'
href={'/game?id=' + i.id}
hidden={!normalConfig?.settings.useLegacyInteractButtons}
title='Click to view game installs'
>
Installs
</Link>
+2 -2
View File
@@ -90,7 +90,7 @@ export default function Settings () {
title="This setting will make it so when you are on a page like this, the games won't disappear."
/>
<Setting
label='Show Installs/Launch Buttons'
label='Show Installs/Launch/Manage Buttons'
value={useLegacyInteractButtons}
onChange={async () => {
while (normalConfig != null) {
@@ -112,7 +112,7 @@ export default function Settings () {
break
}
}}
title='Enable the legacy method of using the installs/launch buttons. In the future this setting may be removed so try and get used to the new method.'
title='Enable the legacy method of using the installs/launch/manage buttons. In the future this setting may be removed so try and get used to the new method.'
/>
<div title='The theme you want the launcher to use.'>
<label className='text-lg'>Theme:</label>