diff --git a/front/src/components/popup/TrackEditPopUp.tsx b/front/src/components/popup/MediaEditPopUp.tsx similarity index 70% rename from front/src/components/popup/TrackEditPopUp.tsx rename to front/src/components/popup/MediaEditPopUp.tsx index 14fbff9..1ec99ad 100644 --- a/front/src/components/popup/TrackEditPopUp.tsx +++ b/front/src/components/popup/MediaEditPopUp.tsx @@ -1,6 +1,6 @@ -import { useRef, useState } from 'react'; +import { useRef } from 'react'; -import { Button, Text, useDisclosure } from '@chakra-ui/react'; +import { Button, Tabs, Text, useDisclosure } from '@chakra-ui/react'; import { MdAdminPanelSettings, MdDeleteForever, MdEdit } from 'react-icons/md'; import { useNavigate, useParams } from 'react-router-dom'; @@ -9,15 +9,14 @@ import { FormGroupShow } from '@/components/form/FormGroup'; import { FormInput } from '@/components/form/FormInput'; import { FormNumber } from '@/components/form/FormNumber'; import { FormSelect } from '@/components/form/FormSelect'; -import { FormSelectMultiple } from '@/components/form/FormSelectMultiple'; import { FormTextarea } from '@/components/form/FormTextarea'; import { ConfirmPopUp } from '@/components/popup/ConfirmPopUp'; import { - DialogBody, - DialogContent, - DialogFooter, - DialogHeader, - DialogRoot, + DialogBody, + DialogContent, + DialogFooter, + DialogHeader, + DialogRoot, } from '@/components/ui/dialog'; import { useMediaService, useSpecificMedia } from '@/service/Media'; import { useOrderedSeasons } from '@/service/Season'; @@ -31,32 +30,31 @@ import { Formidable, useFormidable } from '../formidable'; export type MediaEditPopUpProps = {}; export const MediaEditPopUp = ({}: MediaEditPopUpProps) => { - const { MediaId } = useParams(); - const MediaIdInt = isNullOrUndefined(MediaId) + const { mediaId } = useParams(); + const mediaIdInt = isNullOrUndefined(mediaId) ? undefined - : parseInt(MediaId, 10); + : parseInt(mediaId, 10); const { session } = useServiceContext(); - const { dataTypes } = useOrderedTypes(undefined); - const { dataSeries } = useOrderedSeries(undefined); - const { dataSeasons } = useOrderedSeasons(undefined); + const { dataTypes } = useOrderedTypes(); + const { dataSeries } = useOrderedSeries(); + const { dataSeasons } = useOrderedSeasons(); const { store } = useMediaService(); - const { dataMedia } = useSpecificMedia(MediaIdInt); - const [admin, setAdmin] = useState(false); + const { dataMedia } = useSpecificMedia(mediaIdInt); const navigate = useNavigate(); const disclosure = useDisclosure(); const onClose = () => { - navigate('../../', { relative: 'path' }); + navigate('../../..', { relative: 'path' }); }; const onRemove = () => { - if (isNullOrUndefined(MediaIdInt)) { + if (isNullOrUndefined(mediaIdInt)) { return; } store.remove( - MediaIdInt, + mediaIdInt, MediaResource.remove({ restConfig: session.getRestConfig(), params: { - id: MediaIdInt, + id: mediaIdInt, }, }) ); @@ -69,7 +67,7 @@ export const MediaEditPopUp = ({}: MediaEditPopUpProps) => { deltaConfig: { omit: ['covers'] }, }); const onSave = async (dataDelta: MediaWrite) => { - if (isNullOrUndefined(MediaIdInt)) { + if (isNullOrUndefined(mediaIdInt)) { return; } console.log(`onSave = ${JSON.stringify(dataDelta, null, 2)}`); @@ -78,7 +76,7 @@ export const MediaEditPopUp = ({}: MediaEditPopUpProps) => { restConfig: session.getRestConfig(), data: dataDelta, params: { - id: MediaIdInt, + id: mediaIdInt, }, }) ); @@ -99,8 +97,48 @@ export const MediaEditPopUp = ({}: MediaEditPopUpProps) => { {/* */} - {admin && ( - <> + + + + + Edit + + + + Admin + + + {/* ---------------------- Other Tabs --------------------------- */} + + + + + + + + + {/* ---------------------- Other Tabs --------------------------- */} + {dataMedia?.id} @@ -123,62 +161,18 @@ export const MediaEditPopUp = ({}: MediaEditPopUpProps) => { confirmTitle="Remove" onConfirm={onRemove} /> - - )} - {!admin && ( - <> - - - - - - - - )} + + - - {!admin && form.isFormModified && ( - )} - diff --git a/front/src/components/select/SelectMultiple.tsx b/front/src/components/select/SelectMultiple.tsx index b247a23..96e6926 100644 --- a/front/src/components/select/SelectMultiple.tsx +++ b/front/src/components/select/SelectMultiple.tsx @@ -57,7 +57,8 @@ export const SelectMultiple = ({ return []; } return transformedOption.filter((element) => { - return values.includes(element[keyKey]); + console.log(`plop ${JSON.stringify(values, null, 2)}`); + return values?.includes(element[keyKey]); }); }, [values, transformedOption]); diff --git a/front/src/scene/home/AddPage.tsx b/front/src/scene/home/AddPage.tsx index b640258..4e4c4ff 100644 --- a/front/src/scene/home/AddPage.tsx +++ b/front/src/scene/home/AddPage.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Box, Button, Flex, Input, Table, Text } from '@chakra-ui/react'; import { LuTrash } from 'react-icons/lu'; @@ -12,9 +12,10 @@ import { Series, SeriesResource, Type, - TypeResource + TypeResource, } from '@/back-api'; import { PageLayout } from '@/components/Layout/PageLayout'; +import { ParameterLayout } from '@/components/ParameterLayout'; import { TopBar } from '@/components/TopBar/TopBar'; import { FormSelect } from '@/components/form/FormSelect'; import { Formidable, useFormidable } from '@/components/formidable'; @@ -23,10 +24,20 @@ import { NumberInputField, NumberInputRoot, } from '@/components/ui/number-input'; -import { useMediaService } from '@/service/Media'; -import { useOrderedSeries, useOrderedSeriesWithType, useSeriesService } from '@/service/Series'; +import { + MediaExpandSeason, + useFilteredMedia, + useMediaExpandSeason, + useMediaService, +} from '@/service/Media'; +import { + useOrderedSeries, + useOrderedSeriesWithType, + useSeriesService, +} from '@/service/Series'; import { useServiceContext } from '@/service/ServiceContext'; import { useOrderedTypes, useTypeService } from '@/service/Type'; +import { useDebounce, useDebouncedCallback } from '@/utils/debouncedCallback'; import { isNullOrUndefined } from '@/utils/validator'; export class ElementList { @@ -40,7 +51,7 @@ export class ElementList { export class FileParsedElement { public isSended: boolean = false; public nameDetected: boolean = false; - public mediaIdDetected: boolean = false; + public episodeDetected: boolean = false; public seasonId?: Season['id'] = undefined; public seriesId?: Series['id'] = undefined; constructor( @@ -50,7 +61,7 @@ export class FileParsedElement { public universe?: string, public series?: string, public season?: number, - public mediaId?: number + public episode?: number ) { console.log(`Unique element: ${uniqueId}`); // nothing to do. @@ -66,7 +77,11 @@ export class FileFailParsedElement { // nothing to do. } } - +type InspectionType = { + episodeDetected: boolean; + nameDetected: boolean; + media: MediaExpandSeason; +}; type FormInsertData = { typeId?: number; seriesId?: number; @@ -81,26 +96,95 @@ export const AddPage = () => { const [needSend, setNeedSend] = useState(false); - // list of all files already registered in the bdd to compare with the current list of files. - const [listFileInBdd, setListFileInBdd] = useState( - undefined - ); - const { dataTypes } = useOrderedTypes(); const { dataSeries: dataSeriesFull } = useOrderedSeries(); const { store: storeType } = useTypeService(); const { store: storeSeries } = useSeriesService(); const { store: storeMedia } = useMediaService(); const { session } = useServiceContext(); - const form = useFormidable({ configuration: { enableModifyNotification: false, enableReset: false, }, }); + // list of all files already registered in the bdd to compare with the current list of files. + const [listFileInBdd, setListFileInBdd] = useState< + InspectionType[] | undefined + >(undefined); + const formValuesSelectForCheck = useDebounce(form.values, 700); + const { medias } = useFilteredMedia( + formValuesSelectForCheck.typeId, + formValuesSelectForCheck.seriesId + ); + const { mediasExpand } = useMediaExpandSeason(medias); + useEffect(() => { + handleCheck(''); + }, [ + formValuesSelectForCheck, + formValuesSelectForCheck.typeId, + mediasExpand, + parsedElement, + ]); + + const handleCheck = useDebouncedCallback((_newValue) => { + console.log(`values has changes ... ${mediasExpand.length} medias`); + // find the good case: (ignore the universe that have no sense...) + if (formValuesSelectForCheck.typeId === undefined) { + setListFileInBdd(undefined); + return; + } + const out: InspectionType[] = []; + mediasExpand.forEach((media) => { + out.push({ + episodeDetected: false, + nameDetected: false, + media, + }); + }); + // clear previous data: + out.forEach((elem) => { + elem.episodeDetected = false; + elem.nameDetected = false; + }); + parsedElement.forEach((elem) => { + elem.episodeDetected = false; + elem.nameDetected = false; + }); + // Detect the identical season + identical ID: + console.log(`check:'${JSON.stringify(parsedElement, null, 2)}'`); + out.forEach((checkElem) => { + parsedElement.forEach((parsedElem) => { + if ( + `${parsedElem.season}` === `${checkElem.media?.season?.name}` && + `${parsedElem.episode}` === `${checkElem.media?.episode}` + ) { + checkElem.episodeDetected = true; + parsedElem.episodeDetected = true; + } + if ( + parsedElem.title?.length !== undefined && + parsedElem.title?.length !== 0 && + checkElem.media?.name?.length !== 0 && + checkElem.media?.name?.length !== 0 && + (parsedElem.title.startsWith(checkElem.media?.name) || + checkElem.media?.name.startsWith(parsedElem.title)) + ) { + checkElem.nameDetected = true; + parsedElem.nameDetected = true; + } + }); + }); + setListFileInBdd( + out.sort((a, b) => { + const aPriority = a.episodeDetected || a.nameDetected ? 1 : 0; + const bPriority = b.episodeDetected || b.nameDetected ? 1 : 0; + return bPriority - aPriority; + }) + ); + }, 1000); // I think this does not work ... - const { dataSeries } = useOrderedSeriesWithType(form.values["typeId"]); + const { dataSeries } = useOrderedSeriesWithType(form.values.typeId); const updateNeedSend = () => { if (parsedElement.length === 0) { @@ -145,8 +229,8 @@ export const AddPage = () => { updateNeedSend(); }; - const onMediaId = (data: FileParsedElement, value: any): void => { - data.mediaId = value; + const onEpisode = (data: FileParsedElement, value: any): void => { + data.episode = value; setParsedElement([...parsedElement]); updateNeedSend(); }; @@ -163,15 +247,16 @@ export const AddPage = () => { setSuggestedSeries(undefined); //setSuggestedSeason(undefined); }; - - const regex = /^(?:(?[\w. -]+):)?((?[\w. -]+?)((-s| S)(?\d{1,5}))?(?:(-e|E)(?\d{1,5}))[- ])?\s*(?.+?)\.(webm|WEBM|Webm|mkv|MKV|Mkv)$/; + + const regex = + /^(?:(?<universe>[\w. -]+):)?((?<series>[\w. -]+?)((-s| S)(?<season>\d{1,5}))?(?:(-e|E)(?<episode>\d{1,5}))[- ])?\s*(?<title>.+?)\.(webm|WEBM|Webm|mkv|MKV|Mkv)$/; const addFileWithMetaData = (file: File, id: number) => { // parsedElement: FileParsedElement[] = []; let universe: string | undefined = undefined; let series: string | undefined = undefined; let season: number | undefined = undefined; - let mediaIdNumber: number | undefined = undefined; + let episodeNumber: number | undefined = undefined; let title: string = ''; form.restoreValues(); @@ -180,21 +265,25 @@ export const AddPage = () => { const match = file.name.match(regex); if (match?.groups) { - universe = match.groups.universe || undefined; - series = match.groups.series ? match.groups.series.trim() : undefined; - season = match.groups.season? parseInt(match.groups.season, 10) : undefined; - mediaIdNumber = match.groups.episode ? parseInt(match.groups.episode, 10) : undefined; - title = match.groups.title.trim(); + universe = match.groups.universe || undefined; + series = match.groups.series ? match.groups.series.trim() : undefined; + season = match.groups.season + ? parseInt(match.groups.season, 10) + : undefined; + episodeNumber = match.groups.episode + ? parseInt(match.groups.episode, 10) + : undefined; + title = match.groups.title.trim(); } else { - console.log("❌ not match :", file.name); + console.log('❌ not match :', file.name); title = file.name.trim(); } if (season && isNaN(season)) { season = undefined; } - if (mediaIdNumber && isNaN(mediaIdNumber)) { - mediaIdNumber = undefined; + if (episodeNumber && isNaN(episodeNumber)) { + episodeNumber = undefined; } // remove extension title = title.replace(new RegExp('\\.(webm|WEBM|Webm|mkv|MKV|Mkv)'), ''); @@ -205,7 +294,7 @@ export const AddPage = () => { universe, series, season, - mediaIdNumber + episodeNumber ); console.log(`==>${JSON.stringify(tmp, null, 2)}`); @@ -316,10 +405,10 @@ export const AddPage = () => { typeId: `${form.values['typeId']}`, seriesId: `${form.values['seriesId']}`, season: `${parsedElement[index].season}`, - episode: `${parsedElement[index].mediaId}`, + episode: `${parsedElement[index].episode}`, }; console.log(`data= ${JSON.stringify(data, null, 2)}`); - console.error("Not_ implemented"); + console.error('Not_ implemented'); storeMedia .update( MediaResource.uploadMedia({ @@ -384,7 +473,7 @@ export const AddPage = () => { restConfig: session.getRestConfig(), data: { name: data, - parentId: form.values["typeId"] + parentId: form.values['typeId'], }, }) ); @@ -394,140 +483,160 @@ export const AddPage = () => { <> <TopBar title="Add new media" /> <PageLayout> - <Flex - direction="column" - width="80%" - marginX="auto" - padding="10px" - gap="10px" - > - <Flex direction="column" width="full"> - <Flex> - <Text flex={1}>format:</Text> - <Text flex={4}> - The format of the media permit to automatic find meta-data:<br /> - <li>Universe:Series name-s05-e22-Title.webm/mkv</li> - <li>Universe:Series name S05E22 Title.webm/mkv</li> - <b>example:</b> Stargate:SG1-s05-e22-Tolans.webm - </Text> + <ParameterLayout.Root> + <ParameterLayout.HeaderBase + title="New Media" + description="Add a new media in the library." + /> + <ParameterLayout.Content> + <Flex direction="column" width="full"> + <Flex> + <Text flex={1}>format:</Text> + <Text flex={4}> + The format of the media permit to automatic find meta-data: + <br /> + <li>Universe:Series name-s05-e22-Title.webm/mkv</li> + <li>Universe:Series name S05E22 Title.webm/mkv</li> + <b>example:</b> Stargate:SG1-s05-e22-Tolans.webm + </Text> + </Flex> + <Flex> + <Text flex={1}>Media:</Text> + <Input + flex={4} + type="file" + placeholder="Select a media file" + accept=".webm,.mkv" + multiple + onChange={onChangeFile} + /> + </Flex> </Flex> - <Flex> - <Text flex={1}>Media:</Text> - <Input - flex={4} - type="file" - placeholder="Select a media file" - accept=".webm,.mkv" - multiple - onChange={onChangeFile} - /> - </Flex> - </Flex> - {parsedElement && parsedElement.length !== 0 && ( - <Formidable.From form={form} onSubmit={sendFile}> - <Text fontSize="30px">Meta-data:</Text> - <FormSelect - label="Type" - name="typeId" - options={dataTypes} - addNewItem={addNewType} - isRequired - /> - <FormSelect - label="Series" - name="seriesId" - options={dataSeries} - addNewItem={addNewSeries} - suggestion={suggestedSeries} - disabled={form.values["typeId"] === undefined} - /> + </ParameterLayout.Content> + <ParameterLayout.Footer /> + </ParameterLayout.Root> - <Table.Root - colorPalette="striped" - colorScheme="teal" - background="gray.700" - > - <Table.Header> - <Table.Row> - <Table.ColumnHeader width="10%"> - Season ID - </Table.ColumnHeader> - <Table.ColumnHeader width="10%"> - Media ID - </Table.ColumnHeader> - <Table.ColumnHeader width="full">Title</Table.ColumnHeader> - <Table.ColumnHeader>actions</Table.ColumnHeader> - </Table.Row> - </Table.Header> - <Table.Body> - {parsedElement.map((data) => ( - <Table.Row key={data.uniqueId}> - <Table.Cell> - {form.values["seriesId"] && - <NumberInputRoot - value={data.season ? `${data.season}` : undefined} - onValueChange={(e) => onSeasonId(data, e.value)} - min={0} - max={5000} - backgroundColor={ - data.mediaIdDetected === true - ? 'darkred' - : undefined - } - > - <NumberInputField /> - </NumberInputRoot> - } - </Table.Cell> - <Table.Cell> - {form.values["seriesId"] && - <NumberInputRoot - value={data.mediaId ? `${data.mediaId}` : undefined} - onValueChange={(e) => onMediaId(data, e.value)} - min={0} - max={5000} - backgroundColor={ - data.mediaIdDetected === true - ? 'darkred' - : undefined - } - > - <NumberInputField /> - </NumberInputRoot> - } - </Table.Cell> - <Table.Cell> - <Input - type="text" - placeholder="Name of the Media" - value={data.title} - onChange={(e) => onTitle(data, e.target.value)} - backgroundColor={ - data.title === '' ? 'darkred' : undefined - } - /> - {data.nameDetected === true && ( - <> - <br /> - <Text as="span" color="@danger"> - ^^^This title already exist !!! - </Text> - </> - )} - </Table.Cell> - <Table.Cell> - <Button - colorPalette="@danger" - onClick={(e) => removeElementFromList(data, e.target)} - > - <LuTrash /> Remove - </Button> - </Table.Cell> + {parsedElement && parsedElement.length !== 0 && ( + <Formidable.From form={form} onSubmit={sendFile}> + <ParameterLayout.Root> + <ParameterLayout.HeaderBase + title="Meta-data:" + description="Edit the data that might be upload." + /> + <ParameterLayout.Content> + <FormSelect + label="Type" + name="typeId" + options={dataTypes} + addNewItem={addNewType} + isRequired + /> + <FormSelect + label="Series" + name="seriesId" + options={dataSeries} + addNewItem={addNewSeries} + suggestion={suggestedSeries} + disabled={form.values['typeId'] === undefined} + /> + + <Table.Root + colorPalette="striped" + colorScheme="teal" + background="gray.700" + > + <Table.Header> + <Table.Row> + <Table.ColumnHeader width="10%"> + Season ID + </Table.ColumnHeader> + <Table.ColumnHeader width="10%"> + Media ID + </Table.ColumnHeader> + <Table.ColumnHeader width="full"> + Title + </Table.ColumnHeader> + <Table.ColumnHeader>actions</Table.ColumnHeader> </Table.Row> - ))} - </Table.Body> - </Table.Root> - <Flex marginY="15px"> + </Table.Header> + <Table.Body> + {parsedElement.map((data) => ( + <Table.Row key={data.uniqueId}> + <Table.Cell> + {form.values['seriesId'] && ( + <NumberInputRoot + value={data.season ? `${data.season}` : undefined} + onValueChange={(e) => onSeasonId(data, e.value)} + min={0} + max={5000} + backgroundColor={ + data.episodeDetected === true + ? 'darkred' + : undefined + } + > + <NumberInputField /> + </NumberInputRoot> + )} + </Table.Cell> + <Table.Cell> + {form.values['seriesId'] && ( + <NumberInputRoot + value={ + data.episode ? `${data.episode}` : undefined + } + onValueChange={(e) => onEpisode(data, e.value)} + min={0} + max={5000} + backgroundColor={ + data.episodeDetected === true + ? 'darkred' + : undefined + } + > + <NumberInputField /> + </NumberInputRoot> + )} + </Table.Cell> + <Table.Cell> + <Input + type="text" + placeholder="Name of the Media" + value={data.title} + onChange={(e) => onTitle(data, e.target.value)} + backgroundColor={ + data.title === '' + ? 'purple' + : data.nameDetected === true + ? 'darkred' + : undefined + } + /> + {data.nameDetected === true && ( + <> + <br /> + <Text as="span" color="red"> + ^^^This title already exist !!! + </Text> + </> + )} + </Table.Cell> + <Table.Cell> + <Button + colorPalette="@danger" + onClick={(e) => + removeElementFromList(data, e.target) + } + > + <LuTrash /> Remove + </Button> + </Table.Cell> + </Table.Row> + ))} + </Table.Body> + </Table.Root> + </ParameterLayout.Content> + <ParameterLayout.Footer> <Button colorPalette="brand" type="submit" @@ -537,51 +646,51 @@ export const AddPage = () => { > <MdCloudUpload /> Upload </Button> - </Flex> - </Formidable.From> - )} + </ParameterLayout.Footer> + </ParameterLayout.Root> + </Formidable.From> + )} - {listFileInBdd && ( - <Table.Root - fontPalette="striped" - colorScheme="teal" - background="gray.700" - > - <Table.Header> - <Table.Row> - <Table.ColumnHeader>Media ID</Table.ColumnHeader> - <Table.ColumnHeader width="full">Title</Table.ColumnHeader> - <Table.ColumnHeader>actions</Table.ColumnHeader> - </Table.Row> - </Table.Header> - <Table.Body> - {listFileInBdd.map((data) => ( + {listFileInBdd && ( + <ParameterLayout.Root> + <ParameterLayout.HeaderBase + title="In base" + description="List of file in the base for the Type/series ... (2 seconds before update)" + /> + <ParameterLayout.Content> + <Table.Root + fontPalette="striped" + colorScheme="teal" + background="gray.700" + > + <Table.Header> <Table.Row> - <Table.Cell> - <Text - color={ - data.episodeDetected === true ? 'red' : undefined - } - > - {data.MediaId} - </Text> - </Table.Cell> - <Table.Cell> - <Text - color={data.nameDetected === true ? 'red' : undefined} - > - {data.title} - </Text> - </Table.Cell> - <Table.Cell></Table.Cell> + <Table.ColumnHeader>Media ID</Table.ColumnHeader> + <Table.ColumnHeader>Data ID</Table.ColumnHeader> + <Table.ColumnHeader>Season</Table.ColumnHeader> + <Table.ColumnHeader>Episode</Table.ColumnHeader> + <Table.ColumnHeader width="full">Title</Table.ColumnHeader> + <Table.ColumnHeader>Date</Table.ColumnHeader> + <Table.ColumnHeader>Actions</Table.ColumnHeader> </Table.Row> - ))} - </Table.Body> - </Table.Root> - )} - - {parsedFailedElement && ( - <> + </Table.Header> + <Table.Body> + {listFileInBdd.map((data) => ( + <MediaDetectionDetail data={data} /> + ))} + </Table.Body> + </Table.Root> + </ParameterLayout.Content> + <ParameterLayout.Footer /> + </ParameterLayout.Root> + )} + {parsedFailedElement && ( + <ParameterLayout.Root> + <ParameterLayout.HeaderBase + title="Rejected:" + description="List of files that has been removed due to parsing error" + /> + <ParameterLayout.Content> <Text fontSize="30px">Rejected:</Text> <Table.Root colorPalette="striped" @@ -603,10 +712,12 @@ export const AddPage = () => { ))} </Table.Body> </Table.Root> - </> - )} - <Box height="250px" width="full" /> - </Flex> + </ParameterLayout.Content> + <ParameterLayout.Footer /> + </ParameterLayout.Root> + )} + <Box height="50%" minHeight="50%" width="99%" /> + {/* upload pop-in */} {indexUpload !== undefined && ( <PopUpUploadProgress title="Upload File(s)" @@ -624,3 +735,37 @@ export const AddPage = () => { </> ); }; +export const MediaDetectionDetail = ({ data }: { data: InspectionType }) => { + return ( + <Table.Row> + <Table.Cell> + <Text>{data.media.id}</Text> + </Table.Cell> + <Table.Cell> + <Text>{data.media.dataId.toUpperCase()}</Text> + </Table.Cell> + <Table.Cell> + <Text color={data.episodeDetected === true ? 'red' : undefined}> + {data.media.season?.name} + </Text> + </Table.Cell> + <Table.Cell> + <Text color={data.episodeDetected === true ? 'red' : undefined}> + {data.media.episode} + </Text> + </Table.Cell> + <Table.Cell> + <Text + userSelect="text" + color={data.nameDetected === true ? 'red' : undefined} + > + {data.media.name} + </Text> + </Table.Cell> + <Table.Cell> + <Text>{data.media.date}</Text> + </Table.Cell> + <Table.Cell></Table.Cell> + </Table.Row> + ); +}; diff --git a/front/src/scene/onAir/OnAirPage.tsx b/front/src/scene/onAir/OnAirPage.tsx index 0ad0e4c..066caf7 100644 --- a/front/src/scene/onAir/OnAirPage.tsx +++ b/front/src/scene/onAir/OnAirPage.tsx @@ -11,10 +11,10 @@ import { Route, Routes, useNavigate } from 'react-router-dom'; import { EmptyEnd } from '@/components/EmptyEnd'; import { PageLayout } from '@/components/Layout/PageLayout'; import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter'; +import { BUTTON_TOP_BAR_PROPERTY, TopBar } from '@/components/TopBar/TopBar'; import { DisplayMediaFullId } from '@/components/media/DisplayMediaFullId'; import { SeasonEditPopUp } from '@/components/popup/AlbumEditPopUp'; -import { MediaEditPopUp } from '@/components/popup/TrackEditPopUp'; -import { BUTTON_TOP_BAR_PROPERTY, TopBar } from '@/components/TopBar/TopBar'; +import { MediaEditPopUp } from '@/components/popup/MediaEditPopUp'; import { Button } from '@/components/ui/button'; import { useColorModeValue } from '@/components/ui/color-mode'; import { BASE_WRAP_SPACING } from '@/constants/genericSpacing'; diff --git a/front/src/scene/season/SeasonDetailPage.tsx b/front/src/scene/season/SeasonDetailPage.tsx index 862a66d..930e369 100644 --- a/front/src/scene/season/SeasonDetailPage.tsx +++ b/front/src/scene/season/SeasonDetailPage.tsx @@ -7,12 +7,16 @@ import { Covers } from '@/components/Cover'; import { EmptyEnd } from '@/components/EmptyEnd'; import { PageLayout } from '@/components/Layout/PageLayout'; import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter'; -import { SeasonEditPopUp } from '@/components/popup/AlbumEditPopUp'; -import { MediaEditPopUp } from '@/components/popup/TrackEditPopUp'; import { BUTTON_TOP_BAR_PROPERTY, TopBar } from '@/components/TopBar/TopBar'; +import { SeasonEditPopUp } from '@/components/popup/AlbumEditPopUp'; +import { MediaEditPopUp } from '@/components/popup/MediaEditPopUp'; import { useColorModeValue } from '@/components/ui/color-mode'; import { BASE_WRAP_SPACING } from '@/constants/genericSpacing'; -import { useActivePlaylistService, useSeasonVideo, useSpecificSeason } from '@/service'; +import { + useActivePlaylistService, + useSeasonVideo, + useSpecificSeason, +} from '@/service'; export const SeasonDetailPage = () => { const { SeasonId } = useParams(); diff --git a/front/src/scene/sso/series/SeriesSeasonDetailPage.tsx b/front/src/scene/sso/series/SeriesSeasonDetailPage.tsx index 9ee726e..9be8cf4 100644 --- a/front/src/scene/sso/series/SeriesSeasonDetailPage.tsx +++ b/front/src/scene/sso/series/SeriesSeasonDetailPage.tsx @@ -7,9 +7,9 @@ import { Covers } from '@/components/Cover'; import { EmptyEnd } from '@/components/EmptyEnd'; import { PageLayout } from '@/components/Layout/PageLayout'; import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter'; -import { SeasonEditPopUp } from '@/components/popup/AlbumEditPopUp'; -import { MediaEditPopUp } from '@/components/popup/TrackEditPopUp'; import { BUTTON_TOP_BAR_PROPERTY, TopBar } from '@/components/TopBar/TopBar'; +import { SeasonEditPopUp } from '@/components/popup/AlbumEditPopUp'; +import { MediaEditPopUp } from '@/components/popup/MediaEditPopUp'; import { useActivePlaylistService } from '@/service/ActivePlaylist'; import { useSeasonVideo, useSpecificSeason } from '@/service/Season'; import { useSpecificSeries } from '@/service/Series'; diff --git a/front/src/scene/type/TypesDetailPage.tsx b/front/src/scene/type/TypesDetailPage.tsx index 7c02307..a9ccfef 100644 --- a/front/src/scene/type/TypesDetailPage.tsx +++ b/front/src/scene/type/TypesDetailPage.tsx @@ -11,7 +11,7 @@ import { BUTTON_TOP_BAR_PROPERTY, TopBar } from '@/components/TopBar/TopBar'; //import { useMediasOfAType } from '@/service/Media'; import { DisplayMediaFull } from '@/components/media/DisplayMediaFull'; import { TypeEditPopUp } from '@/components/popup/GenderEditPopUp'; -import { MediaEditPopUp } from '@/components/popup/TrackEditPopUp'; +import { MediaEditPopUp } from '@/components/popup/MediaEditPopUp'; import { DisplaySeries } from '@/components/series/DisplaySeries'; import { useColorModeValue } from '@/components/ui/color-mode'; import { diff --git a/front/src/scene/type/TypesSeriesDetailPage.tsx b/front/src/scene/type/TypesSeriesDetailPage.tsx index 4a41f6b..627ef75 100644 --- a/front/src/scene/type/TypesSeriesDetailPage.tsx +++ b/front/src/scene/type/TypesSeriesDetailPage.tsx @@ -11,7 +11,7 @@ import { BUTTON_TOP_BAR_PROPERTY, TopBar } from '@/components/TopBar/TopBar'; //import { useMediasOfAType } from '@/service/Media'; import { DisplayMediaFull } from '@/components/media/DisplayMediaFull'; import { TypeEditPopUp } from '@/components/popup/GenderEditPopUp'; -import { MediaEditPopUp } from '@/components/popup/TrackEditPopUp'; +import { MediaEditPopUp } from '@/components/popup/MediaEditPopUp'; import { DisplaySeason } from '@/components/season/DisplaySeason'; import { useColorModeValue } from '@/components/ui/color-mode'; import { @@ -34,11 +34,11 @@ export const TypesSeriesDetailPage = () => { const navigate = useNavigate(); const onSelectSeasonItem = (mediaId: number) => { navigate(`/type/${typeId}/series/${seriesId}/season/${mediaId}`); - } + }; const onSelectItem = (mediaId: number) => { let currentPlay = 0; const listMediaId: number[] = []; - console.log(`select item:${mediaId}`); + console.log(`select item:${mediaId}`); if (!videoWithType) { console.log('Fail to get Type...'); return; @@ -108,8 +108,8 @@ export const TypesSeriesDetailPage = () => { > {seasonOfSeriesId?.map((data) => ( <Box - key={data.id} - width="full" + key={data.id} + width="full" maxWidth="calc(min(450px,80%))" height="150px" border="1px" @@ -137,8 +137,8 @@ export const TypesSeriesDetailPage = () => { /> </Box> ))} - </HStack> - <Box width="full" height="10px" background="red"/> + </HStack> + <Box width="full" height="10px" background="red" /> <HStack wrap="wrap" gap="20px" @@ -148,7 +148,7 @@ export const TypesSeriesDetailPage = () => { > {videoWithType?.map((data) => ( <Box - key={data.id} + key={data.id} width="200px" height="300px" border="1px" diff --git a/front/src/scene/type/TypesSeriesSeasonDetailPage.tsx b/front/src/scene/type/TypesSeriesSeasonDetailPage.tsx index 783b646..0e546e5 100644 --- a/front/src/scene/type/TypesSeriesSeasonDetailPage.tsx +++ b/front/src/scene/type/TypesSeriesSeasonDetailPage.tsx @@ -10,7 +10,7 @@ import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter'; import { BUTTON_TOP_BAR_PROPERTY, TopBar } from '@/components/TopBar/TopBar'; import { DisplayMediaListFull } from '@/components/media/DisplayMediaListFull'; import { TypeEditPopUp } from '@/components/popup/GenderEditPopUp'; -import { MediaEditPopUp } from '@/components/popup/TrackEditPopUp'; +import { MediaEditPopUp } from '@/components/popup/MediaEditPopUp'; import { useColorModeValue } from '@/components/ui/color-mode'; import { useActivePlaylistService, @@ -29,7 +29,11 @@ export const TypesSeriesSeasonDetailPage = () => { const { dataType } = useSpecificType(typeIdInt); const { dataSeries } = useSpecificSeries(seriesIdInt); const { dataSeason } = useSpecificSeason(seasonIdInt); - const { videoWithType: videos } = useTypeSeriesSeasonGetVideo(typeIdInt, seriesIdInt, seasonIdInt); + const { videoWithType: videos } = useTypeSeriesSeasonGetVideo( + typeIdInt, + seriesIdInt, + seasonIdInt + ); const navigate = useNavigate(); const onSelectItem = (mediaId: number) => { let currentPlay = 0; @@ -107,7 +111,7 @@ export const TypesSeriesSeasonDetailPage = () => { {videos?.map((data) => ( <Box key={data.id} - width={{sm:"95%", base:"calc(max(300px,50%))"}} + width={{ sm: '95%', base: 'calc(max(300px,50%))' }} height="60px" border="1px" borderColor="brand.900" diff --git a/front/src/service/Media.ts b/front/src/service/Media.ts index 9ef0a00..22691bc 100644 --- a/front/src/service/Media.ts +++ b/front/src/service/Media.ts @@ -1,9 +1,12 @@ import { useMemo } from 'react'; -import { Media, MediaResource } from '@/back-api'; +import { Media, MediaResource, Season } from '@/back-api'; import { useServiceContext } from '@/service/ServiceContext'; import { SessionServiceProps } from '@/service/session'; import { DataStoreType, useDataStore } from '@/utils/data-store'; +import { DataTools, TypeCheck } from '@/utils/data-tools'; + +import { useSeasonService } from './Season'; export type MediaServiceProps = { store: DataStoreType<Media>; @@ -42,3 +45,46 @@ export const useSpecificMedia = (id: number | undefined) => { return { isLoading: store.isLoading, dataMedia }; }; +export const useFilteredMedia = (typeId?: number, seriesId?: number) => { + const { store } = useMediaService(); + const medias = useMemo(() => { + if (typeId === undefined) { + return []; + } + return DataTools.getsWhere( + store.data, + [ + { + check: TypeCheck.EQUAL, + key: 'typeId', + value: typeId, + }, + { + check: TypeCheck.EQUAL, + key: 'seriesId', + value: seriesId, + }, + ], + ['name'] + ); + }, [store.data, typeId, seriesId]); + return { isLoading: store.isLoading, medias }; +}; +export type MediaExpandSeason = Media & { + season?: Season; +}; +export const useMediaExpandSeason = (medias: Media[]) => { + const { store } = useSeasonService(); + const mediasExpand = useMemo(() => { + const out: MediaExpandSeason[] = []; + medias.forEach((media) => { + if (media.seasonId === undefined) { + out.push(media); + } + const tmp = DataTools.get(store.data, media.seasonId, 'id'); + out.push({ ...media, season: tmp }); + }); + return out; + }, [store.data, medias]); + return { isLoading: store.isLoading, mediasExpand }; +}; diff --git a/front/src/service/Series.ts b/front/src/service/Series.ts index 5f39ee2..d3db21d 100644 --- a/front/src/service/Series.ts +++ b/front/src/service/Series.ts @@ -43,6 +43,9 @@ export const useOrderedSeries = (nameFilter?: string) => { const { store } = useSeriesService(); const dataSeries = useMemo(() => { let tmpData = store.data; + if (tmpData == undefined) { + return []; + } if (!isNullOrUndefined(nameFilter)) { tmpData = DataTools.getNameLike(tmpData, nameFilter); } @@ -60,7 +63,10 @@ export const useOrderedSeries = (nameFilter?: string) => { }, [store.data, nameFilter]); return { isLoading: store.isLoading, dataSeries }; }; -export const useOrderedSeriesWithType = (typeId?: Type["id"], nameFilter?: string) => { +export const useOrderedSeriesWithType = ( + typeId?: Type['id'], + nameFilter?: string +) => { const { store } = useSeriesService(); const dataSeries = useMemo(() => { let tmpData = store.data; @@ -70,6 +76,7 @@ export const useOrderedSeriesWithType = (typeId?: Type["id"], nameFilter?: strin if (typeId === undefined) { return []; } + console.log('RRRRRRRRRRRRRRRRRRRRRRRegenarate '); return DataTools.getsWhere( tmpData, [ diff --git a/front/src/service/Type.ts b/front/src/service/Type.ts index 2534b0c..9c9e4a0 100644 --- a/front/src/service/Type.ts +++ b/front/src/service/Type.ts @@ -84,10 +84,10 @@ export const useTypeCountVideo = (id?: number) => { return { isLoading: store.isLoading, countVideoWithType }; }; -export const useTypeGetVideo = (id?: number) => { +export const useTypeGetVideo = (typeId?: number) => { const { store } = useMediaService(); const videoWithType = useMemo(() => { - if (id === undefined) { + if (typeId === undefined) { return []; } return DataTools.getsWhere( @@ -96,7 +96,7 @@ export const useTypeGetVideo = (id?: number) => { { check: TypeCheck.EQUAL, key: 'typeId', - value: id, + value: typeId, }, { check: TypeCheck.EQUAL, @@ -111,7 +111,7 @@ export const useTypeGetVideo = (id?: number) => { ], ['name'] ); - }, [store.data, id]); + }, [store.data, typeId]); return { isLoading: store.isLoading, videoWithType }; }; @@ -154,7 +154,11 @@ export const useTypeSeriesGetVideo = (idType?: number, idSeries?: number) => { return { isLoading: store.isLoading, videoWithType }; }; -export const useTypeSeriesSeasonGetVideo = (idType?: number, idSeries?: number, idSeason?: number) => { +export const useTypeSeriesSeasonGetVideo = ( + idType?: number, + idSeries?: number, + idSeason?: number +) => { const { store } = useMediaService(); const videoWithType = useMemo(() => { if (idType === undefined) { diff --git a/front/src/utils/debouncedCallback.ts b/front/src/utils/debouncedCallback.ts new file mode 100644 index 0000000..ea4a30b --- /dev/null +++ b/front/src/utils/debouncedCallback.ts @@ -0,0 +1,39 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +export const useDebouncedCallback = ( + callback: (...args: any[]) => void, + delay: number = 2000 +) => { + const timeoutRef = useRef<NodeJS.Timeout | null>(null); + + const debouncedFunction = useCallback( + (...args: any[]) => { + // Cancel timeout if it previously exist + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + // start a new timer + timeoutRef.current = setTimeout(() => { + callback(...args); + }, delay); + }, + [callback, delay] + ); + + return debouncedFunction; +}; + +export const useDebounce = <T>(value: T, delay: number = 2000): T => { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => clearTimeout(timer); + }, [value, delay]); + + return debouncedValue; +};