612 lines
23 KiB
TypeScript
612 lines
23 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useEffect, useRef, 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 { 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 {
|
|
isPermissionGranted,
|
|
requestPermission,
|
|
sendNotification
|
|
} from '@tauri-apps/plugin-notification'
|
|
import {
|
|
readNormalConfig,
|
|
readVersionsConfig,
|
|
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'
|
|
|
|
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 [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 [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?.()
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
function handleOverlayClick (e: React.MouseEvent<HTMLDivElement>) {
|
|
if (e.target === e.currentTarget) {
|
|
setFadeOut(true)
|
|
setTimeout(() => setShowPopup(false), 200)
|
|
}
|
|
}
|
|
|
|
const notifyUser = useCallback(
|
|
async (title: string, body: string) => {
|
|
if (!normalConfig?.settings.allowNotifications) return
|
|
|
|
let permissionGranted = await isPermissionGranted()
|
|
if (!permissionGranted) {
|
|
const permission = await requestPermission()
|
|
permissionGranted = permission === 'granted'
|
|
}
|
|
|
|
if (permissionGranted) {
|
|
sendNotification({ title, body })
|
|
}
|
|
},
|
|
[normalConfig]
|
|
)
|
|
|
|
useEffect(() => {
|
|
;(async () => {
|
|
if (process.env.NODE_ENV === 'production') {
|
|
setLoadingText('Checking latest version...')
|
|
try {
|
|
const response = await axios.get(
|
|
'https://games.lncvrt.xyz/database/launcher/latest.php'
|
|
)
|
|
const client = await app.getVersion()
|
|
if (response.data !== client) {
|
|
setOutdated(true)
|
|
return
|
|
}
|
|
} catch {
|
|
setLoadingText('Failed to check latest version.')
|
|
return
|
|
}
|
|
}
|
|
setLoadingText('Loading configs...')
|
|
const normalConfig = await readNormalConfig()
|
|
const versionsConfig = await readVersionsConfig()
|
|
setDownloadedVersionsConfig(versionsConfig)
|
|
setNormalConfig(normalConfig)
|
|
setLoading(false)
|
|
})()
|
|
}, [])
|
|
|
|
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)
|
|
}, [])
|
|
|
|
return (
|
|
<>
|
|
<html lang='en' className={roboto.className}>
|
|
<body>
|
|
{loading ? (
|
|
<div className='w-screen h-screen flex items-center justify-center'>
|
|
{outdated ? (
|
|
<div className='text-center'>
|
|
<p className='text-8xl mb-4'>Outdated Launcher!</p>
|
|
<p className='text-4xl mb-4'>
|
|
Please update to the latest version to continue.
|
|
</p>
|
|
<button
|
|
className='button'
|
|
onClick={() =>
|
|
openUrl('https://games.lncvrt.xyz/berrydash/download')
|
|
}
|
|
>
|
|
Download latest version
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<p className='text-7xl text-center'>{loadingText}</p>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div
|
|
tabIndex={0}
|
|
onKeyDown={e => {
|
|
if (showPopup && e.key === 'Escape') {
|
|
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>
|
|
<div
|
|
className='relative z-[2] ml-[239px] w-[761px] border-b border-[#242424] h-[33px] bg-[#161616]'
|
|
style={{
|
|
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>
|
|
</div>
|
|
{showPopup && (
|
|
<div
|
|
className={`popup-overlay ${fadeOut ? 'fade-out' : ''}`}
|
|
onClick={handleOverlayClick}
|
|
>
|
|
<div className='popup-box'>
|
|
<button
|
|
className='close-button'
|
|
onClick={() => {
|
|
setFadeOut(true)
|
|
setTimeout(() => setShowPopup(false), 200)
|
|
}}
|
|
>
|
|
<FontAwesomeIcon icon={faXmark} />
|
|
</button>
|
|
{popupMode === 0 ? (
|
|
<>
|
|
<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>
|
|
))
|
|
)}
|
|
</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...
|
|
</p>
|
|
) : (
|
|
downloadProgress.map((v, i) => (
|
|
<div
|
|
key={i}
|
|
className='popup-entry flex flex-col justify-between'
|
|
>
|
|
<p className='text-2xl'>
|
|
Berry Dash v{v.version.displayName}
|
|
</p>
|
|
<div className='mt-[25px] flex items-center justify-between'>
|
|
{v.failed ? (
|
|
<>
|
|
<div className='flex items-center'>
|
|
<span className='text-red-500'>
|
|
Download failed
|
|
</span>
|
|
<button
|
|
className='button ml-30 mb-2'
|
|
onClick={() =>
|
|
setDownloadProgress(prev =>
|
|
prev.filter(
|
|
(_, idx) => idx !== i
|
|
)
|
|
)
|
|
}
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</>
|
|
) : v.queued ? (
|
|
<span className='text-yellow-500'>
|
|
Queued…
|
|
</span>
|
|
) : (
|
|
<span>
|
|
Downloading: {v.progress}% done
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</>
|
|
) : popupMode === 2 ? (
|
|
managingVersion ? (
|
|
<>
|
|
<p className='text-xl text-center'>
|
|
Manage version{' '}
|
|
{managingVersion.version.displayName}
|
|
</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
|
|
})
|
|
}
|
|
>
|
|
Uninstall
|
|
</button>
|
|
<button
|
|
className='button'
|
|
onClick={async () =>
|
|
invoke('open_folder', {
|
|
name: managingVersion.version.version
|
|
})
|
|
}
|
|
>
|
|
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>
|
|
</>
|
|
) : (
|
|
<p className='text-xl text-center'>
|
|
No version selected
|
|
</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)
|
|
)
|
|
) {
|
|
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>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</body>
|
|
</html>
|
|
</>
|
|
)
|
|
}
|