Compare commits

...

6 Commits

Author SHA1 Message Date
MerlinScheurer
28f2fbd6a7 Fix remove theater mode localstorage flag 2025-06-22 12:14:32 +02:00
MerlinScheurer
d19190bf6a Add theater mode to normal video player 2025-06-22 12:10:29 +02:00
MerlinScheurer
b14309daeb Fix do not send referrer when opening youtube, sponsorblock or returndislite links 2025-06-22 11:43:32 +02:00
Craig Alexander
990cb9aaec
Move npm install into its own docker stage (#999) 2025-06-20 18:33:43 +02:00
Craig Alexander
bb4e5ecb50
Fix not found message showing as API is loading (#995) 2025-06-15 11:19:14 +02:00
MerlinScheurer
ff94c324b3 Refac extract loading indicator into its own component 2025-06-13 20:10:57 +02:00
14 changed files with 242 additions and 123 deletions

2
.gitignore vendored
View File

@ -12,3 +12,5 @@ backend/.env
# JavaScript stuff # JavaScript stuff
node_modules node_modules
.editorconfig

View File

@ -1,14 +1,18 @@
# multi stage to build tube archivist # multi stage to build tube archivist
# build python wheel, download and extract ffmpeg, copy into final image # build python wheel, download and extract ffmpeg, copy into final image
FROM node:lts-alpine AS npm-builder
COPY frontend/package.json frontend/package-lock.json /
RUN npm i
FROM node:lts-alpine AS node-builder FROM node:lts-alpine AS node-builder
# RUN npm config set registry https://registry.npmjs.org/ # RUN npm config set registry https://registry.npmjs.org/
COPY --from=npm-builder ./node_modules /frontend/node_modules
COPY ./frontend /frontend COPY ./frontend /frontend
WORKDIR /frontend WORKDIR /frontend
RUN npm i
RUN npm run build:deploy RUN npm run build:deploy
WORKDIR / WORKDIR /
@ -54,9 +58,9 @@ RUN apt-get clean && apt-get -y update && apt-get -y install --no-install-recomm
# install debug tools for testing environment # install debug tools for testing environment
RUN if [ "$INSTALL_DEBUG" ] ; then \ RUN if [ "$INSTALL_DEBUG" ] ; then \
apt-get -y update && apt-get -y install --no-install-recommends \ apt-get -y update && apt-get -y install --no-install-recommends \
vim htop bmon net-tools iputils-ping procps lsof \ vim htop bmon net-tools iputils-ping procps lsof \
&& pip install --user ipython pytest pytest-django \ && pip install --user ipython pytest pytest-django \
; fi ; fi
# make folders # make folders

View File

@ -47,7 +47,11 @@ const DownloadListItem = ({ download, setRefresh }: DownloadListItemProps) => {
{!download.channel_indexed && <span>{download.channel_name}</span>} {!download.channel_indexed && <span>{download.channel_name}</span>}
<a href={`https://www.youtube.com/watch?v=${download.youtube_id}`} target="_blank"> <a
href={`https://www.youtube.com/watch?v=${download.youtube_id}`}
target="_blank"
rel="noopener noreferrer"
>
<h3>{download.title}</h3> <h3>{download.title}</h3>
</a> </a>
</div> </div>

View File

@ -1,4 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import LoadingIndicator from './LoadingIndicator';
type InputTextProps = { type InputTextProps = {
type: 'text' | 'number'; type: 'text' | 'number';
@ -51,13 +52,7 @@ const InputConfig = ({ type, name, value, setValue, oldValue, updateCallback }:
</> </>
)} )}
{oldValue !== null && <button onClick={() => handleUpdate(name, null)}>reset</button>} {oldValue !== null && <button onClick={() => handleUpdate(name, null)}>reset</button>}
{loading && ( {loading && <LoadingIndicator />}
<>
<div className="lds-ring" style={{ color: 'var(--accent-font-dark)' }}>
<div />
</div>
</>
)}
{success && <span></span>} {success && <span></span>}
</div> </div>
</div> </div>

View File

@ -0,0 +1,9 @@
const LoadingIndicator = () => {
return (
<div className="lds-ring" style={{ color: 'var(--accent-font-dark)' }}>
<div />
</div>
);
};
export default LoadingIndicator;

View File

@ -140,6 +140,8 @@ const VideoPlayer = ({
const [showHelpDialog, setShowHelpDialog] = useState(false); const [showHelpDialog, setShowHelpDialog] = useState(false);
const [showInfoDialog, setShowInfoDialog] = useState(false); const [showInfoDialog, setShowInfoDialog] = useState(false);
const [infoDialogContent, setInfoDialogContent] = useState(''); const [infoDialogContent, setInfoDialogContent] = useState('');
const [isTheaterMode, setIsTheaterMode] = useState(false);
const [theaterModeKeyPressed, setTheaterModeKeyPressed] = useState(false);
const questionmarkPressed = useKeyPress('?'); const questionmarkPressed = useKeyPress('?');
const mutePressed = useKeyPress('m'); const mutePressed = useKeyPress('m');
@ -151,6 +153,8 @@ const VideoPlayer = ({
const arrowRightPressed = useKeyPress('ArrowRight'); const arrowRightPressed = useKeyPress('ArrowRight');
const arrowLeftPressed = useKeyPress('ArrowLeft'); const arrowLeftPressed = useKeyPress('ArrowLeft');
const pPausedPressed = useKeyPress('p'); const pPausedPressed = useKeyPress('p');
const theaterModePressed = useKeyPress('t');
const escapePressed = useKeyPress('Escape');
const videoId = video.youtube_id; const videoId = video.youtube_id;
const videoUrl = video.media_url; const videoUrl = video.media_url;
@ -345,10 +349,42 @@ const VideoPlayer = ({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [questionmarkPressed]); }, [questionmarkPressed]);
useEffect(() => {
if (embed) {
return;
}
if (theaterModePressed && !theaterModeKeyPressed) {
setTheaterModeKeyPressed(true);
const newTheaterMode = !isTheaterMode;
setIsTheaterMode(newTheaterMode);
infoDialog(newTheaterMode ? 'Theater mode' : 'Normal mode');
} else if (!theaterModePressed) {
setTheaterModeKeyPressed(false);
}
}, [theaterModePressed, isTheaterMode, theaterModeKeyPressed]);
useEffect(() => {
if (embed) {
return;
}
if (escapePressed && isTheaterMode) {
setIsTheaterMode(false);
infoDialog('Normal mode');
}
}, [escapePressed, isTheaterMode]);
return ( return (
<> <>
<div id="player" className={embed ? '' : 'player-wrapper'}> <div
<div className={embed ? '' : 'video-main'}> id="player"
className={embed ? '' : `player-wrapper ${isTheaterMode ? 'theater-mode' : ''}`}
>
<div className={embed ? '' : `video-main ${isTheaterMode ? 'theater-mode' : ''}`}>
<video <video
ref={videoRef} ref={videoRef}
key={`${getApiUrl()}${videoUrl}`} key={`${getApiUrl()}${videoUrl}`}
@ -423,6 +459,18 @@ const VideoPlayer = ({
<td>Toggle fullscreen</td> <td>Toggle fullscreen</td>
<td>f</td> <td>f</td>
</tr> </tr>
{!embed && (
<>
<tr>
<td>Toggle theater mode</td>
<td>t</td>
</tr>
<tr>
<td>Exit theater mode</td>
<td>Esc</td>
</tr>
</>
)}
<tr> <tr>
<td>Toggle subtitles (if available)</td> <td>Toggle subtitles (if available)</td>
<td>c</td> <td>c</td>
@ -467,11 +515,19 @@ const VideoPlayer = ({
<h4> <h4>
This video doesn't have any sponsor segments added. To add a segment go to{' '} This video doesn't have any sponsor segments added. To add a segment go to{' '}
<u> <u>
<a href={`https://www.youtube.com/watch?v=${videoId}`}>this video on YouTube</a> <a
href={`https://www.youtube.com/watch?v=${videoId}`}
target="_blank"
rel="noopener noreferrer"
>
this video on YouTube
</a>
</u>{' '} </u>{' '}
and add a segment using the{' '} and add a segment using the{' '}
<u> <u>
<a href="https://sponsor.ajay.app/">SponsorBlock</a> <a href="https://sponsor.ajay.app/" target="_blank" rel="noopener noreferrer">
SponsorBlock
</a>
</u>{' '} </u>{' '}
extension. extension.
</h4> </h4>
@ -480,11 +536,19 @@ const VideoPlayer = ({
<h4> <h4>
This video has unlocked sponsor segments. Go to{' '} This video has unlocked sponsor segments. Go to{' '}
<u> <u>
<a href={`https://www.youtube.com/watch?v=${videoId}`}>this video on YouTube</a> <a
href={`https://www.youtube.com/watch?v=${videoId}`}
target="_blank"
rel="noopener noreferrer"
>
this video on YouTube
</a>
</u>{' '} </u>{' '}
and vote on the segments using the{' '} and vote on the segments using the{' '}
<u> <u>
<a href="https://sponsor.ajay.app/">SponsorBlock</a> <a href="https://sponsor.ajay.app/" target="_blank" rel="noopener noreferrer">
SponsorBlock
</a>
</u>{' '} </u>{' '}
extension. extension.
</h4> </h4>

View File

@ -1,6 +1,7 @@
import iconUnseen from '/img/icon-unseen.svg'; import iconUnseen from '/img/icon-unseen.svg';
import iconSeen from '/img/icon-seen.svg'; import iconSeen from '/img/icon-seen.svg';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import LoadingIndicator from './LoadingIndicator';
type WatchedCheckBoxProps = { type WatchedCheckBoxProps = {
watched: boolean; watched: boolean;
@ -32,9 +33,7 @@ const WatchedCheckBox = ({ watched, onClick, onDone }: WatchedCheckBoxProps) =>
<> <>
{loading && ( {loading && (
<> <>
<div className="lds-ring" style={{ color: 'var(--accent-font-dark)' }}> <LoadingIndicator />
<div />
</div>
</> </>
)} )}
{!loading && watched && ( {!loading && watched && (

View File

@ -130,7 +130,11 @@ const ChannelAbout = () => {
{channel.channel_active && ( {channel.channel_active && (
<p> <p>
Youtube:{' '} Youtube:{' '}
<a href={`https://www.youtube.com/channel/${channel.channel_id}`} target="_blank"> <a
href={`https://www.youtube.com/channel/${channel.channel_id}`}
target="_blank"
rel="noopener noreferrer"
>
Active Active
</a> </a>
</p> </p>
@ -316,7 +320,7 @@ const ChannelAbout = () => {
<div> <div>
<p> <p>
Overwrite{' '} Overwrite{' '}
<a href="https://sponsor.ajay.app/" target="_blank"> <a href="https://sponsor.ajay.app/" target="_blank" rel="noopener noreferrer">
SponsorBlock SponsorBlock
</a> </a>
</p> </p>

View File

@ -97,104 +97,97 @@ const ChannelVideo = ({ videoType }: ChannelVideoProps) => {
videoId, videoId,
]); ]);
if (!channel) {
return (
<div className="boxed-content">
<br />
<h2>Channel {channelId} not found!</h2>
</div>
);
}
return ( return (
<> channel && (
<title>{`TA | Channel: ${channel.channel_name}`}</title> <>
<ScrollToTopOnNavigate /> <title>{`TA | Channel: ${channel.channel_name}`}</title>
<div className="boxed-content"> <ScrollToTopOnNavigate />
<div className="info-box info-box-2"> <div className="boxed-content">
<ChannelOverview <div className="info-box info-box-2">
channelId={channel.channel_id} <ChannelOverview
channelname={channel.channel_name} channelId={channel.channel_id}
channelSubs={channel.channel_subs} channelname={channel.channel_name}
channelSubscribed={channel.channel_subscribed} channelSubs={channel.channel_subs}
channelThumbUrl={channel.channel_thumb_url} channelSubscribed={channel.channel_subscribed}
setRefresh={setRefresh} channelThumbUrl={channel.channel_thumb_url}
/> setRefresh={setRefresh}
<div className="info-box-item"> />
{videoAggs && ( <div className="info-box-item">
<> {videoAggs && (
<p> <>
{videoAggs.total_items.value} videos <span className="space-carrot">|</span>{' '} <p>
{videoAggs.total_duration.value_str} playback{' '} {videoAggs.total_items.value} videos <span className="space-carrot">|</span>{' '}
<span className="space-carrot">|</span> Total size{' '} {videoAggs.total_duration.value_str} playback{' '}
{humanFileSize(videoAggs.total_size.value, useSiUnits)} <span className="space-carrot">|</span> Total size{' '}
</p> {humanFileSize(videoAggs.total_size.value, useSiUnits)}
<div className="button-box"> </p>
<Button <div className="button-box">
label="Mark as watched" <Button
id="watched-button" label="Mark as watched"
type="button" id="watched-button"
title={`Mark all videos from ${channel.channel_name} as watched`} type="button"
onClick={async () => { title={`Mark all videos from ${channel.channel_name} as watched`}
await updateWatchedState({ onClick={async () => {
id: channel.channel_id, await updateWatchedState({
is_watched: true, id: channel.channel_id,
}); is_watched: true,
});
setRefresh(true); setRefresh(true);
}} }}
/>{' '} />{' '}
<Button <Button
label="Mark as unwatched" label="Mark as unwatched"
id="unwatched-button" id="unwatched-button"
type="button" type="button"
title={`Mark all videos from ${channel.channel_name} as unwatched`} title={`Mark all videos from ${channel.channel_name} as unwatched`}
onClick={async () => { onClick={async () => {
await updateWatchedState({ await updateWatchedState({
id: channel.channel_id, id: channel.channel_id,
is_watched: false, is_watched: false,
}); });
setRefresh(true); setRefresh(true);
}} }}
/> />
</div> </div>
</> </>
)} )}
</div>
</div> </div>
</div> </div>
</div>
<div className={`boxed-content ${gridView}`}> <div className={`boxed-content ${gridView}`}>
<Filterbar <Filterbar
hideToggleText={'Hide watched videos:'} hideToggleText={'Hide watched videos:'}
viewStyle={ViewStyleNames.Home as ViewStyleNamesType} viewStyle={ViewStyleNames.Home as ViewStyleNamesType}
/> />
</div>
<EmbeddableVideoPlayer videoId={videoId} />
<div className={`boxed-content ${gridView}`}>
<div className={`video-list ${viewStyle} ${gridViewGrid}`}>
{!hasVideos && (
<>
<h2>No videos found...</h2>
<p>
Try going to the <Link to={Routes.Downloads}>downloads page</Link> to start the scan
and download tasks.
</p>
</>
)}
<VideoList videoList={videoList} viewStyle={viewStyle} refreshVideoList={setRefresh} />
</div> </div>
</div>
{pagination && ( <EmbeddableVideoPlayer videoId={videoId} />
<div className="boxed-content">
<Pagination pagination={pagination} setPage={setCurrentPage} /> <div className={`boxed-content ${gridView}`}>
<div className={`video-list ${viewStyle} ${gridViewGrid}`}>
{!hasVideos && (
<>
<h2>No videos found...</h2>
<p>
Try going to the <Link to={Routes.Downloads}>downloads page</Link> to start the
scan and download tasks.
</p>
</>
)}
<VideoList videoList={videoList} viewStyle={viewStyle} refreshVideoList={setRefresh} />
</div>
</div> </div>
)} {pagination && (
</> <div className="boxed-content">
<Pagination pagination={pagination} setPage={setCurrentPage} />
</div>
)}
</>
)
); );
}; };

View File

@ -5,6 +5,7 @@ import Colours from '../configuration/colours/Colours';
import Button from '../components/Button'; import Button from '../components/Button';
import signIn from '../api/actions/signIn'; import signIn from '../api/actions/signIn';
import loadAuth from '../api/loader/loadAuth'; import loadAuth from '../api/loader/loadAuth';
import LoadingIndicator from '../components/LoadingIndicator';
const Login = () => { const Login = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -134,10 +135,7 @@ const Login = () => {
{waitingForBackend && ( {waitingForBackend && (
<> <>
<p> <p>
Waiting for backend{' '} Waiting for backend <LoadingIndicator />
<div className="lds-ring" style={{ color: 'var(--accent-font-dark)' }}>
<div />
</div>
</p> </p>
</> </>
)} )}

View File

@ -180,6 +180,7 @@ const Playlist = () => {
<a <a
href={`https://www.youtube.com/playlist?list=${playlist.playlist_id}`} href={`https://www.youtube.com/playlist?list=${playlist.playlist_id}`}
target="_blank" target="_blank"
rel="noopener noreferrer"
> >
Active Active
</a> </a>

View File

@ -779,7 +779,11 @@ const SettingsApplication = () => {
<li>Make sure to contribute to this excellent project.</li> <li>Make sure to contribute to this excellent project.</li>
<li> <li>
More details{' '} More details{' '}
<a target="_blank" href="https://returnyoutubedislike.com/"> <a
href="https://returnyoutubedislike.com/"
target="_blank"
rel="noopener noreferrer"
>
here here
</a> </a>
. .
@ -794,7 +798,11 @@ const SettingsApplication = () => {
<li>Make sure to contribute to this excellent project.</li> <li>Make sure to contribute to this excellent project.</li>
<li> <li>
More details{' '} More details{' '}
<a target="_blank" href="https://sponsor.ajay.app/"> <a
href="https://sponsor.ajay.app/"
target="_blank"
rel="noopener noreferrer"
>
here here
</a> </a>
. .
@ -831,7 +839,11 @@ const SettingsApplication = () => {
<div> <div>
<p> <p>
Enable{' '} Enable{' '}
<a target="_blank" href="https://returnyoutubedislike.com/"> <a
href="https://returnyoutubedislike.com/"
target="_blank"
rel="noopener noreferrer"
>
returnyoutubedislike returnyoutubedislike
</a> </a>
</p> </p>
@ -846,7 +858,7 @@ const SettingsApplication = () => {
<div> <div>
<p> <p>
Enable{' '} Enable{' '}
<a href="https://sponsor.ajay.app/" target="_blank"> <a href="https://sponsor.ajay.app/" target="_blank" rel="noopener noreferrer">
Sponsorblock Sponsorblock
</a> </a>
</p> </p>

View File

@ -279,7 +279,11 @@ const Video = () => {
{video.active && ( {video.active && (
<p> <p>
Youtube:{' '} Youtube:{' '}
<a href={`https://www.youtube.com/watch?v=${video.youtube_id}`} target="_blank"> <a
href={`https://www.youtube.com/watch?v=${video.youtube_id}`}
target="_blank"
rel="noopener noreferrer"
>
Active Active
</a> </a>
</p> </p>

View File

@ -458,6 +458,36 @@ button:hover {
margin: 20px 0; margin: 20px 0;
} }
.player-wrapper.theater-mode {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 1000;
background-color: rgba(0, 0, 0, 0.9);
margin: 0;
display: flex;
align-items: center;
justify-content: center;
}
.video-main.theater-mode {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
}
.video-main.theater-mode video {
max-height: 95vh;
max-width: 95vw;
width: auto;
height: auto;
}
.video-player { .video-player {
display: grid; display: grid;
align-content: space-evenly; align-content: space-evenly;