mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-08-14 04:40:36 +03:00
F: Rework video embedding. Add VK Video
This commit is contained in:
parent
bd6f72aceb
commit
2ecdbd1719
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -177,6 +177,7 @@
|
|||
"Upvote",
|
||||
"Viewset",
|
||||
"viewsets",
|
||||
"vkvideo",
|
||||
"wordform",
|
||||
"Wordforms",
|
||||
"XCSDATN",
|
||||
|
|
|
@ -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 <DlgShowVideo />;
|
||||
case DialogType.CONSTITUENTA_TEMPLATE:
|
||||
return <DlgCstTemplate />;
|
||||
case DialogType.CREATE_CONSTITUENTA:
|
||||
|
|
|
@ -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';
|
||||
|
|
42
rsconcept/frontend/src/components/view/embed-vkvideo.tsx
Normal file
42
rsconcept/frontend/src/components/view/embed-vkvideo.tsx
Normal file
|
@ -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 (
|
||||
<div
|
||||
className='relative h-0 mt-1'
|
||||
style={{
|
||||
paddingBottom: `${pxHeight}px`,
|
||||
paddingLeft: `${pxWidth}px`
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
allowFullScreen
|
||||
title='Встроенное видео ВКонтакте'
|
||||
allow='autoplay; encrypted-media; fullscreen; picture-in-picture; screen-wake-lock;'
|
||||
className='absolute top-0 left-0 border'
|
||||
style={{
|
||||
minHeight: `${pxHeight}px`,
|
||||
minWidth: `${pxWidth}px`
|
||||
}}
|
||||
width={`${pxWidth}px`}
|
||||
height={`${pxHeight}px`}
|
||||
src={`https://vk.com/video_ext.php?${videoID}&hd=1`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<IconVideo
|
||||
className={cn('cursor-pointer', className)}
|
||||
onClick={handleShowExplication}
|
||||
data-tooltip-id={globalIDs.tooltip}
|
||||
data-tooltip-content='Просмотр видео'
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<ModalView fullScreen className='relative w-[calc(100dvw-3rem)] h-[calc(100dvh-3rem)]'>
|
||||
<Tabs
|
||||
className='flex flex-col gap-1 items-center pt-3'
|
||||
selectedIndex={activeTab}
|
||||
onSelect={index => setActiveTab(index as TabID)}
|
||||
>
|
||||
<TabList className='mx-auto flex border divide-x rounded-none'>
|
||||
<TabLabel label='ВК Видео' className='w-32' />
|
||||
<TabLabel label='Youtube' className='w-32' />
|
||||
</TabList>
|
||||
|
||||
<TabPanel>
|
||||
<TabVK videoHeight={videoHeight} videoID={video.vk} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<TabYoutube videoHeight={videoHeight} videoID={video.youtube} />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</ModalView>
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { DlgShowVideo as DlgShowVideo } from './dlg-show-video';
|
|
@ -0,0 +1,10 @@
|
|||
import { EmbedVKVideo } from '@/components/view/embed-vkvideo';
|
||||
|
||||
interface TabVKProps {
|
||||
videoHeight: number;
|
||||
videoID: string;
|
||||
}
|
||||
|
||||
export function TabVK({ videoHeight, videoID }: TabVKProps) {
|
||||
return <EmbedVKVideo videoID={videoID} pxHeight={videoHeight} />;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { EmbedYoutube } from '@/components/view';
|
||||
|
||||
interface TabYoutubeProps {
|
||||
videoHeight: number;
|
||||
videoID: string;
|
||||
}
|
||||
|
||||
export function TabYoutube({ videoHeight, videoID }: TabYoutubeProps) {
|
||||
return <EmbedYoutube videoID={videoID} pxHeight={videoHeight} />;
|
||||
}
|
|
@ -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() {
|
|||
<h1>Портал</h1>
|
||||
<p>
|
||||
Портал позволяет анализировать предметные области, формально записывать системы определений и синтезировать их с
|
||||
помощью математического аппарата <LinkTopic text='Родов структур' topic={HelpTopic.RSLANG} />
|
||||
помощью математического аппарата <LinkTopic text='Родов структур' topic={HelpTopic.RSLANG} />.
|
||||
</p>
|
||||
<p>
|
||||
Такие системы называются <LinkTopic text='Концептуальными схемами' topic={HelpTopic.CC_SYSTEM} /> и состоят из
|
||||
|
@ -19,6 +20,10 @@ export function HelpMain() {
|
|||
определения. Концептуальные схемы могут связываться путем синтеза в{' '}
|
||||
<LinkTopic text='Операционной схеме синтеза' topic={HelpTopic.CC_OSS} />.
|
||||
</p>
|
||||
<p>
|
||||
Значок <IconVideo className='inline-icon' /> при нажатии отображает видео о различных темах и подробностях
|
||||
работы Портала. Просмотр видео доступен на Youtube и ВКонтакте.
|
||||
</p>
|
||||
|
||||
<details>
|
||||
<summary className='text-center font-semibold'>Разделы Справки</summary>
|
||||
|
|
|
@ -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 (
|
||||
<div className='flex flex-col gap-4'>
|
||||
|
@ -23,14 +12,10 @@ export function HelpRSLang() {
|
|||
<h1>Родоструктурная экспликация концептуальных схем</h1>
|
||||
<p>Формальная запись (<i>экспликация</i>) концептуальных схем осуществляется с помощью языка родов структур.</p>
|
||||
<p>Для ознакомления с основами родов структур можно использовать следующие материалы:</p>
|
||||
<p>1. <a className='underline' href={external_urls.intro_video}>Видео: Краткое введение в мат. аппарат</a></p>
|
||||
<p>1. <BadgeVideo className='inline-icon' video={videos.explication} /> Видео: Краткое введение в мат. аппарат</p>
|
||||
<p>2. <a className='underline' href={external_urls.ponomarev}>Текст: Учебник И. Н. Пономарева</a></p>
|
||||
<p>3. <a className='underline' href={external_urls.full_course}>Видео: лекции для 4 курса (второй семестр 2022-23 год)</a></p>
|
||||
</div>
|
||||
<EmbedYoutube
|
||||
videoID={youtube.intro}
|
||||
pxHeight={videoHeight}
|
||||
/>
|
||||
<div className='dense'>
|
||||
<Subtopics headTopic={HelpTopic.RSLANG} />
|
||||
</div>
|
||||
|
|
|
@ -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<DialogsStore>()(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 }),
|
||||
|
|
|
@ -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<PreferencesStore>()(
|
||||
|
@ -66,7 +74,7 @@ export const usePreferencesStore = create<PreferencesStore>()(
|
|||
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<PreferencesStore>()(
|
|||
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'
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue
Block a user