diff --git a/package-lock.json b/package-lock.json index d0ae204..3f3278b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@types/react": "^18.2.28", "@types/react-dom": "^18.2.13", "@types/uuid": "^9.0.5", + "awaiting": "^3.0.0", "country-list": "^2.3.0", "framer-motion": "^10.16.4", "hex-to-rgba": "^2.0.1", @@ -66,6 +67,9 @@ "uniqolor": "^1.1.0" } }, + "latest@types/react@latest": { + "extraneous": true + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -9456,6 +9460,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/awaiting": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/awaiting/-/awaiting-3.0.0.tgz", + "integrity": "sha512-19i4G7Hjxj9idgMlAM0BTRII8HfvsOdlr4D9cf3Dm1MZhvcKjBpzY8AMNEyIKyi+L9TIK15xZatmdcPG003yww==", + "engines": { + "node": ">=7.6.x" + } + }, "node_modules/axe-core": { "version": "4.7.2", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.2.tgz", diff --git a/package.json b/package.json index 1ad3cac..d2c07c7 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@types/react": "^18.2.28", "@types/react-dom": "^18.2.13", "@types/uuid": "^9.0.5", + "awaiting": "^3.0.0", "country-list": "^2.3.0", "framer-motion": "^10.16.4", "hex-to-rgba": "^2.0.1", diff --git a/src/components/ExportData.tsx b/src/components/ExportData.tsx index e100481..27fd998 100644 --- a/src/components/ExportData.tsx +++ b/src/components/ExportData.tsx @@ -1,39 +1,36 @@ -import { Box, Button, Flex } from "@mantine/core"; +import { Box } from "@mantine/core"; import { notifications } from "@mantine/notifications"; -import { memo, useState } from "react"; +import { memo } from "react"; import { useTranslation } from "react-i18next"; import { getAllPlaylists } from "../database/utils"; +import { Playlist } from "../types/interfaces/Playlist"; import { generateAndDownloadFile } from "../utils/generateAndDownloadFile"; -import { TransferList } from "./TransferList"; +import { TransferList, TransferListData } from "./TransferList"; -const loadPlaylistData = (data: any[]) => { +const loadPlaylistData = (playlistsTitle: string[]) => { const playlists = getAllPlaylists(); - return data.map((playlist) => - playlists.find((p) => p.playlistId ?? String(p.ID) === playlist.value), - ); + return playlists.filter((p) => playlistsTitle.includes(p.title)); }; -// const formateToTransferList = (data: Playlist[]) => { -// return data -// .filter((item) => item.videos.length > 0) -// .map((item) => ({ -// value: item.playlistId ?? String(item.ID), -// label: `${item.title} (${item.videos.length} videos)`, -// })) -// .flat(); -// }; +const formatePlaylistsToExport = (playlists: Playlist[]) => { + return playlists.map((p) => ({ + ...p, + videos: p.videos.map((v) => v.videoId), + })); +}; export const ExportData = memo(() => { + const userData = getAllPlaylists().map((p) => p.title); const { t } = useTranslation("translation", { keyPrefix: "settings.data.export", }); - const [data] = useState(getAllPlaylists().map((p) => p.title)); - - console.log(data); - const handleClick = () => { - generateAndDownloadFile(loadPlaylistData(data[1])); + const handleSubmit = (data: TransferListData) => { + const [, importData] = data; + const playlists = loadPlaylistData(importData); + const formatedPlaylists = formatePlaylistsToExport(playlists); + generateAndDownloadFile({ playlists: formatedPlaylists }); notifications.show({ title: t("notification.title"), message: t("notification.message"), @@ -43,18 +40,10 @@ export const ExportData = memo(() => { return ( - - - ); }); diff --git a/src/components/ImportData.tsx b/src/components/ImportData.tsx index b10837c..884a405 100644 --- a/src/components/ImportData.tsx +++ b/src/components/ImportData.tsx @@ -1,17 +1,25 @@ import { + Alert, Box, - Button, Flex, Group, + LoadingOverlay, Text, useMantineColorScheme, useMantineTheme, } from "@mantine/core"; -import { Dropzone } from "@mantine/dropzone"; +import { Dropzone, FileRejection } from "@mantine/dropzone"; import "@mantine/dropzone/styles.css"; import { notifications } from "@mantine/notifications"; -import { IconFileBarcode, IconFileX, IconUpload } from "@tabler/icons-react"; -import { memo, useState } from "react"; +import { + IconFileBarcode, + IconFileX, + IconInfoCircle, + IconUpload, +} from "@tabler/icons-react"; +// @ts-ignore +import { map as cappedAll } from "awaiting"; +import { FC, memo, useState } from "react"; import { useTranslation } from "react-i18next"; import { @@ -23,31 +31,16 @@ import { getPlaylists } from "../database/utils"; import { useSetFavorite } from "../providers/Favorite"; import { useSetPlaylists } from "../providers/Playlist"; import { getVideo } from "../services/video"; +import { Playlist } from "../types/interfaces/Playlist"; import { Video } from "../types/interfaces/Video"; -import { TransferList } from "./TransferList"; - -export const formateToTransferList = (data: ImportDataType[]) => { - return data - .filter((item) => item.videos.length > 0) - .map((item) => ({ - // @ts-ignore - value: String(item.ID), - label: `${item.playlistName} (${item.videos.length} videos)`, - })); -}; - -interface ImportDataType { - playlistName: string; - protected: boolean; - videos: Video[]; -} +import { TransferList, TransferListData } from "./TransferList"; export const ImportData = memo(() => { const theme = useMantineTheme(); const { colorScheme } = useMantineColorScheme(); - const [importedFileData, setImportedFileData] = useState< - ImportDataType[] | null - >(null); + const [importedFileData, setImportedFileData] = useState( + null, + ); const { t } = useTranslation("translation", { keyPrefix: "settings.data.import", }); @@ -58,23 +51,33 @@ export const ImportData = memo(() => { reader.addEventListener("load", (event) => { const fileData = JSON.parse(event.target?.result as string); - setImportedFileData(fileData); + setImportedFileData(fileData?.playlists ?? fileData); }); reader.readAsText(file); }; + const handleReject = ([file]: FileRejection[]) => { + const [error] = file.errors; + notifications.show({ + title: t("notification.error.title"), + message: error.message, + color: "red", + }); + }; + return ( + {importedFileData ? ( - setImportedFileData(null)} /> ) : ( console.log("rejected files", files)} - maxSize={3 * 1024 ** 2} + onReject={handleReject} + maxSize={10 * 1024 ** 2} accept={["application/document", "application/json"]} > { ); }); -const Test = memo( - ({ - importedFileData, - onClear, - }: { - importedFileData: ImportDataType[]; - onClear: () => void; - }) => { +const AlertImportInfos = memo(() => { + return ( + + + + + Only Invidious and HoloPlay export + file can be imported for now. + + + + ); +}); + +interface TransferListContainerProps { + data: Playlist[]; + onClear(): void; +} + +const loadPlaylistsFromFileData = ( + playlists: Playlist[], + playlistsTitle: string[], +) => playlists.filter((p) => playlistsTitle.includes(p.title)); + +type FetchVideosData = { + video: Video; + url: string; +}[]; + +const getVideosData = async ( + videos: Video[], +): Promise<{ + validData: FetchVideosData; + invalidData: FetchVideosData; +}> => { + const data = (await cappedAll(videos, 5, (video: Video) => + getVideo(video.videoId), + )) as FetchVideosData; + const validData = data.filter( + ({ url }) => url !== undefined, + ) as FetchVideosData; + const invalidData = data.filter( + ({ url }) => url === undefined, + ) as FetchVideosData; + + return { + validData, + invalidData, + }; +}; + +const TransferListContainer: FC = memo( + ({ data: importedFileData, onClear }) => { const setFavorite = useSetFavorite(); const setPlaylists = useSetPlaylists(); const [loading, setLoading] = useState(false); - const [importData, setImportData] = useState([ - formateToTransferList(importedFileData), - [], - ]); const { t } = useTranslation("translation", { keyPrefix: "settings.data.import", }); - const handleImportData = async () => { + const handleSubmit = async (transferListData: TransferListData) => { setLoading(true); try { - const favoritesData = importedFileData.find( - (data) => data.playlistName === "Favorites", + const [, importData] = transferListData; + const playlists = loadPlaylistsFromFileData( + importedFileData, + importData, ); - const playlistsData = importedFileData.filter( - (data) => data.playlistName !== "Favorites", - ); - - if (favoritesData) { - const promises = []; - - for (const video of favoritesData.videos) { - promises.push(getVideo(video.videoId)); - } + const favoritePlaylist = playlists.find((p) => p.title === "Favorites"); + const userPlaylists = playlists.filter((p) => p.title !== "Favorites"); - const videosData = await Promise.all(promises); - - importVideosToFavorites(videosData.map(({ video }) => video)); + if (favoritePlaylist) { + const { validData } = await getVideosData(favoritePlaylist.videos); + importVideosToFavorites(validData.map(({ video }) => video)); setFavorite(getFavoritePlaylist()); } - if (playlistsData.length > 0) { - playlistsData.map(async (playlist) => { - const promises = []; - - for (const video of playlist.videos) { - promises.push(getVideo(video.videoId)); - } - - const videosData = await Promise.all(promises); - + if (userPlaylists.length > 0) { + userPlaylists.map(async (playlist) => { + const { validData: videos } = await getVideosData(playlist.videos); importPlaylist({ - title: playlist.playlistName, - videos: videosData.map(({ video }) => video), - videoCount: videosData.length, + title: playlist.title, + videos: videos.map(({ video }) => video), + videoCount: videos.length, }); setPlaylists(getPlaylists()); }); @@ -183,9 +214,6 @@ const Test = memo( title: t("notification.title"), message: t("notification.message"), }); - - onClear(); - setImportData([[], []]); } catch (error) { notifications.show({ title: t("notification.error.title"), @@ -198,27 +226,13 @@ const Test = memo( }; return ( - + + p.title)} + handleSubmit={handleSubmit} + buttonSubmitLabel={t("button.submit")} /> - - - ); }, diff --git a/src/components/TransferList.tsx b/src/components/TransferList.tsx index bba1a44..d589add 100644 --- a/src/components/TransferList.tsx +++ b/src/components/TransferList.tsx @@ -1,5 +1,6 @@ import { ActionIcon, + Button, Checkbox, Combobox, Flex, @@ -13,13 +14,17 @@ import { useTranslation } from "react-i18next"; import classes from "./TransferList.module.css"; +export type TransferListData = [string[], string[]]; + interface TransferListProps { data: string[]; + handleSubmit: (data: TransferListData) => void; + buttonSubmitLabel: string; } export const TransferList: FC = memo( - ({ data: userData }) => { - const [data, setData] = useState<[string[], string[]]>([userData, []]); + ({ data: userData, handleSubmit, buttonSubmitLabel }) => { + const [data, setData] = useState([userData, []]); const handleTransfer = (transferFrom: number, options: string[]) => { setData((current) => { @@ -33,23 +38,30 @@ export const TransferList: FC = memo( result[transferFrom] = transferFromData; result[transferTo] = transferToData; - return result as [string[], string[]]; + return result as TransferListData; }); }; return ( - - handleTransfer(0, options)} - /> - handleTransfer(1, options)} - /> - + <> + + handleTransfer(0, options)} + /> + handleTransfer(1, options)} + /> + + + + + ); }, ); @@ -129,7 +141,6 @@ const RenderList: FC = memo( -
{items.length > 0 ? ( diff --git a/src/services/video.ts b/src/services/video.ts index 9f9fde8..61fdec1 100644 --- a/src/services/video.ts +++ b/src/services/video.ts @@ -10,12 +10,12 @@ export const getVideo = async ( ); const video: Video = await request.json(); - const adaptiveFormat = video.adaptiveFormats.find( + const adaptiveFormat = video.adaptiveFormats?.find( (format) => format.type.match(/webm/i) || format.type.match(/audio/i), ) as AdaptiveFormat; return { video, - url: adaptiveFormat.url, + url: adaptiveFormat?.url, }; };