feat(ui): add environment media playlists
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user