diff --git a/components/CustomVideoSubMenu.tsx b/components/CustomVideoSubMenu.tsx new file mode 100644 index 0000000000..018ebc54e0 --- /dev/null +++ b/components/CustomVideoSubMenu.tsx @@ -0,0 +1,173 @@ +import { Dispatch, Fragment, SetStateAction, useEffect, useState } from 'react' +import { useTranslation } from 'next-i18next' +import { Dialog, Transition } from '@headlessui/react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { DownloadButton } from './DownloadBtnGtoup' + +function TrackContainer({ + track, + index, + setTracks, + title, +}: { + track: Plyr.Track + index: number + setTracks: Dispatch> + title: string +}) { + const { t } = useTranslation() + const setTrack = (value: Plyr.Track) => + setTracks(tracks => { + tracks[index] = value + return [...tracks] + }) + const delTrack = () => { + setTracks(tracks => { + tracks.splice(index, 1) + return [...tracks] + }) + } + return ( +
+
+ +
+

{title}

+
+

{t('Subtitle label')}

+ + setTrack({ + ...track, + label: value.target.value, + }) + } + /> +
+

{t('Subtitle source')}

+ + setTrack({ + ...track, + src: value.target.value, + }) + } + /> +
+
+
+ ) +} + +export default function CustomVideoSubMenu({ + tracks, + setTracks, + menuOpen, + setMenuOpen, +}: { + tracks: Plyr.Track[] + setTracks: Dispatch> + menuOpen: boolean + setMenuOpen: Dispatch> +}) { + const { t } = useTranslation() + + const closeMenu = () => setMenuOpen(false) + + const initTracks = () => JSON.parse(JSON.stringify(tracks)) + const [pendingTracks, setPendingTracks] = useState(initTracks()) + useEffect(() => { + if (menuOpen) { + setPendingTracks(initTracks()) + } + }, [tracks, menuOpen]) + + return ( + + +
+ + + + + {/* This element is to trick the browser into centering the modal contents. */} + + +
+ + {t('Customise subtitle')} + + + {t('Customise subtitle tracks of the media player.')} + + +
+ {pendingTracks.map((track, index) => ( + + ))} +
+ +
+ { + setPendingTracks([ + ...pendingTracks, + { + label: '', + src: '', + kind: 'subtitles', + }, + ]) + }} + btnColor="teal" + btnText={t('Add a track')} + btnIcon="plus" + /> + { + setTracks(pendingTracks) + closeMenu() + }} + btnColor="blue" + btnText={t('Apply tracks')} + btnIcon="download" + /> +
+
+
+
+
+
+ ) +} diff --git a/components/previews/VideoPreview.tsx b/components/previews/VideoPreview.tsx index e109a6bbdd..7ee0755a52 100644 --- a/components/previews/VideoPreview.tsx +++ b/components/previews/VideoPreview.tsx @@ -7,6 +7,7 @@ import { useTranslation } from 'next-i18next' import axios from 'axios' import toast from 'react-hot-toast' import Plyr from 'plyr-react' +import subsrt from '@openfun/subsrt' import { useAsync } from 'react-async-hook' import { useClipboard } from 'use-clipboard-copy' @@ -19,6 +20,7 @@ import { DownloadBtnContainer, PreviewContainer } from './Containers' import FourOhFour from '../FourOhFour' import Loading from '../Loading' import CustomEmbedLinkMenu from '../CustomEmbedLinkMenu' +import CustomVideoSubMenu from '../CustomVideoSubMenu' import 'plyr-react/dist/plyr.css' @@ -28,22 +30,20 @@ const VideoPlayer: FC<{ width?: number height?: number thumbnail: string - subtitle: string + tracks: Plyr.Track[] isFlv: boolean mpegts: any -}> = ({ videoName, videoUrl, width, height, thumbnail, subtitle, isFlv, mpegts }) => { - useEffect(() => { - // Really really hacky way to inject subtitles as file blobs into the video element - axios - .get(subtitle, { responseType: 'blob' }) - .then(resp => { - const track = document.querySelector('track') - track?.setAttribute('src', URL.createObjectURL(resp.data)) - }) - .catch(() => { - console.log('Could not load subtitle.') - }) +}> = ({ videoName, videoUrl, width, height, thumbnail, tracks, isFlv, mpegts }) => { + const { t } = useTranslation() + + // Store transcoded blob links + const [convertedTracks, setConvertedTracks] = useState([]) + // Common plyr configs, including the video source and plyr options + const [plyrSource, setPlyrSource] = useState({ type: 'video', sources: [] }) + const [plyrOptions, setPlyrOptions] = useState({}) + + useEffect(() => { if (isFlv) { const loadFlv = () => { // Really hacky way to get the exposed video element from Plyr @@ -54,24 +54,52 @@ const VideoPlayer: FC<{ } loadFlv() } - }, [videoUrl, isFlv, mpegts, subtitle]) + setPlyrSource({ + type: 'video', + title: videoName, + poster: thumbnail, + tracks: convertedTracks, + sources: isFlv ? [] : [{ src: videoUrl }], + }) + setPlyrOptions({ + ratio: `${width ?? 16}:${height ?? 9}`, + captions: { update: true }, + }) + }, [videoUrl, isFlv, mpegts, videoName, thumbnail, convertedTracks, width, height]) + + useAsync(async () => { + const toastId = toast.loading(t('Loading subtitles...')) + // Remove duplicated items + const noDupTracks = tracks.filter( + (value1, index, self) => + index === self.findIndex(value2 => Object.keys(value2).every(key => value2[key] == value1[key])) + ) + // Get src of transcoded subtitles and build new subtitle tracks + const convertedTrackResults = await Promise.allSettled( + noDupTracks.map(async track => { + const resp = await axios.get(track.src, { responseType: 'blob' }) + let sub: string = await resp.data.text() + if (subsrt.detect(sub) != 'vtt') { + sub = subsrt.convert(sub, { format: 'vtt' }) + } + return { ...track, src: URL.createObjectURL(new Blob([sub])) } as Plyr.Track + }) + ) + setConvertedTracks( + convertedTrackResults + .filter(track => track.status === 'fulfilled') + .map(track => (track as PromiseFulfilledResult).value) + ) + toast.dismiss(toastId) + }, [tracks]) - // Common plyr configs, including the video source and plyr options - const plyrSource = { - type: 'video', - title: videoName, - poster: thumbnail, - tracks: [{ kind: 'captions', label: videoName, src: '', default: true }], - } - const plyrOptions: Plyr.Options = { - ratio: `${width ?? 16}:${height ?? 9}`, - fullscreen: { iosNative: true }, - } - if (!isFlv) { - // If the video is not in flv format, we can use the native plyr and add sources directly with the video URL - plyrSource['sources'] = [{ src: videoUrl }] - } - return + return ( + // Add translate="no" to avoid "Uncaught DOMException: Failed to execute 'removeChild' on 'Node'" error. + // https://github.com/facebook/react/issues/11538 +
+ +
+ ) } const VideoPreview: FC<{ file: OdFileObject }> = ({ file }) => { @@ -79,16 +107,23 @@ const VideoPreview: FC<{ file: OdFileObject }> = ({ file }) => { const hashedToken = getStoredToken(asPath) const clipboard = useClipboard() - const [menuOpen, setMenuOpen] = useState(false) + const [linkMenuOpen, setLinkMenuOpen] = useState(false) + const [trackMenuOpen, setTrackMenuOpen] = useState(false) + const [tracks, setTracks] = useState(() => + Array.from(['.vtt', '.ass', '.srt']).map(suffix => ({ + kind: 'subtitles', + label: `${file.name.substring(0, file.name.lastIndexOf('.'))}${suffix}`, + src: `/api/raw/?path=${asPath.substring(0, asPath.lastIndexOf('.'))}${suffix}${ + hashedToken ? `&odpt=${hashedToken}` : '' + }`, + })) + ) + const { t } = useTranslation() // OneDrive generates thumbnails for its video files, we pick the thumbnail with the highest resolution const thumbnail = `/api/thumbnail/?path=${asPath}&size=large${hashedToken ? `&odpt=${hashedToken}` : ''}` - // We assume subtitle files are beside the video with the same name, only webvtt '.vtt' files are supported - const vtt = `${asPath.substring(0, asPath.lastIndexOf('.'))}.vtt` - const subtitle = `/api/raw/?path=${vtt}${hashedToken ? `&odpt=${hashedToken}` : ''}` - // We also format the raw video file for the in-browser player as well as all other players const videoUrl = `/api/raw/?path=${asPath}${hashedToken ? `&odpt=${hashedToken}` : ''}` @@ -105,7 +140,13 @@ const VideoPreview: FC<{ file: OdFileObject }> = ({ file }) => { return ( <> - + + {error ? ( @@ -118,7 +159,7 @@ const VideoPreview: FC<{ file: OdFileObject }> = ({ file }) => { width={file.video?.width} height={file.video?.height} thumbnail={thumbnail} - subtitle={subtitle} + tracks={tracks} isFlv={isFlv} mpegts={mpegts} /> @@ -143,11 +184,17 @@ const VideoPreview: FC<{ file: OdFileObject }> = ({ file }) => { btnIcon="copy" /> setMenuOpen(true)} + onClickCallback={() => setLinkMenuOpen(true)} btnColor="teal" btnText={t('Customise link')} btnIcon="pen" /> + setTrackMenuOpen(true)} + btnColor="blue" + btnText={t('Customise subtitle')} + btnIcon="pen" + /> window.open(`iina://weblink?url=${getBaseUrl()}${videoUrl}`)} diff --git a/package.json b/package.json index 7e8c2af6df..b5ec2dd367 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@fortawesome/free-solid-svg-icons": "^5.15.3", "@fortawesome/react-fontawesome": "^0.1.14", "@headlessui/react": "^1.4.0", + "@openfun/subsrt": "^1.0.5", "@tailwindcss/line-clamp": "^0.3.1", "awesome-debounce-promise": "^2.1.0", "axios": "^0.25.0", diff --git a/pages/_app.tsx b/pages/_app.tsx index a9599b4cbc..14d7418f37 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -21,6 +21,7 @@ import { faEnvelope, faFlag, faCheckCircle, + faTimesCircle, } from '@fortawesome/free-regular-svg-icons' import { faSearch, @@ -110,6 +111,7 @@ library.add( faThList, faLanguage, faPen, + faTimesCircle, ...iconList ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 57306fef54..47b6d39611 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: 5.3 +lockfileVersion: 5.4 specifiers: '@fortawesome/fontawesome-svg-core': ^1.2.35 @@ -7,6 +7,7 @@ specifiers: '@fortawesome/free-solid-svg-icons': ^5.15.3 '@fortawesome/react-fontawesome': ^0.1.14 '@headlessui/react': ^1.4.0 + '@openfun/subsrt': ^1.0.5 '@tailwindcss/line-clamp': ^0.3.1 '@types/cors': ^2.8.12 '@types/crypto-js': ^4.0.2 @@ -66,8 +67,9 @@ dependencies: '@fortawesome/free-brands-svg-icons': 5.15.4 '@fortawesome/free-regular-svg-icons': 5.15.4 '@fortawesome/free-solid-svg-icons': 5.15.4 - '@fortawesome/react-fontawesome': 0.1.17_f515edce028694561ceb456e3dba224c - '@headlessui/react': 1.4.3_react-dom@17.0.2+react@17.0.2 + '@fortawesome/react-fontawesome': 0.1.17_6uk63tqcq2kfmhhlivxd3orcjq + '@headlessui/react': 1.4.3_sfoxds7t5ydpegc3knd667wn6m + '@openfun/subsrt': 1.0.5 '@tailwindcss/line-clamp': 0.3.1_tailwindcss@3.0.18 awesome-debounce-promise: 2.1.0 axios: 0.25.0 @@ -79,20 +81,20 @@ dependencies: ioredis: 4.28.3 jszip: 3.7.1 mpegts.js: 1.6.10 - next: 12.0.10_react-dom@17.0.2+react@17.0.2 - next-i18next: 10.2.0_61390be992b634a688f7c2555547b55b + next: 12.0.10_sfoxds7t5ydpegc3knd667wn6m + next-i18next: 10.2.0_me4qx2mswy2knchxyjkvkr5vlm nextjs-progressbar: 0.0.13_next@12.0.10+react@17.0.2 - plyr-react: 3.2.1_react-dom@17.0.2+react@17.0.2 + plyr-react: 3.2.1_sfoxds7t5ydpegc3knd667wn6m preview-office-docs: 1.0.2_react@17.0.2 react: 17.0.2 react-async-hook: 4.0.0_react@17.0.2 - react-audio-player: 0.17.0_react-dom@17.0.2+react@17.0.2 + react-audio-player: 0.17.0_sfoxds7t5ydpegc3knd667wn6m react-cookie: 4.1.1_react@17.0.2 react-copy-to-clipboard: 5.0.4_react@17.0.2 react-dom: 17.0.2_react@17.0.2 - react-hot-toast: 2.2.0_6bba596ee6fb84e656d75c53902ecf01 - react-hotkeys-hook: 3.4.4_react-dom@17.0.2+react@17.0.2 - react-markdown: 8.0.0_b08e3c15324cbe90a6ff8fcd416c932c + react-hot-toast: 2.2.0_no5fs3xg7ocomvwxlrjzalwpae + react-hotkeys-hook: 3.4.4_sfoxds7t5ydpegc3knd667wn6m + react-markdown: 8.0.0_wchdyfjsjs7jbjx7r7guc3etfq react-reader: 0.20.5_react@17.0.2 react-syntax-highlighter: 15.4.5_react@17.0.2 react-use-system-theme: 1.1.1_react@17.0.2 @@ -115,13 +117,13 @@ devDependencies: '@types/react-syntax-highlighter': 13.5.2 autoprefixer: 10.4.2_postcss@8.4.6 eslint: 8.8.0 - eslint-config-next: 12.0.10_9534215cc73b6f260bf33f1b86e3ae0e + eslint-config-next: 12.0.10_su2ccxghhnxsmc7th4nyny5oby eslint-config-prettier: 8.3.0_eslint@8.8.0 i18next-parser: 5.4.0 postcss: 8.4.6 prettier: 2.5.1 prettier-plugin-tailwindcss: 0.1.4_prettier@2.5.1 - tailwindcss: 3.0.18_833e1018ad0d7954aa80c53675939269 + tailwindcss: 3.0.18_qm7bagfnbv4vjkuayu3hle4sne typescript: 4.5.5 packages: @@ -216,7 +218,7 @@ packages: '@fortawesome/fontawesome-common-types': 0.2.36 dev: false - /@fortawesome/react-fontawesome/0.1.17_f515edce028694561ceb456e3dba224c: + /@fortawesome/react-fontawesome/0.1.17_6uk63tqcq2kfmhhlivxd3orcjq: resolution: {integrity: sha512-dX43Z5IvMaW7fwzU8farosYjKNGfRb2HB/DgjVBHeJZ/NSnuuaujPPx0YOdcAq+n3mqn70tyCde2HM1mqbhiuw==} peerDependencies: '@fortawesome/fontawesome-svg-core': ~1 || >=1.3.0-beta1 @@ -227,7 +229,7 @@ packages: react: 17.0.2 dev: false - /@headlessui/react/1.4.3_react-dom@17.0.2+react@17.0.2: + /@headlessui/react/1.4.3_sfoxds7t5ydpegc3knd667wn6m: resolution: {integrity: sha512-n2IQkaaw0aAAlQS5MEXsM4uRK+w18CrM72EqnGRl/UBOQeQajad8oiKXR9Nk15jOzTFQjpxzrZMf1NxHidFBiw==} engines: {node: '>=10'} peerDependencies: @@ -383,6 +385,11 @@ packages: fastq: 1.13.0 dev: true + /@openfun/subsrt/1.0.5: + resolution: {integrity: sha512-XS1x7KZPdpWqkID115slsF1HXuPXpmzzG01IpA//8g4iwP3GmPcAWbSnWG0XxDCxgksWylpftlRY4dGM5jzslg==} + hasBin: true + dev: false + /@rushstack/eslint-patch/1.1.0: resolution: {integrity: sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A==} dev: true @@ -392,7 +399,7 @@ packages: peerDependencies: tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1' dependencies: - tailwindcss: 3.0.18_833e1018ad0d7954aa80c53675939269 + tailwindcss: 3.0.18_qm7bagfnbv4vjkuayu3hle4sne dev: false /@types/cookie/0.3.3: @@ -529,7 +536,7 @@ packages: resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} dev: false - /@typescript-eslint/parser/5.10.2_eslint@8.8.0+typescript@4.5.5: + /@typescript-eslint/parser/5.10.2_txwvkng2juu2h6yeaibqmql3uy: resolution: {integrity: sha512-JaNYGkaQVhP6HNF+lkdOr2cAs2wdSZBoalE22uYWq8IEv/OVH0RksSGydk+sW8cLoSeYmC+OHvRyv2i4AQ7Czg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -828,6 +835,8 @@ packages: fs-extra: 8.1.0 heimdalljs-logger: 0.1.10 symlink-or-copy: 1.3.1 + transitivePeerDependencies: + - supports-color dev: true /broccoli-plugin/4.0.7: @@ -841,6 +850,8 @@ packages: quick-temp: 0.1.8 rimraf: 3.0.2 symlink-or-copy: 1.3.1 + transitivePeerDependencies: + - supports-color dev: true /browserslist/4.19.1: @@ -1161,12 +1172,22 @@ packages: /debug/2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true dependencies: ms: 2.0.0 dev: true /debug/3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true dependencies: ms: 2.1.3 dev: true @@ -1425,7 +1446,7 @@ packages: engines: {node: '>=12'} dev: false - /eslint-config-next/12.0.10_9534215cc73b6f260bf33f1b86e3ae0e: + /eslint-config-next/12.0.10_su2ccxghhnxsmc7th4nyny5oby: resolution: {integrity: sha512-l1er6mwSo1bltjLwmd71p5BdT6k/NQxV1n4lKZI6xt3MDMrq7ChUBr+EecxOry8GC/rCRUtPpH8Ygs0BJc5YLg==} peerDependencies: eslint: ^7.23.0 || ^8.0.0 @@ -1437,17 +1458,18 @@ packages: dependencies: '@next/eslint-plugin-next': 12.0.10 '@rushstack/eslint-patch': 1.1.0 - '@typescript-eslint/parser': 5.10.2_eslint@8.8.0+typescript@4.5.5 + '@typescript-eslint/parser': 5.10.2_txwvkng2juu2h6yeaibqmql3uy eslint: 8.8.0 eslint-import-resolver-node: 0.3.6 - eslint-import-resolver-typescript: 2.5.0_392f898cec7735a5f7a99430cbc0b4f4 - eslint-plugin-import: 2.25.4_eslint@8.8.0 + eslint-import-resolver-typescript: 2.5.0_hexytdhmo422l55jsqymxqfu6q + eslint-plugin-import: 2.25.4_4lr7rwbawkv3f23yywoq432o7m eslint-plugin-jsx-a11y: 6.5.1_eslint@8.8.0 eslint-plugin-react: 7.28.0_eslint@8.8.0 eslint-plugin-react-hooks: 4.3.0_eslint@8.8.0 - next: 12.0.10_react-dom@17.0.2+react@17.0.2 + next: 12.0.10_sfoxds7t5ydpegc3knd667wn6m typescript: 4.5.5 transitivePeerDependencies: + - eslint-import-resolver-webpack - supports-color dev: true @@ -1465,9 +1487,11 @@ packages: dependencies: debug: 3.2.7 resolve: 1.22.0 + transitivePeerDependencies: + - supports-color dev: true - /eslint-import-resolver-typescript/2.5.0_392f898cec7735a5f7a99430cbc0b4f4: + /eslint-import-resolver-typescript/2.5.0_hexytdhmo422l55jsqymxqfu6q: resolution: {integrity: sha512-qZ6e5CFr+I7K4VVhQu3M/9xGv9/YmwsEXrsm3nimw8vWaVHRDrQRp26BgCypTxBp3vUp4o5aVEJRiy0F2DFddQ==} engines: {node: '>=4'} peerDependencies: @@ -1476,7 +1500,7 @@ packages: dependencies: debug: 4.3.3 eslint: 8.8.0 - eslint-plugin-import: 2.25.4_eslint@8.8.0 + eslint-plugin-import: 2.25.4_4lr7rwbawkv3f23yywoq432o7m glob: 7.2.0 is-glob: 4.0.3 resolve: 1.22.0 @@ -1485,27 +1509,51 @@ packages: - supports-color dev: true - /eslint-module-utils/2.7.3: + /eslint-module-utils/2.7.3_qvuibbemuhbx2wr7dr4uz5jkrm: resolution: {integrity: sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==} engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true dependencies: + '@typescript-eslint/parser': 5.10.2_txwvkng2juu2h6yeaibqmql3uy debug: 3.2.7 + eslint-import-resolver-node: 0.3.6 + eslint-import-resolver-typescript: 2.5.0_hexytdhmo422l55jsqymxqfu6q find-up: 2.1.0 + transitivePeerDependencies: + - supports-color dev: true - /eslint-plugin-import/2.25.4_eslint@8.8.0: + /eslint-plugin-import/2.25.4_4lr7rwbawkv3f23yywoq432o7m: resolution: {integrity: sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==} engines: {node: '>=4'} peerDependencies: + '@typescript-eslint/parser': '*' eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true dependencies: + '@typescript-eslint/parser': 5.10.2_txwvkng2juu2h6yeaibqmql3uy array-includes: 3.1.4 array.prototype.flat: 1.2.5 debug: 2.6.9 doctrine: 2.1.0 eslint: 8.8.0 eslint-import-resolver-node: 0.3.6 - eslint-module-utils: 2.7.3 + eslint-module-utils: 2.7.3_qvuibbemuhbx2wr7dr4uz5jkrm has: 1.0.3 is-core-module: 2.8.1 is-glob: 4.0.3 @@ -1513,6 +1561,10 @@ packages: object.values: 1.1.5 resolve: 1.22.0 tsconfig-paths: 3.12.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color dev: true /eslint-plugin-jsx-a11y/6.5.1_eslint@8.8.0: @@ -1809,6 +1861,8 @@ packages: fs-extra: 8.1.0 fs-tree-diff: 2.0.1 walk-sync: 2.2.0 + transitivePeerDependencies: + - supports-color dev: true /fs-mkdirp-stream/1.0.0: @@ -1828,6 +1882,8 @@ packages: object-assign: 4.1.1 path-posix: 1.0.0 symlink-or-copy: 1.3.1 + transitivePeerDependencies: + - supports-color dev: true /fs.realpath/1.0.0: @@ -2106,6 +2162,8 @@ packages: dependencies: debug: 2.6.9 heimdalljs: 0.2.6 + transitivePeerDependencies: + - supports-color dev: true /heimdalljs/0.2.6: @@ -2178,6 +2236,8 @@ packages: vinyl: 2.2.1 vinyl-fs: 3.0.3 vue-template-compiler: 2.6.14 + transitivePeerDependencies: + - supports-color dev: true /i18next/21.6.10: @@ -3107,7 +3167,7 @@ packages: resolution: {integrity: sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=} dev: true - /next-i18next/10.2.0_61390be992b634a688f7c2555547b55b: + /next-i18next/10.2.0_me4qx2mswy2knchxyjkvkr5vlm: resolution: {integrity: sha512-HTdXy8U8Ko8oxs+VAog0CQC7Ap/XyvRzwm/g5tF/CzqJ4dSCyWuLmvPjA9BpQ3VEIkxzCA43w1W75CnARRn1lw==} engines: {node: '>=12'} peerDependencies: @@ -3120,9 +3180,9 @@ packages: hoist-non-react-statics: 3.3.2 i18next: 21.6.10 i18next-fs-backend: 1.1.4 - next: 12.0.10_react-dom@17.0.2+react@17.0.2 + next: 12.0.10_sfoxds7t5ydpegc3knd667wn6m react: 17.0.2 - react-i18next: 11.15.3_f9635d5d32d7f130f7fe8a33cbdb7283 + react-i18next: 11.15.3_7frv2xjs27ytb576riz4xw3sqm transitivePeerDependencies: - react-dom - react-native @@ -3132,7 +3192,7 @@ packages: resolution: {integrity: sha1-yobR/ogoFpsBICCOPchCS524NCw=} dev: false - /next/12.0.10_react-dom@17.0.2+react@17.0.2: + /next/12.0.10_sfoxds7t5ydpegc3knd667wn6m: resolution: {integrity: sha512-1y3PpGzpb/EZzz1jgne+JfZXKAVJUjYXwxzrADf/LWN+8yi9o79vMLXpW3mevvCHkEF2sBnIdjzNn16TJrINUw==} engines: {node: '>=12.22.0'} hasBin: true @@ -3180,7 +3240,7 @@ packages: next: '>= 6.0.0 <=12.x.x' react: '>= 16.0.0 <= 17.x.x' dependencies: - next: 12.0.10_react-dom@17.0.2+react@17.0.2 + next: 12.0.10_sfoxds7t5ydpegc3knd667wn6m nprogress: 0.2.0 prop-types: 15.8.1 react: 17.0.2 @@ -3428,7 +3488,7 @@ packages: engines: {node: '>=8.6'} dev: true - /plyr-react/3.2.1_react-dom@17.0.2+react@17.0.2: + /plyr-react/3.2.1_sfoxds7t5ydpegc3knd667wn6m: resolution: {integrity: sha512-ZXMA+d837mZCkb68oTXso6rH8O+B2+A7t8ux1fdU+kFSUh1NZa1/iuItItFgScmVC6FoO3DNj+FkZP2aikxoKg==} engines: {node: '>=10', npm: '>=6'} peerDependencies: @@ -3625,7 +3685,7 @@ packages: react: 17.0.2 dev: false - /react-audio-player/0.17.0_react-dom@17.0.2+react@17.0.2: + /react-audio-player/0.17.0_sfoxds7t5ydpegc3knd667wn6m: resolution: {integrity: sha512-aCZgusPxA9HK7rLZcTdhTbBH9l6do9vn3NorgoDZRxRxJlOy9uZWzPaKjd7QdcuP2vXpxGA/61JMnnOEY7NXeA==} peerDependencies: react: '>=16' @@ -3668,7 +3728,7 @@ packages: scheduler: 0.20.2 dev: false - /react-hot-toast/2.2.0_6bba596ee6fb84e656d75c53902ecf01: + /react-hot-toast/2.2.0_no5fs3xg7ocomvwxlrjzalwpae: resolution: {integrity: sha512-248rXw13uhf/6TNDVzagX+y7R8J183rp7MwUMNkcrBRyHj/jWOggfXTGlM8zAOuh701WyVW+eUaWG2LeSufX9g==} engines: {node: '>=10'} peerDependencies: @@ -3682,7 +3742,7 @@ packages: - csstype dev: false - /react-hotkeys-hook/3.4.4_react-dom@17.0.2+react@17.0.2: + /react-hotkeys-hook/3.4.4_sfoxds7t5ydpegc3knd667wn6m: resolution: {integrity: sha512-vaORq07rWgmuF3owWRhgFV/3VL8/l2q9lz0WyVEddJnWTtKW+AOgU5YgYKuwN6h6h7bCcLG3MFsJIjCrM/5DvQ==} peerDependencies: react: '>=16.8.1' @@ -3693,7 +3753,7 @@ packages: react-dom: 17.0.2_react@17.0.2 dev: false - /react-i18next/11.15.3_f9635d5d32d7f130f7fe8a33cbdb7283: + /react-i18next/11.15.3_7frv2xjs27ytb576riz4xw3sqm: resolution: {integrity: sha512-RSUEM4So3Tu2JHV0JsZ5Yje+4nz66YViMfPZoywxOy0xyn3L7tE2CHvJ7Y9LUsrTU7vGmZ5bwb8PpjnkatdIxg==} peerDependencies: i18next: '>= 19.0.0' @@ -3721,7 +3781,7 @@ packages: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} dev: false - /react-markdown/8.0.0_b08e3c15324cbe90a6ff8fcd416c932c: + /react-markdown/8.0.0_wchdyfjsjs7jbjx7r7guc3etfq: resolution: {integrity: sha512-qbrWpLny6Ef2xHqnYqtot948LXP+4FtC+MWIuaN1kvSnowM+r1qEeEHpSaU0TDBOisQuj+Qe6eFY15cNL3gLAw==} peerDependencies: '@types/react': '>=16' @@ -4210,7 +4270,7 @@ packages: resolution: {integrity: sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA==} dev: true - /tailwindcss/3.0.18_833e1018ad0d7954aa80c53675939269: + /tailwindcss/3.0.18_qm7bagfnbv4vjkuayu3hle4sne: resolution: {integrity: sha512-ihPTpEyA5ANgZbwKlgrbfnzOp9R5vDHFWmqxB1PT8NwOGCOFVVMl+Ps1cQQ369acaqqf1BEF77roCwK0lvNmTw==} engines: {node: '>=12.13.0'} hasBin: true diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 853324dc0f..cfb6692864 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -11,6 +11,8 @@ "Acquired access_token: ": "Acquired access_token: ", "Acquired refresh_token: ": "Acquired refresh_token: ", "Actions": "Actions", + "Add a track": "Add a track", + "Apply tracks": "Apply tracks", "Authorisation is required as no valid <2>access_token or <5>refresh_token is present on this deployed instance. Check the following configurations before proceeding with authorising onedrive-vercel-index with your own Microsoft account.": "Authorisation is required as no valid <2>access_token or <5>refresh_token is present on this deployed instance. Check the following configurations before proceeding with authorising onedrive-vercel-index with your own Microsoft account.", "Cancel": "Cancel", "Cannot preview {{path}}": "Cannot preview {{path}}", @@ -29,6 +31,8 @@ "Copy the permalink to the file to the clipboard": "Copy the permalink to the file to the clipboard", "Customise direct link": "Customise direct link", "Customise link": "Customise link", + "Customise subtitle": "Customise subtitle", + "Customise subtitle tracks of the media player.": "Customise subtitle tracks of the media player.", "Customised": "Customised", "Customised and encoded": "Customised and encoded", "Default": "Default", @@ -72,6 +76,7 @@ "Loading EPUB ...": "Loading EPUB ...", "Loading file content...": "Loading file content...", "Loading FLV extension...": "Loading FLV extension...", + "Loading subtitles...": "Loading subtitles...", "Logout": "Logout", "MIME type": "MIME type", "Name": "Name", @@ -103,6 +108,8 @@ "Store tokens": "Store tokens", "Stored! Going home...": "Stored! Going home...", "Storing tokens": "Storing tokens", + "Subtitle label": "Subtitle label", + "Subtitle source": "Subtitle source", "Success! The API returned what we needed.": "Success! The API returned what we needed.", "The authorisation code extracted is:": "The authorisation code extracted is:", "The OAuth link for getting the authorisation code has been created. Click on the link above to get the <2>authorisation code. Your browser willopen a new tab to Microsoft's account login page. After logging in and authenticating with your Microsoft account, you will be redirected to a blank page on localhost. Paste <6>the entire redirected URL down below.": "The OAuth link for getting the authorisation code has been created. Click on the link above to get the <2>authorisation code. Your browser willopen a new tab to Microsoft's account login page. After logging in and authenticating with your Microsoft account, you will be redirected to a blank page on localhost. Paste <6>the entire redirected URL down below.", diff --git a/public/locales/zh-CN/common.json b/public/locales/zh-CN/common.json index da9e4d1da9..83f5d86056 100644 --- a/public/locales/zh-CN/common.json +++ b/public/locales/zh-CN/common.json @@ -9,6 +9,8 @@ "Acquired access_token: ": "获取 access_token", "Acquired refresh_token: ": "获取 refresh_token", "Actions": "操作", + "Add a track": "添加轨道", + "Apply tracks": "应用轨道", "Authorisation is required as no valid <2>access_token or <5>refresh_token is present on this deployed instance. Check the following configurations before proceeding with authorising onedrive-vercel-index with your own Microsoft account.": "本项目还没有设置有效的 <2>access_token 和 <5>refresh_token,需要进行授权。在继续对 onedrive-vercel-index 授权你的 Microsoft 帐号前,请检查一下下方的配置信息。", "Cancel": "取消", "Cannot preview {{path}}": "无法预览 {{path}}", @@ -27,6 +29,8 @@ "Copy the permalink to the file to the clipboard": "复制文件永久链接到剪贴板", "Customise direct link": "自定义文件直链", "Customise link": "自定义直链", + "Customise subtitle": "自定义字幕", + "Customise subtitle tracks of the media player.": "自定义媒体播放器的字幕轨道。", "Customised": "自定义链接", "Customised and encoded": "URL 编码的自定义链接", "Default": "默认", @@ -70,6 +74,7 @@ "Loading EPUB ...": "加载 EPUB 中…", "Loading file content...": "加载文件内容中…", "Loading FLV extension...": "加载 FLV 扩展中…", + "Loading subtitles...": "加载字幕中…", "Logout": "注销", "MIME type": "MIME 类型", "Name": "文件名", @@ -99,6 +104,8 @@ "Store tokens": "储存 tokens", "Stored! Going home...": "已存储!正在返回首页…", "Storing tokens": "正在存储 token…", + "Subtitle label": "字幕标签", + "Subtitle source": "字幕源", "Success! The API returned what we needed.": "成功!需要的 token 已被返回。", "The authorisation code extracted is:": "提取出的授权码为:", "The OAuth link for getting the authorisation code has been created. Click on the link above to get the <2>authorisation code. Your browser willopen a new tab to Microsoft's account login page. After logging in and authenticating with your Microsoft account, you will be redirected to a blank page on localhost. Paste <6>the entire redirected URL down below.": "创建出的这个 OAuth 链接是用来获取授权码的。点击上方链接以获取所需的 <2>授权码。你的浏览器将在新的标签页打开 Microsoft 帐号登录页面。在登录并验证你的 Microsoft 帐号之后,你将被重定向到一个域名为 localhost 的空白页面。请将<6>完整的重定向后的 URL 整体复制粘贴到下方。",