mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-23 15:17:09 +00:00
Projects (#192)
* Add file save UI and drafts project * Fix build * Create SearchList component as abstraction * Add project page and file tree view * Create Locale context * Create Theme context * Create Version context * Create Title context * Create Project context * Store current file in project context * Fix issues when renaming file and implement deleting * Style improvements * Make all project strings translatable * Fix z-index
This commit is contained in:
28
src/app/App.tsx
Normal file
28
src/app/App.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { RouterOnChangeArgs } from 'preact-router'
|
||||
import { Router } from 'preact-router'
|
||||
import '../styles/global.css'
|
||||
import '../styles/nodes.css'
|
||||
import { Analytics } from './Analytics'
|
||||
import { Header } from './components'
|
||||
import { Category, Changelog, Generator, Home, Project, Sounds } from './pages'
|
||||
import { cleanUrl } from './Utils'
|
||||
|
||||
export function App() {
|
||||
const changeRoute = (e: RouterOnChangeArgs) => {
|
||||
// Needs a timeout to ensure the title is set correctly
|
||||
setTimeout(() => Analytics.pageview(cleanUrl(e.url)))
|
||||
}
|
||||
|
||||
return <>
|
||||
<Header />
|
||||
<Router onChange={changeRoute}>
|
||||
<Home path="/" />
|
||||
<Category path="/worldgen" category="worldgen" />
|
||||
<Category path="/assets" category="assets" />
|
||||
<Sounds path="/sounds" />
|
||||
<Changelog path="/changelog" />
|
||||
<Project path="/project" />
|
||||
<Generator default />
|
||||
</Router>
|
||||
</>
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import config from '../config.json'
|
||||
import English from '../locales/en.json'
|
||||
|
||||
export type Localize = (key: string, ...params: string[]) => string
|
||||
|
||||
interface Locale {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
export const Locales: {
|
||||
[key: string]: Locale,
|
||||
} = {
|
||||
fallback: English,
|
||||
}
|
||||
|
||||
function resolveLocaleParams(value: string, params?: string[]): string {
|
||||
return value.replace(/%\d+%/g, match => {
|
||||
const index = parseInt(match.slice(1, -1))
|
||||
return params?.[index] !== undefined ? params[index] : match
|
||||
})
|
||||
}
|
||||
|
||||
export function locale(language: string, key: string, ...params: string[]): string {
|
||||
const value: string | undefined = Locales[language]?.[key]
|
||||
?? Locales.en?.[key] ?? Locales.fallback[key] ?? key
|
||||
return resolveLocaleParams(value, params)
|
||||
}
|
||||
|
||||
export async function loadLocale(language: string) {
|
||||
const langConfig = config.languages.find(lang => lang.code === language)
|
||||
if (!langConfig) return
|
||||
const data = await import(`../locales/${language}.json`)
|
||||
const schema = langConfig.schemas !== false
|
||||
&& await import(`../../node_modules/@mcschema/locales/src/${language}.json`)
|
||||
Locales[language] = { ...data.default, ...schema.default }
|
||||
}
|
||||
@@ -1,93 +1,21 @@
|
||||
import { render } from 'preact'
|
||||
import type { RouterOnChangeArgs } from 'preact-router'
|
||||
import { getCurrentUrl, Router } from 'preact-router'
|
||||
import { useCallback, useEffect, useState } from 'preact/hooks'
|
||||
import config from '../config.json'
|
||||
import '../styles/global.css'
|
||||
import '../styles/nodes.css'
|
||||
import { Analytics } from './Analytics'
|
||||
import { Header } from './components'
|
||||
import { loadLocale, locale, Locales } from './Locales'
|
||||
import { Category, Changelog, Generator, Home, Sounds } from './pages'
|
||||
import type { VersionId } from './services'
|
||||
import { VersionIds } from './services'
|
||||
import { Store } from './Store'
|
||||
import { cleanUrl, getSearchParams, setSeachParams } from './Utils'
|
||||
|
||||
const VERSIONS_IN_TITLE = 3
|
||||
import { App } from './App'
|
||||
import { LocaleProvider, ProjectProvider, ThemeProvider, TitleProvider, VersionProvider } from './contexts'
|
||||
|
||||
function Main() {
|
||||
const [lang, setLanguage] = useState<string>('none')
|
||||
const changeLanguage = async (language: string) => {
|
||||
if (!Locales[language]) {
|
||||
await loadLocale(language)
|
||||
}
|
||||
Analytics.setLanguage(language)
|
||||
Store.setLanguage(language)
|
||||
setLanguage(language)
|
||||
}
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const target = Store.getLanguage()
|
||||
await Promise.all([
|
||||
loadLocale('en'),
|
||||
...(target !== 'en' ? [loadLocale(target)] : []),
|
||||
])
|
||||
setLanguage(target)
|
||||
})()
|
||||
}, [])
|
||||
|
||||
const [theme, setTheme] = useState<string>(Store.getTheme())
|
||||
const changeTheme = (theme: string) => {
|
||||
Analytics.setTheme(theme)
|
||||
Store.setTheme(theme)
|
||||
setTheme(theme)
|
||||
}
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute('data-theme', theme)
|
||||
}, [theme])
|
||||
|
||||
const searchParams = getSearchParams(getCurrentUrl())
|
||||
const targetVersion = searchParams.get('version')
|
||||
const [version, setVersion] = useState<VersionId>(Store.getVersion())
|
||||
const changeVersion = useCallback((version: VersionId) => {
|
||||
if (getSearchParams(getCurrentUrl()).has('version')) {
|
||||
setSeachParams({ version })
|
||||
}
|
||||
Analytics.setVersion(version)
|
||||
Store.setVersion(version)
|
||||
setVersion(version)
|
||||
}, [targetVersion])
|
||||
useEffect(() => {
|
||||
if (VersionIds.includes(targetVersion as VersionId) && version !== targetVersion) {
|
||||
setVersion(targetVersion as VersionId)
|
||||
}
|
||||
}, [version, targetVersion])
|
||||
|
||||
const [title, setTitle] = useState<string>(locale(lang, 'title.home'))
|
||||
const changeTitle = (title: string, versions?: VersionId[]) => {
|
||||
versions ??= config.versions.map(v => v.id as VersionId)
|
||||
const titleVersions = versions.slice(versions.length - VERSIONS_IN_TITLE)
|
||||
document.title = `${title} Minecraft ${titleVersions.join(', ')}`
|
||||
setTitle(title)
|
||||
}
|
||||
|
||||
const changeRoute = (e: RouterOnChangeArgs) => {
|
||||
// Needs a timeout to ensure the title is set correctly
|
||||
setTimeout(() => Analytics.pageview(cleanUrl(e.url)))
|
||||
}
|
||||
|
||||
return <>
|
||||
<Header {...{lang, title, version, theme, language: lang, changeLanguage, changeTheme}} />
|
||||
<Router onChange={changeRoute}>
|
||||
<Home path="/" {...{lang, changeTitle}} />
|
||||
<Category path="/worldgen" category="worldgen" {...{lang, changeTitle}} />
|
||||
<Category path="/assets" category="assets" {...{lang, changeTitle}} />
|
||||
<Sounds path="/sounds" {...{lang, version, changeTitle, changeVersion}} />
|
||||
<Changelog path="/changelog" {...{lang, changeTitle}} />
|
||||
<Generator default {...{lang, version, changeTitle, changeVersion}} />
|
||||
</Router>
|
||||
</>
|
||||
return <LocaleProvider>
|
||||
<ThemeProvider>
|
||||
<VersionProvider>
|
||||
<TitleProvider>
|
||||
<ProjectProvider>
|
||||
<App />
|
||||
</ProjectProvider>
|
||||
</TitleProvider>
|
||||
</VersionProvider>
|
||||
</ThemeProvider>
|
||||
</LocaleProvider>
|
||||
}
|
||||
|
||||
render(<Main />, document.body)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Project } from './contexts'
|
||||
import { DRAFT_PROJECT } from './contexts'
|
||||
import type { VersionId } from './services'
|
||||
import { VersionIds } from './services'
|
||||
|
||||
@@ -8,6 +10,7 @@ export namespace Store {
|
||||
export const ID_INDENT = 'indentation'
|
||||
export const ID_FORMAT = 'output_format'
|
||||
export const ID_SOUNDS_VERSION = 'minecraft_sounds_version'
|
||||
export const ID_PROJECTS = 'misode_projects'
|
||||
|
||||
export function getLanguage() {
|
||||
return localStorage.getItem(ID_LANGUAGE) ?? 'en'
|
||||
@@ -37,6 +40,14 @@ export namespace Store {
|
||||
return localStorage.getItem(ID_SOUNDS_VERSION) ?? 'latest'
|
||||
}
|
||||
|
||||
export function getProjects(): Project[] {
|
||||
const projects = localStorage.getItem(ID_PROJECTS)
|
||||
if (projects) {
|
||||
return JSON.parse(projects) as Project[]
|
||||
}
|
||||
return [DRAFT_PROJECT]
|
||||
}
|
||||
|
||||
export function setLanguage(language: string | undefined) {
|
||||
if (language) localStorage.setItem(ID_LANGUAGE, language)
|
||||
}
|
||||
@@ -60,4 +71,8 @@ export namespace Store {
|
||||
export function setSoundsVersion(version: string | undefined) {
|
||||
if (version) localStorage.setItem(ID_SOUNDS_VERSION, version)
|
||||
}
|
||||
|
||||
export function setProjects(projects: Project[] | undefined) {
|
||||
if (projects) localStorage.setItem(ID_PROJECTS, JSON.stringify(projects))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,15 @@ interface BtnMenuProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
||||
label?: string,
|
||||
relative?: boolean,
|
||||
tooltip?: string,
|
||||
tooltipLoc?: 'se' | 'sw' | 'nw',
|
||||
children: ComponentChildren,
|
||||
}
|
||||
export function BtnMenu(props: BtnMenuProps) {
|
||||
const { icon, label, relative, tooltip, children } = props
|
||||
const { icon, label, relative, tooltip, tooltipLoc, children } = props
|
||||
const [active, setActive] = useFocus()
|
||||
|
||||
return <div class={`btn-menu${relative === false ? ' no-relative' : ''}`} {...props}>
|
||||
<Btn {...{icon, label, tooltip}} onClick={setActive} />
|
||||
<Btn {...{icon, label, tooltip, tooltipLoc}} onClick={setActive} />
|
||||
{active && <div class="btn-group">
|
||||
{children}
|
||||
</div>}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { getCurrentUrl, Link, route } from 'preact-router'
|
||||
import { Btn, BtnMenu, Icons, Octicon } from '.'
|
||||
import config from '../../config.json'
|
||||
import { locale } from '../Locales'
|
||||
import type { VersionId } from '../services'
|
||||
import { useLocale, useTheme, useTitle, useVersion } from '../contexts'
|
||||
import { checkVersion } from '../services'
|
||||
import { cleanUrl, getGenerator } from '../Utils'
|
||||
|
||||
@@ -12,51 +11,45 @@ const Themes: Record<string, keyof typeof Octicon> = {
|
||||
light: 'sun',
|
||||
}
|
||||
|
||||
type HeaderProps = {
|
||||
lang: string,
|
||||
title: string,
|
||||
version: VersionId,
|
||||
theme: string,
|
||||
changeTheme: (theme: string) => unknown,
|
||||
language: string,
|
||||
changeLanguage: (language: string) => unknown,
|
||||
}
|
||||
export function Header({ lang, title, version, theme, changeTheme, language, changeLanguage }: HeaderProps) {
|
||||
const loc = locale.bind(null, lang)
|
||||
export function Header() {
|
||||
const { lang, locale, changeLanguage } = useLocale()
|
||||
const { theme, changeTheme } = useTheme()
|
||||
const { version } = useVersion()
|
||||
const { title } = useTitle()
|
||||
const gen = getGenerator(getCurrentUrl())
|
||||
|
||||
return <header>
|
||||
<div class="title">
|
||||
<Link class="home-link" href="/" aria-label={loc('home')} data-cy="home-link">{Icons.home}</Link>
|
||||
<Link class="home-link" href="/" aria-label={locale('home')} data-cy="home-link">{Icons.home}</Link>
|
||||
<h1>{title}</h1>
|
||||
{gen && <BtnMenu icon="chevron_down" tooltip={loc('switch_generator')} data-cy="generator-switcher">
|
||||
{gen && <BtnMenu icon="chevron_down" tooltip={locale('switch_generator')} data-cy="generator-switcher">
|
||||
{config.generators
|
||||
.filter(g => g.category === gen?.category && checkVersion(version, g.minVersion))
|
||||
.map(g =>
|
||||
<Btn label={loc(g.id)} active={g.id === gen.id} onClick={() => route(cleanUrl(g.url))} />
|
||||
<Btn label={locale(g.id)} active={g.id === gen.id} onClick={() => route(cleanUrl(g.url))} />
|
||||
)}
|
||||
</BtnMenu>}
|
||||
</div>
|
||||
<nav>
|
||||
<ul>
|
||||
<li data-cy="language-switcher">
|
||||
<BtnMenu icon="globe" tooltip={loc('language')}>
|
||||
<BtnMenu icon="globe" tooltip={locale('language')}>
|
||||
{config.languages.map(({ code, name }) =>
|
||||
<Btn label={name} active={code === language}
|
||||
<Btn label={name} active={code === lang}
|
||||
onClick={() => changeLanguage(code)} />
|
||||
)}
|
||||
</BtnMenu>
|
||||
</li>
|
||||
<li data-cy="theme-switcher">
|
||||
<BtnMenu icon={Themes[theme]} tooltip={loc('theme')}>
|
||||
<BtnMenu icon={Themes[theme]} tooltip={locale('theme')}>
|
||||
{Object.entries(Themes).map(([th, icon]) =>
|
||||
<Btn icon={icon} label={loc(`theme.${th}`)} active={th === theme}
|
||||
<Btn icon={icon} label={locale(`theme.${th}`)} active={th === theme}
|
||||
onClick={() => changeTheme(th)} />
|
||||
)}
|
||||
</BtnMenu>
|
||||
</li>
|
||||
<li class="dimmed">
|
||||
<a href="https://github.com/misode/misode.github.io" target="_blank" rel="noreferrer" class="tooltipped tip-sw" aria-label={loc('github')}>
|
||||
<a href="https://github.com/misode/misode.github.io" target="_blank" rel="noreferrer" class="tooltipped tip-sw" aria-label={locale('github')}>
|
||||
{Octicon.mark_github}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -11,10 +11,13 @@ export const Octicon = {
|
||||
code: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.72 3.22a.75.75 0 011.06 1.06L2.06 8l3.72 3.72a.75.75 0 11-1.06 1.06L.47 8.53a.75.75 0 010-1.06l4.25-4.25zm6.56 0a.75.75 0 10-1.06 1.06L13.94 8l-3.72 3.72a.75.75 0 101.06 1.06l4.25-4.25a.75.75 0 000-1.06l-4.25-4.25z"></path></svg>,
|
||||
dash: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2 8a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H2.75A.75.75 0 012 8z"></path></svg>,
|
||||
device_desktop: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.75 2.5h12.5a.25.25 0 01.25.25v7.5a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25v-7.5a.25.25 0 01.25-.25zM14.25 1H1.75A1.75 1.75 0 000 2.75v7.5C0 11.216.784 12 1.75 12h3.727c-.1 1.041-.52 1.872-1.292 2.757A.75.75 0 004.75 16h6.5a.75.75 0 00.565-1.243c-.772-.885-1.193-1.716-1.292-2.757h3.727A1.75 1.75 0 0016 10.25v-7.5A1.75 1.75 0 0014.25 1zM9.018 12H6.982a5.72 5.72 0 01-.765 2.5h3.566a5.72 5.72 0 01-.765-2.5z"></path></svg>,
|
||||
dot_fill: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"></path></svg>,
|
||||
download: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.47 10.78a.75.75 0 001.06 0l3.75-3.75a.75.75 0 00-1.06-1.06L8.75 8.44V1.75a.75.75 0 00-1.5 0v6.69L4.78 5.97a.75.75 0 00-1.06 1.06l3.75 3.75zM3.75 13a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5z"></path></svg>,
|
||||
duplicate: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M10.5 3a.75.75 0 01.75.75v1h1a.75.75 0 010 1.5h-1v1a.75.75 0 01-1.5 0v-1h-1a.75.75 0 010-1.5h1v-1A.75.75 0 0110.5 3z"></path><path fill-rule="evenodd" d="M6.75 0A1.75 1.75 0 005 1.75v7.5c0 .966.784 1.75 1.75 1.75h7.5A1.75 1.75 0 0016 9.25v-7.5A1.75 1.75 0 0014.25 0h-7.5zM6.5 1.75a.25.25 0 01.25-.25h7.5a.25.25 0 01.25.25v7.5a.25.25 0 01-.25.25h-7.5a.25.25 0 01-.25-.25v-7.5z"></path><path d="M1.75 5A1.75 1.75 0 000 6.75v7.5C0 15.216.784 16 1.75 16h7.5A1.75 1.75 0 0011 14.25v-1.5a.75.75 0 00-1.5 0v1.5a.25.25 0 01-.25.25h-7.5a.25.25 0 01-.25-.25v-7.5a.25.25 0 01.25-.25h1.5a.75.75 0 000-1.5h-1.5z"></path></svg>,
|
||||
eye: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.679 7.932c.412-.621 1.242-1.75 2.366-2.717C5.175 4.242 6.527 3.5 8 3.5c1.473 0 2.824.742 3.955 1.715 1.124.967 1.954 2.096 2.366 2.717a.119.119 0 010 .136c-.412.621-1.242 1.75-2.366 2.717C10.825 11.758 9.473 12.5 8 12.5c-1.473 0-2.824-.742-3.955-1.715C2.92 9.818 2.09 8.69 1.679 8.068a.119.119 0 010-.136zM8 2c-1.981 0-3.67.992-4.933 2.078C1.797 5.169.88 6.423.43 7.1a1.619 1.619 0 000 1.798c.45.678 1.367 1.932 2.637 3.024C4.329 13.008 6.019 14 8 14c1.981 0 3.67-.992 4.933-2.078 1.27-1.091 2.187-2.345 2.637-3.023a1.619 1.619 0 000-1.798c-.45-.678-1.367-1.932-2.637-3.023C11.671 2.992 9.981 2 8 2zm0 8a2 2 0 100-4 2 2 0 000 4z"></path></svg>,
|
||||
eye_closed: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M.143 2.31a.75.75 0 011.047-.167l14.5 10.5a.75.75 0 11-.88 1.214l-2.248-1.628C11.346 13.19 9.792 14 8 14c-1.981 0-3.67-.992-4.933-2.078C1.797 10.832.88 9.577.43 8.9a1.618 1.618 0 010-1.797c.353-.533.995-1.42 1.868-2.305L.31 3.357A.75.75 0 01.143 2.31zm3.386 3.378a14.21 14.21 0 00-1.85 2.244.12.12 0 00-.022.068c0 .021.006.045.022.068.412.621 1.242 1.75 2.366 2.717C5.175 11.758 6.527 12.5 8 12.5c1.195 0 2.31-.488 3.29-1.191L9.063 9.695A2 2 0 016.058 7.52l-2.53-1.832zM8 3.5c-.516 0-1.017.09-1.499.251a.75.75 0 11-.473-1.423A6.23 6.23 0 018 2c1.981 0 3.67.992 4.933 2.078 1.27 1.091 2.187 2.345 2.637 3.023a1.619 1.619 0 010 1.798c-.11.166-.248.365-.41.587a.75.75 0 11-1.21-.887c.148-.201.272-.382.371-.53a.119.119 0 000-.137c-.412-.621-1.242-1.75-2.366-2.717C10.825 4.242 9.473 3.5 8 3.5z"></path></svg>,
|
||||
file: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M3.75 1.5a.25.25 0 00-.25.25v11.5c0 .138.112.25.25.25h8.5a.25.25 0 00.25-.25V6H9.75A1.75 1.75 0 018 4.25V1.5H3.75zm5.75.56v2.19c0 .138.112.25.25.25h2.19L9.5 2.06zM2 1.75C2 .784 2.784 0 3.75 0h5.086c.464 0 .909.184 1.237.513l3.414 3.414c.329.328.513.773.513 1.237v8.086A1.75 1.75 0 0112.25 15h-8.5A1.75 1.75 0 012 13.25V1.75z"></path></svg>,
|
||||
file_directory: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.75 1A1.75 1.75 0 000 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0016 13.25v-8.5A1.75 1.75 0 0014.25 3h-6.5a.25.25 0 01-.2-.1l-.9-1.2c-.33-.44-.85-.7-1.4-.7h-3.5z"></path></svg>,
|
||||
gear: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.429 1.525a6.593 6.593 0 011.142 0c.036.003.108.036.137.146l.289 1.105c.147.56.55.967.997 1.189.174.086.341.183.501.29.417.278.97.423 1.53.27l1.102-.303c.11-.03.175.016.195.046.219.31.41.641.573.989.014.031.022.11-.059.19l-.815.806c-.411.406-.562.957-.53 1.456a4.588 4.588 0 010 .582c-.032.499.119 1.05.53 1.456l.815.806c.08.08.073.159.059.19a6.494 6.494 0 01-.573.99c-.02.029-.086.074-.195.045l-1.103-.303c-.559-.153-1.112-.008-1.529.27-.16.107-.327.204-.5.29-.449.222-.851.628-.998 1.189l-.289 1.105c-.029.11-.101.143-.137.146a6.613 6.613 0 01-1.142 0c-.036-.003-.108-.037-.137-.146l-.289-1.105c-.147-.56-.55-.967-.997-1.189a4.502 4.502 0 01-.501-.29c-.417-.278-.97-.423-1.53-.27l-1.102.303c-.11.03-.175-.016-.195-.046a6.492 6.492 0 01-.573-.989c-.014-.031-.022-.11.059-.19l.815-.806c.411-.406.562-.957.53-1.456a4.587 4.587 0 010-.582c.032-.499-.119-1.05-.53-1.456l-.815-.806c-.08-.08-.073-.159-.059-.19a6.44 6.44 0 01.573-.99c.02-.029.086-.075.195-.045l1.103.303c.559.153 1.112.008 1.529-.27.16-.107.327-.204.5-.29.449-.222.851-.628.998-1.189l.289-1.105c.029-.11.101-.143.137-.146zM8 0c-.236 0-.47.01-.701.03-.743.065-1.29.615-1.458 1.261l-.29 1.106c-.017.066-.078.158-.211.224a5.994 5.994 0 00-.668.386c-.123.082-.233.09-.3.071L3.27 2.776c-.644-.177-1.392.02-1.82.63a7.977 7.977 0 00-.704 1.217c-.315.675-.111 1.422.363 1.891l.815.806c.05.048.098.147.088.294a6.084 6.084 0 000 .772c.01.147-.038.246-.088.294l-.815.806c-.474.469-.678 1.216-.363 1.891.2.428.436.835.704 1.218.428.609 1.176.806 1.82.63l1.103-.303c.066-.019.176-.011.299.071.213.143.436.272.668.386.133.066.194.158.212.224l.289 1.106c.169.646.715 1.196 1.458 1.26a8.094 8.094 0 001.402 0c.743-.064 1.29-.614 1.458-1.26l.29-1.106c.017-.066.078-.158.211-.224a5.98 5.98 0 00.668-.386c.123-.082.233-.09.3-.071l1.102.302c.644.177 1.392-.02 1.82-.63.268-.382.505-.789.704-1.217.315-.675.111-1.422-.364-1.891l-.814-.806c-.05-.048-.098-.147-.088-.294a6.1 6.1 0 000-.772c-.01-.147.039-.246.088-.294l.814-.806c.475-.469.679-1.216.364-1.891a7.992 7.992 0 00-.704-1.218c-.428-.609-1.176-.806-1.82-.63l-1.103.303c-.066.019-.176.011-.299-.071a5.991 5.991 0 00-.668-.386c-.133-.066-.194-.158-.212-.224L10.16 1.29C9.99.645 9.444.095 8.701.031A8.094 8.094 0 008 0zm1.5 8a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM11 8a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>,
|
||||
globe: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.543 7.25h2.733c.144-2.074.866-3.756 1.58-4.948.12-.197.237-.381.353-.552a6.506 6.506 0 00-4.666 5.5zm2.733 1.5H1.543a6.506 6.506 0 004.666 5.5 11.13 11.13 0 01-.352-.552c-.715-1.192-1.437-2.874-1.581-4.948zm1.504 0h4.44a9.637 9.637 0 01-1.363 4.177c-.306.51-.612.919-.857 1.215a9.978 9.978 0 01-.857-1.215A9.637 9.637 0 015.78 8.75zm4.44-1.5H5.78a9.637 9.637 0 011.363-4.177c.306-.51.612-.919.857-1.215.245.296.55.705.857 1.215A9.638 9.638 0 0110.22 7.25zm1.504 1.5c-.144 2.074-.866 3.756-1.58 4.948-.12.197-.237.381-.353.552a6.506 6.506 0 004.666-5.5h-2.733zm2.733-1.5h-2.733c-.144-2.074-.866-3.756-1.58-4.948a11.738 11.738 0 00-.353-.552 6.506 6.506 0 014.666 5.5zM8 0a8 8 0 100 16A8 8 0 008 0z"></path></svg>,
|
||||
history: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.643 3.143L.427 1.927A.25.25 0 000 2.104V5.75c0 .138.112.25.25.25h3.646a.25.25 0 00.177-.427L2.715 4.215a6.5 6.5 0 11-1.18 4.458.75.75 0 10-1.493.154 8.001 8.001 0 101.6-5.684zM7.75 4a.75.75 0 01.75.75v2.992l2.028.812a.75.75 0 01-.557 1.392l-2.5-1A.75.75 0 017 8.25v-3.5A.75.75 0 017.75 4z"></path></svg>,
|
||||
@@ -28,6 +31,7 @@ export const Octicon = {
|
||||
play: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zM6.379 5.227A.25.25 0 006 5.442v5.117a.25.25 0 00.379.214l4.264-2.559a.25.25 0 000-.428L6.379 5.227z"></path></svg>,
|
||||
plus: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 2a.75.75 0 01.75.75v4.5h4.5a.75.75 0 010 1.5h-4.5v4.5a.75.75 0 01-1.5 0v-4.5h-4.5a.75.75 0 010-1.5h4.5v-4.5A.75.75 0 018 2z"></path></svg>,
|
||||
plus_circle: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zm.75 4.75a.75.75 0 00-1.5 0v2.5h-2.5a.75.75 0 000 1.5h2.5v2.5a.75.75 0 001.5 0v-2.5h2.5a.75.75 0 000-1.5h-2.5v-2.5z"></path></svg>,
|
||||
repo: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"></path></svg>,
|
||||
search: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M11.5 7a4.499 4.499 0 11-8.998 0A4.499 4.499 0 0111.5 7zm-.82 4.74a6 6 0 111.06-1.06l3.04 3.04a.75.75 0 11-1.06 1.06l-3.04-3.04z"></path></svg>,
|
||||
sort_asc: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M0 4.25a.75.75 0 01.75-.75h2.5a.75.75 0 010 1.5H.75A.75.75 0 010 4.25zm0 4a.75.75 0 01.75-.75h4.5a.75.75 0 010 1.5H.75A.75.75 0 010 8.25zm0 4a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5H.75a.75.75 0 01-.75-.75zm12.927-9.677a.25.25 0 00-.354 0l-3 3A.25.25 0 009.75 6H12v6.75a.75.75 0 001.5 0V6h2.25a.25.25 0 00.177-.427l-3-3z"></path></svg>,
|
||||
sort_desc: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M0 4.25a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5H.75A.75.75 0 010 4.25zm0 4a.75.75 0 01.75-.75h4.5a.75.75 0 010 1.5H.75A.75.75 0 010 8.25zm0 4a.75.75 0 01.75-.75h2.5a.75.75 0 010 1.5H.75a.75.75 0 01-.75-.75z"></path><path d="M13.5 10h2.25a.25.25 0 01.177.427l-3 3a.25.25 0 01-.354 0l-3-3A.25.25 0 019.75 10H12V3.75a.75.75 0 011.5 0V10z"></path></svg>,
|
||||
|
||||
58
src/app/components/TreeView.tsx
Normal file
58
src/app/components/TreeView.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useMemo, useState } from 'preact/hooks'
|
||||
import { Octicon } from '.'
|
||||
|
||||
const SEPARATOR = '/'
|
||||
|
||||
interface Props {
|
||||
entries: string[],
|
||||
onSelect: (entry: string) => unknown,
|
||||
indent?: number,
|
||||
}
|
||||
export function TreeView({ entries, onSelect, indent }: Props) {
|
||||
const roots = useMemo(() => {
|
||||
const groups: Record<string, string[]> = {}
|
||||
for (const entry of entries) {
|
||||
const i = entry.indexOf(SEPARATOR)
|
||||
if (i >= 0) {
|
||||
const root = entry.slice(0, i)
|
||||
;(groups[root] ??= []).push(entry.slice(i + 1))
|
||||
}
|
||||
}
|
||||
return Object.entries(groups)
|
||||
}, entries)
|
||||
|
||||
const leaves = useMemo(() => {
|
||||
return entries.filter(e => !e.includes(SEPARATOR))
|
||||
}, entries)
|
||||
|
||||
const [hidden, setHidden] = useState(new Set<string>())
|
||||
const toggle = (root: string) => {
|
||||
if (hidden.has(root)) {
|
||||
hidden.delete(root)
|
||||
} else {
|
||||
hidden.add(root)
|
||||
}
|
||||
setHidden(new Set(hidden))
|
||||
}
|
||||
|
||||
return <div class="tree-view" style={`--indent: ${indent ?? 0};`}>
|
||||
{roots.map(([r, entries]) => <div>
|
||||
<TreeViewEntry icon={hidden.has(r) ? 'chevron_right' : 'chevron_down'} key={r} label={r} onClick={() => toggle(r)}/>
|
||||
{!hidden.has(r) &&
|
||||
<TreeView entries={entries} onSelect={e => onSelect(`${r}/${e}`)} indent={(indent ?? 0) + 1} />}
|
||||
</div>)}
|
||||
{leaves.map(e => <TreeViewEntry icon="file" key={e} label={e} onClick={() => onSelect(e)} />)}
|
||||
</div>
|
||||
}
|
||||
|
||||
interface TreeViewEntryProps {
|
||||
icon: keyof typeof Octicon,
|
||||
label: string,
|
||||
onClick?: () => unknown,
|
||||
}
|
||||
function TreeViewEntry({ icon, label, onClick }: TreeViewEntryProps) {
|
||||
return <div class="entry" onClick={onClick} >
|
||||
{Octicon[icon]}
|
||||
{label}
|
||||
</div>
|
||||
}
|
||||
24
src/app/components/forms/SearchList.tsx
Normal file
24
src/app/components/forms/SearchList.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useMemo, useState } from 'preact/hooks'
|
||||
import { Btn, BtnInput } from '..'
|
||||
|
||||
interface Props {
|
||||
values?: string[],
|
||||
onSelect?: (value: string) => unknown,
|
||||
searchPlaceholder?: string,
|
||||
noResults?: string,
|
||||
}
|
||||
export function SearchList({ values, onSelect, searchPlaceholder, noResults }: Props) {
|
||||
const [search, setSearch] = useState('')
|
||||
const results = useMemo(() => {
|
||||
const terms = search.trim().split(' ')
|
||||
return values?.filter(v => terms.every(t => v.includes(t))) ?? []
|
||||
}, [values, search])
|
||||
|
||||
return <>
|
||||
<BtnInput icon="search" large value={search} onChange={setSearch} doSelect={1} placeholder={searchPlaceholder ?? 'Search'} />
|
||||
<div class="result-list">
|
||||
{results.map(v => <Btn key={v} label={v} onClick={() => onSelect?.(v)} />)}
|
||||
{results.length === 0 && <Btn label={noResults ?? 'No results'}/>}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from './Input'
|
||||
export * from './SearchList'
|
||||
|
||||
@@ -8,14 +8,13 @@ import { BiomeSourcePreview, DecoratorPreview, NoisePreview, NoiseSettingsPrevie
|
||||
export const HasPreview = ['dimension', 'worldgen/noise', 'worldgen/noise_settings', 'worldgen/configured_feature']
|
||||
|
||||
type PreviewPanelProps = {
|
||||
lang: string,
|
||||
model: DataModel | null,
|
||||
version: VersionId,
|
||||
id: string,
|
||||
shown: boolean,
|
||||
onError: (message: string) => unknown,
|
||||
}
|
||||
export function PreviewPanel({ lang, model, version, id, shown }: PreviewPanelProps) {
|
||||
export function PreviewPanel({ model, version, id, shown }: PreviewPanelProps) {
|
||||
const [, setCount] = useState(0)
|
||||
|
||||
useModel(model, () => {
|
||||
@@ -24,22 +23,22 @@ export function PreviewPanel({ lang, model, version, id, shown }: PreviewPanelPr
|
||||
|
||||
if (id === 'dimension' && model?.get(new Path(['generator', 'type']))?.endsWith('noise')) {
|
||||
const data = model.get(new Path(['generator', 'biome_source']))
|
||||
if (data) return <BiomeSourcePreview {...{ lang, model, version, shown, data }} />
|
||||
if (data) return <BiomeSourcePreview {...{ model, version, shown, data }} />
|
||||
}
|
||||
|
||||
if (id === 'worldgen/noise' && model) {
|
||||
const data = model.get(new Path([]))
|
||||
if (data) return <NoisePreview {...{ lang, model, version, shown, data }} />
|
||||
if (data) return <NoisePreview {...{ model, version, shown, data }} />
|
||||
}
|
||||
|
||||
if (id === 'worldgen/noise_settings' && model) {
|
||||
const data = model.get(new Path([]))
|
||||
if (data) return <NoiseSettingsPreview {...{ lang, model, version, shown, data }} />
|
||||
if (data) return <NoiseSettingsPreview {...{ model, version, shown, data }} />
|
||||
}
|
||||
|
||||
if (id === 'worldgen/configured_feature' && model) {
|
||||
const data = model.get(new Path([]))
|
||||
if (data) return <DecoratorPreview {...{ lang, model, version, shown, data }} />
|
||||
if (data) return <DecoratorPreview {...{ model, version, shown, data }} />
|
||||
}
|
||||
|
||||
return <></>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { DataModel, ModelPath } from '@mcschema/core'
|
||||
import { DataModel } from '@mcschema/core'
|
||||
import json from 'comment-json'
|
||||
import yaml from 'js-yaml'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { Btn, BtnMenu } from '..'
|
||||
import { useLocale } from '../../contexts'
|
||||
import { useModel } from '../../hooks'
|
||||
import { locale } from '../../Locales'
|
||||
import { transformOutput } from '../../schema/transformOutput'
|
||||
import { getOutput } from '../../schema/transformOutput'
|
||||
import type { BlockStateRegistry } from '../../services'
|
||||
import { Store } from '../../Store'
|
||||
import { message } from '../../Utils'
|
||||
@@ -37,7 +37,6 @@ const FORMATS: Record<string, {
|
||||
}
|
||||
|
||||
type SourcePanelProps = {
|
||||
lang: string,
|
||||
name: string,
|
||||
model: DataModel | null,
|
||||
blockStates: BlockStateRegistry | null,
|
||||
@@ -47,16 +46,16 @@ type SourcePanelProps = {
|
||||
copySuccess: () => unknown,
|
||||
onError: (message: string) => unknown,
|
||||
}
|
||||
export function SourcePanel({ lang, name, model, blockStates, doCopy, doDownload, doImport, copySuccess, onError }: SourcePanelProps) {
|
||||
const loc = locale.bind(null, lang)
|
||||
export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doImport, copySuccess, onError }: SourcePanelProps) {
|
||||
const { locale } = useLocale()
|
||||
const [indent, setIndent] = useState(Store.getIndent())
|
||||
const [format, setFormat] = useState(Store.getFormat())
|
||||
const source = useRef<HTMLTextAreaElement>(null)
|
||||
const download = useRef<HTMLAnchorElement>(null)
|
||||
const retransform = useRef<Function>()
|
||||
|
||||
const getOutput = useCallback((model: DataModel, blockStates: BlockStateRegistry) => {
|
||||
const data = model.schema.hook(transformOutput, new ModelPath(model), model.data, { blockStates })
|
||||
const getSerializedOutput = useCallback((model: DataModel, blockStates: BlockStateRegistry) => {
|
||||
const data = getOutput(model, blockStates)
|
||||
return FORMATS[format].stringify(data, INDENT[indent])
|
||||
}, [indent, format])
|
||||
|
||||
@@ -64,7 +63,7 @@ export function SourcePanel({ lang, name, model, blockStates, doCopy, doDownload
|
||||
retransform.current = () => {
|
||||
if (!model || !blockStates) return
|
||||
try {
|
||||
const output = getOutput(model, blockStates)
|
||||
const output = getSerializedOutput(model, blockStates)
|
||||
if (output.length >= OUTPUT_CHARS_LIMIT) {
|
||||
source.current.value = output.slice(0, OUTPUT_CHARS_LIMIT) + `\n\nOutput is too large to display (+${OUTPUT_CHARS_LIMIT} chars)\nExport to view complete output\n\n`
|
||||
} else {
|
||||
@@ -102,7 +101,7 @@ export function SourcePanel({ lang, name, model, blockStates, doCopy, doDownload
|
||||
|
||||
useEffect(() => {
|
||||
if (doCopy && model && blockStates) {
|
||||
navigator.clipboard.writeText(getOutput(model, blockStates)).then(() => {
|
||||
navigator.clipboard.writeText(getSerializedOutput(model, blockStates)).then(() => {
|
||||
copySuccess()
|
||||
})
|
||||
}
|
||||
@@ -110,7 +109,7 @@ export function SourcePanel({ lang, name, model, blockStates, doCopy, doDownload
|
||||
|
||||
useEffect(() => {
|
||||
if (doDownload && model && blockStates && download.current) {
|
||||
const content = encodeURIComponent(getOutput(model, blockStates))
|
||||
const content = encodeURIComponent(getSerializedOutput(model, blockStates))
|
||||
download.current.setAttribute('href', `data:text/json;charset=utf-8,${content}`)
|
||||
download.current.setAttribute('download', `${name}.${format}`)
|
||||
download.current.click()
|
||||
@@ -136,18 +135,18 @@ export function SourcePanel({ lang, name, model, blockStates, doCopy, doDownload
|
||||
|
||||
return <>
|
||||
<div class="controls">
|
||||
<BtnMenu icon="gear" tooltip={loc('output_settings')} data-cy="source-controls">
|
||||
<BtnMenu icon="gear" tooltip={locale('output_settings')} data-cy="source-controls">
|
||||
{Object.entries(INDENT).map(([key]) =>
|
||||
<Btn label={loc(`indentation.${key}`)} active={indent === key}
|
||||
<Btn label={locale(`indentation.${key}`)} active={indent === key}
|
||||
onClick={() => changeIndent(key)}/>
|
||||
)}
|
||||
<hr />
|
||||
{Object.keys(FORMATS).map(key =>
|
||||
<Btn label={loc(`format.${key}`)} active={format === key}
|
||||
<Btn label={locale(`format.${key}`)} active={format === key}
|
||||
onClick={() => changeFormat(key)} />)}
|
||||
</BtnMenu>
|
||||
</div>
|
||||
<textarea ref={source} class="source" onBlur={onImport} spellcheck={false} autocorrect="off" placeholder={loc('source_placeholder')} data-cy="import-area"></textarea>
|
||||
<textarea ref={source} class="source" onBlur={onImport} spellcheck={false} autocorrect="off" placeholder={locale('source_placeholder', format.toUpperCase())} data-cy="import-area"></textarea>
|
||||
<a ref={download} style="display: none;"></a>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import type { DataModel } from '@mcschema/core'
|
||||
import { useErrorBoundary, useState } from 'preact/hooks'
|
||||
import { useLocale } from '../../contexts'
|
||||
import { useModel } from '../../hooks'
|
||||
import { FullNode } from '../../schema/renderHtml'
|
||||
import type { BlockStateRegistry, VersionId } from '../../services'
|
||||
|
||||
type TreePanelProps = {
|
||||
lang: string,
|
||||
version: VersionId,
|
||||
model: DataModel | null,
|
||||
blockStates: BlockStateRegistry | null,
|
||||
onError: (message: string) => unknown,
|
||||
}
|
||||
export function Tree({ lang, version, model, blockStates, onError }: TreePanelProps) {
|
||||
export function Tree({ version, model, blockStates, onError }: TreePanelProps) {
|
||||
const { lang } = useLocale()
|
||||
if (!model || !blockStates || lang === 'none') return <></>
|
||||
|
||||
const [error] = useErrorBoundary(e => {
|
||||
|
||||
@@ -11,3 +11,4 @@ export * from './Octicon'
|
||||
export * from './previews'
|
||||
export * from './sounds'
|
||||
export * from './ToolCard'
|
||||
export * from './TreeView'
|
||||
|
||||
@@ -4,8 +4,8 @@ import { NoiseGeneratorSettings, TerrainShaper } from 'deepslate'
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import type { PreviewProps } from '.'
|
||||
import { Btn, BtnMenu } from '..'
|
||||
import { useLocale } from '../../contexts'
|
||||
import { useCanvas } from '../../hooks'
|
||||
import { locale } from '../../Locales'
|
||||
import { biomeMap, getBiome } from '../../previews'
|
||||
import { newSeed } from '../../Utils'
|
||||
|
||||
@@ -13,7 +13,8 @@ const LAYERS = ['biomes', 'temperature', 'humidity', 'continentalness', 'erosion
|
||||
|
||||
const OverworldShaper = TerrainShaper.overworld()
|
||||
|
||||
export const BiomeSourcePreview = ({ model, data, shown, lang, version }: PreviewProps) => {
|
||||
export const BiomeSourcePreview = ({ model, data, shown, version }: PreviewProps) => {
|
||||
const { locale } = useLocale()
|
||||
const [scale, setScale] = useState(2)
|
||||
const [focused, setFocused] = useState<string | undefined>(undefined)
|
||||
const [layers, setLayers] = useState(new Set<typeof LAYERS[number]>(['biomes']))
|
||||
@@ -76,12 +77,12 @@ export const BiomeSourcePreview = ({ model, data, shown, lang, version }: Previe
|
||||
<div class="controls">
|
||||
{focused && <Btn label={focused} class="no-pointer" />}
|
||||
{type === 'multi_noise' &&
|
||||
<BtnMenu icon="stack" tooltip={locale(lang, 'configure_layers')}>
|
||||
<BtnMenu icon="stack" tooltip={locale('configure_layers')}>
|
||||
{LAYERS.map(name => {
|
||||
const enabled = layers.has(name)
|
||||
return <Btn label={locale(lang, `layer.${name}`)}
|
||||
return <Btn label={locale(`layer.${name}`)}
|
||||
active={enabled}
|
||||
tooltip={enabled ? locale(lang, 'enabled') : locale(lang, 'disabled')}
|
||||
tooltip={enabled ? locale('enabled') : locale('disabled')}
|
||||
onClick={(e) => {
|
||||
setLayers(new Set([name]))
|
||||
e.stopPropagation()
|
||||
@@ -89,13 +90,13 @@ export const BiomeSourcePreview = ({ model, data, shown, lang, version }: Previe
|
||||
})}
|
||||
</BtnMenu>}
|
||||
{(type === 'multi_noise' || type === 'checkerboard') && <>
|
||||
<Btn icon="dash" tooltip={locale(lang, 'zoom_out')}
|
||||
<Btn icon="dash" tooltip={locale('zoom_out')}
|
||||
onClick={() => changeScale(scale * 1.5)} />
|
||||
<Btn icon="plus" tooltip={locale(lang, 'zoom_in')}
|
||||
<Btn icon="plus" tooltip={locale('zoom_in')}
|
||||
onClick={() => changeScale(scale / 1.5)} />
|
||||
</>}
|
||||
{type === 'multi_noise' &&
|
||||
<Btn icon="sync" tooltip={locale(lang, 'generate_new_seed')}
|
||||
<Btn icon="sync" tooltip={locale('generate_new_seed')}
|
||||
onClick={() => newSeed(model)} />}
|
||||
</div>
|
||||
<canvas ref={canvas} width="200" height="200"></canvas>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import type { PreviewProps } from '.'
|
||||
import { Btn } from '..'
|
||||
import { useLocale } from '../../contexts'
|
||||
import { useCanvas } from '../../hooks'
|
||||
import { locale } from '../../Locales'
|
||||
import { decorator } from '../../previews'
|
||||
import { randomSeed } from '../../Utils'
|
||||
|
||||
export const DecoratorPreview = ({ data, version, shown, lang }: PreviewProps) => {
|
||||
export const DecoratorPreview = ({ data, version, shown }: PreviewProps) => {
|
||||
const { locale } = useLocale()
|
||||
const [scale, setScale] = useState(4)
|
||||
const [seed, setSeed] = useState(randomSeed())
|
||||
|
||||
@@ -28,11 +29,11 @@ export const DecoratorPreview = ({ data, version, shown, lang }: PreviewProps) =
|
||||
|
||||
return <>
|
||||
<div class="controls">
|
||||
<Btn icon="dash" tooltip={locale(lang, 'zoom_out')}
|
||||
<Btn icon="dash" tooltip={locale('zoom_out')}
|
||||
onClick={() => setScale(Math.min(16, scale + 1))} />
|
||||
<Btn icon="plus" tooltip={locale(lang, 'zoom_in')}
|
||||
<Btn icon="plus" tooltip={locale('zoom_in')}
|
||||
onClick={() => setScale(Math.max(1, scale - 1))} />
|
||||
<Btn icon="sync" tooltip={locale(lang, 'generate_new_seed')}
|
||||
<Btn icon="sync" tooltip={locale('generate_new_seed')}
|
||||
onClick={() => setSeed(randomSeed())} />
|
||||
</div>
|
||||
<canvas ref={canvas} width="64" height="64"></canvas>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import type { PreviewProps } from '.'
|
||||
import { Btn } from '..'
|
||||
import { useLocale } from '../../contexts'
|
||||
import { useCanvas } from '../../hooks'
|
||||
import { locale } from '../../Locales'
|
||||
import { normalNoise } from '../../previews'
|
||||
import { randomSeed } from '../../Utils'
|
||||
|
||||
export const NoisePreview = ({ lang, data, shown, version }: PreviewProps) => {
|
||||
export const NoisePreview = ({ data, shown, version }: PreviewProps) => {
|
||||
const { locale } = useLocale()
|
||||
const [seed, setSeed] = useState(randomSeed())
|
||||
const [scale, setScale] = useState(2)
|
||||
const offset = useRef<[number, number]>([0, 0])
|
||||
@@ -41,11 +42,11 @@ export const NoisePreview = ({ lang, data, shown, version }: PreviewProps) => {
|
||||
|
||||
return <>
|
||||
<div class="controls">
|
||||
<Btn icon="dash" tooltip={locale(lang, 'zoom_out')}
|
||||
<Btn icon="dash" tooltip={locale('zoom_out')}
|
||||
onClick={() => changeScale(scale * 1.5)} />
|
||||
<Btn icon="plus" tooltip={locale(lang, 'zoom_in')}
|
||||
<Btn icon="plus" tooltip={locale('zoom_in')}
|
||||
onClick={() => changeScale(scale / 1.5)} />
|
||||
<Btn icon="sync" tooltip={locale(lang, 'generate_new_seed')}
|
||||
<Btn icon="sync" tooltip={locale('generate_new_seed')}
|
||||
onClick={() => setSeed(randomSeed())} />
|
||||
</div>
|
||||
<canvas ref={canvas} width="256" height="256"></canvas>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import type { PreviewProps } from '.'
|
||||
import { Btn, BtnInput, BtnMenu } from '..'
|
||||
import { useLocale } from '../../contexts'
|
||||
import { useCanvas } from '../../hooks'
|
||||
import { locale } from '../../Locales'
|
||||
import { noiseSettings } from '../../previews'
|
||||
import { checkVersion } from '../../services'
|
||||
import { randomSeed } from '../../Utils'
|
||||
|
||||
export const NoiseSettingsPreview = ({ lang, data, shown, version }: PreviewProps) => {
|
||||
const loc = locale.bind(null, lang)
|
||||
export const NoiseSettingsPreview = ({ data, shown, version }: PreviewProps) => {
|
||||
const { locale } = useLocale()
|
||||
const [seed, setSeed] = useState(randomSeed())
|
||||
const [biomeScale, setBiomeScale] = useState(0.2)
|
||||
const [biomeDepth, setBiomeDepth] = useState(0.1)
|
||||
@@ -48,12 +48,12 @@ export const NoiseSettingsPreview = ({ lang, data, shown, version }: PreviewProp
|
||||
<div class="controls">
|
||||
{focused && <Btn label={`Y = ${focused}`} class="no-pointer" />}
|
||||
{checkVersion(version, undefined, '1.17') &&
|
||||
<BtnMenu icon="gear" tooltip={locale(lang, 'terrain_settings')}>
|
||||
<BtnInput label={loc('preview.scale')} value={`${biomeScale}`} onChange={v => setBiomeScale(Number(v))} />
|
||||
<BtnInput label={loc('preview.depth')} value={`${biomeDepth}`} onChange={v => setBiomeDepth(Number(v))} />
|
||||
<BtnMenu icon="gear" tooltip={locale('terrain_settings')}>
|
||||
<BtnInput label={locale('preview.scale')} value={`${biomeScale}`} onChange={v => setBiomeScale(Number(v))} />
|
||||
<BtnInput label={locale('preview.depth')} value={`${biomeDepth}`} onChange={v => setBiomeDepth(Number(v))} />
|
||||
</BtnMenu>
|
||||
}
|
||||
<Btn icon="sync" tooltip={locale(lang, 'generate_new_seed')}
|
||||
<Btn icon="sync" tooltip={locale('generate_new_seed')}
|
||||
onClick={() => setSeed(randomSeed())} />
|
||||
</div>
|
||||
<canvas ref={canvas} width={size} height={size}></canvas>
|
||||
|
||||
@@ -7,7 +7,6 @@ export * from './NoisePreview'
|
||||
export * from './NoiseSettingsPreview'
|
||||
|
||||
export type PreviewProps = {
|
||||
lang: string,
|
||||
model: DataModel,
|
||||
data: any,
|
||||
shown: boolean,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Howl } from 'howler'
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { Btn, NumberInput, RangeInput, TextInput } from '..'
|
||||
import { locale } from '../../Locales'
|
||||
import { useLocale } from '../../contexts'
|
||||
import type { SoundEvents, VersionAssets } from '../../services'
|
||||
import { getResourceUrl } from '../../services'
|
||||
|
||||
@@ -13,15 +13,14 @@ export interface SoundConfig {
|
||||
volume: number,
|
||||
}
|
||||
type SoundConfigProps = SoundConfig & {
|
||||
lang: string,
|
||||
assets: VersionAssets,
|
||||
sounds: SoundEvents,
|
||||
onEdit: (changes: Partial<SoundConfig>) => unknown,
|
||||
onDelete: () => unknown,
|
||||
delayedPlay?: number,
|
||||
}
|
||||
export function SoundConfig({ lang, assets, sounds, sound, delay, pitch, volume, onEdit, onDelete, delayedPlay }: SoundConfigProps) {
|
||||
const loc = locale.bind(null, lang)
|
||||
export function SoundConfig({ assets, sounds, sound, delay, pitch, volume, onEdit, onDelete, delayedPlay }: SoundConfigProps) {
|
||||
const { locale } = useLocale()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [playing, setPlaying] = useState(false)
|
||||
const [invalid, setInvalid] = useState(false)
|
||||
@@ -100,23 +99,23 @@ export function SoundConfig({ lang, assets, sounds, sound, delay, pitch, volume,
|
||||
}
|
||||
|
||||
return <div class={`sound-config${loading ? ' loading' : playing ? ' playing' : ''}${invalid ? ' invalid' : ''}`}>
|
||||
<Btn class="play" icon={invalid ? 'alert' : loading ? 'sync' : 'play'} label={loc('sounds.play')} onClick={play} tooltip={invalid ? loc('sounds.unknown_sound') : loading ? loc('sounds.loading_sound') : loc('sounds.play_sound')} tooltipLoc="se" />
|
||||
<Btn class="play" icon={invalid ? 'alert' : loading ? 'sync' : 'play'} label={locale('sounds.play')} onClick={play} tooltip={invalid ? locale('sounds.unknown_sound') : loading ? locale('sounds.loading_sound') : locale('sounds.play_sound')} tooltipLoc="se" />
|
||||
<TextInput class="btn btn-input sound" list="sound-list" spellcheck={false}
|
||||
value={sound} onChange={sound => onEdit({ sound })} />
|
||||
<label class="delay-label">{loc('sounds.delay')}: </label>
|
||||
<label class="delay-label">{locale('sounds.delay')}: </label>
|
||||
<NumberInput class="btn btn-input delay" min={0}
|
||||
value={delay} onChange={delay => onEdit({ delay })} />
|
||||
<label class="pitch-label">{loc('sounds.pitch')}: </label>
|
||||
<label class="pitch-label">{locale('sounds.pitch')}: </label>
|
||||
<RangeInput class="pitch tooltipped tip-s" min={0.5} max={2} step={0.01}
|
||||
aria-label={pitch.toFixed(2)} style={`--x: ${(pitch - 0.5) * (100 / 1.5)}%`}
|
||||
value={pitch} onChange={pitch => onEdit({ pitch })} />
|
||||
<label class="volume-label">{loc('sounds.volume')}: </label>
|
||||
<label class="volume-label">{locale('sounds.volume')}: </label>
|
||||
<RangeInput class="volume tooltipped tip-s" min={0} max={1} step={0.01}
|
||||
aria-label={volume.toFixed(2)} style={`--x: ${volume * 100}%`}
|
||||
value={volume} onChange={volume => onEdit({ volume })} />
|
||||
<Btn class={`copy${copyActive ? ' active' : ''}`} icon={copyActive ? 'check' : 'terminal'} label={loc('copy')} tooltip={copyActive ? loc('copied') : loc('sounds.copy_command')}
|
||||
<Btn class={`copy${copyActive ? ' active' : ''}`} icon={copyActive ? 'check' : 'terminal'} label={locale('copy')} tooltip={copyActive ? locale('copied') : locale('sounds.copy_command')}
|
||||
onClick={copy} />
|
||||
<Btn class="remove" icon="trashcan" tooltip={loc('sounds.remove_sound')}
|
||||
<Btn class="remove" icon="trashcan" tooltip={locale('sounds.remove_sound')}
|
||||
onClick={() => {onDelete(); stop()}} />
|
||||
</div>
|
||||
}
|
||||
|
||||
89
src/app/contexts/Locale.tsx
Normal file
89
src/app/contexts/Locale.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import type { ComponentChildren } from 'preact'
|
||||
import { createContext } from 'preact'
|
||||
import { useCallback, useContext, useEffect, useState } from 'preact/hooks'
|
||||
import config from '../../config.json'
|
||||
import English from '../../locales/en.json'
|
||||
import { Analytics } from '../Analytics'
|
||||
import { Store } from '../Store'
|
||||
|
||||
interface Locale {
|
||||
lang: string,
|
||||
locale: (key: string, ...params: string[]) => string,
|
||||
changeLanguage: (lang: string) => unknown,
|
||||
}
|
||||
const Locale = createContext<Locale>({
|
||||
lang: 'none',
|
||||
locale: key => key,
|
||||
changeLanguage: () => {},
|
||||
})
|
||||
|
||||
export const Locales: {
|
||||
[key: string]: {
|
||||
[key: string]: string,
|
||||
},
|
||||
} = {
|
||||
fallback: English,
|
||||
}
|
||||
|
||||
export function localize(lang: string, key: string, ...params: string[]) {
|
||||
const value: string | undefined = Locales[lang]?.[key]
|
||||
?? Locales.en?.[key] ?? Locales.fallback[key] ?? key
|
||||
return resolveLocaleParams(value, params)
|
||||
}
|
||||
|
||||
function resolveLocaleParams(value: string, params?: string[]): string {
|
||||
return value.replace(/%\d+%/g, match => {
|
||||
const index = parseInt(match.slice(1, -1))
|
||||
return params?.[index] !== undefined ? params[index] : match
|
||||
})
|
||||
}
|
||||
|
||||
async function loadLocale(language: string) {
|
||||
if (Locales[language]) return
|
||||
const langConfig = config.languages.find(lang => lang.code === language)
|
||||
if (!langConfig) return
|
||||
const data = await import(`../../locales/${language}.json`)
|
||||
const schema = langConfig.schemas !== false
|
||||
&& await import(`../../../node_modules/@mcschema/locales/src/${language}.json`)
|
||||
Locales[language] = { ...data.default, ...schema.default }
|
||||
}
|
||||
|
||||
export function useLocale() {
|
||||
return useContext(Locale)
|
||||
}
|
||||
|
||||
export function LocaleProvider({ children }: { children: ComponentChildren }) {
|
||||
const [lang, setLanguage] = useState('none')
|
||||
|
||||
const locale = useCallback((key: string, ...params: string[]) => {
|
||||
return localize(lang, key, ...params)
|
||||
}, [lang])
|
||||
|
||||
const changeLanguage = useCallback(async (lang: string) => {
|
||||
await loadLocale(lang)
|
||||
Analytics.setLanguage(lang)
|
||||
Store.setLanguage(lang)
|
||||
setLanguage(lang)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const target = Store.getLanguage()
|
||||
await Promise.all([
|
||||
loadLocale('en'),
|
||||
...(target !== 'en' ? [loadLocale(target)] : []),
|
||||
])
|
||||
setLanguage(target)
|
||||
})()
|
||||
}, [])
|
||||
|
||||
const value: Locale = {
|
||||
lang,
|
||||
locale: locale,
|
||||
changeLanguage,
|
||||
}
|
||||
|
||||
return <Locale.Provider value={value}>
|
||||
{children}
|
||||
</Locale.Provider>
|
||||
}
|
||||
129
src/app/contexts/Project.tsx
Normal file
129
src/app/contexts/Project.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import type { ComponentChildren } from 'preact'
|
||||
import { createContext } from 'preact'
|
||||
import { route } from 'preact-router'
|
||||
import { useCallback, useContext, useMemo, useState } from 'preact/hooks'
|
||||
import config from '../../config.json'
|
||||
import type { VersionId } from '../services'
|
||||
import { Store } from '../Store'
|
||||
import { cleanUrl } from '../Utils'
|
||||
|
||||
export type Project = {
|
||||
name: string,
|
||||
namespace: string,
|
||||
version?: VersionId,
|
||||
files: ProjectFile[],
|
||||
}
|
||||
export const DRAFT_PROJECT: Project = {
|
||||
name: 'Drafts',
|
||||
namespace: 'draft',
|
||||
files: [],
|
||||
}
|
||||
|
||||
export type ProjectFile = {
|
||||
type: string,
|
||||
id: string,
|
||||
data: any,
|
||||
}
|
||||
|
||||
interface ProjectContext {
|
||||
project: Project,
|
||||
file?: ProjectFile,
|
||||
changeProject: (name: string) => unknown,
|
||||
updateProject: (project: Partial<Project>) => unknown,
|
||||
updateFile: (type: string, id: string | undefined, file: Partial<ProjectFile>) => boolean,
|
||||
openFile: (type: string, id: string) => unknown,
|
||||
closeFile: () => unknown,
|
||||
}
|
||||
const Project = createContext<ProjectContext>({
|
||||
project: DRAFT_PROJECT,
|
||||
changeProject: () => {},
|
||||
updateProject: () => {},
|
||||
updateFile: () => false,
|
||||
openFile: () => {},
|
||||
closeFile: () => {},
|
||||
})
|
||||
|
||||
export function useProject() {
|
||||
return useContext(Project)
|
||||
}
|
||||
|
||||
export function ProjectProvider({ children }: { children: ComponentChildren }) {
|
||||
const [projects, setProjects] = useState<Project[]>(Store.getProjects())
|
||||
|
||||
const [projectName, setProjectName] = useState<string>(DRAFT_PROJECT.name)
|
||||
const project = useMemo(() => {
|
||||
return projects.find(p => p.name === projectName) ?? DRAFT_PROJECT
|
||||
}, [projects, projectName])
|
||||
|
||||
const [fileId, setFileId] = useState<[string, string] | undefined>(undefined)
|
||||
const file = useMemo(() => {
|
||||
if (!fileId) return undefined
|
||||
return project.files.find(f => f.type === fileId[0] && f.id === fileId[1])
|
||||
}, [project, fileId])
|
||||
|
||||
const changeProjects = useCallback((projects: Project[]) => {
|
||||
Store.setProjects(projects)
|
||||
setProjects(projects)
|
||||
}, [])
|
||||
|
||||
const updateProject = useCallback((edits: Partial<Project>) => {
|
||||
changeProjects(projects.map(p => p.name === projectName ? { ...p, ...edits } : p))
|
||||
}, [projects, projectName])
|
||||
|
||||
const updateFile = useCallback((type: string, id: string | undefined, edits: Partial<ProjectFile>) => {
|
||||
if (!edits.id) { // remove
|
||||
updateProject({ files: project.files.filter(f => f.type !== type || f.id !== id) })
|
||||
} else {
|
||||
const newId = edits.id.includes(':') ? edits.id : `${project.namespace}:${edits.id}`
|
||||
const exists = project.files.some(f => f.type === type && f.id === newId)
|
||||
if (!id) { // create
|
||||
if (exists) return false
|
||||
updateProject({ files: [...project.files, { type, id: newId, data: edits.data ?? {} } ]})
|
||||
setFileId([type, newId])
|
||||
} else { // rename or update data
|
||||
if (file?.id === id && id !== newId && exists) {
|
||||
return false
|
||||
}
|
||||
updateProject({ files: project.files.map(f => f.type === type && f.id === id ? { ...f, ...edits, id: newId } : f)})
|
||||
if (file?.id === id) setFileId([type, newId])
|
||||
}
|
||||
}
|
||||
return true
|
||||
}, [updateProject, project, file])
|
||||
|
||||
const openFile = useCallback((type: string, id: string) => {
|
||||
const gen = config.generators.find(g => g.id === type || g.path === type)
|
||||
if (!gen) {
|
||||
throw new Error(`Cannot find generator of type ${type}`)
|
||||
}
|
||||
setFileId([gen.id, id])
|
||||
route(cleanUrl(gen.url))
|
||||
}, [])
|
||||
|
||||
const closeFile = useCallback(() => {
|
||||
setFileId(undefined)
|
||||
}, [])
|
||||
|
||||
const value: ProjectContext = {
|
||||
project,
|
||||
file,
|
||||
changeProject: setProjectName,
|
||||
updateProject,
|
||||
updateFile,
|
||||
openFile,
|
||||
closeFile,
|
||||
}
|
||||
|
||||
return <Project.Provider value={value}>
|
||||
{children}
|
||||
</Project.Provider>
|
||||
}
|
||||
|
||||
export function getFilePath(file: ProjectFile) {
|
||||
const [namespace, id] = file.id.includes(':') ? file.id.split(':') : ['minecraft', file.id]
|
||||
const gen = config.generators.find(g => g.id === file.type)
|
||||
if (!gen) {
|
||||
throw new Error(`Cannot find generator of type ${file.type}`)
|
||||
}
|
||||
return `data/${namespace}/${gen.path ?? gen.id}/${id}`
|
||||
}
|
||||
41
src/app/contexts/Theme.tsx
Normal file
41
src/app/contexts/Theme.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { ComponentChildren } from 'preact'
|
||||
import { createContext } from 'preact'
|
||||
import { useCallback, useContext, useEffect, useState } from 'preact/hooks'
|
||||
import { Analytics } from '../Analytics'
|
||||
import { Store } from '../Store'
|
||||
|
||||
interface Theme {
|
||||
theme: string,
|
||||
changeTheme: (theme: string) => unknown,
|
||||
}
|
||||
const Theme = createContext<Theme>({
|
||||
theme: 'dark',
|
||||
changeTheme: () => {},
|
||||
})
|
||||
|
||||
export function useTheme() {
|
||||
return useContext(Theme)
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children }: { children: ComponentChildren }) {
|
||||
const [theme, setTheme] = useState(Store.getTheme())
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute('data-theme', theme)
|
||||
}, [theme])
|
||||
|
||||
const changeTheme = useCallback((theme: string) => {
|
||||
Analytics.setTheme(theme)
|
||||
Store.setTheme(theme)
|
||||
setTheme(theme)
|
||||
}, [])
|
||||
|
||||
const value: Theme = {
|
||||
theme,
|
||||
changeTheme,
|
||||
}
|
||||
|
||||
return <Theme.Provider value={value}>
|
||||
{children}
|
||||
</Theme.Provider>
|
||||
}
|
||||
48
src/app/contexts/Title.tsx
Normal file
48
src/app/contexts/Title.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { ComponentChildren } from 'preact'
|
||||
import { createContext } from 'preact'
|
||||
import { useCallback, useContext, useEffect, useState } from 'preact/hooks'
|
||||
import { useLocale } from '.'
|
||||
import config from '../../config.json'
|
||||
import type { VersionId } from '../services'
|
||||
|
||||
const VERSIONS_IN_TITLE = 3
|
||||
|
||||
interface Title {
|
||||
title: string,
|
||||
changeTitle: (title: string, versions?: VersionId[]) => unknown,
|
||||
}
|
||||
const Title = createContext<Title>({
|
||||
title: '',
|
||||
changeTitle: () => {},
|
||||
})
|
||||
|
||||
export function useTitle(title?: string, versions?: VersionId[]) {
|
||||
const context = useContext(Title)
|
||||
useEffect(() => {
|
||||
if (title) {
|
||||
context.changeTitle(title, versions)
|
||||
}
|
||||
}, [title, versions])
|
||||
return context
|
||||
}
|
||||
|
||||
export function TitleProvider({ children }: { children: ComponentChildren }) {
|
||||
const { locale } = useLocale()
|
||||
const [title, setTitle] = useState<string>(locale('title.home'))
|
||||
|
||||
const changeTitle = useCallback((title: string, versions?: VersionId[]) => {
|
||||
versions ??= config.versions.map(v => v.id as VersionId)
|
||||
const titleVersions = versions.slice(versions.length - VERSIONS_IN_TITLE)
|
||||
document.title = `${title} Minecraft ${titleVersions.join(', ')}`
|
||||
setTitle(title)
|
||||
}, [])
|
||||
|
||||
const value = {
|
||||
title,
|
||||
changeTitle,
|
||||
}
|
||||
|
||||
return <Title.Provider value={value}>
|
||||
{children}
|
||||
</Title.Provider>
|
||||
}
|
||||
54
src/app/contexts/Version.tsx
Normal file
54
src/app/contexts/Version.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { ComponentChildren } from 'preact'
|
||||
import { createContext } from 'preact'
|
||||
import { getCurrentUrl } from 'preact-router'
|
||||
import { useCallback, useContext, useEffect, useState } from 'preact/hooks'
|
||||
import { Analytics } from '../Analytics'
|
||||
import type { VersionId } from '../services'
|
||||
import { VersionIds } from '../services'
|
||||
import { Store } from '../Store'
|
||||
import { getSearchParams, setSeachParams } from '../Utils'
|
||||
|
||||
const VERSION_PARAM = 'version'
|
||||
|
||||
interface Version {
|
||||
version: VersionId,
|
||||
changeVersion: (version: VersionId) => unknown,
|
||||
}
|
||||
const Version = createContext<Version>({
|
||||
version: '1.18',
|
||||
changeVersion: () => {},
|
||||
})
|
||||
|
||||
export function useVersion() {
|
||||
return useContext(Version)
|
||||
}
|
||||
|
||||
export function VersionProvider({ children }: { children: ComponentChildren }) {
|
||||
const [version, setVersion] = useState<VersionId>(Store.getVersion())
|
||||
|
||||
const searchParams = getSearchParams(getCurrentUrl())
|
||||
const targetVersion = searchParams.get(VERSION_PARAM)
|
||||
useEffect(() => {
|
||||
if (VersionIds.includes(targetVersion as VersionId) && version !== targetVersion) {
|
||||
setVersion(targetVersion as VersionId)
|
||||
}
|
||||
}, [version, targetVersion])
|
||||
|
||||
const changeVersion = useCallback((version: VersionId) => {
|
||||
if (getSearchParams(getCurrentUrl()).has(VERSION_PARAM)) {
|
||||
setSeachParams({ version })
|
||||
}
|
||||
Analytics.setVersion(version)
|
||||
Store.setVersion(version)
|
||||
setVersion(version)
|
||||
}, [])
|
||||
|
||||
const value: Version = {
|
||||
version,
|
||||
changeVersion,
|
||||
}
|
||||
|
||||
return <Version.Provider value={value}>
|
||||
{children}
|
||||
</Version.Provider>
|
||||
}
|
||||
5
src/app/contexts/index.ts
Normal file
5
src/app/contexts/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './Locale'
|
||||
export * from './Project'
|
||||
export * from './Theme'
|
||||
export * from './Title'
|
||||
export * from './Version'
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './useActiveTimout'
|
||||
export * from './useCanvas'
|
||||
export * from './useFocus'
|
||||
export * from './useModel'
|
||||
|
||||
21
src/app/hooks/useActiveTimout.ts
Normal file
21
src/app/hooks/useActiveTimout.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useRef, useState } from 'preact/hooks'
|
||||
|
||||
interface ActiveTimeoutOptions {
|
||||
cooldown?: number,
|
||||
invert?: boolean,
|
||||
initial?: boolean,
|
||||
}
|
||||
export function useActiveTimeout({ cooldown, invert, initial }: ActiveTimeoutOptions = {}): [boolean | undefined, () => unknown] {
|
||||
const [active, setActive] = useState(initial)
|
||||
const timeout = useRef<number | undefined>(undefined)
|
||||
|
||||
const trigger = () => {
|
||||
setActive(invert ? false : true)
|
||||
if (timeout.current !== undefined) clearTimeout(timeout.current)
|
||||
timeout.current = setTimeout(() => {
|
||||
setActive(invert ? true : false)
|
||||
}, cooldown ?? 2000) as any
|
||||
}
|
||||
|
||||
return [active, trigger]
|
||||
}
|
||||
@@ -1,21 +1,19 @@
|
||||
import config from '../../config.json'
|
||||
import { ToolCard } from '../components'
|
||||
import { locale } from '../Locales'
|
||||
import { useLocale, useTitle } from '../contexts'
|
||||
import { cleanUrl } from '../Utils'
|
||||
|
||||
type WorldgenProps = {
|
||||
interface Props {
|
||||
category: string,
|
||||
lang: string,
|
||||
changeTitle: (title: string) => unknown,
|
||||
path?: string,
|
||||
}
|
||||
export function Category({ category, lang, changeTitle }: WorldgenProps) {
|
||||
const loc = locale.bind(null, lang)
|
||||
changeTitle(loc('title.generator_category', loc(category)))
|
||||
export function Category({ category }: Props) {
|
||||
const { locale } = useLocale()
|
||||
useTitle(locale('title.generator_category', locale(category)))
|
||||
return <main>
|
||||
<div class="category">
|
||||
{config.generators.filter(g => g.category === category).map(g =>
|
||||
<ToolCard title={loc(g.id)} link={cleanUrl(g.url)} />
|
||||
<ToolCard title={locale(g.id)} link={cleanUrl(g.url)} />
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import { marked } from 'marked'
|
||||
import { useEffect, useMemo, useState } from 'preact/hooks'
|
||||
import { Ad, Btn, ErrorPanel, Octicon, TextInput } from '../components'
|
||||
import { locale } from '../Locales'
|
||||
import type { ChangelogEntry, ChangelogVersion, VersionId } from '../services'
|
||||
import { useLocale, useTitle } from '../contexts'
|
||||
import type { ChangelogEntry, ChangelogVersion } from '../services'
|
||||
import { getChangelogs } from '../services'
|
||||
import { hashString } from '../Utils'
|
||||
|
||||
type ChangelogProps = {
|
||||
interface Props {
|
||||
path?: string,
|
||||
lang: string,
|
||||
changeTitle: (title: string, versions?: VersionId[]) => unknown,
|
||||
}
|
||||
export function Changelog({ lang, changeTitle }: ChangelogProps) {
|
||||
const loc = locale.bind(null, lang)
|
||||
export function Changelog({}: Props) {
|
||||
const { locale } = useLocale()
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
changeTitle(loc('title.changelog'))
|
||||
useTitle(locale('title.changelog'))
|
||||
|
||||
const [changelogs, setChangelogs] = useState<ChangelogEntry[]>([])
|
||||
useEffect(() => {
|
||||
@@ -61,7 +59,7 @@ export function Changelog({ lang, changeTitle }: ChangelogProps) {
|
||||
{error && <ErrorPanel error={error} onDismiss={() => setError(null)} />}
|
||||
<div class="changelog-controls">
|
||||
<div class="changelog-query">
|
||||
<TextInput class="btn btn-input changelog-search" list="sound-list" placeholder={loc('changelog.search')}
|
||||
<TextInput class="btn btn-input changelog-search" list="sound-list" placeholder={locale('changelog.search')}
|
||||
value={search} onChange={setSearch} />
|
||||
<Btn icon={sort ? 'sort_desc' : 'sort_asc'} label={sort ? 'Newest first' : 'Oldest first'} onClick={() => setSort(!sort)} />
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import { DataModel, Path } from '@mcschema/core'
|
||||
import { getCurrentUrl } from 'preact-router'
|
||||
import { useEffect, useErrorBoundary, useRef, useState } from 'preact/hooks'
|
||||
import { getCurrentUrl, route } from 'preact-router'
|
||||
import { useEffect, useErrorBoundary, useState } from 'preact/hooks'
|
||||
import config from '../../config.json'
|
||||
import { Analytics } from '../Analytics'
|
||||
import { Ad, Btn, BtnInput, BtnMenu, ErrorPanel, HasPreview, Octicon, PreviewPanel, SourcePanel, Tree } from '../components'
|
||||
import { useModel } from '../hooks'
|
||||
import { locale } from '../Locales'
|
||||
import { Ad, Btn, BtnMenu, ErrorPanel, HasPreview, Octicon, PreviewPanel, SearchList, SourcePanel, TextInput, Tree } from '../components'
|
||||
import { useLocale, useProject, useTitle, useVersion } from '../contexts'
|
||||
import { useActiveTimeout, useModel } from '../hooks'
|
||||
import { getOutput } from '../schema/transformOutput'
|
||||
import type { BlockStateRegistry, VersionId } from '../services'
|
||||
import { checkVersion, fetchPreset, getBlockStates, getCollections, getModel } from '../services'
|
||||
import { getGenerator, getSearchParams, message, setSeachParams } from '../Utils'
|
||||
|
||||
type GeneratorProps = {
|
||||
lang: string,
|
||||
changeTitle: (title: string, versions?: VersionId[]) => unknown,
|
||||
version: VersionId,
|
||||
changeVersion: (version: VersionId) => unknown,
|
||||
interface Props {
|
||||
default?: true,
|
||||
}
|
||||
export function Generator({ lang, changeTitle, version, changeVersion }: GeneratorProps) {
|
||||
const loc = locale.bind(null, lang)
|
||||
export function Generator({}: Props) {
|
||||
const { locale } = useLocale()
|
||||
const { version, changeVersion } = useVersion()
|
||||
const { project, file, updateFile, openFile, closeFile } = useProject()
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [errorBoundary, errorRetry] = useErrorBoundary()
|
||||
if (errorBoundary) {
|
||||
@@ -34,7 +33,7 @@ export function Generator({ lang, changeTitle, version, changeVersion }: Generat
|
||||
.filter(v => checkVersion(v.id, gen.minVersion, gen.maxVersion))
|
||||
.map(v => v.id as VersionId)
|
||||
|
||||
changeTitle(loc('title.generator', loc(gen.id)), allowedVersions)
|
||||
useTitle(locale('title.generator', locale(gen.id)), allowedVersions)
|
||||
|
||||
if (!checkVersion(version, gen.minVersion)) {
|
||||
setError(`The minimum version for this generator is ${gen.minVersion}`)
|
||||
@@ -70,11 +69,58 @@ export function Generator({ lang, changeTitle, version, changeVersion }: Generat
|
||||
.catch(e => { console.error(e); setError(message(e)) })
|
||||
}, [version, gen.id])
|
||||
|
||||
const [dirty, setDirty] = useState(false)
|
||||
useModel(model, () => {
|
||||
setSeachParams({ version: undefined, preset: undefined })
|
||||
setError(null)
|
||||
setDirty(true)
|
||||
})
|
||||
|
||||
const [fileRename, setFileRename] = useState('')
|
||||
const [fileSaved, doSave] = useActiveTimeout()
|
||||
const [fileError, doFileError] = useActiveTimeout()
|
||||
|
||||
const doFileRename = () => {
|
||||
if (fileRename !== file?.id && fileRename && model && blockStates) {
|
||||
const data = getOutput(model, blockStates)
|
||||
const success = updateFile(gen.id, file?.id, { id: fileRename, data })
|
||||
if (success) {
|
||||
doSave()
|
||||
} else {
|
||||
doFileError()
|
||||
if (file) {
|
||||
setFileRename(file?.id)
|
||||
}
|
||||
}
|
||||
} else if (file) {
|
||||
setFileRename(file?.id)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteFile = () => {
|
||||
if (file) {
|
||||
updateFile(gen.id, file.id, {})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (file) {
|
||||
setFileRename(file.id)
|
||||
}
|
||||
}, [file])
|
||||
|
||||
useEffect(() => {
|
||||
if (model) {
|
||||
setFileRename(file?.id ?? '')
|
||||
if (file && gen.id === file.type) {
|
||||
model.reset(DataModel.wrapLists(file.data))
|
||||
} else {
|
||||
model.reset(DataModel.wrapLists(model.schema.default()), true)
|
||||
}
|
||||
setDirty(false)
|
||||
}
|
||||
}, [file, model])
|
||||
|
||||
const reset = () => {
|
||||
Analytics.generatorEvent('reset')
|
||||
model?.reset(DataModel.wrapLists(model.schema.default()), true)
|
||||
@@ -99,28 +145,34 @@ export function Generator({ lang, changeTitle, version, changeVersion }: Generat
|
||||
model?.redo()
|
||||
}
|
||||
}
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.ctrlKey && e.key === 's') {
|
||||
e.preventDefault()
|
||||
if (model && blockStates && file) {
|
||||
Analytics.generatorEvent('save', 'Hotkey')
|
||||
const data = getOutput(model, blockStates)
|
||||
updateFile(gen.id, file?.id, { id: file?.id, data })
|
||||
setDirty(false)
|
||||
doSave()
|
||||
}
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
document.addEventListener('keyup', onKeyUp)
|
||||
document.addEventListener('keydown', onKeyDown)
|
||||
return () => {
|
||||
document.removeEventListener('keyup', onKeyUp)
|
||||
document.removeEventListener('keydown', onKeyDown)
|
||||
}
|
||||
}, [model])
|
||||
}, [model, blockStates, file])
|
||||
|
||||
const [presetFilter, setPresetFilter] = useState('')
|
||||
const [presetResults, setPresetResults] = useState<string[]>([])
|
||||
const [presets, setPresets] = useState<string[]>([])
|
||||
useEffect(() => {
|
||||
getCollections(version)
|
||||
.then(collections => {
|
||||
const terms = (presetFilter ?? '').trim().split(' ')
|
||||
const presets = collections.get(gen.id)
|
||||
.map(p => p.slice(10))
|
||||
.filter(p => terms.every(t => p.includes(t)))
|
||||
if (presets) {
|
||||
setPresetResults(presets)
|
||||
}
|
||||
})
|
||||
getCollections(version).then(collections => {
|
||||
setPresets(collections.get(gen.id).map(p => p.slice(10)))
|
||||
})
|
||||
.catch(e => { console.error(e); setError(e.message) })
|
||||
}, [version, gen.id, presetFilter])
|
||||
}, [version, gen.id])
|
||||
|
||||
const selectPreset = (id: string) => {
|
||||
loadPreset(id).then(preset => {
|
||||
@@ -172,15 +224,7 @@ export function Generator({ lang, changeTitle, version, changeVersion }: Generat
|
||||
setImport(0)
|
||||
}
|
||||
|
||||
const [copyActive, setCopyActive] = useState(false)
|
||||
const copyTimeout = useRef<number | undefined>(undefined)
|
||||
const copySuccess = () => {
|
||||
setCopyActive(true)
|
||||
if (copyTimeout.current !== undefined) clearTimeout(copyTimeout.current)
|
||||
copyTimeout.current = setTimeout(() => {
|
||||
setCopyActive(false)
|
||||
}, 2000) as any
|
||||
}
|
||||
const [copyActive, copySuccess] = useActiveTimeout()
|
||||
|
||||
const [previewShown, setPreviewShown] = useState(false)
|
||||
const hasPreview = HasPreview.includes(gen.id)
|
||||
@@ -201,47 +245,59 @@ export function Generator({ lang, changeTitle, version, changeVersion }: Generat
|
||||
<main class={previewShown ? 'has-preview' : ''}>
|
||||
<Ad id="data-pack-generator" type="text" />
|
||||
<div class="controls">
|
||||
<Btn icon="upload" label={loc('import')} onClick={importSource} />
|
||||
<BtnMenu icon="archive" label={loc('presets')} relative={false}>
|
||||
<BtnInput icon="search" large value={presetFilter} onChange={setPresetFilter} doSelect={1} placeholder={loc('search')} />
|
||||
<div class="result-list">
|
||||
{presetResults.map(preset => <Btn label={preset} onClick={() => selectPreset(preset)} />)}
|
||||
<div class={`project-controls ${file && 'has-file'}`}>
|
||||
<div class="btn-row">
|
||||
<BtnMenu icon="repo" label={project.name} relative={false}>
|
||||
<Btn icon="arrow_left" label={locale('project.go_to')} onClick={() => route('/project')} />
|
||||
{file && <Btn icon="file" label={locale('project.new_file')} onClick={closeFile} />}
|
||||
<SearchList searchPlaceholder={locale(project.name === 'Drafts' ? 'project.search_drafts' : 'project.search')} noResults={locale('project.no_files')} values={project.files.filter(f => f.type === gen.id).map(f => f.id)} onSelect={(id) => openFile(gen.id, id)} />
|
||||
</BtnMenu>
|
||||
<TextInput class="btn btn-input" placeholder={locale('project.unsaved_file')} value={fileRename} onChange={setFileRename} onEnter={doFileRename} onBlur={doFileRename} />
|
||||
{file && <Btn icon="trashcan" tooltip={locale('project.delete_file')} onClick={deleteFile} />}
|
||||
</div>
|
||||
{presetResults.length === 0 && <Btn label={loc('no_presets')}/>}
|
||||
</BtnMenu>
|
||||
<BtnMenu icon="tag" label={version} data-cy="version-switcher">
|
||||
{allowedVersions.reverse().map(v =>
|
||||
<Btn label={v} active={v === version} onClick={() => changeVersion(v)} />
|
||||
)}
|
||||
</BtnMenu>
|
||||
<BtnMenu icon="kebab_horizontal" tooltip={loc('more')}>
|
||||
<Btn icon="history" label={loc('reset')} onClick={reset} />
|
||||
<Btn icon="arrow_left" label={loc('undo')} onClick={undo} />
|
||||
<Btn icon="arrow_right" label={loc('redo')} onClick={redo} />
|
||||
</BtnMenu>
|
||||
{dirty ? <div class="status-icon">{Octicon.dot_fill}</div>
|
||||
: fileSaved ? <div class="status-icon active">{Octicon.check}</div>
|
||||
: fileError && <div class="status-icon danger">{Octicon.x}</div> }
|
||||
</div>
|
||||
<div class="generator-controls">
|
||||
<Btn icon="upload" label={locale('import')} onClick={importSource} />
|
||||
<BtnMenu icon="archive" label={locale('presets')} relative={false}>
|
||||
<SearchList searchPlaceholder={locale('search')} noResults={locale('no_presets')} values={presets} onSelect={selectPreset}/>
|
||||
</BtnMenu>
|
||||
<BtnMenu icon="tag" label={version} tooltip={locale('switch_version')} data-cy="version-switcher">
|
||||
{allowedVersions.reverse().map(v =>
|
||||
<Btn label={v} active={v === version} onClick={() => changeVersion(v)} />
|
||||
)}
|
||||
</BtnMenu>
|
||||
<BtnMenu icon="kebab_horizontal" tooltip={locale('more')}>
|
||||
<Btn icon="history" label={locale('reset')} onClick={reset} />
|
||||
<Btn icon="arrow_left" label={locale('undo')} onClick={undo} />
|
||||
<Btn icon="arrow_right" label={locale('redo')} onClick={redo} />
|
||||
</BtnMenu>
|
||||
</div>
|
||||
</div>
|
||||
{error && <ErrorPanel error={error} onDismiss={() => setError(null)} />}
|
||||
<Tree {...{lang, model, version, blockStates}} onError={setError} />
|
||||
<Tree {...{model, version, blockStates}} onError={setError} />
|
||||
</main>
|
||||
<div class="popup-actions" style={`--offset: -${8 + actionsShown * 50}px;`}>
|
||||
<div class={`popup-action action-preview${hasPreview ? ' shown' : ''} tooltipped tip-nw`} aria-label={loc(previewShown ? 'hide_preview' : 'show_preview')} onClick={togglePreview}>
|
||||
<div class={`popup-action action-preview${hasPreview ? ' shown' : ''} tooltipped tip-nw`} aria-label={locale(previewShown ? 'hide_preview' : 'show_preview')} onClick={togglePreview}>
|
||||
{previewShown ? Octicon.x_circle : Octicon.play}
|
||||
</div>
|
||||
<div class={`popup-action action-download${sourceShown ? ' shown' : ''} tooltipped tip-nw`} aria-label={loc('download')} onClick={downloadSource}>
|
||||
<div class={`popup-action action-download${sourceShown ? ' shown' : ''} tooltipped tip-nw`} aria-label={locale('download')} onClick={downloadSource}>
|
||||
{Octicon.download}
|
||||
</div>
|
||||
<div class={`popup-action action-copy${sourceShown ? ' shown' : ''}${copyActive ? ' active' : ''} tooltipped tip-nw`} aria-label={loc(copyActive ? 'copied' : 'copy')} onClick={copySource}>
|
||||
<div class={`popup-action action-copy${sourceShown ? ' shown' : ''}${copyActive ? ' active' : ''} tooltipped tip-nw`} aria-label={locale(copyActive ? 'copied' : 'copy')} onClick={copySource}>
|
||||
{copyActive ? Octicon.check : Octicon.clippy}
|
||||
</div>
|
||||
<div class={'popup-action action-code shown tooltipped tip-nw'} aria-label={loc(sourceShown ? 'hide_output' : 'show_output')} onClick={toggleSource}>
|
||||
<div class={'popup-action action-code shown tooltipped tip-nw'} aria-label={locale(sourceShown ? 'hide_output' : 'show_output')} onClick={toggleSource}>
|
||||
{sourceShown ? Octicon.chevron_right : Octicon.code}
|
||||
</div>
|
||||
</div>
|
||||
<div class={`popup-preview${previewShown ? ' shown' : ''}`}>
|
||||
<PreviewPanel {...{lang, model, version, id: gen.id}} shown={previewShown} onError={setError} />
|
||||
<PreviewPanel {...{model, version, id: gen.id}} shown={previewShown} onError={setError} />
|
||||
</div>
|
||||
<div class={`popup-source${sourceShown ? ' shown' : ''}`}>
|
||||
<SourcePanel {...{lang, model, blockStates, doCopy, doDownload, doImport}} name={gen.schema ?? 'data'} copySuccess={copySuccess} onError={setError} />
|
||||
<SourcePanel {...{model, blockStates, doCopy, doDownload, doImport}} name={gen.schema ?? 'data'} copySuccess={copySuccess} onError={setError} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
import config from '../../config.json'
|
||||
import { ToolCard } from '../components'
|
||||
import { locale } from '../Locales'
|
||||
import { useLocale, useTitle } from '../contexts'
|
||||
import { cleanUrl } from '../Utils'
|
||||
|
||||
type HomeProps = {
|
||||
lang: string,
|
||||
changeTitle: (title: string) => unknown,
|
||||
interface Props {
|
||||
path?: string,
|
||||
}
|
||||
export function Home({ lang, changeTitle }: HomeProps) {
|
||||
const loc = locale.bind(null, lang)
|
||||
changeTitle(loc('title.home'))
|
||||
export function Home({}: Props) {
|
||||
const { locale } = useLocale()
|
||||
useTitle(locale('title.home'))
|
||||
return <main>
|
||||
<div class="home">
|
||||
<ToolCard title="Data packs">
|
||||
{config.generators.filter(g => !g.category).map(g =>
|
||||
<ToolCard title={loc(g.id)} link={cleanUrl(g.url)} />
|
||||
<ToolCard title={locale(g.id)} link={cleanUrl(g.url)} />
|
||||
)}
|
||||
<ToolCard title={loc('worldgen')} link="/worldgen/" />
|
||||
<ToolCard title={locale('worldgen')} link="/worldgen/" />
|
||||
</ToolCard>
|
||||
<ToolCard title="Resource packs">
|
||||
{config.generators.filter(g => g.category === 'assets').map(g =>
|
||||
<ToolCard title={loc(g.id)} link={cleanUrl(g.url)} />
|
||||
<ToolCard title={locale(g.id)} link={cleanUrl(g.url)} />
|
||||
)}
|
||||
</ToolCard>
|
||||
<ToolCard title="Report Inspector" icon="report"
|
||||
|
||||
28
src/app/pages/Project.tsx
Normal file
28
src/app/pages/Project.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useMemo } from 'preact/hooks'
|
||||
import { Ad, TreeView } from '../components'
|
||||
import { getFilePath, useLocale, useProject, useTitle } from '../contexts'
|
||||
|
||||
interface Props {
|
||||
path?: string,
|
||||
}
|
||||
export function Project({}: Props) {
|
||||
const { locale } = useLocale()
|
||||
const { project, openFile } = useProject()
|
||||
useTitle(locale('title.project', project.name))
|
||||
const entries = useMemo(() => project.files.map(getFilePath), project.files)
|
||||
|
||||
const selectFile = (entry: string) => {
|
||||
const [, namespace, type, ...id] = entry.split('/')
|
||||
openFile(type, `${namespace}:${id}`)
|
||||
}
|
||||
|
||||
return <main>
|
||||
<Ad id="data-pack-project" type="text" />
|
||||
<div class="project">
|
||||
<h2>{project.name}</h2>
|
||||
<div class="file-view">
|
||||
<TreeView entries={entries} onSelect={selectFile}/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
}
|
||||
@@ -1,22 +1,19 @@
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import config from '../../config.json'
|
||||
import { Ad, Btn, BtnMenu, ErrorPanel, SoundConfig, TextInput } from '../components'
|
||||
import { locale } from '../Locales'
|
||||
import { useLocale, useTitle, useVersion } from '../contexts'
|
||||
import type { SoundEvents, VersionAssets, VersionId } from '../services'
|
||||
import { getAssets, getSounds } from '../services'
|
||||
import { hexId, message } from '../Utils'
|
||||
|
||||
type SoundsProps = {
|
||||
interface Props {
|
||||
path?: string,
|
||||
lang: string,
|
||||
changeTitle: (title: string, versions?: VersionId[]) => unknown,
|
||||
version: VersionId,
|
||||
changeVersion: (version: VersionId) => unknown,
|
||||
}
|
||||
export function Sounds({ lang, changeTitle, version, changeVersion }: SoundsProps) {
|
||||
const loc = locale.bind(null, lang)
|
||||
export function Sounds({}: Props) {
|
||||
const { locale } = useLocale()
|
||||
const { version, changeVersion } = useVersion()
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
changeTitle(loc('title.sounds'))
|
||||
useTitle(locale('title.sounds'))
|
||||
|
||||
const [assets, setAssets] = useState<VersionAssets>({})
|
||||
const [sounds, setSounds] = useState<SoundEvents>({})
|
||||
@@ -63,13 +60,13 @@ export function Sounds({ lang, changeTitle, version, changeVersion }: SoundsProp
|
||||
{soundKeys.length > 0 && <>
|
||||
<div class="controls sounds-controls">
|
||||
<div class="sound-search-group">
|
||||
<TextInput class="btn btn-input sound-search" list="sound-list" placeholder={loc('sounds.search')}
|
||||
<TextInput class="btn btn-input sound-search" list="sound-list" placeholder={locale('sounds.search')}
|
||||
value={search} onChange={setSearch} onEnter={addConfig} />
|
||||
<Btn icon="plus" tooltip={loc('sounds.add_sound')} class="add-sound" onClick={addConfig} />
|
||||
<Btn icon="plus" tooltip={locale('sounds.add_sound')} class="add-sound" onClick={addConfig} />
|
||||
</div>
|
||||
{configs.length > 1 && <Btn icon="play" label={ loc('sounds.play_all')} class="play-all-sounds" onClick={playAll} />}
|
||||
{configs.length > 1 && <Btn icon="play" label={ locale('sounds.play_all')} class="play-all-sounds" onClick={playAll} />}
|
||||
<div class="spacer"></div>
|
||||
<Btn icon="download" label={loc('download')} tooltip={loc('sounds.download_function')} class="download-sounds" onClick={downloadFunction} />
|
||||
<Btn icon="download" label={locale('download')} tooltip={locale('sounds.download_function')} class="download-sounds" onClick={downloadFunction} />
|
||||
<BtnMenu icon="tag" label={version}>
|
||||
{config.versions.reverse().map(v =>
|
||||
<Btn label={v.id} active={v.id === version} onClick={() => changeVersion(v.id as VersionId)} />
|
||||
@@ -77,7 +74,7 @@ export function Sounds({ lang, changeTitle, version, changeVersion }: SoundsProp
|
||||
</BtnMenu>
|
||||
</div>
|
||||
<div class="sounds">
|
||||
{configs.map(c => <SoundConfig key={c.id} {...c} {...{ lang, assets, sounds, delayedPlay }} onEdit={editConfig(c.id)} onDelete={deleteConfig(c.id)} />)}
|
||||
{configs.map(c => <SoundConfig key={c.id} {...c} {...{ assets, sounds, delayedPlay }} onEdit={editConfig(c.id)} onDelete={deleteConfig(c.id)} />)}
|
||||
</div>
|
||||
<a ref={download} style="display: none;"></a>
|
||||
</>}
|
||||
|
||||
@@ -2,4 +2,5 @@ export * from './Category'
|
||||
export * from './Changelog'
|
||||
export * from './Generator'
|
||||
export * from './Home'
|
||||
export * from './Project'
|
||||
export * from './Sounds'
|
||||
|
||||
@@ -5,8 +5,8 @@ import { memo } from 'preact/compat'
|
||||
import { useState } from 'preact/hooks'
|
||||
import config from '../../config.json'
|
||||
import { Btn, Octicon } from '../components'
|
||||
import { localize } from '../contexts'
|
||||
import { useFocus } from '../hooks'
|
||||
import { locale } from '../Locales'
|
||||
import type { BlockStateRegistry, VersionId } from '../services'
|
||||
import { CachedDecorator, CachedFeature } from '../services'
|
||||
import { deepClone, deepEqual, hexId, isObject, newSeed } from '../Utils'
|
||||
@@ -115,14 +115,14 @@ const renderHtml: RenderHook = {
|
||||
const node = DataModel.wrapLists(children.default())
|
||||
path.model.set(path, [...value, { node, id: hexId() }])
|
||||
}
|
||||
const suffix = <button class="add tooltipped tip-se" aria-label={locale(lang, 'add_top')} onClick={onAdd}>{Octicon.plus_circle}</button>
|
||||
const suffix = <button class="add tooltipped tip-se" aria-label={localize(lang, 'add_top')} onClick={onAdd}>{Octicon.plus_circle}</button>
|
||||
const body = <>
|
||||
{(value && Array.isArray(value)) && value.map(({ node: cValue, id: cId }, index) => {
|
||||
if (index === maxShown) {
|
||||
return <div class="node node-header">
|
||||
<label>{locale(lang, 'entries_hidden', `${value.length - maxShown}`)}</label>
|
||||
<button onClick={() => setMaxShown(Math.min(maxShown + 50, value.length))}>{locale(lang, 'entries_hidden.more', '50')}</button>
|
||||
<button onClick={() => setMaxShown(value.length)}>{locale(lang, 'entries_hidden.all')}</button>
|
||||
<label>{localize(lang, 'entries_hidden', `${value.length - maxShown}`)}</label>
|
||||
<button onClick={() => setMaxShown(Math.min(maxShown + 50, value.length))}>{localize(lang, 'entries_hidden.more', '50')}</button>
|
||||
<button onClick={() => setMaxShown(value.length)}>{localize(lang, 'entries_hidden.all')}</button>
|
||||
</div>
|
||||
}
|
||||
if (index > maxShown) {
|
||||
@@ -135,7 +135,7 @@ const renderHtml: RenderHook = {
|
||||
if (canToggle && (toggle === false || (toggle === undefined && value.length > 20))) {
|
||||
return <div class="node node-header" data-category={children.category(cPath)}>
|
||||
<ErrorPopup lang={lang} path={cPath} nested />
|
||||
<button class="toggle tooltipped tip-se" aria-label={`${locale(lang, 'expand')}\n${locale(lang, 'expand_all', 'Ctrl')}`} onClick={expand(cId)}>{Octicon.chevron_right}</button>
|
||||
<button class="toggle tooltipped tip-se" aria-label={`${localize(lang, 'expand')}\n${localize(lang, 'expand_all', 'Ctrl')}`} onClick={expand(cId)}>{Octicon.chevron_right}</button>
|
||||
<label>{pathLocale(lang, cPath, `${index}`)}</label>
|
||||
<Collapsed key={cId} path={cPath} value={cValue} schema={children} />
|
||||
</div>
|
||||
@@ -164,16 +164,16 @@ const renderHtml: RenderHook = {
|
||||
},
|
||||
]
|
||||
return <MemoedTreeNode key={cId} path={cPath} schema={children} value={cValue} {...{lang, version, states, actions}} ctx={{...ctx, index: (index === 0 ? 1 : 0) + (index === value.length - 1 ? 2 : 0)}}>
|
||||
{canToggle && <button class="toggle tooltipped tip-se" aria-label={`${locale(lang, 'collapse')}\n${locale(lang, 'collapse_all', 'Ctrl')}`} onClick={collapse(cId)}>{Octicon.chevron_down}</button>}
|
||||
<button class="remove tooltipped tip-se" aria-label={locale(lang, 'remove')} onClick={onRemove}>{Octicon.trashcan}</button>
|
||||
{canToggle && <button class="toggle tooltipped tip-se" aria-label={`${localize(lang, 'collapse')}\n${localize(lang, 'collapse_all', 'Ctrl')}`} onClick={collapse(cId)}>{Octicon.chevron_down}</button>}
|
||||
<button class="remove tooltipped tip-se" aria-label={localize(lang, 'remove')} onClick={onRemove}>{Octicon.trashcan}</button>
|
||||
{value.length > 1 && <div class="node-move">
|
||||
<button class="move tooltipped tip-se" aria-label={locale(lang, 'move_up')} onClick={onMoveUp} disabled={index === 0}>{Octicon.chevron_up}</button>
|
||||
<button class="move tooltipped tip-se" aria-label={locale(lang, 'move_down')} onClick={onMoveDown} disabled={index === value.length - 1}>{Octicon.chevron_down}</button>
|
||||
<button class="move tooltipped tip-se" aria-label={localize(lang, 'move_up')} onClick={onMoveUp} disabled={index === 0}>{Octicon.chevron_up}</button>
|
||||
<button class="move tooltipped tip-se" aria-label={localize(lang, 'move_down')} onClick={onMoveDown} disabled={index === value.length - 1}>{Octicon.chevron_down}</button>
|
||||
</div>}
|
||||
</MemoedTreeNode>
|
||||
})}
|
||||
{(value && value.length > 0 && value.length <= maxShown) && <div class="node node-header">
|
||||
<button class="add tooltipped tip-se" aria-label={locale(lang, 'add_bottom')} onClick={onAddBottom}>{Octicon.plus_circle}</button>
|
||||
<button class="add tooltipped tip-se" aria-label={localize(lang, 'add_bottom')} onClick={onAddBottom}>{Octicon.plus_circle}</button>
|
||||
</div>}
|
||||
</>
|
||||
return [null, suffix, body]
|
||||
@@ -206,7 +206,7 @@ const renderHtml: RenderHook = {
|
||||
}
|
||||
const suffix = <>
|
||||
{keysSchema.hook(this, keyPath, keyPath.get() ?? '', lang, version, states, ctx)[1]}
|
||||
<button class="add tooltipped tip-se" aria-label={locale(lang, 'add')} onClick={onAdd}>{Octicon.plus_circle}</button>
|
||||
<button class="add tooltipped tip-se" aria-label={localize(lang, 'add')} onClick={onAdd}>{Octicon.plus_circle}</button>
|
||||
</>
|
||||
const body = <>
|
||||
{typeof value === 'object' && Object.entries(value).map(([key, cValue]) => {
|
||||
@@ -217,7 +217,7 @@ const renderHtml: RenderHook = {
|
||||
if (canToggle && (toggle === false || (toggle === undefined && value.length > 20))) {
|
||||
return <div class="node node-header" data-category={children.category(cPath)}>
|
||||
<ErrorPopup lang={lang} path={cPath} nested />
|
||||
<button class="toggle tooltipped tip-se" aria-label={`${locale(lang, 'expand')}\n${locale(lang, 'expand_all', 'Ctrl')}`} onClick={expand(key)}>{Octicon.chevron_right}</button>
|
||||
<button class="toggle tooltipped tip-se" aria-label={`${localize(lang, 'expand')}\n${localize(lang, 'expand_all', 'Ctrl')}`} onClick={expand(key)}>{Octicon.chevron_right}</button>
|
||||
<label>{key}</label>
|
||||
<Collapsed key={key} path={cPath} value={cValue} schema={children} />
|
||||
</div>
|
||||
@@ -231,8 +231,8 @@ const renderHtml: RenderHook = {
|
||||
}
|
||||
const onRemove = () => cPath.set(undefined)
|
||||
return <MemoedTreeNode key={key} schema={cSchema} path={cPath} value={cValue} {...{lang, version, states, ctx}} label={key}>
|
||||
{canToggle && <button class="toggle tooltipped tip-se" aria-label={`${locale(lang, 'collapse')}\n${locale(lang, 'collapse_all', 'Ctrl')}`} onClick={collapse(key)}>{Octicon.chevron_down}</button>}
|
||||
<button class="remove tooltipped tip-se" aria-label={locale(lang, 'remove')} onClick={onRemove}>{Octicon.trashcan}</button>
|
||||
{canToggle && <button class="toggle tooltipped tip-se" aria-label={`${localize(lang, 'collapse')}\n${localize(lang, 'collapse_all', 'Ctrl')}`} onClick={collapse(key)}>{Octicon.chevron_down}</button>}
|
||||
<button class="remove tooltipped tip-se" aria-label={localize(lang, 'remove')} onClick={onRemove}>{Octicon.trashcan}</button>
|
||||
</MemoedTreeNode>
|
||||
})}
|
||||
</>
|
||||
@@ -258,17 +258,17 @@ const renderHtml: RenderHook = {
|
||||
if (node.optional()) {
|
||||
if (value === undefined) {
|
||||
const onExpand = () => path.set(DataModel.wrapLists(node.default()))
|
||||
suffix = <button class="collapse closed tooltipped tip-se" aria-label={locale(lang, 'expand')} onClick={onExpand}>{Octicon.plus_circle}</button>
|
||||
suffix = <button class="collapse closed tooltipped tip-se" aria-label={localize(lang, 'expand')} onClick={onExpand}>{Octicon.plus_circle}</button>
|
||||
} else {
|
||||
const onCollapse = () => path.set(undefined)
|
||||
suffix = <button class="collapse open tooltipped tip-se" aria-label={locale(lang, 'remove')} onClick={onCollapse}>{Octicon.trashcan}</button>
|
||||
suffix = <button class="collapse open tooltipped tip-se" aria-label={localize(lang, 'remove')} onClick={onCollapse}>{Octicon.trashcan}</button>
|
||||
}
|
||||
}
|
||||
const context = path.getContext().join('.')
|
||||
if (collapsedFields.includes(context)) {
|
||||
const toggled = isToggled('')
|
||||
prefix = <>
|
||||
<button class="toggle tooltipped tip-se" aria-label={locale(lang, toggled ? 'collapse' : 'expand')} onClick={toggled ? collapse('') : expand('')}>{toggled ? Octicon.chevron_down : Octicon.chevron_right}</button>
|
||||
<button class="toggle tooltipped tip-se" aria-label={localize(lang, toggled ? 'collapse' : 'expand')} onClick={toggled ? collapse('') : expand('')}>{toggled ? Octicon.chevron_down : Octicon.chevron_right}</button>
|
||||
</>
|
||||
if (!toggled) {
|
||||
return [prefix, suffix, null]
|
||||
@@ -357,8 +357,8 @@ function BooleanSuffix({ path, node, value, lang }: NodeProps<BooleanHookParams>
|
||||
path.model.set(path, node.optional() && value === target ? undefined : target)
|
||||
}
|
||||
return <>
|
||||
<button class={value === false ? 'selected' : ''} onClick={() => set(false)}>{locale(lang, 'false')}</button>
|
||||
<button class={value === true ? 'selected' : ''} onClick={() => set(true)}>{locale(lang, 'true')}</button>
|
||||
<button class={value === false ? 'selected' : ''} onClick={() => set(false)}>{localize(lang, 'false')}</button>
|
||||
<button class={value === true ? 'selected' : ''} onClick={() => set(true)}>{localize(lang, 'true')}</button>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -376,7 +376,7 @@ function NumberSuffix({ path, config, integer, value, lang }: NodeProps<NumberHo
|
||||
return <>
|
||||
<input type="text" value={value ?? ''} onBlur={onChange} onKeyDown={evt => {if (evt.key === 'Enter') onChange(evt)}} />
|
||||
{config?.color && <input type="color" value={'#' + (value?.toString(16).padStart(6, '0') ?? '000000')} onChange={onColor} />}
|
||||
{['dimension.generator.seed', 'dimension.generator.biome_source.seed', 'world_settings.seed'].includes(path.getContext().join('.')) && <button onClick={() => newSeed(path.model)} class="tooltipped tip-se" aria-label={locale(lang, 'generate_new_seed')}>{Octicon.sync}</button>}
|
||||
{['dimension.generator.seed', 'dimension.generator.biome_source.seed', 'world_settings.seed'].includes(path.getContext().join('.')) && <button onClick={() => newSeed(path.model)} class="tooltipped tip-se" aria-label={localize(lang, 'generate_new_seed')}>{Octicon.sync}</button>}
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -403,7 +403,7 @@ function StringSuffix({ path, getValues, config, node, value, lang, version, sta
|
||||
context = path
|
||||
}
|
||||
return <select value={value ?? ''} onChange={onChange}>
|
||||
{node.optional() && <option value="">{locale(lang, 'unset')}</option>}
|
||||
{node.optional() && <option value="">{localize(lang, 'unset')}</option>}
|
||||
{values.map(v => <option value={v}>
|
||||
{pathLocale(lang, context.contextPush(v.replace(/^minecraft:/, '')))}
|
||||
</option>)}
|
||||
@@ -424,7 +424,7 @@ function StringSuffix({ path, getValues, config, node, value, lang, version, sta
|
||||
{values.map(v => <option value={v} />)}
|
||||
</datalist>}
|
||||
{gen && values.includes(value) && value.startsWith('minecraft:') &&
|
||||
<a href={`/${gen.url}/?version=${version}&preset=${value.replace(/^minecraft:/, '')}`} class="tooltipped tip-se" aria-label={locale(lang, 'follow_reference')}>{Octicon.link_external}</a>}
|
||||
<a href={`/${gen.url}/?version=${version}&preset=${value.replace(/^minecraft:/, '')}`} class="tooltipped tip-se" aria-label={localize(lang, 'follow_reference')}>{Octicon.link_external}</a>}
|
||||
</>
|
||||
}
|
||||
}
|
||||
@@ -473,11 +473,11 @@ function TreeNode({ label, schema, path, value, lang, version, states, ctx, acti
|
||||
{label ?? pathLocale(lang, path, `${path.last()}`)}
|
||||
{active && <div class="node-menu">
|
||||
{actions?.map(a => <div key={a.label} class="menu-item">
|
||||
<Btn icon={a.icon} tooltip={locale(lang, a.label)} tooltipLoc="se" onClick={() => a.onSelect()}/>
|
||||
<span>{a.description ?? locale(lang, a.label)}</span>
|
||||
<Btn icon={a.icon} tooltip={localize(lang, a.label)} tooltipLoc="se" onClick={() => a.onSelect()}/>
|
||||
<span>{a.description ?? localize(lang, a.label)}</span>
|
||||
</div>)}
|
||||
<div class="menu-item">
|
||||
<Btn icon="clippy" tooltip={locale(lang, 'copy_context')} tooltipLoc="se" onClick={() => navigator.clipboard.writeText(context)} />
|
||||
<Btn icon="clippy" tooltip={localize(lang, 'copy_context')} tooltipLoc="se" onClick={() => navigator.clipboard.writeText(context)} />
|
||||
<span>{context}</span>
|
||||
</div>
|
||||
</div>}
|
||||
@@ -514,7 +514,7 @@ function pathLocale(lang: string, path: Path, ...params: string[]) {
|
||||
const ctx = path.getContext()
|
||||
for (let i = 0; i < ctx.length; i += 1) {
|
||||
const key = ctx.slice(i).join('.')
|
||||
const result = locale(lang, key, ...params)
|
||||
const result = localize(lang, key, ...params)
|
||||
if (key !== result) {
|
||||
return result
|
||||
}
|
||||
@@ -530,13 +530,13 @@ function ErrorPopup({ lang, path, nested }: { lang: string, path: ModelPath, nes
|
||||
? path.model.errors.getAll().filter(e => e.path.startsWith(path))
|
||||
: path.model.errors.get(path, true)
|
||||
if (e.length === 0) return null
|
||||
const message = locale(lang, e[0].error, ...(e[0].params ?? []))
|
||||
const message = localize(lang, e[0].error, ...(e[0].params ?? []))
|
||||
return popupIcon('node-error', 'issue_opened', message)
|
||||
}
|
||||
|
||||
function HelpPopup({ lang, path }: { lang: string, path: Path }) {
|
||||
const key = path.contextPush('help').getContext().join('.')
|
||||
const message = locale(lang, key)
|
||||
const message = localize(lang, key)
|
||||
if (message === key) return null
|
||||
return popupIcon('node-help', 'info', message)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type { Hook } from '@mcschema/core'
|
||||
import { relativePath } from '@mcschema/core'
|
||||
import type { DataModel, Hook } from '@mcschema/core'
|
||||
import { ModelPath, relativePath } from '@mcschema/core'
|
||||
import type { BlockStateRegistry } from '../services'
|
||||
|
||||
export function getOutput(model: DataModel, blockStates: BlockStateRegistry): any {
|
||||
return model.schema.hook(transformOutput, new ModelPath(model), model.data, { blockStates })
|
||||
}
|
||||
|
||||
export type OutputProps = {
|
||||
blockStates: BlockStateRegistry,
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
"title.generator": "%0% Generator",
|
||||
"title.generator_category": "%0% Generators",
|
||||
"title.home": "Data Pack Generators",
|
||||
"title.project": "%0% Project",
|
||||
"title.sounds": "Sound Explorer",
|
||||
"presets": "Presets",
|
||||
"preview": "Visualize",
|
||||
@@ -84,6 +85,13 @@
|
||||
"preview.offset": "Offset",
|
||||
"preview.peaks": "Peaks",
|
||||
"preview.width": "Width",
|
||||
"project.delete_file": "Delete file",
|
||||
"project.go_to": "Go to project",
|
||||
"project.new_file": "New file",
|
||||
"project.no_files": "No files",
|
||||
"project.search": "Search project",
|
||||
"project.search_drafts": "Search drafts",
|
||||
"project.unsaved_file": "Unsaved file",
|
||||
"remove": "Remove",
|
||||
"search": "Search",
|
||||
"show_output": "Show output",
|
||||
@@ -101,8 +109,9 @@
|
||||
"sounds.remove_sound": "Remove sound",
|
||||
"sounds.unknown_sound": "Unknown sound",
|
||||
"sounds.loading_sound": "Loading sound",
|
||||
"source_placeholder": "Paste raw content here",
|
||||
"source_placeholder": "Paste raw %0% content here",
|
||||
"switch_generator": "Switch generator",
|
||||
"switch_version": "Switch version",
|
||||
"terrain_settings": "Terrain settings",
|
||||
"undo": "Undo",
|
||||
"world": "World Settings",
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
--text-3: #c3c3c3;
|
||||
--accent-primary: #50baf9;
|
||||
--accent-success: #3eb84f;
|
||||
--accent-warning: #b8893e;
|
||||
--accent-danger: #cf4945;
|
||||
--accent-sounds-1: #451475;
|
||||
--accent-sounds-2: #39155e;
|
||||
--accent-sounds-3: #6a08a3;
|
||||
@@ -38,6 +40,8 @@
|
||||
--text-3: #494949;
|
||||
--accent-primary: #088cdb;
|
||||
--accent-success: #1a7f37;
|
||||
--accent-warning: #a36f1c;
|
||||
--accent-danger: #bd2f2a;
|
||||
--accent-sounds-1: #b481e7;
|
||||
--accent-sounds-2: #c18df5;
|
||||
--accent-sounds-3: #af72d3;
|
||||
@@ -67,6 +71,8 @@
|
||||
--text-3: #494949;
|
||||
--accent-primary: #088cdb;
|
||||
--accent-success: #1a7f37;
|
||||
--accent-warning: #a36f1c;
|
||||
--accent-danger: #bd2f2a;
|
||||
--accent-sounds-1: #b481e7;
|
||||
--accent-sounds-2: #c18df5;
|
||||
--accent-sounds-3: #af72d3;
|
||||
@@ -119,11 +125,6 @@ header {
|
||||
background-color: var(--background-2);
|
||||
}
|
||||
|
||||
body[data-panel="home"] header,
|
||||
body[data-panel="settings"] header {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -230,29 +231,81 @@ main {
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
left: 16px;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
main > .controls {
|
||||
position: sticky;
|
||||
margin-right: 16px;
|
||||
margin-left: 16px;
|
||||
top: 68px;
|
||||
}
|
||||
|
||||
.controls > * {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.controls > *:not(:last-child) {
|
||||
main > .controls {
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
position: initial;
|
||||
margin-right: 16px;
|
||||
margin-left: 16px;
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
.generator-controls {
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
position: sticky;
|
||||
top: 68px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.generator-controls > *:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.project-controls {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: max-content;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.project-controls > .btn-row > .btn-input {
|
||||
background-color: var(--background-2);
|
||||
}
|
||||
|
||||
.project-controls > .status-icon {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
align-self: center;
|
||||
margin-right: 8px;
|
||||
fill: var(--text-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.project-controls.has-file > .status-icon {
|
||||
right: 38px;
|
||||
}
|
||||
|
||||
.project-controls > .active {
|
||||
fill: var(--accent-success);
|
||||
}
|
||||
|
||||
.project-controls > .danger {
|
||||
fill: var(--accent-danger);
|
||||
}
|
||||
|
||||
.project-controls .btn-menu .btn-group {
|
||||
left: 0;
|
||||
right: unset;
|
||||
}
|
||||
|
||||
.source-controls {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.tree {
|
||||
margin-top: -36px;
|
||||
overflow-x: auto;
|
||||
padding: 4px 16px 50vh;
|
||||
padding: 8px 16px 50vh;
|
||||
}
|
||||
|
||||
.error + .tree {
|
||||
@@ -266,7 +319,7 @@ main > .controls {
|
||||
width: 40vw;
|
||||
left: 100%;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
z-index: 3;
|
||||
transition: transform 0.3s;
|
||||
border-radius: 6px 0 0 0;
|
||||
}
|
||||
@@ -315,7 +368,7 @@ main > .controls {
|
||||
width: 40vw;
|
||||
left: 100%;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
z-index: 3;
|
||||
background-color: var(--background-2);
|
||||
box-shadow: 0 0 7px -3px #000;
|
||||
transition: transform 0.3s;
|
||||
@@ -386,10 +439,6 @@ main.has-preview {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.btn-menu > .btn {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.btn-menu .btn-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -428,6 +477,29 @@ main.has-preview {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.btn-row {
|
||||
display: flex;
|
||||
box-shadow: 0 1px 7px -2px #000;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-row > *,
|
||||
.btn-row > .btn-menu > * {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-row > *:not(:first-child),
|
||||
.btn-row > .btn-menu:not(:first-child) > * {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.btn-row > *:not(:last-child),
|
||||
.btn-row > .btn-menu:not(:last-child) > * {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.btn-input {
|
||||
cursor: initial;
|
||||
padding-right: 7px;
|
||||
@@ -469,7 +541,7 @@ main.has-preview {
|
||||
position: fixed;
|
||||
bottom: 8px;
|
||||
left: 100%;
|
||||
z-index: 5;
|
||||
z-index: 4;
|
||||
padding-right: 16px;
|
||||
background-color: var(--background-4);
|
||||
box-shadow: 0 0 7px -3px #000;
|
||||
@@ -649,7 +721,7 @@ main.has-preview {
|
||||
color: var(--text-1)
|
||||
}
|
||||
|
||||
.home, .category {
|
||||
.home, .category, .project {
|
||||
padding: 16px;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
@@ -804,6 +876,37 @@ hr {
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
.project h2 {
|
||||
color: var(--text-1);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.file-view {
|
||||
background-color: var(--background-2);
|
||||
border-radius: 6px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.tree-view .entry {
|
||||
cursor: pointer;
|
||||
padding: 4px 2px;
|
||||
padding-left: calc(var(--indent, 0) * 24px);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
background-color: var(--background-2);
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.tree-view .entry:hover {
|
||||
background-color: var(--background-3);
|
||||
}
|
||||
|
||||
.tree-view .entry svg {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
[data-ea-publisher] {
|
||||
margin: 0 16px 8px;
|
||||
}
|
||||
@@ -1194,18 +1297,6 @@ hr {
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1300px) {
|
||||
main.has-preview .tree {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
main .tree {
|
||||
margin-top: 4px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* SMALL */
|
||||
@media screen and (max-width: 580px) {
|
||||
.home {
|
||||
@@ -1260,7 +1351,7 @@ hr {
|
||||
|
||||
.btn.btn.large-input,
|
||||
.btn-menu .result-list {
|
||||
width: calc(100vw - 10px);
|
||||
width: calc(100vw - 32px);
|
||||
}
|
||||
|
||||
.generator-picker {
|
||||
|
||||
Reference in New Issue
Block a user