Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
28f2fbd6a7 | ||
|
d19190bf6a | ||
|
b14309daeb | ||
|
990cb9aaec | ||
|
bb4e5ecb50 | ||
|
ff94c324b3 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -12,3 +12,5 @@ backend/.env
|
||||
|
||||
# JavaScript stuff
|
||||
node_modules
|
||||
|
||||
.editorconfig
|
||||
|
14
Dockerfile
14
Dockerfile
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
9
frontend/src/components/LoadingIndicator.tsx
Normal file
9
frontend/src/components/LoadingIndicator.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
const LoadingIndicator = () => {
|
||||
return (
|
||||
<div className="lds-ring" style={{ color: 'var(--accent-font-dark)' }}>
|
||||
<div />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingIndicator;
|
@ -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>
|
||||
|
@ -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 && (
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
@ -180,6 +180,7 @@ const Playlist = () => {
|
||||
<a
|
||||
href={`https://www.youtube.com/playlist?list=${playlist.playlist_id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Active
|
||||
</a>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user