Add sound selector to widgets

This commit is contained in:
2026-03-12 16:14:19 +01:00
parent 5a458d7fca
commit bda52e4390
5 changed files with 119 additions and 1 deletions

View File

@@ -10,6 +10,7 @@ RUN apk add --no-cache php83 php83-fpm php83-curl php83-ctype
RUN echo 'clear_env = no' >> /etc/php83/php-fpm.d/www.conf
COPY --from=build /app/dist /usr/share/nginx/html
COPY deploy/php/media_proxy.php /usr/share/nginx/html/media_proxy.php
COPY deploy/php/sounds_list.php /usr/share/nginx/html/sounds_list.php
COPY client/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["sh", "-c", "php-fpm83 && exec nginx -g 'daemon off;'"]

View File

View File

@@ -47,6 +47,8 @@ type EditorDeps = {
updateStatus: (message: string) => void;
sfxUiBlip: () => void;
sfxUiCancel: () => void;
openSoundPropertyPicker?: (item: WorldItem, key: string) => void;
previewSound?: (soundPath: string) => void;
};
/**
@@ -187,9 +189,13 @@ export function createItemPropertyEditor(deps: EditorDeps): {
deps.openItemPropertyOptionSelect(item, selectedKey);
return;
}
if (metadata?.valueType === 'sound' && deps.openSoundPropertyPicker) {
deps.openSoundPropertyPicker(item, selectedKey);
return;
}
deps.state.mode = 'itemPropertyEdit';
deps.state.editingPropertyKey = selectedKey;
const selectedMetadata = deps.getItemPropertyMetadata(item.type, selectedKey);
const selectedMetadata = metadata;
deps.state.nicknameInput =
selectedKey === 'title'
? item.title
@@ -369,6 +375,13 @@ export function createItemPropertyEditor(deps: EditorDeps): {
const nextIndex = (deps.state.itemPropertyOptionIndex + delta + length * 1000) % length;
deps.state.itemPropertyOptionIndex = nextIndex;
deps.updateStatus(deps.state.itemPropertyOptionValues[nextIndex]);
const pageItem = deps.state.items.get(itemId!);
if (pageItem) {
const pageMeta = deps.getItemPropertyMetadata(pageItem.type, propertyKey);
if (pageMeta?.valueType === 'sound') {
deps.previewSound?.(deps.state.itemPropertyOptionValues[nextIndex]);
}
}
deps.sfxUiBlip();
return;
}
@@ -383,6 +396,13 @@ export function createItemPropertyEditor(deps: EditorDeps): {
if (control.type === 'move') {
deps.state.itemPropertyOptionIndex = control.index;
deps.updateStatus(deps.state.itemPropertyOptionValues[deps.state.itemPropertyOptionIndex]);
const moveItem = deps.state.items.get(itemId!);
if (moveItem) {
const moveMeta = deps.getItemPropertyMetadata(moveItem.type, propertyKey);
if (moveMeta?.valueType === 'sound') {
deps.previewSound?.(deps.state.itemPropertyOptionValues[deps.state.itemPropertyOptionIndex]);
}
}
deps.sfxUiBlip();
return;
}

View File

@@ -1010,6 +1010,60 @@ function openItemPropertyOptionSelect(item: WorldItem, key: string): void {
audio.sfxUiBlip();
}
/** Fetches the list of widget sounds from the server. Returns [] on error. */
async function fetchWidgetSounds(): Promise<string[]> {
try {
const response = await fetch('/sounds_list.php');
if (!response.ok) return [];
const names = (await response.json()) as string[];
if (!Array.isArray(names)) return [];
return names.map((name: string) => `sounds/widgets/${name}`);
} catch {
return [];
}
}
let previewDebounceTimer: ReturnType<typeof setTimeout> | null = null;
/** Plays a sound preview with debounce, for use while navigating sound picker. */
function previewSound(soundPath: string): void {
if (previewDebounceTimer !== null) {
clearTimeout(previewDebounceTimer);
}
previewDebounceTimer = setTimeout(() => {
previewDebounceTimer = null;
if (soundPath) {
void audio.playSample(soundPath, 0.7);
}
}, 200);
}
/** Opens the sound picker for a sound-typed item property, falling back to text edit if no sounds are found. */
async function openSoundPropertyPicker(item: WorldItem, key: string): Promise<void> {
updateStatus('Loading sounds...');
const sounds = await fetchWidgetSounds();
if (sounds.length === 0) {
state.mode = 'itemPropertyEdit';
state.editingPropertyKey = key;
const currentValue = String(item.params[key] ?? '');
state.nicknameInput = currentValue;
state.cursorPos = currentValue.length;
replaceTextOnNextType = true;
updateStatus(`Edit ${itemPropertyLabel(key)}: ${currentValue}`);
audio.sfxUiBlip();
return;
}
const options = ['', ...sounds];
state.mode = 'itemPropertyOptionSelect';
state.editingPropertyKey = key;
state.itemPropertyOptionValues = options;
const currentValue = String(item.params[key] ?? '').trim();
const currentIndex = options.indexOf(currentValue);
state.itemPropertyOptionIndex = currentIndex >= 0 ? currentIndex : 0;
updateStatus(`Select ${itemPropertyLabel(key)}: ${options[state.itemPropertyOptionIndex] || 'none'}`);
audio.sfxUiBlip();
}
/** Returns the active text-input max length for the current UI mode, if applicable. */
function textInputMaxLengthForMode(mode: typeof state.mode): number | null {
if (mode === 'nickname') return NICKNAME_MAX_LENGTH;
@@ -2573,6 +2627,8 @@ const itemPropertyEditor = createItemPropertyEditor({
updateStatus,
sfxUiBlip: () => audio.sfxUiBlip(),
sfxUiCancel: () => audio.sfxUiCancel(),
openSoundPropertyPicker: (item, key) => { void openSoundPropertyPicker(item, key); },
previewSound,
});
/** Handles nickname edit mode submission/cancel and text editing keys. */