'use client' import { useEffect, useState } from 'react' import Sidebar from './componets/Sidebar' import './Globals.css' import { DownloadProgress } from './types/DownloadProgress' import { invoke } from '@tauri-apps/api/core' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faAdd, faCheck, faChevronLeft, faCode, faDownload, faInfo, faRemove, faShieldHalved, faWarning, faXmark } from '@fortawesome/free-solid-svg-icons' import { readNormalConfig, readVersionsConfig, writeVersionsConfig } from './util/BazookaManager' import { VersionsConfig } from './types/VersionsConfig' import { NormalConfig } from './types/NormalConfig' import { app } from '@tauri-apps/api' import axios from 'axios' import { openUrl } from '@tauri-apps/plugin-opener' import { GlobalProvider } from './GlobalProvider' import { Roboto } from 'next/font/google' import { ServerVersionsResponse } from './types/ServerVersionsResponse' import { GameVersion } from './types/GameVersion' import { Game } from './types/Game' import { listen } from '@tauri-apps/api/event' import { usePathname } from 'next/navigation' import { arch, platform } from '@tauri-apps/plugin-os' import VersionInfo from './componets/VersionInfo' import prettyBytes from 'pretty-bytes' import ProgressBar from './componets/ProgressBar' import { notifyUser } from './util/Notifications' import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification' const roboto = Roboto({ subsets: ['latin'] }) export default function RootLayout ({ children }: { children: React.ReactNode }) { const [loading, setLoading] = useState(true) const [loadingText, setLoadingText] = useState('Loading...') const [outdated, setOutdated] = useState(false) const [version, setVersion] = useState(null) const [serverVersionList, setServerVersionList] = useState(null) const [selectedVersionList, setSelectedVersionList] = useState([]) const [downloadedVersionsConfig, setDownloadedVersionsConfig] = useState(null) const [normalConfig, setNormalConfig] = useState(null) const [showPopup, setShowPopup] = useState(false) const [popupMode, setPopupMode] = useState(null) const [fadeOut, setFadeOut] = useState(false) const [downloadProgress, setDownloadProgress] = useState( [] ) const [managingVersion, setManagingVersion] = useState(null) const [viewingInfoFromDownloads, setViewingInfoFromDownloads] = useState(false) const [selectedGame, setSelectedGame] = useState(null) function handleOverlayClick (e: React.MouseEvent) { if (e.target === e.currentTarget) { if (viewingInfoFromDownloads) { setPopupMode(0) setViewingInfoFromDownloads(false) setManagingVersion(null) setSelectedGame(null) } setFadeOut(true) setTimeout(() => setShowPopup(false), 200) } } const pathname = usePathname() useEffect(() => { let unlistenProgress: (() => void) | null = null let unlistenUninstalled: (() => void) | null = null listen('download-progress', event => { const [versionName, progStr, totalSizeStr, speedStr, etaSecsStr] = event.payload.split(':') const prog = Number(progStr) const progBytes = Number(totalSizeStr) const speed = Number(speedStr) const etaSecs = Number(etaSecsStr) setDownloadProgress(prev => { const i = prev.findIndex(d => d.version === versionName) if (i === -1) return prev const copy = [...prev] copy[i] = { ...copy[i], progress: prog, progressBytes: progBytes, speed, etaSecs } return copy }) }).then(f => (unlistenProgress = f)) listen('download-hash-checking', event => { const versionName = event.payload setDownloadProgress(prev => { const i = prev.findIndex(d => d.version === versionName) if (i === -1) return prev const copy = [...prev] copy[i] = { ...copy[i], hash_checking: true } return copy }) }).then(f => (unlistenProgress = f)) listen('download-finishing', event => { const versionName = event.payload setDownloadProgress(prev => { const i = prev.findIndex(d => d.version === versionName) if (i === -1) return prev const copy = [...prev] copy[i] = { ...copy[i], hash_checking: false, finishing: true } return copy }) }).then(f => (unlistenProgress = f)) listen('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?.() } }, []) useEffect(() => { ;(async () => { const client = await app.getVersion() setVersion(client) if (process.env.NODE_ENV === 'production') { setLoadingText('Checking latest version...') try { const response = await axios.get( 'https://games.lncvrt.xyz/api/launcher/latest' ) if (response.data !== client) { setOutdated(true) return } } catch { setLoadingText('Failed to check latest version.') return } } setLoadingText('Downloading version list...') try { const res = await axios.get( `https://games.lncvrt.xyz/api/launcher/versions?platform=${platform()}&arch=${arch()}` ) setServerVersionList(res.data) } catch { setLoadingText('Failed to download versions list.') return } setLoadingText('Loading configs...') const normalConfig = await readNormalConfig() const versionsConfig = await readVersionsConfig() setDownloadedVersionsConfig(versionsConfig) setNormalConfig(normalConfig) setLoading(false) if (!(await isPermissionGranted())) { await requestPermission() } })() }, []) useEffect(() => { if (process.env.NODE_ENV !== 'production') return const handler = (e: MouseEvent) => e.preventDefault() document.addEventListener('contextmenu', handler) 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() 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 { const list = selectedVersionList setSelectedVersionList([]) const newDownloads = list.map( version => new DownloadProgress(version, 0, 0, false, true, false, false, 0, 0) ) setDownloadProgress(newDownloads) for (const download of newDownloads) { const info = getVersionInfo(download.version) if (!info) { setDownloadProgress(prev => prev.filter(d => d.version !== download.version) ) return } const gameInfo = getVersionGame(info.game) if (!gameInfo) { setDownloadProgress(prev => prev.filter(d => d.version !== download.version) ) return } setDownloadProgress(prev => prev.map(d => d.version === download.version ? { ...d, queued: false } : d ) ) const res = await invoke('download', { url: info.downloadUrl, name: info.id, executable: info.executable, hash: info.sha512sum }) if (res == '1') { setDownloadProgress(prev => prev.filter(d => d.version !== download.version) ) let data = downloadedVersionsConfig if (!data) { setDownloadProgress(prev => prev.filter(d => d.version !== download.version) ) return } const date = Date.now() data.list = [...data.list, download.version] data.timestamps = { ...data.timestamps, [download.version]: date } setDownloadedVersionsConfig(data) writeVersionsConfig(data) } else { setDownloadProgress(prev => prev.map(d => d.version === download.version ? { ...d, queued: false, failed: true, progress: 0 } : d ) ) if (normalConfig?.settings.allowNotifications) await notifyUser( 'Download Failed', `The download for version ${gameInfo.name} v${info.versionName} has failed.` ) } } if (normalConfig?.settings.allowNotifications) 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 ( <> {loading ? (
{outdated ? (

Outdated Launcher!

Please update to the latest version to continue.

) : (

{loadingText}

)}
) : (
{ if (showPopup && e.key === 'Escape') { if (popupMode == 0 && selectedGame && pathname === '/') { setSelectedGame(null) setSelectedVersionList([]) } else if (viewingInfoFromDownloads) { setViewingInfoFromDownloads(false) setPopupMode(0) } else { setFadeOut(true) setTimeout(() => setShowPopup(false), 200) } } }} >
{children}
{showPopup && (
{popupMode === 0 && selectedGame ? ( <>

Select versions to download

{getSpecialVersionsList(selectedGame).map( (v, i) => (

{getVersionGame(v.game)?.name} v {v.versionName}

) )}
) : popupMode === 0 && !selectedGame ? ( <>

Select a game to download

{serverVersionList?.games.map((v, i) => (

{v.name}

{(() => { const data = getVersionsAmountData(v.id) if (!data) return 'N/A' return `${data.installed}/${data.total}` })()}{' '} versions installed

))}
) : popupMode === 1 ? ( <>

Downloads

{downloadProgress.length === 0 ? (

No more downloads!

) : ( downloadProgress.map((v, i) => (

{ getVersionGame( getVersionInfo(v.version)?.game )?.name }{' '} v{getVersionInfo(v.version)?.versionName}

{v.failed ? ( <>
Download failed
) : v.queued ? ( Queued… ) : v.queued ? ( Queued… ) : v.hash_checking ? ( Checking hash... ) : v.finishing ? ( Finishing... ) : (
Downloaded{' '} {prettyBytes(v.progressBytes, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}{' '} of{' '} {prettyBytes( getVersionInfo(v.version)?.size ?? 0, { minimumFractionDigits: 1, maximumFractionDigits: 1 } )}{' '} (ETA: {formatEtaSmart(v.etaSecs)}{' '} • Speed:{' '} {prettyBytes(v.speed, { minimumFractionDigits: 1, maximumFractionDigits: 1 })} /s)
)}
)) )}
) : popupMode === 2 ? ( managingVersion ? ( <>

Manage{' '} { getVersionGame( getVersionInfo(managingVersion)?.game )?.name }{' '} v{getVersionInfo(managingVersion)?.versionName}

) : (

No version selected

) ) : popupMode === 3 ? ( managingVersion && downloadedVersionsConfig ? ( ) : (

No version selected

) ) : null} {popupMode == 0 && selectedGame && serverVersionList != null && (
)}
)}
)} ) }