Settings, queue fixes, and more

This commit is contained in:
2025-07-22 17:22:05 -07:00
parent 7077bf6bea
commit c507a6fbe4
11 changed files with 242 additions and 22 deletions

View File

@@ -17,7 +17,7 @@ tauri-plugin-opener = "2.4.0"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.141"
tauri-plugin-os = "2.3.0"
reqwest = { version = "0.12.22", features = ["stream"] }
reqwest = { version = "0.12.22", default-features = false, features = ["stream", "rustls-tls"] }
tokio = "1.46.1"
futures-util = { version = "0.3.31", features = ["io"] }
tauri-plugin-decorum = "1.1.1"

17
src/componets/Setting.css Normal file
View File

@@ -0,0 +1,17 @@
@import 'tailwindcss';
.setting-checkbox-wrapper {
@apply relative w-5 h-5;
}
.setting-checkbox {
@apply appearance-none w-full h-full border-2 border-[#484848] rounded-md bg-[#242424] transition-colors duration-200 cursor-pointer;
}
.setting-checkbox:checked {
@apply bg-blue-500 border-blue-600;
}
.fa-check-icon {
@apply absolute top-1/2 left-1/2 text-white text-[11px] pointer-events-none -translate-x-2/4 -translate-y-2/4;
}

21
src/componets/Setting.tsx Normal file
View File

@@ -0,0 +1,21 @@
import { SettingProps } from '../types/SettingProps'
import './Setting.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCheck } from '@fortawesome/free-solid-svg-icons'
export function Setting ({ label, value, onChange, className }: SettingProps) {
return (
<div className={`flex items-center gap-2 mb-2 ${className}`}>
<label className='text-white text-lg'>{label}</label>
<div className='setting-checkbox-wrapper'>
<input
type='checkbox'
className='setting-checkbox'
checked={value}
onChange={() => onChange(!value)}
/>
{value && <FontAwesomeIcon icon={faCheck} className='fa-check-icon' />}
</div>
</div>
)
}

View File

@@ -1,5 +1,6 @@
export enum Keys {
SERVER_RECEIVE_TRANSFER_KEY = '',
SERVER_SEND_TRANSFER_KEY = '',
CONFIG_ENCRYPTION_KEY = ''
CONFIG_ENCRYPTION_KEY = '',
VERSIONS_ENCRYPTION_KEY = ''
}

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import ReactDOM from 'react-dom/client'
import Installs from './routes/Installs'
import Settings from './routes/Settings'
@@ -14,6 +14,7 @@ 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'
function App () {
const [hash, setHash] = useState(window.location.hash || '#installs')
@@ -23,13 +24,13 @@ function App () {
const [showPopup, setShowPopup] = useState(false)
const [popupMode, setPopupMode] = useState<null | number>(null)
const [fadeOut, setFadeOut] = useState(false)
let activeDownloads = 0
const queue: (() => void)[] = []
const activeDownloads = useRef(0)
const queue = useRef<(() => void)[]>([])
function runNext() {
if (activeDownloads >= 3 || queue.length === 0) return
activeDownloads++
const next = queue.shift()
if (activeDownloads.current >= 3 || queue.current.length === 0) return
activeDownloads.current++
const next = queue.current.shift()
next?.()
}
@@ -49,8 +50,17 @@ function App () {
const unlistenDone = listen<string>('download-done', async (event) => {
const versionName = event.payload
setDownloadProgress(prev => prev.filter(d => d.version.version !== versionName))
activeDownloads--
setDownloadProgress(prev => {
const downloaded = prev.find(d => d.version.version === versionName)
if (downloaded) {
readVersionsConfig().then(cfg => {
cfg.list.push(downloaded.version)
writeVersionsConfig(cfg)
})
}
return prev.filter(d => d.version.version !== versionName)
})
activeDownloads.current--
runNext()
if (downloadProgress.length === 0) {
await notifyUser('Downloads Complete', 'All downloads have completed.')
@@ -64,7 +74,7 @@ function App () {
d.version.version === versionName ? { ...d, failed: true } : d
)
)
activeDownloads--
activeDownloads.current--
runNext()
await notifyUser('Download Failed', `The download for version ${versionName} has failed.`)
})
@@ -76,11 +86,33 @@ function App () {
}
}, [])
function downloadVersions(versions: LauncherVersion[]) {
async function downloadVersions(versions: LauncherVersion[]) {
const config = await readNormalConfig()
const useWine = config.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 === 'macos' || 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)
@@ -91,13 +123,13 @@ function App () {
})
invoke('download', {
url: download.version.downloadUrls[download.version.platforms.indexOf(platform())],
url,
name: download.version.version,
executable: download.version.executables[download.version.platforms.indexOf(platform())]
executable: exe
})
}
queue.push(task)
queue.current.push(task)
runNext()
})
}
@@ -110,6 +142,8 @@ function App () {
}
async function notifyUser(title: string, body: string) {
const config = await readNormalConfig()
if (!config.settings.allowNotifications) return
let permissionGranted = await isPermissionGranted();
if (!permissionGranted) {
const permission = await requestPermission();

View File

@@ -2,15 +2,31 @@ import { useEffect } from 'react'
import axios from 'axios'
import { InstallsProps } from '../types/InstallsProps'
import { platform } from '@tauri-apps/plugin-os'
import { readNormalConfig } from '../util/BazookaManager'
export default function Installs({ downloadProgress, showPopup, setShowPopup, setPopupMode, setFadeOut, setSelectedVersionList, setVersionList }: InstallsProps) {
useEffect(() => {
if (!showPopup) return
setSelectedVersionList([]);
setSelectedVersionList([])
setVersionList(null)
axios.get('https://berrydash.lncvrt.xyz/database/launcher/versions.php')
.then(res => setVersionList(res.data.filter((d: { platforms: string[] }) => d.platforms.includes(platform()))))
.catch(() => setVersionList([]))
;(async () => {
try {
const config = await readNormalConfig()
const useWine = config.settings.useWineOnUnixWhenNeeded
const res = await axios.get('https://berrydash.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)
} catch {
setVersionList([])
}
})()
}, [showPopup])
return (

View File

@@ -1,7 +1,59 @@
import { useEffect, useState } from 'react'
import { Setting } from '../componets/Setting'
import { readNormalConfig, writeNormalConfig } from '../util/BazookaManager'
import { platform } from '@tauri-apps/plugin-os'
export default function Settings () {
const [checkForNewVersionOnLoad, setCheckForNewVersionOnLoad] =
useState(false)
const [useWineOnUnixWhenNeeded, setUseWineOnUnixWhenNeeded] = useState(false)
const [allowNotifications, setAllowNotifications] = useState(false)
useEffect(() => {
;(async () => {
const config = await readNormalConfig()
setCheckForNewVersionOnLoad(config.settings.checkForNewVersionOnLoad)
setUseWineOnUnixWhenNeeded(config.settings.useWineOnUnixWhenNeeded)
setAllowNotifications(config.settings.allowNotifications)
})()
}, [])
return (
<>
<p className='text-3xl ml-4 mt-4'>Settings</p>
<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 () => {
setCheckForNewVersionOnLoad(!checkForNewVersionOnLoad)
const config = await readNormalConfig()
config.settings.checkForNewVersionOnLoad = !checkForNewVersionOnLoad
await writeNormalConfig(config)
}}
/>
<Setting
label='Allow sending notifications'
value={allowNotifications}
onChange={async () => {
setAllowNotifications(!allowNotifications)
const config = await readNormalConfig()
config.settings.allowNotifications = !allowNotifications
await writeNormalConfig(config)
}}
/>
<Setting
label='Use wine to launch Berry Dash when needed'
value={useWineOnUnixWhenNeeded}
onChange={async () => {
setUseWineOnUnixWhenNeeded(!useWineOnUnixWhenNeeded)
const config = await readNormalConfig()
config.settings.useWineOnUnixWhenNeeded = !useWineOnUnixWhenNeeded
await writeNormalConfig(config)
}}
className={platform() == 'linux' || platform() == 'macos' ? '' : 'hidden'}
/>
</div>
</>
)
}

View File

@@ -0,0 +1,6 @@
export type SettingProps = {
label: string
value: boolean
onChange: (val: boolean) => void,
className?: string
}

View File

@@ -1,6 +1,7 @@
export class SettingsType {
constructor (
public checkForNewVersionOnLoad: boolean = true,
public useWineOnUnixWhenNeeded: boolean = false
public allowNotifications: boolean = true,
public useWineOnUnixWhenNeeded: boolean = false,
) {}
}

View File

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

View File

@@ -10,8 +10,9 @@ import {
} from '@tauri-apps/plugin-fs'
import { decrypt, encrypt } from './Encryption'
import { Keys } from '../enums/Keys'
import { VersionsConfig } from '../types/VersionsConfig'
export async function readConfig (): Promise<NormalConfig> {
export async function readNormalConfig (): Promise<NormalConfig> {
const version = await app.getVersion()
const options = {
baseDir: BaseDirectory.AppLocalData
@@ -40,7 +41,7 @@ export async function readConfig (): Promise<NormalConfig> {
)
}
export async function writeConfig (data: NormalConfig) {
export async function writeNormalConfig (data: NormalConfig) {
const options = {
baseDir: BaseDirectory.AppLocalData
}
@@ -67,3 +68,60 @@ export async function writeConfig (data: NormalConfig) {
)
}
}
export async function readVersionsConfig (): Promise<VersionsConfig> {
const version = await app.getVersion()
const options = {
baseDir: BaseDirectory.AppLocalData
}
const doesFolderExist = await exists('', options)
const doesConfigExist = await exists('versions.dat', options)
if (!doesFolderExist || !doesConfigExist) {
if (!doesFolderExist) {
await mkdir('', options)
}
const file = await create('versions.dat', options)
await file.write(
new TextEncoder().encode(
encrypt(
JSON.stringify(new VersionsConfig(version)),
Keys.VERSIONS_ENCRYPTION_KEY
)
)
)
await file.close()
return new VersionsConfig(version)
}
const config = await readTextFile('versions.dat', options)
return VersionsConfig.import(
JSON.parse(decrypt(config, Keys.VERSIONS_ENCRYPTION_KEY))
)
}
export async function writeVersionsConfig (data: VersionsConfig) {
const options = {
baseDir: BaseDirectory.AppLocalData
}
const doesFolderExist = await exists('', options)
const doesConfigExist = await exists('versions.dat', options)
if (!doesFolderExist || !doesConfigExist) {
if (!doesFolderExist) {
await mkdir('', options)
}
const file = await create('versions.dat', options)
await file.write(
new TextEncoder().encode(
encrypt(JSON.stringify(data), Keys.VERSIONS_ENCRYPTION_KEY)
)
)
await file.close()
} else {
await writeFile(
'versions.dat',
new TextEncoder().encode(
encrypt(JSON.stringify(data), Keys.VERSIONS_ENCRYPTION_KEY)
),
options
)
}
}