feat(ui): add environment media playlists

This commit is contained in:
DCCONSTRUCTIONS
2026-07-29 14:20:19 +03:00
parent c455301764
commit 8b1f109b09
11 changed files with 1076 additions and 138 deletions
@@ -2,7 +2,6 @@ import { useEffect, useMemo, useState } from "react";
import {
Button,
FeatureSettingsWindow,
MediaSourceField,
Select,
SettingsCard,
Switch,
@@ -14,12 +13,12 @@ import {
import {
cloneEnvironmentSettings,
type EnvironmentBackground,
type EnvironmentMediaKind,
type EnvironmentPage,
type EnvironmentSettings,
type EnvironmentSurfaceId,
type UploadedEnvironmentMedia,
} from "../core/environment/environmentSettings";
import { EnvironmentMediaPlaylistEditor } from "./EnvironmentMediaPlaylistEditor";
import {
roots,
workspaces,
@@ -35,14 +34,11 @@ interface EnvironmentSettingsWindowProps {
onSave: (settings: EnvironmentSettings) => Promise<EnvironmentSettings>;
onUpload: (
surfaceId: EnvironmentSurfaceId,
itemId: string,
file: File,
) => Promise<UploadedEnvironmentMedia>;
}
function inferMediaKind(url: string): EnvironmentMediaKind {
return /\.(mp4|webm|mov|m4v)(?:[?#].*)?$/i.test(url) ? "video" : "image";
}
function patchPage(
draft: EnvironmentSettings,
surfaceId: EnvironmentSurfaceId,
@@ -108,8 +104,6 @@ export function EnvironmentSettingsWindow({
description: `Стартовая страница раздела «${draft.pages[root.id].title}»`,
})),
], [draft.pages]);
const previewKind = selectedBackground.mediaKind
?? (selectedBackground.url ? inferMediaKind(selectedBackground.url) : null);
const quickActionWorkspaces = useMemo(
() => surfaceId === "home"
? workspaces.filter((workspace) => !workspace.internalOnly)
@@ -138,28 +132,6 @@ export function EnvironmentSettingsWindow({
setDraft((current) => patchPage(current, surfaceId, patch));
};
const uploadFile = async (file?: File) => {
if (!file) return;
setUploading(true);
setLocalError(null);
try {
const uploaded = await onUpload(surfaceId, file);
setDraft((current) => patchBackground(current, surfaceId, {
enabled: true,
source: "file",
url: uploaded.url,
mediaKind: uploaded.mediaKind,
fileName: uploaded.fileName,
}));
} catch (reason) {
setLocalError(reason instanceof Error
? reason.message
: "Не удалось загрузить фон окружения.");
} finally {
setUploading(false);
}
};
const save = async () => {
const invalid = Object.entries(draft.pages).find(([, page]) => (
!page.headerLabel.trim()
@@ -178,6 +150,26 @@ export function EnvironmentSettingsWindow({
setLocalError("Быстрые кнопки должны вести на разные рабочие поверхности.");
return;
}
const invalidMedia = Object.entries(draft.pages).find(([, page]) => (
(page.background.enabled && !page.background.items.length)
|| page.background.items.some((item) => {
if (!item.url || !item.mediaKind) return true;
if (item.source !== "url") return false;
try {
const parsed = new URL(item.url);
return !["http:", "https:"].includes(parsed.protocol);
} catch {
return true;
}
})
));
if (invalidMedia) {
setSurfaceId(invalidMedia[0] as EnvironmentSurfaceId);
setLocalError(
"Каждый элемент фона должен содержать загруженный файл или прямой HTTP(S) URL.",
);
return;
}
setLocalError(null);
try {
await onSave({
@@ -253,8 +245,8 @@ export function EnvironmentSettingsWindow({
checked={selectedBackground.enabled}
label="Показывать фон"
onChange={(enabled) => {
if (enabled && !selectedBackground.url) {
setLocalError("Сначала загрузите файл или укажите URL.");
if (enabled && !selectedBackground.items.length) {
setLocalError("Сначала добавьте медиаконтент.");
return;
}
setLocalError(null);
@@ -345,29 +337,18 @@ export function EnvironmentSettingsWindow({
</div>
</div>
<MediaSourceField
label="Видео / картинка"
kindLabel={previewKind ?? "media"}
source={selectedBackground.source}
url={selectedBackground.url ?? ""}
fileName={selectedBackground.fileName}
uploading={uploading}
previewSrc={selectedBackground.url}
previewKind={previewKind}
accept="image/png,image/jpeg,image/gif,image/webp,image/avif,video/mp4,video/webm,video/quicktime,.png,.jpg,.jpeg,.gif,.webp,.avif,.mp4,.webm,.mov"
path={`${surfaceId}.background → server environment media`}
hint="Файл сохраняется в Mission Core data root. URL должен быть доступен браузеру по HTTP(S)."
<EnvironmentMediaPlaylistEditor
surfaceId={surfaceId}
background={selectedBackground}
disabled={busy}
error={localError ?? error}
onSourceChange={(source) => setDraft((current) =>
patchBackground(current, surfaceId, { source }))}
onUrlChange={(url) => setDraft((current) =>
patchBackground(current, surfaceId, {
source: "url",
url: url || null,
mediaKind: url ? inferMediaKind(url) : null,
fileName: null,
}))}
onFileChange={uploadFile}
onBusyChange={setUploading}
onChange={(background) => {
setLocalError(null);
setDraft((current) =>
patchBackground(current, surfaceId, background));
}}
onUpload={onUpload}
/>
</div>
</SettingsCard>