Add auto updater to the launcher
This commit is contained in:
19 files changed
+170
-39
No files matched your search
@@ -0,0 +1,13 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
.downloads-container {
|
||||
@apply flex justify-center;
|
||||
}
|
||||
|
||||
.downloads-scroll {
|
||||
@apply bg-(--col1) border border-(--col3) rounded-lg overflow-y-auto w-full;
|
||||
}
|
||||
|
||||
.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) cursor-pointer;
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
'use client'
|
||||
|
||||
import './styles.css'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { BirdColor } from '@/types/BerryDash/BirdColor'
|
||||
import { GetIconForUser } from '@/lib/berrydashutil'
|
||||
import Image from 'next/image'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { fetch } from '@tauri-apps/plugin-http'
|
||||
import { verifySignature } from '@/lib/util'
|
||||
|
||||
interface BaseEntry {
|
||||
id: number
|
||||
username: string
|
||||
value: number
|
||||
icon: number
|
||||
overlay: number
|
||||
birdColor: BirdColor
|
||||
overlayColor: BirdColor
|
||||
customIcon: string | null
|
||||
}
|
||||
|
||||
interface Stats {
|
||||
highScore: string
|
||||
totalNormalBerries: string
|
||||
totalPoisonBerries: string
|
||||
totalSlowBerries: string
|
||||
totalUltraBerries: string
|
||||
totalSpeedyBerries: string
|
||||
totalCoinBerries: string
|
||||
totalRandomBerries: string
|
||||
totalAntiBerries: string
|
||||
totalGoldenBerries: string
|
||||
coins: string
|
||||
}
|
||||
|
||||
interface LeaderboardEntry extends BaseEntry {
|
||||
type: 'leaderboard'
|
||||
value: number
|
||||
}
|
||||
|
||||
interface Account extends BaseEntry {
|
||||
type: 'account'
|
||||
stats: Stats
|
||||
xp: bigint
|
||||
}
|
||||
|
||||
export function calculateXP (
|
||||
normalBerries: bigint,
|
||||
poisonBerries: bigint,
|
||||
slowBerries: bigint,
|
||||
ultraBerries: bigint,
|
||||
speedyBerries: bigint,
|
||||
coinBerries: bigint,
|
||||
randomBerries: bigint,
|
||||
antiBerries: bigint,
|
||||
goldenBerries: bigint
|
||||
): bigint {
|
||||
let totalXp = 0n
|
||||
totalXp += normalBerries
|
||||
totalXp -= poisonBerries
|
||||
totalXp -= slowBerries
|
||||
totalXp += ultraBerries * 5n
|
||||
totalXp += speedyBerries * 10n
|
||||
totalXp += coinBerries * 10n
|
||||
totalXp += randomBerries
|
||||
totalXp -= antiBerries
|
||||
totalXp += goldenBerries * 4n
|
||||
|
||||
if (totalXp < 0n) totalXp = 0n
|
||||
return totalXp
|
||||
}
|
||||
|
||||
export function calculateLevel (xp: bigint): number {
|
||||
const levelDivisor = 50.0
|
||||
|
||||
const xpNumber = Number(xp)
|
||||
const discriminant = 95 * 95 + levelDivisor * 2 * xpNumber
|
||||
const level = (-95 + Math.sqrt(discriminant)) / levelDivisor
|
||||
return Math.floor(level) + 1
|
||||
}
|
||||
|
||||
export default function BerryDashLeaderboards () {
|
||||
const [selected, setSelected] = useState<number>(-1)
|
||||
const [selectedBerryOption, setSelectedBerryOption] = useState<number>(0)
|
||||
const [entries, setEntries] = useState<(LeaderboardEntry | Account)[]>([])
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const Refresh = useCallback(async () => {
|
||||
try {
|
||||
if (selected == 3 || selected == 4) {
|
||||
const response = await fetch(
|
||||
'https://games.lncvrt.xyz/api/berrydash/account?username='
|
||||
)
|
||||
const signature = response.headers.get('x-signature') ?? ''
|
||||
const data = await response.json()
|
||||
if (
|
||||
(await verifySignature(JSON.stringify(data), signature)) &&
|
||||
data.success
|
||||
) {
|
||||
let accounts = data.data as Account[]
|
||||
|
||||
accounts = accounts.map(acc => {
|
||||
const xp = calculateXP(
|
||||
BigInt(acc.stats.totalNormalBerries),
|
||||
BigInt(acc.stats.totalPoisonBerries),
|
||||
BigInt(acc.stats.totalSlowBerries),
|
||||
BigInt(acc.stats.totalUltraBerries),
|
||||
BigInt(acc.stats.totalSpeedyBerries),
|
||||
BigInt(acc.stats.totalCoinBerries),
|
||||
BigInt(acc.stats.totalRandomBerries),
|
||||
BigInt(acc.stats.totalAntiBerries),
|
||||
BigInt(acc.stats.totalGoldenBerries)
|
||||
)
|
||||
return { ...acc, xp }
|
||||
})
|
||||
|
||||
accounts.sort((a, b) => (b.xp > a.xp ? 1 : b.xp < a.xp ? -1 : 0))
|
||||
|
||||
setEntries(accounts)
|
||||
} else console.log('Failed')
|
||||
} else {
|
||||
const response = await fetch(
|
||||
'https://games.lncvrt.xyz/api/berrydash/leaderboard/' +
|
||||
(selected == 0
|
||||
? 'score'
|
||||
: selected == 1
|
||||
? 'berry?berry=' + selectedBerryOption
|
||||
: selected == 2
|
||||
? 'coin'
|
||||
: selected == 5
|
||||
? 'legacy'
|
||||
: 'total')
|
||||
)
|
||||
const signature = response.headers.get('x-signature') ?? ''
|
||||
const data = await response.json()
|
||||
if (
|
||||
(await verifySignature(JSON.stringify(data), signature)) &&
|
||||
data.success
|
||||
)
|
||||
setEntries(data.data as LeaderboardEntry[])
|
||||
}
|
||||
} catch {
|
||||
setEntries([])
|
||||
}
|
||||
}, [selected, selectedBerryOption])
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'Lncvrt Games - Berry Dash Leaderboards'
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (selected != -1) setTimeout(() => Refresh(), 0)
|
||||
}, [selected, Refresh])
|
||||
|
||||
return (
|
||||
<div className='mx-4 mt-4'>
|
||||
<div className='flex justify-between items-center mb-4'>
|
||||
<p className='text-3xl'>Berry Dash Leaderboards</p>
|
||||
<div className='flex gap-2'>
|
||||
<button
|
||||
className='button btntheme1'
|
||||
onClick={() => {
|
||||
setEntries([])
|
||||
Refresh()
|
||||
}}
|
||||
title='Click to refresh the leaderboards.'
|
||||
hidden={selected == -1}
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
<button
|
||||
className='button btntheme1'
|
||||
onClick={() => {
|
||||
if (selected == -1) router.push('/main/game?id=1')
|
||||
else {
|
||||
setSelected(-1)
|
||||
setSelectedBerryOption(0)
|
||||
setEntries([])
|
||||
}
|
||||
}}
|
||||
title='Click to go up a level.'
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`box ${
|
||||
platform() == 'windows'
|
||||
? 'h-[calc(100vh-116px)]'
|
||||
: 'h-[calc(100vh-84px)]'
|
||||
}`}
|
||||
>
|
||||
{selected == -1 ? (
|
||||
<>
|
||||
<p className='text-center mt-2 text-xl'>Select a Leaderboard</p>
|
||||
<div className='flex flex-col gap-2 mt-2 items-center justify-center'>
|
||||
<button
|
||||
className='leaderboard-button'
|
||||
onClick={() => setSelected(0)}
|
||||
>
|
||||
Score Leaderboard
|
||||
</button>
|
||||
<button
|
||||
className='leaderboard-button'
|
||||
onClick={() => setSelected(1)}
|
||||
>
|
||||
Berry Leaderboard
|
||||
</button>
|
||||
<button
|
||||
className='leaderboard-button'
|
||||
onClick={() => setSelected(2)}
|
||||
>
|
||||
Coins Leaderboard
|
||||
</button>
|
||||
<button
|
||||
className='leaderboard-button'
|
||||
onClick={() => setSelected(3)}
|
||||
>
|
||||
Level Leaderboard
|
||||
</button>
|
||||
<button
|
||||
className='leaderboard-button'
|
||||
onClick={() => setSelected(4)}
|
||||
>
|
||||
Total XP Leaderboard
|
||||
</button>
|
||||
<button
|
||||
className='leaderboard-button'
|
||||
onClick={() => setSelected(5)}
|
||||
>
|
||||
Legacy Leaderboard
|
||||
</button>
|
||||
<button
|
||||
className='leaderboard-button'
|
||||
onClick={() => setSelected(6)}
|
||||
>
|
||||
Total Berries Leaderboard
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={`flex flex-col gap-2 overflow-y-auto ${
|
||||
selected == 1
|
||||
? platform() == 'windows'
|
||||
? 'h-[calc(100vh-156px)]'
|
||||
: 'h-[calc(100vh-124px)]'
|
||||
: platform() == 'windows'
|
||||
? 'h-[calc(100vh-128px)]'
|
||||
: 'h-[calc(100vh-96px)]'
|
||||
} px-1`}
|
||||
>
|
||||
{entries.map((item, index) => {
|
||||
const isAccount = 'stats' in item
|
||||
const isLeaderboard = 'value' in item && !isAccount
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className='leaderboard-entry flex justify-between items-center'
|
||||
>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Image
|
||||
src={
|
||||
!item.customIcon
|
||||
? `https://games.lncvrt.xyz/api/berrydash/render-icon?icon=${
|
||||
item.icon == 1
|
||||
? GetIconForUser(item.id)
|
||||
: item.icon
|
||||
}&overlay=${item.overlay}&birdR=${
|
||||
item.birdColor[0]
|
||||
}&birdG=${item.birdColor[1]}&birdB=${
|
||||
item.birdColor[2]
|
||||
}&overlayR=${item.overlayColor[0]}&overlayG=${
|
||||
item.overlayColor[1]
|
||||
}&overlayB=${item.overlayColor[2]}`
|
||||
: `https://games.lncvrt.xyz/api/berrydash/icon-marketplace/icon?id=${item.customIcon}&raw=true`
|
||||
}
|
||||
width={48}
|
||||
height={48}
|
||||
alt=''
|
||||
className='pointer-events-none scale-x-[-1]'
|
||||
/>
|
||||
<p>
|
||||
{item.username} (#{index + 1})
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isLeaderboard ? (
|
||||
<p>
|
||||
{selected === 1 || selected === 6
|
||||
? 'Berries'
|
||||
: selected === 2
|
||||
? 'Coins'
|
||||
: 'Score'}
|
||||
: {item.value.toLocaleString()}
|
||||
</p>
|
||||
) : isAccount ? (
|
||||
<p>
|
||||
{selected === 3 ? 'Level' : 'XP'}:{' '}
|
||||
{selected === 3
|
||||
? calculateLevel(item.xp).toLocaleString()
|
||||
: item.xp.toLocaleString()}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{selected == 1 && (
|
||||
<div className='flex justify-center'>
|
||||
<select
|
||||
className='mt-1.25 bg-(--col2) border border-(--col4) rounded-md'
|
||||
value={selectedBerryOption}
|
||||
onChange={e => setSelectedBerryOption(Number(e.target.value))}
|
||||
>
|
||||
<option value='0'>Normal Berry</option>
|
||||
<option value='1'>Poison Berry</option>
|
||||
<option value='2'>Slow Berry</option>
|
||||
<option value='3'>Ultra Berry</option>
|
||||
<option value='4'>Speedy Berry</option>
|
||||
<option value='5'>Coin Berry</option>
|
||||
<option value='6'>Random Berry</option>
|
||||
<option value='7'>Anti Berry</option>
|
||||
<option value='8'>Golden Berry</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
.box {
|
||||
@apply bg-(--col1) border border-(--col3) rounded-lg w-auto p-1;
|
||||
}
|
||||
|
||||
.leaderboard-button {
|
||||
@apply bg-(--col2) hover:bg-(--col4) border border-(--col4) hover:border-(--col6) rounded-lg px-4 py-2 inline-block transition-all duration-200 cursor-pointer;
|
||||
}
|
||||
|
||||
.leaderboard-entry {
|
||||
@apply rounded-lg text-gray-200 text-lg px-1 py-0.5 transition-colors bg-(--col2) hover:bg-(--col3) border border-(--col4) hover:border-(--col5) cursor-pointer;
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
'use client'
|
||||
|
||||
import '../Installs.css'
|
||||
import { useEffect } from 'react'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { useGlobal } from '@/providers/GlobalProvider'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { faWarning } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { ask } from '@tauri-apps/plugin-dialog'
|
||||
import { BaseDirectory, exists, remove } from '@tauri-apps/plugin-fs'
|
||||
import { openFolder } from '@/lib/util'
|
||||
|
||||
export default function Installs () {
|
||||
const {
|
||||
versionsList,
|
||||
serverVersionList,
|
||||
showPopup,
|
||||
setShowPopup,
|
||||
setPopupMode,
|
||||
setFadeOut,
|
||||
setSelectedVersionList,
|
||||
setManagingVersion,
|
||||
setSelectedGame,
|
||||
category,
|
||||
setCategory,
|
||||
downloadVersions,
|
||||
downloadProgress,
|
||||
linuxUseWine,
|
||||
linuxWineCommand,
|
||||
versions
|
||||
} = useGlobal()
|
||||
|
||||
const params = useSearchParams()
|
||||
const router = useRouter()
|
||||
|
||||
const id = Number(params.get('id') || 0)
|
||||
const game = serverVersionList?.games.find(g => g.id === id)
|
||||
|
||||
useEffect(() => {
|
||||
if (!showPopup) return
|
||||
setSelectedVersionList([])
|
||||
}, [setSelectedVersionList, showPopup])
|
||||
|
||||
if (!id || !game) return <p>Invalid game</p>
|
||||
|
||||
const needsRevisionUpdate = (
|
||||
lastRevision: number | undefined,
|
||||
version: string
|
||||
) => {
|
||||
if (!lastRevision) return false
|
||||
return (
|
||||
lastRevision > 0 &&
|
||||
(versionsList == undefined ? 0 : versionsList[version]) / 1000 <=
|
||||
lastRevision
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mx-4 mt-4'>
|
||||
<div className='flex justify-between items-center mb-4'>
|
||||
<p
|
||||
className={`text-3xl truncate ${
|
||||
category != -1
|
||||
? 'w-[calc(100vw-495px)]'
|
||||
: game.id == 1
|
||||
? 'w-[calc(100vw-560px)]'
|
||||
: 'w-[calc(100vw-440px)]'
|
||||
}`}
|
||||
>
|
||||
{game.name} Installs
|
||||
</p>
|
||||
<div className='flex gap-2'>
|
||||
<button
|
||||
className='button btntheme1'
|
||||
onClick={() => {
|
||||
router.push('/main/game/berrydash/leaderboards')
|
||||
}}
|
||||
title='View the leaderboards for this game.'
|
||||
hidden={game.id != 1}
|
||||
>
|
||||
Leaderboards
|
||||
</button>
|
||||
<button
|
||||
className='button btntheme1'
|
||||
onClick={() => {
|
||||
setCategory(-1)
|
||||
}}
|
||||
title='Click to go up a level.'
|
||||
hidden={category == -1}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
className='button btntheme1'
|
||||
onClick={() => {
|
||||
setSelectedGame(id)
|
||||
setPopupMode(0)
|
||||
setShowPopup(true)
|
||||
setFadeOut(false)
|
||||
}}
|
||||
title='Click to download more versions of this game.'
|
||||
>
|
||||
Download versions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='downloads-container'>
|
||||
<div
|
||||
className={`downloads-scroll ${
|
||||
platform() == 'windows'
|
||||
? 'h-[calc(100vh-116px)]'
|
||||
: 'h-[calc(100vh-84px)]'
|
||||
}`}
|
||||
>
|
||||
{category == -1 &&
|
||||
Object.entries(game.categoryNames)
|
||||
.filter(([key]) => {
|
||||
const count = Object.keys(versionsList).filter(v => {
|
||||
const info = serverVersionList?.versions.find(
|
||||
vf => vf.id == v
|
||||
)
|
||||
if (!info) return false
|
||||
|
||||
if (platform() == 'linux' && info.wine && !linuxUseWine)
|
||||
return false
|
||||
|
||||
return info.game === id && info.category === Number(key)
|
||||
}).length
|
||||
|
||||
return count >= 1
|
||||
})
|
||||
.sort(([a], [b]) => Number(b) - Number(a))
|
||||
.map(([key, value]) => {
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className={'downloads-entry'}
|
||||
title={'Click to view category'}
|
||||
onClick={() => setCategory(Number(key))}
|
||||
>
|
||||
<div className='h-18 w-screen relative'>
|
||||
<p className='text-2xl'>{value}</p>
|
||||
|
||||
<div
|
||||
className='entry-info-item flex absolute left-0 bottom-0'
|
||||
title='The amount of versions installed of this game in installed/installable format.'
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<p>
|
||||
{(() => {
|
||||
const count =
|
||||
Object.keys(versionsList).filter(v => {
|
||||
const info = serverVersionList?.versions.find(
|
||||
vf => vf.id == v
|
||||
)
|
||||
if (!info) return false
|
||||
if (
|
||||
platform() == 'linux' &&
|
||||
info.wine &&
|
||||
!linuxUseWine
|
||||
)
|
||||
return false
|
||||
return (
|
||||
info.game === id &&
|
||||
info.category == Number(key)
|
||||
)
|
||||
}).length ?? 0
|
||||
return `${count} install${count === 1 ? '' : 's'}`
|
||||
})()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{Object.keys(versionsList)
|
||||
.filter(v => {
|
||||
const info = serverVersionList?.versions.find(vf => vf.id == v)
|
||||
if (!info) return false
|
||||
if (platform() == 'linux' && info.wine && !linuxUseWine)
|
||||
return false
|
||||
return (
|
||||
info.game === id &&
|
||||
(category == -1
|
||||
? info.category == -1
|
||||
: info.category == category)
|
||||
)
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const infoA = serverVersionList?.versions.find(vf => vf.id == a)
|
||||
const infoB = serverVersionList?.versions.find(vf => vf.id == b)
|
||||
if (!infoA || !infoB) return 0
|
||||
return infoB.place - infoA.place
|
||||
})
|
||||
.map(v => {
|
||||
const versionInfo = serverVersionList?.versions.find(
|
||||
vf => vf.id == v
|
||||
)
|
||||
if (!versionInfo) return
|
||||
|
||||
return (
|
||||
<div
|
||||
key={v}
|
||||
className={'downloads-entry'}
|
||||
title={
|
||||
'Click to launch game. Right-click to manage this version install'
|
||||
}
|
||||
onClick={async () => {
|
||||
if (needsRevisionUpdate(versionInfo.lastRevision, v)) {
|
||||
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
|
||||
versions?.set(
|
||||
'list',
|
||||
Object.fromEntries(
|
||||
Object.entries(versionsList).filter(
|
||||
([k]) => k !== v
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
await exists('game/' + v, {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
})
|
||||
)
|
||||
await remove('game/' + v, {
|
||||
baseDir: BaseDirectory.AppLocalData,
|
||||
recursive: true
|
||||
})
|
||||
|
||||
//reinstall
|
||||
setSelectedVersionList([v])
|
||||
downloadVersions([
|
||||
{
|
||||
id: v,
|
||||
type: 0
|
||||
}
|
||||
])
|
||||
} else {
|
||||
openFolder(v)
|
||||
}
|
||||
return
|
||||
}
|
||||
invoke('launch_game', {
|
||||
name: versionInfo.id,
|
||||
executable: versionInfo.executable,
|
||||
displayName: versionInfo.displayName,
|
||||
useWine: !!(
|
||||
platform() == 'linux' &&
|
||||
versionInfo.wine &&
|
||||
linuxUseWine
|
||||
),
|
||||
wineCommand: linuxWineCommand
|
||||
})
|
||||
}}
|
||||
onContextMenu={e => {
|
||||
e.preventDefault()
|
||||
setManagingVersion(v)
|
||||
setPopupMode(2)
|
||||
setShowPopup(true)
|
||||
setFadeOut(false)
|
||||
}}
|
||||
>
|
||||
<div className='h-18 w-screen relative'>
|
||||
<p className='text-2xl'>{versionInfo.displayName}</p>
|
||||
|
||||
<div className='flex gap-2 absolute left-0 bottom-0'>
|
||||
<div
|
||||
className='entry-info-item'
|
||||
title='The date the game was installed.'
|
||||
>
|
||||
<p>
|
||||
Installed{' '}
|
||||
{new Intl.DateTimeFormat(undefined).format(
|
||||
versionsList[v]
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item'
|
||||
title='This version is using wine. It cannot be guarenteed to work fully and might not work at all.'
|
||||
hidden={
|
||||
!(platform() == 'linux' && versionInfo.wine) ||
|
||||
needsRevisionUpdate(versionInfo.lastRevision, v)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faWarning} color='#ffc800' />
|
||||
<p>Uses wine</p>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item'
|
||||
hidden={
|
||||
!needsRevisionUpdate(versionInfo.lastRevision, v)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faWarning} color='#ffc800' />
|
||||
<p>Needs revision update!</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className='absolute right-0 bottom-0 button'
|
||||
title='Click to manage mods for this game!'
|
||||
onClick={async e => {
|
||||
e.stopPropagation()
|
||||
|
||||
if (
|
||||
!(await exists('game/' + v + '/BepInEx', {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
}))
|
||||
) {
|
||||
if (
|
||||
(await ask(
|
||||
"You don't have BepInEx (the mod loader for Unity Games), would you like to install it now? It is about 1MB in size. If you choose yes, it will download the recommended BepInEx version for " +
|
||||
versionInfo.displayName +
|
||||
'.',
|
||||
{ title: 'BepInEx not found!', kind: 'error' }
|
||||
)) &&
|
||||
!downloadProgress.find(d => d.version == v)
|
||||
) {
|
||||
downloadVersions([
|
||||
{
|
||||
id: v,
|
||||
type: 1
|
||||
}
|
||||
])
|
||||
}
|
||||
return
|
||||
}
|
||||
setManagingVersion(v)
|
||||
setPopupMode(3)
|
||||
setShowPopup(true)
|
||||
setFadeOut(false)
|
||||
}}
|
||||
hidden={!versionInfo.modSupportDownload}
|
||||
>
|
||||
Mod Manager
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,848 @@
|
||||
'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<string | null>(null)
|
||||
const [platformName, setPlatformName] = useState<string | null>(null)
|
||||
|
||||
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 [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 [notificationsAllowed, setNotificationsAllowed] =
|
||||
useState<boolean>(false)
|
||||
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 [versionsList, setVersionsList] = useState<Record<string, number>>({})
|
||||
const [modsList, setModsList] = useState<
|
||||
Record<string, Record<string, number>>
|
||||
>({})
|
||||
|
||||
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<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)
|
||||
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<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) 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<boolean>('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<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) {
|
||||
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<boolean>('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<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))
|
||||
}
|
||||
|
||||
watchSettings<string>('theme', v => setTheme(v ?? 'dark'))
|
||||
watchSettings<boolean>('notificationsAllowed', v =>
|
||||
setNotificationsAllowed(v ?? true)
|
||||
)
|
||||
watchSettings<boolean>('sidebarAlwaysShowGames', v =>
|
||||
setSidebarAlwaysShowGames(v ?? true)
|
||||
)
|
||||
watchSettings<boolean>('linuxUseWine', v => setLinuxUseWine(v ?? false))
|
||||
watchSettings<string>('linuxWineCommand', v =>
|
||||
setLinuxWineCommand(v ?? 'wine %path%')
|
||||
)
|
||||
|
||||
watchVersions<Record<string, number>>('list', v => setVersionsList(v ?? {}))
|
||||
watchVersions<Record<string, Record<string, number>>>('mods', v =>
|
||||
setModsList(v ?? {})
|
||||
)
|
||||
|
||||
return () => {
|
||||
unlisteners.forEach(u => u.then(fn => fn()))
|
||||
}
|
||||
}, [settings, versions])
|
||||
|
||||
return (
|
||||
<>
|
||||
<html lang='en' className={roboto.className}>
|
||||
<body className={theme + '-theme'}>
|
||||
{loading ? (
|
||||
<>
|
||||
<div
|
||||
className='relative z-2 w-screen border-b border-b-(--col3) h-8.25 bg-(--col1)'
|
||||
hidden={platformName != 'windows'}
|
||||
/>
|
||||
<div
|
||||
className={`w-screen ${
|
||||
platformName == 'windows'
|
||||
? 'h-[calc(100vh-64px)]'
|
||||
: 'h-screen'
|
||||
} flex items-center justify-center`}
|
||||
>
|
||||
{outdated ? (
|
||||
<div className='text-center'>
|
||||
<p className='text-6xl mb-4'>Outdated Launcher!</p>
|
||||
<p className='text-2xl mb-4'>
|
||||
Please update to the latest version to continue.
|
||||
</p>
|
||||
<button
|
||||
className='button btntheme1'
|
||||
onClick={() =>
|
||||
openUrl('https://games.lncvrt.xyz/download')
|
||||
}
|
||||
>
|
||||
Download latest version!
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<p className='text-7xl text-center'>{loadingText}</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<GlobalProvider
|
||||
value={{
|
||||
serverVersionList,
|
||||
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,
|
||||
notificationsAllowed,
|
||||
sidebarAlwaysShowGames,
|
||||
linuxUseWine,
|
||||
linuxWineCommand,
|
||||
theme,
|
||||
versionsList,
|
||||
modsList
|
||||
}}
|
||||
>
|
||||
<div
|
||||
tabIndex={0}
|
||||
onKeyDown={e => {
|
||||
if (showPopup && e.key === 'Escape') closePopup()
|
||||
}}
|
||||
>
|
||||
<Sidebar />
|
||||
<div
|
||||
className='relative z-2 ml-59.75 w-[calc(100vw-239px)] border-b border-b-(--col3) h-8.25 bg-(--col1)'
|
||||
hidden={platformName != 'windows'}
|
||||
/>
|
||||
<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 === '/') ||
|
||||
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 />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</GlobalProvider>
|
||||
)}
|
||||
</body>
|
||||
</html>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
'use client'
|
||||
|
||||
import './Installs.css'
|
||||
import { useEffect } from 'react'
|
||||
import { useGlobal } from '@/providers/GlobalProvider'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import {
|
||||
faCheck,
|
||||
faShieldHalved,
|
||||
faWarning
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
export default function Installs () {
|
||||
const {
|
||||
versionsList,
|
||||
serverVersionList,
|
||||
showPopup,
|
||||
setShowPopup,
|
||||
setPopupMode,
|
||||
setFadeOut,
|
||||
setSelectedVersionList,
|
||||
setSelectedGame,
|
||||
setCategory
|
||||
} = useGlobal()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (!showPopup) return
|
||||
setSelectedVersionList([])
|
||||
}, [setSelectedVersionList, showPopup])
|
||||
|
||||
return (
|
||||
<div className='mx-4 mt-4'>
|
||||
<div className='flex justify-between items-center mb-4'>
|
||||
<p className='text-3xl'>Games</p>
|
||||
<button
|
||||
className='button btntheme1'
|
||||
onClick={() => {
|
||||
setSelectedGame(null)
|
||||
setPopupMode(0)
|
||||
setShowPopup(true)
|
||||
setFadeOut(false)
|
||||
}}
|
||||
title='Click to download more games.'
|
||||
>
|
||||
Download game
|
||||
</button>
|
||||
</div>
|
||||
<div className='downloads-container'>
|
||||
<div
|
||||
className={`downloads-scroll ${
|
||||
platform() == 'windows'
|
||||
? 'h-[calc(100vh-116px)]'
|
||||
: 'h-[calc(100vh-84px)]'
|
||||
}`}
|
||||
>
|
||||
{serverVersionList?.games
|
||||
.filter(g =>
|
||||
serverVersionList.versions
|
||||
.filter(v => v.game === g.id)
|
||||
.some(v => Object.keys(versionsList).includes(v.id))
|
||||
)
|
||||
.map(i => (
|
||||
<div
|
||||
key={i.id}
|
||||
className={'downloads-entry'}
|
||||
title={'Click to view game installs'}
|
||||
onClick={() => {
|
||||
setCategory(-1)
|
||||
router.push('/main/game?id=' + i.id)
|
||||
}}
|
||||
>
|
||||
<div className='h-18 w-screen relative'>
|
||||
<p className='text-2xl'>{i.name}</p>
|
||||
|
||||
<div className='flex gap-2 absolute left-0 bottom-0'>
|
||||
<div
|
||||
className='entry-info-item'
|
||||
title='The amount of versions installed of this game in installed/installable format.'
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<p>
|
||||
{(() => {
|
||||
const gameVersions =
|
||||
serverVersionList.versions.filter(
|
||||
v => v.game === i.id
|
||||
)
|
||||
const installed = gameVersions.filter(v =>
|
||||
Object.keys(versionsList).includes(v.id)
|
||||
).length
|
||||
return gameVersions.length
|
||||
? `${installed}/${gameVersions.length}`
|
||||
: 'N/A'
|
||||
})()}{' '}
|
||||
versions installed
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item'
|
||||
hidden={!i.official}
|
||||
title='This game is official.'
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} color='#19c84b' />
|
||||
<p>Official</p>
|
||||
</div>
|
||||
<div
|
||||
className='entry-info-item'
|
||||
hidden={i.official}
|
||||
title={
|
||||
i.verified
|
||||
? 'This game is verified to be safe'
|
||||
: 'This game is not verified to be save. Proceed with caution.'
|
||||
}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={i.verified ? faShieldHalved : faWarning}
|
||||
color={i.verified ? '#19c84b' : '#ffc800'}
|
||||
/>
|
||||
<p>{i.verified ? 'Verified' : 'Unverified'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
'use client'
|
||||
|
||||
import { Setting } from '@/components/Setting'
|
||||
import { useGlobal } from '@/providers/GlobalProvider'
|
||||
import { copyToClipboard } from '@/lib/clipboard'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
|
||||
export default function Settings () {
|
||||
const {
|
||||
settings,
|
||||
version,
|
||||
notificationsAllowed,
|
||||
sidebarAlwaysShowGames,
|
||||
linuxUseWine,
|
||||
linuxWineCommand,
|
||||
theme
|
||||
} = useGlobal()
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='text-3xl ml-4 mt-4'>Settings</p>
|
||||
<div className='ml-4 mt-4 bg-(--col1) border border-(--col3) rounded-lg p-4 w-fit h-fit'>
|
||||
<Setting
|
||||
label='Allow sending notifications'
|
||||
value={notificationsAllowed}
|
||||
onChange={async () => {
|
||||
await settings?.set('notificationsAllowed', !notificationsAllowed)
|
||||
}}
|
||||
title='This setting does as you expect, allow the launcher to send notifications for when stuff like downloading is done.'
|
||||
/>
|
||||
<Setting
|
||||
label='Always show games in sidebar'
|
||||
value={sidebarAlwaysShowGames}
|
||||
onChange={async () => {
|
||||
await settings?.set(
|
||||
'sidebarAlwaysShowGames',
|
||||
!sidebarAlwaysShowGames
|
||||
)
|
||||
}}
|
||||
title="This setting will make it so when you are on a page like this, the games won't disappear."
|
||||
/>
|
||||
<Setting
|
||||
label='Use wine if needed'
|
||||
value={linuxUseWine}
|
||||
onChange={async () => {
|
||||
await settings?.set('linuxUseWine', !linuxUseWine)
|
||||
}}
|
||||
className={platform() == 'linux' ? '' : 'hidden'}
|
||||
/>
|
||||
<p hidden={!(platform() == 'linux' && linuxUseWine)}>Wine Command:</p>
|
||||
<input
|
||||
type='text'
|
||||
value={linuxWineCommand}
|
||||
onChange={async e => {
|
||||
await settings?.set('linuxWineCommand', e.target.value)
|
||||
}}
|
||||
className={`input-field my-1 ${
|
||||
platform() == 'linux' && linuxWineCommand ? '' : 'hidden'
|
||||
}`}
|
||||
></input>
|
||||
<div title='The theme you want the launcher to use.'>
|
||||
<label className='text-lg'>Theme:</label>
|
||||
<select
|
||||
className='ml-2 bg-(--col2) border border-(--col4) rounded-md'
|
||||
value={theme}
|
||||
onChange={async e => {
|
||||
await settings?.set('theme', e.target.value)
|
||||
}}
|
||||
>
|
||||
<option value={'dark'}>Dark (default)</option>
|
||||
<option value={'red'}>Red</option>
|
||||
<option value={'blue'}>Blue</option>
|
||||
<option value={'purple'}>Purple</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
className='fixed bottom-1.5 right-1.5 rounded-md cursor-pointer px-1 border z-100 transition-colors btntheme1'
|
||||
onClick={async () =>
|
||||
await copyToClipboard(`v${version}`, notificationsAllowed)
|
||||
}
|
||||
title='The current launcher version.'
|
||||
>
|
||||
v{version}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user