Almost finished with new launcher
@@ -1,17 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, ReactNode } from 'react'
|
||||
import { LauncherVersion } from './types/LauncherVersion'
|
||||
import { DownloadProgress } from './types/DownloadProgress'
|
||||
import { VersionsConfig } from './types/VersionsConfig'
|
||||
import { NormalConfig } from './types/NormalConfig'
|
||||
import { DownloadedVersion } from './types/DownloadedVersion'
|
||||
import { ServerVersionsResponse } from './types/ServerVersionsResponse'
|
||||
import { GameVersion } from './types/GameVersion'
|
||||
import { Game } from './types/Game'
|
||||
import { ReadonlyURLSearchParams } from 'next/navigation'
|
||||
|
||||
type GlobalCtxType = {
|
||||
versionList: LauncherVersion[] | null
|
||||
setVersionList: (v: LauncherVersion[] | null) => void
|
||||
selectedVersionList: LauncherVersion[]
|
||||
setSelectedVersionList: (v: LauncherVersion[]) => void
|
||||
serverVersionList: ServerVersionsResponse | null
|
||||
selectedVersionList: string[]
|
||||
setSelectedVersionList: (v: string[]) => void
|
||||
downloadProgress: DownloadProgress[]
|
||||
setDownloadProgress: (v: DownloadProgress[]) => void
|
||||
showPopup: boolean
|
||||
@@ -24,8 +25,12 @@ type GlobalCtxType = {
|
||||
setDownloadedVersionsConfig: (v: VersionsConfig | null) => void
|
||||
normalConfig: NormalConfig | null
|
||||
setNormalConfig: (v: NormalConfig | null) => void
|
||||
managingVersion: DownloadedVersion | null
|
||||
setManagingVersion: (v: DownloadedVersion | null) => void
|
||||
managingVersion: string | null
|
||||
setManagingVersion: (v: string | null) => void
|
||||
setSelectedGame: (v: number | null) => void
|
||||
getVersionInfo: (id: string | undefined) => GameVersion | undefined
|
||||
getVersionGame: (id: number | undefined) => Game | undefined
|
||||
getListOfGames: () => Game[]
|
||||
}
|
||||
|
||||
const GlobalCtx = createContext<GlobalCtxType | null>(null)
|
||||
|
||||
@@ -43,7 +43,7 @@ body {
|
||||
}
|
||||
|
||||
.popup-overlay {
|
||||
@apply fixed w-screen h-screen z-[99999] flex justify-center items-center animate-[fadeIn_0.2s_ease-out_forwards] left-0 top-0 bg-[rgba(0,0,0,0.5)];
|
||||
@apply fixed w-screen h-screen z-99999 flex justify-center items-center animate-[fadeIn_0.2s_ease-out_forwards] left-0 top-0 bg-[rgba(0,0,0,0.5)];
|
||||
}
|
||||
|
||||
.popup-overlay.fade-out {
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1,7 +1,7 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
.sidebar {
|
||||
@apply fixed top-0 left-0 w-60 h-screen bg-[#161616] flex flex-col border-e-[1px] border-[#242424] z-[1];
|
||||
@apply fixed top-0 left-0 w-60 h-screen bg-[#161616] flex flex-col border-e border-[#242424] z-1;
|
||||
}
|
||||
|
||||
.sidebar-downloads {
|
||||
|
||||
@@ -7,24 +7,28 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import {
|
||||
faCog,
|
||||
faDownload,
|
||||
faRankingStar,
|
||||
faServer
|
||||
faGamepad,
|
||||
faHexagonNodes
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faDiscord } from '@fortawesome/free-brands-svg-icons'
|
||||
import { useState } from 'react'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { useGlobal } from '../GlobalProvider'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { usePathname, useSearchParams } from 'next/navigation'
|
||||
|
||||
export default function Sidebar () {
|
||||
const [rot, setRot] = useState(0)
|
||||
const [dir, setDir] = useState(1)
|
||||
const { setShowPopup, setPopupMode, setFadeOut, downloadProgress } =
|
||||
useGlobal()
|
||||
const {
|
||||
getListOfGames,
|
||||
setShowPopup,
|
||||
setPopupMode,
|
||||
setFadeOut,
|
||||
downloadProgress
|
||||
} = useGlobal()
|
||||
|
||||
const pathname = usePathname()
|
||||
const params = useSearchParams()
|
||||
|
||||
return (
|
||||
<aside className='sidebar'>
|
||||
@@ -53,48 +57,46 @@ export default function Sidebar () {
|
||||
height={48}
|
||||
alt=''
|
||||
style={{
|
||||
transform: `rotate(${rot}deg)`,
|
||||
transition: 'transform 0.3s ease',
|
||||
marginTop: ['windows', 'macos'].includes(platform())
|
||||
? '20px'
|
||||
: '0px'
|
||||
: '0px',
|
||||
marginBottom: '-20px'
|
||||
}}
|
||||
onClick={() =>
|
||||
setRot(r => {
|
||||
let next = r + dir * 90
|
||||
if (next >= 360) {
|
||||
next = 360
|
||||
setDir(-1)
|
||||
} else if (next <= 0) {
|
||||
next = 0
|
||||
setDir(1)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
onContextMenu={() =>
|
||||
setRot(r => {
|
||||
let next = r - dir * 90
|
||||
if (next >= 360) {
|
||||
next = 360
|
||||
setDir(-1)
|
||||
} else if (next <= 0) {
|
||||
next = 0
|
||||
setDir(1)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<nav className='nav-links'>
|
||||
<Link
|
||||
draggable={false}
|
||||
href='/'
|
||||
className={`link ${pathname === '/' ? 'active' : ''}`}
|
||||
className={`link relative flex items-center ${
|
||||
pathname === '/' || pathname === '/game' ? 'active' : ''
|
||||
}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faServer} className='mr-1' /> Installs
|
||||
<FontAwesomeIcon icon={faHexagonNodes} className='mr-2' /> Games
|
||||
</Link>
|
||||
{getListOfGames()
|
||||
.sort((a, b) => {
|
||||
return a.id - b.id
|
||||
})
|
||||
.map(i => (
|
||||
<Link
|
||||
key={i.id}
|
||||
draggable={false}
|
||||
href={'/game?id=' + i.id}
|
||||
className={`link ${
|
||||
pathname === '/game' && Number(params.get('id') || 0) == i.id
|
||||
? 'active'
|
||||
: ''
|
||||
} ml-auto w-50 ${
|
||||
pathname === '/' || pathname === '/game' ? '' : 'hidden'
|
||||
}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faGamepad} className='mr-1' />{' '}
|
||||
{i.cutOff == null
|
||||
? i.name
|
||||
: i.name.substring(0, i.cutOff) + '...'}
|
||||
</Link>
|
||||
))}
|
||||
<Link
|
||||
draggable={false}
|
||||
href='/settings'
|
||||
@@ -102,13 +104,6 @@ export default function Sidebar () {
|
||||
>
|
||||
<FontAwesomeIcon icon={faCog} className='mr-1' /> Settings
|
||||
</Link>
|
||||
<Link
|
||||
draggable={false}
|
||||
href='/leaderboards'
|
||||
className={`link ${pathname === '/leaderboards' ? 'active' : ''}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faRankingStar} className='mr-1' /> Leaderboards
|
||||
</Link>
|
||||
<a
|
||||
draggable={false}
|
||||
onClick={() => openUrl('https://games.lncvrt.xyz/discord')}
|
||||
|
||||
155
src/app/game/page.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import '../Installs.css'
|
||||
import { format } from 'date-fns'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { message } from '@tauri-apps/plugin-dialog'
|
||||
import { useGlobal } from '../GlobalProvider'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
|
||||
export default function Installs () {
|
||||
const {
|
||||
downloadProgress,
|
||||
showPopup,
|
||||
setShowPopup,
|
||||
setPopupMode,
|
||||
setFadeOut,
|
||||
setSelectedVersionList,
|
||||
downloadedVersionsConfig,
|
||||
normalConfig,
|
||||
setManagingVersion,
|
||||
getVersionInfo,
|
||||
getVersionGame,
|
||||
setSelectedGame
|
||||
} = useGlobal()
|
||||
|
||||
const params = useSearchParams()
|
||||
|
||||
useEffect(() => {
|
||||
if (!showPopup) return
|
||||
setSelectedVersionList([])
|
||||
}, [normalConfig, setSelectedVersionList, showPopup])
|
||||
|
||||
return (
|
||||
<div className='mx-4 mt-4'>
|
||||
<div className='flex justify-between items-center mb-4'>
|
||||
<p className='text-3xl'>Installs</p>
|
||||
<button
|
||||
className='button text-3xl'
|
||||
onClick={() => {
|
||||
setSelectedGame(Number(params.get('id') || 0))
|
||||
setPopupMode(0)
|
||||
setShowPopup(true)
|
||||
setFadeOut(false)
|
||||
}}
|
||||
disabled={downloadProgress.length != 0}
|
||||
>
|
||||
Download versions
|
||||
</button>
|
||||
</div>
|
||||
<div className='downloads-container'>
|
||||
<div className='downloads-scroll'>
|
||||
{downloadedVersionsConfig && downloadedVersionsConfig.list.length ? (
|
||||
downloadedVersionsConfig.list
|
||||
.filter(v => {
|
||||
const info = getVersionInfo(v)
|
||||
if (!info) return false
|
||||
return info.game === Number(params.get('id') || 0)
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const infoA = getVersionInfo(a)
|
||||
const infoB = getVersionInfo(b)
|
||||
if (!infoA && !infoB) return 0
|
||||
if (!infoA) return 1
|
||||
if (!infoB) return -1
|
||||
return infoB.place - infoA.place
|
||||
})
|
||||
.map((entry, i) => (
|
||||
<div key={i} className='downloads-entry'>
|
||||
<div className='flex flex-col'>
|
||||
<p className='text-2xl'>
|
||||
{getVersionGame(getVersionInfo(entry)?.game)?.name} v
|
||||
{getVersionInfo(entry)?.versionName}
|
||||
</p>
|
||||
<p className='text-gray-400 text-md'>
|
||||
Installed{' '}
|
||||
{format(
|
||||
new Date(downloadedVersionsConfig.timestamps[entry]),
|
||||
'MM/dd/yyyy'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex flex-row items-center gap-2'>
|
||||
<button
|
||||
className='button'
|
||||
onClick={async () => {
|
||||
setManagingVersion(entry)
|
||||
setPopupMode(2)
|
||||
setShowPopup(true)
|
||||
setFadeOut(false)
|
||||
}}
|
||||
>
|
||||
Manage
|
||||
</button>
|
||||
<button
|
||||
className='button button-green'
|
||||
onClick={async () => {
|
||||
const verInfo = getVersionInfo(entry)
|
||||
if (verInfo == undefined) return
|
||||
let plat = platform()
|
||||
let willUseWine = false
|
||||
let cfg = null
|
||||
while (normalConfig != null) {
|
||||
cfg = normalConfig
|
||||
break
|
||||
}
|
||||
if (plat === 'macos' || plat === 'linux') {
|
||||
if (
|
||||
!verInfo.platforms.includes(plat) &&
|
||||
verInfo.platforms.includes('windows')
|
||||
) {
|
||||
if (
|
||||
cfg != null &&
|
||||
!cfg.settings.useWineOnUnixWhenNeeded
|
||||
) {
|
||||
await message(
|
||||
'Wine support is disabled in settings and this version requires wine',
|
||||
{
|
||||
title: 'Wine is needed to load this version',
|
||||
kind: 'error'
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
plat = 'windows'
|
||||
willUseWine = true
|
||||
}
|
||||
}
|
||||
invoke('launch_game', {
|
||||
name: verInfo.id,
|
||||
executable:
|
||||
verInfo.executables[
|
||||
verInfo.platforms.indexOf(plat)
|
||||
],
|
||||
wine: willUseWine,
|
||||
wineCommand: cfg?.settings.wineOnUnixCommand
|
||||
})
|
||||
}}
|
||||
>
|
||||
Launch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className='flex justify-center items-center h-full'>
|
||||
<p className='text-3xl'>No games installed</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import Sidebar from './componets/Sidebar'
|
||||
import './Globals.css'
|
||||
import { LauncherVersion } from './types/LauncherVersion'
|
||||
import { DownloadProgress } from './types/DownloadProgress'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { arch, platform } from '@tauri-apps/plugin-os'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faAdd, faRemove, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
faAdd,
|
||||
faChevronLeft,
|
||||
faDownload,
|
||||
faRemove,
|
||||
faXmark
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
isPermissionGranted,
|
||||
requestPermission,
|
||||
@@ -21,13 +25,17 @@ import {
|
||||
writeVersionsConfig
|
||||
} from './util/BazookaManager'
|
||||
import { VersionsConfig } from './types/VersionsConfig'
|
||||
import { DownloadedVersion } from './types/DownloadedVersion'
|
||||
import { NormalConfig } from './types/NormalConfig'
|
||||
import { app } from '@tauri-apps/api'
|
||||
import axios from 'axios'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { GlobalProvider } from './GlobalProvider'
|
||||
import { Roboto } from 'next/font/google'
|
||||
import { ServerVersionsResponse } from './types/ServerVersionsResponse'
|
||||
import { GameVersion } from './types/GameVersion'
|
||||
import { Game } from './types/Game'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { usePathname, useSearchParams } from 'next/navigation'
|
||||
|
||||
const roboto = Roboto({
|
||||
subsets: ['latin']
|
||||
@@ -41,94 +49,24 @@ export default function RootLayout ({
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [loadingText, setLoadingText] = useState('Loading...')
|
||||
const [outdated, setOutdated] = useState(false)
|
||||
const [versionList, setVersionList] = useState<null | LauncherVersion[]>(null)
|
||||
const [selectedVersionList, setSelectedVersionList] = useState<
|
||||
LauncherVersion[]
|
||||
>([])
|
||||
const [downloadProgress, setDownloadProgress] = useState<DownloadProgress[]>(
|
||||
[]
|
||||
)
|
||||
const [showPopup, setShowPopup] = useState(false)
|
||||
const [popupMode, setPopupMode] = useState<null | number>(null)
|
||||
const [fadeOut, setFadeOut] = useState(false)
|
||||
const activeDownloads = useRef(0)
|
||||
const queue = useRef<(() => void)[]>([])
|
||||
|
||||
const [serverVersionList, setServerVersionList] =
|
||||
useState<null | ServerVersionsResponse>(null)
|
||||
const [selectedVersionList, setSelectedVersionList] = useState<string[]>([])
|
||||
|
||||
const [downloadedVersionsConfig, setDownloadedVersionsConfig] =
|
||||
useState<VersionsConfig | null>(null)
|
||||
const [normalConfig, setNormalConfig] = useState<NormalConfig | null>(null)
|
||||
const [managingVersion, setManagingVersion] =
|
||||
useState<DownloadedVersion | null>(null)
|
||||
|
||||
function runNext () {
|
||||
if (activeDownloads.current === 0 && queue.current.length === 0) {
|
||||
setFadeOut(true)
|
||||
setTimeout(() => setShowPopup(false), 200)
|
||||
return
|
||||
}
|
||||
if (activeDownloads.current >= 3 || queue.current.length === 0) return
|
||||
activeDownloads.current++
|
||||
const next = queue.current.shift()
|
||||
next?.()
|
||||
}
|
||||
const [showPopup, setShowPopup] = useState(false)
|
||||
const [popupMode, setPopupMode] = useState<null | number>(null)
|
||||
const [fadeOut, setFadeOut] = useState(false)
|
||||
|
||||
async function downloadVersions (versions: LauncherVersion[]) {
|
||||
while (normalConfig != null) {
|
||||
const useWine = normalConfig.settings.useWineOnUnixWhenNeeded
|
||||
const p = platform()
|
||||
const newDownloads = versions.map(
|
||||
v => new DownloadProgress(v, 0, false, true)
|
||||
)
|
||||
setDownloadProgress(prev => [...prev, ...newDownloads])
|
||||
|
||||
newDownloads.forEach(download => {
|
||||
let plat = p
|
||||
if (p === 'linux' && useWine) {
|
||||
if (
|
||||
!download.version.platforms.includes(p) &&
|
||||
download.version.platforms.includes('windows')
|
||||
) {
|
||||
plat = 'windows'
|
||||
}
|
||||
}
|
||||
const idx = download.version.platforms.indexOf(plat)
|
||||
const url = download.version.downloadUrls[idx]
|
||||
const exe = download.version.executables[idx]
|
||||
|
||||
if (!url) {
|
||||
setDownloadProgress(prev =>
|
||||
prev.map(d =>
|
||||
d.version.version === download.version.version
|
||||
? { ...d, failed: true }
|
||||
: d
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const task = () => {
|
||||
setDownloadProgress(prev => {
|
||||
const i = prev.findIndex(
|
||||
d => d.version.version === download.version.version
|
||||
)
|
||||
if (i === -1) return prev
|
||||
const copy = [...prev]
|
||||
copy[i] = { ...copy[i], queued: false }
|
||||
return copy
|
||||
})
|
||||
|
||||
invoke('download', {
|
||||
url,
|
||||
name: download.version.version,
|
||||
executable: exe
|
||||
})
|
||||
}
|
||||
|
||||
queue.current.push(task)
|
||||
runNext()
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
const [downloadProgress, setDownloadProgress] = useState<DownloadProgress[]>(
|
||||
[]
|
||||
)
|
||||
const [managingVersion, setManagingVersion] = useState<string | null>(null)
|
||||
const [selectedGame, setSelectedGame] = useState<number | null>(null)
|
||||
|
||||
function handleOverlayClick (e: React.MouseEvent<HTMLDivElement>) {
|
||||
if (e.target === e.currentTarget) {
|
||||
@@ -154,6 +92,49 @@ export default function RootLayout ({
|
||||
[normalConfig]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
let unlistenProgress: (() => void) | null = null
|
||||
let unlistenUninstalled: (() => void) | null = null
|
||||
|
||||
listen<string>('download-progress', event => {
|
||||
const [versionName, progStr] = event.payload.split(':')
|
||||
const prog = Number(progStr)
|
||||
setDownloadProgress(prev => {
|
||||
const i = prev.findIndex(d => d.version === versionName)
|
||||
if (i === -1) return prev
|
||||
const copy = [...prev]
|
||||
copy[i] = { ...copy[i], progress: prog }
|
||||
return copy
|
||||
})
|
||||
}).then(f => (unlistenProgress = f))
|
||||
|
||||
listen<string>('version-uninstalled', event => {
|
||||
const versionName = event.payload
|
||||
setDownloadedVersionsConfig(prev => {
|
||||
if (!prev) return prev
|
||||
const updatedList = prev.list.filter(v => v !== versionName)
|
||||
const updatedTimestamps = Object.fromEntries(
|
||||
Object.entries(prev.timestamps).filter(([k]) => k !== versionName)
|
||||
)
|
||||
const updatedConfig = {
|
||||
...prev,
|
||||
list: updatedList,
|
||||
timestamps: updatedTimestamps
|
||||
}
|
||||
writeVersionsConfig(updatedConfig)
|
||||
setManagingVersion(null)
|
||||
setFadeOut(true)
|
||||
setTimeout(() => setShowPopup(false), 200)
|
||||
return updatedConfig
|
||||
})
|
||||
}).then(f => (unlistenUninstalled = f))
|
||||
|
||||
return () => {
|
||||
unlistenProgress?.()
|
||||
unlistenUninstalled?.()
|
||||
}
|
||||
}, [notifyUser])
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
@@ -172,6 +153,16 @@ export default function RootLayout ({
|
||||
return
|
||||
}
|
||||
}
|
||||
setLoadingText('Downloading version list...')
|
||||
try {
|
||||
const res = await axios.get(
|
||||
'https://games.lncvrt.xyz/api/launcher/versions'
|
||||
)
|
||||
setServerVersionList(res.data)
|
||||
} catch {
|
||||
setLoadingText('Failed to download versions list.')
|
||||
return
|
||||
}
|
||||
setLoadingText('Loading configs...')
|
||||
const normalConfig = await readNormalConfig()
|
||||
const versionsConfig = await readVersionsConfig()
|
||||
@@ -181,100 +172,199 @@ export default function RootLayout ({
|
||||
})()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let unlistenProgress: (() => void) | null = null
|
||||
let unlistenDone: (() => void) | null = null
|
||||
let unlistenFailed: (() => void) | null = null
|
||||
let unlistenUninstalled: (() => void) | null = null
|
||||
|
||||
listen<string>('download-progress', event => {
|
||||
const [versionName, progStr] = event.payload.split(':')
|
||||
const prog = Number(progStr)
|
||||
setDownloadProgress(prev => {
|
||||
const i = prev.findIndex(d => d.version.version === versionName)
|
||||
if (i === -1) return prev
|
||||
const copy = [...prev]
|
||||
copy[i] = { ...copy[i], progress: prog }
|
||||
return copy
|
||||
})
|
||||
}).then(f => (unlistenProgress = f))
|
||||
|
||||
listen<string>('download-done', event => {
|
||||
const versionName = event.payload
|
||||
setDownloadProgress(prev => {
|
||||
const downloaded = prev.find(d => d.version.version === versionName)
|
||||
if (!downloaded) return prev
|
||||
|
||||
setDownloadedVersionsConfig(prevConfig => {
|
||||
if (!prevConfig) return prevConfig
|
||||
const newDownloaded = DownloadedVersion.import(downloaded.version)
|
||||
const updatedConfig = {
|
||||
...prevConfig,
|
||||
list: [...prevConfig.list, newDownloaded]
|
||||
}
|
||||
writeVersionsConfig(updatedConfig)
|
||||
return updatedConfig
|
||||
})
|
||||
|
||||
return prev.filter(d => d.version.version !== versionName)
|
||||
})
|
||||
|
||||
activeDownloads.current--
|
||||
runNext()
|
||||
|
||||
setDownloadProgress(curr => {
|
||||
if (curr.length === 0)
|
||||
notifyUser('Downloads Complete', 'All downloads have completed.')
|
||||
return curr
|
||||
})
|
||||
}).then(f => (unlistenDone = f))
|
||||
|
||||
listen<string>('download-failed', async event => {
|
||||
const versionName = event.payload
|
||||
setDownloadProgress(prev =>
|
||||
prev.map(d =>
|
||||
d.version.version === versionName ? { ...d, failed: true } : d
|
||||
)
|
||||
)
|
||||
|
||||
activeDownloads.current--
|
||||
runNext()
|
||||
await notifyUser(
|
||||
'Download Failed',
|
||||
`The download for version ${versionName} has failed.`
|
||||
)
|
||||
}).then(f => (unlistenFailed = f))
|
||||
|
||||
listen<string>('version-uninstalled', event => {
|
||||
const versionName = event.payload
|
||||
setDownloadedVersionsConfig(prev => {
|
||||
if (!prev) return prev
|
||||
const updatedList = prev.list.filter(
|
||||
v => v.version.version !== versionName
|
||||
)
|
||||
const updatedConfig = { ...prev, list: updatedList }
|
||||
writeVersionsConfig(updatedConfig)
|
||||
setManagingVersion(null)
|
||||
setFadeOut(true)
|
||||
setTimeout(() => setShowPopup(false), 200)
|
||||
return updatedConfig
|
||||
})
|
||||
}).then(f => (unlistenUninstalled = f))
|
||||
|
||||
return () => {
|
||||
unlistenProgress?.()
|
||||
unlistenDone?.()
|
||||
unlistenFailed?.()
|
||||
unlistenUninstalled?.()
|
||||
}
|
||||
}, [notifyUser])
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => e.preventDefault()
|
||||
document.addEventListener('contextmenu', handler)
|
||||
return () => document.removeEventListener('contextmenu', handler)
|
||||
}, [])
|
||||
|
||||
function getSpecialVersionsList (game?: number): GameVersion[] {
|
||||
if (!normalConfig || !serverVersionList) return []
|
||||
|
||||
const useWine = normalConfig.settings.useWineOnUnixWhenNeeded
|
||||
const p = platform()
|
||||
const a = arch()
|
||||
|
||||
return serverVersionList.versions
|
||||
.filter(v => !downloadedVersionsConfig?.list.includes(v.id))
|
||||
.filter(v => {
|
||||
if (game && v.game != game) return false
|
||||
if (p === 'macos' || p === 'linux') {
|
||||
if (useWine) {
|
||||
return (
|
||||
v.platforms.includes('windows-x86') ||
|
||||
v.platforms.includes('windows-x64') ||
|
||||
v.platforms.includes(p)
|
||||
)
|
||||
}
|
||||
return v.platforms.includes(p)
|
||||
}
|
||||
|
||||
if (p === 'windows') {
|
||||
if (a === 'x86') return v.platforms.includes('windows-x86')
|
||||
if (a === 'x86_64')
|
||||
return (
|
||||
v.platforms.includes('windows-x86') ||
|
||||
v.platforms.includes('windows-x64')
|
||||
)
|
||||
if (a === 'aarch64') return v.platforms.includes('windows-arm64')
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (b.game !== a.game) return a.game - b.game
|
||||
return b.place - a.place
|
||||
})
|
||||
}
|
||||
|
||||
function getDownloadLink (version: GameVersion): string | undefined {
|
||||
const p = platform()
|
||||
const a = arch()
|
||||
|
||||
const findUrl = (plat: string) => {
|
||||
const i = version.platforms.indexOf(plat)
|
||||
return i >= 0 ? version.downloadUrls[i] : undefined
|
||||
}
|
||||
|
||||
if (p === 'windows') {
|
||||
if (a === 'x86') return findUrl('windows-x86')
|
||||
if (a === 'x86_64')
|
||||
return findUrl('windows-x64') || findUrl('windows-x86')
|
||||
if (a === 'aarch64') return findUrl('windows-arm64')
|
||||
}
|
||||
|
||||
if (p === 'macos' || p === 'linux') {
|
||||
if (normalConfig?.settings.useWineOnUnixWhenNeeded) {
|
||||
return findUrl('windows-x86') || findUrl('windows-x64') || findUrl(p)
|
||||
}
|
||||
return findUrl(p)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function getExecutableName (version: GameVersion): string | undefined {
|
||||
const p = platform()
|
||||
const a = arch()
|
||||
|
||||
const findUrl = (plat: string) => {
|
||||
const i = version.platforms.indexOf(plat)
|
||||
return i >= 0 ? version.executables[i] : undefined
|
||||
}
|
||||
|
||||
if (p === 'windows') {
|
||||
if (a === 'x86') return findUrl('windows-x86')
|
||||
if (a === 'x86_64')
|
||||
return findUrl('windows-x64') || findUrl('windows-x86')
|
||||
}
|
||||
|
||||
if (p === 'macos' || p === 'linux') {
|
||||
if (normalConfig?.settings.useWineOnUnixWhenNeeded) {
|
||||
return findUrl('windows-x86') || findUrl('windows-x64') || findUrl(p)
|
||||
}
|
||||
return findUrl(p)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function getVersionInfo (id: string | undefined): GameVersion | undefined {
|
||||
if (!id) return undefined
|
||||
return serverVersionList?.versions.find(v => v.id === id)
|
||||
}
|
||||
|
||||
function getVersionGame (game: number | undefined): Game | undefined {
|
||||
if (!game) return undefined
|
||||
return serverVersionList?.games.find(g => g.id === game)
|
||||
}
|
||||
|
||||
function getListOfGames (): Game[] {
|
||||
if (!downloadedVersionsConfig?.list) return []
|
||||
|
||||
const gamesMap = new Map<number, Game>()
|
||||
|
||||
downloadedVersionsConfig.list.forEach(i => {
|
||||
const version = getVersionInfo(i)
|
||||
if (!version) return
|
||||
const game = getVersionGame(version.game)
|
||||
if (!game) return
|
||||
gamesMap.set(game.id, game)
|
||||
})
|
||||
|
||||
return Array.from(gamesMap.values())
|
||||
}
|
||||
|
||||
async function downloadVersions (): Promise<void> {
|
||||
const list = selectedVersionList
|
||||
setSelectedVersionList([])
|
||||
|
||||
const newDownloads = list.map(
|
||||
version => new DownloadProgress(version, 0, false, true)
|
||||
)
|
||||
|
||||
setDownloadProgress(newDownloads)
|
||||
|
||||
for (const download of newDownloads) {
|
||||
const info = getVersionInfo(download.version)
|
||||
if (!info) {
|
||||
setDownloadProgress(prev =>
|
||||
prev.filter(d => d.version !== download.version)
|
||||
)
|
||||
return
|
||||
}
|
||||
const downloadLink = getDownloadLink(info)
|
||||
if (!downloadLink) {
|
||||
setDownloadProgress(prev =>
|
||||
prev.filter(d => d.version !== download.version)
|
||||
)
|
||||
return
|
||||
}
|
||||
const executableName = getExecutableName(info)
|
||||
if (!executableName) {
|
||||
setDownloadProgress(prev =>
|
||||
prev.filter(d => d.version !== download.version)
|
||||
)
|
||||
return
|
||||
}
|
||||
setDownloadProgress(prev =>
|
||||
prev.map(d =>
|
||||
d.version === download.version ? { ...d, queued: false } : d
|
||||
)
|
||||
)
|
||||
const res = await invoke<string>('download', {
|
||||
url: downloadLink,
|
||||
name: info.id,
|
||||
executable: executableName
|
||||
})
|
||||
if (res == '1') {
|
||||
setDownloadProgress(prev =>
|
||||
prev.filter(d => d.version !== download.version)
|
||||
)
|
||||
let data = downloadedVersionsConfig
|
||||
if (!data) {
|
||||
setDownloadProgress(prev =>
|
||||
prev.filter(d => d.version !== download.version)
|
||||
)
|
||||
return
|
||||
}
|
||||
const date = Date.now()
|
||||
data.list = [...data.list, download.version]
|
||||
data.timestamps = { ...data.timestamps, [download.version]: date }
|
||||
setDownloadedVersionsConfig(data)
|
||||
writeVersionsConfig(data)
|
||||
} else {
|
||||
setDownloadProgress(prev =>
|
||||
prev.map(d =>
|
||||
d.version === download.version
|
||||
? { ...d, queued: false, failed: true, progress: 0 }
|
||||
: d
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<html lang='en' className={roboto.className}>
|
||||
@@ -301,73 +391,54 @@ export default function RootLayout ({
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<GlobalProvider
|
||||
value={{
|
||||
serverVersionList,
|
||||
selectedVersionList,
|
||||
setSelectedVersionList,
|
||||
downloadProgress,
|
||||
setDownloadProgress,
|
||||
showPopup,
|
||||
setShowPopup,
|
||||
popupMode,
|
||||
setPopupMode,
|
||||
fadeOut,
|
||||
setFadeOut,
|
||||
downloadedVersionsConfig,
|
||||
setDownloadedVersionsConfig,
|
||||
normalConfig,
|
||||
setNormalConfig,
|
||||
managingVersion,
|
||||
setManagingVersion,
|
||||
getVersionInfo,
|
||||
getVersionGame,
|
||||
getListOfGames,
|
||||
setSelectedGame
|
||||
}}
|
||||
>
|
||||
<div
|
||||
tabIndex={0}
|
||||
onKeyDown={e => {
|
||||
if (showPopup && e.key === 'Escape') {
|
||||
setFadeOut(true)
|
||||
setTimeout(() => setShowPopup(false), 200)
|
||||
if (popupMode == 0 && selectedGame) {
|
||||
setSelectedGame(null)
|
||||
setSelectedVersionList([])
|
||||
} else {
|
||||
setFadeOut(true)
|
||||
setTimeout(() => setShowPopup(false), 200)
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<GlobalProvider
|
||||
value={{
|
||||
versionList,
|
||||
setVersionList,
|
||||
selectedVersionList,
|
||||
setSelectedVersionList,
|
||||
downloadProgress,
|
||||
setDownloadProgress,
|
||||
showPopup,
|
||||
setShowPopup,
|
||||
popupMode,
|
||||
setPopupMode,
|
||||
fadeOut,
|
||||
setFadeOut,
|
||||
downloadedVersionsConfig,
|
||||
setDownloadedVersionsConfig,
|
||||
normalConfig,
|
||||
setNormalConfig,
|
||||
managingVersion,
|
||||
setManagingVersion
|
||||
}}
|
||||
>
|
||||
<Sidebar />
|
||||
</GlobalProvider>
|
||||
<Sidebar />
|
||||
<div
|
||||
className='relative z-[2] ml-[239px] w-[761px] border-b border-[#242424] h-[33px] bg-[#161616]'
|
||||
className='relative z-2 ml-[239px] w-[761px] border-b border-[#242424] h-[33px] bg-[#161616]'
|
||||
style={{
|
||||
display: platform() == 'windows' ? 'block' : 'none'
|
||||
display: platform() === 'windows' ? 'block' : 'none'
|
||||
}}
|
||||
></div>
|
||||
/>
|
||||
<div className='relative z-0'>
|
||||
<main style={{ marginLeft: '15rem' }}>
|
||||
<GlobalProvider
|
||||
value={{
|
||||
versionList,
|
||||
setVersionList,
|
||||
selectedVersionList,
|
||||
setSelectedVersionList,
|
||||
downloadProgress,
|
||||
setDownloadProgress,
|
||||
showPopup,
|
||||
setShowPopup,
|
||||
popupMode,
|
||||
setPopupMode,
|
||||
fadeOut,
|
||||
setFadeOut,
|
||||
downloadedVersionsConfig,
|
||||
setDownloadedVersionsConfig,
|
||||
normalConfig,
|
||||
setNormalConfig,
|
||||
managingVersion,
|
||||
setManagingVersion
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</GlobalProvider>
|
||||
</main>
|
||||
<main style={{ marginLeft: '15rem' }}>{children}</main>
|
||||
</div>
|
||||
{showPopup && (
|
||||
<div
|
||||
@@ -378,76 +449,91 @@ export default function RootLayout ({
|
||||
<button
|
||||
className='close-button'
|
||||
onClick={() => {
|
||||
setFadeOut(true)
|
||||
setTimeout(() => setShowPopup(false), 200)
|
||||
if (popupMode == 0 && selectedGame) {
|
||||
setSelectedGame(null)
|
||||
setSelectedVersionList([])
|
||||
} else {
|
||||
setFadeOut(true)
|
||||
setTimeout(() => setShowPopup(false), 200)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
popupMode == 0 && selectedGame
|
||||
? faChevronLeft
|
||||
: faXmark
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
{popupMode === 0 ? (
|
||||
{popupMode === 0 && selectedGame ? (
|
||||
<>
|
||||
<p className='text-xl text-center'>
|
||||
Select versions to download
|
||||
</p>
|
||||
<div className='popup-content'>
|
||||
{versionList == null ? (
|
||||
<p className='text-center'>
|
||||
Getting version list...
|
||||
</p>
|
||||
) : (
|
||||
versionList
|
||||
.filter(
|
||||
v =>
|
||||
!downloadedVersionsConfig?.list.some(
|
||||
dv => dv.version.version === v.version
|
||||
)
|
||||
)
|
||||
.map((v, i) => (
|
||||
<div key={i} className='popup-entry'>
|
||||
<p className='text-2xl'>
|
||||
Berry Dash v{v.displayName}
|
||||
</p>
|
||||
<button
|
||||
className='button right-2 bottom-2'
|
||||
onClick={() => {
|
||||
if (!selectedVersionList) return
|
||||
if (!selectedVersionList.includes(v)) {
|
||||
setSelectedVersionList([
|
||||
...selectedVersionList,
|
||||
v
|
||||
])
|
||||
} else {
|
||||
setSelectedVersionList(
|
||||
selectedVersionList.filter(
|
||||
x => x !== v
|
||||
)
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectedVersionList.includes(v) ? (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faRemove} />{' '}
|
||||
Remove
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAdd} /> Add
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
{getSpecialVersionsList(selectedGame).map(
|
||||
(v, i) => (
|
||||
<div key={i} className='popup-entry'>
|
||||
<p className='text-2xl'>
|
||||
{getVersionGame(v.game)?.name} v
|
||||
{v.versionName}
|
||||
</p>
|
||||
<button
|
||||
className='button right-2 bottom-2'
|
||||
onClick={() => {
|
||||
setSelectedVersionList(prev =>
|
||||
prev.includes(v.id)
|
||||
? prev.filter(i => i !== v.id)
|
||||
: [...prev, v.id]
|
||||
)
|
||||
}}
|
||||
>
|
||||
{selectedVersionList.includes(v.id) ? (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faRemove} />{' '}
|
||||
Remove
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAdd} /> Add
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : popupMode === 0 && !selectedGame ? (
|
||||
<>
|
||||
<p className='text-xl text-center'>
|
||||
Select a game to download
|
||||
</p>
|
||||
<div className='popup-content'>
|
||||
{serverVersionList?.games.map((v, i) => (
|
||||
<div key={i} className='popup-entry'>
|
||||
<p className='text-2xl'>{v.name}</p>
|
||||
<button
|
||||
className='button right-2 bottom-2'
|
||||
onClick={() => setSelectedGame(v.id)}
|
||||
>
|
||||
<>
|
||||
<FontAwesomeIcon icon={faDownload} />{' '}
|
||||
Download
|
||||
</>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : popupMode === 1 ? (
|
||||
<>
|
||||
<p className='text-xl text-center'>Downloads</p>
|
||||
<div className='popup-content'>
|
||||
{downloadProgress.length === 0 ? (
|
||||
<p className='text-center mt-6'>
|
||||
Nothing here...
|
||||
No more downloads!
|
||||
</p>
|
||||
) : (
|
||||
downloadProgress.map((v, i) => (
|
||||
@@ -456,7 +542,12 @@ export default function RootLayout ({
|
||||
className='popup-entry flex flex-col justify-between'
|
||||
>
|
||||
<p className='text-2xl'>
|
||||
Berry Dash v{v.version.displayName}
|
||||
{
|
||||
getVersionGame(
|
||||
getVersionInfo(v.version)?.game
|
||||
)?.name
|
||||
}{' '}
|
||||
v{getVersionInfo(v.version)?.versionName}
|
||||
</p>
|
||||
<div className='mt-[25px] flex items-center justify-between'>
|
||||
{v.failed ? (
|
||||
@@ -467,13 +558,13 @@ export default function RootLayout ({
|
||||
</span>
|
||||
<button
|
||||
className='button ml-30 mb-2'
|
||||
onClick={() =>
|
||||
onClick={() => {
|
||||
setDownloadProgress(prev =>
|
||||
prev.filter(
|
||||
(_, idx) => idx !== i
|
||||
d => d.version !== v.version
|
||||
)
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -498,15 +589,20 @@ export default function RootLayout ({
|
||||
managingVersion ? (
|
||||
<>
|
||||
<p className='text-xl text-center'>
|
||||
Manage version{' '}
|
||||
{managingVersion.version.displayName}
|
||||
Manage{' '}
|
||||
{
|
||||
getVersionGame(
|
||||
getVersionInfo(managingVersion)?.game
|
||||
)?.name
|
||||
}{' '}
|
||||
v{getVersionInfo(managingVersion)?.versionName}
|
||||
</p>
|
||||
<div className='popup-content flex flex-col items-center justify-center gap-2 h-full'>
|
||||
<button
|
||||
className='button'
|
||||
onClick={() =>
|
||||
invoke('uninstall_version', {
|
||||
name: managingVersion.version.version
|
||||
name: managingVersion
|
||||
})
|
||||
}
|
||||
>
|
||||
@@ -516,33 +612,12 @@ export default function RootLayout ({
|
||||
className='button'
|
||||
onClick={async () =>
|
||||
invoke('open_folder', {
|
||||
name: managingVersion.version.version
|
||||
name: managingVersion
|
||||
})
|
||||
}
|
||||
>
|
||||
Open Folder
|
||||
</button>
|
||||
<button
|
||||
className='button'
|
||||
style={{
|
||||
display:
|
||||
platform() == 'macos' ? 'block' : 'none'
|
||||
}}
|
||||
onClick={async () => {
|
||||
const exe =
|
||||
managingVersion.version.executables[
|
||||
managingVersion.version.platforms.indexOf(
|
||||
platform()
|
||||
)
|
||||
]
|
||||
await invoke('fix_mac_permissions', {
|
||||
name: managingVersion.version.version,
|
||||
executable: exe
|
||||
})
|
||||
}}
|
||||
>
|
||||
Fix permissions
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
@@ -551,58 +626,44 @@ export default function RootLayout ({
|
||||
</p>
|
||||
)
|
||||
) : null}
|
||||
{popupMode == 0 && versionList != null && (
|
||||
<div className='flex justify-center'>
|
||||
<button
|
||||
className='button w-fit mt-2 mb-[-16px]'
|
||||
onClick={() => {
|
||||
setFadeOut(true)
|
||||
setTimeout(() => setShowPopup(false), 200)
|
||||
downloadVersions(selectedVersionList)
|
||||
}}
|
||||
>
|
||||
Download {selectedVersionList.length} version
|
||||
{selectedVersionList.length == 1 ? '' : 's'}
|
||||
</button>
|
||||
<button
|
||||
className='button w-fit mt-2 ml-2 mb-[-16px]'
|
||||
onClick={() => {
|
||||
const filtered = versionList.filter(
|
||||
v =>
|
||||
!downloadedVersionsConfig?.list.some(
|
||||
dv => dv.version.version === v.version
|
||||
)
|
||||
)
|
||||
if (
|
||||
selectedVersionList.length ===
|
||||
filtered.length &&
|
||||
filtered.every(v =>
|
||||
selectedVersionList.includes(v)
|
||||
{popupMode == 0 &&
|
||||
selectedGame &&
|
||||
serverVersionList != null && (
|
||||
<div className='flex justify-center'>
|
||||
<button
|
||||
className='button w-fit mt-2 -mb-4'
|
||||
onClick={() => {
|
||||
setFadeOut(true)
|
||||
setTimeout(() => setShowPopup(false), 200)
|
||||
downloadVersions()
|
||||
}}
|
||||
>
|
||||
Download {selectedVersionList.length} version
|
||||
{selectedVersionList.length == 1 ? '' : 's'}
|
||||
</button>
|
||||
<button
|
||||
className='button w-fit mt-2 ml-2 -mb-4'
|
||||
onClick={() => {
|
||||
const allIds = getSpecialVersionsList(
|
||||
selectedGame
|
||||
).map(v => v.id)
|
||||
setSelectedVersionList(prev =>
|
||||
prev.length === allIds.length ? [] : allIds
|
||||
)
|
||||
) {
|
||||
setSelectedVersionList([])
|
||||
} else {
|
||||
setSelectedVersionList(filtered)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectedVersionList.length ===
|
||||
versionList.filter(
|
||||
v =>
|
||||
!downloadedVersionsConfig?.list.some(
|
||||
dv => dv.version.version === v.version
|
||||
)
|
||||
).length
|
||||
? 'Deselect All'
|
||||
: 'Select All'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
}}
|
||||
>
|
||||
{selectedVersionList.length ===
|
||||
getSpecialVersionsList(selectedGame).length
|
||||
? 'Deselect All'
|
||||
: 'Select All'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</GlobalProvider>
|
||||
)}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
.leaderboard-container {
|
||||
@apply flex items-end justify-center gap-3;
|
||||
}
|
||||
|
||||
.leaderboard-scroll {
|
||||
@apply h-[510px] bg-[#161616] border border-[#242424] rounded-lg overflow-y-auto w-[475px] relative;
|
||||
}
|
||||
|
||||
.leaderboard-entry {
|
||||
@apply flex items-center m-2 p-4 rounded-lg text-gray-200 text-lg transition-colors cursor-default bg-[#242424] hover:bg-[#323232] border border-[#484848] hover:border-[#565656];
|
||||
}
|
||||
|
||||
.leaderboard-entry p.score {
|
||||
@apply font-mono text-blue-500 text-lg;
|
||||
}
|
||||
|
||||
.side-dropdown {
|
||||
@apply flex items-end min-w-[52px];
|
||||
}
|
||||
|
||||
.dropdown-root {
|
||||
@apply relative w-max;
|
||||
}
|
||||
|
||||
.dropdown-btn {
|
||||
@apply px-3 py-2 rounded-md bg-[#242424] disabled:bg-[#161616] border border-[#484848] disabled:border-[#383838] text-gray-200 hover:bg-[#323232] hover:border-[#565656] transition-colors cursor-pointer;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
@apply absolute bottom-full mb-2 w-55 bg-[#242424] border border-[#484848] rounded-md shadow-lg hidden z-50;
|
||||
}
|
||||
|
||||
.dropdown-left .dropdown-menu {
|
||||
@apply left-0;
|
||||
}
|
||||
|
||||
.dropdown-right .dropdown-menu {
|
||||
@apply right-0 w-42;
|
||||
}
|
||||
|
||||
.dropdown-menu.open {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
@apply block w-full text-left px-4 py-2 hover:bg-[#323232] text-gray-200 cursor-pointer;
|
||||
}
|
||||
|
||||
.dropdown-item.selected {
|
||||
@apply bg-[#323232] hover:bg-[#484848];
|
||||
}
|
||||
@@ -1,361 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import './Leaderboards.css'
|
||||
import axios from 'axios'
|
||||
import { app } from '@tauri-apps/api'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { decrypt, encrypt } from '../util/Encryption'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import Image from 'next/image'
|
||||
import { LeaderboardResponse } from '../types/LeaderboardResponse'
|
||||
import { faChevronDown } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { getKey } from '../util/KeysHelper'
|
||||
|
||||
import Berry from '../assets/berries/Berry.png'
|
||||
import PoisonBerry from '../assets/berries/PoisonBerry.png'
|
||||
import SlowBerry from '../assets/berries/SlowBerry.png'
|
||||
import UltraBerry from '../assets/berries/UltraBerry.png'
|
||||
import SpeedyBerry from '../assets/berries/SpeedyBerry.png'
|
||||
import CoinBerry from '../assets/berries/CoinBerry.png'
|
||||
import RainbowBerry from '../componets/RandomBerry'
|
||||
import AntiBerry from '../assets/berries/AntiBerry.png'
|
||||
|
||||
export default function Leaderboards () {
|
||||
const [leaderboardData, setLeaderboardData] =
|
||||
useState<LeaderboardResponse | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [leftOpen, setLeftOpen] = useState(false)
|
||||
const [rightOpen, setRightOpen] = useState(false)
|
||||
const leftRef = useRef<HTMLDivElement | null>(null)
|
||||
const rightRef = useRef<HTMLDivElement | null>(null)
|
||||
const formatter = new Intl.NumberFormat('en-US')
|
||||
const [leaderboardType, setLeaderboardType] = useState<number>(0)
|
||||
const [berryType, setBerryType] = useState<number>(0)
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setLeaderboardData(null)
|
||||
try {
|
||||
const launcherVersion = await app.getVersion()
|
||||
const sendKey = await getKey(1)
|
||||
const formData = new URLSearchParams()
|
||||
formData.append(
|
||||
await encrypt('type', sendKey),
|
||||
await encrypt(leaderboardType.toString(), sendKey)
|
||||
)
|
||||
if (leaderboardType == 1) {
|
||||
formData.append(
|
||||
await encrypt('showType', sendKey),
|
||||
await encrypt(berryType.toString(), sendKey)
|
||||
)
|
||||
}
|
||||
const response = await axios.post(
|
||||
'https://games.lncvrt.xyz/database/berrydash/getTopPlayers.php',
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
Requester: 'BerryDashLauncher',
|
||||
LauncherVersion: launcherVersion,
|
||||
ClientPlatform: platform()
|
||||
}
|
||||
}
|
||||
)
|
||||
const decrypted = await decrypt(response.data)
|
||||
setLeaderboardData(JSON.parse(decrypted))
|
||||
} catch (e) {
|
||||
console.error('Error fetching leaderboard data:', e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [leaderboardType, berryType])
|
||||
|
||||
function downloadLeaderboard () {
|
||||
let content = '"Username","Score","ScoreFormatted"\n'
|
||||
leaderboardData?.entries.forEach(entry => {
|
||||
content += `"${entry.username}","${entry.value}","${formatter.format(
|
||||
BigInt(entry.value)
|
||||
)}"\n`
|
||||
})
|
||||
while (content.endsWith('\n')) {
|
||||
content = content.slice(0, -1)
|
||||
}
|
||||
invoke('download_leaderboard', { content })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh()
|
||||
}, [refresh])
|
||||
|
||||
useEffect(() => {
|
||||
function onDocClick (e: MouseEvent) {
|
||||
const t = e.target as Node
|
||||
if (leftRef.current && !leftRef.current.contains(t)) setLeftOpen(false)
|
||||
if (rightRef.current && !rightRef.current.contains(t)) setRightOpen(false)
|
||||
}
|
||||
document.addEventListener('mousedown', onDocClick)
|
||||
return () => document.removeEventListener('mousedown', onDocClick)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='mx-4 mt-4'>
|
||||
<div className='flex justify-between items-center mb-4'>
|
||||
<p className='text-3xl'>Leaderboards</p>
|
||||
<div className='flex gap-2'>
|
||||
<button
|
||||
className='button text-3xl'
|
||||
onClick={downloadLeaderboard}
|
||||
disabled={loading || leaderboardData?.entries?.length === 0}
|
||||
>
|
||||
Download Leaderboards
|
||||
</button>
|
||||
<button
|
||||
className='button text-3xl'
|
||||
onClick={refresh}
|
||||
disabled={loading}
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='leaderboard-container'>
|
||||
<div className='side-dropdown'>
|
||||
<div ref={leftRef} className='dropdown-root dropdown-left'>
|
||||
<button
|
||||
className='dropdown-btn'
|
||||
onClick={() => setLeftOpen(v => !v)}
|
||||
aria-expanded={leftOpen}
|
||||
disabled={loading}
|
||||
>
|
||||
Type{' '}
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronDown}
|
||||
className={leftOpen ? 'rotate-180' : ''}
|
||||
/>
|
||||
</button>
|
||||
<div className={`dropdown-menu ${leftOpen ? 'open' : ''}`}>
|
||||
<button
|
||||
className={`dropdown-item ${
|
||||
leaderboardType == 0 ? 'selected' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
setLeftOpen(false)
|
||||
setLeaderboardType(0)
|
||||
}}
|
||||
>
|
||||
Score Leaderboard
|
||||
</button>
|
||||
<button
|
||||
className={`dropdown-item ${
|
||||
leaderboardType == 1 ? 'selected' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
setLeftOpen(false)
|
||||
setLeaderboardType(1)
|
||||
}}
|
||||
>
|
||||
Berry Leaderboard
|
||||
</button>
|
||||
<button
|
||||
className={`dropdown-item ${
|
||||
leaderboardType == 2 ? 'selected' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
setLeftOpen(false)
|
||||
setLeaderboardType(2)
|
||||
}}
|
||||
>
|
||||
Coins Leaderboard
|
||||
</button>
|
||||
<button
|
||||
className={`dropdown-item ${
|
||||
leaderboardType == 3 ? 'selected' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
setLeftOpen(false)
|
||||
setLeaderboardType(3)
|
||||
}}
|
||||
>
|
||||
Legacy Leaderboard
|
||||
</button>
|
||||
<button
|
||||
className={`dropdown-item ${
|
||||
leaderboardType == 4 ? 'selected' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
setLeftOpen(false)
|
||||
setLeaderboardType(4)
|
||||
}}
|
||||
>
|
||||
Total Berries Leaderboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='leaderboard-scroll'>
|
||||
{leaderboardData?.entries?.length ? (
|
||||
leaderboardData.entries.map((entry, i) => (
|
||||
<div key={i} className='leaderboard-entry justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Image
|
||||
src={
|
||||
entry.customIcon == null
|
||||
? entry.overlay == 0
|
||||
? `https://berrydash-api.lncvrt.xyz/icon?r=${entry.birdColor[0]}&g=${entry.birdColor[1]}&b=${entry.birdColor[2]}&id=${entry.icon}`
|
||||
: `https://berrydash-api.lncvrt.xyz/iconandoverlay?br=${entry.birdColor[0]}&bg=${entry.birdColor[1]}&bb=${entry.birdColor[2]}&bid=${entry.icon}&or=${entry.overlayColor[0]}&og=${entry.overlayColor[1]}&ob=${entry.overlayColor[2]}&oid=${entry.overlay}`
|
||||
: `data:image/png;base64,${
|
||||
leaderboardData.customIcons[entry.customIcon]
|
||||
}`
|
||||
}
|
||||
width={28}
|
||||
height={28}
|
||||
alt=''
|
||||
className='scale-x-[-1] -ml-2'
|
||||
onError={e => {
|
||||
;(e.currentTarget as HTMLImageElement).style.display =
|
||||
'none'
|
||||
}}
|
||||
/>
|
||||
<p>
|
||||
{entry.username} (#{i + 1})
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='score'>
|
||||
{formatter.format(BigInt(entry.value))}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : loading ? (
|
||||
<div className='flex justify-center items-center h-full'>
|
||||
<p className='text-3xl'>Loading...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex justify-center items-center h-full'>
|
||||
<p className='text-3xl'>No data...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='side-dropdown'>
|
||||
<div ref={rightRef} className='dropdown-root dropdown-right'>
|
||||
<button
|
||||
className='dropdown-btn'
|
||||
onClick={() => setRightOpen(v => !v)}
|
||||
aria-expanded={rightOpen}
|
||||
disabled={loading || leaderboardType != 1}
|
||||
>
|
||||
Berry Type{' '}
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronDown}
|
||||
className={rightOpen ? 'rotate-180' : ''}
|
||||
/>
|
||||
</button>
|
||||
<div className={`dropdown-menu ${rightOpen ? 'open' : ''}`}>
|
||||
<button
|
||||
className={`dropdown-item ${berryType == 0 ? 'selected' : ''}`}
|
||||
onClick={() => {
|
||||
setRightOpen(false)
|
||||
setBerryType(0)
|
||||
}}
|
||||
>
|
||||
<span className='flex items-center gap-2'>
|
||||
<Image src={Berry} width={24} height={24} alt='' />
|
||||
Normal Berry
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className={`dropdown-item ${berryType == 1 ? 'selected' : ''}`}
|
||||
onClick={() => {
|
||||
setRightOpen(false)
|
||||
setBerryType(1)
|
||||
}}
|
||||
>
|
||||
<span className='flex items-center gap-2'>
|
||||
<Image src={PoisonBerry} width={24} height={24} alt='' />
|
||||
Poison Berry
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className={`dropdown-item ${berryType == 2 ? 'selected' : ''}`}
|
||||
onClick={() => {
|
||||
setRightOpen(false)
|
||||
setBerryType(2)
|
||||
}}
|
||||
>
|
||||
<span className='flex items-center gap-2'>
|
||||
<Image src={SlowBerry} width={24} height={24} alt='' />
|
||||
Slow Berry
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className={`dropdown-item ${berryType == 3 ? 'selected' : ''}`}
|
||||
onClick={() => {
|
||||
setRightOpen(false)
|
||||
setBerryType(3)
|
||||
}}
|
||||
>
|
||||
<span className='flex items-center gap-2'>
|
||||
<Image src={UltraBerry} width={24} height={24} alt='' />
|
||||
Ultra Berry
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className={`dropdown-item ${berryType == 4 ? 'selected' : ''}`}
|
||||
onClick={() => {
|
||||
setRightOpen(false)
|
||||
setBerryType(4)
|
||||
}}
|
||||
>
|
||||
<span className='flex items-center gap-2'>
|
||||
<Image src={SpeedyBerry} width={24} height={24} alt='' />
|
||||
Speedy Berry
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className={`dropdown-item ${berryType == 5 ? 'selected' : ''}`}
|
||||
onClick={() => {
|
||||
setRightOpen(false)
|
||||
setBerryType(5)
|
||||
}}
|
||||
>
|
||||
<span className='flex items-center gap-2'>
|
||||
<Image src={CoinBerry} width={24} height={24} alt='' />
|
||||
Coin Berry
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className={`dropdown-item ${berryType == 5 ? 'selected' : ''}`}
|
||||
onClick={() => {
|
||||
setRightOpen(false)
|
||||
setBerryType(6)
|
||||
}}
|
||||
>
|
||||
<span className='flex items-center gap-2'>
|
||||
<RainbowBerry />
|
||||
Random Berry
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className={`dropdown-item ${berryType == 5 ? 'selected' : ''}`}
|
||||
onClick={() => {
|
||||
setRightOpen(false)
|
||||
setBerryType(7)
|
||||
}}
|
||||
>
|
||||
<span className='flex items-center gap-2'>
|
||||
<Image src={AntiBerry} width={24} height={24} alt='' />
|
||||
Anti Berry
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
117
src/app/page.tsx
@@ -1,13 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import axios from 'axios'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import './Installs.css'
|
||||
import { format } from 'date-fns'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { message } from '@tauri-apps/plugin-dialog'
|
||||
import { useGlobal } from './GlobalProvider'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function Installs () {
|
||||
const {
|
||||
@@ -17,135 +17,62 @@ export default function Installs () {
|
||||
setPopupMode,
|
||||
setFadeOut,
|
||||
setSelectedVersionList,
|
||||
setVersionList,
|
||||
downloadedVersionsConfig,
|
||||
normalConfig,
|
||||
setManagingVersion
|
||||
setManagingVersion,
|
||||
getVersionInfo,
|
||||
getVersionGame,
|
||||
setSelectedGame,
|
||||
getListOfGames
|
||||
} = useGlobal()
|
||||
|
||||
useEffect(() => {
|
||||
if (!showPopup) return
|
||||
setSelectedVersionList([])
|
||||
setVersionList(null)
|
||||
;(async () => {
|
||||
try {
|
||||
while (normalConfig != null) {
|
||||
const useWine = normalConfig.settings.useWineOnUnixWhenNeeded
|
||||
const res = await axios.get(
|
||||
'https://games.lncvrt.xyz/database/launcher/versions.php'
|
||||
)
|
||||
const p = platform()
|
||||
const filtered = res.data.filter((d: { platforms: string[] }) =>
|
||||
p === 'macos' || p === 'linux'
|
||||
? useWine
|
||||
? d.platforms.includes('windows') || d.platforms.includes(p)
|
||||
: d.platforms.includes(p)
|
||||
: d.platforms.includes(p)
|
||||
)
|
||||
setVersionList(filtered)
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
setVersionList([])
|
||||
}
|
||||
})()
|
||||
}, [normalConfig, setSelectedVersionList, setVersionList, showPopup])
|
||||
}, [normalConfig, setSelectedVersionList, showPopup])
|
||||
|
||||
return (
|
||||
<div className='mx-4 mt-4'>
|
||||
<div className='flex justify-between items-center mb-4'>
|
||||
<p className='text-3xl'>Installs</p>
|
||||
<p className='text-3xl'>Games</p>
|
||||
<button
|
||||
className='button text-3xl'
|
||||
onClick={() => {
|
||||
setSelectedGame(null)
|
||||
setPopupMode(0)
|
||||
setShowPopup(true)
|
||||
setFadeOut(false)
|
||||
}}
|
||||
disabled={downloadProgress.length != 0}
|
||||
>
|
||||
Download new version
|
||||
Download game
|
||||
</button>
|
||||
</div>
|
||||
<div className='downloads-container'>
|
||||
<div className='downloads-scroll'>
|
||||
{downloadedVersionsConfig && downloadedVersionsConfig.list.length ? (
|
||||
downloadedVersionsConfig.list
|
||||
.sort((a, b) => b.version.id - a.version.id)
|
||||
.map((entry, i) => (
|
||||
<div key={i} className='downloads-entry'>
|
||||
getListOfGames()
|
||||
.sort((a, b) => {
|
||||
return a.id - b.id
|
||||
})
|
||||
.map(i => (
|
||||
<div key={i.id} className='downloads-entry'>
|
||||
<div className='flex flex-col'>
|
||||
<p className='text-2xl'>
|
||||
Berry Dash v{entry.version.displayName}
|
||||
</p>
|
||||
<p className='text-2xl'>{i.name}</p>
|
||||
<p className='text-gray-400 text-md'>
|
||||
Installed{' '}
|
||||
{format(new Date(entry.installDate), 'MM/dd/yyyy')}
|
||||
Installed {format(new Date(), 'MM/dd/yyyy')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex flex-row items-center gap-2'>
|
||||
<button
|
||||
className='button'
|
||||
onClick={async () => {
|
||||
setManagingVersion(entry)
|
||||
setPopupMode(2)
|
||||
setShowPopup(true)
|
||||
setFadeOut(false)
|
||||
}}
|
||||
>
|
||||
Manage
|
||||
</button>
|
||||
<button
|
||||
className='button button-green'
|
||||
onClick={async () => {
|
||||
let plat = platform()
|
||||
let willUseWine = false
|
||||
let cfg = null
|
||||
while (normalConfig != null) {
|
||||
cfg = normalConfig
|
||||
break
|
||||
}
|
||||
if (plat === 'macos' || plat === 'linux') {
|
||||
if (
|
||||
!entry.version.platforms.includes(plat) &&
|
||||
entry.version.platforms.includes('windows')
|
||||
) {
|
||||
if (
|
||||
cfg != null &&
|
||||
!cfg.settings.useWineOnUnixWhenNeeded
|
||||
) {
|
||||
await message(
|
||||
'Wine support is disabled in settings and this version requires wine',
|
||||
{
|
||||
title: 'Wine is needed to load this version',
|
||||
kind: 'error'
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
plat = 'windows'
|
||||
willUseWine = true
|
||||
}
|
||||
}
|
||||
invoke('launch_game', {
|
||||
name: entry.version.version,
|
||||
executable:
|
||||
entry.version.executables[
|
||||
entry.version.platforms.indexOf(plat)
|
||||
],
|
||||
wine: willUseWine,
|
||||
wineCommand: cfg?.settings.wineOnUnixCommand
|
||||
})
|
||||
}}
|
||||
>
|
||||
Launch
|
||||
</button>
|
||||
<Link className='button' href={'/game?id=' + i.id}>
|
||||
Installs
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className='flex justify-center items-center h-full'>
|
||||
<p className='text-3xl'>No versions installed</p>
|
||||
<p className='text-3xl'>No games installed</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -7,8 +7,6 @@ import { platform } from '@tauri-apps/plugin-os'
|
||||
import { useGlobal } from '../GlobalProvider'
|
||||
|
||||
export default function Settings () {
|
||||
const [checkForNewVersionOnLoad, setCheckForNewVersionOnLoad] =
|
||||
useState(false)
|
||||
const [allowNotifications, setAllowNotifications] = useState(false)
|
||||
const [useWineOnUnixWhenNeeded, setUseWineOnUnixWhenNeeded] = useState(false)
|
||||
const [wineOnUnixCommand, setWineOnUnixCommand] = useState('wine %path%')
|
||||
@@ -18,9 +16,6 @@ export default function Settings () {
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
while (normalConfig != null) {
|
||||
setCheckForNewVersionOnLoad(
|
||||
normalConfig.settings.checkForNewVersionOnLoad
|
||||
)
|
||||
setUseWineOnUnixWhenNeeded(
|
||||
normalConfig.settings.useWineOnUnixWhenNeeded
|
||||
)
|
||||
@@ -37,19 +32,6 @@ export default function Settings () {
|
||||
<p className='text-3xl ml-4 mt-4'>Settings</p>
|
||||
{loaded && (
|
||||
<div className='ml-4 mt-4 bg-[#161616] border border-[#242424] rounded-lg p-4 w-fit h-fit'>
|
||||
<Setting
|
||||
label='Check for new version on load'
|
||||
value={checkForNewVersionOnLoad}
|
||||
onChange={async () => {
|
||||
while (normalConfig != null) {
|
||||
setCheckForNewVersionOnLoad(!checkForNewVersionOnLoad)
|
||||
normalConfig.settings.checkForNewVersionOnLoad =
|
||||
!checkForNewVersionOnLoad
|
||||
await writeNormalConfig(normalConfig)
|
||||
break
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Setting
|
||||
label='Allow sending notifications'
|
||||
value={allowNotifications}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { LauncherVersion } from './LauncherVersion'
|
||||
|
||||
export class DownloadProgress {
|
||||
constructor (
|
||||
public version: LauncherVersion,
|
||||
constructor(
|
||||
public version: string,
|
||||
public progress: number,
|
||||
public failed: boolean,
|
||||
public queued: boolean
|
||||
) {}
|
||||
) { }
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { LauncherVersion } from './LauncherVersion'
|
||||
|
||||
export class DownloadedVersion {
|
||||
constructor (
|
||||
public version: LauncherVersion,
|
||||
public installDate: number = Date.now()
|
||||
) {}
|
||||
|
||||
static import (data: LauncherVersion) {
|
||||
return new DownloadedVersion(data)
|
||||
}
|
||||
}
|
||||
7
src/app/types/Game.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface Game {
|
||||
id: number
|
||||
name: string
|
||||
official: boolean
|
||||
verified: boolean
|
||||
cutOff: number | null
|
||||
}
|
||||
10
src/app/types/GameVersion.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface GameVersion {
|
||||
id: string
|
||||
versionName: string
|
||||
releaseDate: number
|
||||
downloadUrls: string[]
|
||||
platforms: string[]
|
||||
executables: string[]
|
||||
game: number
|
||||
place: number
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { DownloadedVersion } from './DownloadedVersion'
|
||||
import { DownloadProgress } from './DownloadProgress'
|
||||
import { LauncherVersion } from './LauncherVersion'
|
||||
import { NormalConfig } from './NormalConfig'
|
||||
import { VersionsConfig } from './VersionsConfig'
|
||||
|
||||
export type InstallsProps = {
|
||||
downloadProgress: DownloadProgress[]
|
||||
showPopup: boolean
|
||||
setShowPopup: (v: boolean) => void
|
||||
setPopupMode: (v: null | number) => void
|
||||
setFadeOut: (v: boolean) => void
|
||||
setSelectedVersionList: (v: LauncherVersion[]) => void
|
||||
setVersionList: (v: null | LauncherVersion[]) => void
|
||||
downloadedVersionsConfig: VersionsConfig | null
|
||||
normalConfig: NormalConfig | null
|
||||
setManagingVersion: (v: DownloadedVersion | null) => void
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export interface LauncherVersion {
|
||||
version: string
|
||||
displayName: string
|
||||
platforms: string[]
|
||||
downloadUrls: string[]
|
||||
executables: string[]
|
||||
id: number
|
||||
}
|
||||
7
src/app/types/ServerVersionsResponse.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Game } from "./Game"
|
||||
import { GameVersion } from "./GameVersion"
|
||||
|
||||
export interface ServerVersionsResponse {
|
||||
versions: GameVersion[]
|
||||
games: Game[]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { NormalConfig } from './NormalConfig'
|
||||
|
||||
export type SettingsProps = {
|
||||
normalConfig: NormalConfig | null
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
export class SettingsType {
|
||||
constructor (
|
||||
public checkForNewVersionOnLoad: boolean = true,
|
||||
constructor(
|
||||
public allowNotifications: boolean = true,
|
||||
public useWineOnUnixWhenNeeded: boolean = false,
|
||||
public wineOnUnixCommand: string = 'wine %path%'
|
||||
) {}
|
||||
) { }
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { DownloadProgress } from './DownloadProgress'
|
||||
|
||||
export type SidebarProps = {
|
||||
setShowPopup: (v: boolean) => void
|
||||
setPopupMode: (v: null | number) => void
|
||||
setFadeOut: (v: boolean) => void
|
||||
downloadProgress: DownloadProgress[]
|
||||
}
|
||||
@@ -1,16 +1,20 @@
|
||||
import { DownloadedVersion } from './DownloadedVersion'
|
||||
|
||||
type VersionsConfigData = {
|
||||
version: string
|
||||
list: DownloadedVersion[]
|
||||
list: string[]
|
||||
timestamps: Record<string, number>
|
||||
}
|
||||
|
||||
export class VersionsConfig {
|
||||
constructor (public version: string, public list: DownloadedVersion[] = []) {}
|
||||
constructor(
|
||||
public version: string,
|
||||
public list: string[] = [],
|
||||
public timestamps: Record<string, number> = {}
|
||||
) { }
|
||||
|
||||
static import (data: VersionsConfigData) {
|
||||
static import(data: VersionsConfigData) {
|
||||
const cfg = new VersionsConfig(data.version)
|
||||
cfg.list = [...data.list]
|
||||
cfg.timestamps = { ...data.timestamps }
|
||||
return cfg
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,126 +8,118 @@ import {
|
||||
readTextFile,
|
||||
writeFile
|
||||
} from '@tauri-apps/plugin-fs'
|
||||
import { decrypt, encrypt } from './Encryption'
|
||||
import { VersionsConfig } from '../types/VersionsConfig'
|
||||
import { getKey } from './KeysHelper'
|
||||
|
||||
export async function readNormalConfig (): Promise<NormalConfig> {
|
||||
export async function readNormalConfig(): Promise<NormalConfig> {
|
||||
const version = await app.getVersion()
|
||||
try {
|
||||
const options = {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
}
|
||||
const doesFolderExist = await exists('', options)
|
||||
const doesConfigExist = await exists('config.dat', options)
|
||||
const doesConfigExist = await exists('config.json', options)
|
||||
if (!doesFolderExist || !doesConfigExist) {
|
||||
if (!doesFolderExist) {
|
||||
await mkdir('', options)
|
||||
}
|
||||
const file = await create('config.dat', options)
|
||||
const file = await create('config.json', options)
|
||||
await file.write(
|
||||
new TextEncoder().encode(
|
||||
await encrypt(
|
||||
JSON.stringify(new NormalConfig(version)),
|
||||
await getKey(2)
|
||||
)
|
||||
JSON.stringify(new NormalConfig(version), null, 2),
|
||||
)
|
||||
)
|
||||
await file.close()
|
||||
return new NormalConfig(version)
|
||||
}
|
||||
const config = await readTextFile('config.dat', options)
|
||||
const config = await readTextFile('config.json', options)
|
||||
return NormalConfig.import(
|
||||
JSON.parse(await decrypt(config, await getKey(2)))
|
||||
JSON.parse(config)
|
||||
)
|
||||
} catch {
|
||||
return new NormalConfig(version)
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeNormalConfig (data: NormalConfig) {
|
||||
export async function writeNormalConfig(data: NormalConfig) {
|
||||
const options = {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
}
|
||||
const doesFolderExist = await exists('', options)
|
||||
const doesConfigExist = await exists('config.dat', options)
|
||||
const doesConfigExist = await exists('config.json', options)
|
||||
if (!doesFolderExist || !doesConfigExist) {
|
||||
if (!doesFolderExist) {
|
||||
await mkdir('', options)
|
||||
}
|
||||
const file = await create('config.dat', options)
|
||||
const file = await create('config.json', options)
|
||||
await file.write(
|
||||
new TextEncoder().encode(
|
||||
await encrypt(JSON.stringify(data), await getKey(2))
|
||||
JSON.stringify(data, null, 2)
|
||||
)
|
||||
)
|
||||
await file.close()
|
||||
} else {
|
||||
await writeFile(
|
||||
'config.dat',
|
||||
'config.json',
|
||||
new TextEncoder().encode(
|
||||
await encrypt(JSON.stringify(data), await getKey(2))
|
||||
JSON.stringify(data, null, 2)
|
||||
),
|
||||
options
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function readVersionsConfig (): Promise<VersionsConfig> {
|
||||
export async function readVersionsConfig(): Promise<VersionsConfig> {
|
||||
const version = await app.getVersion()
|
||||
try {
|
||||
const options = {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
}
|
||||
const doesFolderExist = await exists('', options)
|
||||
const doesConfigExist = await exists('versions.dat', options)
|
||||
const doesConfigExist = await exists('versions.json', options)
|
||||
if (!doesFolderExist || !doesConfigExist) {
|
||||
if (!doesFolderExist) {
|
||||
await mkdir('', options)
|
||||
}
|
||||
const file = await create('versions.dat', options)
|
||||
const file = await create('versions.json', options)
|
||||
await file.write(
|
||||
new TextEncoder().encode(
|
||||
await encrypt(
|
||||
JSON.stringify(new VersionsConfig(version)),
|
||||
await getKey(3)
|
||||
)
|
||||
JSON.stringify(new VersionsConfig(version), null, 2)
|
||||
)
|
||||
)
|
||||
await file.close()
|
||||
return new VersionsConfig(version)
|
||||
}
|
||||
const config = await readTextFile('versions.dat', options)
|
||||
const config = await readTextFile('versions.json', options)
|
||||
return VersionsConfig.import(
|
||||
JSON.parse(await decrypt(config, await getKey(3)))
|
||||
JSON.parse(config)
|
||||
)
|
||||
} catch {
|
||||
return new VersionsConfig(version)
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeVersionsConfig (data: VersionsConfig) {
|
||||
export async function writeVersionsConfig(data: VersionsConfig) {
|
||||
const options = {
|
||||
baseDir: BaseDirectory.AppLocalData
|
||||
}
|
||||
const doesFolderExist = await exists('', options)
|
||||
const doesConfigExist = await exists('versions.dat', options)
|
||||
const doesConfigExist = await exists('versions.json', options)
|
||||
if (!doesFolderExist || !doesConfigExist) {
|
||||
if (!doesFolderExist) {
|
||||
await mkdir('', options)
|
||||
}
|
||||
const file = await create('versions.dat', options)
|
||||
const file = await create('versions.json', options)
|
||||
await file.write(
|
||||
new TextEncoder().encode(
|
||||
await encrypt(JSON.stringify(data), await getKey(3))
|
||||
JSON.stringify(data, null, 2)
|
||||
)
|
||||
)
|
||||
await file.close()
|
||||
} else {
|
||||
await writeFile(
|
||||
'versions.dat',
|
||||
'versions.json',
|
||||
new TextEncoder().encode(
|
||||
await encrypt(JSON.stringify(data), await getKey(3))
|
||||
JSON.stringify(data, null, 2)
|
||||
),
|
||||
options
|
||||
)
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import CryptoJS from 'crypto-js'
|
||||
import { getKey } from './KeysHelper'
|
||||
|
||||
export async function encrypt (
|
||||
plainText: string,
|
||||
key?: string
|
||||
): Promise<string> {
|
||||
if (!key) key = await getKey(1)
|
||||
const iv = CryptoJS.lib.WordArray.random(16)
|
||||
const encrypted = CryptoJS.AES.encrypt(
|
||||
plainText,
|
||||
CryptoJS.enc.Utf8.parse(key),
|
||||
{
|
||||
iv,
|
||||
mode: CryptoJS.mode.CBC,
|
||||
padding: CryptoJS.pad.Pkcs7
|
||||
}
|
||||
)
|
||||
const combined = iv.concat(encrypted.ciphertext)
|
||||
return CryptoJS.enc.Base64.stringify(combined)
|
||||
}
|
||||
|
||||
export async function decrypt (dataB64: string, key?: string): Promise<string> {
|
||||
if (!key) key = await getKey(0)
|
||||
const data = CryptoJS.enc.Base64.parse(dataB64)
|
||||
const iv = CryptoJS.lib.WordArray.create(data.words.slice(0, 4), 16)
|
||||
const ciphertext = CryptoJS.lib.WordArray.create(
|
||||
data.words.slice(4),
|
||||
data.sigBytes - 16
|
||||
)
|
||||
const cipherParams = CryptoJS.lib.CipherParams.create({ ciphertext })
|
||||
|
||||
const decrypted = CryptoJS.AES.decrypt(
|
||||
cipherParams,
|
||||
CryptoJS.enc.Utf8.parse(key),
|
||||
{
|
||||
iv,
|
||||
mode: CryptoJS.mode.CBC,
|
||||
padding: CryptoJS.pad.Pkcs7
|
||||
}
|
||||
)
|
||||
const result = decrypted.toString(CryptoJS.enc.Utf8)
|
||||
if (!result) throw new Error(await encrypt('-997'))
|
||||
return result
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
export async function getKey (key: number): Promise<string> {
|
||||
try {
|
||||
const message = await invoke('get_keys_config', { key })
|
||||
return message as string
|
||||
} catch (error) {
|
||||
console.error('Failed to get key from Tauri backend', error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||