From 2ecdbd1719fe8daf768c6855387c51b392b33d0d Mon Sep 17 00:00:00 2001
From: Ivan <8611739+IRBorisov@users.noreply.github.com>
Date: Wed, 13 Aug 2025 16:20:30 +0300
Subject: [PATCH] F: Rework video embedding. Add VK Video
---
.vscode/settings.json | 1 +
rsconcept/frontend/src/app/global-dialogs.tsx | 5 ++
rsconcept/frontend/src/components/icons.tsx | 1 +
.../src/components/view/embed-vkvideo.tsx | 42 ++++++++++++
.../features/help/components/badge-video.tsx | 30 +++++++++
.../dialogs/dlg-show-video/dlg-show-video.tsx | 64 +++++++++++++++++++
.../help/dialogs/dlg-show-video/index.tsx | 1 +
.../help/dialogs/dlg-show-video/tab-vk.tsx | 10 +++
.../dialogs/dlg-show-video/tab-youtube.tsx | 10 +++
.../src/features/help/items/help-main.tsx | 7 +-
.../src/features/help/items/help-rslang.tsx | 21 +-----
rsconcept/frontend/src/stores/dialogs.ts | 7 +-
rsconcept/frontend/src/stores/preferences.ts | 17 ++++-
rsconcept/frontend/src/utils/constants.ts | 22 +++++--
14 files changed, 210 insertions(+), 28 deletions(-)
create mode 100644 rsconcept/frontend/src/components/view/embed-vkvideo.tsx
create mode 100644 rsconcept/frontend/src/features/help/components/badge-video.tsx
create mode 100644 rsconcept/frontend/src/features/help/dialogs/dlg-show-video/dlg-show-video.tsx
create mode 100644 rsconcept/frontend/src/features/help/dialogs/dlg-show-video/index.tsx
create mode 100644 rsconcept/frontend/src/features/help/dialogs/dlg-show-video/tab-vk.tsx
create mode 100644 rsconcept/frontend/src/features/help/dialogs/dlg-show-video/tab-youtube.tsx
diff --git a/.vscode/settings.json b/.vscode/settings.json
index b6c6bf6d..329f582d 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -177,6 +177,7 @@
"Upvote",
"Viewset",
"viewsets",
+ "vkvideo",
"wordform",
"Wordforms",
"XCSDATN",
diff --git a/rsconcept/frontend/src/app/global-dialogs.tsx b/rsconcept/frontend/src/app/global-dialogs.tsx
index bf40d824..d2d2981e 100644
--- a/rsconcept/frontend/src/app/global-dialogs.tsx
+++ b/rsconcept/frontend/src/app/global-dialogs.tsx
@@ -4,6 +4,9 @@ import React from 'react';
import { DialogType, useDialogsStore } from '@/stores/dialogs';
+const DlgShowVideo = React.lazy(() =>
+ import('@/features/help/dialogs/dlg-show-video').then(module => ({ default: module.DlgShowVideo }))
+);
const DlgChangeInputSchema = React.lazy(() =>
import('@/features/oss/dialogs/dlg-change-input-schema').then(module => ({ default: module.DlgChangeInputSchema }))
);
@@ -161,6 +164,8 @@ export const GlobalDialogs = () => {
return null;
}
switch (active) {
+ case DialogType.SHOW_VIDEO:
+ return ;
case DialogType.CONSTITUENTA_TEMPLATE:
return ;
case DialogType.CREATE_CONSTITUENTA:
diff --git a/rsconcept/frontend/src/components/icons.tsx b/rsconcept/frontend/src/components/icons.tsx
index 1fb83dd0..aff85178 100644
--- a/rsconcept/frontend/src/components/icons.tsx
+++ b/rsconcept/frontend/src/components/icons.tsx
@@ -39,6 +39,7 @@ export { RiMenuFoldFill as IconMenuFold } from 'react-icons/ri';
export { RiMenuUnfoldFill as IconMenuUnfold } from 'react-icons/ri';
export { LuMoon as IconDarkTheme } from 'react-icons/lu';
export { LuSun as IconLightTheme } from 'react-icons/lu';
+export { IoVideocamOutline as IconVideo } from 'react-icons/io5';
export { LuFolderTree as IconFolderTree } from 'react-icons/lu';
export { LuFolder as IconFolder } from 'react-icons/lu';
export { LuFolderSearch as IconFolderSearch } from 'react-icons/lu';
diff --git a/rsconcept/frontend/src/components/view/embed-vkvideo.tsx b/rsconcept/frontend/src/components/view/embed-vkvideo.tsx
new file mode 100644
index 00000000..be5450b5
--- /dev/null
+++ b/rsconcept/frontend/src/components/view/embed-vkvideo.tsx
@@ -0,0 +1,42 @@
+interface EmbedVKVideoProps {
+ /** Video ID to embed. */
+ videoID: string;
+
+ /** Display height in pixels. */
+ pxHeight: number;
+
+ /** Display width in pixels. */
+ pxWidth?: number;
+}
+
+/**
+ * Embeds a YouTube video into the page using the given video ID and dimensions.
+ */
+export function EmbedVKVideo({ videoID, pxHeight, pxWidth }: EmbedVKVideoProps) {
+ if (!pxWidth) {
+ pxWidth = (pxHeight * 16) / 9;
+ }
+ return (
+
+
+
+ );
+}
diff --git a/rsconcept/frontend/src/features/help/components/badge-video.tsx b/rsconcept/frontend/src/features/help/components/badge-video.tsx
new file mode 100644
index 00000000..edb80178
--- /dev/null
+++ b/rsconcept/frontend/src/features/help/components/badge-video.tsx
@@ -0,0 +1,30 @@
+'use client';
+
+import { IconVideo } from '@/components/icons';
+import { type Styling } from '@/components/props';
+import { cn } from '@/components/utils';
+import { useDialogsStore } from '@/stores/dialogs';
+import { globalIDs, type IVideo } from '@/utils/constants';
+
+interface BadgeVideoProps extends Styling {
+ video: IVideo;
+}
+
+/** Displays a badge with a video icon to click and open the video. */
+export function BadgeVideo({ video, className, ...restProps }: BadgeVideoProps) {
+ const showVideo = useDialogsStore(state => state.showVideo);
+
+ function handleShowExplication() {
+ showVideo({ video: video });
+ }
+
+ return (
+
+ );
+}
diff --git a/rsconcept/frontend/src/features/help/dialogs/dlg-show-video/dlg-show-video.tsx b/rsconcept/frontend/src/features/help/dialogs/dlg-show-video/dlg-show-video.tsx
new file mode 100644
index 00000000..a983e00c
--- /dev/null
+++ b/rsconcept/frontend/src/features/help/dialogs/dlg-show-video/dlg-show-video.tsx
@@ -0,0 +1,64 @@
+'use client';
+
+import { TabList, TabPanel, Tabs } from 'react-tabs';
+
+import { ModalView } from '@/components/modal';
+import { TabLabel } from '@/components/tabs';
+import { useWindowSize } from '@/hooks/use-window-size';
+import { useDialogsStore } from '@/stores/dialogs';
+import { usePreferencesStore } from '@/stores/preferences';
+import { type IVideo } from '@/utils/constants';
+
+import { TabVK } from './tab-vk';
+import { TabYoutube } from './tab-youtube';
+
+export const TabID = {
+ VK: 0,
+ YOUTUBE: 1
+} as const;
+export type TabID = (typeof TabID)[keyof typeof TabID];
+
+export interface DlgShowVideoProps {
+ video: IVideo;
+}
+
+export function DlgShowVideo() {
+ const preferredPlayer = usePreferencesStore(state => state.preferredPlayer);
+ const setPreferredPlayer = usePreferencesStore(state => state.setPreferredPlayer);
+ const activeTab = preferredPlayer === 'vk' ? TabID.VK : TabID.YOUTUBE;
+ const { video } = useDialogsStore(state => state.props as DlgShowVideoProps);
+ const windowSize = useWindowSize();
+
+ const videoHeight = (() => {
+ const viewH = windowSize.height ?? 0;
+ const viewW = windowSize.width ?? 0;
+ const availableWidth = viewW - 80;
+ return Math.min(Math.max(viewH - 150, 300), Math.floor((availableWidth * 9) / 16));
+ })();
+
+ function setActiveTab(newTab: TabID) {
+ setPreferredPlayer(newTab === TabID.VK ? 'vk' : 'youtube');
+ }
+
+ return (
+
+ setActiveTab(index as TabID)}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/rsconcept/frontend/src/features/help/dialogs/dlg-show-video/index.tsx b/rsconcept/frontend/src/features/help/dialogs/dlg-show-video/index.tsx
new file mode 100644
index 00000000..9a4165cb
--- /dev/null
+++ b/rsconcept/frontend/src/features/help/dialogs/dlg-show-video/index.tsx
@@ -0,0 +1 @@
+export { DlgShowVideo as DlgShowVideo } from './dlg-show-video';
diff --git a/rsconcept/frontend/src/features/help/dialogs/dlg-show-video/tab-vk.tsx b/rsconcept/frontend/src/features/help/dialogs/dlg-show-video/tab-vk.tsx
new file mode 100644
index 00000000..df039f20
--- /dev/null
+++ b/rsconcept/frontend/src/features/help/dialogs/dlg-show-video/tab-vk.tsx
@@ -0,0 +1,10 @@
+import { EmbedVKVideo } from '@/components/view/embed-vkvideo';
+
+interface TabVKProps {
+ videoHeight: number;
+ videoID: string;
+}
+
+export function TabVK({ videoHeight, videoID }: TabVKProps) {
+ return ;
+}
diff --git a/rsconcept/frontend/src/features/help/dialogs/dlg-show-video/tab-youtube.tsx b/rsconcept/frontend/src/features/help/dialogs/dlg-show-video/tab-youtube.tsx
new file mode 100644
index 00000000..b092d697
--- /dev/null
+++ b/rsconcept/frontend/src/features/help/dialogs/dlg-show-video/tab-youtube.tsx
@@ -0,0 +1,10 @@
+import { EmbedYoutube } from '@/components/view';
+
+interface TabYoutubeProps {
+ videoHeight: number;
+ videoID: string;
+}
+
+export function TabYoutube({ videoHeight, videoID }: TabYoutubeProps) {
+ return ;
+}
diff --git a/rsconcept/frontend/src/features/help/items/help-main.tsx b/rsconcept/frontend/src/features/help/items/help-main.tsx
index 8c04cd31..ca825b59 100644
--- a/rsconcept/frontend/src/features/help/items/help-main.tsx
+++ b/rsconcept/frontend/src/features/help/items/help-main.tsx
@@ -1,4 +1,5 @@
import { TextURL } from '@/components/control';
+import { IconVideo } from '@/components/icons';
import { external_urls, prefixes } from '@/utils/constants';
import { LinkTopic } from '../components/link-topic';
@@ -11,7 +12,7 @@ export function HelpMain() {
Портал
Портал позволяет анализировать предметные области, формально записывать системы определений и синтезировать их с
- помощью математического аппарата
+ помощью математического аппарата .
Такие системы называются и состоят из
@@ -19,6 +20,10 @@ export function HelpMain() {
определения. Концептуальные схемы могут связываться путем синтеза в{' '}
.
+
+ Значок при нажатии отображает видео о различных темах и подробностях
+ работы Портала. Просмотр видео доступен на Youtube и ВКонтакте.
+
Разделы Справки
diff --git a/rsconcept/frontend/src/features/help/items/help-rslang.tsx b/rsconcept/frontend/src/features/help/items/help-rslang.tsx
index 76c8176e..74acc476 100644
--- a/rsconcept/frontend/src/features/help/items/help-rslang.tsx
+++ b/rsconcept/frontend/src/features/help/items/help-rslang.tsx
@@ -1,21 +1,10 @@
-import { EmbedYoutube } from '@/components/view';
-import { useWindowSize } from '@/hooks/use-window-size';
-import { external_urls, youtube } from '@/utils/constants';
+import { external_urls, videos } from '@/utils/constants';
+import { BadgeVideo } from '../components/badge-video';
import { Subtopics } from '../components/subtopics';
import { HelpTopic } from '../models/help-topic';
export function HelpRSLang() {
- const windowSize = useWindowSize();
- const isSmall = windowSize.isSmall;
-
- const videoHeight = (() => {
- const viewH = windowSize.height ?? 0;
- const viewW = windowSize.width ?? 0;
- const availableWidth = viewW - (isSmall ? 35 : 310);
- return Math.min(1080, Math.max(viewH - 450, 300), Math.floor((availableWidth * 9) / 16));
- })();
-
// prettier-ignore
return (
-
diff --git a/rsconcept/frontend/src/stores/dialogs.ts b/rsconcept/frontend/src/stores/dialogs.ts
index c2e559ca..30be21a3 100644
--- a/rsconcept/frontend/src/stores/dialogs.ts
+++ b/rsconcept/frontend/src/stores/dialogs.ts
@@ -1,6 +1,7 @@
import { create } from 'zustand';
import { type DlgCreatePromptTemplateProps } from '@/features/ai/dialogs/dlg-create-prompt-template';
+import { type DlgShowVideoProps } from '@/features/help/dialogs/dlg-show-video/dlg-show-video';
import { type DlgChangeLocationProps } from '@/features/library/dialogs/dlg-change-location';
import { type DlgCloneLibraryItemProps } from '@/features/library/dialogs/dlg-clone-library-item';
import { type DlgCreateVersionProps } from '@/features/library/dialogs/dlg-create-version';
@@ -73,7 +74,9 @@ export const DialogType = {
IMPORT_SCHEMA: 31,
AI_PROMPT: 32,
- CREATE_PROMPT_TEMPLATE: 33
+ CREATE_PROMPT_TEMPLATE: 33,
+
+ SHOW_VIDEO: 34
} as const;
export type DialogType = (typeof DialogType)[keyof typeof DialogType];
@@ -86,6 +89,7 @@ interface DialogsStore {
props: unknown;
hideDialog: () => void;
+ showVideo: (props: DlgShowVideoProps) => void;
showCstTemplate: (props: DlgCstTemplateProps) => void;
showCreateCst: (props: DlgCreateCstProps) => void;
showCreateBlock: (props: DlgCreateBlockProps) => void;
@@ -131,6 +135,7 @@ export const useDialogsStore = create()(set => ({
});
},
+ showVideo: props => set({ active: DialogType.SHOW_VIDEO, props: props }),
showCstTemplate: props => set({ active: DialogType.CONSTITUENTA_TEMPLATE, props: props }),
showCreateCst: props => set({ active: DialogType.CREATE_CONSTITUENTA, props: props }),
showCreateOperation: props => set({ active: DialogType.CREATE_SYNTHESIS, props: props }),
diff --git a/rsconcept/frontend/src/stores/preferences.ts b/rsconcept/frontend/src/stores/preferences.ts
index 6ef844d7..382a0924 100644
--- a/rsconcept/frontend/src/stores/preferences.ts
+++ b/rsconcept/frontend/src/stores/preferences.ts
@@ -4,6 +4,11 @@ import { persist } from 'zustand/middleware';
import { PARAMETER } from '@/utils/constants';
+export const videoPlayerTypes = ['vk', 'youtube'] as const;
+
+/** Represents video player type. */
+export type VideoPlayerType = (typeof videoPlayerTypes)[number];
+
interface PreferencesStore {
darkMode: boolean;
toggleDarkMode: () => void;
@@ -31,6 +36,9 @@ interface PreferencesStore {
showOssSidePanel: boolean;
toggleShowOssSidePanel: () => void;
+
+ preferredPlayer: VideoPlayerType;
+ setPreferredPlayer: (value: VideoPlayerType) => void;
}
export const usePreferencesStore = create()(
@@ -66,7 +74,7 @@ export const usePreferencesStore = create()(
adminMode: false,
toggleAdminMode: () => set(state => ({ adminMode: !state.adminMode })),
- libraryPagination: 50,
+ libraryPagination: 20,
setLibraryPagination: value => set({ libraryPagination: value }),
showCstSideList: true,
@@ -82,10 +90,13 @@ export const usePreferencesStore = create()(
toggleShowExpressionControls: () => set(state => ({ showExpressionControls: !state.showExpressionControls })),
showOssSidePanel: false,
- toggleShowOssSidePanel: () => set(state => ({ showOssSidePanel: !state.showOssSidePanel }))
+ toggleShowOssSidePanel: () => set(state => ({ showOssSidePanel: !state.showOssSidePanel })),
+
+ preferredPlayer: 'vk',
+ setPreferredPlayer: value => set({ preferredPlayer: value })
}),
{
- version: 1,
+ version: 2,
name: 'portal.preferences'
}
)
diff --git a/rsconcept/frontend/src/utils/constants.ts b/rsconcept/frontend/src/utils/constants.ts
index 29497219..ca28a2a4 100644
--- a/rsconcept/frontend/src/utils/constants.ts
+++ b/rsconcept/frontend/src/utils/constants.ts
@@ -53,11 +53,6 @@ export const resources = {
db_schema: '/db_schema.svg'
} as const;
-/** Youtube IDs for embedding. */
-export const youtube = {
- intro: '0Ty9mu9sOJo'
-} as const;
-
/** External URLs. */
export const external_urls = {
concept: 'https://www.acconcept.ru/',
@@ -76,6 +71,23 @@ export const external_urls = {
restAPI: 'https://api.portal.acconcept.ru'
} as const;
+/** Youtube and VK IDs for embedding. */
+export interface IVideo {
+ /** Youtube ID. */
+ youtube: string;
+
+ /** VK ID. */
+ vk: string;
+}
+
+/** Youtube and VK IDs for embedding. */
+export const videos = {
+ explication: {
+ youtube: '0Ty9mu9sOJo',
+ vk: 'oid=-232112636&id=456239017'
+ }
+};
+
/** Global element ID. */
export const globalIDs = {
tooltip: 'global_tooltip',