Add dialog preview
BIN
public/images/dialog/background.webp
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
public/images/dialog/button.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
public/images/dialog/button_highlighted.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
public/images/dialog/checkbox.png
Normal file
|
After Width: | Height: | Size: 205 B |
BIN
public/images/dialog/checkbox_selected.png
Normal file
|
After Width: | Height: | Size: 217 B |
BIN
public/images/dialog/slider.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/images/dialog/slider_handle.png
Normal file
|
After Width: | Height: | Size: 156 B |
BIN
public/images/dialog/text_field.png
Normal file
|
After Width: | Height: | Size: 98 B |
BIN
public/images/dialog/warning_button.png
Normal file
|
After Width: | Height: | Size: 379 B |
BIN
public/images/dialog/warning_button_highlighted.png
Normal file
|
After Width: | Height: | Size: 378 B |
@@ -5,9 +5,9 @@ import { useVersion } from '../../contexts/Version.jsx'
|
||||
import { checkVersion } from '../../services/index.js'
|
||||
import { safeJsonParse } from '../../Utils.js'
|
||||
import { ErrorPanel } from '../ErrorPanel.jsx'
|
||||
import { BiomeSourcePreview, BlockStatePreview, DecoratorPreview, DensityFunctionPreview, ItemModelPreview, LootTablePreview, ModelPreview, NoisePreview, NoiseSettingsPreview, RecipePreview, StructureSetPreview } from '../previews/index.js'
|
||||
import { BiomeSourcePreview, BlockStatePreview, DecoratorPreview, DensityFunctionPreview, DialogPreview, ItemModelPreview, LootTablePreview, ModelPreview, NoisePreview, NoiseSettingsPreview, RecipePreview, StructureSetPreview } from '../previews/index.js'
|
||||
|
||||
export const HasPreview = ['loot_table', 'recipe', 'dimension', 'worldgen/density_function', 'worldgen/noise', 'worldgen/noise_settings', 'worldgen/configured_feature', 'worldgen/placed_feature', 'worldgen/structure_set', 'block_definition', 'item_definition', 'model']
|
||||
export const HasPreview = ['loot_table', 'recipe', 'dialog', 'dimension', 'worldgen/density_function', 'worldgen/noise', 'worldgen/noise_settings', 'worldgen/configured_feature', 'worldgen/placed_feature', 'worldgen/structure_set', 'block_definition', 'item_definition', 'model']
|
||||
|
||||
type PreviewPanelProps = {
|
||||
id: string,
|
||||
@@ -50,6 +50,10 @@ export function PreviewContent({ id, docAndNode, shown }: PreviewContentProps) {
|
||||
return <RecipePreview {...{ docAndNode, shown }} />
|
||||
}
|
||||
|
||||
if (id === 'dialog') {
|
||||
return <DialogPreview {...{ docAndNode, shown }} />
|
||||
}
|
||||
|
||||
if (id === 'dimension' && safeJsonParse(docAndNode.doc.getText())?.generator?.type?.endsWith('noise')) {
|
||||
return <BiomeSourcePreview {...{ docAndNode, shown }} />
|
||||
}
|
||||
|
||||
240
src/app/components/previews/DialogPreview.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
import { Identifier, ItemStack } from 'deepslate'
|
||||
import type { ComponentChild } from 'preact'
|
||||
import { useEffect, useRef } from 'preact/hooks'
|
||||
import { safeJsonParse } from '../../Utils.js'
|
||||
import { ItemDisplay } from '../ItemDisplay.jsx'
|
||||
import { TextComponent } from '../TextComponent.jsx'
|
||||
import type { PreviewProps } from './index.js'
|
||||
|
||||
export const DialogPreview = ({ docAndNode }: PreviewProps) => {
|
||||
const overlay = useRef<HTMLDivElement>(null)
|
||||
|
||||
const text = docAndNode.doc.getText()
|
||||
const dialog = safeJsonParse(text) ?? {}
|
||||
const type = dialog.type?.replace(/^minecraft:/, '')
|
||||
const footerHeight = type === 'multi_action_input_form' ? 5 : 33
|
||||
|
||||
useEffect(() => {
|
||||
function resizeHandler() {
|
||||
if (!overlay.current) return
|
||||
const width = Math.floor(overlay.current.clientWidth)
|
||||
overlay.current.style.setProperty('--dialog-px', `${width/400}px`)
|
||||
}
|
||||
resizeHandler()
|
||||
window.addEventListener('resize', resizeHandler)
|
||||
return () => window.removeEventListener('resize', resizeHandler)
|
||||
}, [overlay])
|
||||
|
||||
return <>
|
||||
<div ref={overlay} class="preview-overlay dialog-preview" style="--dialog-px: 1px;">
|
||||
<img src="/images/dialog/background.webp" alt="" draggable={false} />
|
||||
<div style={'top: 0; left: 0; width: 100%; height: 100%;'}>
|
||||
<DialogTitle title={dialog.title} />
|
||||
<div style={`display: flex; flex-direction: column; gap: ${px(10)}; align-items: center; padding-right: ${px(10)} /* MC-297972 */; overflow-y: auto; height: calc(100% - ${px(33 + footerHeight)})`}>
|
||||
<DialogBody body={dialog.body} />
|
||||
<DialogContent dialog={dialog} />
|
||||
</div>
|
||||
<div style={`bottom: 0; left: 0; width: 100%; height: ${px(footerHeight)}; display: flex; justify-content: center; align-items: center;`}>
|
||||
<DialogFooter dialog={dialog} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
function DialogTitle({ title }: { title: any }) {
|
||||
// TODO: add warning button tooltip
|
||||
return <div style={`height: ${px(33)}; display: flex; gap: ${px(10)}; justify-content: center; align-items: center`}>
|
||||
<TextComponent component={title} />
|
||||
<div class="dialog-warning-button" style={`width: ${px(20)}; height: ${px(20)};`}></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
function DialogBody({ body }: { body: any }) {
|
||||
if (!body) {
|
||||
body = []
|
||||
} else if (!Array.isArray(body)) {
|
||||
body = [body]
|
||||
}
|
||||
return <>
|
||||
{body?.map((b: any) => {
|
||||
const type = b.type?.replace(/^minecraft:/, '')
|
||||
if (type === 'plain_message') {
|
||||
// TODO: make this text wrap
|
||||
return <div style={`max-width: ${px(b.width ?? 200)}; padding: ${px(4)}`}>
|
||||
<TextComponent component={b.contents} />
|
||||
</div>
|
||||
}
|
||||
if (type == 'item') {
|
||||
// TODO: add item components
|
||||
const item = new ItemStack(Identifier.parse(b.item?.id ?? 'air'), b.show_decorations ? (b.item?.count ?? 1) : 1)
|
||||
console.log(item)
|
||||
return <div style={`display: flex; gap: ${px(2)}; align-items: center; gap: ${px(4)}`}>
|
||||
<div style={`width: ${px(b.width ?? 16)}; height: ${px(b.height ?? 16)}`}>
|
||||
<ItemDisplay item={item} tooltip={b.show_tooltip ?? true} />
|
||||
</div>
|
||||
{b.description && <div style={`max-width: ${px(b.description.width ?? 200)};`}>
|
||||
<TextComponent component={b.description.contents} />
|
||||
</div>}
|
||||
</div>
|
||||
}
|
||||
return <></>
|
||||
})}
|
||||
</>
|
||||
}
|
||||
|
||||
function DialogContent({ dialog }: { dialog: any }) {
|
||||
const type = dialog.type?.replace(/^minecraft:/, '')
|
||||
|
||||
if (type === 'dialog_list') {
|
||||
let dialogs = []
|
||||
if (Array.isArray(dialog.dialogs)) {
|
||||
dialogs = dialog.dialogs
|
||||
} else if (typeof dialog.dialogs === 'string') {
|
||||
dialogs = [dialog.dialogs]
|
||||
}
|
||||
return <ColumnsGrid columns={dialog.columns ?? 2}>
|
||||
{dialogs.map((d: any) => {
|
||||
let text = Identifier.parse(d).path.replaceAll('/', ' ').replaceAll('_', ' ')
|
||||
text = text.charAt(0).toUpperCase() + text.substring(1)
|
||||
return <Button label={text} width={dialog.button_width ?? 150} />
|
||||
})}
|
||||
</ColumnsGrid>
|
||||
}
|
||||
|
||||
if (type === 'multi_action') {
|
||||
return <ColumnsGrid columns={dialog.columns ?? 2}>
|
||||
{dialog.actions?.map((a: any) =>
|
||||
<Button label={a.label} width={a.width ?? 150} />
|
||||
) ?? []}
|
||||
</ColumnsGrid>
|
||||
}
|
||||
|
||||
if (type === 'multi_action_input_form') {
|
||||
return <>
|
||||
{dialog.inputs?.map((i: any) => <InputControl input={i} />)}
|
||||
<ColumnsGrid columns={2}>
|
||||
{dialog.actions?.map((a: any) =>
|
||||
<Button label={a.label} width={a.width ?? 150} />
|
||||
) ?? []}
|
||||
</ColumnsGrid>
|
||||
</>
|
||||
}
|
||||
|
||||
if (type === 'simple_input_form') {
|
||||
return <>
|
||||
{dialog.inputs?.map((i: any) => <InputControl input={i} />)}
|
||||
</>
|
||||
}
|
||||
|
||||
return <></>
|
||||
}
|
||||
|
||||
function DialogFooter({ dialog }: { dialog: any }) {
|
||||
const type = dialog.type?.replace(/^minecraft:/, '')
|
||||
|
||||
if (type === 'confirmation') {
|
||||
return <div style={`display: flex; gap: ${px(8)}; justify-content: center;`}>
|
||||
<Button label={dialog.yes?.label} width={dialog.yes?.width ?? 150} />
|
||||
<Button label={dialog.no?.label} width={dialog.no?.width ?? 150} />
|
||||
</div>
|
||||
}
|
||||
|
||||
if (type === 'dialog_list') {
|
||||
return <Button label={{translate: dialog.on_cancel ? 'gui.cancel' : 'gui.back'}} width={200} />
|
||||
}
|
||||
|
||||
if (type === 'multi_action') {
|
||||
return <Button label={{translate: dialog.on_cancel ? 'gui.cancel' : 'gui.back'}} width={200} />
|
||||
}
|
||||
|
||||
if (type === 'notice') {
|
||||
return <div style={`display: flex; gap: ${px(8)}; justify-content: center;`}>
|
||||
<Button label={dialog.action?.label} width={dialog.action?.width ?? 150} />
|
||||
</div>
|
||||
}
|
||||
|
||||
if (type === 'server_links') {
|
||||
return <Button label={{translate: dialog.on_cancel ? 'gui.cancel' : 'gui.back'}} width={200} />
|
||||
}
|
||||
|
||||
if (type === 'simple_input_form') {
|
||||
return <div style={`display: flex; gap: ${px(8)}; justify-content: center;`}>
|
||||
<Button label={dialog.action?.label} width={dialog.action?.width ?? 150} />
|
||||
</div>
|
||||
}
|
||||
|
||||
return <></>
|
||||
}
|
||||
|
||||
interface ColumnsGridProps {
|
||||
columns: number
|
||||
children: ComponentChild[]
|
||||
}
|
||||
function ColumnsGrid({ columns, children }: ColumnsGridProps) {
|
||||
const totalCount = children.length
|
||||
const gridCount = Math.floor(totalCount / columns) * columns
|
||||
return <div style={`padding-top: ${px(4)}; display: grid; grid-template-columns: repeat(${columns}, minmax(0, 1fr)); gap: ${px(2)}; justify-content: center;`}>
|
||||
{children.slice(0, gridCount)}
|
||||
{totalCount > gridCount && <div style={`grid-column: span ${columns}; display: flex; gap: ${px(2)}; justify-content: center; padding-top: ${px(2)} /* MC-297977 */;`}>
|
||||
{children.slice(gridCount)}
|
||||
</div>}
|
||||
</div>
|
||||
}
|
||||
|
||||
interface ButtonProps {
|
||||
label: any
|
||||
width: number
|
||||
tooltip?: any
|
||||
}
|
||||
function Button({ label, width }: ButtonProps) {
|
||||
// TODO: add tooltip
|
||||
return <div class="dialog-button" style={`width: ${px(width)}; height: ${px(20)};`}>
|
||||
<TextComponent component={label} />
|
||||
</div>
|
||||
}
|
||||
|
||||
function InputControl({ input }: { input: any }) {
|
||||
const type = input.type?.replace(/^minecraft:/, '')
|
||||
|
||||
if (type === 'boolean') {
|
||||
return <div style={`display: flex; gap: ${px(4)}; align-items: center;`}>
|
||||
<div class={`dialog-checkbox ${input.initial ? 'dialog-selected' : ''}`} style={`width: ${px(17)}; height: ${px(17)}`}></div>
|
||||
<TextComponent component={input.label} base={{color: '#e0e0e0'}} />
|
||||
</div>
|
||||
}
|
||||
|
||||
if (type === 'number_range') {
|
||||
// TODO: use label_format
|
||||
const label = {translate: 'options.generic_value', with: [input.label, input.start ?? 0]}
|
||||
return <div class="dialog-slider" style={`width: ${px(input.width ?? 200)}; height: ${px(20)};`}>
|
||||
<div class="dialog-slider-track"></div>
|
||||
<div class="dialog-slider-handle"></div>
|
||||
<div class="dialog-slider-text">
|
||||
<TextComponent component={label} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
if (type === 'single_option') {
|
||||
const firstOption = input.options?.find((o: any) => o.initial) ?? input.options?.[0]
|
||||
const optionLabel = firstOption?.display ?? firstOption?.id ?? ''
|
||||
const label = input.label_visible === false ? optionLabel : {translate: 'options.generic_value', with: [input.label ?? '', optionLabel]}
|
||||
return <Button label={label} width={input.width ?? 200} />
|
||||
}
|
||||
|
||||
if (type === 'text') {
|
||||
return <div style={`display: flex; flex-direction: column; gap: ${px(4)};`}>
|
||||
{input.label_visible !== false && <TextComponent component={input.label} />}
|
||||
<div class="dialog-edit-box" style={`width: ${px(input.width ?? 200)}; height: ${px(20)};`}>
|
||||
{input.initial && <TextComponent component={input.initial} />}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
return <></>
|
||||
}
|
||||
|
||||
function px(n: number) {
|
||||
return `calc(var(--dialog-px) * ${n})`
|
||||
}
|
||||
@@ -4,6 +4,7 @@ export * from './BiomeSourcePreview.js'
|
||||
export * from './BlockStatePreview.jsx'
|
||||
export * from './DecoratorPreview.js'
|
||||
export * from './DensityFunctionPreview.js'
|
||||
export * from './DialogPreview.js'
|
||||
export * from './ItemModelPreview.jsx'
|
||||
export * from './LootTablePreview.jsx'
|
||||
export * from './ModelPreview.jsx'
|
||||
|
||||
@@ -1876,6 +1876,116 @@ hr {
|
||||
content: '\200b';
|
||||
}
|
||||
|
||||
.dialog-preview {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.dialog-preview * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dialog-preview .text-component {
|
||||
font-size: calc(var(--dialog-px) * 12);
|
||||
}
|
||||
|
||||
.dialog-preview .text-component > .text-foreground {
|
||||
left: calc(var(--dialog-px) * -1.2);
|
||||
top: calc(var(--dialog-px) * -1.2);
|
||||
}
|
||||
|
||||
.dialog-button,
|
||||
.dialog-edit-box,
|
||||
.dialog-checkbox,
|
||||
.dialog-slider-track,
|
||||
.dialog-slider-handle {
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: -webkit-crisp-edges;
|
||||
image-rendering: crisp-edges;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.dialog-warning-button {
|
||||
flex-shrink: 0;
|
||||
background-image: url(/images/dialog/warning_button.png);
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.dialog-warning-button:hover {
|
||||
background-image: url(/images/dialog/warning_button_highlighted.png);
|
||||
}
|
||||
|
||||
.dialog-button {
|
||||
border: solid calc(var(--dialog-px)*2) #000;
|
||||
border-bottom-width: calc(var(--dialog-px)*3);
|
||||
border-image-source: url(/images/dialog/button.png);
|
||||
border-image-slice: 2 2 3 2 fill;
|
||||
border-image-repeat: repeat;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dialog-button:hover {
|
||||
border-image-source: url(/images/dialog/button_highlighted.png);
|
||||
}
|
||||
|
||||
.dialog-edit-box {
|
||||
border: solid calc(var(--dialog-px) * 1) #000;
|
||||
border-image-source: url(/images/dialog/text_field.png);
|
||||
border-image-slice: 1 fill;
|
||||
border-image-repeat: repeat;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: calc(var(--dialog-px) * 4);
|
||||
}
|
||||
|
||||
.dialog-checkbox {
|
||||
background-image: url(/images/dialog/checkbox.png);
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.dialog-checkbox.dialog-selected {
|
||||
background-image: url(/images/dialog/checkbox_selected.png);
|
||||
}
|
||||
|
||||
.dialog-slider {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dialog-slider-track {
|
||||
border: solid calc(var(--dialog-px) * 1) #000;
|
||||
border-image-source: url(/images/dialog/slider.png);
|
||||
border-image-slice: 1 fill;
|
||||
border-image-repeat: repeat;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dialog-slider-handle {
|
||||
background-image: url(/images/dialog/slider_handle.png);
|
||||
background-size: contain;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: calc(var(--dialog-px) * 8);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dialog-slider-text {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.project-files {
|
||||
background-color: var(--background-2);
|
||||
color: var(--text-2);
|
||||
|
||||