diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5d7e6da..a8e5f1f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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" diff --git a/src/componets/Setting.css b/src/componets/Setting.css new file mode 100644 index 0000000..943d096 --- /dev/null +++ b/src/componets/Setting.css @@ -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; +} diff --git a/src/componets/Setting.tsx b/src/componets/Setting.tsx new file mode 100644 index 0000000..3168cc5 --- /dev/null +++ b/src/componets/Setting.tsx @@ -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 ( +
+ +
+ onChange(!value)} + /> + {value && } +
+
+ ) +} diff --git a/src/enums/Keys.ts b/src/enums/Keys.ts index 4718f11..2918760 100644 --- a/src/enums/Keys.ts +++ b/src/enums/Keys.ts @@ -1,5 +1,6 @@ export enum Keys { SERVER_RECEIVE_TRANSFER_KEY = '', SERVER_SEND_TRANSFER_KEY = '', - CONFIG_ENCRYPTION_KEY = '' + CONFIG_ENCRYPTION_KEY = '', + VERSIONS_ENCRYPTION_KEY = '' } diff --git a/src/main.tsx b/src/main.tsx index 8d2c9ec..cbf78aa 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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) 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('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(); diff --git a/src/routes/Installs.tsx b/src/routes/Installs.tsx index b45c3d2..de0a7f6 100644 --- a/src/routes/Installs.tsx +++ b/src/routes/Installs.tsx @@ -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 ( diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx index 4583d67..13ffa03 100644 --- a/src/routes/Settings.tsx +++ b/src/routes/Settings.tsx @@ -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 ( <>

Settings

+
+ { + setCheckForNewVersionOnLoad(!checkForNewVersionOnLoad) + const config = await readNormalConfig() + config.settings.checkForNewVersionOnLoad = !checkForNewVersionOnLoad + await writeNormalConfig(config) + }} + /> + { + setAllowNotifications(!allowNotifications) + const config = await readNormalConfig() + config.settings.allowNotifications = !allowNotifications + await writeNormalConfig(config) + }} + /> + { + setUseWineOnUnixWhenNeeded(!useWineOnUnixWhenNeeded) + const config = await readNormalConfig() + config.settings.useWineOnUnixWhenNeeded = !useWineOnUnixWhenNeeded + await writeNormalConfig(config) + }} + className={platform() == 'linux' || platform() == 'macos' ? '' : 'hidden'} + /> +
) } diff --git a/src/types/SettingProps.ts b/src/types/SettingProps.ts new file mode 100644 index 0000000..b663229 --- /dev/null +++ b/src/types/SettingProps.ts @@ -0,0 +1,6 @@ +export type SettingProps = { + label: string + value: boolean + onChange: (val: boolean) => void, + className?: string +} diff --git a/src/types/SettingsType.ts b/src/types/SettingsType.ts index b115637..714cabf 100644 --- a/src/types/SettingsType.ts +++ b/src/types/SettingsType.ts @@ -1,6 +1,7 @@ export class SettingsType { constructor ( public checkForNewVersionOnLoad: boolean = true, - public useWineOnUnixWhenNeeded: boolean = false + public allowNotifications: boolean = true, + public useWineOnUnixWhenNeeded: boolean = false, ) {} } diff --git a/src/types/VersionsConfig.ts b/src/types/VersionsConfig.ts new file mode 100644 index 0000000..982118f --- /dev/null +++ b/src/types/VersionsConfig.ts @@ -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 + } +} diff --git a/src/util/BazookaManager.ts b/src/util/BazookaManager.ts index 9287081..2a42b21 100644 --- a/src/util/BazookaManager.ts +++ b/src/util/BazookaManager.ts @@ -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 { +export async function readNormalConfig (): Promise { const version = await app.getVersion() const options = { baseDir: BaseDirectory.AppLocalData @@ -40,7 +41,7 @@ export async function readConfig (): Promise { ) } -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 { + 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 + ) + } +}