Move to NextJS + other changes

This commit is contained in:
2025-08-25 01:00:52 -07:00
parent e8e9e4d312
commit 342f8101fe
44 changed files with 3265 additions and 1305 deletions

55
.gitignore vendored
View File

@@ -1,24 +1,41 @@
# Logs
logs
*.log
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
.pnpm-debug.log*
node_modules
dist
dist-ssr
*.local
# env files (can opt-in for committing if needed)
.env*
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}

25
eslint.config.mjs Normal file
View File

@@ -0,0 +1,25 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
ignores: [
"node_modules/**",
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
],
},
];
export default eslintConfig;

View File

@@ -1,11 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

14
next.config.ts Normal file
View File

@@ -0,0 +1,14 @@
import type { NextConfig } from 'next'
const isProd = process.env.NODE_ENV === 'production'
const internalHost = process.env.TAURI_DEV_HOST || 'localhost'
const nextConfig: NextConfig = {
output: 'export',
images: {
unoptimized: true
},
assetPrefix: isProd ? undefined : `http://${internalHost}:3000`,
devIndicators: false
}
export default nextConfig

View File

@@ -4,13 +4,13 @@
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"tauri": "tauri"
},
"dependencies": {
"@fontsource/roboto": "5.2.6",
"@tauri-apps/api": "2.8.0",
"@tauri-apps/plugin-dialog": "2.3.3",
"@tauri-apps/plugin-fs": "2.4.2",
@@ -20,7 +20,8 @@
"axios": "1.11.0",
"date-fns": "4.1.0",
"react": "19.1.1",
"react-dom": "19.1.1"
"react-dom": "19.1.1",
"next": "15.5.0"
},
"devDependencies": {
"@fortawesome/fontawesome-svg-core": "7.0.0",
@@ -32,11 +33,12 @@
"@types/crypto-js": "4.2.2",
"@types/react": "19.1.11",
"@types/react-dom": "19.1.7",
"@vitejs/plugin-react": "5.0.1",
"crypto-js": "4.2.0",
"postcss": "8.5.6",
"@types/node": "24.3.0",
"tailwindcss": "4.1.12",
"typescript": "5.9.2",
"vite": "7.1.3"
"eslint": "9.34.0",
"eslint-config-next": "15.5.0",
"@eslint/eslintrc": "3.3.1"
}
}

View File

@@ -1,5 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {}
}
}
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

View File

