diff --git a/package-lock.json b/package-lock.json index a1bbd800..f821539a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,12 +16,14 @@ "@mcschema/java-1.18": "^0.1.5", "@mcschema/locales": "^0.1.29", "deepslate": "^0.9.0-beta.2", + "howler": "^2.2.3", "rfdc": "^1.3.0" }, "devDependencies": { "@preact/preset-vite": "^2.1.0", "@rollup/plugin-html": "^0.2.3", "@types/google.analytics": "0.0.40", + "@types/howler": "^2.2.4", "@types/seedrandom": "^2.4.28", "@typescript-eslint/eslint-plugin": "^4.25.0", "@typescript-eslint/parser": "^4.25.0", @@ -499,6 +501,12 @@ "integrity": "sha512-R3HpnLkqmKxhUAf8kIVvDVGJqPtaaZlW4yowNwjOZUTmYUQEgHh8Nh5wkSXKMroNAuQM8gbXJHmNbbgA8tdb7Q==", "dev": true }, + "node_modules/@types/howler": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@types/howler/-/howler-2.2.4.tgz", + "integrity": "sha512-/Bs5TyNUWuXPnWe3RB6bS6giQzGHRXmSycq4Mo/2i4C6zwfR7foaAAx1Vo5pMyOAT/ufTfU6WZQHhdvCDBKRig==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.7", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", @@ -1642,6 +1650,11 @@ "node": ">=4" } }, + "node_modules/howler": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.3.tgz", + "integrity": "sha512-QM0FFkw0LRX1PR8pNzJVAY25JhIWvbKMBFM4gqk+QdV+kPXOhleWGCB6AiAF/goGjIHK2e/nIElplvjQwhr0jg==" + }, "node_modules/ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -3066,6 +3079,12 @@ "integrity": "sha512-R3HpnLkqmKxhUAf8kIVvDVGJqPtaaZlW4yowNwjOZUTmYUQEgHh8Nh5wkSXKMroNAuQM8gbXJHmNbbgA8tdb7Q==", "dev": true }, + "@types/howler": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@types/howler/-/howler-2.2.4.tgz", + "integrity": "sha512-/Bs5TyNUWuXPnWe3RB6bS6giQzGHRXmSycq4Mo/2i4C6zwfR7foaAAx1Vo5pMyOAT/ufTfU6WZQHhdvCDBKRig==", + "dev": true + }, "@types/json-schema": { "version": "7.0.7", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", @@ -3902,6 +3921,11 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, + "howler": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.3.tgz", + "integrity": "sha512-QM0FFkw0LRX1PR8pNzJVAY25JhIWvbKMBFM4gqk+QdV+kPXOhleWGCB6AiAF/goGjIHK2e/nIElplvjQwhr0jg==" + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", diff --git a/package.json b/package.json index cab9b26a..a60423c1 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,14 @@ "@mcschema/java-1.18": "^0.1.5", "@mcschema/locales": "^0.1.29", "deepslate": "^0.9.0-beta.2", + "howler": "^2.2.3", "rfdc": "^1.3.0" }, "devDependencies": { "@preact/preset-vite": "^2.1.0", "@rollup/plugin-html": "^0.2.3", "@types/google.analytics": "0.0.40", + "@types/howler": "^2.2.4", "@types/seedrandom": "^2.4.28", "@typescript-eslint/eslint-plugin": "^4.25.0", "@typescript-eslint/parser": "^4.25.0", diff --git a/src/app/DataFetcher.ts b/src/app/DataFetcher.ts index 7ed7a209..5cccbb2a 100644 --- a/src/app/DataFetcher.ts +++ b/src/app/DataFetcher.ts @@ -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(url: string, fn: (v: any) => T = (v: any) => v): Promise { try { const cache = await caches.open(CACHE_NAME) diff --git a/src/app/Main.tsx b/src/app/Main.tsx index 2072d0e9..355433ae 100644 --- a/src/app/Main.tsx +++ b/src/app/Main.tsx @@ -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() { - + + } diff --git a/src/app/Manifest.ts b/src/app/Manifest.ts new file mode 100644 index 00000000..b02a28d7 --- /dev/null +++ b/src/app/Manifest.ts @@ -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 | null = null + +export type VersionAssets = { + [key: string]: { + hash: string, + }, +} +const VersionAssets: Record> = {} + +export type SoundEvents = { + [key: string]: { + sounds: (string | { name: string })[], + }, +} +const SoundEvents: Record> = {} + +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] +} diff --git a/src/app/Store.ts b/src/app/Store.ts index 12794bc1..b591703f 100644 --- a/src/app/Store.ts +++ b/src/app/Store.ts @@ -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) + } } diff --git a/src/app/components/Octicon.tsx b/src/app/components/Octicon.tsx index cb89dad0..4a40c4f8 100644 --- a/src/app/components/Octicon.tsx +++ b/src/app/components/Octicon.tsx @@ -1,4 +1,5 @@ export const Octicon = { + alert: , archive: , arrow_left: , arrow_right: , @@ -30,6 +31,7 @@ export const Octicon = { sun: , sync: , tag: , + terminal: , three_bars: , trashcan: , unfold: , diff --git a/src/app/components/forms/Input.tsx b/src/app/components/forms/Input.tsx new file mode 100644 index 00000000..242c572c --- /dev/null +++ b/src/app/components/forms/Input.tsx @@ -0,0 +1,31 @@ +import type { JSXInternal } from 'preact/src/jsx' + +type InputProps = JSXInternal.HTMLAttributes + +type BaseInputProps = Omit & { + onChange?: (value: T) => unknown, + onEnter?: (value: T) => unknown, +} +function BaseInput(name: string, type: string, fn: (value: string) => T) { + const component = (props: BaseInputProps) => { + 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 + } + 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)) diff --git a/src/app/components/forms/index.ts b/src/app/components/forms/index.ts new file mode 100644 index 00000000..54e51f6c --- /dev/null +++ b/src/app/components/forms/index.ts @@ -0,0 +1 @@ +export * from './Input' diff --git a/src/app/components/PreviewPanel.tsx b/src/app/components/generator/PreviewPanel.tsx similarity index 91% rename from src/app/components/PreviewPanel.tsx rename to src/app/components/generator/PreviewPanel.tsx index b343d43c..dd854adc 100644 --- a/src/app/components/PreviewPanel.tsx +++ b/src/app/components/generator/PreviewPanel.tsx @@ -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'] diff --git a/src/app/components/SourcePanel.tsx b/src/app/components/generator/SourcePanel.tsx similarity index 91% rename from src/app/components/SourcePanel.tsx rename to src/app/components/generator/SourcePanel.tsx index fdd61a1d..12dbfe0c 100644 --- a/src/app/components/SourcePanel.tsx +++ b/src/app/components/generator/SourcePanel.tsx @@ -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 diff --git a/src/app/components/Tree.tsx b/src/app/components/generator/Tree.tsx similarity index 82% rename from src/app/components/Tree.tsx rename to src/app/components/generator/Tree.tsx index f0929306..00cba130 100644 --- a/src/app/components/Tree.tsx +++ b/src/app/components/generator/Tree.tsx @@ -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, diff --git a/src/app/components/generator/index.ts b/src/app/components/generator/index.ts new file mode 100644 index 00000000..81efcba3 --- /dev/null +++ b/src/app/components/generator/index.ts @@ -0,0 +1,3 @@ +export * from './PreviewPanel' +export * from './SourcePanel' +export * from './Tree' diff --git a/src/app/components/index.ts b/src/app/components/index.ts index 3c6a0fb3..c55b6dea 100644 --- a/src/app/components/index.ts +++ b/src/app/components/index.ts @@ -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' diff --git a/src/app/components/sounds/SoundConfig.tsx b/src/app/components/sounds/SoundConfig.tsx new file mode 100644 index 00000000..68e7db19 --- /dev/null +++ b/src/app/components/sounds/SoundConfig.tsx @@ -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) => 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([]) + 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(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
+ + onEdit({ sound })} /> + + onEdit({ delay })} /> + + onEdit({ pitch })} /> + + onEdit({ volume })} /> + + {onDelete(); stop()}} /> +
+} diff --git a/src/app/components/sounds/index.ts b/src/app/components/sounds/index.ts new file mode 100644 index 00000000..4c8f9dbf --- /dev/null +++ b/src/app/components/sounds/index.ts @@ -0,0 +1 @@ +export * from './SoundConfig' diff --git a/src/app/pages/Generator.tsx b/src/app/pages/Generator.tsx index 06976063..beebd199 100644 --- a/src/app/pages/Generator.tsx +++ b/src/app/pages/Generator.tsx @@ -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(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 {allowedVersions.reverse().map(v => - onChangeVersion(v)} /> + changeVersion(v)} /> )} diff --git a/src/app/pages/Home.tsx b/src/app/pages/Home.tsx index 7d6a8ac2..478e1fab 100644 --- a/src/app/pages/Home.tsx +++ b/src/app/pages/Home.tsx @@ -21,7 +21,7 @@ export function Home({ lang, changeTitle }: HomeProps) {

Analyse your performance reports

- +

Browse through and mix all the vanilla sounds

diff --git a/src/app/pages/Sounds.tsx b/src/app/pages/Sounds.tsx new file mode 100644 index 00000000..445cfe22 --- /dev/null +++ b/src/app/pages/Sounds.tsx @@ -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(null) + changeTitle(loc('title.sounds')) + + const [assets, setAssets] = useState({}) + const [sounds, setSounds] = useState({}) + 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([]) + const addConfig = () => { + setConfigs([{ id: hexId(), sound: search, delay: 0, pitch: 1, volume: 1 }, ...configs]) + } + const editConfig = (id: string) => (changes: Partial) => { + 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(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
+ + {error && setError(null)} />} + {soundKeys.length > 0 && <> +
+
+ + +
+ {configs.length > 1 && } +
+ + + {config.versions.reverse().map(v => + changeVersion(v.id as VersionId)} /> + )} + +
+
+ {configs.map(c => )} +
+ + } + + {soundKeys.map(s => +
+} diff --git a/src/app/pages/index.ts b/src/app/pages/index.ts index 370745f1..2d435cf4 100644 --- a/src/app/pages/index.ts +++ b/src/app/pages/index.ts @@ -1,3 +1,4 @@ export * from './Generator' export * from './Home' +export * from './Sounds' export * from './Worldgen' diff --git a/src/config.json b/src/config.json index 9450a592..1a131eb0 100644 --- a/src/config.json +++ b/src/config.json @@ -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", diff --git a/src/locales/en.json b/src/locales/en.json index d24491a8..bbb996f6 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -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", diff --git a/src/styles/global.css b/src/styles/global.css index 1d07f327..28232691 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -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%; - } }