'use client' import '@/styles/globals.css' import { useCallback, useEffect, useRef, useState } from 'react' import Sidebar from '@/components/Sidebar' import { DownloadProgress } from '@/types/DownloadProgress' import { app } from '@tauri-apps/api' import { openUrl } from '@tauri-apps/plugin-opener' import { GlobalProvider } from '@/providers/GlobalProvider' import { Roboto } from 'next/font/google' import { ServerVersionsResponse } from '@/types/ServerVersionsResponse' import { listen } from '@tauri-apps/api/event' import { usePathname } from 'next/navigation' import { arch, platform } from '@tauri-apps/plugin-os' import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification' import { BaseDirectory, exists, readTextFile, remove } from '@tauri-apps/plugin-fs' import { fetch } from '@tauri-apps/plugin-http' import { verifySignature } from '@/lib/util' import { getCurrentWindow, UserAttentionType } from '@tauri-apps/api/window' import { Mod } from '@/types/Mod' import { load, Store } from '@tauri-apps/plugin-store' import { notifyUser } from '@/lib/notifications' import { GameVersion } from '@/types/GameVersion' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faChevronLeft, faXmark } from '@fortawesome/free-solid-svg-icons' import { invoke } from '@tauri-apps/api/core' import VersionsDownloadPopup from '@/popups/VersionsDownload' import GamesDownloadPopup from '@/popups/GamesDownload' import DownloadsPopup from '@/popups/Downloads' import ManageVersionPopup from '@/popups/ManageVersion' import ModDownloadsPopup from '@/popups/ModDownloads' import VersionChangelogPopup from '@/popups/VersionChangelog' 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 [platformName, setPlatformName] = useState(null) const [serverVersionList, setServerVersionList] = useState(null) const [selectedVersionList, setSelectedVersionList] = useState([]) const [showPopup, setShowPopup] = useState(false) const [popupMode, setPopupMode] = useState(null) const [fadeOut, setFadeOut] = useState(false) const [downloadProgress, setDownloadProgress] = useState( [] ) const [downloadQueue, setDownloadQueue] = useState([]) const [isProcessingQueue, setIsProcessingQueue] = useState(false) const [managingVersion, setManagingVersion] = useState(null) const [viewingInfoFromDownloads, setViewingInfoFromDownloads] = useState(false) const [selectedGame, setSelectedGame] = useState(null) const [category, setCategory] = useState(-1) const [showModInfo, setShowModInfo] = useState(null) const pathname = usePathname() const revisionCheck = useRef(false) const [settings, setSettings] = useState(null) const [versions, setVersions] = useState(null) const [notificationsAllowed, setNotificationsAllowed] = useState(false) const [sidebarAlwaysShowGames, setSidebarAlwaysShowGames] = useState(false) const [linuxUseWine, setLinuxUseWine] = useState(false) const [linuxWineCommand, setLinuxWineCommand] = useState('wine %path%') const [theme, setTheme] = useState('dark') const [versionsList, setVersionsList] = useState>({}) const [modsList, setModsList] = useState< Record> >({}) function getSpecialVersionsList (game?: number): GameVersion[] { if (!serverVersionList) return [] return serverVersionList.versions .filter(v => !Object.keys(versionsList).includes(v.id)) .filter(v => { if (platformName == 'linux' && v.wine && linuxUseWine) return false if (game && v.game != game) return false if (category != -1 && v.category != category) 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 closePopup = useCallback(() => { if (popupMode == 0 && selectedGame && pathname === '/') { setSelectedGame(null) setSelectedVersionList([]) } else if (viewingInfoFromDownloads) { setViewingInfoFromDownloads(false) setPopupMode(0) } else if (popupMode == 3 && showModInfo) { setShowModInfo(null) } else if (popupMode == 4) { setPopupMode(2) } else { setFadeOut(true) setTimeout(() => setShowPopup(false), 200) } }, [popupMode, selectedGame, pathname, viewingInfoFromDownloads, showModInfo]) useEffect(() => { const unlisteners: (() => void)[] = [] const setupListeners = async () => { unlisteners.push( await listen('download-size', event => { const [displayName, sizeStr] = event.payload.split(':') const size = Number(sizeStr) setDownloadProgress(prev => { const i = prev.findIndex(d => d.version === displayName) if (i === -1) return prev const copy = [...prev] copy[i] = { ...copy[i], size } return copy }) }) ) unlisteners.push( await listen('download-progress', async 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 }) }) ) unlisteners.push( await 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, downloading: false } return copy }) }) ) unlisteners.push( await listen('unzip-start', 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, unzipping: true } return copy }) }) ) unlisteners.push( await listen('unzip-progress', async event => { const [displayName, unzippedStr, unzipTotalStr] = event.payload.split(':') const unzipped = Number(unzippedStr) const unzipTotal = Number(unzipTotalStr) setDownloadProgress(prev => { const i = prev.findIndex(d => d.version === displayName) if (i === -1) return prev const copy = [...prev] copy[i] = { ...copy[i], unzipped, unzipTotal } return copy }) }) ) unlisteners.push( await listen('download-start', 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], downloading: true, paused: false, failed: false } return copy }) }) ) unlisteners.push( await listen('download-stop', 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], downloading: false } return copy }) }) ) } setupListeners() return () => { unlisteners.forEach(unlisten => unlisten()) } }, []) useEffect(() => { ;(async () => { setPlatformName(platform()) const client = await app.getVersion() setVersion(client) if (process.env.NODE_ENV === 'production') { try { const response = await fetch( 'https://games.lncvrt.xyz/api/launcher/latest' ) const signature = response.headers.get('x-signature') ?? '' const data = await response.text() if (await verifySignature(data, signature)) { if (data !== client) { setOutdated(true) return } } else { setLoadingText('Failed to check latest version.') return } } catch { setLoadingText('Failed to check latest version.') return } } try { const response = await fetch( `https://games.lncvrt.xyz/api/launcher/versions?platform=${platform()}&arch=${arch()}` ) const signature = response.headers.get('x-signature') ?? '' const data = await response.json() if (await verifySignature(JSON.stringify(data), signature)) { setServerVersionList(data) } else { setLoadingText('Failed to download versions list.') return } } catch { setLoadingText('Failed to download versions list.') return } const settingsLocal = await load('settings.json', { autoSave: true, defaults: { version: client, notificationsAllowed: true, sidebarAlwaysShowGames: true, linuxUseWine: false, linuxWineCommand: 'wine %path%', theme: 'dark' } }) const versionsLocal = await load('versions.json', { autoSave: true, defaults: { version: client, list: {}, mods: {} } }) settingsLocal.set('version', client) versionsLocal.set('version', client) const legacyOptions = { baseDir: BaseDirectory.AppLocalData } if (await exists('config.json', legacyOptions)) { const config = await readTextFile('config.json', legacyOptions) const raw = JSON.parse(config) if ( raw.settings && raw.settings.theme && (raw.version == '1.0.0' || raw.version == '1.1.0' || raw.version == '1.1.1' || raw.version == '1.2.0' || raw.version == '1.3.0' || raw.version == '1.3.1' || raw.version == '1.4.0' || raw.version == '1.5.0' || raw.version == '1.5.1' || raw.version == '1.5.2' || raw.version == '1.5.3' || raw.version == '1.5.4') ) { const parsed = Number(raw.settings.theme) if (parsed == 3) raw.settings.theme = 2 if (parsed == 4) raw.settings.theme = 3 else if (parsed != 0 && parsed != 1) raw.settings.theme = 0 } if (raw.settings.allowNotifications) settingsLocal?.set( 'notificationsAllowed', Boolean(raw.settings.allowNotifications) ) if (raw.settings.alwaysShowGamesInSidebar) settingsLocal?.set( 'sidebarAlwaysShowGames', Boolean(raw.settings.alwaysShowGamesInSidebar) ) if (raw.settings.useWineOnUnixWhenNeeded) settingsLocal?.set( 'linuxUseWine', Boolean(raw.settings.useWineOnUnixWhenNeeded) ) if (raw.settings.wineOnUnixCommand) settingsLocal?.set( 'linuxWineCommand', String(raw.settings.wineOnUnixCommand) ) if (raw.settings.theme) { const tempTheme = (() => { switch (Number(raw.settings.theme)) { case 1: return 'red' case 2: return 'blue' case 3: return 'purple' default: return 'dark' } })() settingsLocal?.set('theme', tempTheme) } await remove('config.json', legacyOptions) } setSettings(settingsLocal) setVersions(versionsLocal) setLoading(false) if (!(await isPermissionGranted())) { await requestPermission() } })() }, []) const downloadVersions = useCallback( async ( list: Array<{ id: string type: 0 | 1 | 2 modDownload?: number gameId?: number modId?: number modVersion?: string }> ): Promise => { if (list.length === 0) return setSelectedVersionList([]) const newVersions = list.filter( item => !downloadQueue.includes(item.id) && !downloadProgress.some(d => d.version === item.id) ) if (newVersions.length === 0) return const newDownloads = newVersions.map( item => new DownloadProgress( item.id, 0, 0, false, false, false, false, true, false, 0, 0, 0, false, 0, 0, null, null, null, item.type, item.modDownload ?? null, item.gameId ?? null, item.modId ?? null, item.modVersion ?? null ) ) setDownloadProgress(prev => [...prev, ...newDownloads]) setDownloadQueue(prev => [...prev, ...newVersions.map(v => v.id)]) }, [downloadQueue, downloadProgress] ) useEffect(() => { if (isProcessingQueue || downloadQueue.length === 0) return const processNextDownload = async () => { setIsProcessingQueue(true) const versionId = downloadQueue[0] const downloadInfo = downloadProgress.find(d => d.version == versionId) if (!downloadInfo) return const info = serverVersionList?.versions.find(vf => vf.id == versionId) if (!info) { setDownloadProgress(prev => prev.filter(d => d.version !== versionId)) setDownloadQueue(prev => prev.slice(1)) setIsProcessingQueue(false) return } const gameInfo = serverVersionList?.games.find(vf => vf.id == info.game) if (!gameInfo) { setDownloadProgress(prev => prev.filter(d => d.version !== versionId)) setDownloadQueue(prev => prev.slice(1)) setIsProcessingQueue(false) return } setDownloadProgress(prev => prev.map(d => (d.version === versionId ? { ...d, queued: false } : d)) ) const downloadInfoRequest = await fetch( 'https://games.lncvrt.xyz/api/launcher/download' + (downloadInfo.type == 0 ? '?versionId=' + info.id + '&downloadId=' + info.download : '?downloadId=' + (downloadInfo.type == 1 ? info.modSupportDownload : downloadInfo.modDownload)) + (downloadInfo.type == 2 ? '&modId=' + downloadInfo.modId : '') ) const signature = downloadInfoRequest.headers.get('x-signature') ?? '' const data = await downloadInfoRequest.json() if ( !(await verifySignature(JSON.stringify(data), signature)) || !data.success ) { setDownloadProgress(prev => prev.map(d => d.version === versionId ? { ...d, queued: false, failed: true, progress: 0 } : d ) ) if (await settings?.get('notifications-allowed')) await notifyUser( 'Download Failed', `The download for version ${info.displayName} has failed.` ) await getCurrentWindow().requestUserAttention( UserAttentionType.Critical ) return } setDownloadProgress(prev => prev.map(d => d.version === versionId ? { ...d, url: data.data.url, executable: downloadInfo.type == 0 ? info.executable : null, hash: data.data.hash } : d ) ) const res = await invoke('download', { url: data.data.url, name: info.id, hash: data.data.hash, downloadType: downloadInfo.type, modId: String(downloadInfo.modId ?? '') }) if (res === '1') { setDownloadProgress(prev => prev.filter(d => d.version !== versionId)) if (downloadInfo.type != 1) { if (downloadInfo.type == 2) { versions?.set('mods', { ...modsList, [downloadInfo.modGame! + '-' + downloadInfo.modId!]: { [downloadInfo.modVersion!]: Date.now() } }) } else { versions?.set('list', { ...versionsList, [versionId]: Date.now() }) } } } else if (res == '0') { setDownloadProgress(prev => { const i = prev.findIndex(d => d.version === info.id) if (i === -1) return prev if (prev[i].canceled) { return prev.filter((_, idx) => idx !== i) } const copy = [...prev] copy[i] = { ...copy[i], downloading: false, paused: true, failed: false } return copy }) } else if (res == '-1') { setDownloadProgress(prev => prev.map(d => d.version === versionId ? { ...d, queued: false, failed: true, progress: 0 } : d ) ) if (await settings?.get('notifications-allowed')) await notifyUser( 'Download Failed', `The download for version ${info.displayName} has failed.` ) await getCurrentWindow().requestUserAttention( UserAttentionType.Critical ) } setDownloadQueue(prev => prev.slice(1)) setIsProcessingQueue(false) } processNextDownload() }, [ downloadQueue, isProcessingQueue, downloadProgress, serverVersionList, settings, versions, versionsList, modsList ]) useEffect(() => { if (revisionCheck.current) return if (!serverVersionList) return revisionCheck.current = true ;(async () => { for (const [key, value] of Object.entries(versionsList)) { 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, downloadVersions, versionsList]) useEffect(() => { if ( downloadProgress.length === 0 && downloadQueue.length === 0 && showPopup && popupMode === 1 ) { setTimeout(async () => { if (notificationsAllowed) { await notifyUser('Downloads Complete', 'All downloads finished!') } await getCurrentWindow().requestUserAttention( UserAttentionType.Informational ) closePopup() }, 0) } }, [ downloadProgress, downloadQueue, showPopup, popupMode, closePopup, notificationsAllowed ]) useEffect(() => { if (!settings || !versions) return const unlisteners: Promise<() => void>[] = [] function watchSettings (key: string, fn: (v: T | undefined) => void) { settings!.get(key).then(fn) unlisteners.push(settings!.onKeyChange(key, fn)) } function watchVersions (key: string, fn: (v: T | undefined) => void) { versions!.get(key).then(fn) unlisteners.push(versions!.onKeyChange(key, fn)) } watchSettings('theme', v => setTheme(v ?? 'dark')) watchSettings('notificationsAllowed', v => setNotificationsAllowed(v ?? true) ) watchSettings('sidebarAlwaysShowGames', v => setSidebarAlwaysShowGames(v ?? true) ) watchSettings('linuxUseWine', v => setLinuxUseWine(v ?? false)) watchSettings('linuxWineCommand', v => setLinuxWineCommand(v ?? 'wine %path%') ) watchVersions>('list', v => setVersionsList(v ?? {})) watchVersions>>('mods', v => setModsList(v ?? {}) ) return () => { unlisteners.forEach(u => u.then(fn => fn())) } }, [settings, versions]) return ( <> {loading ? ( <>