(null);\n const { type = 'text', id, label, suffix, autoSelect, ...rest } = props;\n\n useLayoutEffect(() => {\n if (autoSelect) {\n inputRef?.current?.select();\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n if (!label)\n return (\n <>\n \n {suffix ? {suffix} : null}\n >\n );\n\n return (\n \n );\n};\n\nexport default Input;\n","import Select, { Theme } from 'react-select';\n\nimport styles from './InputList.module.scss';\n\nexport type InputListOption = {\n value: string;\n label: string;\n};\n\ntype InputListProps = {\n disabled?: boolean;\n id: string;\n label?: string;\n onSelect: (option: InputListOption) => void;\n options: InputListOption[];\n selectedOption?: InputListOption;\n};\n\nconst InputList = ({ disabled, id, label, onSelect, options, selectedOption }: InputListProps) => {\n const handleChange = (option: InputListOption | null) => {\n option && onSelect(option);\n };\n\n const className = disabled ? styles.disabledInput : styles.inputList;\n\n return (\n \n {label && (\n \n )}\n
\n );\n};\n\nexport default InputList;\n","import Alert from '@mui/material/Alert';\nimport { useMemo } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { Locales } from 'kognia/i18n/types';\nimport Input from 'shared/components/input';\nimport InputList, { InputListOption } from 'shared/components/input-list';\nimport { useBranding } from 'shared/hooks/use-branding/useBranding';\nimport { User } from 'shared/types';\n\nimport styles from './AccountForm.module.scss';\n\ntype AccountFormProps = {\n user: User;\n};\n\nconst AccountForm = ({ user }: AccountFormProps): JSX.Element => {\n const { t } = useTranslation();\n const branding = useBranding();\n const languageOptions = useMemo(\n () =>\n [\n { label: t('languages:english'), value: Locales['en-US'] },\n { label: t('languages:spanish'), value: Locales['es-ES'] },\n ] as InputListOption[],\n [t],\n );\n\n const getSelectedLocale = (locale: string, options: InputListOption[]) => {\n return options.find((option) => option.value === locale);\n };\n\n const selectedLocale = useMemo(() => getSelectedLocale(user.locale, languageOptions), [languageOptions, user.locale]);\n\n return (\n \n );\n};\n\nexport default AccountForm;\n","import { ErrorBoundary } from '@sentry/react';\nimport { ReactNode } from 'react';\n\nimport { ContainerWrapper, ContainerWrapperProps, Content } from './index.styled';\nimport { ErrorContainer } from '../error-boundary/error-container';\n\ninterface Props extends ContainerWrapperProps {\n children: ReactNode;\n fullScreen?: boolean;\n}\n\nconst Container = ({ children, fullScreen = false, backgroundImage, backgroundSize, backgroundColor }: Props) => {\n return (\n }>\n \n {children}\n \n \n );\n};\n\nexport default Container;\n","import { useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport AccountForm from 'pages/account/components/account-form';\nimport Container from 'shared/components/container';\nimport { SidebarLayout } from 'shared/components/sidebar-layout';\nimport { useUser } from 'shared/contexts/app-state';\nimport { useBranding } from 'shared/hooks/use-branding/useBranding';\n\nconst AccountPageContainer = () => {\n const user = useUser();\n const { t } = useTranslation();\n const branding = useBranding();\n\n useEffect(() => {\n document.title = t('common:metas.title.account', { clientDisplayName: branding.displayName });\n }, [branding.displayName, t]);\n\n return (\n \n \n \n \n \n );\n};\n\nexport default AccountPageContainer;\n","export enum SolutionCardVariants {\n TAGGING_TOOL = 'taggingTool',\n LIVE_RECORDINGS = 'liveRecordings',\n RECORDINGS = 'recordings',\n PLAYLISTS = 'playlists',\n}\n","import { SvgIcon, styled } from '@mui/material';\n\nexport const SolutionSvgIconContainer = styled(SvgIcon)(({ theme }) => ({\n maxWidth: '100%',\n height: 'auto',\n margin: theme.spacing(2, 0, 4, 0),\n}));\n","import { SvgIconProps } from '@mui/material';\n\nimport { SolutionSvgIconContainer } from '../solution-svg-icon-container/SolutionSvgIconContainer';\n\nexport const IconMatchAnalysis = ({ sx, ...props }: SvgIconProps) => {\n return (\n \n \n \n );\n};\n","import { SvgIconProps } from '@mui/material';\n\nimport { SolutionSvgIconContainer } from '../solution-svg-icon-container/SolutionSvgIconContainer';\n\nexport const IconPlaylists = ({ sx, ...props }: SvgIconProps) => {\n return (\n \n \n \n );\n};\n","import { SvgIconProps } from '@mui/material';\n\nimport { SolutionSvgIconContainer } from '../solution-svg-icon-container/SolutionSvgIconContainer';\n\nexport const IconTaggingTool = ({ sx, ...props }: SvgIconProps) => {\n return (\n \n \n \n );\n};\n","import React, { ReactNode } from 'react';\nimport { Link } from 'react-router-dom';\n\ntype LinkWithExternalProps = {\n to: string;\n onClick: ((event: any) => any) | undefined;\n children: ReactNode;\n className?: string;\n};\n\nconst LinkWithExternal = ({ to, children, ...props }: LinkWithExternalProps) => {\n // It is intended to be an external link\n if (/^https?:\\/\\//.test(to))\n return (\n \n {children}\n \n );\n\n // Finally, it is an internal link\n return (\n \n {children}\n \n );\n};\n\nexport default LinkWithExternal;\n","import { Box, styled } from '@mui/material';\nimport { Colors } from 'kognia-ui';\n\nimport { SolutionCardVariants } from 'pages/home/types/solutionCardVariants';\n\nexport const SolutionCardWrapper = styled(Box, {\n shouldForwardProp: (prop) => prop !== 'disabled' && prop !== 'variant',\n})<{ disabled?: boolean; variant: SolutionCardVariants }>(({ theme, variant, disabled }) => ({\n flex: 1,\n width: '224px',\n height: '352px',\n borderRadius: '8px',\n borderWidth: '4px',\n borderStyle: 'solid',\n transition: theme.transitions.create('all', {\n easing: theme.transitions.easing.easeInOut,\n duration: theme.transitions.duration.shorter,\n }),\n userSelect: 'none',\n\n '&:hover': {\n div: {\n opacity: 1,\n },\n },\n\n '&:active': {\n // TODO use from theme\n borderColor: Colors.ghost,\n },\n\n a: {\n display: 'flex',\n flexDirection: 'column',\n justifyContent: 'space-between',\n alignItems: 'center',\n flex: 1,\n height: '100%',\n padding: theme.spacing(3, 2, 2),\n borderRadius: '8px',\n textDecoration: 'none',\n cursor: 'pointer',\n transition: theme.transitions.create('all', {\n easing: theme.transitions.easing.easeInOut,\n duration: theme.transitions.duration.shorter,\n }),\n\n '&:active, &:hover, &:focus, &:focus-within': {\n zIndex: 'auto',\n color: 'inherit',\n // TODO use from theme\n boxShadow: `0 32px 40px -2px rgba(10, 22, 70, 0.12), 0 0 1px 0 rgba(10, 22, 70, 0.06)`,\n },\n\n '&:after': {\n content: 'none',\n border: 0,\n },\n\n '&:active:after': {\n content: 'none',\n border: 'none',\n },\n\n [theme.breakpoints.up('xl')]: {\n padding: theme.spacing(4, 3, 3),\n },\n\n h3: {\n textAlign: 'center',\n },\n },\n\n ...(variant === SolutionCardVariants.TAGGING_TOOL && {\n '& h3': {\n color: theme.palette.common.white,\n },\n // TODO use from theme\n backgroundColor: Colors.brandPastel,\n borderColor: Colors.brandPastel,\n }),\n\n ...(variant === SolutionCardVariants.LIVE_RECORDINGS && {\n '& h3': {\n color: theme.palette.common.white,\n },\n background: theme.palette.tertiary.main,\n borderColor: theme.palette.tertiary.main,\n }),\n\n ...(variant === SolutionCardVariants.PLAYLISTS && {\n // TODO use from theme\n backgroundColor: Colors.peach,\n borderColor: Colors.peach,\n }),\n\n ...(variant === SolutionCardVariants.RECORDINGS && {\n // TODO use from theme\n backgroundColor: Colors.aqua,\n borderColor: Colors.aqua,\n }),\n\n ...(disabled && {\n background: theme.palette.grey[200],\n borderColor: theme.palette.grey[200],\n\n pointerEvents: 'none',\n\n '& h3': {\n color: theme.palette.secondary.light,\n },\n\n '& a': {\n cursor: 'default',\n\n '&:hover, &:focus, &:focus-within': {\n boxShadow: 'none',\n },\n },\n }),\n}));\n","import { ReactNode } from 'react';\n\nimport LinkWithExternal from 'kognia/router/link-with-external';\nimport { SolutionCardVariants } from 'pages/home/types/solutionCardVariants';\n\nimport { SolutionCardWrapper } from './ui/solution-card-wrapper/SolutionCardWrapper';\n\ninterface Props {\n variant: SolutionCardVariants;\n children: ReactNode;\n disabled?: boolean;\n to: string;\n}\n\nconst SolutionCard = ({ children, variant, disabled, to }: Props) => {\n return (\n \n event.preventDefault() : undefined}\n to={to}\n data-testid={`solution-card-${variant}`}\n >\n {children}\n \n \n );\n};\n\nexport default SolutionCard;\n","import { Box, Grid, styled } from '@mui/material';\nimport { useTranslation } from 'react-i18next';\nimport { generatePath } from 'react-router-dom';\n\nimport { routes } from 'kognia/router/routes';\nimport { SolutionCardVariants } from 'pages/home/types/solutionCardVariants';\n\nimport { IconMatchAnalysis } from './ui/icons/icon-match-analysis/IconMatchAnalysis';\nimport { IconPlaylists } from './ui/icons/icon-playlists/IconPlaylists';\nimport { IconTaggingTool } from './ui/icons/icon-tagging-tool/IconTaggingTool';\nimport SolutionCard from './ui/solution-card/SolutionCard';\n\nconst SolutionActionDescription = styled(Box)(({ theme }) => ({\n display: 'flex',\n justifyContent: 'center',\n alignItems: 'center',\n width: '100%',\n minHeight: '48px',\n padding: theme.spacing(1),\n borderRadius: '2px',\n backgroundColor: theme.palette.common.white,\n opacity: 0.6,\n lineHeight: '24px',\n textAlign: 'center',\n textTransform: 'uppercase',\n transition: theme.transitions.create('all', {\n easing: theme.transitions.easing.easeInOut,\n duration: theme.transitions.duration.shorter,\n }),\n}));\n\nconst SolutionCardContainer = styled(Box)(({ theme }) => ({\n margin: theme.spacing(2),\n}));\n\nexport const Solutions = () => {\n const { t } = useTranslation();\n\n return (\n \n \n \n \n {t('home:solutions.tactical-analysis.title')}
\n \n {t('home:solutions.tactical-analysis.cta')}\n \n \n \n\n \n \n \n {t('home:solutions.tagging.title')}
\n \n {t('home:solutions.tagging.cta.start-tagging')}\n \n \n\n \n \n {t('home:solutions.playlists.title')}
\n \n {t('home:solutions.playlists.cta')}\n \n \n \n \n );\n};\n","import { useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport Container from 'shared/components/container';\nimport { SidebarLayout } from 'shared/components/sidebar-layout';\nimport { useUser } from 'shared/contexts/app-state';\nimport { useBranding } from 'shared/hooks/use-branding/useBranding';\n\nimport styles from './HomeContainer.module.scss';\nimport { Solutions } from './ui/solutions/Solutions';\n\nexport const HomeContainer = () => {\n const user = useUser();\n const { t } = useTranslation();\n const branding = useBranding();\n\n useEffect(() => {\n document.title = t('common:metas.title.home', { clientDisplayName: branding.displayName });\n }, [branding.displayName, t]);\n\n return (\n \n \n \n
{t('home:welcome', { firstName: user.firstName })}
\n
{t('home:what-would-you-like-to-do')}
\n
\n
\n \n \n );\n};\n\nexport default HomeContainer;\n","import { queryClient } from '../../config';\n\nexport const useAppQueryClient = () => {\n return { queryClient: queryClient };\n};\n","import { UseQueryResult } from '@tanstack/react-query';\n\nimport { useAppQueryClient } from 'api/hooks/useAppQueryClient';\nimport { useFetchRequest } from 'api/hooks/useFetchRequest';\nimport { liveTaggingSessionUrl } from 'api/routes';\n\nimport { LiveTaggingSession } from '../types';\n\nexport const generateLiveSessionRef = (liveTaggingSessionId: string) => {\n return [`fetch-live-tagging-session:${liveTaggingSessionId}`];\n};\n\ninterface useFetchLiveTaggingSessionInterface {\n (liveTaggingSessionId: string): UseQueryResult & {\n setQueryData: (data: LiveTaggingSession) => void;\n };\n}\n\nexport const useFetchLiveTaggingSession: useFetchLiveTaggingSessionInterface = (liveTaggingSessionId) => {\n const fetchQueryRef = generateLiveSessionRef(liveTaggingSessionId);\n const { queryClient } = useAppQueryClient();\n\n const fetchRequest = useFetchRequest({\n queryRef: fetchQueryRef,\n url: liveTaggingSessionUrl(liveTaggingSessionId),\n transformer: (response: LiveTaggingSession) => response,\n });\n\n const invalidateQuery = () => queryClient.invalidateQueries(fetchQueryRef);\n const setQueryData = (data: LiveTaggingSession) => queryClient.setQueryData(fetchQueryRef, data);\n\n return { ...fetchRequest, invalidateQuery, setQueryData };\n};\n","import { useMutation } from '@tanstack/react-query';\nimport { useTranslation } from 'react-i18next';\n\nimport { useBackendApi } from 'api/hooks/useBackendApi';\nimport { liveTaggingSessionUrl } from 'api/routes';\nimport { HTTPMethod } from 'api/types';\nimport { NotificationType, useNotifications } from 'shared/hooks/notifications';\n\nimport { LiveTaggingSession } from '../types';\nimport { useFetchLiveTaggingSession } from '../use-fetch-live-session';\n\ntype LiveTaggingSessionUpdate = {\n name: string;\n};\n\nexport const useUpdateLiveTaggingSession = (\n liveTaggingSessionId: string,\n onSuccess?: () => void,\n onError?: () => void,\n onSettled?: () => void,\n) => {\n const { t } = useTranslation();\n const { setQueryData } = useFetchLiveTaggingSession(liveTaggingSessionId);\n\n const onPatchSuccess = (data: LiveTaggingSession) => data;\n const triggerNotification = useNotifications();\n\n const updateLiveTaggingSession = useMutation(\n (params) => useBackendApi(liveTaggingSessionUrl(liveTaggingSessionId), HTTPMethod.PATCH, onPatchSuccess, params),\n {\n onMutate: async (params: LiveTaggingSessionUpdate) => {},\n onError: () => {\n if (onError) onError();\n triggerNotification({ type: NotificationType.ERROR, message: t('api:use-update-playlist.error') });\n },\n onSuccess: (updatedLiveTaggingSession) => {\n setQueryData && setQueryData(updatedLiveTaggingSession);\n if (onSuccess) onSuccess();\n },\n onSettled: () => {\n if (onSettled) onSettled();\n },\n },\n );\n\n const sendLiveTaggingSessionUpdate = (name: string) => {\n const updateParams = { name: name } as LiveTaggingSessionUpdate;\n updateLiveTaggingSession.mutate(updateParams);\n };\n\n return { sendLiveTaggingSessionUpdate, isLoading: updateLiveTaggingSession.isLoading };\n};\n","import { Button } from '@mui/material';\nimport isEqual from 'lodash/isEqual';\nimport { useState } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { LiveTaggingSession } from 'api/tagging-tool/types';\nimport Input from 'shared/components/input';\n\nimport styles from './LiveTaggingSessionEditForm.module.scss';\n\ntype LiveTaggingSessionEditFormProps = {\n initialData: LiveTaggingSession;\n onCancel: () => void;\n onSubmit: (params: string) => void;\n};\n\nconst LiveTaggingSessionEditForm = ({ initialData, onCancel, onSubmit }: LiveTaggingSessionEditFormProps) => {\n const { t } = useTranslation();\n const [liveTaggingSessionData, setLiveTaggingSessionData] = useState(initialData);\n\n const onChange = (event: React.ChangeEvent) => {\n setLiveTaggingSessionData({ ...liveTaggingSessionData, ...{ name: event.target.value } });\n };\n\n return (\n <>\n {t('recording-edit:form.title')}
\n \n \n
\n \n \n \n
\n >\n );\n};\n\nexport default LiveTaggingSessionEditForm;\n","import React, { useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { generatePath, RouteComponentProps, useHistory } from 'react-router-dom';\n\nimport { useFetchLiveTaggingSession } from 'api/tagging-tool/use-fetch-live-session';\nimport { useUpdateLiveTaggingSession } from 'api/tagging-tool/use-update-live-session';\nimport { routes } from 'kognia/router/routes';\nimport Container from 'shared/components/container';\nimport { SidebarLayout } from 'shared/components/sidebar-layout';\nimport Spinner from 'shared/components/spinner';\n\nimport styles from './LiveTaggingSessionEditPage.module.scss';\nimport LiveTaggingSessionEditForm from '../live-tagging-session-edit-form';\n\ntype RouteParams = { recordingId: string };\n\nconst LiveTaggingSessionEditPage = ({ liveTaggingSessionId }: LiveTaggingSessionEditPageProps) => {\n const { data, isError, isFetching, isSuccess } = useFetchLiveTaggingSession(liveTaggingSessionId);\n const history = useHistory();\n const redirectToRecordingListPage = () => {\n const recordingListPath = generatePath(routes.TAGGING_TOOL);\n history.push(recordingListPath);\n };\n\n const { sendLiveTaggingSessionUpdate } = useUpdateLiveTaggingSession(liveTaggingSessionId, () =>\n redirectToRecordingListPage(),\n );\n\n const { t } = useTranslation();\n\n const [isPageReady, setIsPageReady] = useState(false);\n\n useEffect(() => {\n if (isError || (!isFetching && !isPageReady)) {\n if (isSuccess) {\n setIsPageReady(true);\n } else {\n setIsPageReady(false);\n }\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isFetching, isError]);\n\n const showLoading = !isPageReady && isFetching;\n const showPage = isPageReady && isSuccess;\n const showError = !isPageReady && isError;\n\n const onSubmit = (params: string) => {\n sendLiveTaggingSessionUpdate(params);\n };\n\n return (\n \n {showLoading && (\n \n \n \n )}\n {showPage && (\n \n {data && (\n redirectToRecordingListPage()}\n onSubmit={onSubmit}\n />\n )}\n \n )}\n {showError && {t('live-session-edit:error')}
}\n \n );\n};\n\nconst LiveTaggingSessionEditContainer = (props: RouteComponentProps) => {\n const {\n match: {\n params: { recordingId },\n },\n } = props;\n\n return ;\n};\n\ntype LiveTaggingSessionEditPageProps = {\n liveTaggingSessionId: string;\n};\n\nexport default LiveTaggingSessionEditContainer;\n","import { Box, styled } from '@mui/material';\nimport React from 'react';\n\ninterface Props {\n children: React.ReactNode;\n}\n\nconst DialogActionsWrapper = styled(Box)(({ theme }) => ({\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n padding: `0 ${theme.spacing(4)} ${theme.spacing(4)} ${theme.spacing(4)}`,\n}));\n\nexport const DialogActions = ({ children }: Props) => {\n return {children};\n};\n","import { Box, styled } from '@mui/material';\nimport React from 'react';\n\ninterface Props {\n children: React.ReactNode;\n}\n\nconst DialogContentWrapper = styled(Box)(({ theme }) => ({\n padding: `0 ${theme.spacing(4)} ${theme.spacing(4)} ${theme.spacing(4)}`,\n}));\n\nexport const DialogContent = ({ children }: Props) => {\n return {children};\n};\n","import { DialogContentTextProps, styled } from '@mui/material';\nimport MuiDialogContentText from '@mui/material/DialogContentText';\nimport { Colors, fontSizes } from 'kognia-ui';\n\nexport enum DialogTextVariants {\n Primary = 'primary',\n Secondary = 'secondary',\n}\n\ninterface DialogContentTextWrapperProps {\n textVariant: DialogTextVariants.Primary | DialogTextVariants.Secondary;\n}\n\nconst DialogContentTextWrapper = styled(MuiDialogContentText, {\n shouldForwardProp: (prop) => prop !== 'textVariant',\n})(({ theme, textVariant }) => ({\n fontSize: fontSizes.default,\n color: textVariant === DialogTextVariants.Secondary ? Colors.storm : Colors.night,\n marginBottom: theme.spacing(1),\n}));\n\ninterface Props extends DialogContentTextProps {\n textVariant?: DialogTextVariants.Primary | DialogTextVariants.Secondary;\n}\n\nexport const DialogContentText = ({\n children,\n textVariant = DialogTextVariants.Secondary,\n textAlign = 'center',\n ...rest\n}: Props) => {\n return (\n \n {children}\n \n );\n};\n","import { Box, styled } from '@mui/material';\nimport MuiDialogTitle from '@mui/material/DialogTitle';\nimport { Colors, fontSizes, fontWeight, letterSpacing } from 'kognia-ui';\nimport React from 'react';\n\ninterface Props {\n children: React.ReactNode;\n icon?: React.ReactNode;\n}\n\nconst DialogHeaderWrapper = styled(Box)(({ theme }) => ({\n display: 'flex',\n justifyContent: 'center',\n alignItems: 'center',\n textTransform: 'uppercase',\n flexDirection: 'column',\n padding: theme.spacing(3),\n}));\n\nconst DialogTitle = styled(MuiDialogTitle)({\n fontSize: fontSizes.modalTitle,\n letterSpacing: letterSpacing.modalTitle,\n padding: 0,\n fontWeight: fontWeight.modalTitle,\n color: Colors.night,\n});\n\nconst DialogIcon = styled(Box)(({ theme }) => ({\n padding: theme.spacing(1.5),\n backgroundColor: Colors.background,\n borderRadius: '50%',\n marginBottom: theme.spacing(3),\n}));\n\nexport const DialogHeader = ({ children, icon }: Props) => {\n return (\n \n {icon && {icon}}\n {children}\n \n );\n};\n","import { SvgIcon, type SvgIconProps } from '../svg-icon/SvgIcon';\n\nexport const IconChevronRight = (props: Omit): JSX.Element => {\n return (\n \n \n \n );\n};\n","import { useUser } from 'shared/contexts/app-state';\nimport { Client } from 'shared/types/user/types';\n\ninterface UseSourceClientReturn {\n (clientIds: string[]): Client[];\n}\nexport const useCommonResourceClients = (): UseSourceClientReturn => {\n const user = useUser();\n\n return (clientIds: string[]) =>\n user.clients.reduce((acc: Client[], client: Client) => {\n if (!clientIds.includes(client.id)) return acc;\n\n return [...acc, client];\n }, []);\n};\n","import { Box, Button, styled } from '@mui/material';\nimport { Colors } from 'kognia-ui';\nimport debounce from 'lodash/debounce';\nimport { useCallback, useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { useHistory } from 'react-router-dom';\n\nimport { routes } from 'kognia/router/routes';\nimport SwitchEnvironmentBackground from 'shared/assets/page-background.svg';\nimport Container from 'shared/components/container';\nimport { Dialog } from 'shared/components/dialog';\nimport { DialogActions } from 'shared/components/dialog/dialog-actions';\nimport { DialogContent } from 'shared/components/dialog/dialog-content';\nimport { DialogContentText, DialogTextVariants } from 'shared/components/dialog/dialog-content-text';\nimport { DialogHeader } from 'shared/components/dialog/dialog-header';\nimport { IconChevronRight } from 'shared/components/icons/icon-chevron-right';\nimport { IconUser } from 'shared/components/icons/icon-user';\nimport { SidebarLayout } from 'shared/components/sidebar-layout';\nimport Spinner from 'shared/components/spinner';\nimport { useClientId } from 'shared/contexts/app-state';\nimport { useCommonResourceClients } from 'shared/hooks/use-source-client';\nimport { Client } from 'shared/types/user/types';\n\nconst ClientButton = styled(Button)(({ theme }) => ({\n display: 'flex',\n justifyContent: 'space-between',\n backgroundColor: Colors.background,\n height: '48px',\n paddingLeft: theme.spacing(2),\n paddingRight: theme.spacing(2),\n textTransform: 'none',\n}));\n\nconst ClientButtonsWrapper = styled(Box)(({ theme }) => ({\n display: 'flex',\n flexDirection: 'column',\n flexGrow: 1,\n gap: theme.spacing(1),\n}));\n\ninterface SwitchEnvironmentProps {\n resourceClientsIds: string[];\n}\n\nexport const SwitchEnvironment = ({ resourceClientsIds }: SwitchEnvironmentProps) => {\n const { t } = useTranslation();\n const [newClientId, setNewClientId] = useState(null);\n const history = useHistory();\n const { setClientId } = useClientId();\n const getCommonClients = useCommonResourceClients();\n const commonClients = getCommonClients(resourceClientsIds);\n const isLoading = Boolean(newClientId);\n\n const handleEnvironmentSwitch = useCallback((client: Client) => {\n setNewClientId(client.id);\n }, []);\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const handleClientChange = useCallback(\n debounce((newClientId: string) => {\n setClientId(newClientId);\n setNewClientId(null);\n }, 500),\n [history, setClientId],\n );\n\n useEffect(() => {\n if (newClientId) {\n handleClientChange(newClientId);\n }\n }, [handleClientChange, newClientId, setClientId]);\n\n const handleClose = useCallback(() => {\n history.replace(routes.HOME_PAGE);\n }, [history]);\n\n const isSingleSelection = commonClients.length === 1;\n\n return (\n \n \n \n \n \n );\n};\n","export const PLAYLIST_ITEM_GAP = 4;\nexport const PLAYLIST_ITEM_WIDTH = 152;\nexport const PLAYLIST_ITEM_HEIGHT = 70;\nexport const PLAYLIST_ITEM_FULL_WIDTH = PLAYLIST_ITEM_WIDTH + PLAYLIST_ITEM_GAP * 2;\n\nexport const PLAYLIST_TIMELINE_HEIGHT = 172;\nexport const PLAYLIST_TIMELINE_HEADER_HEIGHT = 56;\n\nexport const PLAYLIST_HEADER_HEIGHT = 56;\n","import { Grid, styled } from '@mui/material';\n\nimport { PLAYLIST_HEADER_HEIGHT, PLAYLIST_TIMELINE_HEIGHT } from '../config/Playlist.config';\n\nexport const PLAYLIST_VIDEO_PLAYER_HEIGHT = `calc(100% - ${PLAYLIST_HEADER_HEIGHT}px - ${PLAYLIST_TIMELINE_HEIGHT}px)`;\n\nexport const PlaylistVideoPlayerContainer = styled(Grid)({\n height: PLAYLIST_VIDEO_PLAYER_HEIGHT,\n});\n","import { Grid, Skeleton } from '@mui/material';\n\nimport { PLAYLIST_HEADER_HEIGHT, PLAYLIST_TIMELINE_HEIGHT } from 'entities/playlist/config/Playlist.config';\n\nimport { PlaylistVideoPlayerContainer } from '../../../../entities/playlist/ui/PlaylistVideoPlayerContainer';\n\nconst PlaylistDetailSkeleton = () => {\n return (\n \n \n \n \n \n \n \n \n \n \n \n );\n};\n\nexport default PlaylistDetailSkeleton;\n","import { z } from 'zod';\n\nimport { RecordingTeam } from 'pages/recordings-list/types';\nimport { Zones } from 'pages/tactical-analysis/components/tactical-analysis/filters/types';\n\nimport {\n RecordingFiltersScenariosOrTacticsInsideSchema,\n RecordingFiltersTacticSchema,\n RecordingFiltersTacticsSchema,\n RecordingsFiltersEventsSchema,\n RecordingsFiltersSchema,\n} from './schemas';\n\nexport enum RecordingTypes {\n ALL = 'all',\n TRAINING = 'training',\n GAME = 'game',\n}\n\nexport enum TacticalAnalysisStates {\n STARTED = 'STARTED',\n FINISHED = 'FINISHED',\n VALIDATED = 'VALIDATED',\n}\n\nexport interface TacticalAnalysis {\n id: string;\n recordingId: string;\n tacticalAnalysisGitCommit: string;\n numberOfEpisodes: number;\n startTime: Date;\n state: TacticalAnalysisStates;\n}\n\nexport enum VideoSourceStates {\n STARTED = 'STARTED',\n FINISHED = 'FINISHED',\n}\n\nexport interface VideoSourceState {\n playBackType: PlayBackTypes;\n viewType: ViewTypes;\n state: VideoSourceStates;\n}\n\nexport type Recording = {\n competitionName: string;\n date: Date;\n duration: number | null;\n extraTime: number | null;\n hasTaggingEvents: boolean;\n id: string;\n isLive: boolean;\n isProcessingVideo: boolean;\n matchDay: string;\n name: string;\n tacticalAnalysis: TacticalAnalysis | null;\n teams: RecordingTeam[];\n type: RecordingTypes;\n videoSourcesStates: VideoSourceState[];\n matchReportDownloadUrl: string | null;\n};\n\nexport type EditFormRecording = {\n homeTeamScore: number;\n awayTeamScore: number;\n competitionName: string;\n date: Date;\n duration: number | null;\n id: string;\n matchDay: string;\n name: string;\n type: RecordingTypes;\n};\n\nexport interface RecordingsGroup {\n matchDay: string;\n recordings: Recording[];\n}\n\nexport interface VideoSourceBase {\n id: string;\n recordingId: string;\n playBackType: PlayBackTypes;\n poster?: string | null;\n src: string;\n srcDownload?: string;\n videoView: VideoView;\n}\n\nexport interface VideoSource extends VideoSourceBase {\n duration: number;\n}\n\nexport interface VideoSourceAPI extends VideoSourceBase {\n duration: string;\n}\n\nexport interface VideoView {\n type: ViewTypes;\n}\n\nexport enum ViewTypes {\n TACTICAL = 'tactical',\n PANORAMA = 'panorama',\n UPLOAD = 'upload',\n}\n\nexport enum PlayBackTypes {\n VOD = 'vod',\n LIVE = 'live',\n}\n\nexport type RecordingByName = {\n awayTeamScore: number | null;\n competitionName?: string;\n date: Date;\n duration: number | null;\n homeTeamScore: number | null;\n id: string;\n matchDay?: string;\n name: string;\n videoSources: VideoSource[];\n type: RecordingTypes;\n hasTacticalAnalysis?: boolean;\n hasHomographies: boolean;\n};\n\nexport type ZoneValues = {\n [Zones.zone1]: boolean;\n [Zones.zone2]: boolean;\n [Zones.zone3]: boolean;\n [Zones.zone4]: boolean;\n};\n\nexport type RecordingFiltersTactic = z.TypeOf;\nexport type RecordingFiltersTactics = z.TypeOf;\nexport type RecordingsFiltersEvents = z.TypeOf;\nexport type RecordingFiltersScenariosOrTacticsInside = z.TypeOf;\nexport type RecordingsFilters = z.TypeOf;\n","import { PlayBackTypes, VideoSource, ViewTypes } from 'shared/types/recording/types';\n\nexport const getVODTacticalCameraVideo = (videoSources: VideoSource[]): VideoSource | undefined => {\n return videoSources.find(\n (source) => source.playBackType === PlayBackTypes.VOD && source.videoView.type === ViewTypes.TACTICAL,\n );\n};\n\nexport const getVODPanoramaVideoSource = (videoSources: VideoSource[]): VideoSource => {\n return (\n videoSources.find(\n (source) => source.playBackType === PlayBackTypes.VOD && source.videoView.type === ViewTypes.PANORAMA,\n )\n );\n};\n\nexport const getVODUploadedVideoSource = (videoSources: VideoSource[]): VideoSource | undefined => {\n return videoSources.find(\n (source) => source.playBackType === PlayBackTypes.VOD && source.videoView.type === ViewTypes.UPLOAD,\n );\n};\n\nexport const defaultEmptyVideoSource = {\n id: '',\n recordingId: '',\n playBackType: PlayBackTypes.LIVE,\n videoView: {\n type: ViewTypes.TACTICAL,\n },\n src: '',\n};\n\nexport const getDefaultVideoSource = (videoSources: VideoSource[]): VideoSource => {\n // TODO when the separation of recording and live tagging is done default parameter should be removed\n return (\n getVODTacticalCameraVideo(videoSources) ??\n getVODPanoramaVideoSource(videoSources) ??\n getVODUploadedVideoSource(videoSources) ??\n defaultEmptyVideoSource\n );\n};\n\nexport interface GetVideoSourcesResponse {\n tacticalCameraVideo: VideoSource;\n panoramicVideo: VideoSource;\n uploadedVideo: VideoSource;\n}\n\nexport const getVideoSources = (videoSources: VideoSource[]): GetVideoSourcesResponse => {\n return {\n tacticalCameraVideo: getVODTacticalCameraVideo(videoSources),\n panoramicVideo: getVODPanoramaVideoSource(videoSources),\n uploadedVideo: getVODUploadedVideoSource(videoSources),\n };\n};\n","export function iso8601DurationToSeconds(duration: string): number {\n const regex = /P(?:([0-9]+)D)?T?(?:([0-9]+)H)?(?:([0-9]+)M)?(?:([0-9.]+)S)?/;\n const matches = duration.match(regex);\n\n if (!matches) {\n throw new Error('Invalid ISO 8601 duration format');\n }\n\n const days = parseInt(matches[1], 10) || 0;\n const hours = parseInt(matches[2], 10) || 0;\n const minutes = parseInt(matches[3], 10) || 0;\n const seconds = parseFloat(matches[4]) || 0;\n\n return days * 86400 + hours * 3600 + minutes * 60 + seconds; // 86400 seconds in a day\n}\n","import memoize from 'lodash/memoize';\n\nexport const round = memoize((time: number) => {\n return Math.round((time + Number.EPSILON) * 100) / 100;\n});\n","export enum MatchSegmentTypes {\n FIRST = 'first',\n SECOND = 'second',\n FIRST_EXTRA = 'firstextra',\n SECOND_EXTRA = 'secondextra',\n PENALTY = 'penalty',\n}\n\nexport type SegmentConfig = {\n length: number;\n start: number;\n segment: {\n length: number;\n name: MatchSegmentTypes;\n start: number;\n };\n};\n","import { Episode } from 'shared/types';\nimport { MatchSegmentTypes, SegmentConfig } from 'shared/types/segment/types';\n\nexport const enrichEpisodesWithName = (episodes: Episode[]): Episode[] => {\n return episodes\n .sort((a, b) => (a.startTime > b.startTime ? 1 : -1))\n .map((episode, idx) => ({\n ...episode,\n name: `${idx + 1}`,\n }));\n};\nconst defaultMatchSegments = [\n {\n segment: {\n name: MatchSegmentTypes.FIRST,\n start: 0,\n length: 2700,\n },\n start: 0,\n length: 2700,\n },\n {\n segment: {\n name: MatchSegmentTypes.SECOND,\n start: 2700,\n length: 2700,\n },\n start: 2700,\n length: 2700,\n },\n];\nexport const enrichSegmentMatchStarts = (episodes: Episode[]): SegmentConfig[] => {\n if (episodes.length === 0) {\n return [];\n }\n\n const segments: SegmentConfig[] = [];\n\n defaultMatchSegments.forEach((matchSegment) => {\n const filteredEpisodes = episodes.filter((episode) => episode.matchSegment === matchSegment.segment.name);\n if (!filteredEpisodes.length) return;\n\n const episodeStartTime = Math.min(\n ...filteredEpisodes.map((episode) => {\n return episode.startTime;\n }),\n );\n const episodeEndTime = Math.max(\n ...filteredEpisodes.map((episode) => {\n return episode.endTime;\n }),\n );\n\n segments.push({\n ...matchSegment,\n start: episodeStartTime,\n length: episodeEndTime - episodeStartTime,\n });\n });\n\n return segments;\n};\nexport const generateMatchSegments = (episodes: Episode[]): SegmentConfig[] => {\n return enrichSegmentMatchStarts(episodes);\n};\n","import map from 'lodash/map';\n\nimport { RecordingFiltersScenariosOrTacticsInside, RecordingsFilters } from 'shared/types/recording/types';\nimport { MatchTeam } from 'shared/types/teams/types';\n\nimport { TacticalAnalysesFilters, TacticalAnalysesPlaylistItemsFiltersAPI } from './useEpisodesWithFilters/types';\n\nexport const transformFilters = (filters: TacticalAnalysesPlaylistItemsFiltersAPI): TacticalAnalysesFilters => {\n const eventsStarting = new Set(\n map(filters.eventStarting.options, (event, key: string) => {\n return key;\n }),\n );\n\n const eventsEnding = new Set(\n map(filters.eventEnding.options, (event, key: string) => {\n return key;\n }),\n );\n\n const scenarios = map(filters.scenario.options, (scenario, key: string) => {\n return { id: key, name: scenario.title };\n });\n\n const offensiveTactics = filters.tactic.options.offensive?.options\n ? map(filters.tactic.options.offensive.options, (tactic, key: string) => {\n return { id: key, name: tactic.title };\n })\n : [];\n\n const defensiveTactics = filters.tactic.options.defensive?.options\n ? map(filters.tactic.options.defensive.options, (tactic, key: string) => {\n return { id: key, name: tactic.title };\n })\n : [];\n\n function sortByJerseyNumber() {\n return (player1: { name: string }, player2: { name: string }) => {\n const matchPlayer1Number = player1.name.split('.')[0].split('-')[0];\n const matchPlayer2Number = player2.name.split('.')[0].split('-')[0];\n\n return Number(matchPlayer1Number) > Number(matchPlayer2Number) ? 1 : -1;\n };\n }\n\n const teams: MatchTeam[] = map(filters.team.options, (team, teamId) => {\n return {\n id: teamId,\n name: team.title,\n logo: '',\n players: map(team.options.players.options, (player, playerId) => ({\n id: playerId,\n name: player.title,\n })).sort(sortByJerseyNumber()),\n };\n });\n\n return {\n eventsStarting,\n eventsEnding,\n scenarios,\n tactics: {\n offensive: offensiveTactics,\n defensive: defensiveTactics,\n },\n teams,\n };\n};\n\nexport const transformScenariosOrTacticsInside = (\n filtersScenariosOrTacticsInside?: RecordingFiltersScenariosOrTacticsInside,\n) => {\n if (!filtersScenariosOrTacticsInside) return [];\n\n const offensiveTactics = filtersScenariosOrTacticsInside.tactics.offensive.filter((tactic) => {\n return tactic.tacticalFundamentalType !== '' || tactic.teamIds.length > 0 || tactic.playerIds.length > 0;\n });\n\n const defensiveTactics = filtersScenariosOrTacticsInside.tactics.defensive.filter((tactic) => {\n return tactic.tacticalFundamentalType !== '' || tactic.teamIds.length > 0 || tactic.playerIds.length > 0;\n });\n\n const tactics = offensiveTactics.concat(defensiveTactics);\n\n return [\n {\n ...filtersScenariosOrTacticsInside,\n tactics: tactics.length > 0 ? tactics : [],\n },\n ];\n};\n\nexport const transformFiltersForRequest = (filters: RecordingsFilters) => {\n return {\n ...filters,\n scenariosOrTacticsInside: transformScenariosOrTacticsInside(filters.scenariosOrTacticsInside),\n eventsEnding: filters.eventsEnding ?? undefined,\n eventsStarting: filters.eventsStarting ?? undefined,\n };\n};\n","import { RecordingVideoSourceAPI } from 'pages/recordings-list/api/types';\nimport { Episode, Match, MatchApi } from 'shared/types';\nimport { MatchWithEpisodes } from 'shared/types/match/types';\nimport { PlayBackTypes, VideoSource, ViewTypes } from 'shared/types/recording/types';\nimport { getDefaultVideoSource } from 'shared/utils/get-video-sources';\nimport { iso8601DurationToSeconds } from 'shared/utils/iso-8601-duration-to-time';\nimport { round } from 'shared/utils/round';\n\nimport { enrichEpisodesWithName, generateMatchSegments } from './utils';\nimport { TacticalAnalysesPlaylistItemsFiltersAPI } from '../../../../recording/useEpisodesWithFilters/types';\nimport { transformFilters } from '../../../../recording/utils';\n\nexport const transformRecordingVideoSources = (videoSources: RecordingVideoSourceAPI[]): VideoSource[] => {\n return videoSources.map((video) => ({\n id: video.id,\n recordingId: video.recordingId,\n src: video.src,\n srcDownload: video.srcDownload,\n poster: video.poster,\n duration: iso8601DurationToSeconds(video.duration),\n playBackType: video.playBackType as PlayBackTypes,\n videoView: {\n type: video.videoView.type as ViewTypes,\n },\n }));\n};\n\nexport interface MatchWithEpisodesAPIResponse {\n match: MatchApi;\n episodes: Episode[];\n filters: TacticalAnalysesPlaylistItemsFiltersAPI;\n}\n\nexport const transformMatchWithEpisodes = ({\n match,\n episodes,\n filters,\n}: MatchWithEpisodesAPIResponse): MatchWithEpisodes => {\n const videoSources = transformRecordingVideoSources(match.videoSources);\n\n const defaultVideoSource = getDefaultVideoSource(videoSources);\n const recordingMatch: Match = {\n id: match.id,\n clientIds: match.clientIds,\n title: match.title,\n videoSources,\n hasHomographies: match.hasHomographies,\n segments: match.segments.map((segment) => ({\n length: round(segment.length),\n start: round(segment.start),\n segment: {\n length: round(segment.segmentType.length),\n name: segment.segmentType.name,\n start: round(segment.segmentType.start),\n },\n })),\n teams: match.teams,\n defaultVideoSource,\n showOverlays: match.showOverlays,\n };\n\n const recordingEpisodes = enrichEpisodesWithName(episodes);\n\n return {\n matchSegments: generateMatchSegments(recordingEpisodes),\n match: recordingMatch,\n episodes: recordingEpisodes,\n filters: Object.keys(filters).length === 0 ? undefined : transformFilters(filters),\n };\n};\n","export interface RecordingTeam {\n name: string;\n logo: string;\n score: number;\n}\n\nexport enum RecordingAnnotationTypes {\n all = 'ALL',\n}\n\nexport enum RecordingFilters {\n ANNOTATION_TYPE = 'annotationType',\n COMPETITION = 'competition',\n DATE = 'date',\n MATCHDAY = 'matchday',\n TEAM = 'team',\n TYPE = 'type',\n}\n","import map from 'lodash/map';\n\nimport { transformRecordingVideoSources } from 'api/match/transformers/match/matchWithEpisodes';\nimport { LinkApiResponse } from 'api/types';\nimport { APIRecording, RecordingEditAPI } from 'pages/recordings-list/api/types';\nimport { RecordingFilters } from 'pages/recordings-list/types';\nimport { FilterOptions, FiltersList } from 'shared/types/filters/types';\nimport { Pagination } from 'shared/types/pagination/types';\nimport {\n EditFormRecording,\n PlayBackTypes,\n Recording,\n RecordingByName,\n RecordingTypes,\n TacticalAnalysis,\n VideoSourceStates,\n ViewTypes,\n} from 'shared/types/recording/types';\n\nimport { RecordingByNameAPIResponse } from '../types';\n\ninterface RecordingsResponseData {\n filters: FiltersList;\n page: Pagination;\n recordings: Recording[];\n}\n\ninterface RecordingsResponse {\n data: RecordingsResponseData;\n nextCursor: number;\n}\n\nconst mapAnnotationTypeFilters = (filters: FiltersList): FiltersList => {\n const allAnnotationFiltersApplied =\n map(filters[RecordingFilters.ANNOTATION_TYPE].options, (option) => option.isApplied).every(Boolean) ||\n map(filters[RecordingFilters.ANNOTATION_TYPE].options, (option) => !option.isApplied).every(Boolean);\n\n if (allAnnotationFiltersApplied)\n Object.values(filters[RecordingFilters.ANNOTATION_TYPE].options).forEach((option) => (option.isApplied = false));\n\n const newAnnotationTypeFilterOptions: FilterOptions = {\n ALL: { title: 'All', isApplied: allAnnotationFiltersApplied, options: {} },\n };\n Object.entries(filters[RecordingFilters.ANNOTATION_TYPE].options).forEach(\n (option) => (newAnnotationTypeFilterOptions[option[0]] = option[1]),\n );\n\n filters[RecordingFilters.ANNOTATION_TYPE].options = newAnnotationTypeFilterOptions;\n return filters;\n};\n\nexport const transformRecording = (r: APIRecording): Recording => {\n const isProcessingVideo =\n Boolean(r.videoSourcesStates?.length > 0) &&\n r.videoSourcesStates.every((videoSourceState) => videoSourceState.state === VideoSourceStates.STARTED);\n\n return {\n competitionName: r.competitionName ?? '',\n duration: r.duration,\n id: r.id,\n matchDay: r.matchday ?? '',\n name: r.name,\n type: r.type as RecordingTypes,\n isLive: r.isLive,\n date: new Date(r.date),\n extraTime: r.extraTime,\n teams: r.teams,\n hasTaggingEvents: r.hasTaggingEvents,\n tacticalAnalysis: r.tacticalAnalysis as TacticalAnalysis | null,\n videoSourcesStates: r?.videoSourcesStates\n ? r.videoSourcesStates.map((videoSourceState) => ({\n playBackType: videoSourceState.playBackType as PlayBackTypes,\n viewType: videoSourceState.viewType as ViewTypes,\n state: videoSourceState.state as VideoSourceStates,\n }))\n : [],\n isProcessingVideo,\n matchReportDownloadUrl: r.matchReportDownloadUrl,\n };\n};\n\nexport const transformEditRecording = (r: RecordingEditAPI): EditFormRecording => {\n return {\n awayTeamScore: r.away_team_score,\n homeTeamScore: r.home_team_score,\n competitionName: r.competitionName ?? '',\n duration: r.duration,\n id: r.id,\n matchDay: r.matchday ?? '',\n name: r.name,\n type: r.type as RecordingTypes,\n date: new Date(r.date),\n };\n};\n\nexport const transformRecordingByName = (r: RecordingByNameAPIResponse): RecordingByName => {\n return {\n awayTeamScore: r.away_team_score,\n competitionName: r.competitionName,\n duration: r.duration,\n homeTeamScore: r.home_team_score,\n id: r.id,\n matchDay: r.matchday,\n name: r.name,\n type: r.type as RecordingTypes,\n date: new Date(r.date),\n videoSources: transformRecordingVideoSources(r.videoSources),\n hasTacticalAnalysis: r.hasTacticalAnalysis,\n hasHomographies: r.hasHomographies,\n };\n};\n\nexport interface RecordingsAPIResponse {\n cards: {\n links: LinkApiResponse[];\n content: APIRecording[];\n page: Pagination;\n };\n filters: FiltersList;\n}\n\nexport const transformRecordings = (recording: RecordingsAPIResponse): RecordingsResponse => {\n return {\n data: {\n recordings: recording.cards.content.map(transformRecording),\n page: recording.cards.page,\n filters: mapAnnotationTypeFilters(recording.filters),\n },\n nextCursor: recording.cards.page.totalPages > recording.cards.page.number ? recording.cards.page.number + 1 : 0,\n };\n};\n","import reduce from 'lodash/reduce';\n\nimport { FilterOptions, FiltersList, Playlist, PlaylistItemWithoutVideoSources, TacticIdOrAll } from 'shared/types';\nimport { Pagination } from 'shared/types/pagination/types';\n\nimport { transformRecordingByName } from '../../recording/transformers';\nimport {\n PlaylistApiResponse,\n PlaylistFiltersAPIResponse,\n PlaylistItemApiResponse,\n PlaylistsWithFiltersAPIResponse,\n} from '../types';\n\nexport const transformPlaylistItem = (\n playlistItem: PlaylistItemApiResponse,\n recordingHasHomographies = false,\n): PlaylistItemWithoutVideoSources => {\n return {\n id: playlistItem.id,\n endTime: playlistItem.endTime,\n name: playlistItem.name,\n startTime: playlistItem.startTime,\n index: playlistItem.index,\n origin: playlistItem.origin,\n recordingId: playlistItem.recordingId,\n hasHomographies: recordingHasHomographies,\n fundamentalsSelected: {\n tacticalAnalysisId: playlistItem.fundamentalsSelected.tacticalAnalysisId,\n fundamentalsSelected: playlistItem.fundamentalsSelected.fundamentalsSelected as TacticIdOrAll[],\n },\n episodesVideos: playlistItem.episodesVideos.map((episodeVideo) => ({\n startTime: episodeVideo.startTime,\n endTime: episodeVideo.endTime,\n videoSrc: {\n endTime: episodeVideo.endTime,\n endTimeInMatch: episodeVideo.endTime,\n poster: episodeVideo.videoSrc.poster,\n src: episodeVideo.videoSrc.src,\n srcDownload: episodeVideo.videoSrc.srcDownload,\n startTime: episodeVideo.startTime,\n startTimeInMatch: episodeVideo.startTime,\n type: episodeVideo.videoSrc.playBackType,\n id: episodeVideo.videoSrc.id,\n },\n })),\n };\n};\n\nexport const transformPlaylist = (data: PlaylistApiResponse): Playlist => {\n const recordingsByName = data.recordings.map((recordingItem) => transformRecordingByName(recordingItem));\n return {\n createdAt: data.createdAt,\n clientId: data.clientId,\n description: data.description,\n id: data.id,\n name: data.name,\n playlistItems: data.playlistItems.map((playlistItem) =>\n transformPlaylistItem(\n playlistItem,\n recordingsByName.find((recording) => recording.id === playlistItem.recordingId)?.hasHomographies || false,\n ),\n ),\n poster: data.poster,\n duration: data.duration,\n user: {\n userId: data.user.userId,\n firstName: data.user.firstName,\n lastName: data.user.lastName,\n },\n updatedAt: data.updatedAt,\n recordings: recordingsByName,\n teams: data.teams,\n };\n};\n\ninterface PlaylistsResponseData {\n filters: FiltersList;\n page: Pagination;\n playlists: Playlist[];\n}\n\ninterface PlaylistsResponse {\n data: PlaylistsResponseData;\n nextCursor?: number;\n}\n\nconst transformFilters = (filters: PlaylistFiltersAPIResponse) => {\n return reduce(\n filters,\n (acc, filter, filterKey) => {\n return {\n ...acc,\n [filterKey]: {\n title: filter.title,\n options: reduce(\n filter.options,\n (acc, option, optionKey) => {\n return {\n ...acc,\n [optionKey]: { title: option.title },\n };\n },\n {} as FilterOptions,\n ),\n },\n };\n },\n {} as FiltersList,\n );\n};\n\nexport const transformPlaylists = (response: PlaylistsWithFiltersAPIResponse): PlaylistsResponse => {\n return {\n data: {\n playlists: response.playlists.content.map(transformPlaylist),\n page: response.playlists.page,\n filters: transformFilters(response.filters),\n },\n nextCursor:\n response.playlists.page.totalPages > response.playlists.page.number\n ? response.playlists.page.number + 1\n : undefined,\n };\n};\n","import { UseQueryResult } from '@tanstack/react-query';\nimport { useCallback, useMemo } from 'react';\n\nimport { transformPlaylist } from 'api/playlist/transformers';\nimport { playlistUrl } from 'api/routes';\nimport { Playlist } from 'shared/types';\n\nimport { queryClient } from '../../config';\nimport { useFetchRequest } from '../../hooks/useFetchRequest';\n\nexport const generateFetchPlaylistQueryRef = (playlistId: string) => `fetchPlaylist-playlistId:${playlistId}`;\n\ntype UseGetPlaylistResult = UseQueryResult & {\n setQueryData: (...args: any[]) => void;\n};\n\ninterface Parameters {\n playlistId: string;\n onSuccess?: (data: Playlist) => void;\n}\n\nconst fetchPlaylistQueryRef = 'fetchPlaylist';\n\nexport const invalidatePlaylistQuery = () => queryClient.invalidateQueries({ queryKey: [fetchPlaylistQueryRef] });\n\nexport const usePlaylist = ({ playlistId, onSuccess }: Parameters): UseGetPlaylistResult => {\n const queryRef = useMemo(() => [fetchPlaylistQueryRef, generateFetchPlaylistQueryRef(playlistId)], [playlistId]);\n const fetchRequest = useFetchRequest({\n queryRef: [fetchPlaylistQueryRef, generateFetchPlaylistQueryRef(playlistId)],\n url: playlistUrl(playlistId),\n transformer: transformPlaylist,\n options: {\n enabled: !!playlistId,\n },\n onSuccess,\n });\n\n const setQueryData = useCallback((data: Playlist) => queryClient.setQueryData(queryRef, data), [queryRef]);\n return { ...fetchRequest, setQueryData };\n};\n","import { UserPreset } from 'shared/types/user-preset/types';\n\nexport const transformUserPreset = (data: UserPreset[]): UserPreset[] => {\n return data;\n};\n","import { UseQueryResult } from '@tanstack/react-query';\n\nimport { queryClient } from 'api/config';\nimport { useFetchRequest } from 'api/hooks/useFetchRequest';\nimport { userPresetsWithFiltersUrl } from 'api/routes';\nimport { UserPreset, UserPresetScope } from 'shared/types/user-preset/types';\n\nimport { transformUserPreset } from '../transformers';\n\ntype Result = UseQueryResult[]>;\n\ninterface Parameters {\n scope: UserPresetScope;\n onSuccess?: (data: UserPreset[]) => void;\n key?: string;\n ref?: string;\n prefix?: string;\n}\n\nconst queryRef = 'user-presets';\n\nexport const invalidateUserPresetsQuery = () => queryClient.invalidateQueries({ queryKey: [queryRef] });\n\nexport const useUserPresets = ({ scope, onSuccess, ref, key, prefix = '' }: Parameters): Result => {\n return useFetchRequest[]>({\n queryRef: [queryRef, `${queryRef}-${scope}-${key ?? ''}-${ref ?? ''}-${prefix}`],\n url: userPresetsWithFiltersUrl({ scope, ref, key }),\n transformer: transformUserPreset,\n onSuccess,\n options: {\n cacheTime: Infinity,\n staleTime: Infinity,\n },\n });\n};\n","import { z } from 'zod';\n\nimport { PRESET_SCHEMA } from 'shared/constants/user-presets/userPresetsSchema';\n\nexport type UserPresetValues = {\n [K in keyof typeof PRESET_SCHEMA]: z.TypeOf<(typeof PRESET_SCHEMA)[K]>;\n};\n\nexport type UserPresetKeysUnion = keyof UserPresetValues;\n\nexport enum UserPresetScope {\n timeline = 'timeline',\n playlist = 'playlist',\n videoPlayer = 'video-player',\n}\n\nexport interface UserPreset {\n scope: UserPresetScope;\n key: UserPresetKeysUnion;\n value: T;\n ref?: string;\n}\n","// NOTE: schema for getPreset function created based on this object\nexport const USER_PRESET_KEYS = {\n multiMatchAppliedFilters: 'multimatch-applied-filters',\n playingMode: 'playing-mode',\n selectedTactics: 'selected-tactics',\n zoomLevel: 'zoom-level',\n height: 'height',\n pinScenarios: 'pin-scenarios',\n headersWidth: 'headers-width',\n timeLineAppliedFilters: 'timeline-applied-filters',\n filters: 'filters',\n time: 'time',\n teamIdFocus: 'team-id-focus',\n showBallPossession: 'show-ball-possession',\n showNoBallPossession: 'show-no-ball-possession',\n speed: 'speed',\n} as const;\n","import { UserPresetScope } from 'shared/types/user-preset/types';\n\nimport { USER_PRESET_KEYS } from './userPresetsKeys';\n\nexport const playlistMultimatchAppliedFilters = {\n scope: UserPresetScope.playlist,\n key: USER_PRESET_KEYS.multiMatchAppliedFilters,\n} as const;\n","import { OverlayGeneratorChunkData, OverlayGeneratorStore } from '../index';\n\nconst CHUNKS_QUANTITY_TO_STORE = 10;\n\nconst getChunksToKeep = (data: OverlayGeneratorChunkData, currentChunkNumber: number): OverlayGeneratorChunkData => {\n const nextChunkNumber = currentChunkNumber + 1;\n const chunksToKeep = Object.entries(data).filter(\n ([key]) => Number(key) === currentChunkNumber || Number(key) === nextChunkNumber,\n );\n const chunksQuantityToRemove = CHUNKS_QUANTITY_TO_STORE - chunksToKeep.length;\n const chunksCleaned = Object.entries(data)\n .filter(([key]) => Number(key) !== currentChunkNumber && Number(key) !== nextChunkNumber)\n .slice(-chunksQuantityToRemove);\n\n const chunksToStore = [...chunksToKeep, ...chunksCleaned];\n\n return { ...chunksToStore.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) };\n};\n\nexport const validateChunkData = (state: OverlayGeneratorStore, currentChunkNumber: number) => {\n const chunksToKeep = getChunksToKeep(state.chunkData, currentChunkNumber);\n\n return { ...state, data: chunksToKeep };\n};\n","import { createStore, StoreApi } from 'zustand/vanilla';\nimport { Homographies, INITIAL_QUALITY, PlayersPositions, QUALITY_TO_SCALE_FACTOR, TacticId } from '../main';\nimport { OverlayElementDebugInfo } from '../overlay-canvas-renderer';\nimport { Teams } from '../overlay-canvas-renderer/types';\nimport { PitchSize } from '../types';\nimport { OverlayTactic, Segment } from '../utils/loaders';\nimport { validateChunkData } from './utils/validateChunkData';\n\nconst DEFAULT_CHUNK_SIZE = 3750;\n\nexport const DEFAULT_DATA: OverlayGeneratorChunkData = {};\n\nexport type FrameInfo = {\n frameNumber: number;\n frameTactics: TacticId[];\n overlayElementsDebugInfo: OverlayElementDebugInfo[];\n};\n\nexport type MetaData = {\n pitch: { size: PitchSize; originalSize: PitchSize };\n video: { width: number; height: number; frameRate: number; duration: number; frameCount: number };\n teams: Teams;\n segments: Segment[];\n chunkSize: number;\n};\n\nexport const DEFAULT_META_DATA: MetaData = {\n pitch: {\n size: {\n width: 0,\n length: 0,\n },\n originalSize: { width: 0, length: 0 },\n },\n segments: [],\n video: { width: 0, height: 0, frameRate: 25, duration: 0, frameCount: 0 },\n teams: { homeTeam: { id: '', playerIds: [] }, awayTeam: { id: '', playerIds: [] } },\n chunkSize: DEFAULT_CHUNK_SIZE,\n};\n\nconst INITIAL_STATUS = {\n isLoadingData: false,\n isLoadingAsyncData: false,\n};\n\nexport const INITIAL_FRAME_INFO = {\n frameNumber: 0,\n frameTactics: [],\n overlayElementsDebugInfo: [],\n};\n\ntype OverlayGeneratorData = {\n homographies: Homographies;\n playersPositions: PlayersPositions;\n overlayTactics: OverlayTactic[];\n};\n\nexport type OverlayGeneratorChunkData = {\n [key in number]:\n | {\n status: 'success';\n data: OverlayGeneratorData;\n }\n | {\n status: 'error';\n data: undefined;\n };\n};\n\ntype Status = {\n isLoadingData: boolean;\n isLoadingAsyncData: boolean;\n};\n\nexport type OverlayGeneratorStore = {\n chunkData: OverlayGeneratorChunkData;\n renderScale: number;\n tacticalAnalysisId: string;\n recordingId: string;\n metaData: MetaData;\n status: Status;\n frameInfo: FrameInfo;\n};\n\nenum ActionTypes {\n SET_SUCCESS_CHUNK_DATA = 'SET_SUCCESS_CHUNK_DATA',\n SET_ERROR_CHUNK_DATA = 'SET_ERROR_CHUNK_DATA',\n SET_TACTICAL_ANALYSIS_ID = 'SET_TACTICAL_ANALYSIS_ID',\n SET_RECORDING_ID = 'SET_RECORDING_ID',\n SET_META_DATA = 'SET_META_DATA',\n RESET = 'RESET',\n SET_RENDER_SCALE = 'SET_RENDER_SCALE',\n START_LOADING_DATA = 'START_LOADING_DATA',\n START_LOADING_ASYNC_DATA = 'START_LOADING_ASYNC_DATA',\n FINISH_LOADING_DATA = 'FINISH_LOADING_DATA',\n FINISH_LOADING_ASYNC_DATA = 'FINISH_LOADING_ASYNC_DATA',\n SET_FRAME_INFO = 'SET_FRAME_INFO',\n VALIDATE_CHUNK_DATA_MEMORY = 'VALIDATE_CHUNK_DATA_MEMORY',\n}\n\ntype Actions =\n | { type: ActionTypes.SET_SUCCESS_CHUNK_DATA; chunkNumber: number; payload: OverlayGeneratorData }\n | { type: ActionTypes.SET_ERROR_CHUNK_DATA; chunkNumber: number }\n | { type: ActionTypes.SET_TACTICAL_ANALYSIS_ID; tacticalAnalysisId: string }\n | { type: ActionTypes.SET_RECORDING_ID; recordingId: string }\n | { type: ActionTypes.SET_META_DATA; payload: MetaData }\n | { type: ActionTypes.RESET }\n | { type: ActionTypes.START_LOADING_DATA }\n | { type: ActionTypes.START_LOADING_ASYNC_DATA }\n | { type: ActionTypes.FINISH_LOADING_DATA }\n | { type: ActionTypes.FINISH_LOADING_ASYNC_DATA }\n | { type: ActionTypes.SET_FRAME_INFO; frameInfo: FrameInfo }\n | { type: ActionTypes.SET_RENDER_SCALE; renderScale: number }\n | { type: ActionTypes.VALIDATE_CHUNK_DATA_MEMORY; chunkNumber: number };\n\nconst reducer = (state: OverlayGeneratorStore, action: Actions): OverlayGeneratorStore => {\n switch (action.type) {\n case ActionTypes.SET_SUCCESS_CHUNK_DATA:\n return {\n ...state,\n chunkData: { ...state.chunkData, [action.chunkNumber]: { status: 'success', data: action.payload } },\n };\n case ActionTypes.SET_ERROR_CHUNK_DATA:\n return {\n ...state,\n chunkData: { ...state.chunkData, [action.chunkNumber]: { status: 'error', data: undefined } },\n };\n case ActionTypes.SET_TACTICAL_ANALYSIS_ID:\n return { ...state, tacticalAnalysisId: action.tacticalAnalysisId, chunkData: DEFAULT_DATA };\n case ActionTypes.SET_RECORDING_ID:\n return { ...state, recordingId: action.recordingId, chunkData: DEFAULT_DATA };\n case ActionTypes.SET_META_DATA:\n return { ...state, metaData: action.payload };\n case ActionTypes.SET_RENDER_SCALE:\n return { ...state, renderScale: action.renderScale };\n case ActionTypes.START_LOADING_DATA:\n return { ...state, status: { ...state.status, isLoadingData: true } };\n case ActionTypes.START_LOADING_ASYNC_DATA:\n return { ...state, status: { ...state.status, isLoadingAsyncData: true } };\n case ActionTypes.FINISH_LOADING_DATA:\n return { ...state, status: { ...state.status, isLoadingData: false } };\n case ActionTypes.FINISH_LOADING_ASYNC_DATA:\n return { ...state, status: { ...state.status, isLoadingAsyncData: false } };\n case ActionTypes.SET_FRAME_INFO:\n return { ...state, frameInfo: action.frameInfo };\n case ActionTypes.VALIDATE_CHUNK_DATA_MEMORY:\n return validateChunkData(state, action.chunkNumber);\n case ActionTypes.RESET:\n return { ...state, chunkData: DEFAULT_DATA, metaData: DEFAULT_META_DATA, status: INITIAL_STATUS };\n default:\n return state;\n }\n};\n\ntype StoreActions = {\n setChunkData: (chunkNumber: number, payload: OverlayGeneratorData) => void;\n setErrorChunkData: (chunkNumber: number) => void;\n setTacticalAnalysisId: (tacticalAnalysisId: string) => void;\n setRecordingId: (recordingId: string) => void;\n setMetaData: (payload: MetaData) => void;\n reset: () => void;\n setRenderScale: (renderScale: number) => void;\n setFrameInfo: (frameInfo: FrameInfo) => void;\n startLoadingData: () => void;\n startLoadingAsyncData: () => void;\n finishLoadingData: () => void;\n finishLoadingAsyncData: () => void;\n validateChunkDataMemory: (chunkNumber: number) => void;\n};\n\nconst initStore = (): { store: StoreApi; actions: StoreActions } => {\n const store = createStore(() => ({\n chunkData: DEFAULT_DATA,\n metaData: DEFAULT_META_DATA,\n tacticalAnalysisId: '',\n recordingId: '',\n renderScale: QUALITY_TO_SCALE_FACTOR[INITIAL_QUALITY],\n status: INITIAL_STATUS,\n frameInfo: INITIAL_FRAME_INFO,\n }));\n\n const { getState, setState } = store;\n\n const actions = {\n setChunkData: (chunkNumber: number, payload: OverlayGeneratorData) =>\n setState(\n reducer(getState(), {\n type: ActionTypes.SET_SUCCESS_CHUNK_DATA,\n chunkNumber: chunkNumber,\n payload,\n }),\n ),\n setErrorChunkData: (chunkNumber: number) =>\n setState(\n reducer(getState(), {\n type: ActionTypes.SET_ERROR_CHUNK_DATA,\n chunkNumber: chunkNumber,\n }),\n ),\n setTacticalAnalysisId: (tacticalAnalysisId: string) =>\n setState(reducer(getState(), { type: ActionTypes.SET_TACTICAL_ANALYSIS_ID, tacticalAnalysisId })),\n setRecordingId: (recordingId: string) =>\n setState(reducer(getState(), { type: ActionTypes.SET_RECORDING_ID, recordingId })),\n setMetaData: (payload: MetaData) => setState(reducer(getState(), { type: ActionTypes.SET_META_DATA, payload })),\n reset: () => setState(reducer(getState(), { type: ActionTypes.RESET })),\n setRenderScale: (renderScale: number) =>\n setState(reducer(getState(), { type: ActionTypes.SET_RENDER_SCALE, renderScale })),\n startLoadingData: () => setState(reducer(getState(), { type: ActionTypes.START_LOADING_DATA })),\n finishLoadingData: () => setState(reducer(getState(), { type: ActionTypes.FINISH_LOADING_DATA })),\n startLoadingAsyncData: () => setState(reducer(getState(), { type: ActionTypes.START_LOADING_ASYNC_DATA })),\n finishLoadingAsyncData: () => setState(reducer(getState(), { type: ActionTypes.FINISH_LOADING_ASYNC_DATA })),\n setFrameInfo: (frameInfo: FrameInfo) =>\n setState(reducer(getState(), { type: ActionTypes.SET_FRAME_INFO, frameInfo })),\n validateChunkDataMemory: (chunkNumber: number) =>\n setState(reducer(getState(), { type: ActionTypes.VALIDATE_CHUNK_DATA_MEMORY, chunkNumber: chunkNumber })),\n };\n\n return { actions, store };\n};\n\nexport { initStore };\n","import { OverlayGeneratorChunkData } from '../index';\n\nexport const preloadData = (\n data: OverlayGeneratorChunkData,\n currentChunkNumber: number,\n currentFrame: number,\n chunkSize: number,\n) => {\n const percentageOfChunkToStartPreload = 0.5;\n const isNextChunkLoaded = data[currentChunkNumber + 1];\n const shouldPreloadNextChunk = currentFrame % chunkSize > chunkSize * percentageOfChunkToStartPreload;\n\n return !isNextChunkLoaded && shouldPreloadNextChunk;\n};\n","export const getChunkNumberFromFrameAndChunkSize = (frame: number, chunkSize: number) => {\n return Math.floor(frame / chunkSize);\n};\n","export const getVideoScale = (videoWidth: number, newWidth: number) => {\n return newWidth / videoWidth;\n};\n","interface MakeFetchParams {\n url: string;\n fetchInterface: any;\n headers?: HeadersInit;\n}\n\nexport const makeFetch = async ({ url, headers, fetchInterface }: MakeFetchParams): Promise => {\n const response = await fetch(url, { headers });\n\n if (response.status !== 200) {\n throw new Error(`Error fetching ${url}, status: ${response.status}, statusText: ${response.statusText}`);\n }\n\n return response.json();\n};\n","import { Homographies, TimeSeries } from '../main';\n\nexport const transformTimeSeries = (timeSeries: TimeSeries) => {\n const homographies: Homographies = {};\n\n for (const [frame, series] of Object.entries(timeSeries)) {\n homographies[Number(frame)] = [\n [series[1], series[2], series[3]],\n [series[4], series[6], series[7]],\n [series[8], series[9], series[10]],\n ];\n }\n\n return homographies;\n};\n","import { Duration } from 'luxon';\nimport { Homographies, PlayersPositions, TacticId, TimeSeries } from '../main';\nimport { OverlayElementGlyphs } from '../overlay-canvas-renderer/overlay-elements/interface';\nimport { Teams } from '../overlay-canvas-renderer/types';\nimport { OverlayElementNames } from '../types';\nimport { makeFetch } from './makeFetch';\nimport { transformTimeSeries } from './transformTimeSeries';\n\nexport interface RecordingOverlayData {\n overlayTactics: OverlayTactic[];\n trajectories: PlayersPositions;\n homographies: Homographies;\n}\n\ninterface OverlayData {\n overlayTactics: OverlayTactic[];\n trajectories: PlayersPositions;\n timeSeries: TimeSeries;\n}\n\nexport interface OverlayTactic {\n tacticTypeId: TacticId;\n startFrame: number;\n endFrame: number;\n overlayElements: OverlayElement[];\n}\n\nexport interface OverlayTacticWithGlyphs {\n tacticId: TacticId;\n startFrame: number;\n endFrame: number;\n overlayElementsGlyphs: OverlayElementGlyphs[];\n}\n\nexport interface OverlayElement {\n overlayElementTypeId: OverlayElementNames;\n startFrame: number;\n endFrame: number;\n references: Reference[];\n}\n\nexport type Reference =\n | {\n referenceType: ReferenceType.Players;\n values: string[];\n }\n | {\n referenceType: ReferenceType.StaticCoordinates;\n values: [[number, number][]];\n };\n\nexport enum ReferenceType {\n Players = 'players',\n StaticCoordinates = 'static-coordinates',\n}\n\nexport type Segment = {\n segmentType: {\n name: string;\n start: number;\n length: number;\n };\n startFrame: number;\n endFrame: number;\n pitchLeftSideTeamId: string;\n pitchRightSideTeamId: string;\n};\n\ninterface OverlayElementsMetaDataApi {\n pitchSize: { length: number; width: number };\n segments: Segment[];\n video: { height: number; width: number; frameRate: number; duration: string };\n teams: Teams;\n}\n\nexport interface OverlayElementsMetaData extends OverlayElementsMetaDataApi {\n video: { height: number; width: number; frameRate: number; duration: string; frameCount: number };\n}\n\ninterface LoadMetaDataParams {\n recordingId: string;\n domainUrl?: string;\n headers?: HeadersInit;\n fetchInterface: any;\n}\n\nexport const loadMetaData = async ({\n recordingId,\n domainUrl = '',\n headers,\n fetchInterface,\n}: LoadMetaDataParams): Promise => {\n const overlayMetaData = await makeFetch({\n url: `${domainUrl}/api/overlay-elements-metadata?recordingId=${recordingId}`,\n headers,\n fetchInterface,\n });\n\n return {\n ...overlayMetaData,\n video: {\n ...overlayMetaData.video,\n frameCount:\n (Duration.fromISO(overlayMetaData.video.duration).toMillis() / 1000) * overlayMetaData.video.frameRate,\n },\n };\n};\n\ninterface LoadChunkParams {\n tacticalAnalysisId: string;\n startFrame: number;\n endFrame: number;\n domainUrl?: string;\n headers?: HeadersInit;\n fetchInterface: any;\n}\n\nexport const loadChunk = async ({\n tacticalAnalysisId,\n startFrame,\n endFrame,\n domainUrl = '',\n headers,\n fetchInterface,\n}: LoadChunkParams) => {\n const { overlayTactics, trajectories, timeSeries } = await makeFetch({\n url: `${domainUrl}/api/overlay-data/${tacticalAnalysisId}?startFrame=${startFrame}&endFrame=${endFrame}&smoothingEnabled=true`,\n headers,\n fetchInterface,\n });\n\n const homographies = transformTimeSeries(timeSeries);\n\n return {\n homographies,\n trajectories,\n overlayTactics,\n };\n};\n","import { multiply } from 'mathjs';\nimport { Homography } from '../types';\n\n// Function that given a 3x3 homography matrix it returns the matrix upscale by a value\nexport const scaleMatrix = (matrix: number[][], matrixScale: number, videoScale: number): Homography => {\n const upscaleMatrix = [\n [1 / matrixScale, 0, 0],\n [0, 1 / matrixScale, 0],\n [0, 0, 1],\n ];\n\n const videoScaleMatrix = [\n [videoScale, 0, 0],\n [0, videoScale, 0],\n [0, 0, 1],\n ];\n\n const adjustedByVideoScale = multiply(videoScaleMatrix, matrix);\n\n return multiply(adjustedByVideoScale, upscaleMatrix) as Homography;\n};\n","export const transformHomographyIntoMatrix3d = (h: number[][]) => {\n return [h[0][0], h[1][0], 0, h[2][0], h[0][1], h[1][1], 0, h[2][1], 0, 0, 1, 0, h[0][2], h[1][2], 0, h[2][2]];\n};\n","import { OverlayRenderer } from './overlay-canvas-renderer';\nimport { initStore, MetaData, OverlayGeneratorChunkData } from './store';\nimport { preloadData } from './store/utils/preloadData';\nimport { Coordinates, Events, Homography, Size } from './types';\n\nimport { LogEvent, Time } from './utils/decorators';\nimport { getChunkNumberFromFrameAndChunkSize } from './utils/getChunkNumberFromFrameAndChunkSize';\nimport { getVideoScale } from './utils/getVideoScale';\nimport { loadChunk, loadMetaData, OverlayElementsMetaData, RecordingOverlayData } from './utils/loaders';\nimport { scaleMatrix } from './utils/scaleMatrix';\nimport { transformHomographyIntoMatrix3d } from './utils/transformHomographyIntoMatrix3d';\n\nexport const offensiveTactics = [\n 'accompany-play-team-together',\n 'balance-of-the-team-after-recovery',\n 'cross-into-the-box',\n 'finishing',\n 'finishing-after-cross',\n 'finishing-pass',\n 'goal',\n 'goal-assist',\n 'goal-chance',\n 'goal-kick-start-long-inside-channels',\n 'goal-kick-start-long-outside-channels',\n 'goal-kick-start-short-inside-channels',\n 'goal-kick-start-short-outside-channels',\n 'identifying-passing-lines-under-pressure',\n 'long-ball',\n 'lost-ball',\n 'moving-behind-the-defensive-line',\n 'occupying-space-in-the-box',\n 'open-passing-lines-after-long-ball',\n 'overcoming-opponents-with-vertical-passes',\n 'passing-between-lines',\n 'pass-behind-defensive-line',\n 'positioning-behind-center-backs-when-lateral-balls',\n 'possession-after-recovery',\n 'progression-after-recovery',\n 'realized-emergency-support',\n 'realized-finishing-support',\n 'realized-horizontal-overcoming-support',\n 'realized-striker-support',\n 'realized-vertical-overcoming-support',\n 'receive-foul-after-recovery',\n 'receiving-between-lines',\n 'receiving-positioning-between-lines',\n 'running-into-the-box',\n 'second-ball-offensive-winning-after-cross',\n 'second-ball-offensive-winning-after-direct-play',\n 'second-ball-offensive-winning-after-finishing',\n 'second-ball-offensive-winning-after-set-piece',\n 'space-between-defensive-line-and-halfway-line',\n 'supports',\n 'switch-of-play',\n 'taking-advantage-of-defensive-line-imbalances',\n 'width-of-the-team',\n 'width-of-the-team-opposite-channel',\n] as const;\n\nexport const defensiveTactics = [\n 'balance-of-the-team',\n 'balance-of-the-team-after-loss',\n 'clear-the-box',\n 'commit-foul-after-loss',\n 'compactness-of-team',\n 'defending-against-the-possessor',\n 'defending-moving-behind-the-defensive-line',\n 'defending-running-into-the-box',\n 'defensive-line-imbalance-in-depth',\n 'defensive-line-imbalance-in-width',\n 'hold-after-loss',\n 'marking-opponents-inside-the-box',\n 'marking-supports',\n 'moving-forward-during-organized-pressure',\n 'neutralizing-opponent-advantage-of-defensive-line-imbalance',\n 'press-after-loss',\n 'pressure-on-the-ball-possessor',\n 'recovered-ball',\n 'second-ball-defensive-winning-after-cross',\n 'second-ball-defensive-winning-after-direct-play',\n 'second-ball-defensive-winning-after-finishing',\n 'second-ball-defensive-winning-after-set-piece',\n 'tackle',\n] as const;\n\nexport type OffensiveTacticId = (typeof offensiveTactics)[number];\nexport type DefensiveTacticId = (typeof defensiveTactics)[number];\n\nexport type TacticId = OffensiveTacticId | DefensiveTacticId;\n\nexport type PlayersPosition = { [key in string]: Coordinates };\nexport type PlayersPositions = { [key in string]: PlayersPosition };\nexport type Homographies = { [key in number]: Homography };\nexport type TimeSeries = { [key in number]: Array };\n\nexport enum Quality {\n LOW = 'low',\n MEDIUM = 'medium',\n HIGH = 'high',\n VERY_HIGH = 'veryHigh',\n}\n\nexport const INITIAL_QUALITY = Quality.VERY_HIGH;\n\nexport const QUALITY_TO_SCALE_FACTOR = {\n [Quality.LOW]: 8,\n [Quality.MEDIUM]: 10,\n [Quality.HIGH]: 12,\n [Quality.VERY_HIGH]: 16,\n};\n\nexport const IDENTIFY_TRANSFORMATION_MATRIX = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0];\nconst CHUNK_SIZE = 3750;\n\nexport interface OverlayGeneratorConfig {\n domainUrl?: string;\n headers?: HeadersInit;\n imageInterface: unknown;\n fetchInterface: unknown;\n useContainer?: boolean;\n recordingData?: RecordingOverlayData | undefined;\n overlayElementsMetaData?: OverlayElementsMetaData | undefined;\n}\n\nexport class OverlayGenerator {\n overlayRendered: OverlayRenderer;\n store = initStore();\n config: OverlayGeneratorConfig;\n overlayElementsMetaData?: OverlayElementsMetaData | undefined;\n\n constructor(config: OverlayGeneratorConfig) {\n this.config = config;\n this.overlayRendered = new OverlayRenderer({\n imageInterface: this.config.imageInterface,\n useContainer: this.config.useContainer ?? true,\n });\n }\n\n @LogEvent({ type: Events.INIT })\n async init({ tacticalAnalysisId, recordingId }: { tacticalAnalysisId: string; recordingId: string }) {\n if (this.getTacticalAnalysisId() !== tacticalAnalysisId || this.getRecordingId() !== recordingId) {\n this.overlayRendered = new OverlayRenderer({\n imageInterface: this.config.imageInterface,\n useContainer: this.config.useContainer ?? true,\n });\n this.store.actions.setTacticalAnalysisId(tacticalAnalysisId);\n this.store.actions.setRecordingId(recordingId);\n }\n\n return await this.loadMetadata();\n }\n\n @Time()\n @LogEvent({ type: Events.DRAW_FRAME_IN_CANVAS })\n async drawFrameInCanvas(\n container: HTMLDivElement,\n frame: number,\n options: {\n tactics?: TacticId[];\n },\n ) {\n if (this.store.store.getState().status.isLoadingData) return false;\n const currentFrameChunkNumber = this.getChunkNumberFromFrame(frame);\n const isCurrentFrameChunkLoaded = Object.keys(this.getChunksData()).includes(currentFrameChunkNumber.toString());\n const shouldPreloadNextChunk = preloadData(\n this.getChunksData(),\n currentFrameChunkNumber,\n frame,\n this.getChunkSize(),\n );\n\n if (!isCurrentFrameChunkLoaded && !this.store.store.getState().status.isLoadingData) {\n // TODO How to handle when trajectories are not available?\n await this.load(currentFrameChunkNumber);\n this.store.actions.validateChunkDataMemory(currentFrameChunkNumber);\n }\n\n if (shouldPreloadNextChunk && !this.store.store.getState().status.isLoadingAsyncData) {\n this.load(currentFrameChunkNumber + 1, true);\n }\n\n const metaData = this.getMetaData();\n const frameInfo = this.overlayRendered.renderFrame({\n frame,\n playersPositions: this.getTrajectories(currentFrameChunkNumber),\n scale: this.getRenderScale(),\n overlayTactics: this.getOverlayTactics(currentFrameChunkNumber),\n filters: { tactics: options.tactics },\n pitchSize: metaData.pitch.originalSize,\n segments: metaData.segments,\n teams: metaData.teams,\n container,\n });\n\n this.store.actions.setFrameInfo(frameInfo);\n }\n\n getHomography(frame: number): Homography | undefined {\n const chunkNumber = this.getChunkNumberFromFrame(frame);\n const homographies = this.getHomographies(chunkNumber);\n return homographies[frame];\n }\n\n getTransformationMatrix(frame: number, videoSourceSize: Size): number[] | undefined {\n const metaData = this.getMetaData();\n const videoScale = getVideoScale(metaData.video.width, videoSourceSize.width);\n\n return this.generateTransformationMatrix(frame, videoScale);\n }\n\n @LogEvent({ type: Events.SET_RECORDING_DATA })\n reset() {\n this.config.recordingData = undefined;\n this.config.overlayElementsMetaData = undefined;\n this.store.actions.reset();\n }\n\n @LogEvent({ type: Events.SET_RECORDING_DATA })\n setRecordingData(recordingData?: RecordingOverlayData | undefined) {\n if (!recordingData) return;\n this.config.recordingData = recordingData;\n }\n\n @LogEvent({ type: Events.OVERLAY_ELEMENTS_META_DATA })\n setOverlayElementsMetaData(overlayElementsMetaData?: OverlayElementsMetaData | undefined) {\n if (!overlayElementsMetaData) return;\n this.config.overlayElementsMetaData = overlayElementsMetaData;\n }\n\n @LogEvent({ type: Events.CHANGE_QUALITY })\n setQuality(quality: Quality) {\n if (!quality) return;\n this.setRenderScale(QUALITY_TO_SCALE_FACTOR[quality]);\n }\n\n getMetaData() {\n return this.store.store.getState().metaData;\n }\n\n @LogEvent({ type: Events.CHANGE_RENDER_SCALE })\n setRenderScale(renderScale: number) {\n const metaData = this.getMetaData();\n const updatedMetadata = {\n ...metaData,\n pitch: {\n ...metaData.pitch,\n size: {\n width: metaData.pitch.originalSize.width * renderScale,\n length: metaData.pitch.originalSize.length * renderScale,\n },\n },\n };\n this.setMetaData(updatedMetadata);\n this.store.actions.setRenderScale(renderScale);\n }\n\n @LogEvent({ type: Events.LOAD_METADATA })\n private async loadMetadata() {\n const resultMetaData = this.config.overlayElementsMetaData\n ? this.config.overlayElementsMetaData\n : await loadMetaData({\n recordingId: this.getRecordingId(),\n domainUrl: this.config.domainUrl,\n headers: this.config.headers,\n fetchInterface: this.config.fetchInterface,\n });\n\n const metaData = {\n pitch: {\n size: {\n length: resultMetaData.pitchSize.length * this.getRenderScale(),\n width: resultMetaData.pitchSize.width * this.getRenderScale(),\n },\n originalSize: {\n length: resultMetaData.pitchSize.length,\n width: resultMetaData.pitchSize.width,\n },\n },\n segments: resultMetaData.segments,\n teams: resultMetaData.teams,\n video: { ...resultMetaData.video, duration: 0 },\n chunkSize: CHUNK_SIZE,\n };\n\n this.setMetaData(metaData);\n }\n\n @LogEvent({ type: Events.LOAD })\n private async load(chunkNumber: number = 0, async: boolean = false) {\n if (!async) this.startDataLoading();\n if (async) this.startDataLoadingAsync();\n const startFrame = chunkNumber * this.getMetaData().chunkSize;\n const endFrame = startFrame + this.getMetaData().chunkSize;\n try {\n const { homographies, trajectories, overlayTactics } = this.config.recordingData\n ? this.config.recordingData\n : await loadChunk({\n domainUrl: this.config?.domainUrl,\n tacticalAnalysisId: this.getTacticalAnalysisId(),\n startFrame,\n endFrame,\n headers: this.config.headers,\n fetchInterface: this.config.fetchInterface,\n });\n\n this.store.actions.setChunkData(chunkNumber, {\n homographies,\n playersPositions: trajectories,\n overlayTactics,\n });\n } catch (e) {\n console.error(`Error loading chunk ${chunkNumber} - ${e}`);\n this.store.actions.setErrorChunkData(chunkNumber);\n }\n\n if (!async) this.endDataLoading();\n if (async) this.endDataLoadingAsync();\n }\n\n @LogEvent({ type: Events.START_LOADING_DATA })\n private startDataLoading() {\n this.store.actions.startLoadingData();\n }\n\n @LogEvent({ type: Events.START_LOADING_ASYNC_DATA })\n private startDataLoadingAsync() {\n this.store.actions.startLoadingAsyncData();\n }\n\n @LogEvent({ type: Events.END_LOADING_DATA })\n private endDataLoading() {\n this.store.actions.finishLoadingData();\n }\n\n @LogEvent({ type: Events.END_LOADING_ASYNC_DATA })\n private endDataLoadingAsync() {\n this.store.actions.finishLoadingAsyncData();\n }\n\n @LogEvent({ type: Events.UPDATE_METADATA })\n private setMetaData(metaData: MetaData) {\n this.store.actions.setMetaData(metaData);\n }\n\n private generateTransformationMatrix(frame: number, videoScale: number) {\n const homographies = this.getHomographies(this.getChunkNumberFromFrame(frame));\n\n if (!homographies || !homographies[frame]) {\n return;\n }\n\n const scaledMatrix = scaleMatrix(homographies[frame], this.getRenderScale(), videoScale);\n return transformHomographyIntoMatrix3d(scaledMatrix);\n }\n\n private getChunkNumberFromFrame = (frame: number) => {\n return getChunkNumberFromFrameAndChunkSize(frame, this.getChunkSize());\n };\n\n private getTacticalAnalysisId = () => {\n return this.store.store.getState().tacticalAnalysisId;\n };\n\n private getRecordingId = () => {\n return this.store.store.getState().recordingId;\n };\n\n private getChunksData = (): OverlayGeneratorChunkData => {\n return this.store.store.getState().chunkData;\n };\n\n private getChunkSize = () => {\n return this.store.store.getState().metaData.chunkSize;\n };\n\n private getRenderScale() {\n return this.store.store.getState().renderScale;\n }\n\n private getHomographies(chunkNumber: number) {\n return this.getChunksData()[chunkNumber]?.data?.homographies ?? [];\n }\n\n private getTrajectories(chunkNumber: number) {\n return this.getChunksData()[chunkNumber]?.data?.playersPositions ?? {};\n }\n\n private getOverlayTactics(chunkNumber: number) {\n return this.getChunksData()[chunkNumber]?.data?.overlayTactics ?? [];\n }\n}\n","import Konva from 'konva';\nimport { IFrame } from 'konva/lib/types';\nimport { useCallback, useEffect, useMemo } from 'react';\nimport { create, StoreApi, UseBoundStore } from 'zustand';\nimport { OverlayGenerator, Quality } from '../main';\nimport { OverlayGeneratorStore } from '../store';\nimport { OverlayElementsMetaData, RecordingOverlayData } from '../utils/loaders';\n\ninterface UseOverlayGeneratorParams {\n id?: string;\n recordingData?: RecordingOverlayData;\n overlayElementsMetaData?: OverlayElementsMetaData;\n containerRef?: React.RefObject;\n}\n\nconst cache: {\n [key in string]: {\n overlayGenerator: OverlayGenerator;\n useStore: UseBoundStore>;\n };\n} = {};\n\nexport const useOverlayGeneratorById = (\n id: string,\n): [OverlayGenerator, UseBoundStore>] => {\n return useMemo(() => {\n if (!cache[id]) {\n const generator = new OverlayGenerator({\n imageInterface: Image,\n fetchInterface: fetch,\n });\n const useStore = create(generator.store.store);\n\n cache[id] = { overlayGenerator: generator, useStore };\n return [generator, useStore];\n }\n\n return [cache[id].overlayGenerator, cache[id].useStore];\n }, [id]);\n};\n\nexport const useOverlayGenerator = ({\n id = 'default-overlay-generator',\n recordingData,\n overlayElementsMetaData,\n}: UseOverlayGeneratorParams = {}) => {\n const [overlayGenerator, useStore] = useOverlayGeneratorById(id);\n\n useEffect(() => {\n recordingData && overlayGenerator.setRecordingData(recordingData);\n overlayElementsMetaData && overlayGenerator.setOverlayElementsMetaData(overlayElementsMetaData);\n\n return () => {\n overlayGenerator.reset();\n };\n }, [overlayGenerator, recordingData, overlayElementsMetaData]);\n\n const frameRate = useStore((state) => state.metaData.video.frameRate);\n const isReady = useStore((state) => !state.status.isLoadingData);\n const frameInfo = useStore((state) => state.frameInfo);\n\n const createAnimation = useCallback(\n (callback: (frame?: IFrame) => void) => {\n return new Konva.Animation((frame) => {\n return callback(frame);\n }, overlayGenerator.overlayRendered.stage);\n },\n [overlayGenerator.overlayRendered.stage],\n );\n\n const changeQuality = useCallback(\n (quality: Quality) => {\n if (!quality) return;\n\n overlayGenerator.setQuality(quality);\n },\n [overlayGenerator],\n );\n\n return {\n overlayGenerator,\n isReady,\n frameRate,\n frameInfo,\n changeQuality,\n createAnimation,\n };\n};\n","import { OverlayElementNames } from '../types';\nimport { ReferenceType } from '../utils/loaders';\n\nexport enum OffensiveTacticNames {\n ACCOMPANY_PLAY_TEAM_TOGETHER = 'accompany-play-team-together',\n FINISHING = 'finishing',\n FINISHING_PASS = 'finishing-pass',\n IDENTIFYING_PASSING_LINES_UNDER_PRESSURE = 'identifying-passing-lines-under-pressure',\n LONG_BALL = 'long-ball',\n MOVING_BEHIND_THE_DEFENSIVE_LINE = 'moving-behind-the-defensive-line',\n OCCUPYING_SPACE_IN_THE_BOX = 'occupying-space-in-the-box',\n OPEN_PASSING_LINES_AFTER_LONG_BALL = 'open-passing-lines-after-long-ball',\n OVERCOMING_OPPONENTS_WITH_VERTICAL_PASSES = 'overcoming-opponents-with-vertical-passes',\n POSITIONING_BEHIND_CENTER_BACKS_WHEN_LATERAL_BALLS = 'positioning-behind-center-backs-when-lateral-balls',\n REALIZED_EMERGENCY_SUPPORT = 'realized-emergency-support',\n REALIZED_STRIKER_SUPPORT = 'realized-striker-support',\n REALIZED_FINISHING_SUPPORT = 'realized-finishing-support',\n REALIZED_HORIZONTAL_OVERCOMING_SUPPORT = 'realized-horizontal-overcoming-support',\n REALIZED_VERTICAL_OVERCOMING_SUPPORT = 'realized-vertical-overcoming-support',\n RECEIVING_BETWEEN_LINES = 'receiving-between-lines',\n RECEIVING_POSITIONING_BETWEEN_LINES = 'receiving-positioning-between-lines',\n PASSING_BETWEEN_LINES = 'passing-between-lines',\n SPACE_BETWEEN_DEFENSIVE_LINE_AND_HALFWAY_LINE = 'space-between-defensive-line-and-halfway-line',\n SUPPORTS = 'supports',\n SWITCH_OF_PLAY = 'switch-of-play',\n BALANCE_OF_THE_TEAM_AFTER_RECOVERY = 'balance-of-the-team-after-recovery',\n TAKING_ADVANTAGE_OF_DEFENSIVE_LINE_IMBALANCES = 'taking-advantage-of-defensive-line-imbalances',\n WIDTH_OF_THE_TEAM = 'width-of-the-team',\n WIDTH_OF_THE_TEAM_OPPOSITE_CHANNEL = 'width-of-the-team-opposite-channel',\n GOAL_KICK_START_LONG_OUTSIDE_CHANNELS = 'goal-kick-start-long-outside-channels',\n GOAL_KICK_START_LONG_INSIDE_CHANNELS = 'goal-kick-start-long-inside-channels',\n GOAL_KICK_START_SHORT_OUTSIDE_CHANNELS = 'goal-kick-start-short-outside-channels',\n GOAL_KICK_START_SHORT_INSIDE_CHANNELS = 'goal-kick-start-short-inside-channels',\n CROSS_INTO_THE_BOX = 'cross-into-the-box',\n FINISHING_AFTER_CROSS = 'finishing-after-cross',\n PROGRESSION_AFTER_RECOVERY = 'progression-after-recovery',\n POSSESSION_AFTER_RECOVERY = 'possession-after-recovery',\n RECEIVE_FOUL_AFTER_RECOVERY = 'receive-foul-after-recovery',\n GOAL_CHANCE = 'goal-chance',\n GOAL = 'goal',\n GOAL_ASSIST = 'goal-assist',\n LOST_BALL = 'lost-ball',\n RUNNING_INTO_THE_BOX = 'running-into-the-box',\n SECOND_BALL_OFFENSIVE_WINNING_AFTER_DIRECT_PLAY = 'second-ball-offensive-winning-after-direct-play',\n SECOND_BALL_OFFENSIVE_WINNING_AFTER_FINISHING = 'second-ball-offensive-winning-after-finishing',\n SECOND_BALL_OFFENSIVE_WINNING_AFTER_CROSS = 'second-ball-offensive-winning-after-cross',\n SECOND_BALL_OFFENSIVE_WINNING_AFTER_SET_PIECE = 'second-ball-offensive-winning-after-set-piece',\n PASS_BEHIND_DEFENSIVE_LINE = 'pass-behind-defensive-line',\n}\n\nexport enum DefensiveTacticNames {\n BALANCE_OF_THE_TEAM = 'balance-of-the-team',\n BALANCE_OF_THE_TEAM_AFTER_LOSS = 'balance-of-the-team-after-loss',\n COMPACTNESS_OF_TEAM = 'compactness-of-team',\n DEFENDING_AGAINST_THE_POSSESSOR = 'defending-against-the-possessor',\n DEFENSIVE_LINE_IMBALANCE_IN_DEPTH = 'defensive-line-imbalance-in-depth',\n DEFENSIVE_LINE_IMBALANCE_IN_WIDTH = 'defensive-line-imbalance-in-width',\n MARKING_OPPONENTS_INSIDE_THE_BOX = 'marking-opponents-inside-the-box',\n MARKING_SUPPORTS = 'marking-supports',\n NEUTRALIZING_OPPONENT_ADVANTAGE_OF_DEFENSIVE_LINE_IMBALANCE = 'neutralizing-opponent-advantage-of-defensive-line-imbalance',\n HOLD_AFTER_LOSS = 'hold-after-loss',\n PRESS_AFTER_LOSS = 'press-after-loss',\n COMMIT_FOUL_AFTER_LOSS = 'commit-foul-after-loss',\n RECOVERED_BALL = 'recovered-ball',\n TACKLE = 'tackle',\n PRESSURE_ON_THE_BALL_POSSESSOR = 'pressure-on-the-ball-possessor',\n MOVING_FORWARD_DURING_ORGANIZED_PRESSURE = 'moving-forward-during-organized-pressure',\n DEFENDING_MOVING_BEHIND_THE_DEFENSIVE_LINE = 'defending-moving-behind-the-defensive-line',\n DEFENDING_RUNNING_INTO_THE_BOX = 'defending-running-into-the-box',\n CLEAR_THE_BOX = 'clear-the-box',\n SECOND_BALL_DEFENSIVE_WINNING_AFTER_DIRECT_PLAY = 'second-ball-defensive-winning-after-direct-play',\n SECOND_BALL_DEFENSIVE_WINNING_AFTER_FINISHING = 'second-ball-defensive-winning-after-finishing',\n SECOND_BALL_DEFENSIVE_WINNING_AFTER_CROSS = 'second-ball-defensive-winning-after-cross',\n SECOND_BALL_DEFENSIVE_WINNING_AFTER_SET_PIECE = 'second-ball-defensive-winning-after-set-piece',\n}\n\nexport type OverlayElement = {\n overlayElementTypeId: OverlayElementNames;\n references: {\n referenceType: ReferenceType;\n }[];\n};\n\nexport type TacticConfig = {\n name: OffensiveTacticNames | DefensiveTacticNames;\n overlayElements: OverlayElement[];\n};\n\nexport type TacticsConfig = {\n [key in OffensiveTacticNames | DefensiveTacticNames]?: TacticConfig;\n};\n\nexport const tacticsConfig: TacticsConfig = {\n [OffensiveTacticNames.WIDTH_OF_THE_TEAM]: {\n name: OffensiveTacticNames.WIDTH_OF_THE_TEAM,\n overlayElements: [\n {\n overlayElementTypeId: OverlayElementNames.WIDE_PLAYER_HIGHLIGHT,\n references: [\n {\n referenceType: ReferenceType.Players,\n },\n ],\n },\n ],\n },\n [DefensiveTacticNames.PRESSURE_ON_THE_BALL_POSSESSOR]: {\n name: DefensiveTacticNames.PRESSURE_ON_THE_BALL_POSSESSOR,\n overlayElements: [\n {\n overlayElementTypeId: OverlayElementNames.DEFENDER_PRESSING_POSSESSOR_HIGHLIGHT,\n references: [\n {\n referenceType: ReferenceType.Players,\n },\n ],\n },\n ],\n },\n [DefensiveTacticNames.MOVING_FORWARD_DURING_ORGANIZED_PRESSURE]: {\n name: DefensiveTacticNames.MOVING_FORWARD_DURING_ORGANIZED_PRESSURE,\n overlayElements: [\n {\n overlayElementTypeId: OverlayElementNames.MOVING_FORWARD_DURING_ORGANIZED_PRESSURE_ARROWS,\n references: [\n {\n referenceType: ReferenceType.Players,\n },\n ],\n },\n ],\n },\n [DefensiveTacticNames.DEFENDING_MOVING_BEHIND_THE_DEFENSIVE_LINE]: {\n name: DefensiveTacticNames.DEFENDING_MOVING_BEHIND_THE_DEFENSIVE_LINE,\n overlayElements: [\n {\n overlayElementTypeId: OverlayElementNames.DEFENDING_RUN_BEHIND_DEFENSIVE_LINE,\n references: [\n {\n referenceType: ReferenceType.Players,\n },\n ],\n },\n ],\n },\n [OffensiveTacticNames.MOVING_BEHIND_THE_DEFENSIVE_LINE]: {\n name: OffensiveTacticNames.MOVING_BEHIND_THE_DEFENSIVE_LINE,\n overlayElements: [\n {\n overlayElementTypeId: OverlayElementNames.RUN_BEHIND_DEFENSIVE_LINE,\n references: [\n {\n referenceType: ReferenceType.Players,\n },\n ],\n },\n ],\n },\n [DefensiveTacticNames.CLEAR_THE_BOX]: {\n name: DefensiveTacticNames.CLEAR_THE_BOX,\n overlayElements: [\n {\n overlayElementTypeId: OverlayElementNames.CLEAR_THE_BOX_DEFENDER_ARROW,\n references: [\n {\n referenceType: ReferenceType.Players,\n },\n ],\n },\n ],\n },\n [DefensiveTacticNames.SECOND_BALL_DEFENSIVE_WINNING_AFTER_DIRECT_PLAY]: {\n name: DefensiveTacticNames.SECOND_BALL_DEFENSIVE_WINNING_AFTER_DIRECT_PLAY,\n overlayElements: [\n {\n overlayElementTypeId: OverlayElementNames.SECOND_BALL_DEFENSIVE_CONTROLLER_HIGHLIGHT,\n references: [\n {\n referenceType: ReferenceType.Players,\n },\n ],\n },\n ],\n },\n [DefensiveTacticNames.SECOND_BALL_DEFENSIVE_WINNING_AFTER_FINISHING]: {\n name: DefensiveTacticNames.SECOND_BALL_DEFENSIVE_WINNING_AFTER_FINISHING,\n overlayElements: [\n {\n overlayElementTypeId: OverlayElementNames.SECOND_BALL_DEFENSIVE_CONTROLLER_HIGHLIGHT,\n references: [\n {\n referenceType: ReferenceType.Players,\n },\n ],\n },\n ],\n },\n [DefensiveTacticNames.SECOND_BALL_DEFENSIVE_WINNING_AFTER_CROSS]: {\n name: DefensiveTacticNames.SECOND_BALL_DEFENSIVE_WINNING_AFTER_CROSS,\n overlayElements: [\n {\n overlayElementTypeId: OverlayElementNames.SECOND_BALL_DEFENSIVE_CONTROLLER_HIGHLIGHT,\n references: [\n {\n referenceType: ReferenceType.Players,\n },\n ],\n },\n ],\n },\n [DefensiveTacticNames.SECOND_BALL_DEFENSIVE_WINNING_AFTER_SET_PIECE]: {\n name: DefensiveTacticNames.SECOND_BALL_DEFENSIVE_WINNING_AFTER_SET_PIECE,\n overlayElements: [\n {\n overlayElementTypeId: OverlayElementNames.SECOND_BALL_DEFENSIVE_CONTROLLER_HIGHLIGHT,\n references: [\n {\n referenceType: ReferenceType.Players,\n },\n ],\n },\n ],\n },\n [OffensiveTacticNames.SECOND_BALL_OFFENSIVE_WINNING_AFTER_DIRECT_PLAY]: {\n name: OffensiveTacticNames.SECOND_BALL_OFFENSIVE_WINNING_AFTER_DIRECT_PLAY,\n overlayElements: [\n {\n overlayElementTypeId: OverlayElementNames.SECOND_BALL_OFFENSIVE_CONTROLLER_HIGHLIGHT,\n references: [\n {\n referenceType: ReferenceType.Players,\n },\n ],\n },\n ],\n },\n [OffensiveTacticNames.SECOND_BALL_OFFENSIVE_WINNING_AFTER_FINISHING]: {\n name: OffensiveTacticNames.SECOND_BALL_OFFENSIVE_WINNING_AFTER_FINISHING,\n overlayElements: [\n {\n overlayElementTypeId: OverlayElementNames.SECOND_BALL_OFFENSIVE_CONTROLLER_HIGHLIGHT,\n references: [\n {\n referenceType: ReferenceType.Players,\n },\n ],\n },\n ],\n },\n [OffensiveTacticNames.SECOND_BALL_OFFENSIVE_WINNING_AFTER_CROSS]: {\n name: OffensiveTacticNames.SECOND_BALL_OFFENSIVE_WINNING_AFTER_CROSS,\n overlayElements: [\n {\n overlayElementTypeId: OverlayElementNames.SECOND_BALL_OFFENSIVE_CONTROLLER_HIGHLIGHT,\n references: [\n {\n referenceType: ReferenceType.Players,\n },\n ],\n },\n ],\n },\n [OffensiveTacticNames.SECOND_BALL_OFFENSIVE_WINNING_AFTER_SET_PIECE]: {\n name: OffensiveTacticNames.SECOND_BALL_OFFENSIVE_WINNING_AFTER_SET_PIECE,\n overlayElements: [\n {\n overlayElementTypeId: OverlayElementNames.SECOND_BALL_OFFENSIVE_CONTROLLER_HIGHLIGHT,\n references: [\n {\n referenceType: ReferenceType.Players,\n },\n ],\n },\n ],\n },\n [DefensiveTacticNames.DEFENDING_RUNNING_INTO_THE_BOX]: {\n name: DefensiveTacticNames.DEFENDING_RUNNING_INTO_THE_BOX,\n overlayElements: [\n {\n overlayElementTypeId: OverlayElementNames.DEFENDING_RUN_INTO_THE_BOX,\n references: [\n {\n referenceType: ReferenceType.Players,\n },\n ],\n },\n ],\n },\n [OffensiveTacticNames.RUNNING_INTO_THE_BOX]: {\n name: OffensiveTacticNames.RUNNING_INTO_THE_BOX,\n overlayElements: [\n {\n overlayElementTypeId: OverlayElementNames.RUN_INTO_THE_BOX,\n references: [\n {\n referenceType: ReferenceType.Players,\n },\n ],\n },\n ],\n },\n [OffensiveTacticNames.PASS_BEHIND_DEFENSIVE_LINE]: {\n name: OffensiveTacticNames.PASS_BEHIND_DEFENSIVE_LINE,\n overlayElements: [\n {\n overlayElementTypeId: OverlayElementNames.PASS_BEHIND_DEFENSIVE_LINE_RECEIVER_HIGHLIGHT,\n references: [\n {\n referenceType: ReferenceType.StaticCoordinates,\n },\n ],\n },\n {\n overlayElementTypeId: OverlayElementNames.PASS_BEHIND_DEFENSIVE_LINE_ARROW,\n references: [\n {\n referenceType: ReferenceType.Players,\n },\n ],\n },\n ],\n },\n};\n","import { RefObject } from 'react';\n\nimport { PLAYER_ACTIONS } from './state/states';\nimport { FundamentalsSelection } from '../../types/playlist/types';\n\nexport type PlayerType = RefObject;\nexport type PlayerContainerType = RefObject;\n\nexport enum PlayingModes {\n TACTICAL_CAMERA = 'TACTICAL_CAMERA',\n EPISODES = 'EPISODES',\n PLAYLIST = 'PLAYLIST',\n PANORAMIC = 'PANORAMIC',\n}\n\nexport interface PlayingMode {\n mode: PlayingModes;\n showOverlays: boolean;\n useEffectiveTime: boolean;\n isPreferred?: boolean;\n}\n\nexport interface PlayerStatePlaylist {\n preferredPlayingMode: PlayingMode;\n currentSelectedPlayingMode: PlayingMode;\n currentPlaylistItemId: string;\n playlistItems: PlaylistItemType[];\n playingItem: {\n currentSourceTime: number;\n videoSourceIndex: number;\n playlistItem: PlaylistItemType;\n };\n}\n\nexport interface PlayerStateMachineContext {\n currentTime: number;\n isFullScreen: boolean;\n isInStandBy: boolean;\n isPlaying: boolean;\n autoPlayNextPlaylistItem: boolean;\n playerId: string;\n playlist: PlayerStatePlaylist;\n videoRef: PlayerType | undefined;\n}\n\nexport type SetPlaylistAction = {\n autoplay?: boolean;\n playerRef?: PlayerType;\n playingMode?: PlayingMode;\n playlistItems: PlaylistItemType[];\n tryToKeepCurrentTime?: boolean;\n type: PLAYER_ACTIONS.LOAD_PLAYLIST;\n initialStartTime: number;\n};\n\nexport type AddPlaylistItemsAction = {\n type: PLAYER_ACTIONS.REPLACE_PLAYLIST_ITEMS;\n playlistItems: PlaylistItemType[];\n initialStartTime?: number;\n};\nexport type UpdatePlaylistItemsAction = {\n type: PLAYER_ACTIONS.UPDATE_PLAYLIST_ITEMS;\n playlistItems: PlaylistItemType[];\n};\nexport type JumpToPercentTimeAction = { type: PLAYER_ACTIONS.JUMP_TO_TIME_PERCENT; percent: number };\nexport type JumpToMatchTimeAction = { type: PLAYER_ACTIONS.JUMP_TO_MATCH_TIME; time: number };\n\nexport type SetPlaylistItemAction = {\n type: PLAYER_ACTIONS.LOAD_PLAYLIST_ITEM;\n playlistItemId: string;\n autoPlay: boolean;\n};\nexport type SetReplacePlaylistItemAction = {\n type: PLAYER_ACTIONS.REPLACE_PLAYLIST;\n playlistItems: PlaylistItemType[];\n playingMode: PlayingMode;\n tryToKeepCurrentTime?: boolean;\n autoplay?: boolean;\n};\nexport type RemovePlaylistAction = { type: PLAYER_ACTIONS.REMOVE_PLAYLIST_ITEM; playlistItemId: string };\nexport type RemovePlaylistItemsAction = { type: PLAYER_ACTIONS.REMOVE_PLAYLIST_ITEMS; playlistItemsIds: string[] };\nexport type ChangePlayingModeAction = {\n type: PLAYER_ACTIONS.CHANGE_PLAYING_MODE;\n playingMode: PlayingMode;\n tryToKeepCurrentTime?: boolean;\n autoplay?: boolean;\n};\n\nexport type UpdatePlaylistItemAction = {\n type: PLAYER_ACTIONS.UPDATE_PLAYLIST_ITEM;\n playlistItem: PlaylistItemType;\n currentTime?: number;\n};\n\nexport type ReorderPlaylistItemAction = {\n type: PLAYER_ACTIONS.REORDER_PLAYLIST_ITEM;\n currentVideoIndex: number;\n newVideoIndex: number;\n};\n\nexport type ChangeAutoplayPlaylistItemAction = {\n type: PLAYER_ACTIONS.CHANGE_AUTOPLAY_NEXT_PLAYLIST_ITEM;\n autoplayNextPlaylistItem: boolean;\n};\n\nexport type PlayerStateMachineEvent =\n | AddPlaylistItemsAction\n | ChangePlayingModeAction\n | JumpToMatchTimeAction\n | JumpToPercentTimeAction\n | RemovePlaylistAction\n | RemovePlaylistItemsAction\n | ReorderPlaylistItemAction\n | SetPlaylistAction\n | SetPlaylistItemAction\n | SetReplacePlaylistItemAction\n | UpdatePlaylistItemAction\n | UpdatePlaylistItemsAction\n | ChangeAutoplayPlaylistItemAction\n | { type: PLAYER_ACTIONS };\n\nexport interface VideoSourceWithTimes {\n endTime: number;\n endTimeInMatch?: number;\n poster?: string | null;\n src: string;\n srcDownload?: string;\n startTime: number;\n startTimeInMatch?: number;\n type?: string | null;\n id: string;\n}\n\nexport interface VideoSourceType {\n playingMode: PlayingMode;\n videoSources: VideoSourceWithTimes[];\n}\n\nexport interface PlaylistItemType {\n id: string;\n duration: number;\n name?: string;\n index?: number;\n videoTypes: VideoSourceType[];\n fundamentalsSelected: FundamentalsSelection;\n hasHomographies: boolean;\n origin?: {\n type: string;\n id: string;\n };\n recordingName?: string | undefined;\n recordingMatchday?: string | undefined;\n recordingId: string;\n}\n\nexport type PlaylistVideo = {\n id: string;\n startTime: number;\n endTime: number;\n videoSource: VideoSource;\n};\n\nexport interface VideoSource {\n id: string;\n poster?: string | null;\n src: string;\n srcDownload?: string;\n type?: string | null;\n}\n","export enum TacticsVariants {\n OFFENSIVE = 'offensive',\n DEFENSIVE = 'defensive',\n}\n","import { z } from 'zod';\n\nimport { TacticsVariants } from '../tactics/types';\n\nexport const RecordingsFiltersEventsSchema = z.object({\n teams: z.array(z.string()),\n event: z.string(),\n zones: z.array(z.number()),\n players: z.array(z.string()),\n});\n\nexport const RecordingFiltersTacticSchema = z.object({\n teamIds: z.array(z.string()),\n tacticalFundamentalType: z.string(),\n playerIds: z.array(z.string()),\n category: z.enum([TacticsVariants.OFFENSIVE, TacticsVariants.DEFENSIVE]),\n});\n\nexport const RecordingFiltersTacticsSchema = z.object({\n offensive: z.array(RecordingFiltersTacticSchema),\n defensive: z.array(RecordingFiltersTacticSchema),\n});\n\nexport const RecordingFiltersScenariosOrTacticsInsideSchema = z.object({\n teams: z.array(z.string()),\n scenario: z.string(),\n zones: z.array(z.number()),\n tactics: RecordingFiltersTacticsSchema,\n});\n\nexport const RecordingsFiltersSchema = z.object({\n recordingIds: z.array(z.string()),\n eventsStarting: RecordingsFiltersEventsSchema.optional(),\n eventsEnding: RecordingsFiltersEventsSchema.optional(),\n scenariosOrTacticsInside: RecordingFiltersScenariosOrTacticsInsideSchema.optional(),\n});\n","export const PLAYBACK_RATES = {\n extraSlow: 0.25,\n slow: 0.5,\n normal: 1,\n fast: 1.5,\n extraFast: 2,\n} as const;\n\nexport const PLAYBACK_RATES_VALUES = Object.values(PLAYBACK_RATES);\n","export const ZOOM_LEVELS = {\n extraLarge: 0,\n large: 1,\n mediumLarge: 2,\n medium: 3,\n mediumSmall: 4,\n small: 5,\n extraSmall: 6,\n} as const;\n\nexport const ZOOM_LEVELS_VALUES = Object.values(ZOOM_LEVELS);\n","import { defensiveTactics, offensiveTactics } from 'overlay-generator';\nimport { z } from 'zod';\n\nimport { PlayingModes } from 'shared/components/video-player/types';\nimport { RecordingsFiltersSchema } from 'shared/types/recording/schemas';\n\nimport { USER_PRESET_KEYS } from './userPresetsKeys';\nimport { PLAYBACK_RATES } from '../playback-rates/paybackRates';\nimport { ZOOM_LEVELS } from '../zoom-range/zoomLevelsValues';\n\nconst selectedTacticsValidateData = [...defensiveTactics, ...offensiveTactics, 'all'] as const;\n\n// NOTE: schema based on USER_PRESET_KEYS object\nexport const PRESET_SCHEMA = {\n [USER_PRESET_KEYS.multiMatchAppliedFilters]: RecordingsFiltersSchema,\n [USER_PRESET_KEYS.playingMode]: z.object({\n mode: z.nativeEnum(PlayingModes),\n showOverlays: z.boolean(),\n useEffectiveTime: z.boolean(),\n isPreferred: z.boolean().optional(),\n }),\n [USER_PRESET_KEYS.selectedTactics]: z.array(z.enum(selectedTacticsValidateData)),\n [USER_PRESET_KEYS.zoomLevel]: z.union([\n z.literal(ZOOM_LEVELS.extraLarge),\n z.literal(ZOOM_LEVELS.large),\n z.literal(ZOOM_LEVELS.mediumLarge),\n z.literal(ZOOM_LEVELS.medium),\n z.literal(ZOOM_LEVELS.mediumSmall),\n z.literal(ZOOM_LEVELS.small),\n z.literal(ZOOM_LEVELS.extraSmall),\n ]),\n [USER_PRESET_KEYS.height]: z.number(),\n [USER_PRESET_KEYS.pinScenarios]: z.boolean(),\n [USER_PRESET_KEYS.headersWidth]: z.number(),\n [USER_PRESET_KEYS.timeLineAppliedFilters]: RecordingsFiltersSchema,\n [USER_PRESET_KEYS.filters]: z.string(),\n [USER_PRESET_KEYS.time]: z.number(),\n [USER_PRESET_KEYS.teamIdFocus]: z.string(),\n [USER_PRESET_KEYS.showBallPossession]: z.boolean(),\n [USER_PRESET_KEYS.showNoBallPossession]: z.boolean(),\n [USER_PRESET_KEYS.speed]: z.union([\n z.literal(PLAYBACK_RATES.extraSlow),\n z.literal(PLAYBACK_RATES.slow),\n z.literal(PLAYBACK_RATES.normal),\n z.literal(PLAYBACK_RATES.fast),\n z.literal(PLAYBACK_RATES.extraFast),\n ]),\n} as const;\n","import { ZodSchema } from 'zod';\n\nimport { PRESET_SCHEMA } from 'shared/constants/user-presets/userPresetsSchema';\nimport { UserPreset, UserPresetKeysUnion, UserPresetValues } from 'shared/types/user-preset/types';\n\nexport function validateWithSchema(data: unknown, schema: ZodSchema): boolean {\n const result = schema.safeParse(data);\n return result.success;\n}\n\nexport const getPreset = (data: UserPreset[], key: T) => {\n const value = data.find((preset) => preset.key === key)?.value;\n\n if (value === undefined || !validateWithSchema(value, PRESET_SCHEMA[key])) {\n return undefined;\n }\n\n return value as UserPresetValues[T];\n};\n","import { atomFamily, useRecoilValue, useSetRecoilState } from 'recoil';\n\nimport { RecordingsFilters } from 'shared/types/recording/types';\n\nconst playlistMultimatchAppliedFiltersAtom = atomFamily({\n key: 'playlist-multimatch-applied-filters',\n default: {\n recordingIds: [],\n },\n});\nexport const useSetPlaylistMultimatchAppliedFilters = (id: string) => {\n return useSetRecoilState(playlistMultimatchAppliedFiltersAtom(id));\n};\nexport const usePlaylistMultimatchAppliedFilters = (id: string) => {\n return useRecoilValue(playlistMultimatchAppliedFiltersAtom(id));\n};\n","import { useUserPresets } from 'api/user-presets/use-user-presets';\nimport { playlistMultimatchAppliedFilters } from 'shared/constants/user-presets/userPresetsPlayList';\nimport { UserPresetScope } from 'shared/types/user-preset/types';\nimport { getPreset } from 'shared/utils/user-presets/getPreset';\n\nimport { useSetPlaylistMultimatchAppliedFilters } from '../store/atoms';\n\ninterface UseAppDataInterface {\n isFetching: boolean;\n isError: boolean;\n isSuccess: boolean;\n isLoading: boolean;\n}\n\ntype Options = {\n playlistId: string;\n};\n\nexport const useMultimatchAppliedFiltersPreset = ({ playlistId }: Options): UseAppDataInterface => {\n const setPlaylistMultimatchAppliedFilters = useSetPlaylistMultimatchAppliedFilters(playlistId);\n\n const fetchUserPresets = useUserPresets({\n prefix: 'playlist',\n scope: UserPresetScope.playlist,\n ref: playlistId,\n onSuccess: (data) => {\n const preset = getPreset(data, playlistMultimatchAppliedFilters.key);\n if (preset) setPlaylistMultimatchAppliedFilters(preset);\n },\n });\n\n const isLoading = fetchUserPresets.isLoading;\n const isFetching = fetchUserPresets.isFetching;\n const isSuccess = fetchUserPresets.isSuccess;\n const isError = fetchUserPresets.isError;\n\n return {\n isError,\n isSuccess,\n isFetching,\n isLoading,\n };\n};\n","import { usePlaylist } from 'api/playlist/usePlaylist';\nimport { useClientId } from 'shared/contexts/app-state';\n\nimport { useMultimatchAppliedFiltersPreset } from './useMultimatchAppliedFiltersPreset';\n\nexport const usePlaylistPage = ({ playlistId }: { playlistId: string }) => {\n const { isLoading } = useMultimatchAppliedFiltersPreset({ playlistId });\n const { clientId } = useClientId();\n const playlist = usePlaylist({ playlistId });\n\n const isInvalidClient = clientId !== playlist?.data?.clientId;\n\n return {\n ...playlist,\n isLoading: isLoading || playlist.isLoading,\n isInvalidClient,\n };\n};\n","import { PlayingMode, PlayingModes } from './types';\n\nexport const TACTICAL_CAMERA_WITH_OVERLAYS_PLAYING_MODE: PlayingMode = {\n mode: PlayingModes.TACTICAL_CAMERA,\n showOverlays: true,\n useEffectiveTime: true,\n};\n\nexport const TACTICAL_CAMERA_WITHOUT_OVERLAYS_PLAYING_MODE: PlayingMode = {\n mode: PlayingModes.TACTICAL_CAMERA,\n showOverlays: false,\n useEffectiveTime: false,\n};\n\nexport const PANORAMIC_PLAYING_MODE: PlayingMode = {\n mode: PlayingModes.PANORAMIC,\n showOverlays: false,\n useEffectiveTime: false,\n};\n\nexport const EPISODES_PLAYING_MODE: PlayingMode = {\n mode: PlayingModes.EPISODES,\n showOverlays: true,\n useEffectiveTime: true,\n};\n\nexport const PLAYLIST_WITH_OVERLAYS_PLAYING_MODE: PlayingMode = {\n mode: PlayingModes.PLAYLIST,\n showOverlays: true,\n useEffectiveTime: false,\n};\n\nexport const PLAYLIST_WITHOUT_OVERLAYS_PLAYING_MODE: PlayingMode = {\n mode: PlayingModes.PLAYLIST,\n showOverlays: false,\n useEffectiveTime: false,\n};\n\nexport const getPlayingMode = ({\n playingMode,\n mode,\n showOverlays,\n useEffectiveTime,\n}: {\n playingMode: PlayingMode;\n mode?: PlayingModes;\n showOverlays?: boolean;\n useEffectiveTime?: boolean;\n}): PlayingMode => {\n return {\n ...playingMode,\n mode: mode ?? playingMode.mode,\n showOverlays: showOverlays ?? playingMode.showOverlays,\n useEffectiveTime: useEffectiveTime ?? playingMode.useEffectiveTime,\n };\n};\n\nexport const DEFAULT_TACTICAL_CAMERA_PLAYING_MODE = TACTICAL_CAMERA_WITHOUT_OVERLAYS_PLAYING_MODE;\nexport const DEFAULT_EFFECTIVE_TIME_PLAYING_MODE = EPISODES_PLAYING_MODE;\n","import isEmpty from 'lodash/isEmpty';\n\nimport {\n EPISODES_PLAYING_MODE,\n PANORAMIC_PLAYING_MODE,\n TACTICAL_CAMERA_WITH_OVERLAYS_PLAYING_MODE,\n TACTICAL_CAMERA_WITHOUT_OVERLAYS_PLAYING_MODE,\n} from 'shared/components/video-player/defaultPlayingModes';\nimport { VideoSourceType, VideoSourceWithTimes } from 'shared/components/video-player/types';\nimport { EpisodeClip } from 'shared/types';\nimport { VideoSource } from 'shared/types/recording/types';\nimport { GetVideoSourcesResponse } from 'shared/utils/get-video-sources';\n\ninterface GenerateVideoSourcesFromVideoTypes {\n startTime: number;\n endTime: number;\n episodesVideos: EpisodeClip[];\n videoSources?: GetVideoSourcesResponse;\n useCustomOverlays: boolean;\n}\n\nconst findEpisodeForPlaylistItem = (startTime: number, endTime: number, episodeClips: EpisodeClip[]) => {\n return episodeClips.find((episodeClip) => startTime >= episodeClip.startTime && endTime <= episodeClip.endTime);\n};\n\nconst generateFullMatchVideoSource = (\n startTime: number,\n endTime: number,\n fullMatchVideo: VideoSource,\n): VideoSourceWithTimes => {\n return {\n startTime,\n endTime,\n src: fullMatchVideo.src,\n srcDownload: fullMatchVideo.srcDownload,\n id: fullMatchVideo.id,\n };\n};\n\nconst generatePlaylistVideoSource = (\n startTime: number,\n endTime: number,\n episodeClip: EpisodeClip,\n): VideoSourceWithTimes => {\n return {\n startTime: startTime - episodeClip.startTime,\n startTimeInMatch: episodeClip.startTime,\n endTimeInMatch: endTime,\n endTime: endTime - episodeClip.startTime,\n src: episodeClip.videoSrc.src,\n srcDownload: episodeClip.videoSrc.srcDownload,\n id: episodeClip.videoSrc.id,\n };\n};\n\nexport const generateVideoSourcesFromVideoTypes = ({\n startTime,\n endTime,\n episodesVideos,\n videoSources,\n useCustomOverlays = false,\n}: GenerateVideoSourcesFromVideoTypes): VideoSourceType[] => {\n const episodeClip = findEpisodeForPlaylistItem(startTime, endTime, episodesVideos);\n const hasEpisodes = !isEmpty(episodesVideos) && !!episodeClip;\n const hasTacticalCameraVideo = videoSources?.tacticalCameraVideo;\n const hasUploadedVideo = videoSources?.uploadedVideo;\n const hasPanoramaVideo = videoSources?.panoramicVideo;\n\n return [\n ...(hasEpisodes && !useCustomOverlays\n ? [\n {\n playingMode: EPISODES_PLAYING_MODE,\n videoSources: [generatePlaylistVideoSource(startTime, endTime, episodeClip)],\n },\n ]\n : []),\n ...(useCustomOverlays && hasTacticalCameraVideo\n ? [\n {\n playingMode: TACTICAL_CAMERA_WITH_OVERLAYS_PLAYING_MODE,\n videoSources: [generateFullMatchVideoSource(startTime, endTime, videoSources.tacticalCameraVideo)],\n },\n ]\n : []),\n ...(hasTacticalCameraVideo && (!useCustomOverlays || !hasEpisodes)\n ? [\n {\n playingMode: TACTICAL_CAMERA_WITHOUT_OVERLAYS_PLAYING_MODE,\n videoSources: [generateFullMatchVideoSource(startTime, endTime, videoSources.tacticalCameraVideo)],\n },\n ]\n : []),\n ...(hasUploadedVideo\n ? [\n {\n playingMode: TACTICAL_CAMERA_WITHOUT_OVERLAYS_PLAYING_MODE,\n videoSources: [generateFullMatchVideoSource(startTime, endTime, videoSources.uploadedVideo)],\n },\n ]\n : []),\n ...(hasPanoramaVideo\n ? [\n {\n playingMode: PANORAMIC_PLAYING_MODE,\n videoSources: [generateFullMatchVideoSource(startTime, endTime, videoSources.panoramicVideo)],\n },\n ]\n : []),\n ];\n};\n","import { useCallback } from 'react';\n\nimport { FEATURE_FLAG } from 'api/user/use-fetch-feature-flags';\nimport { PlaylistItemType, VideoSourceType } from 'shared/components/video-player/types';\nimport { useFeatureFlag } from 'shared/contexts/app-state';\nimport { Playlist } from 'shared/types';\nimport { getVideoSources } from 'shared/utils/get-video-sources';\n\nimport { generateVideoSourcesFromVideoTypes } from './utils/generateVideoSourcesFromVideoTypes';\n\nexport const useMapVideos = () => {\n const customOverlaysFeatureFlag = useFeatureFlag(FEATURE_FLAG.CUSTOM_OVERLAYS);\n\n return useCallback(\n (playlist: Playlist): PlaylistItemType[] => {\n return playlist.playlistItems.map((playlistItem) => {\n const playlistItemRecording = playlist.recordings.find(\n (recording) => recording.id === playlistItem.recordingId,\n );\n const videoSources = getVideoSources(playlistItemRecording?.videoSources || []);\n const videoTypes: VideoSourceType[] = generateVideoSourcesFromVideoTypes({\n startTime: playlistItem.startTime,\n endTime: playlistItem.endTime,\n episodesVideos: playlistItem.episodesVideos,\n videoSources,\n useCustomOverlays: customOverlaysFeatureFlag && playlistItem.hasHomographies,\n });\n\n return {\n id: playlistItem.id,\n name: playlistItem.name,\n index: playlistItem.index,\n duration: playlistItem.endTime - playlistItem.startTime,\n videoTypes,\n recordingName: playlistItemRecording?.name,\n recordingMatchday: playlistItemRecording?.matchDay,\n recordingId: playlistItem.recordingId,\n hasHomographies: playlistItem.hasHomographies,\n fundamentalsSelected: playlistItem.fundamentalsSelected,\n };\n });\n },\n [customOverlaysFeatureFlag],\n );\n};\n","export const VIDEO_PLAYER_MACHINE_ID = 'video-player-machine';\n\nexport enum PLAYER_STATES {\n CHECKING_NEXT_PLAYLIST_ITEM = 'CHECKING_NEXT_PLAYLIST_ITEM',\n ENDED = 'ENDED',\n ERROR = 'ERROR',\n IDLE = 'IDLE',\n EMPTY_PLAYLIST = 'EMPTY_PLAYLIST',\n JUMP_TO_TIME_PERCENT = 'JUMP_TO_TIME_PERCENT',\n LOADING = 'LOADING',\n LOADING_PLAYLIST_ITEM = 'LOADING_PLAYLIST_ITEM',\n LOADING_VIDEO_SOURCE = 'LOADING_VIDEO_SOURCE',\n READY = 'READY',\n}\n\nexport enum READY_STATES {\n ENDED = 'ENDED',\n IDLE = 'IDLE',\n PAUSED = 'PAUSED',\n PLAYING = 'PLAYING',\n SEEKING_NEW_TIME = 'SEEKING_NEW_TIME',\n STAND_BY = 'STAND_BY',\n TIMING = 'TIMING',\n}\n\nexport enum PLAYER_ACTIONS {\n ENDED = 'READY.ENDED',\n ERROR = 'ERROR',\n REFRESH = 'REFRESH',\n RESTART = 'RESTART',\n TOGGLE_FULL_SCREEN = 'READY.TOGGLE_FULL_SCREEN',\n CHANGE_PLAYING_MODE = 'CHANGE_PLAYING_MODE',\n JUMP_TO_TIME_PERCENT = 'JUMP_TO_TIME_PERCENT',\n JUMP_TO_MATCH_TIME = 'JUMP_TO_MATCH_TIME',\n LOAD_PLAYLIST = 'LOAD_PLAYLIST',\n REPLACE_PLAYLIST = 'READY.REPLACE_PLAYLIST',\n CHANGE_AUTOPLAY_NEXT_PLAYLIST_ITEM = 'READY.CHANGE_AUTOPLAY_NEXT_PLAYLIST_ITEM',\n REPLACE_PLAYLIST_ITEMS = 'READY.REPLACE_PLAYLIST_ITEMS',\n LOAD_PLAYLIST_ITEM = 'READY.LOAD_PLAYLIST_ITEM',\n NEXT_PLAYLIST_ITEM = 'READY.NEXT_PLAYLIST_ITEM',\n AUTOPLAY_NEXT_PLAYLIST_ITEM = 'READY.AUTOPLAY_NEXT_PLAYLIST_ITEM',\n NEXT_VIDEO_SOURCE = 'READY.NEXT_VIDEO_SOURCE',\n PREVIOUS_VIDEO_SOURCE = 'READY.PREVIOUS_VIDEO_SOURCE',\n PAUSE = 'READY.PAUSE',\n PLAY = 'READY.PLAY',\n PREVIOUS_PLAYLIST_ITEM = 'READY.PREVIOUS_PLAYLIST_ITEM',\n READY = 'READY',\n REMOVE_PLAYLIST_ITEM = 'READY.REMOVE_PLAYLIST_ITEM',\n REMOVE_PLAYLIST_ITEMS = 'READY.REMOVE_PLAYLIST_ITEMS',\n REORDER_PLAYLIST_ITEM = 'REORDER_PLAYLIST_ITEM',\n REPLAY = 'REPLAY',\n RESUME_STAND_BY = 'READY.RESUME_STAND_BY',\n SKIP_BACKWARD = 'READY.SKIP_BACKWARD',\n SKIP_FORWARD = 'READY.SKIP_FORWARD',\n STAND_BY = 'READY.STAND_BY',\n TIMING = 'READY.TIMING',\n TOGGLE_PLAYING = 'READY.TOGGLE_PLAYING',\n UPDATE_PLAYLIST_ITEM = 'READY.UPDATE_PLAYLIST_ITEM',\n UPDATE_PLAYLIST_ITEMS = 'READY.UPDATE_PLAYLIST_ITEMS',\n UPDATE_PLAYLIST_ITEM_CUSTOM_VIEW = 'READY.UPDATE_PLAYLIST_ITEM_CUSTOM_VIEW',\n ADD_PLAYLIST_ITEM_CUSTOM_VIEW = 'READY.ADD_PLAYLIST_ITEM_CUSTOM_VIEW',\n REMOVE_PLAYLIST_ITEM_CUSTOM_VIEW = 'READY.REMOVE_PLAYLIST_ITEM_CUSTOM_VIEW',\n}\n","import { atomFamily } from 'recoil';\n\nimport { PLAYER_STATES, READY_STATES } from 'shared/components/video-player/state/states';\nimport { PlayerStatePlaylist } from 'shared/components/video-player/types';\n\nimport { EPISODES_PLAYING_MODE } from '../../defaultPlayingModes';\n\nconst currentTime = atomFamily({\n key: 'video-player-current-time',\n default: 0,\n});\n\ntype States = READY_STATES | PLAYER_STATES;\nconst playerStatus = atomFamily({\n key: 'video-player-state',\n default: PLAYER_STATES.IDLE,\n});\n\nconst isPlaying = atomFamily({\n key: 'video-player-is-playing',\n default: false,\n});\n\nconst playlistDefault: PlayerStatePlaylist = {\n preferredPlayingMode: EPISODES_PLAYING_MODE,\n currentSelectedPlayingMode: EPISODES_PLAYING_MODE,\n currentPlaylistItemId: '',\n playlistItems: [],\n playingItem: {\n currentSourceTime: 0,\n playlistItem: {\n videoTypes: [],\n duration: 0,\n name: '',\n id: '',\n index: 0,\n recordingMatchday: '',\n recordingName: '',\n recordingId: '',\n hasHomographies: false,\n fundamentalsSelected: {\n tacticalAnalysisId: undefined,\n fundamentalsSelected: [],\n },\n },\n videoSourceIndex: 0,\n },\n};\n\nconst playlist = atomFamily({\n key: 'video-player-playlist',\n default: playlistDefault,\n});\n\nconst isInStandBy = atomFamily({\n key: 'video-player-is-stand-by',\n default: false,\n});\n\nexport const videoPlayerStateAtoms = {\n currentTime,\n isPlaying,\n isInStandBy,\n playlist,\n playerStatus,\n};\n","import { PlayingModes } from '../types';\n\nexport const isFullMatchVideo = (playingMode: PlayingModes) => {\n return playingMode === PlayingModes.PANORAMIC || playingMode === PlayingModes.TACTICAL_CAMERA;\n};\n","import isEmpty from 'lodash/isEmpty';\n\nimport { PlayingMode, PlayingModes, VideoSourceType } from 'shared/components/video-player/types';\n\nimport { PlaylistItemWithoutVideoSources } from '../../../types/playlist/types';\nimport { isFullMatchVideo } from '../is-full-match-video';\nimport { PlayerStateMachineContext, PlaylistItemType, VideoSourceWithTimes } from '../types';\n\nexport const getVideoByVideoType = (playlistItem: PlaylistItemType, playingMode: PlayingMode): VideoSourceType => {\n // Search for preferred video type\n const preferredVideoType = playlistItem.videoTypes.find((videoType) => videoType.playingMode.isPreferred);\n if (preferredVideoType) return preferredVideoType;\n\n // Find video with overlays\n const videoTypeWithOverlays = playlistItem.videoTypes.find(\n (videoType) =>\n videoType.playingMode.showOverlays &&\n videoType.playingMode.showOverlays === playingMode.showOverlays &&\n !isEmpty(videoType.videoSources),\n );\n if (videoTypeWithOverlays) return videoTypeWithOverlays;\n\n // Find video type more similar to current PlayingMode\n const videoType = playlistItem.videoTypes.find(\n (videoType) =>\n videoType.playingMode.mode === playingMode.mode ||\n (playingMode.mode === PlayingModes.PLAYLIST &&\n isFullMatchVideo(videoType.playingMode.mode) &&\n !isEmpty(videoType.videoSources)),\n );\n if (videoType) return videoType;\n\n //Return first videoType that has no empty videoSources\n const isAnyVideoTypeWithSources = playlistItem.videoTypes.find((item) => !isEmpty(item.videoSources));\n if (isAnyVideoTypeWithSources) return isAnyVideoTypeWithSources;\n\n // Return an empty value\n return {\n playingMode,\n videoSources: [{ startTime: 0, endTime: 0, src: '', id: '' }],\n };\n};\n\nexport const getVideoSourceByIndex = (\n playlistItem: PlaylistItemType,\n playingMode: PlayingMode,\n videoSourceIndex: number,\n): VideoSourceWithTimes => {\n const { videoSources } = getVideoByVideoType(playlistItem, playingMode);\n\n return videoSources[videoSourceIndex];\n};\n\nexport const getCurrentVideoSource = (context: PlayerStateMachineContext): VideoSourceWithTimes => {\n const { currentSelectedPlayingMode } = context.playlist;\n const { playlistItem, videoSourceIndex } = context.playlist.playingItem;\n return getVideoSourceByIndex(playlistItem, currentSelectedPlayingMode, videoSourceIndex);\n};\n\nconst getClosestMatchingVideoSourceIndex = (\n matchTime: number,\n startTime: number,\n endTime: number,\n currentVideoIndex: number,\n newVideoIndex: number,\n): number => {\n if (matchTime <= endTime && matchTime >= startTime) {\n return newVideoIndex;\n }\n\n if (startTime >= matchTime && currentVideoIndex === -1) {\n return newVideoIndex;\n }\n\n return currentVideoIndex;\n};\n\nexport const getCurrentTimeAndVideoSourceIndex = (\n matchTime: number,\n videoSources: VideoSourceWithTimes[],\n playingMode: PlayingMode,\n): { currentTime: number; videoIndex: number } => {\n const result = videoSources.reduce(\n (result, videoSource, currentVideoIndex) => {\n const endTime = videoSource.endTimeInMatch ? videoSource.endTimeInMatch : videoSource.endTime;\n const startTime = videoSource.startTimeInMatch ? videoSource.startTimeInMatch : videoSource.startTime;\n const isFullMatch = isFullMatchVideo(playingMode.mode);\n\n const foundVideoIndex = getClosestMatchingVideoSourceIndex(\n matchTime,\n startTime,\n endTime,\n result.videoIndex,\n currentVideoIndex,\n );\n\n if (foundVideoIndex === result.videoIndex) return result;\n\n const time = isFullMatch ? matchTime : matchTime - startTime;\n\n return {\n videoIndex: foundVideoIndex,\n currentTime: foundVideoIndex !== result.videoIndex ? time : result.currentTime,\n };\n },\n {\n videoIndex: -1,\n currentTime: isFullMatchVideo(playingMode.mode) || !playingMode.useEffectiveTime ? matchTime : 0,\n },\n );\n\n const videoSourceIndex = result.videoIndex >= 0 ? result.videoIndex : 0;\n const currentTime =\n result.currentTime > 0 &&\n result.currentTime > videoSources[videoSourceIndex].startTime &&\n result.currentTime < videoSources[videoSourceIndex].endTime\n ? result.currentTime\n : videoSources[videoSourceIndex].startTime;\n\n return {\n videoIndex: videoSourceIndex,\n currentTime: currentTime > 0 ? currentTime : videoSources[videoSourceIndex].startTime,\n };\n};\n\nexport const areAllOverlayTacticsSelected = (playlistItems: PlaylistItemType | PlaylistItemWithoutVideoSources) => {\n return playlistItems.fundamentalsSelected.fundamentalsSelected[0] === 'all';\n};\n","import { FrameInfo, TacticId, useOverlayGenerator } from 'overlay-generator';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { atomFamily, useRecoilValue, useSetRecoilState } from 'recoil';\n\nimport { useCurrentPlaylistItem, useVideoPlayerActions, useVideoPlayerId, useVideoPlayerRef } from '../';\n\nconst areOverlaysReady = atomFamily({\n key: 'overlays-ready',\n default: false,\n});\n\nconst frameRate = atomFamily({\n key: 'overlays-render-frame-rate',\n default: 0,\n});\n\nconst frameInfo = atomFamily({\n key: 'overlays-frame-info',\n default: {\n frameNumber: 0,\n frameTactics: [],\n overlayElementsDebugInfo: [],\n },\n});\n\nexport const useSetAreOverlaysReady = () => {\n const playerId = useVideoPlayerId();\n return useSetRecoilState(areOverlaysReady(playerId));\n};\n\nexport const useAreOverlaysReady = () => {\n const playerId = useVideoPlayerId();\n return useRecoilValue(areOverlaysReady(playerId));\n};\n\nexport const useSetOverlaysFrameInfo = () => {\n const playerId = useVideoPlayerId();\n return useSetRecoilState(frameInfo(playerId));\n};\n\nexport const useOverlaysFrameInfo = () => {\n const playerId = useVideoPlayerId();\n return useRecoilValue(frameInfo(playerId));\n};\n\nexport const useRenderFrameRate = () => {\n const playerId = useVideoPlayerId();\n return useRecoilValue(frameRate(playerId));\n};\n\nexport const useOverlaysController = (videoPlayerContainerRef: React.RefObject) => {\n const playlistItem = useCurrentPlaylistItem();\n const container = useRef(null);\n const { overlayGenerator, frameRate, frameInfo, isReady } = useOverlayGenerator({\n id: playlistItem.recordingId,\n });\n\n const videoPlayerRef = useVideoPlayerRef();\n const setAreOverlaysReady = useSetAreOverlaysReady();\n const setOverlaysFrameInfo = useSetOverlaysFrameInfo();\n const actions = useVideoPlayerActions();\n const [matrix3dTransformation, setMatrix3dTransformation] = useState([]);\n\n useEffect(() => {\n isReady ? actions.resumeStandBy() : actions.handleStandBy();\n setAreOverlaysReady(isReady);\n }, [actions, setAreOverlaysReady, isReady]);\n\n useEffect(() => {\n setOverlaysFrameInfo(frameInfo);\n }, [setOverlaysFrameInfo, frameInfo]);\n\n const handleUpdateFrame = useCallback(\n async ({\n frame,\n tactics,\n videoPlayerWidth,\n videoPlayerHeight,\n overlayContainer,\n }: {\n frame: number;\n tactics: TacticId[] | undefined;\n videoPlayerWidth: number;\n videoPlayerHeight: number;\n overlayContainer: HTMLDivElement;\n }) => {\n await overlayGenerator.drawFrameInCanvas(overlayContainer, frame, {\n tactics,\n });\n\n const matrix3d = overlayGenerator.getTransformationMatrix(frame, {\n width: videoPlayerWidth,\n height: videoPlayerHeight,\n });\n\n setMatrix3dTransformation(matrix3d);\n return Boolean(matrix3d);\n },\n [overlayGenerator],\n );\n\n const handleVideoTimeUpdate = useCallback(async () => {\n const videoPlayerWidth = videoPlayerRef.current?.offsetWidth;\n const videoPlayerHeight = videoPlayerRef.current?.offsetHeight;\n if (!container.current || !videoPlayerRef.current || !videoPlayerWidth || !videoPlayerHeight) return;\n\n const videoFrame = Math.round(videoPlayerRef.current.currentTime * frameRate) - 1;\n\n const tactics =\n playlistItem.fundamentalsSelected && playlistItem.fundamentalsSelected.fundamentalsSelected[0] === 'all'\n ? undefined\n : (playlistItem.fundamentalsSelected.fundamentalsSelected as TacticId[]);\n\n return await handleUpdateFrame({\n frame: videoFrame,\n tactics,\n videoPlayerWidth,\n videoPlayerHeight,\n overlayContainer: container.current,\n });\n }, [container, frameRate, handleUpdateFrame, playlistItem.fundamentalsSelected, videoPlayerRef]);\n\n useEffect(() => {\n if (!videoPlayerContainerRef?.current) return;\n\n const video = videoPlayerRef.current;\n const videoPlayerContainer = videoPlayerContainerRef.current;\n\n const update = () => {\n handleVideoTimeUpdate();\n };\n\n const observer = new ResizeObserver(update);\n observer.observe(videoPlayerContainer as HTMLElement);\n video?.addEventListener('vmFullscreenChange', update);\n video?.addEventListener('vmCurrentTimeChange', update);\n video?.addEventListener('vmBufferingChange', update);\n video?.addEventListener('vmCurrentSrcChange', update);\n video?.addEventListener('vmDurationChange', update);\n video?.addEventListener('vmPausedChange', update);\n video?.addEventListener('vmSeeked', update);\n\n return () => {\n observer.unobserve(video as HTMLElement);\n video?.removeEventListener('vmFullscreenChange', update);\n video?.removeEventListener('vmCurrentTimeChange', update);\n video?.removeEventListener('vmBufferingChange', update);\n video?.removeEventListener('vmCurrentSrcChange', update);\n video?.removeEventListener('vmDurationChange', update);\n video?.removeEventListener('vmPausedChange', update);\n video?.removeEventListener('vmSeeked', update);\n observer.disconnect();\n };\n }, [handleVideoTimeUpdate, videoPlayerRef, videoPlayerContainerRef]);\n\n return { container, overlayGenerator, matrix3dTransformation };\n};\n","import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';\n\nimport { videoPlayerStateAtoms } from '../';\n\nexport const usePlayerSetIsPlaying = (playerId: string) => {\n return useSetRecoilState(videoPlayerStateAtoms.isPlaying(playerId));\n};\n\nexport const usePlayerSetPlaylist = (playerId: string) => {\n return useSetRecoilState(videoPlayerStateAtoms.playlist(playerId));\n};\n\nexport const usePlayerUpdateStandBy = (playerId: string) => {\n return useSetRecoilState(videoPlayerStateAtoms.isInStandBy(playerId));\n};\n\nexport const useSetCurrentTime = (playerId: string) => {\n return useSetRecoilState(videoPlayerStateAtoms.currentTime(playerId));\n};\n\nexport const useGetCurrentTime = (playerId: string) => {\n return useRecoilValue(videoPlayerStateAtoms.currentTime(playerId));\n};\n\nexport const usePlayerState = (playerId: string) => {\n return useRecoilState(videoPlayerStateAtoms.playerStatus(playerId));\n};\n","import { PlayingMode } from 'shared/components/video-player/types';\nimport { round } from 'shared/utils/round';\n\nimport { PlaylistItemType } from '../../../types';\nimport { getVideoByVideoType } from '../../../util';\n\nexport const getCurrentPlaylistItemTime = (\n playingMode: PlayingMode,\n absoluteCurrentTime: number,\n videoSourceIndex: number,\n playlistItem?: PlaylistItemType,\n) => {\n if (!playlistItem) return 0;\n\n const isFullMatchNotEffectiveTime = playingMode.mode === 'TACTICAL_CAMERA' && !playingMode.useEffectiveTime;\n\n const { videoSources } = getVideoByVideoType(playlistItem, playingMode);\n\n const adjustedStartTime = videoSources.reduce((startTime, source, idx) => {\n return videoSourceIndex > idx ? startTime + (source.endTime - source.startTime) : startTime;\n }, 0);\n\n const currentTime = isFullMatchNotEffectiveTime\n ? absoluteCurrentTime\n : absoluteCurrentTime - videoSources[videoSourceIndex].startTime + adjustedStartTime;\n\n return round(currentTime > 0 ? currentTime : 0);\n};\n","import { usePlayerContext } from '@vime/react';\nimport get from 'lodash/get';\nimport React, { useMemo } from 'react';\nimport { useRouteMatch } from 'react-router-dom';\nimport { atomFamily, useRecoilValue } from 'recoil';\n\nimport { FEATURE_FLAG } from 'api/user/use-fetch-feature-flags';\nimport { routes } from 'kognia/router/routes';\nimport { videoPlayerStateAtoms } from 'shared/components/video-player/state/atoms';\nimport { PlayingMode, PlayingModes, VideoSourceType, VideoSourceWithTimes } from 'shared/components/video-player/types';\nimport { getVideoByVideoType, getVideoSourceByIndex } from 'shared/components/video-player/util';\n\nimport { useAreOverlaysReady } from './use-overlays-controller';\nimport { useFeatureFlag } from '../../../contexts/app-state';\nimport { VideoPlayerActions, VideoPlayerStateFixedContext } from '../index';\nimport { useGetCurrentTime } from '../state/atoms/hooks';\nimport { PLAYER_STATES, READY_STATES } from '../state/states';\nimport { getCurrentPlaylistItemTime } from '../state/utils/get-current-playlist-item-time';\nimport { PlaylistItemType } from '../types';\n\nconst getVideoPlayerFixedStateContext = (hookName: string) => {\n const context = React.useContext(VideoPlayerStateFixedContext);\n\n if (context === undefined) {\n throw new Error(`${hookName} must be used within a VideoPlayerStateFixedContext`);\n }\n\n return context;\n};\n\nexport const useVideoPlayerRef = () => {\n const context = getVideoPlayerFixedStateContext('useVideoPlayerRef');\n\n return context.state.videoPlayerRef;\n};\n\nexport const useVideoPlayerContainerRef = () => {\n const context = getVideoPlayerFixedStateContext('useVideoPlayerContainerRef');\n\n return context.state.videoPlayerContainerRef;\n};\n\nexport const useVideoPlayerId = (): string => {\n const context = getVideoPlayerFixedStateContext('useVideoPlayerId');\n\n return context.state.videoPlayerId;\n};\n\nexport const useVideoPlayerRefreshData = (): (() => void) | (() => Promise) | undefined => {\n const context = getVideoPlayerFixedStateContext('useVideoPlayerRefreshData');\n\n return context.state.refreshData;\n};\n\nexport const useVideoPlayerActions = (): VideoPlayerActions => {\n const context = getVideoPlayerFixedStateContext('useVideoPlayerActions');\n\n return context.state.actions;\n};\n\nexport const usePlayerCurrentSource = () => {\n const playerId = useVideoPlayerId();\n const playlist = useRecoilValue(videoPlayerStateAtoms.playlist(playerId));\n\n return useMemo(\n () =>\n getVideoSourceByIndex(\n playlist.playingItem.playlistItem,\n playlist.currentSelectedPlayingMode,\n playlist.playingItem.videoSourceIndex,\n ),\n [playlist.playingItem.playlistItem, playlist.currentSelectedPlayingMode, playlist.playingItem.videoSourceIndex],\n );\n};\n\nexport const useCurrentVideoSourceDuration = () => {\n const videoSource = usePlayerCurrentSource();\n\n const startTime = videoSource?.startTime ?? 0;\n const endTime = videoSource?.endTime ?? 0;\n\n return endTime - startTime;\n};\n\nexport const useVideoPlayerPlayingMode = (): PlayingMode => {\n const playerId = useVideoPlayerId();\n const playlist = useRecoilValue(videoPlayerStateAtoms.playlist(playerId));\n\n return playlist.currentSelectedPlayingMode;\n};\n\nexport const useCurrentPlaylistItemId = (): string => {\n const playerId = useVideoPlayerId();\n const playlist = useRecoilValue(videoPlayerStateAtoms.playlist(playerId));\n\n return playlist.currentPlaylistItemId;\n};\n\nexport const useVideoPlayerPlaylistItem = (id: string): PlaylistItemType => {\n const playerId = useVideoPlayerId();\n const playlist = useRecoilValue(videoPlayerStateAtoms.playlist(playerId));\n\n const playlistItem = playlist.playlistItems.find((playlistItem) => playlistItem.id === id);\n\n if (!playlistItem) return playlist.playlistItems[0];\n\n return playlistItem;\n};\n\nexport const useCurrentPlaylistItem = (): PlaylistItemType => {\n const playerId = useVideoPlayerId();\n const playlist = useRecoilValue(videoPlayerStateAtoms.playlist(playerId));\n\n return useMemo(() => {\n const currentPlaylistItem = playlist.playlistItems.find(\n (playlistItem) => playlistItem.id === playlist.currentPlaylistItemId,\n );\n if (!currentPlaylistItem) return playlist.playingItem.playlistItem;\n return currentPlaylistItem;\n }, [playlist.playlistItems, playlist.currentPlaylistItemId, playlist.playingItem.playlistItem]);\n};\n\nexport const useDuration = () => {\n const playerId = useVideoPlayerId();\n const playlist = useRecoilValue(videoPlayerStateAtoms.playlist(playerId));\n\n return playlist.playingItem.playlistItem.duration ?? 0;\n};\n\nexport const useIsSeeking = (): boolean => {\n const context = getVideoPlayerFixedStateContext('useIsSeeking');\n const [seeking] = usePlayerContext(context.state.videoPlayerRef, 'seeking', false);\n\n return seeking;\n};\n\nexport const useIsBuffering = () => {\n const showCustomOverlaysFlag = useFeatureFlag(FEATURE_FLAG.CUSTOM_OVERLAYS);\n const matchPath = useRouteMatch(routes.RECORDING_PLAYLIST_DETAIL);\n const areOverlaysReady = useAreOverlaysReady();\n const { showOverlays } = useVideoPlayerPlayingMode();\n const playlistItem = useCurrentPlaylistItem();\n const context = getVideoPlayerFixedStateContext('useIsBuffering');\n const [buffering] = usePlayerContext(context.state.videoPlayerRef, 'buffering', false);\n\n return matchPath && showOverlays && playlistItem.hasHomographies && showCustomOverlaysFlag\n ? buffering || !areOverlaysReady\n : buffering;\n};\n\nexport const useVideoPlayerIsPlaying = () => {\n const playerId = useVideoPlayerId();\n\n return useRecoilValue(videoPlayerStateAtoms.isPlaying(playerId));\n};\n\nexport const useVideoPlayerIsInStandBy = () => {\n const playerId = useVideoPlayerId();\n\n return useRecoilValue(videoPlayerStateAtoms.isInStandBy(playerId));\n};\n\nexport const useVideoPlayerState = () => {\n const playerId = useVideoPlayerId();\n const state = useRecoilValue(videoPlayerStateAtoms.playerStatus(playerId));\n\n const readyState = get(state, 'READY');\n\n return {\n state: readyState ? PLAYER_STATES.READY : state,\n isEnded: state === READY_STATES.ENDED,\n readyState: readyState,\n isPlaylistEmpty: state === PLAYER_STATES.EMPTY_PLAYLIST && readyState,\n };\n};\n\nexport const usePlaylistCurrentPlaylistItemId = (): string => {\n const playerId = useVideoPlayerId();\n const playlist = useRecoilValue(videoPlayerStateAtoms.playlist(playerId));\n\n return playlist.currentPlaylistItemId;\n};\n\nexport const useIsCurrentPlaylistItem = (id: string): boolean => {\n const playerId = useVideoPlayerId();\n const playlist = useRecoilValue(videoPlayerStateAtoms.playlist(playerId));\n\n return playlist.currentPlaylistItemId === id;\n};\n\nexport const usePlaylistItems = (): PlaylistItemType[] => {\n const playerId = useVideoPlayerId();\n const playlist = useRecoilValue(videoPlayerStateAtoms.playlist(playerId));\n\n return playlist.playlistItems;\n};\n\nexport const useCurrentPlaylistItemVideoSources = (): VideoSourceWithTimes[] => {\n const playlistItem = useCurrentPlaylistItem();\n const playingMode = useVideoPlayerPlayingMode();\n const { videoSources } = getVideoByVideoType(playlistItem, playingMode);\n\n return videoSources;\n};\n\nexport const useCurrentPlaylistItemVideo = (): ((playingMode: PlayingMode) => VideoSourceType) => {\n const playlistItem = useCurrentPlaylistItem();\n\n return (playingMode: PlayingMode) => getVideoByVideoType(playlistItem, playingMode);\n};\n\nexport const useCurrentVideoSourceIndex = (): number => {\n const playerId = useVideoPlayerId();\n const playlist = useRecoilValue(videoPlayerStateAtoms.playlist(playerId));\n\n return playlist.playingItem.videoSourceIndex;\n};\n\nexport const useCurrentVideoSource = (): VideoSourceWithTimes => {\n const index = useCurrentVideoSourceIndex();\n const videoSources = useCurrentPlaylistItemVideoSources();\n\n return videoSources[index];\n};\n\nexport const useCurrentVideoSourceTime = () => {\n const playerId = useVideoPlayerId();\n return useGetCurrentTime(playerId);\n};\n\nconst nonReactiveCurrentMatchTimeAtomFamily = atomFamily<{ time: number }, string>({\n key: 'non-reactive-current-match-time',\n default: { time: 0 },\n dangerouslyAllowMutability: true,\n});\n\nexport const useNonReactiveCurrentTime = () => {\n return useRecoilValue(nonReactiveCurrentMatchTimeAtomFamily(useVideoPlayerId()));\n};\n\nconst getCurrentTimeByVideoSource = (\n currentTime: number,\n currentVideoSource: VideoSourceWithTimes,\n hasMultipleVideoSources: boolean,\n playingMode: PlayingMode,\n): number => {\n if (playingMode.mode !== PlayingModes.EPISODES) return currentTime;\n if (!hasMultipleVideoSources) return currentTime;\n\n return (currentVideoSource.startTimeInMatch ?? 0) + currentTime;\n};\n\nexport const useCurrentMatchTime = () => {\n const mutableMatchTime = useNonReactiveCurrentTime();\n const playlistItem = useCurrentPlaylistItem();\n const currentTime = useCurrentVideoSourceTime();\n const videoSourceIndex = useCurrentVideoSourceIndex();\n const playingMode = useVideoPlayerPlayingMode();\n const { videoSources } = getVideoByVideoType(playlistItem, playingMode);\n\n const time = getCurrentTimeByVideoSource(\n currentTime,\n videoSources[videoSourceIndex],\n videoSources.length > 1,\n playingMode,\n );\n\n mutableMatchTime.time = time;\n\n return time;\n};\n\nexport const useCurrentTime = () => {\n const playlistItem = useCurrentPlaylistItem();\n const currentTime = useCurrentVideoSourceTime();\n const videoSourceIndex = useCurrentVideoSourceIndex();\n const playingMode = useVideoPlayerPlayingMode();\n\n return getCurrentPlaylistItemTime(playingMode, currentTime, videoSourceIndex, playlistItem);\n};\n","import { PlayingMode } from 'shared/components/video-player/types';\n\nimport { PlayerType, PlaylistItemType } from '../../types';\n\nexport interface ChangeSourceAndTimeMachineContext {\n playingMode: PlayingMode;\n playlistItem: PlaylistItemType | undefined;\n videoRef: PlayerType | undefined;\n videoSourceIndex: number;\n currentTime: number;\n}\n\nexport enum CHANGE_SOURCE_STATES {\n CHECK_CURRENT_INITIAL_STATE = 'CHECK_INITIAL_STATE',\n CHECK_CURRENT_SOURCE = 'CHECK_CURRENT_SOURCE',\n CHECK_CURRENT_TIME = 'CHECK_CURRENT_TIME',\n ENDED = 'ENDED',\n}\n\nexport enum CHANGE_SOURCE_ACTIONS {\n CHECK_CURRENT_SOURCE = 'CHECK_CURRENT_SOURCE',\n CHECK_CURRENT_TIME = 'CHECK_CURRENT_TIME',\n}\n","import { sendParent } from 'xstate';\n\nimport { getVideoSourceByIndex } from '../../../util';\nimport { PLAYER_ACTIONS } from '../../states';\nimport { ChangeSourceAndTimeMachineContext } from '../types';\n\nexport const isSameVideoSource = (context: ChangeSourceAndTimeMachineContext) => {\n if (!context.videoRef?.current || !context.playlistItem) return false;\n const videoSource = getVideoSourceByIndex(context.playlistItem, context.playingMode, context.videoSourceIndex);\n\n return context.videoRef?.current?.currentSrc === videoSource?.src;\n};\n\nexport const isVideoSourceReady = (context: ChangeSourceAndTimeMachineContext) => {\n if (!context.videoRef?.current || !context.playlistItem) return false;\n\n const videoSource = getVideoSourceByIndex(context.playlistItem, context.playingMode, context.videoSourceIndex);\n const isCurrentSource = context.videoRef?.current?.currentSrc === videoSource?.src;\n\n if (!isCurrentSource) {\n if (!context.videoRef?.current?.paused) {\n context.videoRef?.current?.pause();\n }\n\n context.videoRef.current.currentSrc = videoSource?.src;\n return false;\n }\n\n return isCurrentSource;\n};\n\nexport const isCurrentTime = (context: ChangeSourceAndTimeMachineContext) => {\n if (!context.videoRef?.current?.currentTime && context.videoRef?.current?.currentTime !== 0) return false;\n\n const correction = context.currentTime === 0 ? 0.0001 : 0;\n const timeDifference = Math.abs(context.videoRef?.current?.currentTime - context.currentTime + correction);\n\n const isInStartTime =\n context.videoRef?.current?.currentTime === context.currentTime + correction || timeDifference < 0.1;\n\n if (!isInStartTime) {\n context.videoRef.current.currentTime = context.currentTime + correction;\n return false;\n }\n\n sendParent({ type: PLAYER_ACTIONS.TIMING, time: context.currentTime });\n return isInStartTime;\n};\n","import { createMachine } from 'xstate';\n\nimport { CHANGE_SOURCE_ACTIONS, CHANGE_SOURCE_STATES, ChangeSourceAndTimeMachineContext } from './types';\nimport { isCurrentTime, isSameVideoSource, isVideoSourceReady } from './utils/guards';\n\nexport const changeSourceMachine = createMachine(\n {\n predictableActionArguments: true,\n id: 'change-source-machine',\n initial: CHANGE_SOURCE_STATES.CHECK_CURRENT_INITIAL_STATE,\n states: {\n [CHANGE_SOURCE_STATES.CHECK_CURRENT_INITIAL_STATE]: {\n always: [\n { target: CHANGE_SOURCE_STATES.CHECK_CURRENT_TIME, cond: 'isSameVideoSource' },\n { target: CHANGE_SOURCE_STATES.CHECK_CURRENT_SOURCE },\n ],\n },\n [CHANGE_SOURCE_STATES.CHECK_CURRENT_SOURCE]: {\n after: {\n 10: [\n { target: CHANGE_SOURCE_ACTIONS.CHECK_CURRENT_TIME, cond: 'isVideoSourceReady' },\n { target: CHANGE_SOURCE_ACTIONS.CHECK_CURRENT_SOURCE },\n ],\n },\n },\n [CHANGE_SOURCE_STATES.CHECK_CURRENT_TIME]: {\n after: {\n 10: [{ target: 'ENDED', cond: 'isCurrentTime' }, { target: CHANGE_SOURCE_ACTIONS.CHECK_CURRENT_TIME }],\n },\n },\n ENDED: { type: 'final' },\n },\n },\n { guards: { isVideoSourceReady, isCurrentTime, isSameVideoSource } },\n);\n","import { PlayingMode, PlaylistItemType } from '../../types';\nimport { PLAYER_ACTIONS } from '../states';\nimport { Send } from '../types';\n\nexport const createActions = (send: Send) => {\n return {\n reorder: (currentVideoIndex: number, newVideoIndex: number) => {\n send({\n type: PLAYER_ACTIONS.REORDER_PLAYLIST_ITEM,\n currentVideoIndex,\n newVideoIndex,\n });\n },\n toggleFullScreen: () => send({ type: PLAYER_ACTIONS.TOGGLE_FULL_SCREEN }),\n changeAutoplayNextPlaylistItem: (autoplayNextPlaylistItem: boolean) =>\n send({\n type: PLAYER_ACTIONS.CHANGE_AUTOPLAY_NEXT_PLAYLIST_ITEM,\n autoplayNextPlaylistItem: autoplayNextPlaylistItem,\n }),\n changePlayingMode: (playingMode: PlayingMode, tryToKeepCurrentTime = true, autoplay = false) =>\n send({\n type: PLAYER_ACTIONS.CHANGE_PLAYING_MODE,\n playingMode,\n tryToKeepCurrentTime,\n autoplay,\n }),\n jumpToMatchTime: (time: number) => send({ type: PLAYER_ACTIONS.JUMP_TO_MATCH_TIME, time }),\n jumpToTimePercent: (percent: number) => {\n send({ type: PLAYER_ACTIONS.JUMP_TO_TIME_PERCENT, percent });\n },\n loadPlaylist: (playlistItems: PlaylistItemType[], initialStartTime = 0) =>\n send({ type: PLAYER_ACTIONS.LOAD_PLAYLIST, playlistItems, initialStartTime }),\n restart: () => send({ type: PLAYER_ACTIONS.RESTART }),\n refresh: () => send({ type: PLAYER_ACTIONS.REFRESH }),\n setPlaylist: (\n playlistItems: PlaylistItemType[],\n playingMode: PlayingMode,\n autoplay?: boolean,\n tryToKeepCurrentTime?: boolean,\n ) => send({ type: PLAYER_ACTIONS.REPLACE_PLAYLIST, playlistItems, playingMode, tryToKeepCurrentTime, autoplay }),\n pause: () => send({ type: PLAYER_ACTIONS.PAUSE }),\n play: () => send({ type: PLAYER_ACTIONS.PLAY }),\n replacePlaylistItems: (playlistItems: PlaylistItemType[]) => {\n send({ type: PLAYER_ACTIONS.REPLACE_PLAYLIST_ITEMS, playlistItems });\n },\n updatePlaylistItems: (playlistItems: PlaylistItemType[]) => {\n send({ type: PLAYER_ACTIONS.UPDATE_PLAYLIST_ITEMS, playlistItems });\n },\n togglePlaying: () => send({ type: PLAYER_ACTIONS.TOGGLE_PLAYING }),\n setPlaylistItem: (id: string, autoPlay: boolean) =>\n send({\n type: PLAYER_ACTIONS.LOAD_PLAYLIST_ITEM,\n playlistItemId: id,\n autoPlay,\n }),\n removePlaylistItem: (id: string) => send({ type: PLAYER_ACTIONS.REMOVE_PLAYLIST_ITEM, playlistItemId: id }),\n removePlaylistItems: (playlistItemsIds: string[]) =>\n send({ type: PLAYER_ACTIONS.REMOVE_PLAYLIST_ITEMS, playlistItemsIds }),\n updatePlaylistItem: (playlistItem: PlaylistItemType, currentTime?: number) =>\n send({ type: PLAYER_ACTIONS.UPDATE_PLAYLIST_ITEM, playlistItem, currentTime }),\n skipForward5s: () => send({ type: PLAYER_ACTIONS.SKIP_FORWARD }),\n skipBackward5s: () => send({ type: PLAYER_ACTIONS.SKIP_BACKWARD }),\n nextPlaylistItem: () => send({ type: PLAYER_ACTIONS.NEXT_PLAYLIST_ITEM }),\n autoplayNextPlaylistItem: () => send({ type: PLAYER_ACTIONS.AUTOPLAY_NEXT_PLAYLIST_ITEM }),\n previousPlaylistItem: () => send({ type: PLAYER_ACTIONS.PREVIOUS_PLAYLIST_ITEM }),\n nextVideoSource: () => send({ type: PLAYER_ACTIONS.NEXT_VIDEO_SOURCE }),\n previousVideoSource: () => send({ type: PLAYER_ACTIONS.PREVIOUS_VIDEO_SOURCE }),\n replay: () => send({ type: PLAYER_ACTIONS.REPLAY }),\n resumeStandBy: () => send({ type: PLAYER_ACTIONS.RESUME_STAND_BY }),\n handleStandBy: () => send({ type: PLAYER_ACTIONS.STAND_BY }),\n };\n};\n","import throttle from 'lodash/throttle';\nimport { useCallback, useEffect, useMemo } from 'react';\n\nimport { getIsFormTag } from 'shared/utils/is-form-tag';\n\nimport { createActions } from './utils';\nimport { Send } from '../types';\n\nconst CONTROLS_THROTTLE_TIME = 10;\n\nexport const useGenerateVideoPlayerActions = (send: Send) => {\n const actions = useMemo(() => createActions(send), [send]);\n\n const throttlePlay = useMemo(() => throttle(actions.play, CONTROLS_THROTTLE_TIME), [actions]);\n\n const throttlePause = useMemo(\n () =>\n throttle(async () => {\n actions.pause();\n }, CONTROLS_THROTTLE_TIME),\n [actions],\n );\n\n const throttlePreviousPlaylistItem = useMemo(\n () =>\n throttle(async () => {\n actions.previousPlaylistItem();\n }, CONTROLS_THROTTLE_TIME),\n [actions],\n );\n\n const throttleNextPlaylistItem = useMemo(\n () =>\n throttle(async () => {\n actions.nextPlaylistItem();\n }, CONTROLS_THROTTLE_TIME),\n [actions],\n );\n\n const throttlePreviousVideoSource = useMemo(\n () =>\n throttle(async () => {\n actions.previousVideoSource();\n }, CONTROLS_THROTTLE_TIME),\n [actions],\n );\n\n const throttleNextVideoSource = useMemo(\n () =>\n throttle(async () => {\n actions.nextVideoSource();\n }, CONTROLS_THROTTLE_TIME),\n [actions],\n );\n\n const throttleRemovePlaylistItem = useMemo(\n () =>\n throttle(async (id: string) => {\n actions.removePlaylistItem(id);\n }, CONTROLS_THROTTLE_TIME),\n [actions],\n );\n\n const throttleTogglePlaying = useMemo(\n () =>\n throttle(async () => {\n actions.togglePlaying();\n }, CONTROLS_THROTTLE_TIME),\n [actions],\n );\n\n const throttleSkipForward5s = useMemo(\n () =>\n throttle(async () => {\n actions.skipForward5s();\n }, CONTROLS_THROTTLE_TIME),\n [actions],\n );\n\n const throttleSkipBackward5s = useMemo(\n () =>\n throttle(async () => {\n actions.skipBackward5s();\n }, CONTROLS_THROTTLE_TIME),\n [actions],\n );\n\n const throttleJumpToTimeInMatch = useMemo(\n () =>\n throttle((time: number) => {\n actions.jumpToMatchTime(time);\n }, CONTROLS_THROTTLE_TIME),\n [actions],\n );\n\n const throttleJumpToTimePercentage = useMemo(\n () =>\n throttle((percent: number) => {\n actions.jumpToTimePercent(percent);\n }, CONTROLS_THROTTLE_TIME),\n [actions],\n );\n\n const handleKeyDown = useCallback(\n (event: KeyboardEvent) => {\n const isFormTag = getIsFormTag((event.target as HTMLElement).tagName);\n if (isFormTag) return;\n\n if (event.key === ' ') throttleTogglePlaying();\n if (event.key === 'ArrowRight') {\n event.ctrlKey ? throttleNextVideoSource() : throttleSkipForward5s();\n }\n if (event.key === 'ArrowLeft') {\n event.ctrlKey ? throttlePreviousVideoSource() : throttleSkipBackward5s();\n }\n },\n [\n throttlePreviousVideoSource,\n throttleNextVideoSource,\n throttleSkipBackward5s,\n throttleSkipForward5s,\n throttleTogglePlaying,\n ],\n );\n\n useEffect(() => {\n window.addEventListener('keydown', handleKeyDown);\n\n return () => {\n window.removeEventListener('keydown', handleKeyDown);\n };\n }, [handleKeyDown]);\n\n return useMemo(\n () => ({\n ...actions,\n jumpToTimeInMatch: throttleJumpToTimeInMatch,\n jumpToTimePercent: throttleJumpToTimePercentage,\n nextPlaylistItem: throttleNextPlaylistItem,\n nextVideoSource: throttleNextVideoSource,\n pause: throttlePause,\n play: throttlePlay,\n previousPlaylistItem: throttlePreviousPlaylistItem,\n previousVideoSource: throttlePreviousVideoSource,\n removePlaylistItem: throttleRemovePlaylistItem,\n }),\n [\n actions,\n throttleJumpToTimeInMatch,\n throttleJumpToTimePercentage,\n throttleNextPlaylistItem,\n throttleNextVideoSource,\n throttlePause,\n throttlePlay,\n throttlePreviousPlaylistItem,\n throttlePreviousVideoSource,\n throttleRemovePlaylistItem,\n ],\n );\n};\n","import { PlaylistItemType } from '../../../types';\n\nexport const getNextPlaylistItem = (playlistItemId: string, playlistItems: PlaylistItemType[]) => {\n const currentPlaylistItemIndex = playlistItems.findIndex(\n (playlistItem: PlaylistItemType) => playlistItem.id === playlistItemId,\n );\n const nextVideoIndex = currentPlaylistItemIndex + 1;\n return playlistItems[nextVideoIndex < playlistItems.length ? nextVideoIndex : 0];\n};\n","import { PlaylistItemType } from '../../../types';\n\nexport const getPreviousPlaylistItem = (playlistItemId: string, playlistItems: PlaylistItemType[]) => {\n const currentPlaylistItemIndex = playlistItems.findIndex(\n (playlistItem: PlaylistItemType) => playlistItem.id === playlistItemId,\n );\n const previousVideoIndex = currentPlaylistItemIndex - 1;\n\n return playlistItems[previousVideoIndex < 0 ? 0 : previousVideoIndex];\n};\n","import { PlayingMode, PlayingModes, PlaylistItemType } from '../../../types';\nimport { getVideoByVideoType } from '../../../util';\n\nconst getVideoSourceIndexAndTimeFromPlaylistTime = (\n playingMode: PlayingMode,\n playlistItem: PlaylistItemType | undefined,\n time: number,\n) => {\n if (!playlistItem) throw Error('Playlist is mandatory');\n\n const { videoSources } = getVideoByVideoType(playlistItem, playingMode);\n\n const result = videoSources.reduce(\n (result, videoSource, currentVideoIndex) => {\n const timeInVideoSource = videoSource.startTime + (time - result.startTime);\n\n const foundVideoIndex =\n timeInVideoSource <= videoSource.endTime && timeInVideoSource >= videoSource.startTime\n ? currentVideoIndex\n : result.videoIndex;\n\n const accumulatedStartTime = videoSource.endTime - videoSource.startTime + result.startTime;\n\n return {\n startTime: accumulatedStartTime,\n videoIndex: foundVideoIndex,\n timeInVideoSource: foundVideoIndex !== result.videoIndex ? timeInVideoSource : result.timeInVideoSource,\n };\n },\n { startTime: 0, videoIndex: -1, timeInVideoSource: 0 },\n );\n\n return { videoSourceIndex: result.videoIndex, currentSourceTime: result.timeInVideoSource };\n};\n\nexport const getVideoSourceIndexAndTimeFromMatchTime = (\n playingMode: PlayingMode,\n playlistItem: PlaylistItemType,\n timeInMatch: number,\n): { currentSourceTime: number; videoSourceIndex: number } => {\n const { videoSources } = getVideoByVideoType(playlistItem, playingMode);\n const isPlayingSelectionNotEffectiveTime =\n playingMode.mode === 'TACTICAL_CAMERA' && !playingMode.useEffectiveTime && videoSources.length > 1;\n\n for (let x = 0; x <= videoSources.length; x++) {\n const videoSource = videoSources[x];\n\n if (playingMode.mode !== PlayingModes.EPISODES) {\n if (timeInMatch >= videoSource.startTime && timeInMatch <= videoSource.endTime) {\n return { videoSourceIndex: x, currentSourceTime: timeInMatch };\n }\n\n if (!playingMode.useEffectiveTime && !isPlayingSelectionNotEffectiveTime) {\n return { videoSourceIndex: x, currentSourceTime: timeInMatch };\n }\n\n if (timeInMatch > videoSource.endTime && videoSources[x + 1] === undefined) {\n return { videoSourceIndex: x, currentSourceTime: videoSources[x].startTime };\n }\n\n if (timeInMatch > videoSource.endTime && timeInMatch < videoSources[x + 1].startTime) {\n return { videoSourceIndex: x + 1, currentSourceTime: videoSources[x + 1].startTime };\n }\n\n if (timeInMatch < videoSource.startTime) {\n return { videoSourceIndex: x, currentSourceTime: videoSource.startTime };\n }\n\n continue;\n }\n\n if (videoSource.startTimeInMatch === undefined || videoSource.endTimeInMatch === undefined) {\n throw Error('Action not valid in context without a game');\n }\n\n const diff = timeInMatch - videoSource.startTimeInMatch;\n\n if (\n timeInMatch >= videoSource.startTimeInMatch &&\n timeInMatch <= videoSource.endTimeInMatch &&\n diff >= videoSource.startTime &&\n diff <= videoSource.endTime\n ) {\n return { videoSourceIndex: x, currentSourceTime: diff };\n }\n\n if (\n timeInMatch >= videoSource.startTimeInMatch &&\n timeInMatch <= videoSource.endTimeInMatch &&\n diff < videoSource.startTime\n ) {\n return { videoSourceIndex: x, currentSourceTime: videoSource.startTime };\n }\n\n if (timeInMatch < videoSource.startTimeInMatch) {\n return { videoSourceIndex: x, currentSourceTime: videoSource.startTime };\n }\n }\n\n return { videoSourceIndex: 0, currentSourceTime: 0 };\n};\n\nexport const getVideoSourceIndexAndTimeFromPercentTime = (\n playingMode: PlayingMode,\n playlistItem: PlaylistItemType,\n timeInPercentage: number,\n): { currentSourceTime: number; videoSourceIndex: number } => {\n const secondInPercent = (timeInPercentage * playlistItem.duration) / 100;\n\n const isFullMatchNotEffectiveTime = playingMode.mode === 'TACTICAL_CAMERA' && !playingMode.useEffectiveTime;\n\n return isFullMatchNotEffectiveTime\n ? getVideoSourceIndexAndTimeFromMatchTime(playingMode, playlistItem, secondInPercent)\n : getVideoSourceIndexAndTimeFromPlaylistTime(playingMode, playlistItem, secondInPercent);\n};\n","import { PlayerStateMachineContext, PlayerStateMachineEvent, UpdatePlaylistItemAction } from '../../types';\nimport { getVideoByVideoType } from '../../util';\n\nexport function isPaused(context: PlayerStateMachineContext) {\n return !context.isPlaying;\n}\n\nexport function isPlaying(context: PlayerStateMachineContext) {\n return context.isPlaying;\n}\n\nexport function isInStandBy(context: PlayerStateMachineContext) {\n return context.isInStandBy;\n}\n\nexport function isLastVideoSource(context: PlayerStateMachineContext) {\n if (!context.playlist.playingItem.playlistItem) return false;\n\n const { videoSources } = getVideoByVideoType(\n context.playlist.playingItem.playlistItem,\n context.playlist.currentSelectedPlayingMode,\n );\n\n return videoSources.length - 1 === context.playlist.playingItem.videoSourceIndex;\n}\n\nexport function isLastPlaylistItem(context: PlayerStateMachineContext) {\n if (!context.playlist.playingItem.playlistItem) return false;\n\n const currentPlaylistItemIndex = context.playlist.playlistItems.findIndex(\n (playlistItem) => playlistItem.id === context.playlist.currentPlaylistItemId,\n );\n\n return currentPlaylistItemIndex === context.playlist.playlistItems.length - 1;\n}\n\nexport function isFirstPlaylistItem(context: PlayerStateMachineContext) {\n if (!context.playlist.playingItem.playlistItem) return false;\n\n const currentPlaylistItemIndex = context.playlist.playlistItems.findIndex(\n (playlistItem) => playlistItem.id === context.playlist.currentPlaylistItemId,\n );\n\n return currentPlaylistItemIndex === 0;\n}\n\nexport function isPlaylistEmpty(context: PlayerStateMachineContext) {\n return !Boolean(context.playlist.playlistItems.length);\n}\n\nexport function isPlayerNotReady(context: PlayerStateMachineContext) {\n return !Boolean(context.videoRef?.current);\n}\n\nexport function isPlayerReady(context: PlayerStateMachineContext) {\n return Boolean(context.videoRef?.current);\n}\n\nexport function isNotLastPlaylistItem(context: PlayerStateMachineContext) {\n return !isLastPlaylistItem(context);\n}\n\nexport function shouldAutoplayNextPlaylistItem(context: PlayerStateMachineContext) {\n return context.autoPlayNextPlaylistItem && isNotLastPlaylistItem(context);\n}\n\nexport function isCurrentPlaylistItem(context: PlayerStateMachineContext, _event: PlayerStateMachineEvent) {\n const event = _event as UpdatePlaylistItemAction;\n return context.playlist.currentPlaylistItemId === event.playlistItem.id;\n}\n\nexport const hasPlaylistItemNoDuration = (context: PlayerStateMachineContext) => {\n return context.playlist.playingItem.playlistItem.duration === 0;\n};\n","import { PlayerStateMachineContext, PlaylistItemType } from '../../../types';\nimport { getVideoByVideoType } from '../../../util';\n\nexport const jumpToPlaylistItem = (context: PlayerStateMachineContext, playlistItem: PlaylistItemType) => {\n const selectedVideoType = getVideoByVideoType(playlistItem, context.playlist.preferredPlayingMode);\n const videoSource = selectedVideoType.videoSources[0];\n\n return {\n currentTime: videoSource.startTime,\n playlist: {\n ...context.playlist,\n currentPlaylistItemId: playlistItem.id,\n currentSelectedPlayingMode: selectedVideoType.playingMode,\n playingItem: {\n currentSourceTime: 0,\n playlistItem,\n videoSourceIndex: 0,\n },\n },\n };\n};\n","import { PlayerStateMachineContext } from '../../../types';\nimport { getVideoSourceByIndex } from '../../../util';\n\nexport const jumpToVideoSource = (videoSourceIndex: number, context: PlayerStateMachineContext) => {\n const videoSource = getVideoSourceByIndex(\n context.playlist.playingItem.playlistItem,\n context.playlist.currentSelectedPlayingMode,\n videoSourceIndex,\n );\n\n return {\n currentTime: videoSource.startTime,\n playlist: {\n ...context.playlist,\n playingItem: {\n ...context.playlist.playingItem,\n videoSourceIndex: videoSourceIndex,\n },\n },\n };\n};\n","export class AsyncQueue {\n private queue: (() => Promise)[] = [];\n private isProcessing = false;\n\n public enqueue(task: () => Promise): void {\n this.queue.push(task);\n if (!this.isProcessing) this.processQueue();\n }\n\n private async processQueue() {\n if (this.isProcessing) return;\n this.isProcessing = true;\n\n while (this.queue.length > 0) {\n const currentTask = this.queue.shift();\n if (currentTask) {\n try {\n await currentTask();\n } catch (error) {\n console.error('Error processing task:', error);\n }\n }\n }\n\n this.isProcessing = false;\n }\n}\n\nexport const playerPlayingControlQueue = new AsyncQueue();\n","import { PlaylistItemType } from '../../../types';\n\nexport const replacePlayListItemByIndex = (array: Array, index: number, value: PlaylistItemType) => {\n const ret = array.slice(0);\n ret[index] = value;\n return ret;\n};\n","import { PlayerStateMachineContext } from '../../../types';\nimport { getCurrentPlaylistItemTime } from '../get-current-playlist-item-time';\nimport { getVideoSourceIndexAndTimeFromPercentTime } from '../get-video-source-index';\n\nexport const SKIP_STEP_SIZE = 5;\n\nexport const skipTime = (context: PlayerStateMachineContext, time: number) => {\n const currentPlaylistItemTime = getCurrentPlaylistItemTime(\n context.playlist.currentSelectedPlayingMode,\n context.videoRef?.current?.currentTime ?? 0,\n context.playlist.playingItem.videoSourceIndex,\n context.playlist.playingItem.playlistItem,\n );\n\n const percent = ((currentPlaylistItemTime + time) * 100) / context.playlist.playingItem.playlistItem.duration;\n\n const { videoSourceIndex, currentSourceTime } = getVideoSourceIndexAndTimeFromPercentTime(\n context.playlist.currentSelectedPlayingMode,\n context.playlist.playingItem.playlistItem,\n percent <= 0 ? 0 : percent >= 100 ? 100 : percent,\n );\n\n return {\n currentTime: currentSourceTime,\n playlist: {\n ...context.playlist,\n playingItem: {\n ...context.playlist.playingItem,\n videoSourceIndex,\n },\n },\n };\n};\n","import { arrayMoveImmutable } from 'array-move';\nimport isEmpty from 'lodash/isEmpty';\nimport { assign } from 'xstate';\n\nimport {\n AddPlaylistItemsAction,\n ChangeAutoplayPlaylistItemAction,\n PlayingModes,\n RemovePlaylistItemsAction,\n UpdatePlaylistItemAction,\n VideoSourceWithTimes,\n} from 'shared/components/video-player/types';\n\nimport { getNextPlaylistItem } from './get-next-playlist-item';\nimport { getPreviousPlaylistItem } from './get-previous-playlist-item';\nimport {\n getVideoSourceIndexAndTimeFromMatchTime,\n getVideoSourceIndexAndTimeFromPercentTime,\n} from './get-video-source-index';\nimport { isFirstPlaylistItem, isLastPlaylistItem } from './guards';\nimport { jumpToPlaylistItem } from './jump-to-playlist-item';\nimport { jumpToVideoSource } from './jump-to-video-source';\nimport { playerPlayingControlQueue } from './playing-control-queue';\nimport { replacePlayListItemByIndex } from './replace-playlist-item-by-index';\nimport { SKIP_STEP_SIZE, skipTime } from './skip-time';\nimport {\n ChangePlayingModeAction,\n JumpToMatchTimeAction,\n JumpToPercentTimeAction,\n PlayerStateMachineContext,\n PlayerStateMachineEvent,\n PlaylistItemType,\n RemovePlaylistAction,\n ReorderPlaylistItemAction,\n SetPlaylistAction,\n SetPlaylistItemAction,\n VideoSourceType,\n} from '../../types';\nimport {\n getCurrentTimeAndVideoSourceIndex,\n getCurrentVideoSource,\n getVideoByVideoType,\n getVideoSourceByIndex,\n} from '../../util';\n\nexport const nextVideoSource = assign((context) => {\n const { videoSources } = getVideoByVideoType(\n context.playlist.playingItem.playlistItem,\n context.playlist.currentSelectedPlayingMode,\n );\n\n const nextVideoSourceIndex = context.playlist.playingItem.videoSourceIndex + 1;\n const isLastVideoSource = nextVideoSourceIndex >= videoSources.length;\n\n if (isLastVideoSource) {\n return !isLastPlaylistItem(context) && context.autoPlayNextPlaylistItem\n ? jumpToPlaylistItem(\n context,\n getNextPlaylistItem(context.playlist.currentPlaylistItemId, context.playlist.playlistItems),\n )\n : {\n isPlaying: false,\n ...(context?.videoRef?.current && {\n currentTime: context.videoRef.current.currentTime,\n }),\n };\n }\n\n return jumpToVideoSource(nextVideoSourceIndex, context);\n});\n\nexport const previousVideoSource = assign((context) => {\n const previousVideoSourceIndex = context.playlist.playingItem.videoSourceIndex - 1;\n\n if (context.playlist.playingItem.videoSourceIndex === 0 && !isFirstPlaylistItem(context)) {\n return jumpToPlaylistItem(\n context,\n getPreviousPlaylistItem(context.playlist.currentPlaylistItemId, context.playlist.playlistItems),\n );\n }\n\n return jumpToVideoSource(previousVideoSourceIndex >= 0 ? previousVideoSourceIndex : 0, context);\n});\n\nexport const restart = assign((context) => {\n const videoSource = getVideoSourceByIndex(\n context.playlist.playingItem.playlistItem,\n context.playlist.currentSelectedPlayingMode,\n 0,\n );\n\n return {\n currentTime: videoSource.startTime,\n playlist: {\n ...context.playlist,\n playingItem: {\n ...context.playlist.playingItem,\n videoSourceIndex: 0,\n },\n },\n };\n});\n\nexport const seekNewTime = assign((context, _event) => {\n const event = _event as JumpToPercentTimeAction;\n\n const { videoSourceIndex, currentSourceTime } = getVideoSourceIndexAndTimeFromPercentTime(\n context.playlist.currentSelectedPlayingMode,\n context.playlist.playingItem.playlistItem,\n event.percent,\n );\n\n return {\n currentTime: currentSourceTime,\n playlist: {\n ...context.playlist,\n playingItem: {\n ...context.playlist.playingItem,\n videoSourceIndex,\n },\n },\n };\n});\n\nexport const jumpToMatchTime = assign((context, _event) => {\n const event = _event as JumpToMatchTimeAction;\n\n const { videoSourceIndex, currentSourceTime } = getVideoSourceIndexAndTimeFromMatchTime(\n context.playlist.currentSelectedPlayingMode,\n context.playlist.playingItem.playlistItem,\n event.time,\n );\n\n return {\n currentTime: currentSourceTime,\n playlist: {\n ...context.playlist,\n playingItem: {\n ...context.playlist.playingItem,\n videoSourceIndex,\n },\n },\n };\n});\n\nexport const setVideoRef = assign({\n videoRef: (context: PlayerStateMachineContext, _event: PlayerStateMachineEvent) => {\n const event = _event as SetPlaylistAction;\n return event.playerRef;\n },\n});\n\nconst getValidCurrentTime = (newTime: number | undefined, videoSource: VideoSourceWithTimes) => {\n if (!newTime) return videoSource.startTime;\n if (newTime < videoSource.startTime || newTime > videoSource.endTime) return videoSource.startTime;\n\n return newTime;\n};\n\nexport const addPlaylistItems = assign(\n (context: PlayerStateMachineContext, _event: PlayerStateMachineEvent) => {\n const event = _event as AddPlaylistItemsAction;\n\n if (event.playlistItems.length === 0) return context;\n const isCurrentPlaylistEmpty = context.playlist.playlistItems.length === 0;\n\n if (isCurrentPlaylistEmpty) {\n const { videoSources: currentVideoSource } = getVideoByVideoType(\n event.playlistItems[0],\n context.playlist.currentSelectedPlayingMode,\n );\n context.currentTime = currentVideoSource[0].startTime;\n\n const playingItem = isCurrentPlaylistEmpty\n ? {\n ...context.playlist.playingItem,\n currentSourceTime: currentVideoSource[0].startTime,\n videoSourceIndex: 0,\n playlistItem: event.playlistItems[0],\n }\n : context.playlist.playingItem;\n\n const currentPlaylistItemId = isCurrentPlaylistEmpty\n ? event.playlistItems[0].id\n : context.playlist.currentPlaylistItemId;\n\n return {\n playlist: {\n ...context.playlist,\n currentPlaylistItemId,\n playlistItems: event.playlistItems,\n playingItem,\n },\n };\n }\n\n const updatedPlaylistItem =\n event.playlistItems.find((item) => item.id === context.playlist.playingItem.playlistItem.id) ||\n context.playlist.playingItem.playlistItem;\n\n const { videoSources } = getVideoByVideoType(updatedPlaylistItem, context.playlist.currentSelectedPlayingMode);\n const currentTime = getValidCurrentTime(\n context.videoRef?.current?.currentTime,\n videoSources[context.playlist.playingItem.currentSourceTime],\n );\n\n return {\n ...context,\n currentTime,\n playlist: {\n ...context.playlist,\n playlistItems: event.playlistItems,\n playingItem: {\n ...context.playlist.playingItem,\n playlistItem: updatedPlaylistItem,\n },\n },\n };\n },\n);\n\nexport const updatePlaylistItem = assign((context, _event) => {\n const event = _event as UpdatePlaylistItemAction;\n\n let currentTime = context.currentTime;\n\n const playlistItemIndex = context.playlist.playlistItems.findIndex(\n (playlistItem: PlaylistItemType) => playlistItem.id === event.playlistItem.id,\n );\n\n const isCurrentPlaylistItem = context.playlist.currentPlaylistItemId === context.playlist.playingItem.playlistItem.id;\n\n if (isCurrentPlaylistItem) {\n const { videoSources } = getVideoByVideoType(event.playlistItem, context.playlist.currentSelectedPlayingMode);\n currentTime = event.currentTime\n ? event.currentTime\n : getValidCurrentTime(context.videoRef?.current?.currentTime, videoSources[0]) ?? videoSources[0].startTime;\n }\n\n return {\n currentTime,\n playlist: {\n ...context.playlist,\n playingItem: {\n ...context.playlist.playingItem,\n playlistItem: isCurrentPlaylistItem ? event.playlistItem : context.playlist.playingItem.playlistItem,\n },\n playlistItems: replacePlayListItemByIndex(context.playlist.playlistItems, playlistItemIndex, event.playlistItem),\n },\n };\n});\n\nconst EMPTY_PLAYLIST = {\n playlistItems: [],\n currentPlaylistItemId: '',\n playingItem: {\n currentSourceTime: 0,\n playlistItem: {\n videoTypes: [],\n id: '',\n\n duration: 0,\n recordingId: '',\n fundamentalsSelected: {\n tacticalAnalysisId: undefined,\n fundamentalsSelected: [],\n },\n hasHomographies: false,\n },\n videoSourceIndex: 0,\n },\n};\n\nexport const updatePlaylistItems = assign((context, _event) => {\n const event = _event as SetPlaylistAction;\n\n if (!event.playlistItems || event.playlistItems.length === 0) {\n return {\n currentTime: 0,\n playlist: {\n ...context.playlist,\n ...EMPTY_PLAYLIST,\n },\n };\n }\n\n return {\n playlist: {\n ...context.playlist,\n playlistItems: event.playlistItems,\n },\n };\n});\n\nexport const setPlaylistItems = assign((context, _event) => {\n const event = _event as SetPlaylistAction;\n const currentVideoSource = getCurrentVideoSource(context);\n\n if (!event.playlistItems || event.playlistItems.length === 0) {\n return {\n isPlaying: !!event.autoplay,\n playlist: {\n ...context.playlist,\n ...EMPTY_PLAYLIST,\n preferredPlayingMode: event.playingMode ?? context.playlist.preferredPlayingMode,\n },\n };\n }\n\n const playingMode = event.playingMode ? event.playingMode : context.playlist.preferredPlayingMode;\n const { id } = event.playlistItems[0];\n const currentPlaylistItem = event.playlistItems[0];\n\n const { videoSources, playingMode: currentPlayingMode } = getVideoByVideoType(currentPlaylistItem, playingMode);\n const currentMatchTime =\n currentPlayingMode.mode === PlayingModes.EPISODES\n ? (context.videoRef?.current?.currentTime ?? 0) + (currentVideoSource?.startTimeInMatch ?? 0)\n : context.videoRef?.current?.currentTime ?? 0;\n\n const initialTime = event.tryToKeepCurrentTime ? currentMatchTime : event.initialStartTime ?? 0;\n\n // TODO: when setting new playlist items there is a case when videoSources are empty for a split of second.\n // This is a temporary fix.\n // it's happening when we play the row with episodes videos.\n if (videoSources.length === 0) return context;\n\n const { videoIndex, currentTime } = getCurrentTimeAndVideoSourceIndex(initialTime, videoSources, playingMode);\n // TODO: return it instead of modifying the context. Return doesn't work with trim player.\n context.isPlaying = event.autoplay ? context.isPlaying : false;\n context.playlist = {\n ...context.playlist,\n currentPlaylistItemId: id,\n currentSelectedPlayingMode: currentPlayingMode,\n playlistItems: event.playlistItems,\n playingItem: {\n currentSourceTime: 0,\n playlistItem: currentPlaylistItem,\n videoSourceIndex: videoIndex,\n },\n };\n\n context.currentTime = currentTime;\n\n return context;\n});\n\nexport const setPlayerToPlay = assign({\n isPlaying: true,\n});\n\nexport const setResumeStandBy = assign({\n isInStandBy: false,\n});\n\nexport const setStandBy = assign({\n isInStandBy: true,\n});\n\nexport const toggleFullScreen = assign({\n isFullScreen: (context: PlayerStateMachineContext) => !context.isFullScreen,\n});\n\nexport const setPlayerToPause = assign({\n isPlaying: false,\n});\n\nexport const reorderPlaylistItem = assign({\n playlist: (context: PlayerStateMachineContext, _event: PlayerStateMachineEvent) => {\n const event = _event as ReorderPlaylistItemAction;\n\n const reorderedPlaylistItems = arrayMoveImmutable(\n context.playlist.playlistItems,\n event.currentVideoIndex,\n event.newVideoIndex,\n );\n\n return {\n ...context.playlist,\n playlistItems: reorderedPlaylistItems,\n };\n },\n});\n\nexport const changeAutoplayNextPlaylistItem = assign(\n (context: PlayerStateMachineContext, _event: PlayerStateMachineEvent) => {\n const event = _event as ChangeAutoplayPlaylistItemAction;\n\n return {\n autoPlayNextPlaylistItem: event.autoplayNextPlaylistItem,\n };\n },\n);\n\nconst findNearestPlaylistItem = (playlistItems: PlaylistItemType[], playlistItemIndex: number) => {\n return playlistItemIndex <= playlistItems.length - 1 ? playlistItemIndex + 1 : playlistItems.length - 1;\n};\n\nexport const changePlayingMode = assign((context, _event) => {\n const event = _event as ChangePlayingModeAction;\n\n const playlistItemIndex = context.playlist.playlistItems.findIndex(\n (playlistItem: PlaylistItemType) => playlistItem.id === context.playlist.currentPlaylistItemId,\n );\n const videoSource = getVideoSourceByIndex(context.playlist.playlistItems[playlistItemIndex], event.playingMode, 0);\n const oldVideoSource = getVideoSourceByIndex(\n context.playlist.playlistItems[playlistItemIndex],\n context.playlist.currentSelectedPlayingMode,\n 0,\n );\n\n context.currentTime =\n videoSource.src === oldVideoSource.src\n ? context.videoRef?.current?.currentTime ?? videoSource.startTime\n : videoSource.startTime + context.currentTime - oldVideoSource.startTime;\n\n context.playlist = {\n ...context.playlist,\n currentSelectedPlayingMode: event.playingMode,\n playingItem: {\n currentSourceTime: 0,\n videoSourceIndex: 0,\n playlistItem: context.playlist.playlistItems[playlistItemIndex],\n },\n };\n\n return context;\n});\n\nexport const removePlaylistItem = assign((context, _event) => {\n const event = _event as RemovePlaylistAction;\n\n const playlistItemIndex = context.playlist.playlistItems.findIndex(\n (playlistItem: PlaylistItemType) => playlistItem.id === event.playlistItemId,\n );\n\n const isCurrentPlaylistItem = event.playlistItemId === context.playlist.currentPlaylistItemId;\n const modifiedPlaylistItems = context.playlist.playlistItems.filter((item, index) => index !== playlistItemIndex);\n\n if (isCurrentPlaylistItem) {\n const newCurrentVideoIndex = modifiedPlaylistItems[playlistItemIndex]\n ? playlistItemIndex\n : findNearestPlaylistItem(modifiedPlaylistItems, playlistItemIndex);\n\n const videoSource = isEmpty(modifiedPlaylistItems)\n ? { currentTime: 0, endTime: 0, startTime: 0, duration: 0 }\n : getVideoSourceByIndex(\n modifiedPlaylistItems[newCurrentVideoIndex],\n context.playlist.currentSelectedPlayingMode,\n 0,\n );\n\n return {\n currentTime: videoSource.startTime,\n playlist: {\n ...context.playlist,\n playlistItems: modifiedPlaylistItems,\n currentPlaylistItemId: isEmpty(modifiedPlaylistItems) ? '' : modifiedPlaylistItems[newCurrentVideoIndex].id,\n playingItem: {\n currentSourceTime: 0,\n videoSourceIndex: 0,\n playlistItem: isEmpty(modifiedPlaylistItems)\n ? {\n videoTypes: [],\n id: '',\n\n duration: 0,\n recordingId: '',\n fundamentalsSelected: {\n tacticalAnalysisId: undefined,\n fundamentalsSelected: [],\n },\n hasHomographies: false,\n }\n : modifiedPlaylistItems[newCurrentVideoIndex],\n },\n },\n };\n }\n\n return {\n playlist: {\n ...context.playlist,\n playlistItems: modifiedPlaylistItems,\n },\n };\n});\n\nexport const removePlaylistItems = assign((context, _event) => {\n const event = _event as RemovePlaylistItemsAction;\n\n let currentTime = context.currentTime;\n\n const isCurrentPlaylistItem = event.playlistItemsIds.includes(context.playlist.currentPlaylistItemId);\n\n const modifiedPlaylistItems = context.playlist.playlistItems.filter(\n (item) => !event.playlistItemsIds.includes(item.id),\n );\n\n if (isEmpty(modifiedPlaylistItems)) {\n return {\n currentTime: 0,\n playlist: {\n ...context.playlist,\n playlistItems: [],\n currentPlaylistItemId: '',\n playingItem: {\n currentSourceTime: 0,\n playlistItem: {\n videoTypes: [],\n id: '',\n\n duration: 0,\n recordingId: '',\n fundamentalsSelected: {\n tacticalAnalysisId: undefined,\n fundamentalsSelected: [],\n },\n hasHomographies: false,\n },\n videoSourceIndex: 0,\n },\n },\n };\n }\n\n if (isCurrentPlaylistItem && !isEmpty(modifiedPlaylistItems)) {\n const videoSource = getVideoSourceByIndex(modifiedPlaylistItems[0], context.playlist.currentSelectedPlayingMode, 0);\n\n currentTime = videoSource.startTime;\n }\n\n return {\n currentTime,\n playlist: {\n ...context.playlist,\n playlistItems: modifiedPlaylistItems,\n ...(isCurrentPlaylistItem && {\n currentPlaylistItemId: isEmpty(modifiedPlaylistItems) ? '' : modifiedPlaylistItems[0]?.id,\n playingItem: {\n currentSourceTime: 0,\n videoSourceIndex: 0,\n playlistItem: isEmpty(modifiedPlaylistItems)\n ? {\n videoTypes: [],\n id: '',\n\n duration: 0,\n recordingId: '',\n fundamentalsSelected: {\n tacticalAnalysisId: undefined,\n fundamentalsSelected: [],\n },\n hasHomographies: false,\n }\n : modifiedPlaylistItems[0],\n },\n }),\n },\n };\n});\n\nexport const setPlaylistItem = assign((context, _event) => {\n const event = _event as SetPlaylistItemAction;\n\n const playlistItemIndex = context.playlist.playlistItems.findIndex(\n (playlistItem: PlaylistItemType) => playlistItem.id === event.playlistItemId,\n );\n\n const currentPlaylistItem = context.playlist.playlistItems[playlistItemIndex];\n\n const selectedVideoType = getVideoByVideoType(currentPlaylistItem, context.playlist.preferredPlayingMode);\n const videoSource = selectedVideoType.videoSources[0];\n\n return {\n currentTime: videoSource.startTime,\n isPlaying: event.autoPlay,\n playlist: {\n ...context.playlist,\n currentPlaylistItemId: currentPlaylistItem.id,\n currentSelectedPlayingMode: selectedVideoType.playingMode,\n playingItem: {\n currentSourceTime: 0,\n playlistItem: currentPlaylistItem,\n videoSourceIndex: 0,\n },\n },\n };\n});\n\nexport const play = async (context: PlayerStateMachineContext) => {\n if (!context.videoRef?.current) return;\n\n if ((!context.videoRef.current.playing && !context.isInStandBy) || context.videoRef.current.playing) {\n playerPlayingControlQueue.enqueue(async () => context.videoRef?.current?.play());\n } else if (context.isInStandBy) {\n playerPlayingControlQueue.enqueue(async () => context.videoRef?.current?.pause());\n }\n};\n\nexport const pause = async (context: PlayerStateMachineContext) => {\n if (!context.videoRef?.current) return;\n\n if (context.videoRef.current.playing) {\n playerPlayingControlQueue.enqueue(async () => context.videoRef?.current?.pause());\n }\n};\n\nexport const skipForward = assign((context) => {\n return skipTime(context, SKIP_STEP_SIZE);\n});\n\nexport const skipBackward = assign((context) => {\n return skipTime(context, -SKIP_STEP_SIZE);\n});\n\nexport const nextPlaylistItem = assign((context) => {\n const nextPlaylistItem = getNextPlaylistItem(context.playlist.currentPlaylistItemId, context.playlist.playlistItems);\n\n return jumpToPlaylistItem(context, nextPlaylistItem);\n});\n\nexport const firstPlaylistItem = assign((context) => {\n const firstPlaylistItem = context.playlist.playlistItems[0];\n\n const selectedVideoType = getVideoByVideoType(firstPlaylistItem, context.playlist.preferredPlayingMode);\n const videoSource = selectedVideoType.videoSources[0];\n\n return {\n currentTime: videoSource.startTime,\n playlist: {\n ...context.playlist,\n currentPlaylistItemId: firstPlaylistItem.id,\n currentSelectedPlayingMode: selectedVideoType.playingMode,\n playingItem: {\n currentSourceTime: 0,\n playlistItem: firstPlaylistItem,\n videoSourceIndex: 0,\n },\n },\n };\n});\n\nexport const previousPlaylistItem = assign((context) => {\n const previousPlaylistItem = getPreviousPlaylistItem(\n context.playlist.currentPlaylistItemId,\n context.playlist.playlistItems,\n );\n\n const selectedVideoType = getVideoByVideoType(previousPlaylistItem, context.playlist.preferredPlayingMode);\n const videoSource = selectedVideoType.videoSources[0];\n\n return {\n currentTime: videoSource.startTime,\n playlist: {\n ...context.playlist,\n currentPlaylistItemId: previousPlaylistItem.id,\n currentSelectedPlayingMode: selectedVideoType.playingMode,\n playingItem: {\n currentSourceTime: 0,\n playlistItem: previousPlaylistItem,\n videoSourceIndex: 0,\n },\n },\n };\n});\n\nconst canDurationToPlaylistItemBeChanged = (videoTypes: VideoSourceType[], hasHomographies: boolean) => {\n if (!hasHomographies && videoTypes.some((videoType) => videoType.videoSources.length > 1)) {\n console.error('Not possible to add duration to VideoSourceType with more the one VideoSource');\n return false;\n }\n\n return true;\n};\n\nconst addDurationToPlaylistItem = (videoTypes: VideoSourceType[], duration: number): VideoSourceType[] => {\n return videoTypes.map((videoType) => {\n return {\n ...videoType,\n videoSources: videoType.videoSources.map((videoSource) => ({\n ...videoSource,\n ...(!videoSource?.startTimeInMatch && {\n startTimeInMatch: videoSource.startTime,\n }),\n endTime: duration,\n endTimeInMatch: duration,\n })),\n };\n });\n};\n\nexport const fixPlaylistItemWithoutNoDuration = assign(\n (context) => {\n const playlistItem = context.playlist.playingItem.playlistItem;\n\n if (!canDurationToPlaylistItemBeChanged(playlistItem.videoTypes, playlistItem.hasHomographies)) return context;\n\n const duration = context.videoRef?.current?.duration ?? 0;\n\n const videoTypes = addDurationToPlaylistItem(playlistItem.videoTypes, duration);\n\n const adjustedPlaylistItem = {\n ...playlistItem,\n duration,\n videoTypes,\n };\n\n return {\n playlist: {\n ...context.playlist,\n playingItem: {\n ...context.playlist.playingItem,\n playlistItem: adjustedPlaylistItem,\n },\n },\n };\n },\n);\n","import { useMachine } from '@xstate/react';\nimport { useEffect, useRef } from 'react';\nimport { createMachine, send } from 'xstate';\n\nimport { PlayingMode } from 'shared/components/video-player/types';\n\nimport {\n usePlayerSetIsPlaying,\n usePlayerSetPlaylist,\n usePlayerState,\n usePlayerUpdateStandBy,\n useSetCurrentTime,\n} from './atoms/hooks';\nimport { changeSourceMachine } from './change-source-and-time-machine';\nimport { useGenerateVideoPlayerActions } from './hooks';\nimport { PLAYER_ACTIONS, PLAYER_STATES, READY_STATES, VIDEO_PLAYER_MACHINE_ID } from './states';\nimport {\n addPlaylistItems,\n changeAutoplayNextPlaylistItem,\n changePlayingMode,\n firstPlaylistItem,\n fixPlaylistItemWithoutNoDuration,\n jumpToMatchTime,\n nextPlaylistItem,\n nextVideoSource,\n pause,\n play,\n previousPlaylistItem,\n previousVideoSource,\n removePlaylistItem,\n removePlaylistItems,\n reorderPlaylistItem,\n restart,\n seekNewTime,\n setPlayerToPause,\n setPlayerToPlay,\n setPlaylistItem,\n setPlaylistItems,\n setResumeStandBy,\n setStandBy,\n setVideoRef,\n skipBackward,\n skipForward,\n toggleFullScreen,\n updatePlaylistItem,\n updatePlaylistItems,\n} from './utils/actions';\nimport {\n hasPlaylistItemNoDuration,\n isCurrentPlaylistItem,\n isInStandBy,\n isLastPlaylistItem,\n isLastVideoSource,\n isNotLastPlaylistItem,\n isPaused,\n isPlayerNotReady,\n isPlayerReady,\n isPlaying,\n isPlaylistEmpty,\n shouldAutoplayNextPlaylistItem,\n} from './utils/guards';\nimport {\n PlayerStateMachineContext,\n PlayerStateMachineEvent,\n PlayerType,\n SetPlaylistAction,\n SetReplacePlaylistItemAction,\n} from '../types';\n\nconst defaultPlayerState = (playingMode: PlayingMode, playerId: string, player: PlayerType, currentTime: number) => {\n const initialState: PlayerStateMachineContext = {\n playerId,\n playlist: {\n preferredPlayingMode: playingMode,\n currentSelectedPlayingMode: playingMode,\n currentPlaylistItemId: '',\n playlistItems: [],\n playingItem: {\n currentSourceTime: 0,\n playlistItem: {\n videoTypes: [],\n duration: 0,\n name: '',\n id: '',\n index: 0,\n recordingId: '',\n hasHomographies: false,\n fundamentalsSelected: {\n tacticalAnalysisId: undefined,\n fundamentalsSelected: [],\n },\n },\n videoSourceIndex: 0,\n },\n },\n isFullScreen: false,\n isInStandBy: false,\n isPlaying: false,\n autoPlayNextPlaylistItem: true,\n currentTime,\n videoRef: player,\n };\n return {\n context: initialState,\n };\n};\n\nexport const playerStateMachine = createMachine(\n {\n predictableActionArguments: true,\n id: VIDEO_PLAYER_MACHINE_ID,\n initial: PLAYER_STATES.IDLE,\n states: {\n [PLAYER_STATES.IDLE]: {\n on: {\n [PLAYER_ACTIONS.LOAD_PLAYLIST]: [\n {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['setPlaylistItems'],\n cond: 'isPlayerReady',\n },\n {\n target: PLAYER_STATES.IDLE,\n actions: ['setPlaylistItems'],\n cond: 'isPlayerNotReady',\n },\n ],\n [PLAYER_ACTIONS.REFRESH]: [\n {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n },\n ],\n },\n },\n [PLAYER_STATES.EMPTY_PLAYLIST]: {\n on: {\n [PLAYER_ACTIONS.REPLACE_PLAYLIST_ITEMS]: {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['addPlaylistItems'],\n },\n [PLAYER_ACTIONS.REPLACE_PLAYLIST]: [\n {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['setPlaylistItems'],\n },\n ],\n [PLAYER_ACTIONS.LOAD_PLAYLIST]: [\n {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['setPlaylistItems'],\n },\n ],\n },\n },\n [PLAYER_STATES.ERROR]: {\n on: {\n [PLAYER_ACTIONS.LOAD_PLAYLIST]: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n },\n },\n [PLAYER_STATES.LOADING_VIDEO_SOURCE]: {\n on: {\n [PLAYER_ACTIONS.STAND_BY]: {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['setStandBy'],\n },\n [PLAYER_ACTIONS.PLAY]: {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['setPlayerToPlay', 'play'],\n },\n [PLAYER_ACTIONS.PAUSE]: {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['setPlayerToPause', 'pause'],\n },\n [PLAYER_ACTIONS.RESUME_STAND_BY]: {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['setResumeStandBy'],\n },\n [PLAYER_ACTIONS.CHANGE_PLAYING_MODE]: {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['changePlayingMode', 'pause'],\n },\n },\n always: {\n target: PLAYER_STATES.EMPTY_PLAYLIST,\n cond: 'isPlaylistEmpty',\n },\n invoke: {\n src: changeSourceMachine,\n onDone: { target: PLAYER_STATES.READY },\n data: {\n playingMode: (context: PlayerStateMachineContext) => context.playlist.currentSelectedPlayingMode,\n videoRef: (context: PlayerStateMachineContext) => context.videoRef,\n playlistItem: (context: PlayerStateMachineContext) => context.playlist.playingItem.playlistItem,\n videoSourceIndex: (context: PlayerStateMachineContext) => context.playlist.playingItem.videoSourceIndex,\n currentTime: (context: PlayerStateMachineContext): number => context.currentTime,\n },\n },\n },\n [PLAYER_STATES.READY]: {\n initial: READY_STATES.IDLE,\n on: {\n [PLAYER_ACTIONS.UPDATE_PLAYLIST_ITEMS]: [\n {\n target: PLAYER_STATES.READY,\n actions: ['updatePlaylistItems'],\n },\n ],\n [PLAYER_ACTIONS.CHANGE_AUTOPLAY_NEXT_PLAYLIST_ITEM]: [\n {\n target: PLAYER_STATES.READY,\n actions: ['changeAutoplayNextPlaylistItem'],\n },\n ],\n [PLAYER_ACTIONS.REORDER_PLAYLIST_ITEM]: [\n {\n target: PLAYER_STATES.READY,\n actions: ['reorderPlaylistItem', 'pause'],\n },\n ],\n [PLAYER_ACTIONS.REPLACE_PLAYLIST_ITEMS]: {\n target: PLAYER_STATES.READY,\n actions: ['addPlaylistItems'],\n },\n [PLAYER_ACTIONS.REMOVE_PLAYLIST_ITEM]: [\n {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['removePlaylistItem', 'pause'],\n },\n ],\n [PLAYER_ACTIONS.REMOVE_PLAYLIST_ITEMS]: [\n {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['removePlaylistItems', 'pause'],\n },\n ],\n [PLAYER_ACTIONS.NEXT_PLAYLIST_ITEM]: [\n {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['nextPlaylistItem', 'pause'],\n },\n ],\n [PLAYER_ACTIONS.AUTOPLAY_NEXT_PLAYLIST_ITEM]: [\n {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n cond: 'shouldAutoplayNextPlaylistItem',\n actions: ['nextPlaylistItem', 'pause'],\n },\n {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n cond: 'isLastPlaylistItem',\n actions: ['setPlayerToPause', 'pause', 'firstPlaylistItem'],\n },\n {\n target: PLAYER_STATES.READY,\n actions: ['setPlayerToPause', 'pause'],\n },\n ],\n [PLAYER_ACTIONS.PREVIOUS_PLAYLIST_ITEM]: [\n {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['previousPlaylistItem', 'pause'],\n },\n ],\n [PLAYER_ACTIONS.NEXT_VIDEO_SOURCE]: [\n {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n cond: 'isNotLastPlaylistItem',\n actions: ['nextVideoSource', 'pause'],\n },\n {\n target: PLAYER_STATES.READY,\n cond: 'isLastVideoSource',\n actions: ['setPlayerToPause', 'pause'],\n },\n {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['nextVideoSource', 'pause'],\n },\n ],\n [PLAYER_ACTIONS.PREVIOUS_VIDEO_SOURCE]: [\n {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['previousVideoSource', 'pause'],\n },\n ],\n [PLAYER_ACTIONS.UPDATE_PLAYLIST_ITEM]: [\n {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['updatePlaylistItem'],\n },\n ],\n [PLAYER_ACTIONS.CHANGE_PLAYING_MODE]: [\n {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['changePlayingMode', 'pause'],\n },\n ],\n [PLAYER_ACTIONS.TOGGLE_FULL_SCREEN]: [\n {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['toggleFullScreen'],\n },\n ],\n [PLAYER_ACTIONS.JUMP_TO_MATCH_TIME]: [\n {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['pause', 'jumpToMatchTime', 'setPlayerToPlay'],\n },\n ],\n [PLAYER_ACTIONS.REPLACE_PLAYLIST]: [\n {\n target: PLAYER_STATES.IDLE,\n actions: [\n send((context, _event) => {\n const event = _event as SetReplacePlaylistItemAction;\n\n return {\n type: PLAYER_ACTIONS.LOAD_PLAYLIST,\n playlistItems: event.playlistItems,\n playingMode: event.playingMode,\n autoplay: event.autoplay,\n tryToKeepCurrentTime: event.tryToKeepCurrentTime,\n } as SetPlaylistAction;\n }),\n ],\n },\n ],\n [PLAYER_ACTIONS.RESUME_STAND_BY]: {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['setResumeStandBy'],\n },\n [PLAYER_ACTIONS.RESTART]: {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['setPlayerToPlay', 'restart'],\n },\n },\n states: {\n [READY_STATES.IDLE]: {\n always: [\n {\n target: READY_STATES.SEEKING_NEW_TIME,\n cond: 'hasPlaylistItemNoDuration',\n actions: ['fixPlaylistItemWithoutNoDuration'],\n },\n { target: READY_STATES.STAND_BY, cond: 'isInStandBy', actions: ['pause'] },\n { target: READY_STATES.PAUSED, cond: 'isPaused', actions: ['pause', 'setPlayerToPause'] },\n { target: READY_STATES.PLAYING, cond: 'isPlaying', actions: ['play', 'setPlayerToPlay'] },\n ],\n },\n [READY_STATES.STAND_BY]: {\n on: {\n [PLAYER_ACTIONS.REPLACE_PLAYLIST_ITEMS]: {\n target: READY_STATES.STAND_BY,\n actions: ['addPlaylistItems'],\n },\n [PLAYER_ACTIONS.RESUME_STAND_BY]: [\n {\n target: READY_STATES.PLAYING,\n cond: 'isPlaying',\n actions: ['setResumeStandBy', 'play'],\n },\n { target: READY_STATES.PAUSED, cond: 'isPaused', actions: ['setResumeStandBy', 'pause'] },\n ],\n [PLAYER_ACTIONS.JUMP_TO_TIME_PERCENT]: {\n target: READY_STATES.STAND_BY,\n actions: ['seekNewTime', 'pause'],\n },\n },\n },\n [READY_STATES.SEEKING_NEW_TIME]: {\n invoke: {\n src: changeSourceMachine,\n onDone: { target: READY_STATES.IDLE },\n data: {\n playingMode: (context: PlayerStateMachineContext) => context.playlist.currentSelectedPlayingMode,\n videoRef: (context: PlayerStateMachineContext) => context.videoRef,\n playlistItem: (context: PlayerStateMachineContext) => context.playlist.playingItem.playlistItem,\n videoSourceIndex: (context: PlayerStateMachineContext) => context.playlist.playingItem.videoSourceIndex,\n currentTime: (context: PlayerStateMachineContext) => context.currentTime,\n },\n },\n },\n [PLAYER_STATES.LOADING_PLAYLIST_ITEM]: {\n always: { target: READY_STATES.SEEKING_NEW_TIME, actions: ['pause'] },\n },\n [READY_STATES.PLAYING]: {\n on: {\n [PLAYER_ACTIONS.REPLACE_PLAYLIST_ITEMS]: {\n target: READY_STATES.SEEKING_NEW_TIME,\n actions: ['addPlaylistItems'],\n },\n [PLAYER_ACTIONS.LOAD_PLAYLIST_ITEM]: [\n {\n target: PLAYER_STATES.LOADING_PLAYLIST_ITEM,\n actions: ['setPlaylistItem', 'pause'],\n },\n ],\n [PLAYER_ACTIONS.TOGGLE_PLAYING]: [\n { target: READY_STATES.PAUSED, cond: 'isPlaying', actions: ['setPlayerToPause', 'pause'] },\n { target: READY_STATES.PLAYING, cond: 'isPaused', actions: ['setPlayerToPlay', 'play'] },\n ],\n [PLAYER_ACTIONS.SKIP_FORWARD]: [\n {\n target: READY_STATES.SEEKING_NEW_TIME,\n actions: ['skipForward', 'pause'],\n },\n ],\n [PLAYER_ACTIONS.SKIP_BACKWARD]: [\n {\n target: READY_STATES.SEEKING_NEW_TIME,\n actions: ['skipBackward', 'pause'],\n },\n ],\n [PLAYER_ACTIONS.JUMP_TO_TIME_PERCENT]: {\n target: READY_STATES.SEEKING_NEW_TIME,\n actions: ['seekNewTime', 'pause'],\n },\n [PLAYER_ACTIONS.STAND_BY]: { target: READY_STATES.STAND_BY, actions: ['pause', 'setStandBy'] },\n [PLAYER_ACTIONS.PAUSE]: { target: READY_STATES.PAUSED, actions: ['setPlayerToPause', 'pause'] },\n },\n },\n [READY_STATES.PAUSED]: {\n on: {\n [PLAYER_ACTIONS.REPLACE_PLAYLIST_ITEMS]: {\n target: READY_STATES.SEEKING_NEW_TIME,\n actions: ['addPlaylistItems'],\n },\n [PLAYER_ACTIONS.PAUSE]: { target: READY_STATES.PAUSED, actions: ['setPlayerToPause', 'pause'] },\n [PLAYER_ACTIONS.LOAD_PLAYLIST_ITEM]: [\n {\n target: PLAYER_STATES.LOADING_PLAYLIST_ITEM,\n actions: ['setPlaylistItem', 'pause'],\n },\n ],\n [PLAYER_ACTIONS.TOGGLE_PLAYING]: [\n { target: READY_STATES.PAUSED, cond: 'isPlaying', actions: ['setPlayerToPause', 'pause'] },\n { target: READY_STATES.PLAYING, cond: 'isPaused', actions: ['setPlayerToPlay', 'play'] },\n ],\n [PLAYER_ACTIONS.SKIP_FORWARD]: [\n {\n target: READY_STATES.SEEKING_NEW_TIME,\n actions: ['skipForward', 'pause'],\n },\n ],\n [PLAYER_ACTIONS.SKIP_BACKWARD]: [\n {\n target: READY_STATES.SEEKING_NEW_TIME,\n actions: ['skipBackward', 'pause'],\n },\n ],\n [PLAYER_ACTIONS.JUMP_TO_TIME_PERCENT]: {\n target: READY_STATES.SEEKING_NEW_TIME,\n actions: ['seekNewTime'],\n },\n [PLAYER_ACTIONS.PLAY]: {\n target: READY_STATES.PLAYING,\n actions: ['setPlayerToPlay', 'play'],\n },\n [PLAYER_ACTIONS.STAND_BY]: {\n target: READY_STATES.STAND_BY,\n actions: ['pause', 'setStandBy'],\n },\n },\n },\n },\n onDone: [\n { target: PLAYER_STATES.CHECKING_NEXT_PLAYLIST_ITEM, cond: 'isLastVideoSource' },\n {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n actions: ['nextVideoSource', 'pause'],\n },\n ],\n },\n [PLAYER_STATES.CHECKING_NEXT_PLAYLIST_ITEM]: {\n always: [\n {\n target: PLAYER_STATES.READY,\n cond: 'isLastPlaylistItem',\n actions: ['pause', 'setPlayerToPause'],\n },\n {\n target: PLAYER_STATES.LOADING_VIDEO_SOURCE,\n cond: 'isNotLastPlaylistItem',\n actions: ['pause', 'nextPlaylistItem'],\n },\n ],\n },\n [PLAYER_STATES.JUMP_TO_TIME_PERCENT]: {\n invoke: {\n src: changeSourceMachine,\n onDone: { target: PLAYER_STATES.READY },\n data: {\n videoRef: (context: PlayerStateMachineContext) => context.videoRef,\n playlistItem: (context: PlayerStateMachineContext) => context.playlist.playingItem.playlistItem,\n videoSourceIndex: (context: PlayerStateMachineContext) => context.playlist.playingItem.videoSourceIndex,\n currentTime: (context: PlayerStateMachineContext) => context.currentTime,\n },\n },\n },\n },\n },\n {\n actions: {\n changePlayingMode,\n fixPlaylistItemWithoutNoDuration,\n jumpToMatchTime,\n nextPlaylistItem,\n firstPlaylistItem,\n nextVideoSource,\n pause,\n play,\n previousPlaylistItem,\n previousVideoSource,\n removePlaylistItem,\n removePlaylistItems,\n reorderPlaylistItem,\n changeAutoplayNextPlaylistItem,\n restart,\n seekNewTime,\n setPlayerToPause,\n setPlayerToPlay,\n setPlaylistItem,\n setPlaylistItems,\n updatePlaylistItems,\n addPlaylistItems,\n setResumeStandBy,\n setStandBy,\n setVideoRef,\n skipBackward,\n skipForward,\n toggleFullScreen,\n updatePlaylistItem,\n },\n guards: {\n hasPlaylistItemNoDuration,\n isInStandBy,\n isLastPlaylistItem,\n shouldAutoplayNextPlaylistItem,\n isCurrentPlaylistItem,\n isLastVideoSource,\n isNotLastPlaylistItem,\n isPaused,\n isPlayerNotReady,\n isPlayerReady,\n isPlaying,\n isPlaylistEmpty,\n },\n },\n);\n\nexport const usePlayerStateMachine = (playerId: string, playingMode: PlayingMode) => {\n const [playerStatus, setPlayerState] = usePlayerState(playerId);\n const setIsPlaying = usePlayerSetIsPlaying(playerId);\n const setPlaylist = usePlayerSetPlaylist(playerId);\n const setIsInStandBy = usePlayerUpdateStandBy(playerId);\n const player = useRef(null);\n const playerContainerRef = useRef(null);\n const setCurrentTime = useSetCurrentTime(playerId);\n\n const [current, send] = useMachine(\n playerStateMachine,\n defaultPlayerState(playingMode, playerId, player, 0),\n );\n const actions = useGenerateVideoPlayerActions(send);\n\n useEffect(() => {\n if (!current.context.videoRef?.current?.ready) return;\n\n setCurrentTime(current.context.currentTime);\n }, [\n current.context.videoRef,\n current.context.currentTime,\n current.context.playlist.playingItem.videoSourceIndex,\n current.context.playlist.playingItem.playlistItem,\n setCurrentTime,\n ]);\n\n useEffect(() => {\n if (playerStatus !== current.value) {\n const value = current.value as PLAYER_STATES;\n setPlayerState(value);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [current.value]);\n\n useEffect(() => {\n setIsPlaying(current.context.isPlaying);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [current.context.isPlaying]);\n\n useEffect(() => {\n if (!current.context.videoRef?.current?.ready && current.context.playlist.currentPlaylistItemId === '') return;\n setPlaylist(current.context.playlist);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [current.context.playlist, current.context.videoRef]);\n\n useEffect(() => {\n setIsInStandBy(current.context.isInStandBy);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [current.context.isInStandBy]);\n\n return {\n PLAYER_STATES,\n READY_STATES,\n PLAYER_ACTIONS,\n actions,\n send,\n player,\n playerContainerRef,\n };\n};\n","import { createContext, useEffect, useMemo } from 'react';\n\nimport { PlayerContainerType, PlayingMode } from 'shared/components/video-player/types';\n\nimport {\n useCurrentPlaylistItem,\n useCurrentTime,\n useDuration,\n useIsBuffering,\n useIsCurrentPlaylistItem,\n useIsSeeking,\n usePlayerCurrentSource,\n usePlaylistCurrentPlaylistItemId,\n usePlaylistItems,\n useVideoPlayerActions,\n useVideoPlayerPlayingMode,\n useVideoPlayerPlaylistItem,\n useVideoPlayerRef,\n useVideoPlayerState,\n} from './hooks';\nimport { usePlayerStateMachine } from './state';\nimport { PlayerType, PlaylistItemType } from './types';\n\nexport interface VideoPlayerActions {\n skipBackward5s: () => void;\n changePlayingMode: (playingMode: PlayingMode, tryToKeepCurrentTime?: boolean, autoplay?: boolean) => void;\n changeAutoplayNextPlaylistItem: (autoplayNextPlaylistItem: boolean) => void;\n skipForward5s: () => void;\n handleStandBy: () => void;\n jumpToTimeInMatch: (time: number) => void;\n jumpToTimePercent: (percent: number) => void;\n nextPlaylistItem: () => void;\n autoplayNextPlaylistItem: () => void;\n nextVideoSource: () => void;\n previousVideoSource: () => void;\n restart: () => void;\n refresh: () => void;\n pause: () => void;\n play: () => void;\n replacePlaylistItems: (playlistItems: PlaylistItemType[]) => void;\n setPlaylist: (\n playlist: PlaylistItemType[],\n playingMode: PlayingMode,\n autoplay?: boolean,\n tryToKeepCurrentTime?: boolean,\n ) => void;\n previousPlaylistItem: () => void;\n removePlaylistItem: (id: string) => void;\n removePlaylistItems: (ids: string[]) => void;\n reorder: (currentVideoIndex: number, newVideoIndex: number) => void;\n replay: () => void;\n resumeStandBy: () => void;\n setPlaylistItem: (id: string, autoPlay: boolean) => void;\n updatePlaylistItem: (playlistItem: PlaylistItemType, currenTime?: number) => void;\n updatePlaylistItems: (playlistItems: PlaylistItemType[]) => void;\n loadPlaylist: (playlistItems: PlaylistItemType[], initialStartTime: number) => void;\n}\n\ninterface VideoPlayerFixedState {\n videoPlayerRef: PlayerType;\n videoPlayerContainerRef: PlayerContainerType;\n videoPlayerId: string;\n refreshData?: (() => void) | (() => Promise);\n actions: VideoPlayerActions;\n}\n\ntype VideoPlayerStateProviderProps = {\n children: React.ReactNode;\n refreshData?: (() => void) | (() => Promise);\n playerId: string;\n playingMode: PlayingMode;\n playlistItems: PlaylistItemType[];\n initialStartTime?: number | undefined;\n};\n\nexport const VideoPlayerStateFixedContext = createContext<{ state: VideoPlayerFixedState } | undefined>(undefined);\n\nfunction VideoPlayerStateProvider({\n children,\n refreshData,\n playerId,\n playingMode,\n playlistItems,\n initialStartTime,\n}: VideoPlayerStateProviderProps) {\n const { actions, player, playerContainerRef } = usePlayerStateMachine(playerId, playingMode);\n\n useEffect(() => {\n actions.loadPlaylist(playlistItems, initialStartTime);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [playlistItems]);\n\n return (\n ({\n state: {\n refreshData,\n videoPlayerId: playerId,\n videoPlayerRef: player,\n videoPlayerContainerRef: playerContainerRef,\n actions,\n },\n }),\n [actions, player, playerContainerRef, playerId, refreshData],\n )}\n >\n {children}\n \n );\n}\n\nexport {\n VideoPlayerStateProvider,\n useCurrentTime,\n useDuration,\n useIsBuffering,\n useIsCurrentPlaylistItem,\n useIsSeeking,\n usePlayerCurrentSource,\n useVideoPlayerRef,\n useCurrentPlaylistItem,\n useVideoPlayerActions,\n usePlaylistCurrentPlaylistItemId,\n usePlaylistItems,\n useVideoPlayerPlayingMode,\n useVideoPlayerPlaylistItem,\n useVideoPlayerState,\n};\n","import { useMutation } from '@tanstack/react-query';\nimport { useTranslation } from 'react-i18next';\n\nimport { useBackendApi } from 'api/hooks/useBackendApi';\nimport { transformPlaylist } from 'api/playlist/transformers';\nimport { playlistUrl } from 'api/routes';\nimport { HTTPMethod } from 'api/types';\nimport { NotificationType, useNotifications } from 'shared/hooks/notifications';\n\nimport { PlaylistApiResponse } from '../types';\nimport { usePlaylist } from '../usePlaylist';\n\ntype UpdatePlaylistParams = {\n name: string;\n onSuccess?: (data: unknown) => void;\n};\n\ntype PlaylistUpdate = {\n name: string;\n};\n\nexport const useUpdatePlaylist = (\n playlistId: string,\n onSuccess?: () => void,\n onError?: () => void,\n onSettled?: () => void,\n) => {\n const { t } = useTranslation();\n const { setQueryData } = usePlaylist({ playlistId });\n const onPatchSuccess = (data: PlaylistApiResponse) => transformPlaylist(data);\n const patchUrl = playlistUrl(playlistId);\n const triggerNotification = useNotifications();\n const updatePlaylist = useMutation((params) => useBackendApi(patchUrl, HTTPMethod.PATCH, onPatchSuccess, params), {\n onMutate: async (params: PlaylistUpdate) => {},\n onError: () => {\n if (onError) onError();\n triggerNotification({ type: NotificationType.ERROR, message: t('api:use-update-playlist.error') });\n },\n onSuccess: (updatedPlaylist) => {\n setQueryData(updatedPlaylist);\n if (onSuccess) onSuccess();\n },\n onSettled: () => {\n if (onSettled) onSettled();\n },\n });\n\n const updatePlaylistName = ({ name, onSuccess }: UpdatePlaylistParams) => {\n const updateParams = { name: name };\n updatePlaylist.mutate(updateParams, { onSuccess: onSuccess });\n };\n\n return { updatePlaylistName, isLoading: updatePlaylist.isLoading };\n};\n","import { atomFamily } from 'recoil';\n\nexport const bulkMode = atomFamily({\n key: 'bulk-mode',\n default: false,\n});\n\nexport const bulkSelectedItems = atomFamily({\n key: 'selected-items-bulk-mode',\n default: [],\n});\n","import { useRecoilValue, useSetRecoilState } from 'recoil';\n\nimport { bulkMode } from '../store/UserPlaylist.state';\n\nexport const useIsBulkModeActive = (id: string) => {\n return useRecoilValue(bulkMode(id));\n};\n\nexport const useSetIsBulkModeActive = (id: string) => {\n return useSetRecoilState(bulkMode(id));\n};\n","import { Stack } from '@mui/material';\nimport React from 'react';\n\nexport const PlaylistActions = ({ children }: React.PropsWithChildren) => {\n return (\n \n {children}\n \n );\n};\n","import { Grid, styled } from '@mui/material';\n\nexport const PlaylistContainerGrid = styled(Grid)({\n height: '100%',\n overflow: 'hidden',\n});\n","import { SvgIcon, type SvgIconProps } from '../svg-icon/SvgIcon';\n\nconst IconTime = (props: Omit): JSX.Element => {\n return (\n \n \n \n );\n};\n\nexport default IconTime;\n","import memoize from 'lodash/memoize';\n\nconst formatDuration = (seconds: number, shortForm: boolean) => {\n const mins = ~~(seconds / 60);\n const secs = ~~seconds % 60;\n const ret = [];\n\n ret.push(`${String(mins).padStart(shortForm ? 1 : 2, '0')}:${secs < 10 ? '0' : ''}`);\n ret.push(`${secs}`);\n return ret.join('');\n};\n\nexport const secondsAsDuration = memoize(formatDuration, (seconds: number, shortForm: boolean) =>\n [seconds, shortForm].join('-'),\n);\n","import { Box, styled } from '@mui/material';\nimport { Colors, fontSizes } from 'kognia-ui';\n\nexport const BadgeText = styled(Box)(({ theme }) => ({\n background: Colors.background,\n display: 'flex',\n fontSize: fontSizes.xxSmall,\n fontWeight: theme.typography.fontWeightMedium,\n alignItems: 'center',\n padding: theme.spacing(0, 1),\n borderRadius: '2px',\n height: '24px',\n lineHeight: fontSizes.xxSmall,\n}));\n","import { Stack, Typography } from '@mui/material';\nimport { fontSizes } from 'kognia-ui';\nimport { useTranslation } from 'react-i18next';\n\nimport IconTime from 'shared/components/icons/icon-time';\nimport { Playlist } from 'shared/types';\nimport { secondsAsDuration } from 'shared/utils/seconds-as-duration';\n\nimport { BadgeText } from './BadgetText';\n\ntype Props = {\n playlist: Playlist;\n};\n\nexport const PlaylistDetails = ({ playlist }: Props) => {\n const { t } = useTranslation();\n\n return (\n \n {t('playlist-detail:all-playlist')}\n {t('playlist-detail:clips', { count: playlist.playlistItems.length })}\n \n {secondsAsDuration(playlist.duration, false)}\n \n \n );\n};\n","import { Grid, styled } from '@mui/material';\n\nimport { PLAYLIST_HEADER_HEIGHT } from '../config/Playlist.config';\n\nexport const PlaylistHeaderGrid = styled(Grid)(({ theme }) => ({\n display: 'flex',\n justifyContent: 'space-between',\n padding: theme.spacing(1, 8, 1, 4),\n height: PLAYLIST_HEADER_HEIGHT,\n}));\n","import { Grid, styled } from '@mui/material';\nimport { Colors } from 'kognia-ui';\n\nimport { PLAYLIST_TIMELINE_HEIGHT } from '../config/Playlist.config';\n\nexport const PlaylistTimelineContainer = styled(Grid)(() => ({\n height: PLAYLIST_TIMELINE_HEIGHT,\n position: 'relative',\n display: 'flex',\n flexDirection: 'column',\n // TODO use from theme\n backgroundColor: Colors.background,\n}));\n","import { Stack, styled } from '@mui/material';\nimport { boxShadows } from 'kognia-ui';\n\nimport { PLAYLIST_TIMELINE_HEADER_HEIGHT } from '../config/Playlist.config';\n\nexport const PlaylistTimelineHeader = styled(Stack)(({ theme }) => ({\n alignItems: 'center',\n justifyContent: 'space-between',\n flexDirection: 'row',\n padding: theme.spacing(0, 2),\n boxShadow: boxShadows[2],\n height: PLAYLIST_TIMELINE_HEADER_HEIGHT,\n background: theme.palette.common.white,\n}));\n","import { useMutation, UseMutationResult } from '@tanstack/react-query';\nimport { AxiosError } from 'axios';\nimport { isFunction } from 'lodash';\nimport { ReactNode } from 'react';\n\nimport { BackendApiRequestTypes, useBackendApi } from 'api/hooks/useBackendApi';\nimport { NotificationType, useNotifications } from 'shared/hooks/notifications';\n\nimport { HTTPMethod } from '../../types';\n\nexport interface MutationRequestParams {\n transformer?: (response: TApiData) => void;\n onSuccess?: (response: TData) => void;\n onError?: (data: AxiosError) => void;\n onSettled?: () => void;\n errorMessage?: string | ((data: TApiError | undefined) => string | ReactNode);\n successMessage?: string;\n type?: BackendApiRequestTypes;\n}\n\ntype Params = {\n url: string;\n data?: {\n [key: string]: any;\n };\n};\n\nexport const useMutationRequest = ({\n onSuccess,\n onError,\n onSettled,\n transformer = (response: TApiData) => response,\n errorMessage,\n successMessage,\n type = HTTPMethod.POST,\n}: MutationRequestParams): UseMutationResult => {\n const triggerNotification = useNotifications();\n\n return useMutation(\n ({ url, data = {} }: Params) => {\n return useBackendApi(url, type, transformer, data);\n },\n {\n onMutate: async (params: any) => {\n return params;\n },\n onError: (error: AxiosError) => {\n if (onError) onError(error);\n if (errorMessage) {\n triggerNotification({\n type: NotificationType.ERROR,\n message: isFunction(errorMessage) ? errorMessage(error.response?.data) : errorMessage,\n });\n }\n },\n onSuccess: (response: any) => {\n if (onSuccess) onSuccess(response);\n if (successMessage) {\n triggerNotification({ type: NotificationType.SUCCESS, message: successMessage });\n }\n },\n onSettled: () => {\n if (onSettled) onSettled();\n },\n },\n );\n};\n","import { useCallback } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { useMutationRequest } from '../../hooks/useMutationRequest';\nimport { downloadPlaylistUrl } from '../../routes';\n\nexport const useDownloadPlaylist = () => {\n const { t } = useTranslation();\n const { mutate } = useMutationRequest({\n successMessage: t('api:use-download-playlist.success'),\n errorMessage: t('api:use-download-playlist.error'),\n });\n\n return useCallback(\n ({\n playlistId,\n joinSources,\n showOverlays = true,\n showTitles = true,\n }: {\n playlistId: string;\n joinSources: boolean;\n showOverlays: boolean;\n showTitles: boolean;\n }) => {\n mutate({\n url: downloadPlaylistUrl({ playlistId, joinSources, showOverlays, showTitles }),\n data: { playlistId },\n });\n },\n [mutate],\n );\n};\n","import { useCallback } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { useMutationRequest } from '../../hooks/useMutationRequest';\nimport { downloadClipsUrl } from '../../routes';\n\nexport const useDownloadPlaylistItems = () => {\n const { t } = useTranslation();\n const { mutate } = useMutationRequest({\n successMessage: t('api:use-download-playlist-items.success'),\n errorMessage: t('api:use-download-playlist-items.error'),\n });\n\n return useCallback(\n ({\n playlistId,\n playlistItems,\n joinSources,\n showOverlays = true,\n showTitles = true,\n }: {\n playlistId: string;\n playlistItems: string[];\n joinSources: boolean;\n showOverlays: boolean;\n showTitles: boolean;\n }) => {\n mutate({\n url: downloadClipsUrl({ playlistId, joinSources, showOverlays, showTitles }),\n data: playlistItems,\n });\n },\n [mutate],\n );\n};\n","import { useBackendApi } from 'api/hooks/useBackendApi';\nimport { downloadPlaylistXmlUrl } from 'api/routes';\nimport { HTTPMethod } from 'api/types';\n\nexport const useDownloadPlaylistXml = (playlistId: string, playlistItems: string[]) => {\n const downloadXmlFile = (response: any) => {\n const blob = new Blob([response], {\n type: 'text/xml',\n });\n const blobURL = window.URL.createObjectURL(blob);\n\n const downloadLink = document.createElement('a');\n downloadLink.target = '_blank';\n downloadLink.href = blobURL;\n downloadLink.download = `playlist-${playlistId}.xml`;\n downloadLink.dispatchEvent(\n new MouseEvent('click', {\n bubbles: true,\n cancelable: true,\n view: window,\n }),\n );\n setTimeout(function () {\n window.URL.revokeObjectURL(blobURL);\n }, 200);\n };\n\n return () =>\n useBackendApi(downloadPlaylistXmlUrl(playlistId), HTTPMethod.POST, downloadXmlFile, playlistItems as any);\n};\n","import { SvgIcon, type SvgIconProps } from '../svg-icon/SvgIcon';\n\nexport const IconDownload = (props: Omit): JSX.Element => {\n return (\n \n \n \n );\n};\n","import { Box, styled } from '@mui/material';\n\nexport const DownloadModalWrapper = styled(Box)(({ theme }) => ({\n position: 'absolute',\n top: '50%',\n left: '50%',\n maxWidth: '510px',\n width: '100%',\n padding: theme.spacing(3, 4.5),\n borderRadius: '8px',\n backgroundColor: theme.palette.common.white,\n transform: 'translate(-50%, -50%)',\n}));\n","import {\n Box,\n Button,\n Checkbox,\n FormControl,\n FormControlLabel,\n FormGroup,\n Modal,\n Radio,\n RadioGroup,\n Typography,\n} from '@mui/material';\nimport classNames from 'classnames';\nimport { Colors, fontSizes } from 'kognia-ui';\nimport isEmpty from 'lodash/isEmpty';\nimport React, { SyntheticEvent, useCallback } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { useDownloadPlaylist } from 'api/playlist/useDownloadPlaylist';\nimport { useDownloadPlaylistItems } from 'api/playlist/useDownloadPlaylistItems';\nimport { useDownloadPlaylistXml } from 'api/playlist/useDownloadPlaylistXml';\nimport IconClose from 'shared/components/icons/icon-close';\nimport { IconDownload } from 'shared/components/icons/icon-download';\n\nimport styles from './DownloadModal.module.scss';\nimport { DownloadModalWrapper } from './ui/download-modal-wrapper/DownloadModalWrapper';\n\ninterface Props {\n isPlaylistDownload?: boolean;\n itemsToDownload?: string[];\n onClose: () => void;\n playlistId: string;\n}\n\nconst DOWNLOAD_TYPE = {\n SINGLE_VIDEO_FILE: 'single-video-file',\n MULTIPLE_VIDEO_FILES: 'multiple-video-files',\n};\n\nconst DOWNLOAD_BUTTON_MIN_WIDTH = 136;\n\nexport const DownloadPlaylistItemsModal = ({\n onClose,\n playlistId,\n isPlaylistDownload = true,\n itemsToDownload,\n}: Props): JSX.Element => {\n const { t } = useTranslation();\n const [showOverlays, setShowOverlays] = React.useState(true);\n const [downloadTypeValue, setDownloadTypeValue] = React.useState(DOWNLOAD_TYPE.SINGLE_VIDEO_FILE);\n const [showTitles, setShowTitles] = React.useState(true);\n const [exportXml, setExportXml] = React.useState(false);\n const downloadPlaylist = useDownloadPlaylist();\n const downloadPlaylistItems = useDownloadPlaylistItems();\n const downloadPlaylistXml = useDownloadPlaylistXml(playlistId, itemsToDownload ?? []);\n\n const isSinglePlaylistItemDownload = itemsToDownload?.length === 1;\n\n const handleSubmit = useCallback(() => {\n const joinSources = isSinglePlaylistItemDownload ? false : downloadTypeValue === DOWNLOAD_TYPE.SINGLE_VIDEO_FILE;\n\n if (isPlaylistDownload) {\n downloadPlaylist({\n playlistId,\n joinSources,\n showOverlays,\n showTitles,\n });\n } else if (!isPlaylistDownload && itemsToDownload && !isEmpty(itemsToDownload)) {\n downloadPlaylistItems({\n playlistId,\n playlistItems: itemsToDownload,\n joinSources,\n showOverlays,\n showTitles,\n });\n }\n if (exportXml) downloadPlaylistXml();\n\n onClose();\n }, [\n isSinglePlaylistItemDownload,\n downloadTypeValue,\n showOverlays,\n isPlaylistDownload,\n itemsToDownload,\n onClose,\n downloadPlaylist,\n playlistId,\n showTitles,\n exportXml,\n downloadPlaylistXml,\n downloadPlaylistItems,\n ]);\n\n const handleDownloadTypeChange = useCallback((event: React.ChangeEvent) => {\n setDownloadTypeValue((event.target as HTMLInputElement).value);\n }, []);\n\n const handleShowOverlaysChange = useCallback((event: SyntheticEvent) => {\n const showOverlays = (event.target as HTMLInputElement).checked;\n setShowOverlays(showOverlays);\n }, []);\n\n const handleTitleChange = useCallback((event: SyntheticEvent) => {\n const showTitle = (event.target as HTMLInputElement).checked;\n setShowTitles(showTitle);\n }, []);\n\n const handleExportXml = useCallback((event: SyntheticEvent) => {\n const exportXmlChecked = (event.target as HTMLInputElement).checked;\n setExportXml(exportXmlChecked);\n }, []);\n\n return (\n \n \n \n \n \n \n\n \n {t('playlist-detail:download-modal.title')}\n \n \n\n \n {!isSinglePlaylistItemDownload ? (\n \n \n {t('playlist-detail:download-modal.choose-download-type')}
\n \n }\n label={\n \n {t('playlist-detail:download-modal.single-video-file')}\n
\n }\n />\n }\n label={\n \n {t('playlist-detail:download-modal.multiple-video-files')}\n
\n }\n />\n \n \n \n ) : null}\n \n\n \n {t('playlist-detail:download-modal.choose-preferences')}
\n \n }\n label={\n \n {t('playlist-detail:download-modal.show-tactical-drawings')}\n \n }\n onChange={handleShowOverlaysChange}\n />\n }\n label={\n {t('playlist-detail:download-modal.show-titles')}\n }\n onChange={handleTitleChange}\n />\n }\n label={\n {t('playlist-detail:download-modal.export-xml')}\n }\n onChange={handleExportXml}\n />\n \n \n\n \n \n \n \n\n spacing(1)} right={({ spacing }) => spacing(1)}>\n \n \n \n \n );\n};\n","import React from 'react';\n\nimport { DownloadPlaylistItemsModal } from 'features/playlist/download-playlist-items-modal/DownloadPlaylistItemsModal';\nimport { useCurrentPlaylistItem } from 'shared/components/video-player';\n\ninterface Props {\n playlistId: string;\n isOpen: boolean;\n onClose: () => void;\n}\n\nexport const DownloadCurrentPlaylistItemModal = ({ playlistId, isOpen, onClose }: Props) => {\n const playlistItem = useCurrentPlaylistItem();\n\n return isOpen ? (\n \n ) : null;\n};\n","import { Button } from '@mui/material';\nimport isEmpty from 'lodash/isEmpty';\nimport { useTranslation } from 'react-i18next';\n\nimport { useIsBulkModeActive, useSetIsBulkModeActive } from 'entities/playlist/hooks/useIsBulkModeActive';\nimport { usePlaylistItems, useVideoPlayerActions } from 'shared/components/video-player';\n\ntype Props = {\n playlistId: string;\n};\n\nexport const EnableBulkModeButton = ({ playlistId }: Props) => {\n const { t } = useTranslation();\n const playingItems = usePlaylistItems();\n const actions = useVideoPlayerActions();\n const enabledBulkMode = useIsBulkModeActive(playlistId);\n const setEnabledBulkMode = useSetIsBulkModeActive(playlistId);\n\n const handleSelectClick = () => {\n setEnabledBulkMode(!enabledBulkMode);\n\n actions.pause();\n };\n\n if (isEmpty(playingItems)) return null;\n\n return (\n \n );\n};\n","import throttle from 'lodash/throttle';\nimport { useCallback } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { playlistItemsBaseUrl } from 'api/routes';\nimport { Playlist } from 'shared/types';\n\nimport { useMutationRequest } from '../../hooks/useMutationRequest';\nimport { HTTPMethod } from '../../types';\nimport { transformPlaylist } from '../transformers';\nimport { PlaylistApiResponse } from '../types';\n\nenum BatchUpdatePlaylistItemsActions {\n SELECT_FUNDAMENTALS = 'selectFundamentals',\n TRIM_TIME = 'trimTime',\n}\n\nexport type SelectFundamentalsInItems = {\n action: BatchUpdatePlaylistItemsActions.SELECT_FUNDAMENTALS;\n playlistItemIds: string[];\n fundamentalsSelected: string[];\n};\n\nexport type TrimItems = {\n action: BatchUpdatePlaylistItemsActions.TRIM_TIME;\n playlistItemIds: string[];\n timeToTrim: {\n before: number;\n after: number;\n };\n};\n\nexport type BatchUpdatePlaylistItems = SelectFundamentalsInItems | TrimItems;\n\nexport const createSelectFundamentalsPayload = (\n playlistItemIds: string[],\n fundamentalsSelected: string[],\n): SelectFundamentalsInItems => ({\n action: BatchUpdatePlaylistItemsActions.SELECT_FUNDAMENTALS,\n playlistItemIds,\n fundamentalsSelected,\n});\n\nexport const createTrimItemsPayload = ({\n playlistItemIds,\n before,\n after,\n}: {\n playlistItemIds: string[];\n before?: number;\n after?: number;\n}): TrimItems => ({\n action: BatchUpdatePlaylistItemsActions.TRIM_TIME,\n playlistItemIds,\n timeToTrim: {\n before: before ?? 0,\n after: after ?? 0,\n },\n});\n\nexport const useBatchUpdatePlaylistItems = (playlistId: string, onSuccess?: (playlist: Playlist) => void) => {\n const { t } = useTranslation();\n\n const { mutate, isLoading, isError, isSuccess } = useMutationRequest({\n transformer: transformPlaylist,\n type: HTTPMethod.PATCH,\n errorMessage: t('api:use-update-playlist-item.error'),\n onSuccess: async (playlist: Playlist) => {\n onSuccess && onSuccess(playlist);\n },\n });\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const updateBatchPlaylistItems = useCallback(\n throttle(\n (data: BatchUpdatePlaylistItems, onSuccess?: (playlist: Playlist) => void) => {\n mutate(\n {\n url: playlistItemsBaseUrl(playlistId),\n data,\n },\n { onSuccess },\n );\n },\n 1000,\n { leading: true },\n ),\n [],\n );\n\n return { updateBatchPlaylistItems, isLoading, isError, isSuccess };\n};\n","import { SvgIcon, type SvgIconProps } from '../svg-icon/SvgIcon';\n\nexport const IconOverlays = (props: Omit): JSX.Element => {\n return (\n \n \n \n \n \n \n \n \n \n );\n};\n","import { useCallback } from 'react';\nimport { atomFamily, useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';\n\nconst overlayPanelOpenState = atomFamily({\n key: 'is-overlay-panel-open',\n default: false,\n});\n\nexport const useIsOverlayPanelOpen = (id: string) => {\n return useRecoilValue(overlayPanelOpenState(id));\n};\n\nconst overlayPanelSelectedTab = atomFamily({\n key: 'overlay-selected-tab',\n default: 0,\n});\n\nexport const useOverlaySelectedTab = (id: string) => {\n return useRecoilValue(overlayPanelSelectedTab(id));\n};\n\nexport const useSetOverlaySelectedTab = (id: string) => {\n return useSetRecoilState(overlayPanelSelectedTab(id));\n};\n\nexport const useSetIsOverlayPanelOpen = (id: string, callback?: (isOpen: boolean) => void) => {\n const [isOverlayPanelOpen, setIsOverlayPanelOpen] = useRecoilState(overlayPanelOpenState(id));\n\n return useCallback(\n (open: boolean) => {\n if (isOverlayPanelOpen !== open) {\n callback?.(open);\n setIsOverlayPanelOpen(open);\n }\n },\n [callback, setIsOverlayPanelOpen, isOverlayPanelOpen],\n );\n};\n","import { Checkbox as MuiCheckbox, CheckboxProps, styled } from '@mui/material';\n\ntype CustomColors = 'white' | 'typography' | 'tertiaryLight' | 'quaternaryLight';\n\ninterface Props extends CheckboxProps {\n customColor: CustomColors;\n}\n\nexport const CheckboxWithCustomColor = styled(MuiCheckbox, {\n shouldForwardProp: (prop) => prop !== 'customColor',\n})(({ theme, customColor }) => ({\n ...(customColor === 'white' && {\n color: theme.palette.common.white,\n '&.Mui-checked': {\n color: theme.palette.common.white,\n },\n '&.MuiCheckbox-indeterminate': {\n color: theme.palette.common.white,\n },\n }),\n\n ...(customColor === 'typography' && {\n '&.Mui-checked': {\n color: theme.palette.text.primary,\n },\n '&.MuiCheckbox-indeterminate': {\n color: theme.palette.text.primary,\n },\n }),\n\n ...(customColor === 'tertiaryLight' && {\n color: theme.palette.tertiary.light,\n '&.Mui-checked': {\n color: theme.palette.tertiary.light,\n },\n '&.MuiCheckbox-indeterminate': {\n color: theme.palette.tertiary.light,\n },\n }),\n\n ...(customColor === 'quaternaryLight' && {\n color: theme.palette.quaternary.light,\n '&.Mui-checked': {\n color: theme.palette.quaternary.light,\n },\n '&.MuiCheckbox-indeterminate': {\n color: theme.palette.quaternary.light,\n },\n }),\n}));\n","import { styled, Tabs as MUITabs } from '@mui/material';\nimport { fontSizes } from 'kognia-ui';\n\nexport const Tabs = styled(MUITabs)(({ theme }) => ({\n '& .MuiTab-root': {\n fontWeight: 'normal',\n fontSize: fontSizes.default,\n textTransform: 'none',\n borderBottom: `2px solid ${theme.palette.secondary.main}`,\n color: theme.palette.secondary.main,\n },\n '& .Mui-selected': {\n '&.MuiTab-root': {\n color: theme.palette.text.primary,\n },\n },\n '& .MuiTabs-indicator': {\n height: theme.spacing(0.5),\n backgroundColor: theme.palette.text.primary,\n },\n}));\n","import { Box, styled } from '@mui/material';\n\ninterface Props {\n children?: React.ReactNode;\n dir?: string;\n index: number;\n value: number;\n}\n\nexport const TabPanelContainer = styled(Box)(({ theme }) => ({\n padding: theme.spacing(0, 1, 1),\n}));\n\nexport function TabPanel(props: Props) {\n const { children, value, index, ...other } = props;\n\n return (\n \n {value === index && {children}}\n \n );\n}\n\nexport const TabsPanel = styled(Box)(({ theme }) => ({ overflowY: 'scroll', marginTop: theme.spacing(2) }));\n","import { Box, Typography } from '@mui/material';\nimport { fontSizes } from 'kognia-ui';\nimport { TacticId } from 'overlay-generator';\nimport { useCallback } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { CheckboxWithCustomColor } from '../CheckboxWithCustomColor/CheckboxWithCustomColor';\n\ntype TacticCheckBoxProps = {\n tactic: TacticId;\n selected: boolean;\n onChange: (tactic: TacticId) => void;\n isAvailableInFrame: boolean;\n};\n\nexport const TacticCheckBox = ({ tactic, selected, onChange, isAvailableInFrame }: TacticCheckBoxProps) => {\n const { t } = useTranslation();\n const handleToggleTactic = useCallback(() => {\n onChange(tactic);\n }, [tactic, onChange]);\n\n return (\n \n \n {/* TODO use font styles from theme */}\n \n {t(`fundamentals:fundamentals.${tactic}`)}\n \n \n );\n};\n","import { SvgIcon, type SvgIconProps } from '../svg-icon/SvgIcon';\n\nexport const IconChevronLeft = (props: Omit): JSX.Element => {\n return (\n \n \n \n );\n};\n","import { Box, Stack, styled } from '@mui/material';\nimport { animationDurations, animations, fontSizes } from 'kognia-ui';\n\nimport { IconChevronLeft } from 'shared/components/icons/icon-chevron-left';\n\ntype PanelProps = {\n isOpen: boolean;\n};\n\nexport const Panel = styled(Stack, { shouldForwardProp: (prop) => prop !== 'isOpen' })(\n ({ theme, isOpen }) => ({\n background: isOpen ? theme.palette.common.white : 'transparent',\n display: 'grid',\n gridTemplateRows: 'auto 1fr',\n maxHeight: '100%',\n paddingLeft: theme.spacing(2),\n paddingRight: isOpen ? theme.spacing(2) : 0,\n paddingTop: isOpen ? theme.spacing() : 0,\n transition: 'width 0.2s ease-out',\n }),\n);\n\ntype PanelContentProps = {\n width: number;\n isOpen: boolean;\n};\n\nexport const PanelContent = styled(Box, {\n shouldForwardProp: (prop) => prop !== 'isOpen' && prop !== 'width',\n})(({ width, isOpen, theme }) => ({\n width: `${width - 32}px`,\n display: isOpen ? 'flex' : 'none',\n flexDirection: 'column',\n overflow: 'hidden',\n maxHeight: '100%',\n paddingBottom: theme.spacing(2),\n animation: `${animations.fadeIn} ${animationDurations.fast} ease-out`,\n}));\n\nexport const PanelTitle = styled(Stack)(({ theme }) => ({\n alignItems: 'center',\n fontSize: fontSizes.default,\n gap: theme.spacing(1),\n}));\n\nexport const PanelSectionTitle = styled(Stack)(({ theme }) => ({\n background: theme.palette.secondary.light,\n fontSize: fontSizes.default,\n padding: theme.spacing(1),\n}));\n\ntype IconChevronProps = {\n isOpen: boolean;\n};\n\nexport const IconChevron = styled(IconChevronLeft, {\n shouldForwardProp: (prop) => prop !== 'isOpen',\n})(({ isOpen }) => ({\n transition: 'transform 0.3s',\n transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)',\n}));\n\nexport const ClosedPanelButton = styled(Box)(({ theme }) => ({\n display: 'flex',\n alignItems: 'center',\n cursor: 'pointer',\n border: `1px solid ${theme.palette.secondary.light}`,\n borderRight: 0,\n borderTopLeftRadius: '8px',\n borderBottomLeftRadius: '8px',\n background: theme.palette.common.white,\n padding: theme.spacing(1, 1, 1, 0),\n}));\n","import { Alert, Stack, Tab } from '@mui/material';\nimport { DefensiveTacticId, defensiveTactics, OffensiveTacticId, offensiveTactics, TacticId } from 'overlay-generator';\nimport React, { useCallback, useMemo } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { CheckboxWithCustomColor } from 'shared/components/CheckboxWithCustomColor/CheckboxWithCustomColor';\nimport { Tabs } from 'shared/components/tabs';\nimport { TabPanel, TabsPanel } from 'shared/components/tabs/components/tab-panel';\n\nimport { TacticCheckBox } from '../../tactic-checkbox';\nimport {\n useOverlaySelectedTab,\n useSetOverlaySelectedTab,\n} from '../../video-player/video-player-component/hooks/overlay-panel';\nimport { PanelContent, PanelSectionTitle } from '../styled';\n\ntype Props = {\n id: string;\n width: number;\n isOpen: boolean;\n availableTactics: TacticId[];\n initialSelectedTactics: TacticId[];\n frameTactics: TacticId[];\n onTacticsChange: (tactic: TacticId[]) => void;\n};\n\nexport const OverlaySelectorContent = ({\n id,\n width,\n isOpen,\n availableTactics,\n initialSelectedTactics,\n frameTactics,\n onTacticsChange,\n}: Props) => {\n const { t } = useTranslation();\n const selectedTab = useOverlaySelectedTab(id);\n const setSelectedTab = useSetOverlaySelectedTab(id);\n\n const selectedTactics = useMemo(() => new Set(initialSelectedTactics), [initialSelectedTactics]);\n\n const availableOffensiveTactics = useMemo(\n () =>\n availableTactics.filter((tactic) => offensiveTactics.includes(tactic as OffensiveTacticId), [availableTactics]),\n [availableTactics],\n );\n\n const availableDefensiveTactics = useMemo(\n () =>\n availableTactics.filter((tactic) => defensiveTactics.includes(tactic as DefensiveTacticId), [availableTactics]),\n [availableTactics],\n );\n\n const selectedOffensiveTactics = useMemo(\n () =>\n availableTactics.filter(\n (tactic) => offensiveTactics.includes(tactic as OffensiveTacticId) && selectedTactics.has(tactic),\n [selectedTactics],\n ),\n [availableTactics, selectedTactics],\n );\n\n const selectedDefensiveTactics = useMemo(\n () =>\n availableTactics.filter(\n (tactic) => defensiveTactics.includes(tactic as DefensiveTacticId) && selectedTactics.has(tactic),\n [selectedTactics],\n ),\n [availableTactics, selectedTactics],\n );\n\n const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {\n setSelectedTab(newValue);\n };\n\n const handleTacticToggle = useCallback(\n (tacticId: TacticId) => {\n if (selectedTactics.has(tacticId)) {\n selectedTactics.delete(tacticId);\n } else {\n selectedTactics.add(tacticId);\n }\n onTacticsChange(Array.from(selectedTactics));\n },\n [onTacticsChange, selectedTactics],\n );\n\n const areAllOffensiveTacticsSelected = useMemo(() => {\n return (\n availableOffensiveTactics.filter((tactic) => selectedTactics.has(tactic)).length ===\n availableOffensiveTactics.length\n );\n }, [availableOffensiveTactics, selectedTactics]);\n\n const areAllDefensiveTacticsSelected = useMemo(\n () =>\n availableDefensiveTactics.filter((tactic) => selectedTactics.has(tactic)).length ===\n availableDefensiveTactics.length,\n [availableDefensiveTactics, selectedTactics],\n );\n\n const handleToggleAllOffensiveTactics = useCallback(() => {\n if (areAllOffensiveTacticsSelected) {\n availableOffensiveTactics.forEach((tactic) => {\n selectedTactics.delete(tactic);\n });\n } else {\n availableOffensiveTactics.forEach((tactic) => {\n selectedTactics.add(tactic);\n });\n }\n onTacticsChange(Array.from(selectedTactics));\n }, [availableOffensiveTactics, onTacticsChange, areAllOffensiveTacticsSelected, selectedTactics]);\n\n const handleToggleAllDefensiveTactics = useCallback(() => {\n if (areAllDefensiveTacticsSelected) {\n availableDefensiveTactics.forEach((tactic) => {\n selectedTactics.delete(tactic);\n });\n } else {\n availableDefensiveTactics.forEach((tactic) => {\n selectedTactics.add(tactic);\n });\n }\n onTacticsChange(Array.from(selectedTactics));\n }, [areAllDefensiveTacticsSelected, availableDefensiveTactics, onTacticsChange, selectedTactics]);\n\n if (availableTactics.length === 0) return No tactics available;\n\n return (\n \n \n {t('video-player:overlays.tactics')}\n \n \n \n \n \n \n \n \n \n 0 && !areAllOffensiveTacticsSelected}\n />\n {t('video-player:overlays.all-tactics')} ({selectedOffensiveTactics.length} /{' '}\n {availableOffensiveTactics.length})\n \n \n {availableOffensiveTactics.map((tactic) => (\n \n ))}\n \n \n \n \n \n \n 0 && !areAllDefensiveTacticsSelected}\n />\n {t('video-player:overlays.all-tactics')} ({selectedDefensiveTactics.length} /{' '}\n {availableDefensiveTactics.length})\n \n \n {availableDefensiveTactics.map((tactic) => (\n \n ))}\n \n \n \n \n \n );\n};\n","import { Box, Typography } from '@mui/material';\nimport { fontWeight } from 'kognia-ui';\nimport { TacticId } from 'overlay-generator';\nimport React, { useCallback, useMemo } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { IconOverlays } from 'shared/components/icons/icon-overlays';\nimport Spinner from 'shared/components/spinner';\nimport {\n useIsOverlayPanelOpen,\n useSetIsOverlayPanelOpen,\n} from 'shared/components/video-player/video-player-component/hooks/overlay-panel';\n\nimport { OverlaySelectorContent } from './overlay-selector-content';\nimport { ClosedPanelButton, IconChevron, Panel, PanelTitle } from './styled';\n\ntype Props = {\n id: string;\n availableTactics: TacticId[] | undefined;\n initialSelectedTactics: TacticId[] | undefined;\n frameTactics: TacticId[];\n onTacticsChange: (tactics: TacticId[]) => void;\n onIsOverlayPanelOpenChange?: (isOpen: boolean) => void;\n isLoading: boolean;\n};\n\nexport const MAX_PANEL_WIDTH = 354;\nexport const MIN_PANEL_WIDTH = 74;\n\nexport const OverlaySelectorPanel = ({\n id,\n availableTactics,\n frameTactics,\n initialSelectedTactics,\n onTacticsChange,\n isLoading,\n onIsOverlayPanelOpenChange,\n}: Props) => {\n const { t } = useTranslation();\n const isOverlayPanelOpen = useIsOverlayPanelOpen(id);\n const setIsOverlayPanelOpenState = useSetIsOverlayPanelOpen(id, onIsOverlayPanelOpenChange);\n const handlePanelToggle = useCallback(() => {\n setIsOverlayPanelOpenState(!isOverlayPanelOpen);\n }, [setIsOverlayPanelOpenState, isOverlayPanelOpen]);\n\n const panelWidth = useMemo(() => (isOverlayPanelOpen ? MAX_PANEL_WIDTH : MIN_PANEL_WIDTH), [isOverlayPanelOpen]);\n\n return (\n \n \n {isOverlayPanelOpen ? (\n <>\n \n \n \n \n \n {t('video-player:overlays.overlays-settings')}\n \n >\n ) : (\n \n \n \n \n )}\n \n {isLoading ? (\n \n \n \n ) : null}\n {availableTactics && initialSelectedTactics && isOverlayPanelOpen && !isLoading ? (\n \n ) : null}\n \n );\n};\n","import { useRecoilValue, useSetRecoilState } from 'recoil';\n\nimport { bulkSelectedItems } from 'entities/playlist/store/UserPlaylist.state';\n\nexport const useBulkSelectedItems = (id: string) => {\n return useRecoilValue(bulkSelectedItems(id));\n};\n\nexport const useSetBulkSelectedItems = (id: string) => {\n return useSetRecoilState(bulkSelectedItems(id));\n};\n","import { useMemo } from 'react';\n\nimport { useBulkSelectedItems } from 'entities/playlist/hooks/useBulkSelectedItems';\nimport { useIsBulkModeActive } from 'entities/playlist/hooks/useIsBulkModeActive';\nimport { useCurrentPlaylistItem, usePlaylistItems } from 'shared/components/video-player';\n\nimport { SelectedPlaylistItems } from '../types/PlaylistOverlaySelector.types';\n\nexport const useActivePlaylistItemIds = (playlistId: string): SelectedPlaylistItems => {\n const playlistItems = usePlaylistItems();\n const playlistItem = useCurrentPlaylistItem();\n const isBulkModeActive = useIsBulkModeActive(playlistId);\n const bulkSelectedPlaylistItems = useBulkSelectedItems(playlistId);\n\n return useMemo(() => {\n const playlistItemsIds = isBulkModeActive ? bulkSelectedPlaylistItems : [playlistItem.id];\n\n return playlistItemsIds.reduce(\n (selectedPlaylistItems, id) => {\n const playlistItem = playlistItems.find((playlistItem) => playlistItem.id === id);\n\n if (!playlistItem || !playlistItem.hasHomographies) return selectedPlaylistItems;\n\n return {\n ids: [...selectedPlaylistItems.ids, playlistItem.id],\n playlistItems: [...selectedPlaylistItems.playlistItems, playlistItem],\n };\n },\n { ids: [], playlistItems: [] },\n );\n }, [playlistItems, isBulkModeActive, bulkSelectedPlaylistItems, playlistItem]);\n};\n","import { MutateOptions } from '@tanstack/react-query';\nimport { TacticId } from 'overlay-generator';\nimport { useCallback } from 'react';\n\nimport { useMutationRequest } from 'api/hooks/useMutationRequest';\nimport { tacticsInRange } from 'api/routes';\n\ntype TacticsInRangeApiResponse = { [key in TacticId]: { itemIds: string[]; teamIds: string[] } };\nexport type TacticsInRange = { tactics: TacticId[]; playlistItemsTactics: { [key in string]: TacticId[] } };\n\nconst transformTactics = (response: TacticsInRangeApiResponse): TacticsInRange => {\n const tacticsList = Object.keys(response) as TacticId[];\n\n return {\n tactics: tacticsList,\n playlistItemsTactics: tacticsList.reduce<{ [key in string]: TacticId[] }>((playlistItemsTactics, tacticId) => {\n response[tacticId].itemIds.forEach((playlistItemId: string) => {\n if (!playlistItemsTactics[playlistItemId]) playlistItemsTactics[playlistItemId] = [];\n playlistItemsTactics[playlistItemId].push(tacticId);\n });\n\n return playlistItemsTactics;\n }, {}),\n };\n};\n\nexport const useTacticsInRange = () => {\n const { mutate, ...rest } = useMutationRequest({\n transformer: transformTactics,\n });\n\n const getTacticsInRange = useCallback(\n (\n data: { startTime: number; endTime: number; itemId: string; tacticalAnalysisId: string }[],\n options?: MutateOptions,\n ) => {\n mutate({ url: tacticsInRange, data }, options);\n },\n [mutate],\n );\n\n return { ...rest, getTacticsInRange };\n};\n","import { useEffect, useState } from 'react';\n\nimport { useVideoPlayerPlayingMode } from 'shared/components/video-player';\nimport { getVideoByVideoType } from 'shared/components/video-player/util';\n\nimport { useActivePlaylistItemIds } from './useActivePlaylistItemIds';\nimport { TacticsInRange, useTacticsInRange } from '../api/useTacticsInRange';\n\nexport const useSelectedItemsOverlayTactics = (playlistId: string) => {\n const [overlayTacticsAndSelectedItems, setOverlayTacticsAndSelectedItems] = useState();\n const response = useTacticsInRange();\n const playingMode = useVideoPlayerPlayingMode();\n const playlistItemIds = useActivePlaylistItemIds(playlistId);\n const { getTacticsInRange } = response;\n\n useEffect(() => {\n const data = playlistItemIds.playlistItems\n .filter((item) => item.fundamentalsSelected.tacticalAnalysisId)\n .map((item) => {\n const { videoSources } = getVideoByVideoType(item, playingMode);\n\n return {\n startTime: videoSources[0].startTime,\n endTime: videoSources[0].endTime,\n itemId: item.id,\n tacticalAnalysisId: item.fundamentalsSelected.tacticalAnalysisId ?? '',\n };\n });\n\n getTacticsInRange(data, {\n onSuccess: setOverlayTacticsAndSelectedItems,\n onError: () => setOverlayTacticsAndSelectedItems(undefined),\n });\n }, [playingMode, getTacticsInRange, playlistItemIds]);\n\n return { ...response, data: overlayTacticsAndSelectedItems };\n};\n","import { TacticId } from 'overlay-generator';\nimport React, { useCallback, useMemo } from 'react';\n\nimport { createSelectFundamentalsPayload, useBatchUpdatePlaylistItems } from 'api/playlist/useBatchUpdatePlaylistItems';\nimport { useMapVideos } from 'entities/playlist/hooks/use-map-videos/useMapVideos';\nimport { useIsBulkModeActive } from 'entities/playlist/hooks/useIsBulkModeActive';\nimport { OverlaySelectorPanel } from 'shared/components/overlay-selector-panel';\nimport {\n useCurrentPlaylistItem,\n usePlaylistItems,\n useVideoPlayerActions,\n useVideoPlayerId,\n} from 'shared/components/video-player/hooks';\nimport { useOverlaysFrameInfo } from 'shared/components/video-player/hooks/use-overlays-controller';\nimport { areAllOverlayTacticsSelected } from 'shared/components/video-player/util';\nimport { Playlist, TacticIdOrAll } from 'shared/types/playlist/types';\n\nimport { useActivePlaylistItemIds } from './hooks/useActivePlaylistItemIds';\nimport { useSelectedItemsOverlayTactics } from './hooks/useSelectedItemsOverlayTactics';\n\ntype Props = {\n playlistId: string;\n readOnly?: boolean;\n};\n\nexport const PlaylistOverlaysSelectorPanelFeature = ({ playlistId, readOnly = false }: Props) => {\n const isBulkModeActive = useIsBulkModeActive(playlistId);\n const videoPlayerId = useVideoPlayerId();\n const playlistItems = usePlaylistItems();\n const playlistItem = useCurrentPlaylistItem();\n const actions = useVideoPlayerActions();\n const mapVideos = useMapVideos();\n const playlistItemIds = useActivePlaylistItemIds(playlistId);\n const { data, isLoading } = useSelectedItemsOverlayTactics(playlistId);\n const frameInfo = useOverlaysFrameInfo();\n\n const handleReplacePlaylistItem = useCallback(\n (playlist: Playlist) => {\n actions.updatePlaylistItems(mapVideos(playlist));\n },\n [mapVideos, actions],\n );\n\n const { updateBatchPlaylistItems } = useBatchUpdatePlaylistItems(playlistId);\n\n const handleToggleTactic = useCallback(\n (tactics: TacticId[]) => {\n const selectedTactics = (tactics.length === data?.tactics.length ? ['all'] : tactics) as TacticIdOrAll[];\n updateBatchPlaylistItems(\n createSelectFundamentalsPayload(playlistItemIds.ids, selectedTactics),\n handleReplacePlaylistItem,\n );\n },\n [handleReplacePlaylistItem, updateBatchPlaylistItems, playlistItemIds, data],\n );\n\n const handleToggleTacticReadOnly = useCallback(\n (tactics: TacticId[]) => {\n const selectedTactics = (tactics.length === data?.tactics.length ? ['all'] : tactics) as TacticIdOrAll[];\n\n actions.updatePlaylistItem({\n ...playlistItem,\n fundamentalsSelected: { ...playlistItem.fundamentalsSelected, fundamentalsSelected: selectedTactics },\n });\n },\n [actions, data, playlistItem],\n );\n\n const onIsOverlayPanelOpenChange = useCallback(\n (open: boolean) => {\n actions.changeAutoplayNextPlaylistItem(!open);\n },\n [actions],\n );\n\n const selectedTactics = useMemo(() => {\n const activePlaylistItems = playlistItems.filter((playlistItem) => playlistItemIds.ids.includes(playlistItem.id));\n\n return activePlaylistItems\n .map((item) =>\n areAllOverlayTacticsSelected(item)\n ? data?.playlistItemsTactics[item.id] ?? []\n : (item.fundamentalsSelected.fundamentalsSelected as TacticId[]),\n )\n .flat();\n }, [data, playlistItemIds, playlistItems]);\n\n return (\n \n );\n};\n","import { useTranslation } from 'react-i18next';\n\nimport { queryClient } from 'api/config';\nimport { deletePlaylistItemsUrl } from 'api/routes';\n\nimport { useMutationRequest } from '../../hooks/useMutationRequest';\nimport { HTTPMethod } from '../../types';\nimport { generateFetchPlaylistQueryRef } from '../usePlaylist';\n\nexport const useDeletePlaylistItems = (playlistId: string) => {\n const { t } = useTranslation();\n const fetchQueryRef = generateFetchPlaylistQueryRef(playlistId);\n const { mutate, isLoading, isError } = useMutationRequest({\n type: HTTPMethod.DELETE,\n errorMessage: t('api:use-delete-playlist-items.error'),\n successMessage: t('api:use-delete-playlist-items.success'),\n onSuccess: async () => {\n if (fetchQueryRef) await queryClient.invalidateQueries([fetchQueryRef]);\n },\n });\n\n const deletePlaylistItems = (playlistItemsIds: string[], onSuccess?: (data: unknown) => void) => {\n mutate({ url: deletePlaylistItemsUrl(playlistId), data: { data: playlistItemsIds } }, { onSuccess });\n };\n\n return { deletePlaylistItems, isLoading, isError };\n};\n","import { MutateOptions } from '@tanstack/react-query';\nimport { useCallback } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { duplicatePlaylistItemsUrl } from 'api/routes';\nimport { HTTPMethod } from 'api/types';\nimport { Playlist } from 'shared/types';\n\nimport { useMutationRequest } from '../../hooks/useMutationRequest';\nimport { transformPlaylist } from '../transformers';\nimport { PlaylistApiResponse } from '../types';\n\nexport const useDuplicatePlaylistItems = (playlistId: string) => {\n const { t } = useTranslation();\n\n const { mutate, isLoading, isError, isSuccess } = useMutationRequest({\n type: HTTPMethod.POST,\n transformer: transformPlaylist,\n errorMessage: t('api:use-duplicate-playlist-items.error'),\n successMessage: t('api:use-duplicate-playlist-items.success'),\n });\n\n const duplicatePlaylistItems = useCallback(\n ({\n playlistItemIds,\n options,\n }: {\n playlistItemIds: string[];\n options?: MutateOptions;\n }) => {\n const url = duplicatePlaylistItemsUrl(playlistId);\n\n return mutate({ url, data: playlistItemIds }, options);\n },\n [mutate, playlistId],\n );\n\n return { duplicatePlaylistItems, isLoading, isError, isSuccess };\n};\n","import { Box, styled } from '@mui/material';\nimport { Colors } from 'kognia-ui';\n\nimport { PLAYLIST_TIMELINE_HEADER_HEIGHT } from '../config/Playlist.config';\n\nexport const ItemsListBulk = styled(Box)(({ theme }) => ({\n height: PLAYLIST_TIMELINE_HEADER_HEIGHT,\n backgroundColor: Colors.shark,\n color: theme.palette.common.white,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: theme.spacing(0.5, 1),\n position: 'absolute',\n top: 0,\n width: '100%',\n}));\n","import { Box, styled } from '@mui/material';\n\nexport const ItemsListContainer = styled(Box)(({ theme }) => ({\n flexGrow: 1,\n padding: theme.spacing(2, 0, 1, 2),\n}));\n","export default \"data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='192'%20height='120'%20fill='none'%3e%3cstyle%3e.B{fill-rule:evenodd}.C{fill:%23ceced9}.D{stroke:%23ceced9}.E{fill:%23c4c4c4}%3c/style%3e%3cg%20fill='%23fff'%20class='D'%3e%3crect%20x='56.5'%20y='.5'%20width='135'%20height='95'%20rx='3.5'/%3e%3crect%20x='61.5'%20y='5.5'%20width='125'%20height='85'%20rx='1.5'/%3e%3c/g%3e%3cmask%20id='A'%20maskUnits='userSpaceOnUse'%20x='96'%20y='32'%20width='56'%20height='32'%3e%3cpath%20d='M96%2032h56v32H96z'%20class='E'/%3e%3c/mask%3e%3cg%20mask='url(%23A)'%20class='B%20C'%3e%3cpath%20d='M108.437%2060.5h20.126c4.105-.003%207.433-2.644%207.437-5.903v-.805l.132.106%206.892%205.215c.071.054.147.1.226.138a2.51%202.51%200%20001.083.248v-.001c1.473%200%202.667-1.283%202.667-2.866V39.366c0-.97-.457-1.875-1.214-2.404s-1.711-.61-2.536-.216a1.315%201.315%200%2000-.226.138l-6.892%205.215-.132.106v-.845c-.005-3.235-3.308-5.857-7.385-5.861h-20.178c-4.105.003-7.433%202.644-7.437%205.903v13.194c.004%203.259%203.332%205.899%207.437%205.903z'/%3e%3c/g%3e%3ccircle%20cx='174'%20cy='78'%20r='7'%20class='C'/%3e%3cg%20fill='%23fff'%3e%3cpath%20d='M171.922%2081.818l6-3.818-6-3.818v7.636z'%20class='B'/%3e%3cg%20class='D'%3e%3crect%20x='28.5'%20y='12.5'%20width='135'%20height='95'%20rx='3.5'/%3e%3crect%20x='33.5'%20y='17.5'%20width='125'%20height='85'%20rx='1.5'/%3e%3c/g%3e%3c/g%3e%3cmask%20id='B'%20maskUnits='userSpaceOnUse'%20x='68'%20y='44'%20width='56'%20height='32'%3e%3cpath%20d='M68%2044h56v32H68z'%20class='E'/%3e%3c/mask%3e%3cg%20mask='url(%23B)'%20class='B%20C'%3e%3cpath%20d='M80.438%2072.5h20.126c4.105-.003%207.433-2.644%207.437-5.903v-.805l.132.106%206.892%205.215c.071.054.147.1.226.138a2.51%202.51%200%20001.083.248v-.001c1.473%200%202.667-1.283%202.667-2.866V51.366c0-.97-.457-1.875-1.214-2.404s-1.711-.61-2.536-.216a1.315%201.315%200%2000-.226.138l-6.892%205.215-.132.106v-.845c-.005-3.235-3.308-5.857-7.385-5.861H80.438c-4.106.003-7.433%202.644-7.437%205.903v13.194c.004%203.259%203.332%205.899%207.438%205.903z'/%3e%3c/g%3e%3ccircle%20cx='146'%20cy='90'%20r='7'%20class='C'/%3e%3cg%20fill='%23fff'%3e%3cpath%20d='M143.922%2093.818l6-3.818-6-3.818v7.636z'%20class='B'/%3e%3cg%20class='D'%3e%3crect%20x='.5'%20y='24.5'%20width='135'%20height='95'%20rx='3.5'/%3e%3crect%20x='5.5'%20y='29.5'%20width='125'%20height='85'%20rx='1.5'/%3e%3c/g%3e%3c/g%3e%3cmask%20id='C'%20maskUnits='userSpaceOnUse'%20x='40'%20y='56'%20width='56'%20height='32'%3e%3cpath%20d='M40%2056h56v32H40z'%20class='E'/%3e%3c/mask%3e%3cg%20mask='url(%23C)'%20class='B'%3e%3cpath%20d='M52.438%2084.5h20.125c4.106-.003%207.433-2.644%207.437-5.903v-.805l.132.106%206.892%205.215a1.31%201.31%200%2000.226.138%202.51%202.51%200%20001.084.248v-.001c1.473%200%202.667-1.283%202.667-2.866V63.366c0-.97-.457-1.875-1.214-2.404s-1.711-.61-2.536-.216a1.315%201.315%200%2000-.226.138L80.132%2066.1l-.132.106v-.845c-.005-3.235-3.308-5.857-7.385-5.861H52.438c-4.106.003-7.433%202.644-7.437%205.903v13.194c.004%203.259%203.332%205.899%207.438%205.903z'%20fill='%235440f7'/%3e%3c/g%3e%3ccircle%20cx='118'%20cy='102'%20r='7'%20fill='%23171928'/%3e%3cpath%20d='M115.922%20105.818l6-3.818-6-3.818v7.636z'%20fill='%23fff'%20class='B'/%3e%3c/svg%3e\"","import { styled } from '@mui/material';\n\nexport const PlaylistItemsListEmptyImg = styled('img')({\n width: '72px',\n});\n","import { Stack, Typography } from '@mui/material';\nimport { Colors, fontSizes, fontWeight } from 'kognia-ui';\nimport { useTranslation } from 'react-i18next';\n\nimport NotFoundImg from 'shared/assets/cameras.svg';\n\nimport { PlaylistItemsListEmptyImg } from './PlaylistItemsListEmptyImg';\n\nexport const PlaylistItemsEmpty = () => {\n const { t } = useTranslation();\n\n return (\n \n \n\n \n {t('playlist-detail:not-found.header')}\n \n {t('playlist-detail:not-found.description')}\n \n \n \n );\n};\n","import { ButtonBase, styled, Typography } from '@mui/material';\nimport { fontSizes } from 'kognia-ui';\nimport React, { ComponentType } from 'react';\n\nimport { IconSizes, SvgIconProps } from 'shared/components/icons/svg-icon/SvgIcon';\n\nexport const PlaylistBulkButtonBase = styled(ButtonBase)(({ theme }) => ({\n borderRadius: theme.shape.borderRadius,\n display: 'flex',\n flexDirection: 'column',\n padding: theme.spacing(0.5, 1),\n transition: theme.transitions.create(['color']),\n ':disabled': { opacity: 0.8 },\n '&:hover': {\n color: theme.palette.primary.light,\n\n '& svg': {\n fill: theme.palette.primary.light,\n },\n },\n}));\n\ninterface Props {\n onClick: () => void;\n IconComponent: ComponentType;\n iconColor?: SvgIconProps['color'];\n iconSize?: IconSizes;\n label?: string;\n disabled?: boolean;\n}\n\nexport const PlaylistBulkButton = ({\n onClick,\n IconComponent,\n label,\n iconColor,\n iconSize = 'small',\n disabled = false,\n}: Props) => {\n return (\n \n \n {label ? (\n \n {label}\n \n ) : null}\n \n );\n};\n","import { MutateOptions } from '@tanstack/react-query';\nimport { useCallback } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { useMutationRequest } from 'api/hooks/useMutationRequest';\nimport { postManyPlaylistItem } from 'api/routes';\nimport { HTTPMethod } from 'api/types';\nimport { Playlist } from 'shared/types';\n\nimport { transformPlaylist } from '../transformers';\nimport { PlaylistApiResponse, PostNewPlaylistItem } from '../types';\n\ninterface Params {\n successMessage?: string;\n}\n\nexport const useAddManyToPlaylist = ({ successMessage }: Params = {}) => {\n const { t } = useTranslation();\n\n const { mutate, isLoading, isError, isSuccess } = useMutationRequest({\n type: HTTPMethod.POST,\n transformer: transformPlaylist,\n errorMessage: t('api:use-add-many-to-playlist.error'),\n successMessage: successMessage !== undefined ? successMessage : t('api:use-add-many-to-playlist.success'),\n });\n\n const addManyToPlaylist = useCallback(\n ({ items, options }: { items: PostNewPlaylistItem[]; options?: MutateOptions }) => {\n const { playlistId } = items[0];\n const url = postManyPlaylistItem(playlistId);\n\n return mutate({ url, data: items }, options);\n },\n [mutate],\n );\n\n return { addManyToPlaylist, isLoading, isError, isSuccess };\n};\n","import { useCallback } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nexport const useDates = () => {\n const { i18n } = useTranslation();\n\n const parseBackendDateString = useCallback((dateString: string): Date => {\n return new Date(dateString);\n }, []);\n\n const dateToString = useCallback(\n (date: Date | string): string => {\n const options = { year: 'numeric', month: 'short', day: 'numeric' } as Intl.DateTimeFormatOptions;\n\n if (date instanceof Date) {\n return date.toLocaleDateString(i18n.language, options);\n }\n\n return parseBackendDateString(date).toLocaleDateString(i18n.language, options);\n },\n [i18n.language, parseBackendDateString],\n );\n\n const dateAndTimeToString = useCallback(\n (date: Date | string): string => {\n const options = {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n hour: 'numeric',\n minute: 'numeric',\n } as Intl.DateTimeFormatOptions;\n\n if (date instanceof Date) {\n return date.toLocaleDateString(i18n.language, options);\n }\n\n return parseBackendDateString(date).toLocaleDateString(i18n.language, options);\n },\n [i18n.language, parseBackendDateString],\n );\n\n const dateToTime = useCallback(\n (date: Date | string): string => {\n const options = { hour: 'numeric', minute: 'numeric', second: 'numeric' } as Intl.DateTimeFormatOptions;\n\n if (date instanceof Date) {\n return date.toLocaleTimeString(i18n.language, options);\n }\n\n return parseBackendDateString(date).toLocaleTimeString(i18n.language, options);\n },\n [i18n.language, parseBackendDateString],\n );\n\n const parseDateForApi = useCallback((date: Date): string => {\n return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date\n .getDate()\n .toString()\n .padStart(2, '0')}`;\n }, []);\n\n return { dateToString, dateAndTimeToString, dateToTime, parseBackendDateString, parseDateForApi };\n};\n","export const guid = () => {\n const s4 = () => {\n return Math.floor((1 + Math.random()) * 0x10000)\n .toString(16)\n .substring(1);\n };\n return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();\n};\n","export interface FiltersKeyValuesHash {\n [key: string]: string[];\n}\n\nexport interface FilterOption {\n title: string;\n isApplied?: boolean;\n options?: FilterOptions;\n}\n\nexport interface FilterOptions {\n [key: string]: FilterOption;\n}\n\nexport interface Filter {\n title: string;\n options: FilterOptions;\n}\n\nexport interface FiltersList {\n [key: string]: Filter;\n}\n\nexport enum SortDirection {\n DESC = 'desc',\n ASC = 'asc',\n}\n","import { FiltersList, Playlist, SortDirection } from 'shared/types';\n\nexport enum PlaylistSortOptions {\n UPDATED_AT = 'updatedAt',\n NAME = 'name',\n}\n\nexport enum PlaylistFiltersNames {\n name = 'name',\n competition = 'competition',\n from = 'from',\n to = 'to',\n type = 'type',\n sort = 'sort',\n sortDirection = 'sortDirection',\n}\n\nexport interface PlaylistsFilters {\n [PlaylistFiltersNames.name]: string;\n [PlaylistFiltersNames.competition]: string[];\n [PlaylistFiltersNames.from]: string | null;\n [PlaylistFiltersNames.to]: string | null;\n [PlaylistFiltersNames.type]: string[];\n [PlaylistFiltersNames.sort]: PlaylistSortOptions;\n [PlaylistFiltersNames.sortDirection]: SortDirection;\n}\n\nexport interface PlaylistsWithFiltersPage {\n data: {\n nextCursor: number;\n page: { size: number; totalElements: number; totalPages: number; number: number };\n playlists: Playlist[];\n filters: FiltersList;\n };\n nextCursor: number;\n}\n\nexport interface PlaylistsWithFiltersPageQueryData {\n pages: PlaylistsWithFiltersPage[];\n pageParams: any;\n}\n","import { atomFamily, useRecoilValue, useSetRecoilState } from 'recoil';\n\nimport { SortDirection } from 'shared/types/filters/types';\n\nimport { PlaylistsFilters, PlaylistSortOptions } from './types';\n\nexport const INITIAL_PLAYLISTS_FILTERS: PlaylistsFilters = {\n competition: [],\n from: '',\n name: '',\n to: '',\n type: [],\n sort: PlaylistSortOptions.UPDATED_AT,\n sortDirection: SortDirection.DESC,\n};\n\nconst playlistFilters = atomFamily({\n key: 'playlists-filters',\n default: INITIAL_PLAYLISTS_FILTERS,\n});\n\nexport const usePlaylistsFilters = (id: string) => {\n return useRecoilValue(playlistFilters(id));\n};\n\nexport const useSetPlaylistsFilters = (id: string) => {\n return useSetRecoilState(playlistFilters(id));\n};\n","import { Playlist } from 'shared/types';\nimport { FiltersList } from 'shared/types/filters/types';\n\nimport { PlaylistsWithFiltersPage } from './types';\n\nexport const getTotalElementsFromPage = (pages: PlaylistsWithFiltersPage[] | undefined): number => {\n if (!pages || pages.length === 0) return 0;\n\n const lastPage = pages[pages.length - 1];\n\n return lastPage.data.page.totalElements;\n};\n\nexport const getFilters = (pages: PlaylistsWithFiltersPage[] | undefined): FiltersList => {\n if (!pages || pages.length === 0) return {};\n\n const lastPage = pages[pages.length - 1];\n\n return lastPage.data.filters;\n};\n\nexport const getPlaylistItems = (pages: PlaylistsWithFiltersPage[] | undefined): Playlist[] => {\n return pages\n ? pages.reduce((acc: Playlist[], page: any) => {\n return acc.concat(page.data.playlists);\n }, [])\n : [];\n};\n\nconst AUTOCOMPLETE_PAGE_SIZE_INITIAL = 6;\nconst AUTOCOMPLETE_PAGE_SIZE_SEARCH = 10;\n\nexport const getTimelinePlaylistPageSize = (nameParam: string) => {\n if (nameParam) {\n return AUTOCOMPLETE_PAGE_SIZE_SEARCH;\n }\n\n return AUTOCOMPLETE_PAGE_SIZE_INITIAL;\n};\n","import { useInfiniteQuery } from '@tanstack/react-query';\nimport { useCallback, useEffect, useMemo, useState } from 'react';\n\nimport { queryClient } from 'api/config';\nimport { useBackendApi } from 'api/hooks/useBackendApi';\nimport { playlistsWithFiltersUrl } from 'api/routes';\nimport { fetchQueryResponse, HTTPMethod } from 'api/types';\nimport { useDates } from 'shared/hooks/use-dates';\nimport { FiltersList, Playlist, SortDirection } from 'shared/types';\nimport { guid } from 'shared/utils/guid';\n\nimport { INITIAL_PLAYLISTS_FILTERS, usePlaylistsFilters, useSetPlaylistsFilters } from './atoms';\nimport {\n PlaylistFiltersNames,\n PlaylistsFilters,\n PlaylistSortOptions,\n PlaylistsWithFiltersPage,\n PlaylistsWithFiltersPageQueryData,\n} from './types';\nimport { getFilters, getPlaylistItems, getTimelinePlaylistPageSize, getTotalElementsFromPage } from './util';\nimport { transformPlaylists } from '../transformers';\n\nexport type PlaylistsData = {\n fetchNextPage: () => void;\n filters: FiltersList;\n playlists: Playlist[];\n totalElements: number;\n};\n\nexport interface PlaylistsFilterActions {\n setCompetitions: (competition: string[]) => void;\n setDateRange: (from: Date | null, to: Date | null) => void;\n setName: (name: string) => void;\n setTypes: (name: string[]) => void;\n setSort: (sort: PlaylistSortOptions, sortDirection: SortDirection) => void;\n}\n\ninterface useFetchPlaylistInterface extends fetchQueryResponse {\n fetchNextPage: () => void;\n filterActions: PlaylistsFilterActions;\n setPageSizeParam: (pageSize: number) => void;\n appliedFilters: PlaylistsFilters;\n}\n\nconst PAGE_SIZE = 8;\n\nconst NAME_SEARCH_LENGTH_LIMIT = 2;\nexport const FETCH_PLAYLIST_QUERY_KEY = 'fetchPlaylists';\n\nexport const invalidatePlaylistsQuery = () => queryClient.invalidateQueries([FETCH_PLAYLIST_QUERY_KEY]);\n\ninterface Options {\n enabled?: boolean;\n initialFilters?: PlaylistsFilters;\n isAutocomplete?: boolean;\n refetchInterval?: number | false;\n playlistId?: string;\n}\n\nconst usePlaylists = ({\n initialFilters = INITIAL_PLAYLISTS_FILTERS,\n refetchInterval = 60000,\n enabled = true,\n isAutocomplete = false,\n playlistId = guid(),\n}: Options): useFetchPlaylistInterface => {\n const { parseDateForApi } = useDates();\n const appliedFilters = usePlaylistsFilters(playlistId);\n const setFilters = useSetPlaylistsFilters(playlistId);\n\n useEffect(() => {\n setFilters(initialFilters);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const [pageSizeParam, setPageSizeParam] = useState(\n isAutocomplete ? getTimelinePlaylistPageSize(appliedFilters['name']) : PAGE_SIZE,\n );\n\n const queryRef = useMemo(\n () => [FETCH_PLAYLIST_QUERY_KEY, pageSizeParam, appliedFilters],\n [pageSizeParam, appliedFilters],\n );\n\n const fetchRequest = useInfiniteQuery(\n queryRef,\n (options) => {\n return useBackendApi(\n playlistsWithFiltersUrl({\n size: pageSizeParam,\n sort: `${appliedFilters.sort},${appliedFilters.sortDirection}`,\n page: options.pageParam,\n name: appliedFilters[PlaylistFiltersNames.name],\n competition: appliedFilters[PlaylistFiltersNames.competition],\n from: appliedFilters[PlaylistFiltersNames.from] ?? undefined,\n to: appliedFilters[PlaylistFiltersNames.to] ?? undefined,\n type: appliedFilters[PlaylistFiltersNames.type] ?? undefined,\n }),\n HTTPMethod.GET,\n transformPlaylists,\n );\n },\n {\n getNextPageParam: (lastPage: PlaylistsWithFiltersPage) => {\n return lastPage.nextCursor;\n },\n refetchInterval,\n enabled,\n staleTime: 200,\n },\n );\n\n const setQueryData = (data: any) => queryClient.setQueryData(queryRef, data);\n const invalidateQuery = () => queryClient.invalidateQueries(queryRef);\n\n const setName = useCallback(\n (name: PlaylistsFilters[PlaylistFiltersNames.name]) => {\n name.length >= NAME_SEARCH_LENGTH_LIMIT\n ? setFilters({ ...appliedFilters, name })\n : setFilters({ ...appliedFilters, name: '' });\n },\n [setFilters, appliedFilters],\n );\n\n const setTypes = useCallback(\n (type: PlaylistsFilters[PlaylistFiltersNames.type]) => {\n setFilters({ ...appliedFilters, type });\n },\n [setFilters, appliedFilters],\n );\n\n const setCompetitions = useCallback(\n (competition: PlaylistsFilters[PlaylistFiltersNames.competition]) => {\n setFilters({ ...appliedFilters, competition });\n },\n [setFilters, appliedFilters],\n );\n\n const setDateRange = useCallback(\n (from: Date | null, to: Date | null) => {\n setFilters({ ...appliedFilters, from: from ? parseDateForApi(from) : null, to: to ? parseDateForApi(to) : null });\n },\n [parseDateForApi, setFilters, appliedFilters],\n );\n\n const setSort = useCallback(\n (sort: PlaylistSortOptions, sortDirection: SortDirection) => {\n setFilters({ ...appliedFilters, sort, sortDirection });\n },\n [setFilters, appliedFilters],\n );\n\n const filterActions = useMemo(\n () => ({ setCompetitions, setDateRange, setName, setTypes, setSort }),\n [setCompetitions, setDateRange, setName, setTypes, setSort],\n );\n\n const data = useMemo(\n () => ({\n playlists: getPlaylistItems(fetchRequest?.data?.pages),\n totalElements: getTotalElementsFromPage(fetchRequest?.data?.pages),\n fetchNextPage: fetchRequest?.fetchNextPage,\n filters: getFilters(fetchRequest?.data?.pages),\n }),\n [fetchRequest?.data?.pages, fetchRequest?.fetchNextPage],\n );\n\n return {\n ...fetchRequest,\n data,\n setQueryData,\n appliedFilters,\n filterActions,\n invalidateQuery,\n setPageSizeParam,\n };\n};\n\nconst removePlaylistItemFromList = async (playlistId: string) => {\n queryClient.setQueryData([FETCH_PLAYLIST_QUERY_KEY], (data) => {\n if (!data) return;\n\n return {\n pages: data.pages.map((page: PlaylistsWithFiltersPage) => {\n return {\n ...page,\n data: {\n ...page.data,\n playlists: page.data.playlists.filter((playlist: Playlist) => playlist.id !== playlistId),\n },\n };\n }),\n pageParams: data.pageParams,\n };\n });\n\n await queryClient.invalidateQueries([FETCH_PLAYLIST_QUERY_KEY]);\n};\n\nexport { usePlaylists, removePlaylistItemFromList };\n","import { styled } from '@mui/material';\nimport { Colors } from 'kognia-ui';\nimport { Link } from 'react-router-dom';\n\nexport const NotificationLink = styled(Link)(({ theme }) => ({\n marginLeft: theme.spacing(0.5),\n marginRight: theme.spacing(0.5),\n color: theme.palette.common.white,\n '&::after': {\n backgroundColor: theme.palette.common.white,\n },\n '&:focus': {\n //TODO use theme color\n color: Colors.ghost,\n },\n '&:hover': {\n //TODO use theme color\n color: Colors.ghost,\n },\n}));\n","import { useCallback } from 'react';\nimport { Trans, useTranslation } from 'react-i18next';\nimport { generatePath } from 'react-router-dom';\n\nimport { useMutationRequest } from 'api/hooks/useMutationRequest';\nimport { playlistsUrl } from 'api/routes';\nimport { HTTPMethod } from 'api/types';\nimport { routes } from 'kognia/router/routes';\nimport { NotificationLink } from 'shared/components/NotificationLink';\nimport { NotificationType, useNotifications } from 'shared/hooks/notifications';\nimport { Playlist } from 'shared/types/playlist/types';\n\nimport { transformPlaylist } from '../transformers';\nimport { PlaylistApiResponse, PostNewPlaylist } from '../types';\n\nexport const generateFetchPlaylistsQueryRef = (recordingId: string) => [`fetchPlaylists-recordingId:${recordingId}`];\n\nexport const useCreatePlaylist = () => {\n const triggerNotification = useNotifications();\n const { t } = useTranslation();\n\n const { mutate, isLoading, isError, isSuccess } = useMutationRequest({\n type: HTTPMethod.POST,\n errorMessage: t('api:use-add-playlist.error'),\n transformer: transformPlaylist,\n });\n\n const handleAddToPlaylistSuccess = useCallback(\n (playlist: Playlist) => {\n const translation = (\n \n ),\n }}\n values={{ playlistName: playlist.name }}\n />\n );\n\n triggerNotification({\n type: NotificationType.SUCCESS,\n message: translation,\n });\n },\n [triggerNotification],\n );\n\n const createPlaylist = ({\n data,\n onSuccess = () => {},\n }: {\n data: PostNewPlaylist;\n onSuccess?: (res: Playlist) => void;\n }) => {\n mutate(\n { url: playlistsUrl, data },\n {\n onSuccess: (res: Playlist) => {\n onSuccess && onSuccess(res);\n handleAddToPlaylistSuccess(res);\n },\n },\n );\n };\n\n return { createPlaylist, isLoading, isError, isSuccess };\n};\n","import { SvgIcon, type SvgIconProps } from '../svg-icon/SvgIcon';\n\nconst IconAddFolder = (props: Omit): JSX.Element => {\n return (\n \n \n \n \n \n );\n};\n\nexport default IconAddFolder;\n","import { Button, MenuItem, TextField } from '@mui/material';\nimport { Colors, fontSizes } from 'kognia-ui';\nimport React, { useCallback, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { useCreatePlaylist } from 'api/playlist/useCreatePlaylist';\nimport { invalidatePlaylistsQuery } from 'api/playlist/useFetchPlaylists';\nimport IconAddFolder from 'shared/components/icons/icon-add-folder';\nimport { NotificationType, useNotifications } from 'shared/hooks/notifications';\n\nimport styles from './MenuItemNewPlaylist.module.scss';\n\nconst MenuItemNewPlaylist = () => {\n const { t } = useTranslation();\n const [showForm, setShowForm] = useState(false);\n const [inputValue, setInputValue] = useState('');\n const triggerNotification = useNotifications();\n const { createPlaylist } = useCreatePlaylist();\n\n const handleSubmit = useCallback(\n (event: React.FormEvent) => {\n event.preventDefault();\n if (!inputValue.trim()) {\n return triggerNotification({\n type: NotificationType.ERROR,\n message: t('playlists:forms.add-a-name-for-the-playlist'),\n });\n }\n\n createPlaylist({ data: { name: inputValue as string }, onSuccess: invalidatePlaylistsQuery });\n setInputValue('');\n setShowForm(false);\n },\n [createPlaylist, inputValue, t, triggerNotification],\n );\n\n const handleInputChange = useCallback((event: React.ChangeEvent) => {\n setInputValue(event.target.value);\n }, []);\n\n return (\n <>\n {!showForm && (\n \n )}\n\n {showForm && (\n event.stopPropagation()} className={styles.formContainer}>\n
\n
\n )}\n >\n );\n};\n\nexport default MenuItemNewPlaylist;\n","import makeStyles from '@mui/styles/makeStyles';\nimport { Colors } from 'kognia-ui';\n\nexport const useAutocompleteStyles = makeStyles(() => ({\n root: {\n minHeight: '40px',\n },\n inputRoot: {\n paddingRight: `8px !important`,\n },\n endAdornment: {\n position: 'absolute',\n top: `calc(50% - 16px})`,\n },\n option: {\n padding: '0 !important',\n transition: 'all 0.2s ease-out',\n // Hover\n '&[data-focus=\"true\"]': {\n backgroundColor: Colors.athens,\n },\n // Selected\n '&[aria-selected=\"true\"]': {\n backgroundColor: 'transparent',\n '&:hover': {\n backgroundColor: Colors.athens,\n },\n },\n },\n}));\n","import { SvgIcon, type SvgIconProps } from '../svg-icon/SvgIcon';\n\nconst IconSearch = (props: Omit): JSX.Element => {\n return (\n \n \n \n );\n};\nexport default IconSearch;\n","import { AutocompleteChangeReason, AutocompleteCloseReason } from '@mui/base';\nimport {\n Autocomplete as AutocompleteMUI,\n AutocompleteRenderInputParams,\n PopperProps,\n Stack,\n TextField,\n} from '@mui/material';\nimport { AutocompleteRenderOptionState } from '@mui/material/Autocomplete/Autocomplete';\nimport { AutocompleteGetTagProps, AutocompleteValue } from '@mui/material/useAutocomplete';\nimport debounce from 'lodash/debounce';\nimport React, { KeyboardEventHandler, useCallback, useMemo } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { useAutocompleteStyles } from './styles';\nimport { guid } from '../../utils/guid';\nimport IconClose from '../icons/icon-close';\nimport IconSearch from '../icons/icon-search';\nimport Spinner from '../spinner';\n\nexport type UpdateAutocompleteValue = (item: T) => void;\n\ninterface Props {\n autoFocus?: boolean;\n PaperComponent?: React.JSXElementConstructor>;\n PopperComponent?: React.JSXElementConstructor;\n fetchNextPage: () => void;\n getItemLabel: (item: TItem) => string;\n getOptionDisabled?: (item: TItem) => boolean;\n isOptionEqualToValue?: (option: TItem, value: TItem) => boolean;\n inputWidth?: number | string;\n isLoading: boolean;\n className?: string;\n listWidth?: number;\n inputValue?: string;\n useInputChange?: boolean;\n multiple?: Multiple;\n onChange: (searchTerm: string) => void;\n open?: boolean | undefined;\n options: TItem[];\n onClose?: (event: React.SyntheticEvent, reason: AutocompleteCloseReason) => void;\n onSearchTermChange?: (name: string) => void;\n onFocus?: () => void;\n onBlur?: () => void;\n onSubmit?: () => void;\n onKeyUp?: (key: string) => void;\n placeholder?: string;\n autoHighlight?: boolean;\n renderOption: (\n props: React.HTMLAttributes,\n item: TItem,\n state: AutocompleteRenderOptionState,\n ) => JSX.Element;\n renderTags?: (value: TItem[], getTagProps: AutocompleteGetTagProps) => React.ReactNode;\n resultsHeight?: number | string;\n debounceTime?: number;\n resultsNoMatches?: string | React.ReactNode;\n updateValue: (\n item: AutocompleteValue,\n reason: AutocompleteChangeReason,\n ) => void;\n value?: AutocompleteValue | undefined;\n}\n\nconst specialKeysCodes = [\n 'Alt',\n 'ArrowDown',\n 'ArrowLeft',\n 'ArrowRight',\n 'ArrowUp',\n 'CapsLock',\n 'Control',\n 'End',\n 'Enter',\n 'Escape',\n 'Home',\n 'Insert',\n 'PageDown',\n 'PageUp',\n 'Pause',\n 'PrintScreen',\n 'Shift',\n 'Tab',\n];\n\nconst DEFAULT_RESULTS_HEIGHT = 100;\nconst DEFAULT_INPUT_WIDTH = 400;\nconst DEFAULT_LIST_WIDTH = 400;\nconst CONTAINER_PADDING = 4;\nconst DEBOUNCE_TIME = 300;\n\nexport const Autocomplete = ({\n autoFocus = false,\n getItemLabel,\n getOptionDisabled,\n inputWidth = DEFAULT_INPUT_WIDTH,\n listWidth = DEFAULT_LIST_WIDTH,\n multiple,\n className = '',\n placeholder = '',\n renderOption,\n renderTags,\n resultsHeight = DEFAULT_RESULTS_HEIGHT,\n resultsNoMatches,\n updateValue,\n inputValue,\n open = undefined,\n PopperComponent,\n autoHighlight = true,\n PaperComponent,\n isOptionEqualToValue,\n useInputChange,\n options,\n value,\n onBlur,\n onChange,\n onClose,\n onKeyUp,\n onSubmit,\n onSearchTermChange,\n onFocus,\n isLoading,\n fetchNextPage,\n}: Props) => {\n const [autocompleteKey, setAutocompleteKey] = React.useState(guid());\n const autocompleteClasses = useAutocompleteStyles();\n const { t } = useTranslation();\n\n const loadNextPageOnScrollEnd = useCallback(\n async (event: React.UIEvent | React.KeyboardEvent) => {\n const target = event.currentTarget;\n const isScrollBottom = target.offsetHeight + target.scrollTop + CONTAINER_PADDING >= target.scrollHeight;\n\n if (isScrollBottom) {\n await fetchNextPage();\n }\n },\n [fetchNextPage],\n );\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const handleOnChange = useCallback(\n debounce(async (searchTerm: string) => {\n onChange(searchTerm);\n }, DEBOUNCE_TIME),\n [DEBOUNCE_TIME, onChange],\n );\n\n const handleOnKeyUp: KeyboardEventHandler = useCallback(\n (event) => {\n if (specialKeysCodes.includes(event.key)) return;\n\n onKeyUp && onKeyUp(event.key);\n const target = event.target as HTMLInputElement;\n if (!(target.value.length === 1)) {\n handleOnChange(target.value);\n }\n },\n [onKeyUp, handleOnChange],\n );\n\n const handleOnKeyDown: KeyboardEventHandler = useCallback(\n (event) => {\n if (event.key !== 'Enter' || !onSubmit) return;\n\n event.preventDefault();\n onBlur && onBlur();\n onSubmit();\n },\n [onBlur, onSubmit],\n );\n\n const renderInput = useCallback(\n (params: AutocompleteRenderInputParams) => {\n return (\n ) => {\n onChange && onChange('');\n onSearchTermChange && onSearchTermChange('');\n setAutocompleteKey(guid());\n event.preventDefault();\n event.stopPropagation();\n }}\n isButton\n size='small'\n color='secondary'\n />\n ) : (\n \n ),\n }}\n placeholder={inputValue ? inputValue : placeholder}\n size='small'\n sx={{ paddingTop: 0.25, paddingBottom: 0.25, marginBottom: 0 }}\n variant='outlined'\n onClick={(event) => event.stopPropagation()}\n onMouseEnter={(event) => event.stopPropagation()}\n onMouseDown={(event) => event.stopPropagation()}\n onFocus={(event) => {\n event.stopPropagation();\n onFocus && onFocus();\n }}\n />\n );\n },\n [autoFocus, onChange, inputValue, onSearchTermChange, onFocus, placeholder],\n );\n\n const style = useMemo(() => ({ width: inputWidth }), [inputWidth]);\n\n const listProps = useMemo(\n () => ({\n style: {\n maxHeight: resultsHeight,\n padding: 0,\n maxWidth: listWidth,\n },\n onScroll: loadNextPageOnScrollEnd,\n role: 'list-box',\n }),\n [listWidth, resultsHeight, loadNextPageOnScrollEnd],\n );\n\n const handleAutocompleteOnChange = useCallback(\n (\n event: React.SyntheticEvent,\n newValue: AutocompleteValue,\n reason: AutocompleteChangeReason,\n ) => {\n event.stopPropagation();\n updateValue(newValue, reason);\n },\n [updateValue],\n );\n\n const handleInputChange = useCallback(\n (event: React.SyntheticEvent, newValue: string) => {\n event.stopPropagation();\n onChange(newValue);\n },\n [onChange],\n );\n\n return (\n options}\n noOptionsText={resultsNoMatches !== undefined ? resultsNoMatches : t('common:no-results')}\n onInputChange={useInputChange ? handleInputChange : undefined}\n onChange={handleAutocompleteOnChange}\n className={className}\n autoSelect={false}\n renderTags={renderTags}\n ListboxProps={listProps}\n getOptionLabel={getItemLabel}\n getOptionDisabled={getOptionDisabled}\n loading={isLoading}\n onClose={onClose}\n onFocus={onFocus}\n onBlur={onBlur}\n loadingText={\n \n \n \n }\n renderInput={renderInput}\n renderOption={renderOption}\n style={style}\n open={open}\n PopperComponent={PopperComponent}\n PaperComponent={PaperComponent}\n />\n );\n};\n","import { Card } from '@mui/material';\nimport React from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport styles from './PlaylistResults.module.scss';\n\ninterface ResultsWrapperProps {\n children?: React.ReactNode;\n showSearchResults?: boolean;\n open: boolean;\n title?: string;\n}\n\nexport const SearchPlaylistResults = (props: ResultsWrapperProps) => {\n const { t } = useTranslation();\n\n if (!open) return null;\n\n return (\n \n {props.showSearchResults && (\n
{props.title ? props.title : t('timeline:latest-playlists')}
\n )}\n
{props.children}
\n
\n );\n};\n\nexport const SearchPlaylistResultsPopOver = (props: ResultsWrapperProps) => {\n const { t } = useTranslation();\n\n if (!open) return null;\n\n return (\n \n {props.showSearchResults && (\n {props.title ? props.title : t('timeline:latest-playlists')}
\n )}\n {props.children}
\n \n );\n};\n","import { AutocompleteChangeReason, AutocompleteCloseReason } from '@mui/base';\nimport { Box, ListItem, PopperProps, styled } from '@mui/material';\nimport { Colors, fontSizes } from 'kognia-ui';\nimport debounce from 'lodash/debounce';\nimport React, { useCallback, useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { usePlaylists } from 'api/playlist/useFetchPlaylists';\n\nimport { usePlaylistsFilters } from '../../../api/playlist/useFetchPlaylists/atoms';\nimport { Playlist } from '../../types';\nimport { Autocomplete } from '../autocomplete';\nimport { SearchPlaylistResults, SearchPlaylistResultsPopOver } from '../search-playlist-results';\n\nconst PopperWrapper: React.JSXElementConstructor = ({ children, open }) => {\n if (!open) return null;\n return {children as React.ReactNode}
;\n};\n\nconst SearchItemClipsCount = styled('span')(({ theme }) => ({\n marginLeft: theme.spacing(1),\n marginRight: theme.spacing(0.5),\n fontSize: fontSizes.default,\n color: Colors.storm,\n}));\n\ninterface Props {\n playlistId: string;\n className?: string;\n onUpdateValue?: (playlist: Playlist | null, name: string, reason: AutocompleteChangeReason) => void;\n onClickElement?: (item: Playlist) => void;\n onClose?: (event: React.SyntheticEvent, reason: AutocompleteCloseReason) => void;\n onFocus?: () => void;\n onSearchTermChange?: (name: string) => void;\n onKeyDown?: (key: string) => void;\n onSubmit?: (name?: string) => void;\n onBlur?: (name?: string) => void;\n open?: boolean;\n showResultsInPopOver?: boolean;\n title?: string;\n}\n\nconst DEBOUNCE_TIME = 300;\n\nexport const SearchPlaylistAutocomplete = ({\n playlistId,\n onUpdateValue,\n className,\n onFocus,\n onBlur,\n onSearchTermChange,\n onSubmit,\n onKeyDown,\n onClickElement,\n open = false,\n title,\n showResultsInPopOver = false,\n}: Props) => {\n const filters = usePlaylistsFilters(playlistId);\n const [searchTerm, setSearchTerm] = React.useState(filters.name);\n const { t } = useTranslation();\n const [isActive, setIsActive] = useState(false);\n const { data, isLoading, appliedFilters, filterActions, fetchNextPage } = usePlaylists({\n playlistId,\n refetchInterval: false,\n isAutocomplete: true,\n });\n\n useEffect(() => {\n setSearchTerm(filters.name);\n }, [filters.name]);\n\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const handleTermChange = useCallback(\n debounce((name: string) => {\n setSearchTerm(name);\n filterActions.setName(name);\n }, DEBOUNCE_TIME),\n [filterActions.setName],\n );\n\n const handleOnFocus = useCallback(() => {\n onFocus && onFocus();\n setIsActive(true);\n }, [onFocus]);\n\n const handleOnBlur = useCallback(() => {\n onBlur && onBlur(appliedFilters.name);\n setIsActive(false);\n }, [appliedFilters.name, onBlur]);\n\n const handleOnClose = useCallback(() => {\n setIsActive(false);\n }, []);\n\n const handleOnSubmit = useCallback(() => {\n onSubmit && onSubmit(appliedFilters.name);\n setIsActive(false);\n }, [appliedFilters.name, onSubmit]);\n\n const handleOnKeyUp = useCallback(\n (key: string) => {\n onKeyDown && onKeyDown(key);\n setIsActive(true);\n },\n [onKeyDown],\n );\n\n const isOpen = open || isActive;\n\n const PlaylistResultsComponent = useCallback(\n (props: any) => (\n \n ),\n [appliedFilters.name, isOpen, title],\n );\n\n const PlaylistResultsPopOverComponent = useCallback(\n (props: any) => (\n \n ),\n [appliedFilters.name, isOpen, title],\n );\n\n const renderOption = useCallback((props: React.ComponentProps, item: Playlist) => {\n return (\n \n \n {item.name} {item.playlistItems.length}\n \n \n );\n }, []);\n\n const handleUpdateValue = useCallback(\n (playlist: Playlist | Playlist[] | null, reason: AutocompleteChangeReason) => {\n if (Array.isArray(playlist)) return;\n\n if (reason === 'selectOption') {\n onClickElement && playlist && onClickElement(playlist);\n }\n\n onUpdateValue && onUpdateValue(playlist, appliedFilters.name, reason);\n },\n [appliedFilters.name, onClickElement, onUpdateValue],\n );\n\n return (\n item.name}\n inputValue={searchTerm}\n inputWidth='100%'\n isLoading={isLoading}\n onBlur={handleOnBlur}\n onChange={handleTermChange}\n onClose={handleOnClose}\n onFocus={handleOnFocus}\n onKeyUp={handleOnKeyUp}\n onSearchTermChange={onSearchTermChange}\n onSubmit={handleOnSubmit}\n open={open}\n options={data.playlists}\n placeholder={t('common:actions.search')}\n renderOption={renderOption}\n resultsHeight={288}\n updateValue={handleUpdateValue}\n useInputChange\n />\n );\n};\n","import { AutocompleteChangeReason } from '@mui/base';\nimport { Box, Popover, styled } from '@mui/material';\nimport { Colors } from 'kognia-ui';\nimport React from 'react';\n\nimport MenuItemNewPlaylist from './menu-item-new-playlist/MenuItemNewPlaylist';\nimport { Playlist } from '../../types';\nimport { SearchPlaylistAutocomplete } from '../search-playlist-autocomplete';\n\nexport enum PlaylistMenuVerticalPosition {\n Top = 'top',\n Bottom = 'bottom',\n Center = 'center',\n}\n\nexport enum PlaylistMenuHorizontalPosition {\n Left = 'left',\n Right = 'right',\n Center = 'center',\n}\n\ninterface Props {\n anchorEl: HTMLElement | null;\n onClose?: (event: React.MouseEvent) => void;\n onPopoverClose?: (event: React.MouseEvent) => void;\n onClickItem?: (playlist: Playlist | null, name: string, reason: AutocompleteChangeReason) => void;\n verticalPosition?: PlaylistMenuVerticalPosition;\n horizontalPosition?: PlaylistMenuHorizontalPosition;\n}\n\nconst AutoCompleteWrapper = styled(Box)(({ theme }) => ({\n backgroundColor: Colors.white,\n padding: theme.spacing(3),\n width: '385px',\n}));\n\nconst PLAYLIST_ID = 'search-playlist-dialog';\n\nexport const SelectPlaylistDialog = ({\n anchorEl,\n onClose = () => {},\n onPopoverClose,\n onClickItem = () => {},\n verticalPosition = PlaylistMenuVerticalPosition.Bottom,\n horizontalPosition = PlaylistMenuHorizontalPosition.Left,\n}: Props) => {\n const openPopover = Boolean(anchorEl);\n\n return (\n \n \n \n \n \n \n );\n};\n","import React, { useCallback } from 'react';\nimport { Trans } from 'react-i18next';\nimport { generatePath } from 'react-router-dom';\n\nimport { useAddManyToPlaylist } from 'api/playlist/useAddManyToPlaylist';\nimport { usePlaylists } from 'api/playlist/useFetchPlaylists';\nimport { routes } from 'kognia/router/routes';\nimport { NotificationLink } from 'shared/components/NotificationLink';\nimport {\n PlaylistMenuHorizontalPosition,\n PlaylistMenuVerticalPosition,\n SelectPlaylistDialog,\n} from 'shared/components/select-playlist-dialog/SelectPlaylistDialog';\nimport { NotificationType, useNotifications } from 'shared/hooks/notifications';\nimport { FundamentalsSelection, Playlist } from 'shared/types/index';\n\nimport { PostNewPlaylistItem } from '../../api/playlist/types';\n\nexport interface CopyToPlaylistItemDetails {\n endTime: number;\n name: string;\n startTime: number;\n recordingId: string;\n fundamentalsSelected: FundamentalsSelection;\n}\n\ninterface Props {\n anchorEl: HTMLElement | null;\n onClose: () => void;\n onSettled?: () => void;\n playlistItems: CopyToPlaylistItemDetails[];\n}\n\nexport const CopyToDialog = ({ anchorEl, playlistItems, onClose, onSettled }: Props) => {\n const { addManyToPlaylist } = useAddManyToPlaylist({ successMessage: '' });\n const { invalidateQuery } = usePlaylists({ refetchInterval: false, enabled: false });\n const triggerNotification = useNotifications();\n\n const handleAddToPlaylistSuccess = useCallback(\n (playlist: Playlist) => {\n const translation = (\n \n ),\n }}\n values={{ playlistName: playlist.name }}\n />\n );\n\n triggerNotification({\n type: NotificationType.SUCCESS,\n message: translation,\n });\n },\n [triggerNotification],\n );\n\n const handleAddToPlaylistSettle = useCallback(() => {\n invalidateQuery && invalidateQuery();\n onSettled && onSettled();\n onClose();\n }, [invalidateQuery, onClose, onSettled]);\n\n const handleCopyToPlaylist = useCallback(\n (playlist: Playlist | null) => {\n if (playlist === null) return;\n\n const data: PostNewPlaylistItem[] = playlistItems.map((item) => {\n return {\n startTime: item.startTime,\n endTime: item.endTime,\n playlistId: playlist.id,\n name: item.name,\n recordingId: item.recordingId ?? '',\n fundamentalsSelected: item.fundamentalsSelected,\n };\n });\n\n addManyToPlaylist({\n items: data,\n options: {\n onSuccess: handleAddToPlaylistSuccess,\n onSettled: handleAddToPlaylistSettle,\n },\n });\n },\n [addManyToPlaylist, handleAddToPlaylistSettle, handleAddToPlaylistSuccess, playlistItems],\n );\n\n return (\n \n );\n};\n","import { Playlist } from 'shared/types/index';\n\nimport { CopyToDialog } from './CopyToDialog';\n\ntype Props = {\n anchor: HTMLElement | null;\n onClose: () => void;\n playlist: Playlist;\n selectedItems: string[];\n};\n\nexport const SelectedItemsCopyToDialog = ({ playlist, selectedItems, onClose, anchor }: Props) => {\n return (\n {\n const playlistItem = playlist.playlistItems.find((item) => item.id === id);\n\n return {\n startTime: playlistItem?.startTime ?? 0,\n endTime: playlistItem?.endTime ?? 0,\n name: playlistItem?.name ?? '',\n recordingId: playlistItem?.recordingId ?? '',\n fundamentalsSelected: playlistItem?.fundamentalsSelected ?? {\n tacticalAnalysisId: undefined,\n fundamentalsSelected: [],\n },\n };\n })}\n onClose={onClose}\n anchorEl={anchor}\n />\n );\n};\n","import { useCallback } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport { updatePlaylistItemUrl } from 'api/routes';\nimport { useVideoPlayerId } from 'shared/components/video-player/hooks';\nimport { usePlayerSetPlaylist } from 'shared/components/video-player/state/atoms/hooks';\nimport { PlaylistItemType } from 'shared/components/video-player/types';\n\nimport { useMutationRequest } from '../../hooks/useMutationRequest';\nimport { HTTPMethod } from '../../types';\nimport { transformPlaylist } from '../transformers';\n\nconst moveTo = function (arrayToReorder: T[], from: number, to: number) {\n const reorderedArray = [...arrayToReorder];\n reorderedArray.splice(to, 0, reorderedArray.splice(from, 1)[0]);\n\n return reorderedArray;\n};\n\nexport const useUpdatePlaylistItemOrder = (playlistId: string) => {\n const { t } = useTranslation();\n const playerId = useVideoPlayerId();\n const setPlaylist = usePlayerSetPlaylist(playerId);\n const { mutate, isLoading, isError, isSuccess } = useMutationRequest({\n type: HTTPMethod.PATCH,\n transformer: transformPlaylist,\n errorMessage: t('api:use-update-playlist-item-order.error'),\n });\n\n const updatePlaylistItemOrder = useCallback(\n (playlistItemId: string, newIndex: number) => {\n //Optimistic update of the reordered playlist\n setPlaylist((playlist) => {\n const playlistItemIndex = playlist.playlistItems.findIndex(\n (playlistItem) => playlistItem.id === playlistItemId,\n );\n const playlistItems = [...playlist.playlistItems];\n\n const reorderedItems = moveTo(playlistItems, playlistItemIndex, newIndex);\n return { ...playlist, playlistItems: reorderedItems };\n });\n mutate({ url: updatePlaylistItemUrl(playlistId, playlistItemId), data: { index: newIndex } });\n },\n [setPlaylist, mutate, playlistId],\n );\n\n return { updatePlaylistItemOrder, isLoading, isError, isSuccess };\n};\n","import { useCallback } from 'react';\n\nimport { useBulkSelectedItems, useSetBulkSelectedItems } from 'entities/playlist/hooks/useBulkSelectedItems';\n\nexport const useHandleSelect = (playlistId: string) => {\n const selectedItems = useBulkSelectedItems(playlistId);\n const setSelectedItems = useSetBulkSelectedItems(playlistId);\n\n return useCallback(\n (value: string) => {\n if (!selectedItems.includes(value)) {\n return setSelectedItems([...selectedItems, value]);\n }\n\n const filteredItems = selectedItems.filter((item) => item !== value);\n\n return setSelectedItems(filteredItems);\n },\n [setSelectedItems, selectedItems],\n );\n};\n","import { CSSProperties, forwardRef } from 'react';\nimport AutoSizer from 'react-virtualized-auto-sizer';\nimport { FixedSizeList as List } from 'react-window';\n\nimport { PlaylistItemType } from 'shared/components/video-player/types';\n\nimport { PLAYLIST_ITEM_FULL_WIDTH, PLAYLIST_ITEM_HEIGHT } from '../../entities/playlist/config/Playlist.config';\n\nexport type RowProps = {\n data: PlaylistItemType[];\n index: number;\n style: CSSProperties;\n};\n\ntype Props = {\n playlistItems: PlaylistItemType[];\n Row: React.FC;\n};\n\nexport const PlaylistItemsList = forwardRef(({ playlistItems, Row }: Props, ref: any) => {\n return (\n \n {({ height, width }) => (\n data[index].id}\n >\n {Row}\n
\n )}\n \n );\n});\n\nPlaylistItemsList.displayName = 'PlaylistItemsList';\n","import { useTranslation } from 'react-i18next';\n\nimport { useMutationRequest } from 'api/hooks/useMutationRequest';\nimport { invalidatePlaylistQuery } from 'api/playlist/usePlaylist';\nimport { deletePlaylistItemUrl } from 'api/routes';\nimport { HTTPMethod } from 'api/types';\n\nimport { invalidatePlaylistsQuery } from '../useFetchPlaylists';\n\nexport const useDeletePlaylistItem = (playlistId: string) => {\n const { t } = useTranslation();\n const { mutate, isLoading, isError } = useMutationRequest({\n type: HTTPMethod.DELETE,\n errorMessage: t('api:use-delete-playlist-item.error'),\n successMessage: t('api:use-delete-playlist-item.success'),\n });\n\n const deletePlaylistItem = (playlistItemId: string, onSuccess?: () => void) => {\n mutate(\n { url: deletePlaylistItemUrl(playlistId, playlistItemId) },\n {\n onSuccess: () => {\n invalidatePlaylistQuery();\n invalidatePlaylistsQuery();\n if (onSuccess) onSuccess();\n },\n },\n );\n };\n\n return { deletePlaylistItem, isLoading, isError };\n};\n","import { useTranslation } from 'react-i18next';\n\nimport { updatePlaylistItemUrl } from 'api/routes';\nimport { Playlist } from 'shared/types';\n\nimport { useMutationRequest } from '../../hooks/useMutationRequest';\nimport { HTTPMethod } from '../../types';\nimport { transformPlaylist } from '../transformers';\nimport { PlaylistApiResponse, UpdateParams } from '../types';\n\nexport const useUpdatePlaylistItem = (playlistId: string, onSuccess?: (playlist: Playlist) => void) => {\n const { t } = useTranslation();\n const { mutate, isLoading, isError, isSuccess } = useMutationRequest({\n transformer: transformPlaylist,\n type: HTTPMethod.PATCH,\n errorMessage: t('api:use-update-playlist-item.error'),\n onSuccess: async (playlist: Playlist) => {\n onSuccess && onSuccess(playlist);\n },\n });\n\n const updatePlaylistItem = (\n playlistItemId: string,\n values: UpdateParams,\n onSuccess?: (playlist: Playlist) => void,\n ) => {\n mutate({ url: updatePlaylistItemUrl(playlistId, playlistItemId), data: values }, { onSuccess });\n };\n\n return { updatePlaylistItem, isLoading, isError, isSuccess };\n};\n","import { useCallback } from 'react';\n\nimport { useVideoPlayerActions, useVideoPlayerId } from 'shared/components/video-player/hooks';\nimport { useSetCurrentTime } from 'shared/components/video-player/state/atoms/hooks';\nimport { PlaylistItemType } from 'shared/components/video-player/types';\n\nexport const useHandleSetPlaylistItem = (playlistItem: PlaylistItemType) => {\n const playerId = useVideoPlayerId();\n const actions = useVideoPlayerActions();\n const setCurrentTime = useSetCurrentTime(playerId);\n\n return useCallback(\n (autoPlay = true) => {\n setCurrentTime(0);\n actions.setPlaylistItem(playlistItem.id, autoPlay);\n },\n [setCurrentTime, actions, playlistItem.id],\n );\n};\n","import { useCallback } from 'react';\n\nimport { useIsBulkModeActive } from 'entities/playlist/hooks/useIsBulkModeActive';\nimport { PlaylistItemType } from 'shared/components/video-player/types';\n\nimport { useHandleSelect } from './useHandleSelect';\nimport { useHandleSetPlaylistItem } from './useHandleSetPlaylistItem';\n\nexport const useHandlePlaylistItemClick = (playlistItem: PlaylistItemType, playlistId: string) => {\n const handleSelectItem = useHandleSelect(playlistId);\n const enabledBulkMode = useIsBulkModeActive(playlistId);\n const handleSetPlaylistItem = useHandleSetPlaylistItem(playlistItem);\n\n return useCallback(\n (event?: React.MouseEvent) => {\n event && event.stopPropagation();\n if (enabledBulkMode) {\n return handleSelectItem(playlistItem.id);\n }\n\n return handleSetPlaylistItem(true);\n },\n [enabledBulkMode, handleSetPlaylistItem, handleSelectItem, playlistItem.id],\n );\n};\n","import { Stack, styled } from '@mui/material';\nimport { boxShadows, Colors } from 'kognia-ui';\n\nimport { PLAYLIST_ITEM_HEIGHT } from '../config/Playlist.config';\n\ninterface PlaylistItemWrapperProps {\n isCurrent: boolean;\n isDeleting?: boolean;\n isEditing: boolean;\n isDisabled: boolean;\n isDraggable: boolean;\n}\n\nexport const PlaylistItemWrapper = styled(Stack, {\n shouldForwardProp: (prop) =>\n prop !== 'isCurrent' &&\n prop !== 'isEditing' &&\n prop !== 'isDraggable' &&\n prop !== 'isDisabled' &&\n prop !== 'isDeleting',\n})(({ theme, isCurrent, isDeleting = false, isEditing, isDisabled, isDraggable }) => ({\n borderRadius: theme.shape.borderRadius,\n border: `1px solid transparent`,\n backgroundColor: theme.palette.common.white,\n alignItems: 'center',\n height: PLAYLIST_ITEM_HEIGHT,\n position: 'relative',\n overflow: 'hidden',\n cursor: isDraggable ? 'pointer' : 'default',\n transition: theme.transitions.easing.easeOut,\n\n '&:hover': {\n borderColor: theme.palette.primary.main,\n },\n\n ...(isCurrent && {\n borderColor: 'transparent',\n backgroundColor: theme.palette.primary.light,\n boxShadow: boxShadows[2],\n\n '[data-element-name=\"name-text\"], [data-element-name=\"duration\"]': {\n color: theme.palette.common.white,\n },\n\n '[data-element-name=\"counter\"]': {\n backgroundColor: theme.palette.common.white,\n color: theme.palette.primary.main,\n },\n\n svg: {\n fill: theme.palette.common.white,\n },\n }),\n\n ...(isDisabled && {\n backdropFilter: 'grayscale(100%)',\n pointerEvents: 'none',\n borderColor: theme.palette.secondary.main,\n // TODO use from theme\n backgroundColor: Colors.background,\n opacity: 0.5,\n\n '[data-element-name=\"name-text\"], [data-element-name=\"duration\"]': {\n color: theme.palette.secondary.main,\n },\n\n '[data-element-name=\"counter\"]': {\n backgroundColor: theme.palette.secondary.main,\n color: theme.palette.common.white,\n },\n\n svg: {\n fill: theme.palette.secondary.main,\n },\n }),\n\n ...(isEditing && {\n pointerEvents: 'none',\n borderColor: theme.palette.warning.main,\n backgroundColor: theme.palette.warning.light,\n opacity: 0.5,\n\n '[data-element-name=\"name-text\"], [data-element-name=\"duration\"]': {\n color: theme.palette.warning.main,\n },\n\n '[data-element-name=\"counter\"]': {\n backgroundColor: theme.palette.warning.main,\n color: theme.palette.common.white,\n },\n\n svg: {\n fill: theme.palette.warning.main,\n },\n }),\n\n ...(isDeleting && {\n boxShadows: boxShadows[1],\n borderColor: theme.palette.error.main,\n opacity: 0.3,\n backgroundColor: theme.palette.error.main,\n\n '[data-element-name=\"name-text\"], [data-element-name=\"duration\"]': {\n color: theme.palette.common.white,\n },\n\n '[data-element-name=\"counter\"]': {\n backgroundColor: theme.palette.common.white,\n color: theme.palette.error.main,\n },\n\n svg: {\n fill: theme.palette.error.main,\n },\n }),\n}));\n","import { isFullMatchVideo } from 'shared/components/video-player/is-full-match-video';\nimport { PlaylistItemType } from 'shared/components/video-player/types';\n\nexport const getPlaylistItemFirstVideoSource = (playlistItem: PlaylistItemType) => {\n return playlistItem.videoTypes.find((videoType) => isFullMatchVideo(videoType.playingMode.mode))?.videoSources[0];\n};\n","import { PlaylistItemType } from 'shared/components/video-player/types';\n\nimport { getPlaylistItemFirstVideoSource } from './getPlaylistItemFirstVideoSource';\n\nexport const generateCopyToPlaylistItem = (playlistItem: PlaylistItemType) => {\n const playlistItemVideoSource = getPlaylistItemFirstVideoSource(playlistItem);\n\n return {\n startTime: playlistItemVideoSource?.startTime ?? 0,\n endTime: playlistItemVideoSource?.endTime ?? 0,\n name: playlistItem.name ?? '',\n recordingId: playlistItem.recordingId ?? '',\n fundamentalsSelected: playlistItem.fundamentalsSelected,\n };\n};\n","import { Box, styled } from '@mui/material';\n\nexport const ContentBottomRow = styled(Box)(({ theme }) => ({\n paddingLeft: theme.spacing(1),\n paddingTop: theme.spacing(0.5),\n}));\n","import { Stack, styled } from '@mui/material';\n\nexport const ContentTopRow = styled(Stack)(({ theme }) => ({\n paddingLeft: theme.spacing(0.5),\n paddingTop: theme.spacing(0.5),\n alignItems: 'center',\n display: 'flex',\n flexDirection: 'row',\n}));\n","import { Box, styled } from '@mui/material';\nimport { Colors, fontSizes } from 'kognia-ui';\n\nexport const CounterBadge = styled(Box)(({ theme }) => ({\n display: 'flex',\n fontWeight: theme.typography.fontWeightMedium,\n fontSize: fontSizes.xxSmall,\n // TODO use from theme\n backgroundColor: Colors.melrose,\n color: theme.palette.primary.main,\n padding: theme.spacing(0, 0.5),\n borderRadius: '2px',\n alignItems: 'center',\n height: '18px',\n lineHeight: 1,\n}));\n","import { Box, styled } from '@mui/material';\n\nexport const ItemContentWrapper = styled(Box)({\n position: 'relative',\n display: 'flex',\n alignItems: 'center',\n height: '100%',\n flexGrow: 1,\n justifyContent: 'space-between',\n maxWidth: '100%',\n});\n","import { SvgIcon, type SvgIconProps } from '../svg-icon/SvgIcon';\n\nconst IconPlay = (props: Omit): JSX.Element => {\n return (\n \n \n \n );\n};\n\nexport default IconPlay;\n","import { SvgIcon, type SvgIconProps } from '../svg-icon/SvgIcon';\n\nconst IconVerticalMenu = (props: Omit): JSX.Element => {\n return (\n \n \n \n );\n};\n\nexport default IconVerticalMenu;\n","import { Box, Button, Divider, Stack } from '@mui/material';\nimport Fade from '@mui/material/Fade';\nimport Menu from '@mui/material/Menu';\nimport MenuItem from '@mui/material/MenuItem';\nimport { PopoverProps } from '@mui/material/Popover';\nimport { PopoverOrigin } from '@mui/material/Popover/Popover';\nimport React, { useCallback } from 'react';\n\nimport IconClose from '../icons/icon-close';\nimport { MenuListOption } from '../kebab-menu';\n\ninterface Props {\n isOpen: boolean;\n id?: string;\n onClose: (event: React.SyntheticEvent | React.MouseEvent | MouseEvent | TouchEvent) => void;\n anchorEl: PopoverProps['anchorEl'];\n options: MenuListOption[];\n anchorOrigin?: PopoverOrigin;\n transformOrigin?: PopoverOrigin;\n}\nexport const MenuList = ({\n id = '',\n anchorEl,\n isOpen,\n onClose,\n options,\n anchorOrigin = { vertical: 'top', horizontal: 'left' },\n transformOrigin = { vertical: 'top', horizontal: 'left' },\n}: Props) => {\n const handleOnClick = useCallback(\n (event: React.SyntheticEvent | React.MouseEvent | MouseEvent | TouchEvent) => {\n event.stopPropagation();\n },\n [],\n );\n return (\n \n );\n};\n","import { Button } from '@mui/material';\nimport { MenuItemProps } from '@mui/material/MenuItem/MenuItem';\nimport { PopoverOrigin } from '@mui/material/Popover/Popover';\nimport React, { forwardRef, useCallback, useState } from 'react';\n\nimport IconVerticalMenu from 'shared/components/icons/icon-vertical-menu';\n\nimport { MenuList } from '../menu-list';\n\nexport type MenuListOption = {\n avoidCloseOnClick?: boolean;\n displayText?: string | JSX.Element;\n icon?: JSX.Element;\n isHidden?: boolean | undefined;\n onClick?: (event: React.MouseEvent) => void;\n selected?: boolean;\n menuItemProps?: MenuItemProps;\n};\n\ntype Props = {\n id?: string;\n options?: MenuListOption[];\n triggerComponent?: JSX.Element;\n anchorOrigin?: PopoverOrigin;\n transformOrigin?: PopoverOrigin;\n disableButtonPadding?: boolean;\n};\n\nexport type Ref = HTMLDivElement;\n\nconst defaultTriggerComponent = ;\n\nconst KebabMenu = forwardRef[(\n (\n {\n anchorOrigin,\n transformOrigin,\n options = [],\n triggerComponent = defaultTriggerComponent,\n id,\n disableButtonPadding,\n },\n ref,\n ) => {\n const [anchorEl, setAnchorEl] = useState(null);\n const menuId = id ? `kebab-menu-${id}` : 'kebab-menu';\n\n const handleClick = (event: React.MouseEvent) => {\n event.stopPropagation();\n setAnchorEl(event.currentTarget);\n };\n\n const handleClose = useCallback(\n (event: React.SyntheticEvent | React.MouseEvent | MouseEvent | TouchEvent) => {\n event && event.stopPropagation();\n if (!anchorEl) return;\n\n setAnchorEl(null);\n },\n [anchorEl],\n );\n\n const menuOptions = options.filter((option) => !option.isHidden);\n\n return (\n ]\n \n \n
\n );\n },\n);\n\nKebabMenu.displayName = 'KebabMenu';\n\nexport default KebabMenu;\n","import { SvgIcon, type SvgIconProps } from '../svg-icon/SvgIcon';\n\nconst IconPause = (props: Omit): JSX.Element => {\n return (\n \n \n \n );\n};\n\nexport default IconPause;\n","import React, { useCallback } from 'react';\n\nimport IconPause from 'shared/components/icons/icon-pause';\nimport IconPlay from 'shared/components/icons/icon-play';\nimport {\n useVideoPlayerActions,\n useVideoPlayerIsPlaying,\n useVideoPlayerState,\n} from 'shared/components/video-player/hooks';\nimport { PlaylistItemType } from 'shared/components/video-player/types';\n\ninterface Props {\n playlistItem: PlaylistItemType;\n}\n\nexport const PlayButton = ({ playlistItem }: Props) => {\n const isPlaying = useVideoPlayerIsPlaying();\n const { isEnded } = useVideoPlayerState();\n const actions = useVideoPlayerActions();\n\n const handlePause = useCallback(\n (event: React.MouseEvent) => {\n event.stopPropagation();\n actions.pause();\n },\n [actions],\n );\n\n const handlePlay = useCallback(\n (event: React.MouseEvent) => {\n event.stopPropagation();\n !isPlaying && !isEnded ? actions.play() : actions.setPlaylistItem(playlistItem.id, true);\n },\n [playlistItem, isPlaying, actions, isEnded],\n );\n\n return isPlaying ? (\n \n ) : (\n \n );\n};\n","import { motion, useMotionTemplate, useMotionValue } from 'framer-motion';\nimport { Colors } from 'kognia-ui';\nimport { useEffect } from 'react';\n\nimport { useCurrentTime, useDuration, useVideoPlayerPlayingMode } from 'shared/components/video-player';\nimport { useCurrentVideoSource } from 'shared/components/video-player/hooks';\n\ntype ProgressBarProps = {\n isEditing: boolean;\n};\n\nconst ProgressBar = ({ isEditing }: ProgressBarProps) => {\n const duration = useDuration();\n const currentTime = useCurrentTime();\n const videoSource = useCurrentVideoSource();\n const playingMode = useVideoPlayerPlayingMode();\n const width = useMotionValue(0);\n const widthPercent = useMotionTemplate`${width}%`;\n\n useEffect(() => {\n const currentSourceTime = playingMode.useEffectiveTime ? currentTime : currentTime - videoSource.startTime;\n const currentPercent = (100 * currentSourceTime) / duration;\n\n width.set(currentSourceTime > 0.1 ? currentPercent : 0);\n }, [duration, videoSource.startTime, width, currentTime, playingMode]);\n\n return (\n \n );\n};\n\ntype Props = {\n isEditing: boolean;\n isVisible: boolean;\n};\n\nexport const PlaylistItemProgressBar = ({ isEditing, isVisible }: Props) => {\n if (!isVisible) return null;\n\n return ;\n};\n","import { Checkbox, Stack, Typography } from '@mui/material';\nimport { Colors, fontSizes } from 'kognia-ui';\n\nimport { useIsBulkModeActive } from 'entities/playlist/hooks/useIsBulkModeActive';\nimport { ContentBottomRow } from 'entities/playlist/ui/ContentBottomRow';\nimport { ContentTopRow } from 'entities/playlist/ui/ContentTopRow';\nimport { CounterBadge } from 'entities/playlist/ui/CounterBadge';\nimport { ItemContentWrapper } from 'entities/playlist/ui/ItemContentWrapper';\nimport IconPlay from 'shared/components/icons/icon-play';\nimport IconTime from 'shared/components/icons/icon-time';\nimport KebabMenu, { MenuListOption } from 'shared/components/kebab-menu';\nimport { useIsCurrentPlaylistItem } from 'shared/components/video-player';\nimport { PlaylistItemType } from 'shared/components/video-player/types';\nimport { secondsAsDuration } from 'shared/utils/seconds-as-duration';\n\nimport { PlayButton } from './ui/PlayButton';\nimport { PlaylistItemProgressBar } from './ui/PlaylistItemProgressBar';\n\ntype Props = {\n showProgressBar: boolean;\n isEditingMode: boolean;\n isChecked: boolean;\n getMenuOptions: (hasOverlays: boolean) => MenuListOption[];\n playlistItem: PlaylistItemType;\n videoIndex: number;\n onClick: (value: boolean) => void;\n playlistId: string;\n kebabRef: React.MutableRefObject;\n};\n\nexport const PlaylistItemContent = ({\n playlistId,\n onClick,\n showProgressBar,\n isEditingMode,\n getMenuOptions,\n playlistItem,\n videoIndex,\n isChecked,\n kebabRef,\n}: Props) => {\n const enabledBulkMode = useIsBulkModeActive(playlistId);\n const isCurrentPlaylistItem = useIsCurrentPlaylistItem(playlistItem.id);\n\n return (\n \n \n \n \n {enabledBulkMode ? : null}\n {isCurrentPlaylistItem && !enabledBulkMode ? (\n \n ) : enabledBulkMode ? null : (\n onClick(true)} isButton size={'small'} color={'primary'} />\n )}\n {(videoIndex + 1).toString().padStart(3, '0')}\n \n \n {secondsAsDuration(playlistItem.duration, false)}\n \n \n \n \n {playlistItem.name}\n \n \n {`${playlistItem.recordingName} (${playlistItem.recordingMatchday})`}\n \n \n \n {!enabledBulkMode ? (\n \n \n \n ) : null}\n \n );\n};\n","import {\n Button,\n ButtonProps,\n DialogActions,\n DialogContent,\n DialogContentText,\n Popover,\n PopoverOrigin,\n} from '@mui/material';\nimport { fontSizes } from 'kognia-ui';\nimport React from 'react';\n\nimport styles from './ConfirmPopoverDialog.module.scss';\nimport IconClose from '../icons/icon-close';\n\ninterface Props {\n cancelLabel: string;\n confirmLabel: string;\n description: string;\n onCancel?: () => void;\n onConfirm: () => void;\n isOpen: boolean;\n buttonColor?: ButtonProps['color'];\n anchorEl: HTMLDivElement | HTMLElement | null;\n setIsOpen: (isOpen: boolean) => void;\n anchorOrigin?: PopoverOrigin | undefined;\n transformOrigin?: PopoverOrigin | undefined;\n}\n\nconst ConfirmPopoverDialog = ({\n anchorEl,\n isOpen,\n cancelLabel,\n confirmLabel,\n description,\n onCancel,\n onConfirm,\n setIsOpen,\n buttonColor = 'error',\n anchorOrigin = {\n vertical: 'bottom',\n horizontal: 'left',\n },\n transformOrigin = {\n vertical: 'center',\n horizontal: 'left',\n },\n}: Props) => {\n const handleCancel = (event: React.MouseEvent) => {\n event.stopPropagation();\n if (onCancel) onCancel();\n setIsOpen(false);\n };\n\n const handleConfirm = (event: React.MouseEvent) => {\n event.stopPropagation();\n onConfirm();\n setIsOpen(false);\n };\n\n return (\n \n event.stopPropagation()}>\n
\n \n
\n
\n \n {description}\n \n \n
\n \n \n \n
\n \n );\n};\n\nexport default ConfirmPopoverDialog;\n","import { SvgIcon, type SvgIconProps } from '../svg-icon/SvgIcon';\n\nconst IconAnalysis = (props: Omit): JSX.Element => {\n return (\n \n \n \n );\n};\n\nexport default IconAnalysis;\n","import { SvgIcon, type SvgIconProps } from '../svg-icon/SvgIcon';\n\nconst IconBackward = (props: Omit): JSX.Element => {\n return (\n \n \n \n );\n};\n\nexport default IconBackward;\n","import { SvgIcon, type SvgIconProps } from '../svg-icon/SvgIcon';\n\nconst IconBackward5 = (props: Omit): JSX.Element => {\n return (\n \n \n \n );\n};\n\nexport default IconBackward5;\n","import { SvgIcon, type SvgIconProps } from '../svg-icon/SvgIcon';\n\nconst IconCamera = (props: Omit): JSX.Element => {\n return (\n \n \n \n );\n};\n\nexport default IconCamera;\n","import { SvgIcon, type SvgIconProps } from '../svg-icon/SvgIcon';\n\nconst IconChevronUp = (props: Omit): JSX.Element => {\n return (\n \n \n \n );\n};\n\nexport default IconChevronUp;\n","import { SvgIcon, type SvgIconProps } from '../svg-icon/SvgIcon';\n\nconst IconCopy = (props: Omit): JSX.Element => {\n return (\n \n \n \n );\n};\n\nexport default IconCopy;\n","import { SvgIcon, type SvgIconProps } from '../svg-icon/SvgIcon';\n\nconst IconDefense = (props: Omit): JSX.Element => {\n return (\n \n \n \n \n );\n};\n\nexport default IconDefense;\n","import { SvgIcon, type SvgIconProps } from '../svg-icon/SvgIcon';\n\nconst IconDelete = (props: Omit): JSX.Element => {\n return (\n \n