Add settings page & more

This commit is contained in:
Lncvrt committed 2026-03-26 12:51:46 -07:00
1 parent 87c91e7b5e
commit 0d5993c4fc
16 files changed
+361 -41

No files matched your search

+9
View File
@@ -12,9 +12,12 @@
"@fortawesome/react-fontawesome": "^3.3.0", "@fortawesome/react-fontawesome": "^3.3.0",
"@tauri-apps/api": "2.10.1", "@tauri-apps/api": "2.10.1",
"@tauri-apps/plugin-clipboard-manager": "~2", "@tauri-apps/plugin-clipboard-manager": "~2",
"@tauri-apps/plugin-fs": "~2",
"@tauri-apps/plugin-http": "~2", "@tauri-apps/plugin-http": "~2",
"@tauri-apps/plugin-notification": "~2", "@tauri-apps/plugin-notification": "~2",
"@tauri-apps/plugin-opener": "~2", "@tauri-apps/plugin-opener": "~2",
"@tauri-apps/plugin-os": "~2",
"@tauri-apps/plugin-store": "~2",
"next": "16.2.1", "next": "16.2.1",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4", "react-dom": "19.2.4",
@@ -268,12 +271,18 @@
"@tauri-apps/plugin-clipboard-manager": ["@tauri-apps/plugin-clipboard-manager@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ=="], "@tauri-apps/plugin-clipboard-manager": ["@tauri-apps/plugin-clipboard-manager@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ=="],
"@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.4.5", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA=="],
"@tauri-apps/plugin-http": ["@tauri-apps/plugin-http@2.5.7", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-+F2lEH/c9b0zSsOXKq+5hZNcd9F4IIKCK1T17RqMwpCmVnx2aoqY8yIBccCd25HTYUb3j6NPVbRax/m00hKG8A=="], "@tauri-apps/plugin-http": ["@tauri-apps/plugin-http@2.5.7", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-+F2lEH/c9b0zSsOXKq+5hZNcd9F4IIKCK1T17RqMwpCmVnx2aoqY8yIBccCd25HTYUb3j6NPVbRax/m00hKG8A=="],
"@tauri-apps/plugin-notification": ["@tauri-apps/plugin-notification@2.3.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg=="], "@tauri-apps/plugin-notification": ["@tauri-apps/plugin-notification@2.3.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg=="],
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ=="], "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ=="],
"@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A=="],
"@tauri-apps/plugin-store": ["@tauri-apps/plugin-store@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-0ClHS50Oq9HEvLPhNzTNFxbWVOqoAp3dRvtewQBeqfIQ0z5m3JRnOISIn2ZVPCrQC0MyGyhTS9DWhHjpigQE7A=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+3
View File
@@ -17,9 +17,12 @@
"@fortawesome/react-fontawesome": "^3.3.0", "@fortawesome/react-fontawesome": "^3.3.0",
"@tauri-apps/api": "2.10.1", "@tauri-apps/api": "2.10.1",
"@tauri-apps/plugin-clipboard-manager": "~2", "@tauri-apps/plugin-clipboard-manager": "~2",
"@tauri-apps/plugin-fs": "~2",
"@tauri-apps/plugin-http": "~2", "@tauri-apps/plugin-http": "~2",
"@tauri-apps/plugin-notification": "~2", "@tauri-apps/plugin-notification": "~2",
"@tauri-apps/plugin-opener": "~2", "@tauri-apps/plugin-opener": "~2",
"@tauri-apps/plugin-os": "~2",
"@tauri-apps/plugin-store": "~2",
"next": "16.2.1", "next": "16.2.1",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4" "react-dom": "19.2.4"
+3
View File
@@ -20,6 +20,9 @@ tauri-plugin-http = "2"
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
tauri-plugin-notification = "2" tauri-plugin-notification = "2"
tauri-plugin-clipboard-manager = "2" tauri-plugin-clipboard-manager = "2"
tauri-plugin-store = "2"
tauri-plugin-fs = "2"
tauri-plugin-os = "2"
[profile.release] [profile.release]
opt-level = 3 opt-level = 3
+8 -3
View File
@@ -2,7 +2,9 @@
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default", "identifier": "default",
"description": "Capability for the main window", "description": "Capability for the main window",
"windows": ["main"], "windows": [
"main"
],
"permissions": [ "permissions": [
"core:default", "core:default",
"opener:default", "opener:default",
@@ -43,6 +45,9 @@
"url": "https://games.lncvrt.xyz/api/berrydash/leaderboard/total" "url": "https://games.lncvrt.xyz/api/berrydash/leaderboard/total"
} }
] ]
} },
"store:default",
"fs:default",
"os:default"
] ]
} }
+3
View File
@@ -1,6 +1,9 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
+36 -19
View File
@@ -2,7 +2,6 @@
import '@/styles/globals.css' import '@/styles/globals.css'
import { Roboto } from 'next/font/google' import { Roboto } from 'next/font/google'
import LauncherContext from '@/context/LauncherContext'
import Sidebar from '@/componets/Sidebar' import Sidebar from '@/componets/Sidebar'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@@ -13,12 +12,43 @@ import {
isPermissionGranted, isPermissionGranted,
requestPermission requestPermission
} from '@tauri-apps/plugin-notification' } from '@tauri-apps/plugin-notification'
import GlobalProvider from '@/providers/GlobalProvider'
import { SettingsProvider, useSettings } from '@/providers/SettingsProvider'
const roboto = Roboto({ const roboto = Roboto({
variable: '--font-roboto', variable: '--font-roboto',
subsets: ['latin'] subsets: ['latin']
}) })
function RootLayoutContent ({
children,
loading
}: {
children: React.ReactNode
loading: 0 | 1 | 2 | 3
}) {
const { theme } = useSettings()
return (
<body className={theme + '-theme'}>
{loading == 3 ? (
<>
<Sidebar />
<main className='ml-60 relative'>{children}</main>
</>
) : (
<p className='flex justify-center items-center h-screen w-screen text-6xl'>
{loading == 0
? 'Loading'
: loading == 1
? 'Failed to load launcher'
: 'Outdated launcher!'}
</p>
)}
</body>
)
}
export default function RootLayout ({ export default function RootLayout ({
children children
}: Readonly<{ }: Readonly<{
@@ -67,24 +97,11 @@ export default function RootLayout ({
return ( return (
<html lang='en' className={roboto.className}> <html lang='en' className={roboto.className}>
<body className='dark-theme'> <GlobalProvider.Provider value={{ launcherManifest }}>
<LauncherContext.Provider value={{ launcherManifest }}> <SettingsProvider>
{loading == 3 ? ( <RootLayoutContent loading={loading}>{children}</RootLayoutContent>
<> </SettingsProvider>
<Sidebar /> </GlobalProvider.Provider>
<main className='ml-60 relative'>{children}</main>
</>
) : (
<p className='flex justify-center items-center h-screen w-screen text-6xl'>
{loading == 0
? 'Loading'
: loading == 1
? 'Failed to load launcher'
: 'Outdated launcher!'}
</p>
)}
</LauncherContext.Provider>
</body>
</html> </html>
) )
} }
+1 -1
View File
@@ -1,5 +1,5 @@
'use client' 'use client'
export default function Home () { export default function HomePage () {
return <div></div> return <div></div>
} }
+95
View File
@@ -0,0 +1,95 @@
'use client'
import { Setting } from '@/componets/Setting'
import { copyToClipboard } from '@/lib/clipboard'
import { useSettings } from '@/providers/SettingsProvider'
import { app } from '@tauri-apps/api'
import { platform } from '@tauri-apps/plugin-os'
import { useState, useEffect } from 'react'
export default function SettingsPage () {
const {
notificationsAllowed,
sidebarAlwaysShowGames,
linuxUseWine,
linuxWineCommand,
theme,
settings
} = useSettings()
const [version, setVersion] = useState('')
useEffect(() => {
app.getVersion().then(setVersion)
}, [])
return (
<>
<p className='text-3xl ml-4 mt-4'>Settings</p>
<div className='ml-4 mt-4 bg-(--col1) border border-(--col3) rounded-lg p-4 w-fit h-fit'>
<Setting
label='Allow sending notifications'
value={notificationsAllowed}
onChange={async () => {
await settings?.set('notificationsAllowed', !notificationsAllowed)
}}
title='This setting does as you expect, allow the launcher to send notifications for when stuff like downloading is done.'
/>
<Setting
label='Always show games in sidebar'
value={sidebarAlwaysShowGames}
onChange={async () => {
await settings?.set(
'sidebarAlwaysShowGames',
!sidebarAlwaysShowGames
)
}}
title="This setting will make it so when you are on a page like this, the games won't disappear."
/>
<Setting
label='Use wine if needed'
value={linuxUseWine}
onChange={async () => {
await settings?.set('linuxUseWine', !linuxUseWine)
}}
className={platform() == 'linux' ? '' : 'hidden'}
/>
<p hidden={!(platform() == 'linux' && linuxUseWine)}>Wine Command:</p>
<input
type='text'
value={linuxWineCommand}
onChange={async e => {
await settings?.set('linuxWineCommand', e.target.value)
}}
className={`input-field my-1 ${
platform() == 'linux' && linuxWineCommand ? '' : 'hidden'
}`}
/>
<div title='The theme you want the launcher to use.'>
<label className='text-lg'>Theme:</label>
<select
className='ml-2 bg-(--col2) border border-(--col4) rounded-md'
value={theme}
onChange={async e => {
await settings?.set('theme', e.target.value)
}}
>
<option value={'dark'}>Dark (default)</option>
<option value={'red'}>Red</option>
<option value={'blue'}>Blue</option>
<option value={'purple'}>Purple</option>
</select>
</div>
</div>
<p
className='fixed bottom-1.5 right-1.5 rounded-md cursor-pointer px-1 border z-100 transition-colors btntheme1'
onClick={async () => {
await copyToClipboard('v' + version, true)
}}
title='The current launcher version.'
>
v{version}
</p>
</>
)
}
+27
View File
@@ -0,0 +1,27 @@
import '@/styles/setting.css'
import { SettingProps } from '@/types/SettingProps'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCheck } from '@fortawesome/free-solid-svg-icons'
export function Setting ({
label,
value,
onChange,
className,
title
}: SettingProps) {
return (
<div className={`flex items-center gap-2 mb-2 ${className}`} title={title}>
<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>
)
}
+16 -4
View File
@@ -2,10 +2,13 @@
import '@/styles/sidebar.css' import '@/styles/sidebar.css'
import { Lexend } from 'next/font/google' import { Lexend } from 'next/font/google'
import Image from 'next/image'
import Icon from '@/assets/Icon.png' import Icon from '@/assets/Icon.png'
import Image from 'next/image'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCog } from '@fortawesome/free-solid-svg-icons'
import { faDiscord } from '@fortawesome/free-brands-svg-icons' import { faDiscord } from '@fortawesome/free-brands-svg-icons'
import { openUrl } from '@tauri-apps/plugin-opener' import { openUrl } from '@tauri-apps/plugin-opener'
@@ -15,13 +18,22 @@ const lexend = Lexend({
}) })
export default function Sidebar () { export default function Sidebar () {
const pathname = usePathname()
return ( return (
<aside className={'sidebar ' + lexend.className}> <aside className='sidebar'>
<div className='logo'> <div className='logo'>
<Image src={Icon} width={36} height={36} alt='' /> <Image src={Icon} width={36} height={36} alt='' />
<p>Lncvrt Games Launcher</p> <p className={lexend.className}>Lncvrt Games Launcher</p>
</div> </div>
<nav> <nav>
<Link
draggable={false}
href='/settings'
className={`link ${pathname === '/settings' ? 'active' : ''}`}
>
<FontAwesomeIcon icon={faCog} className='mr-1' /> Settings
</Link>
<button <button
onClick={() => openUrl('https://games.lncvrt.xyz/discord')} onClick={() => openUrl('https://games.lncvrt.xyz/discord')}
className='link mr-auto' className='link mr-auto'
-14
View File
@@ -1,14 +0,0 @@
'use client'
import { createContext, useContext } from 'react'
import type LauncherManifest from '@/types/LauncherManifest'
type LauncherContextType = {
launcherManifest: LauncherManifest | null
}
const LauncherContext = createContext<LauncherContextType | null>(null)
export const useLauncher = () => useContext(LauncherContext)
export default LauncherContext
+14
View File
@@ -0,0 +1,14 @@
'use client'
import { createContext, useContext } from 'react'
import type LauncherManifest from '@/types/LauncherManifest'
type GlobalProviderType = {
launcherManifest: LauncherManifest | null
}
const GlobalProvider = createContext<GlobalProviderType | null>(null)
export const useLauncher = () => useContext(GlobalProvider)
export default GlobalProvider
+102
View File
@@ -0,0 +1,102 @@
'use client'
import {
createContext,
useContext,
useState,
useEffect,
ReactNode
} from 'react'
import { load, Store } from '@tauri-apps/plugin-store'
type SettingsContextType = {
theme: string
notificationsAllowed: boolean
sidebarAlwaysShowGames: boolean
linuxUseWine: boolean
linuxWineCommand: string
settings: Store | null
}
const SettingsCtx = createContext<SettingsContextType | null>(null)
export const useSettings = () => {
const context = useContext(SettingsCtx)
if (!context) throw new Error('useSettings must be inside SettingsProvider')
return context
}
export function SettingsProvider ({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState('dark')
const [notificationsAllowed, setNotificationsAllowedState] = useState(false)
const [sidebarAlwaysShowGames, setSidebarAlwaysShowGamesState] =
useState(false)
const [linuxUseWine, setLinuxUseWineState] = useState(false)
const [linuxWineCommand, setLinuxWineCommandState] = useState('wine %path%')
const [store, setStore] = useState<Store | null>(null)
useEffect(() => {
;(async () => {
const settingsStore = await load('settings.json', {
autoSave: true,
defaults: {
theme: 'dark',
notificationsAllowed: false,
sidebarAlwaysShowGames: false,
linuxUseWine: false,
linuxWineCommand: 'wine %path%'
}
})
setStore(settingsStore)
const themeValue = (await settingsStore.get('theme')) as string
const notificationsValue = (await settingsStore.get(
'notificationsAllowed'
)) as boolean
const sidebarValue = (await settingsStore.get(
'sidebarAlwaysShowGames'
)) as boolean
const wineValue = (await settingsStore.get('linuxUseWine')) as boolean
const wineCommandValue = (await settingsStore.get(
'linuxWineCommand'
)) as string
setThemeState(themeValue)
setNotificationsAllowedState(notificationsValue)
setSidebarAlwaysShowGamesState(sidebarValue)
setLinuxUseWineState(wineValue)
setLinuxWineCommandState(wineCommandValue)
settingsStore.onKeyChange('theme', (value: unknown) =>
setThemeState(value as string)
)
settingsStore.onKeyChange('notificationsAllowed', (value: unknown) =>
setNotificationsAllowedState(value as boolean)
)
settingsStore.onKeyChange('sidebarAlwaysShowGames', (value: unknown) =>
setSidebarAlwaysShowGamesState(value as boolean)
)
settingsStore.onKeyChange('linuxUseWine', (value: unknown) =>
setLinuxUseWineState(value as boolean)
)
settingsStore.onKeyChange('linuxWineCommand', (value: unknown) =>
setLinuxWineCommandState(value as string)
)
})()
}, [])
return (
<SettingsCtx.Provider
value={{
theme,
notificationsAllowed,
sidebarAlwaysShowGames,
linuxUseWine,
linuxWineCommand,
settings: store
}}
>
{children}
</SettingsCtx.Provider>
)
}
+20
View File
@@ -55,3 +55,23 @@ body {
--col8: rgb(124, 112, 136); --col8: rgb(124, 112, 136);
--col9: rgb(140, 128, 152); --col9: rgb(140, 128, 152);
} }
.button {
@apply rounded-md cursor-pointer text-[16px] py-1 px-2 transition-colors border;
}
.btntheme1 {
@apply bg-(--col2) border-(--col4) hover:bg-(--col4) hover:border-(--col6) disabled:text-gray-300 disabled:bg-(--col1) disabled:border-(--col5) disabled:hover:text-gray-100 disabled:hover:bg-(--col2) disabled:hover:border-(--col6);
}
.btntheme2,
.downloads-entry .entry-info-item,
.downloads-entry .button {
@apply bg-(--col3) border-(--col5) hover:bg-(--col5) hover:border-(--col7) disabled:text-gray-300 disabled:bg-(--col2) disabled:border-(--col6) disabled:hover:text-gray-100 disabled:hover:bg-(--col3) disabled:hover:border-(--col7);
}
.btntheme3,
.downloads-entry:hover .entry-info-item,
.downloads-entry:hover .button {
@apply bg-(--col4) border-(--col6) hover:bg-(--col6) hover:border-(--col8) disabled:text-gray-300 disabled:bg-(--col2) disabled:border-(--col7) disabled:hover:text-gray-100 disabled:hover:bg-(--col4) disabled:hover:border-(--col8);
}
+17
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-(--col4) rounded-md bg-(--col2) transition-colors cursor-pointer;
}
.setting-checkbox:checked {
@apply bg-(--col4) border-(--col6);
}
.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;
}
+7
View File
@@ -0,0 +1,7 @@
export type SettingProps = {
label: string
value: boolean
onChange: (val: boolean) => void
className?: string
title?: string
}