feat(ui): add environment media playlists
This commit is contained in:
@@ -4,14 +4,20 @@ export type EnvironmentSurfaceId = "home" | RootId;
|
||||
export type EnvironmentMediaKind = "image" | "video";
|
||||
export type EnvironmentMediaSource = "file" | "url";
|
||||
|
||||
export interface EnvironmentBackground {
|
||||
enabled: boolean;
|
||||
export interface EnvironmentMediaItem {
|
||||
id: string;
|
||||
source: EnvironmentMediaSource;
|
||||
url: string | null;
|
||||
mediaKind: EnvironmentMediaKind | null;
|
||||
fileName: string | null;
|
||||
}
|
||||
|
||||
export interface EnvironmentBackground {
|
||||
enabled: boolean;
|
||||
imageDurationSeconds: number;
|
||||
items: EnvironmentMediaItem[];
|
||||
}
|
||||
|
||||
export interface EnvironmentPage {
|
||||
headerLabel: string;
|
||||
eyebrow: string;
|
||||
@@ -29,6 +35,7 @@ export interface EnvironmentSettings {
|
||||
|
||||
export interface UploadedEnvironmentMedia {
|
||||
surfaceId: EnvironmentSurfaceId;
|
||||
itemId: string;
|
||||
url: string;
|
||||
fileName: string;
|
||||
mediaKind: EnvironmentMediaKind;
|
||||
@@ -47,9 +54,20 @@ const surfaceIds: readonly EnvironmentSurfaceId[] = [
|
||||
"polygon",
|
||||
];
|
||||
|
||||
export const defaultEnvironmentImageDurationSeconds = 10;
|
||||
export const maxEnvironmentMediaItems = 24;
|
||||
|
||||
function emptyBackground(): EnvironmentBackground {
|
||||
return {
|
||||
enabled: false,
|
||||
imageDurationSeconds: defaultEnvironmentImageDurationSeconds,
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function createEnvironmentMediaItem(): EnvironmentMediaItem {
|
||||
return {
|
||||
id: `media-${crypto.randomUUID()}`,
|
||||
source: "file",
|
||||
url: null,
|
||||
mediaKind: null,
|
||||
@@ -57,6 +75,10 @@ function emptyBackground(): EnvironmentBackground {
|
||||
};
|
||||
}
|
||||
|
||||
export function inferEnvironmentMediaKind(url: string): EnvironmentMediaKind {
|
||||
return /\.(mp4|webm|mov)(?:[?#].*)?$/i.test(url) ? "video" : "image";
|
||||
}
|
||||
|
||||
const defaultQuickActions: Record<
|
||||
EnvironmentSurfaceId,
|
||||
readonly [string | null, string | null]
|
||||
@@ -110,26 +132,57 @@ function requireString(
|
||||
return value;
|
||||
}
|
||||
|
||||
function decodeMediaItem(value: unknown, path: string): EnvironmentMediaItem {
|
||||
const record = requireRecord(value, path);
|
||||
if (
|
||||
typeof record.id !== "string"
|
||||
|| !/^media-[a-z0-9-]{1,58}$/.test(record.id)
|
||||
) {
|
||||
throw new Error(`${path}.id не поддерживается.`);
|
||||
}
|
||||
if (!["file", "url"].includes(String(record.source))) {
|
||||
throw new Error(`${path}.source не поддерживается.`);
|
||||
}
|
||||
if (!["image", "video"].includes(String(record.media_kind))) {
|
||||
throw new Error(`${path}.media_kind не поддерживается.`);
|
||||
}
|
||||
return {
|
||||
id: record.id,
|
||||
source: record.source as EnvironmentMediaSource,
|
||||
url: requireString(record.url, `${path}.url`),
|
||||
mediaKind: record.media_kind as EnvironmentMediaKind,
|
||||
fileName: requireString(record.file_name, `${path}.file_name`, true),
|
||||
};
|
||||
}
|
||||
|
||||
function decodeBackground(value: unknown, path: string): EnvironmentBackground {
|
||||
const record = requireRecord(value, path);
|
||||
if (typeof record.enabled !== "boolean") {
|
||||
throw new Error(`${path}.enabled должен быть boolean.`);
|
||||
}
|
||||
if (!["file", "url"].includes(String(record.source))) {
|
||||
throw new Error(`${path}.source не поддерживается.`);
|
||||
if (
|
||||
typeof record.image_duration_seconds !== "number"
|
||||
|| !Number.isSafeInteger(record.image_duration_seconds)
|
||||
|| record.image_duration_seconds < 1
|
||||
|| record.image_duration_seconds > 300
|
||||
) {
|
||||
throw new Error(`${path}.image_duration_seconds некорректен.`);
|
||||
}
|
||||
if (
|
||||
record.media_kind !== null
|
||||
&& !["image", "video"].includes(String(record.media_kind))
|
||||
!Array.isArray(record.items)
|
||||
|| record.items.length > maxEnvironmentMediaItems
|
||||
) {
|
||||
throw new Error(`${path}.media_kind не поддерживается.`);
|
||||
throw new Error(`${path}.items некорректен.`);
|
||||
}
|
||||
const items = record.items.map((item, index) =>
|
||||
decodeMediaItem(item, `${path}.items[${index}]`));
|
||||
if (new Set(items.map((item) => item.id)).size !== items.length) {
|
||||
throw new Error(`${path}.items содержит повторяющиеся id.`);
|
||||
}
|
||||
return {
|
||||
enabled: record.enabled,
|
||||
source: record.source as EnvironmentMediaSource,
|
||||
url: requireString(record.url, `${path}.url`, true),
|
||||
mediaKind: record.media_kind as EnvironmentMediaKind | null,
|
||||
fileName: requireString(record.file_name, `${path}.file_name`, true),
|
||||
imageDurationSeconds: record.image_duration_seconds,
|
||||
items,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -156,7 +209,7 @@ function decodePage(value: unknown, path: string): EnvironmentPage {
|
||||
|
||||
export function decodeEnvironmentSettings(value: unknown): EnvironmentSettings {
|
||||
const record = requireRecord(value, "environment");
|
||||
if (record.schema_version !== "missioncore.operator-environment/v2") {
|
||||
if (record.schema_version !== "missioncore.operator-environment/v3") {
|
||||
throw new Error("Версия настроек окружения не поддерживается.");
|
||||
}
|
||||
if (
|
||||
@@ -190,10 +243,14 @@ export function encodeEnvironmentSettings(settings: EnvironmentSettings): unknow
|
||||
secondary_workspace_id: page.secondaryWorkspaceId,
|
||||
background: {
|
||||
enabled: page.background.enabled,
|
||||
source: page.background.source,
|
||||
url: page.background.url,
|
||||
media_kind: page.background.mediaKind,
|
||||
file_name: page.background.fileName,
|
||||
image_duration_seconds: page.background.imageDurationSeconds,
|
||||
items: page.background.items.map((item) => ({
|
||||
id: item.id,
|
||||
source: item.source,
|
||||
url: item.url,
|
||||
media_kind: item.mediaKind,
|
||||
file_name: item.fileName,
|
||||
})),
|
||||
},
|
||||
}];
|
||||
})),
|
||||
@@ -204,12 +261,18 @@ export function decodeUploadedEnvironmentMedia(
|
||||
value: unknown,
|
||||
): UploadedEnvironmentMedia {
|
||||
const record = requireRecord(value, "environment media");
|
||||
if (record.schema_version !== "missioncore.operator-environment-media/v1") {
|
||||
if (record.schema_version !== "missioncore.operator-environment-media/v2") {
|
||||
throw new Error("Версия загруженного media не поддерживается.");
|
||||
}
|
||||
if (!surfaceIds.includes(record.surface_id as EnvironmentSurfaceId)) {
|
||||
throw new Error("Экран загруженного media не поддерживается.");
|
||||
}
|
||||
if (
|
||||
typeof record.item_id !== "string"
|
||||
|| !/^media-[a-z0-9-]{1,58}$/.test(record.item_id)
|
||||
) {
|
||||
throw new Error("Идентификатор загруженного media не поддерживается.");
|
||||
}
|
||||
if (!["image", "video"].includes(String(record.media_kind))) {
|
||||
throw new Error("Тип загруженного media не поддерживается.");
|
||||
}
|
||||
@@ -222,6 +285,7 @@ export function decodeUploadedEnvironmentMedia(
|
||||
}
|
||||
return {
|
||||
surfaceId: record.surface_id as EnvironmentSurfaceId,
|
||||
itemId: record.item_id,
|
||||
url: requireString(record.url, "environment media.url")!,
|
||||
fileName: requireString(record.file_name, "environment media.file_name")!,
|
||||
mediaKind: record.media_kind as EnvironmentMediaKind,
|
||||
@@ -240,7 +304,12 @@ export function cloneEnvironmentSettings(
|
||||
surfaceId,
|
||||
{
|
||||
...settings.pages[surfaceId],
|
||||
background: { ...settings.pages[surfaceId].background },
|
||||
background: {
|
||||
...settings.pages[surfaceId].background,
|
||||
items: settings.pages[surfaceId].background.items.map((item) => ({
|
||||
...item,
|
||||
})),
|
||||
},
|
||||
},
|
||||
])) as Record<EnvironmentSurfaceId, EnvironmentPage>,
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ interface EnvironmentSettingsController {
|
||||
save: (draft: EnvironmentSettings) => Promise<EnvironmentSettings>;
|
||||
upload: (
|
||||
surfaceId: EnvironmentSurfaceId,
|
||||
itemId: string,
|
||||
file: File,
|
||||
) => Promise<UploadedEnvironmentMedia>;
|
||||
}
|
||||
@@ -101,17 +102,21 @@ export function useEnvironmentSettings(): EnvironmentSettingsController {
|
||||
|
||||
const upload = useCallback(async (
|
||||
surfaceId: EnvironmentSurfaceId,
|
||||
itemId: string,
|
||||
file: File,
|
||||
) => {
|
||||
const response = await fetch(`/api/v1/environment/media/${surfaceId}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": file.type || "application/octet-stream",
|
||||
"X-NODEDC-File-Name": encodeURIComponent(file.name),
|
||||
const response = await fetch(
|
||||
`/api/v1/environment/media/${surfaceId}/${itemId}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": file.type || "application/octet-stream",
|
||||
"X-NODEDC-File-Name": encodeURIComponent(file.name),
|
||||
},
|
||||
body: file,
|
||||
},
|
||||
body: file,
|
||||
});
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(await responseError(
|
||||
response,
|
||||
|
||||
Reference in New Issue
Block a user