'use client' import { useCallback, useEffect, useRef, 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' import VersionChangelog from './componets/VersionChangelog' import { BaseDirectory, exists, remove } from '@tauri-apps/plugin-fs' import VersionUpdateWarning from './componets/VersionUpdateWarning' 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) const pathname = usePathname() const revisionCheck = useRef(false) function getSpecialVersionsList (game?: number): GameVersion[] { if (!normalConfig || !serverVersionList) return [] return serverVersionList.versions .filter( v => !Object.keys(downloadedVersionsConfig?.list ?? []).includes(v.id) ) .filter(v => { if ( platform() === 'linux' && v.wine && !normalConfig.settings.useWineOnUnixWhenNeeded ) return false 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 }) } const getVersionInfo = useCallback( (id: string | undefined): GameVersion | undefined => { if (!id) return undefined return serverVersionList?.versions.find(v => v.id === id) }, [serverVersionList] ) const getGameInfo = useCallback( (game: number | undefined): Game | undefined => { if (!game) return undefined return serverVersionList?.games.find(g => g.id === game) }, [serverVersionList] ) function getListOfGames (): Game[] { if (!downloadedVersionsConfig?.list) return [] const gamesMap = new Map() Object.keys(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 allowWine = platform() !== 'linux' || normalConfig?.settings.useWineOnUnixWhenNeeded const installed = Object.keys(downloadedVersionsConfig.list).filter(v => { const info = getVersionInfo(v) if (!info) return false if (info.wine && !allowWine) return false return getGameInfo(info.game)?.id === gameId }).length const total = serverVersionList.versions.filter(v => { if (!v) return false if (v.wine && !allowWine) return false return 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) } } useEffect(() => { let unlistenProgress: (() => void) | null = null listen('download-progress', event => { const [displayName, 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 === displayName) 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 displayName = event.payload setDownloadProgress(prev => { const i = prev.findIndex(d => d.version === displayName) 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 displayName = event.payload setDownloadProgress(prev => { const i = prev.findIndex(d => d.version === displayName) if (i === -1) return prev const copy = [...prev] copy[i] = { ...copy[i], hash_checking: false, finishing: true } return copy }) }).then(f => (unlistenProgress = f)) return () => { unlistenProgress?.() } }, []) 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) }, []) const downloadVersions = useCallback( async (list: string[]): Promise => { if (list.length === 0) return 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) ) continue } const gameInfo = getGameInfo(info.game) if (!gameInfo) { setDownloadProgress(prev => prev.filter(d => d.version !== download.version) ) continue } setDownloadProgress(prev => prev.map(d => d.version === download.version ? { ...d, queued: false } : d ) ) try { await axios.get( 'https://games.lncvrt.xyz/api/launcher/download?id=' + info.id ) } catch {} 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) ) setDownloadedVersionsConfig(prev => { if (!prev) return prev const updated = { ...prev, list: { ...prev.list, [download.version]: Date.now() } } writeVersionsConfig(updated) return updated }) } 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 ${info.displayName} has failed.` ) } } if (normalConfig?.settings.allowNotifications) await notifyUser('Downloads Finished', 'All downloads have finished.') setFadeOut(true) setTimeout(() => setShowPopup(false), 200) }, [getGameInfo, getVersionInfo, normalConfig] ) useEffect(() => { if (revisionCheck.current) return if (!serverVersionList || !downloadedVersionsConfig) return revisionCheck.current = true ;(async () => { for (const [key, value] of Object.entries( downloadedVersionsConfig.list )) { const verInfo = serverVersionList.versions.find(item => item.id === key) if ( !verInfo || (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 }) } } })() }, [serverVersionList, downloadedVersionsConfig, downloadVersions]) return ( <> {loading ? (
{outdated ? (

Outdated Launcher!

Please update to the latest version to continue.

) : (

{loadingText}

)}
) : (
{ if (showPopup && e.key === 'Escape') closePopup() }} >
{children}
{showPopup && (
) => { if (e.target === e.currentTarget) { if (viewingInfoFromDownloads) { setPopupMode(0) setViewingInfoFromDownloads(false) setManagingVersion(null) setSelectedGame(null) } setFadeOut(true) setTimeout(() => setShowPopup(false), 200) } }} >
{popupMode === 0 && selectedGame ? ( <>

Select versions to download

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

{v.displayName}

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

Select a game to download

{serverVersionList?.games .filter(v => { const data = getVersionsAmountData(v.id) if (!data) return false if (data.total > 0) return true }) .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.map((v, i) => (

{getVersionInfo(v.version)?.displayName}

{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{' '} {getVersionInfo(managingVersion)?.displayName}

) : (

No version selected

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

No version selected

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

No version selected

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

No version selected

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