985 lines
31 KiB
TypeScript
985 lines
31 KiB
TypeScript
'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 { 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,
|
|
copyFile,
|
|
exists,
|
|
readTextFile,
|
|
remove
|
|
} from '@tauri-apps/plugin-fs'
|
|
import { fetch } from '@tauri-apps/plugin-http'
|
|
import { openFolder, 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 semver from 'semver'
|
|
import { exit } from '@tauri-apps/plugin-process'
|
|
import { ask } from '@tauri-apps/plugin-dialog'
|
|
|
|
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'
|
|
import ManageGamePopup from '@/popups/ManageGame'
|
|
|
|
const roboto = Roboto({
|
|
subsets: ['latin']
|
|
})
|
|
|
|
export default function RootLayout ({
|
|
children
|
|
}: {
|
|
children: React.ReactNode
|
|
}) {
|
|
const [loading, setLoading] = useState(true)
|
|
const [loadingText, setLoadingText] = useState('Loading...')
|
|
const [version, setVersion] = useState<string | null>(null)
|
|
const [platformName, setPlatformName] = useState<string | null>(null)
|
|
const [movingData, setMovingData] = useState(false)
|
|
|
|
const [serverVersionList, setServerVersionList] =
|
|
useState<null | ServerVersionsResponse>(null)
|
|
const [selectedVersionList, setSelectedVersionList] = useState<string[]>([])
|
|
|
|
const [showPopup, setShowPopup] = useState(false)
|
|
const [popupMode, setPopupMode] = useState<null | number>(null)
|
|
const [fadeOut, setFadeOut] = useState(false)
|
|
|
|
const [downloadProgress, setDownloadProgress] = useState<DownloadProgress[]>(
|
|
[]
|
|
)
|
|
const [downloadQueue, setDownloadQueue] = useState<string[]>([])
|
|
const [isProcessingQueue, setIsProcessingQueue] = useState<boolean>(false)
|
|
const [managingVersion, setManagingVersion] = useState<string | null>(null)
|
|
const [managingGame, setManagingGame] = useState<number | null>(null)
|
|
const [viewingInfoFromDownloads, setViewingInfoFromDownloads] =
|
|
useState<boolean>(false)
|
|
const [selectedGame, setSelectedGame] = useState<number | null>(null)
|
|
|
|
const [category, setCategory] = useState<number>(-1)
|
|
const [showModInfo, setShowModInfo] = useState<Mod | null>(null)
|
|
|
|
const pathname = usePathname()
|
|
const revisionCheck = useRef(false)
|
|
|
|
const [settings, setSettings] = useState<Store | null>(null)
|
|
const [versions, setVersions] = useState<Store | null>(null)
|
|
const [account, setAccount] = useState<Store | null>(null)
|
|
|
|
const [sidebarAlwaysShowGames, setSidebarAlwaysShowGames] =
|
|
useState<boolean>(false)
|
|
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 [developerMode, setDeveloperMode] = useState<boolean>(false)
|
|
|
|
const [versionsList, setVersionsList] = useState<Record<string, number>>({})
|
|
const [modsList, setModsList] = useState<
|
|
Record<string, Record<string, number>>
|
|
>({})
|
|
|
|
const [accountSession, setAccountSession] = useState<string | null>(null)
|
|
const [accountId, setAccountId] = useState<string | null>(null)
|
|
const [accountName, setAccountName] = useState<string | null>(null)
|
|
const [accountAdmin, setAccountAdmin] = useState<boolean>(false)
|
|
|
|
const downloadVersions = useCallback(
|
|
async (
|
|
list: Array<{
|
|
id: string
|
|
type: 0 | 1 | 2
|
|
modDownload?: number
|
|
gameId?: number
|
|
modId?: number
|
|
modVersion?: string
|
|
}>
|
|
): Promise<void> => {
|
|
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) {
|
|
setDownloadProgress(prev => prev.filter(d => d.version !== versionId))
|
|
setDownloadQueue(prev => prev.slice(1))
|
|
setIsProcessingQueue(false)
|
|
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 : ''),
|
|
{
|
|
headers: {
|
|
Requester: 'LncvrtGamesLauncherClient',
|
|
ClientVersion: await app.getVersion(),
|
|
ClientPlatform: platform() + '-' + arch()
|
|
}
|
|
}
|
|
)
|
|
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
|
|
)
|
|
)
|
|
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<string>('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) {
|
|
const currentMods =
|
|
(await versions?.get<Record<string, Record<string, number>>>(
|
|
'mods'
|
|
)) ?? {}
|
|
await versions?.set('mods', {
|
|
...currentMods,
|
|
[downloadInfo.modGame! + '-' + downloadInfo.modId!]: {
|
|
[downloadInfo.modVersion!]: Date.now()
|
|
}
|
|
})
|
|
} else {
|
|
const currentList =
|
|
(await versions?.get<Record<string, number>>('list')) ?? {}
|
|
await versions?.set('list', {
|
|
...currentList,
|
|
[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
|
|
)
|
|
)
|
|
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
|
|
])
|
|
|
|
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 === '/main') {
|
|
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])
|
|
|
|
const needsRevisionUpdate = (
|
|
lastRevision: number | undefined,
|
|
version: string
|
|
) => {
|
|
if (!lastRevision) return false
|
|
return (
|
|
lastRevision > 0 &&
|
|
(versionsList == undefined ? 0 : versionsList[version]) / 1000 <=
|
|
lastRevision
|
|
)
|
|
}
|
|
|
|
const launchGame = async (versionInfo: GameVersion) => {
|
|
if (needsRevisionUpdate(versionInfo.lastRevision, versionInfo.id)) {
|
|
const answer = await ask(
|
|
'Before proceeding, if you do not want your installation directory wiped just yet, please backup the files to another directory. When you click "Yes", it will be wiped. Click "No" if you want to open the installation folder instead.',
|
|
{
|
|
title: 'Revision Update',
|
|
kind: 'warning'
|
|
}
|
|
)
|
|
if (answer) {
|
|
const answer2 = await ask(
|
|
'Are you sure you want to update? If you did not read the last popup, please go back and read it.',
|
|
{
|
|
title: 'Revision Update',
|
|
kind: 'warning'
|
|
}
|
|
)
|
|
if (!answer2) return
|
|
|
|
//open downloads popup
|
|
setPopupMode(1)
|
|
setShowPopup(true)
|
|
setFadeOut(false)
|
|
|
|
//uninstall
|
|
await versions?.set(
|
|
'list',
|
|
Object.fromEntries(
|
|
Object.entries(versionsList).filter(([k]) => k !== versionInfo.id)
|
|
)
|
|
)
|
|
|
|
if (
|
|
await exists(
|
|
customDataLocation
|
|
? customDataLocation + '/'
|
|
: null + 'game/' + versionInfo.id,
|
|
{
|
|
baseDir: customDataLocation
|
|
? undefined
|
|
: BaseDirectory.AppLocalData
|
|
}
|
|
)
|
|
)
|
|
await remove(
|
|
customDataLocation
|
|
? customDataLocation + '/'
|
|
: null + 'game/' + versionInfo.id,
|
|
{
|
|
baseDir: customDataLocation
|
|
? undefined
|
|
: BaseDirectory.AppLocalData,
|
|
recursive: true
|
|
}
|
|
)
|
|
|
|
//reinstall
|
|
setSelectedVersionList([versionInfo.id])
|
|
downloadVersions([
|
|
{
|
|
id: versionInfo.id,
|
|
type: 0
|
|
}
|
|
])
|
|
} else {
|
|
openFolder(versionInfo.id)
|
|
}
|
|
return
|
|
}
|
|
invoke('launch_game', {
|
|
name: versionInfo.id,
|
|
executable: versionInfo.executable,
|
|
displayName: versionInfo.displayName,
|
|
useWine: !!(platform() == 'linux' && versionInfo.wine && linuxUseWine),
|
|
wineCommand: linuxWineCommand
|
|
})
|
|
}
|
|
|
|
useEffect(() => {
|
|
const unlisteners: (() => void)[] = []
|
|
|
|
const setupListeners = async () => {
|
|
unlisteners.push(
|
|
await listen<string>('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<string>('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<string>('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<string>('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<string>('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<string>('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<string>('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)
|
|
const legacyOptions = {
|
|
baseDir: BaseDirectory.AppLocalData
|
|
}
|
|
if (
|
|
platform() == 'windows' &&
|
|
(await exists('versions.json', legacyOptions))
|
|
) {
|
|
if (
|
|
await exists('versions.json', {
|
|
baseDir: BaseDirectory.AppConfig
|
|
})
|
|
)
|
|
await remove('versions.json', {
|
|
baseDir: BaseDirectory.AppConfig
|
|
})
|
|
|
|
await copyFile('versions.json', 'versions.json', {
|
|
fromPathBaseDir: BaseDirectory.AppLocalData,
|
|
toPathBaseDir: BaseDirectory.AppConfig
|
|
})
|
|
await remove('versions.json', legacyOptions)
|
|
}
|
|
|
|
const settingsLocal = await load('settings.json', {
|
|
autoSave: true,
|
|
defaults: {
|
|
version: client,
|
|
sidebarAlwaysShowGames: true,
|
|
linuxUseWine: false,
|
|
linuxWineCommand: 'wine %path%',
|
|
theme: 'dark',
|
|
customDataLocation: '',
|
|
developerMode: false
|
|
}
|
|
})
|
|
const versionsLocal = await load('versions.json', {
|
|
autoSave: true,
|
|
defaults: {
|
|
version: client,
|
|
list: {},
|
|
mods: {}
|
|
}
|
|
})
|
|
const accountLocal = await load('account.json', {
|
|
autoSave: true,
|
|
defaults: {
|
|
version: client,
|
|
session: null,
|
|
id: null,
|
|
name: null,
|
|
admin: false
|
|
}
|
|
})
|
|
const cfgVer = (await settingsLocal.get<string>('version')) ?? client
|
|
const versVer = (await versionsLocal.get<string>('version')) ?? client
|
|
if (semver.gt(cfgVer, client) || semver.gt(versVer, client)) {
|
|
exit(1)
|
|
return
|
|
}
|
|
|
|
await settingsLocal.set('version', client)
|
|
await versionsLocal.set('version', client)
|
|
await accountLocal.set('version', client)
|
|
|
|
if (await exists('config.json', legacyOptions)) {
|
|
const config = await readTextFile('config.json', legacyOptions)
|
|
const raw = JSON.parse(config)
|
|
if (
|
|
raw.settings &&
|
|
raw.settings.theme &&
|
|
semver.satisfies(raw.version, '>=1.0.0 <=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)
|
|
await settingsLocal?.set(
|
|
'notificationsAllowed',
|
|
Boolean(raw.settings.allowNotifications)
|
|
)
|
|
if (raw.settings.alwaysShowGamesInSidebar)
|
|
await settingsLocal?.set(
|
|
'sidebarAlwaysShowGames',
|
|
Boolean(raw.settings.alwaysShowGamesInSidebar)
|
|
)
|
|
if (raw.settings.useWineOnUnixWhenNeeded)
|
|
await settingsLocal?.set(
|
|
'linuxUseWine',
|
|
Boolean(raw.settings.useWineOnUnixWhenNeeded)
|
|
)
|
|
if (raw.settings.wineOnUnixCommand)
|
|
await 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'
|
|
}
|
|
})()
|
|
await settingsLocal?.set('theme', tempTheme)
|
|
}
|
|
await remove('config.json', legacyOptions)
|
|
}
|
|
setSettings(settingsLocal)
|
|
setVersions(versionsLocal)
|
|
setAccount(accountLocal)
|
|
|
|
try {
|
|
const response = await fetch(
|
|
'https://games.lncvrt.xyz/api/launcher/versions',
|
|
{
|
|
headers: {
|
|
Requester: 'LncvrtGamesLauncherClient',
|
|
ClientVersion: await app.getVersion(),
|
|
ClientPlatform: platform() + '-' + arch(),
|
|
Authorization: (await accountLocal.get('session')) ?? ''
|
|
}
|
|
}
|
|
)
|
|
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
|
|
}
|
|
|
|
setLoading(false)
|
|
|
|
if (!(await isPermissionGranted())) {
|
|
await requestPermission()
|
|
}
|
|
})()
|
|
}, [])
|
|
|
|
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
|
|
) {
|
|
await invoke('remove_stale_executable', {
|
|
version: key,
|
|
executable: verInfo.executable
|
|
})
|
|
}
|
|
}
|
|
})()
|
|
}, [serverVersionList, versionsList])
|
|
|
|
useEffect(() => {
|
|
if (
|
|
downloadProgress.length === 0 &&
|
|
downloadQueue.length === 0 &&
|
|
showPopup &&
|
|
popupMode === 1
|
|
) {
|
|
setTimeout(async () => {
|
|
await notifyUser('Downloads Complete', 'All downloads finished!')
|
|
await getCurrentWindow().requestUserAttention(
|
|
UserAttentionType.Informational
|
|
)
|
|
closePopup()
|
|
}, 0)
|
|
}
|
|
}, [downloadProgress, downloadQueue, showPopup, popupMode, closePopup])
|
|
|
|
useEffect(() => {
|
|
if (!settings || !versions) return
|
|
const unlisteners: Promise<() => void>[] = []
|
|
|
|
function watchSettings<T> (key: string, fn: (v: T | undefined) => void) {
|
|
settings!.get<T>(key).then(fn)
|
|
unlisteners.push(settings!.onKeyChange<T>(key, fn))
|
|
}
|
|
|
|
function watchVersions<T> (key: string, fn: (v: T | undefined) => void) {
|
|
versions!.get<T>(key).then(fn)
|
|
unlisteners.push(versions!.onKeyChange<T>(key, fn))
|
|
}
|
|
|
|
function watchAccount<T> (key: string, fn: (v: T | undefined) => void) {
|
|
account!.get<T>(key).then(fn)
|
|
unlisteners.push(account!.onKeyChange<T>(key, fn))
|
|
}
|
|
|
|
watchSettings<string>('theme', v => setTheme(v ?? 'dark'))
|
|
watchSettings<boolean>('sidebarAlwaysShowGames', v =>
|
|
setSidebarAlwaysShowGames(v ?? true)
|
|
)
|
|
watchSettings<boolean>('linuxUseWine', v => setLinuxUseWine(v ?? false))
|
|
watchSettings<string>('linuxWineCommand', v =>
|
|
setLinuxWineCommand(v ?? 'wine %path%')
|
|
)
|
|
watchSettings<string>('customDataLocation', v =>
|
|
setCustomDataLocation(v ?? '')
|
|
)
|
|
watchSettings<boolean>('developerMode', v => setDeveloperMode(v ?? false))
|
|
|
|
watchVersions<Record<string, number>>('list', v => setVersionsList(v ?? {}))
|
|
watchVersions<Record<string, Record<string, number>>>('mods', v =>
|
|
setModsList(v ?? {})
|
|
)
|
|
|
|
watchAccount<string | null>('session', v => setAccountSession(v ?? null))
|
|
watchAccount<string | null>('id', v => setAccountId(v ?? null))
|
|
watchAccount<string | null>('name', v => setAccountName(v ?? null))
|
|
watchAccount<boolean>('admin', v => setAccountAdmin(v ?? false))
|
|
|
|
return () => {
|
|
unlisteners.forEach(u => u.then(fn => fn()))
|
|
}
|
|
}, [settings, versions, account])
|
|
|
|
return (
|
|
<>
|
|
<html lang='en' className={roboto.className}>
|
|
<body className={theme + '-theme'}>
|
|
{loading ? (
|
|
<>
|
|
{platformName == 'windows' && (
|
|
<div className='relative z-2 w-screen border-b border-b-(--col3) h-8.25 bg-(--col1)' />
|
|
)}
|
|
<div
|
|
className={`w-screen ${
|
|
platformName == 'windows'
|
|
? 'h-[calc(100vh-64px)]'
|
|
: 'h-screen'
|
|
} flex items-center justify-center`}
|
|
>
|
|
<p className='text-7xl text-center'>{loadingText}</p>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<GlobalProvider
|
|
value={{
|
|
serverVersionList,
|
|
setServerVersionList,
|
|
selectedVersionList,
|
|
setSelectedVersionList,
|
|
downloadProgress,
|
|
setDownloadProgress,
|
|
showPopup,
|
|
setShowPopup,
|
|
popupMode,
|
|
setPopupMode,
|
|
fadeOut,
|
|
setFadeOut,
|
|
managingVersion,
|
|
setManagingVersion,
|
|
setSelectedGame,
|
|
viewingInfoFromDownloads,
|
|
version,
|
|
downloadVersions,
|
|
category,
|
|
setCategory,
|
|
downloadQueue,
|
|
setDownloadQueue,
|
|
closePopup,
|
|
selectedGame,
|
|
setViewingInfoFromDownloads,
|
|
showModInfo,
|
|
setShowModInfo,
|
|
getSpecialVersionsList,
|
|
settings,
|
|
versions,
|
|
account,
|
|
sidebarAlwaysShowGames,
|
|
linuxUseWine,
|
|
linuxWineCommand,
|
|
theme,
|
|
customDataLocation,
|
|
developerMode,
|
|
versionsList,
|
|
modsList,
|
|
accountSession,
|
|
accountId,
|
|
accountName,
|
|
accountAdmin,
|
|
movingData,
|
|
setMovingData,
|
|
managingGame,
|
|
setManagingGame,
|
|
needsRevisionUpdate,
|
|
launchGame
|
|
}}
|
|
>
|
|
<div
|
|
tabIndex={0}
|
|
onKeyDown={e => {
|
|
if (showPopup && e.key === 'Escape') closePopup()
|
|
}}
|
|
>
|
|
<Sidebar />
|
|
{platformName == 'windows' && (
|
|
<div className='relative z-2 ml-59.75 w-[calc(100vw-239px)] border-b border-b-(--col3) h-8.25 bg-(--col1)' />
|
|
)}
|
|
<div className='relative z-0'>
|
|
<main className='ml-60'>{children}</main>
|
|
</div>
|
|
{showPopup && (
|
|
<div
|
|
className={`popup-overlay ${fadeOut ? 'fade-out' : ''}`}
|
|
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
|
|
if (e.target === e.currentTarget) {
|
|
if (showModInfo) setShowModInfo(null)
|
|
if (viewingInfoFromDownloads) {
|
|
setPopupMode(0)
|
|
setViewingInfoFromDownloads(false)
|
|
setManagingVersion(null)
|
|
setSelectedGame(null)
|
|
}
|
|
setFadeOut(true)
|
|
setTimeout(() => setShowPopup(false), 200)
|
|
}
|
|
}}
|
|
>
|
|
<div className='popup-box'>
|
|
<button
|
|
className='popup-top-button left-2 btntheme1'
|
|
onClick={() => closePopup()}
|
|
>
|
|
<FontAwesomeIcon
|
|
icon={
|
|
(popupMode == 0 &&
|
|
selectedGame &&
|
|
pathname === '/main') ||
|
|
viewingInfoFromDownloads ||
|
|
(popupMode == 3 && showModInfo) ||
|
|
popupMode == 4
|
|
? faChevronLeft
|
|
: faXmark
|
|
}
|
|
/>
|
|
</button>
|
|
{popupMode === 0 && selectedGame ? (
|
|
<VersionsDownloadPopup />
|
|
) : popupMode === 0 && !selectedGame ? (
|
|
<GamesDownloadPopup />
|
|
) : popupMode === 1 ? (
|
|
<DownloadsPopup />
|
|
) : popupMode === 2 ? (
|
|
<ManageVersionPopup />
|
|
) : popupMode === 3 ? (
|
|
<ModDownloadsPopup />
|
|
) : popupMode === 4 ? (
|
|
<VersionChangelogPopup />
|
|
) : popupMode === 5 ? (
|
|
<ManageGamePopup />
|
|
) : null}
|
|
</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>
|
|
)}
|
|
</body>
|
|
</html>
|
|
</>
|
|
)
|
|
}
|