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
node_modules
.editorconfig

View File

@ -1,14 +1,18 @@
# multi stage to build tube archivist
# 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
# RUN npm config set registry https://registry.npmjs.org/
COPY --from=npm-builder ./node_modules /frontend/node_modules
COPY ./frontend /frontend
WORKDIR /frontend
RUN npm i
RUN npm run build:deploy
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
RUN if [ "$INSTALL_DEBUG" ] ; then \
apt-get -y update && apt-get -y install --no-install-recommends \
vim htop bmon net-tools iputils-ping procps lsof \
&& pip install --user ipython pytest pytest-django \
apt-get -y update && apt-get -y install --no-install-recommends \
vim htop bmon net-tools iputils-ping procps lsof \
&& pip install --user ipython pytest pytest-django \
; fi
# make folders

View File

@ -47,7 +47,11 @@ const DownloadListItem = ({ download, setRefresh }: DownloadListItemProps) => {
{!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>
</a>
</div>

View File

@ -1,4 +1,5 @@
import { useState } from 'react';
import LoadingIndicator from './LoadingIndicator';
type InputTextProps = {
type: 'text' | 'number';
@ -51,13 +52,7 @@ const InputConfig = ({ type, name, value, setValue, oldValue, updateCallback }:
</>
)}
{oldValue !== null && <button onClick={() => handleUpdate(name, null)}>reset</button>}
{loading && (
<>
<div className="lds-ring" style={{ color: 'var(--accent-font-dark)' }}>
<div />
</div>
</>
)}
{loading && <LoadingIndicator />}
{success && <span></span>}
</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 [showInfoDialog, setShowInfoDialog] = useState(false);
const [infoDialogContent, setInfoDialogContent] = useState('');
const [isTheaterMode, setIsTheaterMode] = useState(false);
const [theaterModeKeyPressed, setTheaterModeKeyPressed] = useState(false);
const questionmarkPressed = useKeyPress('?');
const mutePressed = useKeyPress('m');
@ -151,6 +153,8 @@ const VideoPlayer = ({
const arrowRightPressed = useKeyPress('ArrowRight');
const arrowLeftPressed = useKeyPress('ArrowLeft');
const pPausedPressed = useKeyPress('p');
const theaterModePressed = useKeyPress('t');
const escapePressed = useKeyPress('Escape');
const videoId = video.youtube_id;
const videoUrl = video.media_url;
@ -345,10 +349,42 @@ const VideoPlayer = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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 (
<>
<div id="player" className={embed ? '' : 'player-wrapper'}>
<div className={embed ? '' : 'video-main'}>
<div
id="player"
className={embed ? '' : `player-wrapper ${isTheaterMode ? 'theater-mode' : ''}`}
>
<div className={embed ? '' : `video-main ${isTheaterMode ? 'theater-mode' : ''}`}>
<video
ref={videoRef}
key={`${getApiUrl()}${videoUrl}`}
@ -423,6 +459,18 @@ const VideoPlayer = ({
<td>Toggle fullscreen</td>
<td>f</td>
</tr>
{!embed && (
<>
<tr>
<td>Toggle theater mode</td>
<td>t</td>
</tr>
<tr>
<td>Exit theater mode</td>
<td>Esc</td>
</tr>
</>
)}
<tr>
<td>Toggle subtitles (if available)</td>
<td>c</td>
@ -467,11 +515,19 @@ const VideoPlayer = ({
<h4>
This video doesn't have any sponsor segments added. To add a segment go to{' '}
<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>{' '}
and add a segment using the{' '}
<u>
<a href="https://sponsor.ajay.app/">SponsorBlock</a>
<a href="https://sponsor.ajay.app/" target="_blank" rel="noopener noreferrer">
SponsorBlock
</a>
</u>{' '}
extension.
</h4>
@ -480,11 +536,19 @@ const VideoPlayer = ({
<h4>
This video has unlocked sponsor segments. Go to{' '}
<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>{' '}
and vote on the segments using the{' '}
<u>
<a href="https://sponsor.ajay.app/">SponsorBlock</a>
<a href="https://sponsor.ajay.app/" target="_blank" rel="noopener noreferrer">
SponsorBlock
</a>
</u>{' '}
extension.
</h4>

View File

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

View File

@ -130,7 +130,11 @@ const ChannelAbout = () => {
{channel.channel_active && (
<p>
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
</a>
</p>
@ -316,7 +320,7 @@ const ChannelAbout = () => {
<div>
<p>
Overwrite{' '}
<a href="https://sponsor.ajay.app/" target="_blank">
<a href="https://sponsor.ajay.app/" target="_blank" rel="noopener noreferrer">
SponsorBlock
</a>
</p>

View File

@ -97,104 +97,97 @@ const ChannelVideo = ({ videoType }: ChannelVideoProps) => {
videoId,
]);
if (!channel) {
return (
<div className="boxed-content">
<br />
<h2>Channel {channelId} not found!</h2>
</div>
);
}
return (
<>
<title>{`TA | Channel: ${channel.channel_name}`}</title>
<ScrollToTopOnNavigate />
<div className="boxed-content">
<div className="info-box info-box-2">
<ChannelOverview
channelId={channel.channel_id}
channelname={channel.channel_name}
channelSubs={channel.channel_subs}
channelSubscribed={channel.channel_subscribed}
channelThumbUrl={channel.channel_thumb_url}
setRefresh={setRefresh}
/>
<div className="info-box-item">
{videoAggs && (
<>
<p>
{videoAggs.total_items.value} videos <span className="space-carrot">|</span>{' '}
{videoAggs.total_duration.value_str} playback{' '}
<span className="space-carrot">|</span> Total size{' '}
{humanFileSize(videoAggs.total_size.value, useSiUnits)}
</p>
<div className="button-box">
<Button
label="Mark as watched"
id="watched-button"
type="button"
title={`Mark all videos from ${channel.channel_name} as watched`}
onClick={async () => {
await updateWatchedState({
id: channel.channel_id,
is_watched: true,
});
channel && (
<>
<title>{`TA | Channel: ${channel.channel_name}`}</title>
<ScrollToTopOnNavigate />
<div className="boxed-content">
<div className="info-box info-box-2">
<ChannelOverview
channelId={channel.channel_id}
channelname={channel.channel_name}
channelSubs={channel.channel_subs}
channelSubscribed={channel.channel_subscribed}
channelThumbUrl={channel.channel_thumb_url}
setRefresh={setRefresh}
/>
<div className="info-box-item">
{videoAggs && (
<>
<p>
{videoAggs.total_items.value} videos <span className="space-carrot">|</span>{' '}
{videoAggs.total_duration.value_str} playback{' '}
<span className="space-carrot">|</span> Total size{' '}
{humanFileSize(videoAggs.total_size.value, useSiUnits)}
</p>
<div className="button-box">
<Button
label="Mark as watched"
id="watched-button"
type="button"
title={`Mark all videos from ${channel.channel_name} as watched`}
onClick={async () => {
await updateWatchedState({
id: channel.channel_id,
is_watched: true,
});
setRefresh(true);
}}
/>{' '}
<Button
label="Mark as unwatched"
id="unwatched-button"
type="button"
title={`Mark all videos from ${channel.channel_name} as unwatched`}
onClick={async () => {
await updateWatchedState({
id: channel.channel_id,
is_watched: false,
});
setRefresh(true);
}}
/>{' '}
<Button
label="Mark as unwatched"
id="unwatched-button"
type="button"
title={`Mark all videos from ${channel.channel_name} as unwatched`}
onClick={async () => {
await updateWatchedState({
id: channel.channel_id,
is_watched: false,
});
setRefresh(true);
}}
/>
</div>
</>
)}
setRefresh(true);
}}
/>
</div>
</>
)}
</div>
</div>
</div>
</div>
<div className={`boxed-content ${gridView}`}>
<Filterbar
hideToggleText={'Hide watched videos:'}
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 className={`boxed-content ${gridView}`}>
<Filterbar
hideToggleText={'Hide watched videos:'}
viewStyle={ViewStyleNames.Home as ViewStyleNamesType}
/>
</div>
</div>
{pagination && (
<div className="boxed-content">
<Pagination pagination={pagination} setPage={setCurrentPage} />
<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>
)}
</>
{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 signIn from '../api/actions/signIn';
import loadAuth from '../api/loader/loadAuth';
import LoadingIndicator from '../components/LoadingIndicator';
const Login = () => {
const navigate = useNavigate();
@ -134,10 +135,7 @@ const Login = () => {
{waitingForBackend && (
<>
<p>
Waiting for backend{' '}
<div className="lds-ring" style={{ color: 'var(--accent-font-dark)' }}>
<div />
</div>
Waiting for backend <LoadingIndicator />
</p>
</>
)}

View File

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

View File

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

View File

@ -279,7 +279,11 @@ const Video = () => {
{video.active && (
<p>
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
</a>
</p>

View File

@ -458,6 +458,36 @@ button:hover {
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 {
display: grid;
align-content: space-evenly;