Add sounds explorer tool

This commit is contained in:
Misode
2021-10-08 02:34:38 +02:00
parent c1de35b6c2
commit 79b3291d06
23 changed files with 690 additions and 56 deletions
+38
View File
@@ -1,5 +1,6 @@
import type { CollectionRegistry } from '@mcschema/core'
import config from '../config.json'
import type { VersionAssets, VersionManifest } from './Manifest'
import type { BlockStateRegistry, VersionId } from './Schemas'
import { checkVersion } from './Schemas'
import { message } from './Utils'
@@ -21,6 +22,9 @@ declare var __VANILLA_DATAPACK_SUMMARY_HASH__: string
const mcdataUrl = 'https://raw.githubusercontent.com/Arcensoth/mcdata'
const vanillaDatapackUrl = 'https://raw.githubusercontent.com/SPGoding/vanilla-datapack'
const manifestUrl = 'https://launchermeta.mojang.com/mc/game/version_manifest.json'
const resourceUrl = 'https://resources.download.minecraft.net/'
const corsUrl = 'https://misode-cors-anywhere.herokuapp.com/'
const refs: {
id: VersionRef,
@@ -166,6 +170,40 @@ export async function fetchPreset(version: VersionId, registry: string, id: stri
}
}
export async function fetchManifest() {
try {
const res = await fetch(manifestUrl)
return await res.json()
} catch (e) {
throw new Error(`Error occurred while fetching version manifest: ${message(e)}`)
}
}
export async function fetchAssets(versionId: VersionId, manifest: VersionManifest) {
const version = config.versions.find(v => v.id === versionId)
const id = version?.latest ?? manifest.latest.snapshot
try {
const versionMeta = await getData(manifest.versions.find(v => v.id === id)!.url)
return (await getData(versionMeta.assetIndex.url)).objects
} catch (e) {
throw new Error(`Error occurred while fetching assets for ${version}: ${message(e)}`)
}
}
export async function fetchSounds(version: VersionId, assets: VersionAssets) {
try {
const hash = assets['minecraft/sounds.json'].hash
return await getData(getResourceUrl(hash))
} catch (e) {
throw new Error(`Error occurred while fetching sounds for ${version}: ${message(e)}`)
}
}
export function getResourceUrl(hash: string) {
return `${corsUrl}${resourceUrl}${hash.slice(0, 2)}/${hash}`
}
async function getData<T = any>(url: string, fn: (v: any) => T = (v: any) => v): Promise<T> {
try {
const cache = await caches.open(CACHE_NAME)
+3 -2
View File
@@ -8,7 +8,7 @@ import '../styles/nodes.css'
import { Analytics } from './Analytics'
import { Header } from './components'
import { loadLocale, locale, Locales } from './Locales'
import { Generator, Home, Worldgen } from './pages'
import { Generator, Home, Sounds, Worldgen } from './pages'
import type { VersionId } from './Schemas'
import { Store } from './Store'
import { cleanUrl } from './Utils'
@@ -71,7 +71,8 @@ function Main() {
<Router onChange={changeRoute}>
<Home path="/" {...{lang, changeTitle}} />
<Worldgen path="/worldgen" {...{lang, changeTitle}} />
<Generator default {...{lang, version, changeTitle}} onChangeVersion={changeVersion} />
<Sounds path="/sounds" {...{lang, version, changeTitle, changeVersion}} />
<Generator default {...{lang, version, changeTitle, changeVersion}} />
</Router>
</>
}
+56
View File
@@ -0,0 +1,56 @@
import { fetchAssets, fetchManifest, fetchSounds } from './DataFetcher'
import type { VersionId } from './Schemas'
export type VersionManifest = {
latest: {
release: string,
snapshot: string,
},
versions: {
id: string,
type: string,
url: string,
}[],
}
let Manifest: VersionManifest | Promise<VersionManifest> | null = null
export type VersionAssets = {
[key: string]: {
hash: string,
},
}
const VersionAssets: Record<string, VersionAssets | Promise<VersionAssets>> = {}
export type SoundEvents = {
[key: string]: {
sounds: (string | { name: string })[],
},
}
const SoundEvents: Record<string, SoundEvents | Promise<SoundEvents>> = {}
export async function getManifest() {
if (!Manifest) {
Manifest = fetchManifest()
}
return Manifest
}
export async function getAssets(version: VersionId) {
if (!VersionAssets[version]) {
VersionAssets[version] = (async () => {
const manifest = await getManifest()
return await fetchAssets(version, manifest)
})()
}
return VersionAssets[version]
}
export async function getSounds(version: VersionId) {
if (!SoundEvents[version]) {
SoundEvents[version] = (async () => {
const assets = await getAssets(version)
return await fetchSounds(version, assets)
})()
}
return SoundEvents[version]
}
+9
View File
@@ -6,6 +6,7 @@ export namespace Store {
export const ID_THEME = 'theme'
export const ID_VERSION = 'schema_version'
export const ID_INDENT = 'indentation'
export const ID_SOUNDS_VERSION = 'minecraft_sounds_version'
export function getLanguage() {
return localStorage.getItem(ID_LANGUAGE) ?? 'en'
@@ -27,6 +28,10 @@ export namespace Store {
return localStorage.getItem(ID_INDENT) ?? '2_spaces'
}
export function getSoundsVersion() {
return localStorage.getItem(ID_SOUNDS_VERSION) ?? 'latest'
}
export function setLanguage(language: string | undefined) {
if (language) localStorage.setItem(ID_LANGUAGE, language)
}
@@ -42,4 +47,8 @@ export namespace Store {
export function setIndent(indent: string | undefined) {
if (indent) localStorage.setItem(ID_INDENT, indent)
}
export function setSoundsVersion(version: string | undefined) {
if (version) localStorage.setItem(ID_SOUNDS_VERSION, version)
}
}
+2
View File
@@ -1,4 +1,5 @@
export const Octicon = {
alert: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.22 1.754a.25.25 0 00-.44 0L1.698 13.132a.25.25 0 00.22.368h12.164a.25.25 0 00.22-.368L8.22 1.754zm-1.763-.707c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0114.082 15H1.918a1.75 1.75 0 01-1.543-2.575L6.457 1.047zM9 11a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z"></path></svg>,
archive: <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.5a.25.25 0 00-.25.25v1.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-1.5a.25.25 0 00-.25-.25H1.75zM0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v1.5A1.75 1.75 0 0114.25 6H1.75A1.75 1.75 0 010 4.25v-1.5zM1.75 7a.75.75 0 01.75.75v5.5c0 .138.112.25.25.25h10.5a.25.25 0 00.25-.25v-5.5a.75.75 0 111.5 0v5.5A1.75 1.75 0 0113.25 15H2.75A1.75 1.75 0 011 13.25v-5.5A.75.75 0 011.75 7zm4.5 1a.75.75 0 000 1.5h3.5a.75.75 0 100-1.5h-3.5z"></path></svg>,
arrow_left: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.78 12.53a.75.75 0 01-1.06 0L2.47 8.28a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 1.06L4.81 7h7.44a.75.75 0 010 1.5H4.81l2.97 2.97a.75.75 0 010 1.06z"></path></svg>,
arrow_right: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.22 2.97a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06l2.97-2.97H3.75a.75.75 0 010-1.5h7.44L8.22 4.03a.75.75 0 010-1.06z"></path></svg>,
@@ -30,6 +31,7 @@ export const Octicon = {
sun: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 10.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zM8 12a4 4 0 100-8 4 4 0 000 8zM8 0a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0V.75A.75.75 0 018 0zm0 13a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 018 13zM2.343 2.343a.75.75 0 011.061 0l1.06 1.061a.75.75 0 01-1.06 1.06l-1.06-1.06a.75.75 0 010-1.06zm9.193 9.193a.75.75 0 011.06 0l1.061 1.06a.75.75 0 01-1.06 1.061l-1.061-1.06a.75.75 0 010-1.061zM16 8a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0116 8zM3 8a.75.75 0 01-.75.75H.75a.75.75 0 010-1.5h1.5A.75.75 0 013 8zm10.657-5.657a.75.75 0 010 1.061l-1.061 1.06a.75.75 0 11-1.06-1.06l1.06-1.06a.75.75 0 011.06 0zm-9.193 9.193a.75.75 0 010 1.06l-1.06 1.061a.75.75 0 11-1.061-1.06l1.06-1.061a.75.75 0 011.061 0z"></path></svg>,
sync: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 2.5a5.487 5.487 0 00-4.131 1.869l1.204 1.204A.25.25 0 014.896 6H1.25A.25.25 0 011 5.75V2.104a.25.25 0 01.427-.177l1.38 1.38A7.001 7.001 0 0114.95 7.16a.75.75 0 11-1.49.178A5.501 5.501 0 008 2.5zM1.705 8.005a.75.75 0 01.834.656 5.501 5.501 0 009.592 2.97l-1.204-1.204a.25.25 0 01.177-.427h3.646a.25.25 0 01.25.25v3.646a.25.25 0 01-.427.177l-1.38-1.38A7.001 7.001 0 011.05 8.84a.75.75 0 01.656-.834z"></path></svg>,
tag: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.5 7.775V2.75a.25.25 0 01.25-.25h5.025a.25.25 0 01.177.073l6.25 6.25a.25.25 0 010 .354l-5.025 5.025a.25.25 0 01-.354 0l-6.25-6.25a.25.25 0 01-.073-.177zm-1.5 0V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 010 2.474l-5.026 5.026a1.75 1.75 0 01-2.474 0l-6.25-6.25A1.75 1.75 0 011 7.775zM6 5a1 1 0 100 2 1 1 0 000-2z"></path></svg>,
terminal: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0114.25 15H1.75A1.75 1.75 0 010 13.25V2.75zm1.75-.25a.25.25 0 00-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25V2.75a.25.25 0 00-.25-.25H1.75zM7.25 8a.75.75 0 01-.22.53l-2.25 2.25a.75.75 0 11-1.06-1.06L5.44 8 3.72 6.28a.75.75 0 111.06-1.06l2.25 2.25c.141.14.22.331.22.53zm1.5 1.5a.75.75 0 000 1.5h3a.75.75 0 000-1.5h-3z"></path></svg>,
three_bars: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1 2.75A.75.75 0 011.75 2h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 2.75zm0 5A.75.75 0 011.75 7h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 7.75zM1.75 12a.75.75 0 100 1.5h12.5a.75.75 0 100-1.5H1.75z"></path></svg>,
trashcan: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M6.5 1.75a.25.25 0 01.25-.25h2.5a.25.25 0 01.25.25V3h-3V1.75zm4.5 0V3h2.25a.75.75 0 010 1.5H2.75a.75.75 0 010-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75zM4.496 6.675a.75.75 0 10-1.492.15l.66 6.6A1.75 1.75 0 005.405 15h5.19c.9 0 1.652-.681 1.741-1.576l.66-6.6a.75.75 0 00-1.492-.149l-.66 6.6a.25.25 0 01-.249.225h-5.19a.25.25 0 01-.249-.225l-.66-6.6z"></path></svg>,
unfold: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M8.177.677l2.896 2.896a.25.25 0 01-.177.427H8.75v1.25a.75.75 0 01-1.5 0V4H5.104a.25.25 0 01-.177-.427L7.823.677a.25.25 0 01.354 0zM7.25 10.75a.75.75 0 011.5 0V12h2.146a.25.25 0 01.177.427l-2.896 2.896a.25.25 0 01-.354 0l-2.896-2.896A.25.25 0 015.104 12H7.25v-1.25zm-5-2a.75.75 0 000-1.5h-.5a.75.75 0 000 1.5h.5zM6 8a.75.75 0 01-.75.75h-.5a.75.75 0 010-1.5h.5A.75.75 0 016 8zm2.25.75a.75.75 0 000-1.5h-.5a.75.75 0 000 1.5h.5zM12 8a.75.75 0 01-.75.75h-.5a.75.75 0 010-1.5h.5A.75.75 0 0112 8zm2.25.75a.75.75 0 000-1.5h-.5a.75.75 0 000 1.5h.5z"></path></svg>,
+31
View File
@@ -0,0 +1,31 @@
import type { JSXInternal } from 'preact/src/jsx'
type InputProps = JSXInternal.HTMLAttributes<HTMLInputElement>
type BaseInputProps<T> = Omit<InputProps, 'onChange' | 'type'> & {
onChange?: (value: T) => unknown,
onEnter?: (value: T) => unknown,
}
function BaseInput<T>(name: string, type: string, fn: (value: string) => T) {
const component = (props: BaseInputProps<T>) => {
const onChange = props.onChange && ((evt: Event) => {
const value = (evt.target as HTMLInputElement).value
props.onChange?.(fn(value))
})
const onKeyDown = props.onEnter && ((evt: KeyboardEvent) => {
if (evt.key === 'Enter') {
const value = (evt.target as HTMLInputElement).value
props.onEnter?.(fn(value))
}
})
return <input {...props} {...{ type, onChange, onKeyDown }} />
}
component.displayName = name
return component
}
export const TextInput = BaseInput('TextInput', 'text', v => v)
export const NumberInput = BaseInput('NumberInput', 'number', v => Number(v))
export const RangeInput = BaseInput('RangeInput', 'range', v => Number(v))
+1
View File
@@ -0,0 +1 @@
export * from './Input'
@@ -1,9 +1,9 @@
import type { DataModel } from '@mcschema/core'
import { Path } from '@mcschema/core'
import { useState } from 'preact/hooks'
import { useModel } from '../hooks'
import type { VersionId } from '../Schemas'
import { BiomeSourcePreview, DecoratorPreview, NoiseSettingsPreview } from './previews'
import { useModel } from '../../hooks'
import type { VersionId } from '../../Schemas'
import { BiomeSourcePreview, DecoratorPreview, NoiseSettingsPreview } from '../previews'
export const HasPreview = ['dimension', 'worldgen/noise_settings', 'worldgen/configured_feature']
@@ -1,12 +1,12 @@
import { DataModel, ModelPath } from '@mcschema/core'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import { Btn, BtnMenu } from '.'
import { useModel } from '../hooks'
import { locale } from '../Locales'
import { transformOutput } from '../schema/transformOutput'
import type { BlockStateRegistry } from '../Schemas'
import { Store } from '../Store'
import { message } from '../Utils'
import { Btn, BtnMenu } from '..'
import { useModel } from '../../hooks'
import { locale } from '../../Locales'
import { transformOutput } from '../../schema/transformOutput'
import type { BlockStateRegistry } from '../../Schemas'
import { Store } from '../../Store'
import { message } from '../../Utils'
const OUTPUT_CHARS_LIMIT = 10000
@@ -1,8 +1,8 @@
import type { DataModel } from '@mcschema/core'
import { useErrorBoundary, useState } from 'preact/hooks'
import { useModel } from '../hooks'
import { FullNode } from '../schema/renderHtml'
import type { BlockStateRegistry, VersionId } from '../Schemas'
import { useModel } from '../../hooks'
import { FullNode } from '../../schema/renderHtml'
import type { BlockStateRegistry, VersionId } from '../../Schemas'
type TreePanelProps = {
lang: string,
+3
View File
@@ -0,0 +1,3 @@
export * from './PreviewPanel'
export * from './SourcePanel'
export * from './Tree'
+4 -3
View File
@@ -3,10 +3,11 @@ export * from './Btn'
export * from './BtnInput'
export * from './BtnMenu'
export * from './ErrorPanel'
export * from './forms'
export * from './generator'
export * from './Header'
export * from './Icons'
export * from './Octicon'
export * from './PreviewPanel'
export * from './SourcePanel'
export * from './previews'
export * from './sounds'
export * from './ToolCard'
export * from './Tree'
+122
View File
@@ -0,0 +1,122 @@
import { Howl } from 'howler'
import { useEffect, useRef, useState } from 'preact/hooks'
import { Btn, NumberInput, RangeInput, TextInput } from '..'
import { getResourceUrl } from '../../DataFetcher'
import { locale } from '../../Locales'
import type { SoundEvents, VersionAssets } from '../../Manifest'
export interface SoundConfig {
id: string,
sound: string,
delay: number,
pitch: number,
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)
const [loading, setLoading] = useState(true)
const [playing, setPlaying] = useState(false)
const [invalid, setInvalid] = useState(false)
const howls = useRef<Howl[]>([])
const command = `playsound minecraft:${sound} master @s ~ ~ ~ ${volume} ${pitch}`
useEffect(() => {
const soundEvent = sounds[sound]
setInvalid((soundEvent?.sounds?.length ?? 0) === 0)
howls.current.forEach(h => h.stop())
howls.current = (soundEvent?.sounds ?? []).map(entry => {
const soundPath = typeof entry === 'string' ? entry : entry.name
const hash = assets[`minecraft/sounds/${soundPath}.ogg`].hash
const url = getResourceUrl(hash)
const howl = new Howl({
src: [url],
format: ['ogg'],
volume,
rate: pitch,
})
howl.on('end', () => {
setPlaying(false)
})
const completed = () => {
if (loading && howls.current.every(h => h.state() === 'loaded')) {
setLoading(false)
}
}
if (howl.state() === 'loaded') {
setTimeout(() => completed())
} else {
howl.on('load', () => {
completed()
})
}
return howl
})
setLoading(true)
}, [sound, sounds])
useEffect(() => {
howls.current.forEach(h => h.rate(pitch))
}, [pitch])
useEffect(() => {
howls.current.forEach(h => h.volume(volume))
}, [volume])
const play = () => {
if (loading || invalid) return
stop()
const howl = Math.floor(Math.random() * howls.current.length)
howls.current[howl].play()
setPlaying(true)
}
const stop = () => {
howls.current.forEach(h => h.stop())
}
useEffect(() => {
if (delayedPlay) setTimeout(() => play(), delay * 50)
}, [delayedPlay])
useEffect(() => {
return () => stop()
}, [])
const [copyActive, setCopyActive] = useState(false)
const copyTimeout = useRef<number | undefined>(undefined)
const copy = () => {
navigator.clipboard.writeText(command)
setCopyActive(true)
if (copyTimeout.current !== undefined) clearTimeout(copyTimeout.current)
copyTimeout.current = setTimeout(() => {
setCopyActive(false)
}, 2000) as any
}
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" />
<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>
<NumberInput class="btn btn-input delay" min={0}
value={delay} onChange={delay => onEdit({ delay })} />
<label class="pitch-label">{loc('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>
<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')}
onClick={copy} />
<Btn class="remove" icon="trashcan" tooltip={loc('sounds.remove_sound')}
onClick={() => {onDelete(); stop()}} />
</div>
}
+1
View File
@@ -0,0 +1 @@
export * from './SoundConfig'
+5 -5
View File
@@ -9,16 +9,16 @@ import { useModel } from '../hooks'
import { locale } from '../Locales'
import type { BlockStateRegistry, VersionId } from '../Schemas'
import { checkVersion, getBlockStates, getCollections, getModel } from '../Schemas'
import { getGenerator } from '../Utils'
import { getGenerator, message } from '../Utils'
type GeneratorProps = {
lang: string,
changeTitle: (title: string, versions?: VersionId[]) => unknown,
version: VersionId,
onChangeVersion: (version: VersionId) => unknown,
changeVersion: (version: VersionId) => unknown,
default?: true,
}
export function Generator({ lang, changeTitle, version, onChangeVersion }: GeneratorProps) {
export function Generator({ lang, changeTitle, version, changeVersion }: GeneratorProps) {
const loc = locale.bind(null, lang)
const [error, setError] = useState<string | null>(null)
const [errorBoundary, errorRetry] = useErrorBoundary()
@@ -53,7 +53,7 @@ export function Generator({ lang, changeTitle, version, onChangeVersion }: Gener
.then(b => setBlockStates(b))
getModel(version, gen.id)
.then(m => setModel(m))
.catch(e => { console.error(e); setError(e.message) })
.catch(e => { console.error(e); setError(message(e)) })
}, [version, gen.id])
useModel(model, () => {
@@ -183,7 +183,7 @@ export function Generator({ lang, changeTitle, version, onChangeVersion }: Gener
</BtnMenu>
<BtnMenu icon="tag" label={version}>
{allowedVersions.reverse().map(v =>
<Btn label={v} active={v === version} onClick={() => onChangeVersion(v)} />
<Btn label={v} active={v === version} onClick={() => changeVersion(v)} />
)}
</BtnMenu>
<BtnMenu icon="kebab_horizontal" tooltip={loc('more')}>
+1 -1
View File
@@ -21,7 +21,7 @@ export function Home({ lang, changeTitle }: HomeProps) {
<ToolCard title="Report Inspector" icon="report" link="https://misode.github.io/report/">
<p>Analyse your performance reports</p>
</ToolCard>
<ToolCard title="Minecraft Sounds" icon="sounds" link="https://misode.github.io/sounds/">
<ToolCard title="Minecraft Sounds" icon="sounds" link="/sounds/">
<p>Browse through and mix all the vanilla sounds</p>
</ToolCard>
<ToolCard title="Data Pack Upgrader" link="https://misode.github.io/upgrader/">
+89
View File
@@ -0,0 +1,89 @@
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 type { SoundEvents, VersionAssets } from '../Manifest'
import { getAssets, getSounds } from '../Manifest'
import type { VersionId } from '../Schemas'
import { hexId, message } from '../Utils'
type SoundsProps = {
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)
const [error, setError] = useState<string | null>(null)
changeTitle(loc('title.sounds'))
const [assets, setAssets] = useState<VersionAssets>({})
const [sounds, setSounds] = useState<SoundEvents>({})
const soundKeys = Object.keys(sounds ?? {})
useEffect(() => {
getAssets(version)
.then(assets => { setAssets(assets); return getSounds(version) })
.then(sounds => { if (sounds) setSounds(sounds) })
.catch(e => { console.error(e); setError(message(e)) })
}, [version])
const [search, setSearch] = useState('')
const [configs, setConfigs] = useState<SoundConfig[]>([])
const addConfig = () => {
setConfigs([{ id: hexId(), sound: search, delay: 0, pitch: 1, volume: 1 }, ...configs])
}
const editConfig = (id: string) => (changes: Partial<SoundConfig>) => {
setConfigs(configs.map(c => c.id === id ? { ...c, ...changes } : c))
}
const deleteConfig = (id: string) => () => {
setConfigs(configs.filter(c => c.id !== id))
}
const [delayedPlay, setDelayedPlay] = useState(0)
const playAll = () => {
setDelayedPlay(delayedPlay + 1)
}
const download = useRef<HTMLAnchorElement>(null)
const downloadFunction = () => {
const hasDelay = configs.some(c => c.delay > 0)
const content = configs
.sort((a, b) => a.delay - b.delay)
.map(c => `${hasDelay ? `execute if score @s delay matches ${c.delay} run ` : ''}playsound minecraft:${c.sound} master @s ~ ~ ~ ${c.volume} ${c.pitch}`)
.join('\n')
download.current.setAttribute('href', 'data:text/plain;charset=utf-8,' + content + '%0A')
download.current.setAttribute('download', 'sounds.mcfunction')
download.current.click()
}
return <main>
<Ad type="text" id="sounds" />
{error && <ErrorPanel error={error} onDismiss={() => setError(null)} />}
{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')}
value={search} onChange={setSearch} onEnter={addConfig} />
<Btn icon="plus" tooltip={loc('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} />}
<div class="spacer"></div>
<Btn icon="download" label={loc('download')} tooltip={loc('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)} />
)}
</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)} />)}
</div>
<a ref={download} style="display: none;"></a>
</>}
<datalist id="sound-list">
{soundKeys.map(s => <option key={s} value={s} />)}
</datalist>
</main>
}
+1
View File
@@ -1,3 +1,4 @@
export * from './Generator'
export * from './Home'
export * from './Sounds'
export * from './Worldgen'
+3
View File
@@ -49,12 +49,14 @@
"versions": [
{
"id": "1.15",
"latest": "1.15.2",
"refs": {
"mcdata_master": "13355f7"
}
},
{
"id": "1.16",
"latest": "1.16.5",
"refs": {
"mcdata_master": "1.16.4",
"vanilla_datapack_data": "1.16.4-data",
@@ -63,6 +65,7 @@
},
{
"id": "1.17",
"latest": "1.17.1",
"refs": {
"mcdata_master": "1.17.1",
"vanilla_datapack_data": "1.17.1-data",
+14
View File
@@ -54,6 +54,7 @@
"title.generator": "%0% Generator",
"title.generator_category": "%0% Generators",
"title.home": "Data Pack Generators",
"title.sounds": "Sound Explorer",
"presets": "Presets",
"preview": "Visualize",
"preview.scale": "Scale",
@@ -66,6 +67,19 @@
"search": "Search",
"show_output": "Show JSON output",
"show_preview": "Show preview",
"sounds.play": "Play",
"sounds.play_sound": "Play sound",
"sounds.play_all": "Play all",
"sounds.search": "Search sounds",
"sounds.download_function": "Download Mcfunction",
"sounds.delay": "Delay",
"sounds.pitch": "Pitch",
"sounds.volume": "Volume",
"sounds.copy_command": "Copy command",
"sounds.add_sound": "Add sound",
"sounds.remove_sound": "Remove sound",
"sounds.unknown_sound": "Unknown sound",
"sounds.loading_sound": "Loading sound",
"source_placeholder": "Paste JSON content here",
"switch_generator": "Switch generator",
"terrain_settings": "Terrain settings",
+268 -32
View File
@@ -10,6 +10,10 @@
--text-3: #c3c3c3;
--accent-primary: #50baf9;
--accent-success: #3eb84f;
--accent-sounds-1: #451475;
--accent-sounds-2: #39155e;
--accent-sounds-3: #6a08a3;
--accent-sounds-4: #d1a5e6;
--nav: #91908f;
--nav-hover: #b4b3b0;
--nav-faded: #4d4c4c;
@@ -17,6 +21,7 @@
--selection: #6786dd99;
--errors-background: #62190f;
--errors-text: #ffffffcc;
--invalid-text: #fd7951;
}
:root[data-theme=light] {
@@ -31,6 +36,10 @@
--text-3: #494949;
--accent-primary: #088cdb;
--accent-success: #1a7f37;
--accent-sounds-1: #b481e7;
--accent-sounds-2: #c18df5;
--accent-sounds-3: #af72d3;
--accent-sounds-4: #efd3fd;
--nav: #343a40;
--nav-hover: #565d64;
--nav-faded: #9fa2a7;
@@ -38,6 +47,7 @@
--selection: #6786dd99;
--errors-background: #f66653;
--errors-text: #000000cc;
--invalid-text: #a32600;
}
@media (prefers-color-scheme: light) {
@@ -53,6 +63,10 @@
--text-3: #494949;
--accent-primary: #088cdb;
--accent-success: #1a7f37;
--accent-sounds-1: #b481e7;
--accent-sounds-2: #c18df5;
--accent-sounds-3: #af72d3;
--accent-sounds-4: #efd3fd;
--nav: #343a40;
--nav-hover: #565d64;
--nav-faded: #9fa2a7;
@@ -60,6 +74,7 @@
--selection: #6786dd99;
--errors-background: #f66653;
--errors-text: #000000cc;
--invalid-text: #a32600;
}
}
@@ -80,6 +95,7 @@ a svg {
body {
font-size: 18px;
font-family: Arial, Helvetica, sans-serif;
min-height: 100vh;
overflow-x: hidden;
background-color: var(--background-1);
}
@@ -206,6 +222,7 @@ main {
position: fixed;
top: 12px;
right: 16px;
left: 16px;
z-index: 1;
pointer-events: none;
}
@@ -213,7 +230,7 @@ main {
main > .controls {
position: sticky;
margin-right: 16px;
right: 16px;
margin-left: 16px;
top: 68px;
}
@@ -502,48 +519,55 @@ main.has-preview {
opacity: 0;
}
.tooltipped.tip-nw::after,
.tooltipped.tip-ne::after {
bottom: 100%;
margin-bottom: 6px;
}
.tooltipped.tip-se::after,
.tooltipped.tip-s::after,
.tooltipped.tip-sw::after {
top: 100%;
margin-top: 6px;
}
.tooltipped.tip-ne::after
.tooltipped.tip-se::after {
left: 50%;
margin-left: -16px;
}
.tooltipped.tip-nw::after {
bottom: 100%;
margin-bottom: 6px;
.tooltipped.tip-nw::after,
.tooltipped.tip-sw::after {
right: 50%;
margin-right: -16px;
}
.tooltipped.tip-ne::before,
.tooltipped.tip-n::before,
.tooltipped.tip-nw::before {
bottom: auto;
top: -7px;
border-top-color: var(--background-6);
}
.tooltipped.tip-se::after {
top: 100%;
margin-top: 6px;
left: 50%;
margin-left: -16px;
}
.tooltipped.tip-sw::after {
top: 100%;
margin-top: 6px;
right: 50%;
margin-right: -16px;
}
.tooltipped.tip-se::before,
.tooltipped.tip-s::before,
.tooltipped.tip-sw::before {
top: auto;
bottom: -7px;
border-bottom-color: var(--background-6);
}
.tooltipped.tip-s::after,
.tooltipped.tip-n::after,
.tooltipped.tip-s::before,
.tooltipped.tip-n::before {
left: var(--x, 50%);
transform: translate(-50%, 8px);
}
.tooltipped::before {
content: '';
position: absolute;
@@ -768,12 +792,236 @@ hr {
color: var(--text-3) !important;
}
@keyframes spinner {
.sounds {
padding: 16px;
}
.sound-search-group {
flex-basis: 350px;
height: 32px;
display: flex;
border-radius: 6px;
box-shadow: 0 1px 7px -2px #000;
}
.sound-search {
flex-basis: 100%;
padding: 8px;
color: var(--text-1);
background-color: var(--background-2);
border: none;
border-radius: 6px;
font-size: 16px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
margin-right: 0 !important;
box-shadow: none;
}
.btn.add-sound {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
background-color: var(--accent-sounds-1);
box-shadow: none;
}
.btn.add-sound:hover {
background-color: var(--accent-sounds-2);
}
.spacer {
margin-right: auto !important;
}
.sound-config {
display: grid;
grid-template-columns: min-content 2fr min-content min-content min-content 1fr min-content 1fr min-content min-content;
align-items: center;
gap: 12px 8px;
padding: 10px;
background-color: var(--background-2);
border-radius: 5px;
}
.sound-config:not(:last-child) {
margin-bottom: 8px;
}
.sound-config .btn {
box-shadow: none;
}
.sound-config .sound {
width: 100%;
}
.sound-config label {
color: var(--text-2);
white-space: nowrap;
}
.sound-config .delay {
width: 50px;
padding: 4px;
}
.sound-config input[type=range] {
-webkit-appearance: none;
width: 100%;
background: transparent;
}
.sound-config input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
}
.sound-config input[type=range]:focus {
outline: none;
}
.sound-config input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
border: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: var(--text-3);
cursor: pointer;
margin-top: -5px;
}
.sound-config input[type=range]::-moz-range-thumb {
border: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: var(--text-3);
cursor: pointer;
}
.sound-config input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 8px;
cursor: pointer;
background: var(--background-4);
border-radius: 2px;
border: none;
}
.sound-config input[type=range]:focus::-webkit-slider-runnable-track {
background: var(--background-5);
}
.sound-config input[type=range]::-moz-range-track {
width: 100%;
height: 8px;
cursor: pointer;
background: var(--background-4);
border-radius: 2px;
border: none;
}
.sound-config input[type=range]:focus::-moz-range-track {
background: var(--background-5);
}
.sound-config .copy[data-command] {
position: relative;
}
.sound-config .copy[data-command]::after {
content: attr(data-command);
position: absolute;
top: 100%;
right: 0;
margin-top: 6px;
padding: 8px 12px;
background-color: var(--background-3);
border-radius: 5px;
box-shadow: 0 2px 4px var(--background-1);
cursor: initial;
}
.sound-config.invalid .play,
.sound-config.loading .play {
cursor: initial;
}
.sound-config.playing {
background-color: var(--background-3);
}
.sound-config.playing .play {
background-image: linear-gradient(110deg, var(--accent-sounds-3), var(--accent-sounds-3) 45%, var(--accent-sounds-4) 47%, var(--accent-sounds-4) 53%, var(--accent-sounds-3) 55%);
background-size: 300%;
background-position: right;
animation: playing 1s infinite;
}
@keyframes playing {
0% {
background-position: left;
}
100% {
background-position: right;
}
}
.sound-config.loading:not(.invalid) .play svg {
animation: spinning 2s infinite linear;
}
.sound-config.invalid .sound {
color: var(--invalid-text);
}
@media screen and (max-width: 720px) {
.sound-search-group {
margin-bottom: 8px;
flex-basis: 100%;
margin-right: 0 !important;
}
.sounds-controls {
flex-wrap: wrap;
}
.sounds .btn {
padding: 8px 10px;
}
.sounds .btn svg {
margin-right: 0 !important;
}
.sounds .btn span {
display: none;
}
.sound-config {
grid-template-columns: min-content min-content 1fr min-content 1fr min-content;
grid-template-areas:
"play sound sound sound sound copy"
"pitch-label pitch-label pitch volume-label volume remove";
}
.sound-config .play { grid-area: play; }
.sound-config .sound { grid-area: sound; }
.sound-config .delay-label { display: none; }
.sound-config .delay { display: none; }
.sound-config .pitch-label { grid-area: pitch-label; }
.sound-config .pitch { grid-area: pitch; }
.sound-config .volume-label { grid-area: volume-label; }
.sound-config .volume { grid-area: volume; }
.sound-config .copy { grid-area: copy; }
.sound-config .remove { grid-area: remove; }
}
@keyframes spinning {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
transform: rotate(-360deg);
}
}
@@ -849,16 +1097,4 @@ hr {
.generator-picker {
justify-content: center;
}
.field-list li {
flex-direction: column;
}
.field-prop {
width: 100%;
}
.field-prop input {
width: 100%;
}
}