@@ -5,9 +5,9 @@
"identifier": "xyz.lncvrt.berrydash-launcher",
"build": {
"beforeDevCommand": "yarn dev",
"devUrl": "http://localhost:1420",
"devUrl": "http://localhost:3000",
"beforeBuildCommand": "yarn build",
"frontendDist": "../dist"
"frontendDist": "../out"
},
"app": {
"windows": [

View File

@@ -5,9 +5,9 @@
"identifier": "xyz.lncvrt.berrydash-launcher",
"build": {
"beforeDevCommand": "yarn dev",
"devUrl": "http://localhost:1420",
"devUrl": "http://localhost:3000",
"beforeBuildCommand": "yarn build",
"frontendDist": "../dist"
"frontendDist": "../out"
},
"app": {
"windows": [

View File

@@ -5,9 +5,9 @@
"identifier": "xyz.lncvrt.berrydash-launcher",
"build": {
"beforeDevCommand": "yarn dev",
"devUrl": "http://localhost:1420",
"devUrl": "http://localhost:3000",
"beforeBuildCommand": "yarn build",
"frontendDist": "../dist"
"frontendDist": "../out"
},
"app": {
"macOSPrivateApi": true,

View File

@@ -5,9 +5,9 @@
"identifier": "xyz.lncvrt.berrydash-launcher",
"build": {
"beforeDevCommand": "yarn dev",
"devUrl": "http://localhost:1420",
"devUrl": "http://localhost:3000",
"beforeBuildCommand": "yarn build",
"frontendDist": "../dist"
"frontendDist": "../out"
},
"app": {
"withGlobalTauri": true,

View File

@@ -0,0 +1,47 @@
'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'
type GlobalCtxType = {
versionList: LauncherVersion[] | null
setVersionList: (v: LauncherVersion[] | null) => void
selectedVersionList: LauncherVersion[]
setSelectedVersionList: (v: LauncherVersion[]) => void
downloadProgress: DownloadProgress[]
setDownloadProgress: (v: DownloadProgress[]) => void
showPopup: boolean
setShowPopup: (v: boolean) => void
popupMode: number | null
setPopupMode: (v: number | null) => void
fadeOut: boolean
setFadeOut: (v: boolean) => void
downloadedVersionsConfig: VersionsConfig | null
setDownloadedVersionsConfig: (v: VersionsConfig | null) => void
normalConfig: NormalConfig | null
setNormalConfig: (v: NormalConfig | null) => void
managingVersion: DownloadedVersion | null
setManagingVersion: (v: DownloadedVersion | null) => void
}
const GlobalCtx = createContext<GlobalCtxType | null>(null)
export const useGlobal = () => {
const ctx = useContext(GlobalCtx)
if (!ctx) throw new Error('useGlobal must be inside GlobalProvider')
return ctx
}
export const GlobalProvider = ({
children,
value
}: {
children: ReactNode
value: GlobalCtxType
}) => {
return <GlobalCtx.Provider value={value}>{children}</GlobalCtx.Provider>
}

View File

@@ -1,7 +1,6 @@
@import 'tailwindcss';
@import "tailwindcss";
body {
font-family: 'Roboto', sans-serif;
@apply bg-[#0f0f0f] text-white select-none;
}

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,4 +1,4 @@
@import 'tailwindcss';
@import "tailwindcss";
.downloads-container {
@apply flex justify-center;

View File

@@ -1,4 +1,4 @@
@import 'tailwindcss';
@import "tailwindcss";
.setting-checkbox-wrapper {
@apply relative w-5 h-5;

View File

@@ -1,7 +1,7 @@
import { SettingProps } from '../types/SettingProps'
import './Setting.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCheck } from '@fortawesome/free-solid-svg-icons'
import { SettingProps } from '../types/SettingProps'
export function Setting ({ label, value, onChange, className }: SettingProps) {
return (

View File

@@ -1,4 +1,4 @@
@import 'tailwindcss';
@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];

View File

@@ -1,3 +1,5 @@
'use client'
import './Sidebar.css'
import Icon from '../Icon.png'
import { openUrl } from '@tauri-apps/plugin-opener'
@@ -12,16 +14,17 @@ 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 { SidebarProps } from '../types/SidebarProps'
import { useGlobal } from '../GlobalProvider'
import Image from 'next/image'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
export default function Sidebar ({
setShowPopup,
setPopupMode,
setFadeOut,
downloadProgress
}: SidebarProps) {
export default function Sidebar () {
const [rot, setRot] = useState(0)
const [dir, setDir] = useState(1)
const { setShowPopup, setPopupMode, setFadeOut, downloadProgress } =
useGlobal()
const pathname = usePathname()
return (
<aside className='sidebar'>
@@ -43,11 +46,12 @@ export default function Sidebar ({
}}
></div>
<div className='logo'>
<img
<Image
draggable={false}
src={Icon}
width={48}
height={48}
alt=''
style={{
transform: `rotate(${rot}deg)`,
transition: 'transform 0.3s ease',
@@ -84,39 +88,27 @@ export default function Sidebar ({
/>
</div>
<nav className='nav-links'>
<a
<Link
draggable={false}
href='#installs'
className={`link ${
(window.location.hash || '#installs') === '#installs'
? 'active'
: ''
}`}
href='/'
className={`link ${pathname === '/' ? 'active' : ''}`}
>
<FontAwesomeIcon icon={faServer} className='mr-1' /> Installs
</a>
<a
</Link>
<Link
draggable={false}
href='#settings'
className={`link ${
(window.location.hash || '#installs') === '#settings'
? 'active'
: ''
}`}
href='/settings'
className={`link ${pathname === '/settings' ? 'active' : ''}`}
>
<FontAwesomeIcon icon={faCog} className='mr-1' /> Settings
</a>
<a
</Link>
<Link
draggable={false}
href='#leaderboards'
className={`link ${
(window.location.hash || '#installs') === '#leaderboards'
? 'active'
: ''
}`}
href='/leaderboards'
className={`link ${pathname === '/leaderboards' ? 'active' : ''}`}
>
<FontAwesomeIcon icon={faRankingStar} className='mr-1' /> Leaderboards
</a>
</Link>
<a
draggable={false}
onClick={() => openUrl('https://berrydash.lncvrt.xyz/discord')}

604
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,604 @@
'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, faX } 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 () => {
setLoadingText('Checking latest version...')
try {
const response = await axios.get(
'https://berrydash.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)
if (platform() == 'windows') {
invoke('windows_rounded_corners', {
enabled: normalConfig.settings.useWindowsRoundedCorners
})
}
setLoading(false)
})()
}, [])
useEffect(() => {
const unlistenProgress = 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
})
})
const unlistenDone = listen<string>('download-done', async event => {
const versionName = event.payload
setDownloadProgress(prev => {
const downloaded = prev.find(d => d.version.version === versionName)
if (downloaded && downloadedVersionsConfig) {
const newDownloaded = DownloadedVersion.import(downloaded.version)
const updatedConfig = {
...downloadedVersionsConfig,
list: [...downloadedVersionsConfig.list, newDownloaded]
}
setDownloadedVersionsConfig(updatedConfig)
writeVersionsConfig(updatedConfig)
}
return prev.filter(d => d.version.version !== versionName)
})
activeDownloads.current--
runNext()
if (downloadProgress.length === 0) {
await notifyUser('Downloads Complete', 'All downloads have completed.')
}
})
const unlistenFailed = 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.`
)
})
const unlistenUninstalled = listen<string>(
'version-uninstalled',
async 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
})
}
)
return () => {
unlistenProgress.then(f => f())
unlistenDone.then(f => f())
unlistenFailed.then(f => f())
unlistenUninstalled.then(f => f())
}
}, [downloadedVersionsConfig, downloadProgress, 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://berrydash.lncvrt.xyz/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={faX} />
</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>
</>
)
}

View File

@@ -1,4 +1,4 @@
@import 'tailwindcss';
@import "tailwindcss";
.leaderboard-container {
@apply flex justify-center;

View File

@@ -1,3 +1,5 @@
'use client'
import { useEffect, useState } from 'react'
import './Leaderboards.css'
import axios from 'axios'

View File

@@ -1,24 +1,28 @@
'use client'
import { useEffect } from 'react'
import axios from 'axios'
import { InstallsProps } from '../types/InstallsProps'
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'
export default function Installs () {
const {
downloadProgress,
showPopup,
setShowPopup,
setPopupMode,
setFadeOut,
setSelectedVersionList,
setVersionList,
downloadedVersionsConfig,
normalConfig,
setManagingVersion
} = useGlobal()
export default function Installs ({
downloadProgress,
showPopup,
setShowPopup,
setPopupMode,
setFadeOut,
setSelectedVersionList,
setVersionList,
downloadedVersionsConfig,
normalConfig,
setManagingVersion
}: InstallsProps) {
useEffect(() => {
if (!showPopup) return
setSelectedVersionList([])
@@ -45,7 +49,7 @@ export default function Installs ({
setVersionList([])
}
})()
}, [showPopup])
}, [normalConfig, setSelectedVersionList, setVersionList, showPopup])
return (
<div className='mx-4 mt-4'>

View File

@@ -1,11 +1,13 @@
'use client'
import { useEffect, useState } from 'react'
import { Setting } from '../componets/Setting'
import { writeNormalConfig } from '../util/BazookaManager'
import { platform } from '@tauri-apps/plugin-os'
import { SettingsProps } from '../types/SettingsProps'
import { invoke } from '@tauri-apps/api/core'
import { useGlobal } from '../GlobalProvider'
export default function Settings ({ normalConfig }: SettingsProps) {
export default function Settings () {
const [checkForNewVersionOnLoad, setCheckForNewVersionOnLoad] =
useState(false)
const [allowNotifications, setAllowNotifications] = useState(false)
@@ -13,6 +15,7 @@ export default function Settings ({ normalConfig }: SettingsProps) {
const [useWindowsRoundedCorners, setUseWindowsRoundedCorners] =
useState(false)
const [loaded, setLoaded] = useState(false)
const { normalConfig } = useGlobal()
useEffect(() => {
;(async () => {
@@ -31,7 +34,7 @@ export default function Settings ({ normalConfig }: SettingsProps) {
break
}
})()
}, [])
}, [normalConfig])
return (
<>

View File

@@ -1,14 +1,21 @@
import { SettingsType } from './SettingsType'
type NormalConfigData = {
version: string
settings?: Partial<SettingsType>
}
export class NormalConfig {
constructor (
public version: string,
public settings: SettingsType = new SettingsType()
) {}
static import (data: any) {
static import (data: NormalConfigData) {
const cfg = new NormalConfig(data.version)
Object.assign(cfg.settings, data.settings)
if (data.settings) {
cfg.settings = { ...cfg.settings, ...data.settings }
}
return cfg
}
}

View File

@@ -1,11 +1,16 @@
import { DownloadedVersion } from './DownloadedVersion'
type VersionsConfigData = {
version: string
list: DownloadedVersion[]
}
export class VersionsConfig {
constructor (public version: string, public list: DownloadedVersion[] = []) {}
static import (data: any) {
static import (data: VersionsConfigData) {
const cfg = new VersionsConfig(data.version)
Object.assign(cfg.list, data.list)
cfg.list = [...data.list]
return cfg
}
}

View File

@@ -37,8 +37,10 @@ export async function readNormalConfig (): Promise<NormalConfig> {
return new NormalConfig(version)
}
const config = await readTextFile('config.dat', options)
return NormalConfig.import(JSON.parse(await decrypt(config, await getKey(2))))
} catch (_) {
return NormalConfig.import(
JSON.parse(await decrypt(config, await getKey(2)))
)
} catch {
return new NormalConfig(version)
}
}
@@ -99,7 +101,7 @@ export async function readVersionsConfig (): Promise<VersionsConfig> {
return VersionsConfig.import(
JSON.parse(await decrypt(config, await getKey(3)))
)
} catch (_) {
} catch {
return new VersionsConfig(version)
}
}

View File

@@ -1,554 +0,0 @@
import { useEffect, useRef, useState } from 'react'
import ReactDOM from 'react-dom/client'
import Installs from './routes/Installs'
import Settings from './routes/Settings'
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, faX } from '@fortawesome/free-solid-svg-icons'
import '@fontsource/roboto'
import Leaderboards from './routes/Leaderboards'
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'
function App () {
const [hash, setHash] = useState(window.location.hash || '#installs')
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?.()
}
useEffect(() => {
;(async () => {
setLoadingText('Checking latest version...')
try {
const response = await axios.get(
'https://berrydash.lncvrt.xyz/database/launcher/latest.php'
)
const client = await app.getVersion()
if (response.data !== client) {
setOutdated(true)
return
}
} catch (e) {
setLoadingText('Failed to check latest version.')
return
}
setLoadingText('Loading configs...')
const normalConfig = await readNormalConfig()
const versionsConfig = await readVersionsConfig()
setDownloadedVersionsConfig(versionsConfig)
setNormalConfig(normalConfig)
if (platform() == 'windows') {
invoke('windows_rounded_corners', {
enabled: normalConfig.settings.useWindowsRoundedCorners
})
}
setLoading(false)
})()
}, [])
useEffect(() => {
const unlistenProgress = 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
})
})
const unlistenDone = listen<string>('download-done', async event => {
const versionName = event.payload
setDownloadProgress(prev => {
const downloaded = prev.find(d => d.version.version === versionName)
if (downloaded && downloadedVersionsConfig) {
const newDownloaded = DownloadedVersion.import(downloaded.version)
const updatedConfig = {
...downloadedVersionsConfig,
list: [...downloadedVersionsConfig.list, newDownloaded]
}
setDownloadedVersionsConfig(updatedConfig)
writeVersionsConfig(updatedConfig)
}
return prev.filter(d => d.version.version !== versionName)
})
activeDownloads.current--
runNext()
if (downloadProgress.length === 0) {
await notifyUser('Downloads Complete', 'All downloads have completed.')
}
})
const unlistenFailed = 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.`
)
})
const unlistenUninstalled = listen<string>(
'version-uninstalled',
async 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
})
}
)
return () => {
unlistenProgress.then(f => f())
unlistenDone.then(f => f())
unlistenFailed.then(f => f())
unlistenUninstalled.then(f => f())
}
}, [downloadedVersionsConfig])
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)
}
}
async function notifyUser (title: string, body: string) {
while (normalConfig != null) {
if (!normalConfig.settings.allowNotifications) return
break
}
let permissionGranted = await isPermissionGranted()
if (!permissionGranted) {
const permission = await requestPermission()
permissionGranted = permission === 'granted'
}
if (permissionGranted) {
sendNotification({ title, body })
}
}
useEffect(() => {
const onHashChange = () => setHash(window.location.hash || '#installs')
window.addEventListener('hashchange', onHashChange)
return () => window.removeEventListener('hashchange', onHashChange)
}, [])
useEffect(() => {
const handler = (e: MouseEvent) => e.preventDefault()
document.addEventListener('contextmenu', handler)
return () => document.removeEventListener('contextmenu', handler)
}, [])
function renderContent () {
if (hash === '#installs') {
return (
<Installs
downloadProgress={downloadProgress}
showPopup={showPopup}
setShowPopup={setShowPopup}
setPopupMode={setPopupMode}
setFadeOut={setFadeOut}
setSelectedVersionList={setSelectedVersionList}
setVersionList={setVersionList}
downloadedVersionsConfig={downloadedVersionsConfig}
normalConfig={normalConfig}
setManagingVersion={setManagingVersion}
/>
)
} else if (hash === '#settings') {
return <Settings normalConfig={normalConfig} />
} else if (hash === '#leaderboards') {
return <Leaderboards />
}
return null
}
return 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://berrydash.lncvrt.xyz/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)
}
}}
>
<Sidebar
setShowPopup={setShowPopup}
setPopupMode={setPopupMode}
setFadeOut={setFadeOut}
downloadProgress={downloadProgress}
/>
<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' }}>{renderContent()}</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={faX} />
</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>
</>
)
}
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<App />
)

1
src/vite-env.d.ts vendored
View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@@ -1,25 +1,27 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
/* Bundler mode */
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,32 +0,0 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST;
// https://vitejs.dev/config/
export default defineConfig(async () => ({
plugins: [react()],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
// 1. prevent vite from obscuring rust errors
clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? {
protocol: "ws",
host,
port: 1421,
}
: undefined,
watch: {
// 3. tell vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**"],
},
},
}));

2994
yarn.lock

File diff suppressed because it is too large Load Diff