import fixes

This commit is contained in:
Vitaly Turovsky 2025-03-19 02:12:18 +03:00
commit 61bf7b0cb7
8 changed files with 305 additions and 4 deletions

View file

@ -3,7 +3,7 @@ import * as react from 'react'
import { gt } from 'semver'
import { proxy } from 'valtio'
import { options } from './optionsStorage'
import { getStoredValue, setStoredValue } from './react/storageProvider'
import { appStorage } from './react/appStorageProvider'
import { showOptionsModal } from './react/SelectOption'
// #region Database
@ -243,11 +243,11 @@ const checkModsUpdates = async () => {
const refreshModRepositories = async () => {
if (options.modsAutoUpdate === 'never') return
const lastCheck = getStoredValue('modsAutoUpdateLastCheck')
const lastCheck = appStorage.modsAutoUpdateLastCheck
if (lastCheck && Date.now() - lastCheck < 1000 * 60 * 60 * options.modsUpdatePeriodCheck) return
await fetchAllRepositories()
// todo think of not updating check timestamp on offline access
setStoredValue('modsAutoUpdateLastCheck', Date.now())
appStorage.modsAutoUpdateLastCheck = Date.now()
}
export const installModByName = async (repoUrl: string, name: string) => {

View file

@ -214,7 +214,7 @@ export const guiOptionsScheme: {
{
custom () {
const modsUpdateSnapshot = useSnapshot(modsUpdateStatus)
return <Button label={`Client Mods: ${Object.keys(window.loadedMods ?? {}).length} (${Object.keys(modsUpdateSnapshot).length})}`} onClick={() => showModal({ reactType: 'mods' })} inScreen />
return <Button label={`Client Mods: ${Object.keys(window.loadedMods ?? {}).length} (${Object.keys(modsUpdateSnapshot).length})`} onClick={() => showModal({ reactType: 'mods' })} inScreen />
},
},
{

View file

@ -62,6 +62,9 @@ const defaultOptions = {
// todo ui setting, maybe enable by default?
waitForChunksRender: 'sp-only' as 'sp-only' | boolean,
jeiEnabled: true as boolean | Array<'creative' | 'survival' | 'adventure' | 'spectator'>,
modsSupport: false,
modsAutoUpdate: 'check' as 'check' | 'never' | 'always',
modsUpdatePeriodCheck: 24, // hours
// antiAliasing: false,

View file

@ -1,13 +1,169 @@
import { useEffect, useState } from 'react'
import { getAllModsDisplayList } from '../clientMods'
import { useIsModalActive } from './utilsApp'
import Input from './Input'
import Button from './Button'
import styles from './mods.module.css'
type ModsData = Awaited<ReturnType<typeof getAllModsDisplayList>>
export default () => {
const isModalActive = useIsModalActive('mods')
const [modsData, setModsData] = useState<ModsData | null>(null)
const [search, setSearch] = useState('')
const [showOnlyInstalled, setShowOnlyInstalled] = useState(false)
const [selectedMod, setSelectedMod] = useState<ModsData['repos'][0]['packages'][0] | null>(null)
const [expandedRepos, setExpandedRepos] = useState<Record<string, boolean>>({})
useEffect(() => {
if (isModalActive) {
void getAllModsDisplayList().then(setModsData)
}
}, [isModalActive])
if (!isModalActive) return null
const toggleRepo = (repoUrl: string) => {
setExpandedRepos(prev => ({
...prev,
[repoUrl]: !prev[repoUrl]
}))
}
const filteredMods = modsData ? {
repos: modsData.repos.map(repo => ({
...repo,
packages: repo.packages.filter(mod => {
const matchesSearch = mod.name.toLowerCase().includes(search.toLowerCase()) ||
mod.description?.toLowerCase().includes(search.toLowerCase())
const matchesFilter = !showOnlyInstalled || mod.installed
return matchesSearch && matchesFilter
})
})),
modsWithoutRepos: modsData.modsWithoutRepos.filter(mod => {
const matchesSearch = mod.name.toLowerCase().includes(search.toLowerCase()) ||
mod.description?.toLowerCase().includes(search.toLowerCase())
const matchesFilter = !showOnlyInstalled || mod.installed
return matchesSearch && matchesFilter
})
} : null
return <div>
<div className="dirt-bg" />
<div className="fullscreen">
<div className="screen-title">Client Mods</div>
<div className={styles.root}>
<div className={styles.header}>
<Input
className={styles.searchBar}
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search mods..."
/>
<Button
className={styles.filterButton}
onClick={() => setShowOnlyInstalled(!showOnlyInstalled)}
>
{showOnlyInstalled ? 'Show All' : 'Show Installed'}
</Button>
<Button onClick={() => {}}>Manage Repos</Button>
</div>
<div className={styles.content}>
<div className={styles.modList}>
{filteredMods ? (
<>
{filteredMods.repos.map(repo => (
<div key={repo.url}>
<div
className={styles.repoHeader}
onClick={() => toggleRepo(repo.url)}
>
<span>{expandedRepos[repo.url] ? '▼' : '▶'}</span>
<span>{repo.name || repo.url}</span>
<span>({repo.packages.length})</span>
</div>
{expandedRepos[repo.url] && (
<div className={styles.repoContent}>
{repo.packages.map(mod => (
<div
key={mod.name}
className={styles.modRow}
onClick={() => setSelectedMod(mod)}
>
<div className={styles.modRowTitle}>{mod.name}</div>
<div className={styles.modRowInfo}>
{mod.description}
{mod.author && ` • By ${mod.author}`}
{mod.version && ` • v${mod.version}`}
</div>
</div>
))}
</div>
)}
</div>
))}
{filteredMods.modsWithoutRepos.length > 0 && (
<div>
<div className={styles.repoHeader}>
<span></span>
<span>Other Mods</span>
<span>({filteredMods.modsWithoutRepos.length})</span>
</div>
<div className={styles.repoContent}>
{filteredMods.modsWithoutRepos.map(mod => (
<div
key={mod.name}
className={styles.modRow}
onClick={() => setSelectedMod(mod)}
>
<div className={styles.modRowTitle}>{mod.name}</div>
<div className={styles.modRowInfo}>
{mod.description}
{mod.author && ` • By ${mod.author}`}
{mod.version && ` • v${mod.version}`}
</div>
</div>
))}
</div>
</div>
)}
</>
) : (
<div className={styles.modRowInfo}>Loading mods...</div>
)}
</div>
<div className={styles.sidebar}>
{selectedMod ? (
<>
<div className={styles.modInfo}>
<div className={styles.modInfoTitle}>{selectedMod.name}</div>
<div className={styles.modInfoText}>
{selectedMod.description}
</div>
<div className={styles.modInfoText}>
{selectedMod.author && `Author: ${selectedMod.author}`}
{selectedMod.version && `\nVersion: ${selectedMod.version}`}
{selectedMod.section && `\nSection: ${selectedMod.section}`}
</div>
</div>
<div className={styles.modActions}>
<Button onClick={() => {}}>
{selectedMod.installed ? 'Uninstall' : 'Install'}
</Button>
{selectedMod.installed && (
<>
<Button onClick={() => {}}>Update</Button>
<Button onClick={() => {}}>Delete</Button>
</>
)}
</div>
</>
) : (
<div className={styles.modInfoText}>Select a mod to view details</div>
)}
</div>
</div>
</div>
</div>
</div>
}

View file

@ -37,6 +37,7 @@ type StorageData = {
serversHistory: ServerHistoryEntry[]
authenticatedAccounts: AuthenticatedAccount[]
serversList: StoreServerItem[] | undefined
modsAutoUpdateLastCheck: number | undefined
}
const oldKeysAliases: Partial<Record<keyof StorageData, string>> = {
@ -76,6 +77,7 @@ const defaultStorageData: StorageData = {
serversHistory: [],
authenticatedAccounts: [],
serversList: undefined,
modsAutoUpdateLastCheck: undefined,
}
export const setStorageDataOnAppConfigLoad = () => {

117
src/react/mods.module.css Normal file
View file

@ -0,0 +1,117 @@
.root {
display: flex;
flex-direction: column;
height: 100%;
padding: 10px;
gap: 10px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
}
.searchBar {
flex: 1;
}
.filterButton {
width: 120px;
}
.content {
display: flex;
flex: 1;
gap: 10px;
overflow: hidden;
}
.modList {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 5px;
}
.sidebar {
width: 200px;
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px;
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
}
.modInfo {
display: flex;
flex-direction: column;
gap: 5px;
}
.modInfoTitle {
font-size: 12px;
font-weight: bold;
color: white;
}
.modInfoText {
font-size: 10px;
color: #bcbcbc;
}
.modActions {
display: flex;
flex-direction: column;
gap: 5px;
}
.modRow {
display: flex;
flex-direction: column;
padding: 8px;
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
cursor: pointer;
}
.modRow:hover {
background: rgba(0, 0, 0, 0.3);
}
.modRowTitle {
font-size: 12px;
color: white;
margin-bottom: 4px;
}
.modRowInfo {
font-size: 10px;
color: #bcbcbc;
}
.repoHeader {
display: flex;
align-items: center;
gap: 4px;
color: #bcbcbc;
font-size: 8px;
cursor: pointer;
padding: 4px;
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
.repoHeader:hover {
background: rgba(0, 0, 0, 0.3);
}
.repoContent {
margin-left: 10px;
display: flex;
flex-direction: column;
gap: 5px;
}

22
src/react/mods.module.css.d.ts vendored Normal file
View file

@ -0,0 +1,22 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
content: string;
filterButton: string;
header: string;
modActions: string;
modInfo: string;
modInfoText: string;
modInfoTitle: string;
modList: string;
modRow: string;
modRowInfo: string;
modRowTitle: string;
repoContent: string;
repoHeader: string;
root: string;
searchBar: string;
sidebar: string;
}
declare const cssExports: CssExports;
export default cssExports;

View file

@ -211,6 +211,7 @@ const App = () => {
<NoModalFoundProvider />
<PacketsReplayProvider />
<NotificationProvider />
<ModsPage />
</RobustPortal>
<RobustPortal to={document.body}>
<div className='overlay-top-scaled'>