Moving main ThemeContext.Provider into Theme module. Using live elapsed timers on Downloads page.

This commit is contained in:
Roland 2022-09-28 09:38:05 -06:00
parent 854cdf597a
commit fdfd1e98fc
No known key found for this signature in database
GPG key ID: 9D6DBD3ACF11AB93
9 changed files with 163 additions and 91 deletions

View file

@ -1,4 +1,4 @@
import React, {useContext, useEffect, useState} from 'react';
import React, {useContext, useState} from 'react';
import './App.css';
import {NavBar} from "./components/Nav";
import {Link, Route, Routes} from "react-router-dom";
@ -16,8 +16,8 @@ import {LoadStatistic, PageContainer, SearchInput, useTitle} from "./components/
import {FilesRoute, FilesSearchView} from "./components/Files";
import {useSearchFiles, useSettingsInterval, useStatus} from "./hooks/customHooks";
import {MapRoute} from "./components/Map";
import {darkTheme, lightTheme, SettingsContext, ThemeContext} from "./contexts/contexts";
import {Header, Segment, Statistic, StatisticGroup} from "./components/Theme";
import {SettingsContext, ThemeContext} from "./contexts/contexts";
import {Header, Segment, Statistic, StatisticGroup, ThemeWrapper} from "./components/Theme";
function Dashboard() {
useTitle('Dashboard');
@ -119,42 +119,8 @@ function Footer() {
export default function App() {
const {settings} = useSettingsInterval();
const [i, setI] = useState({});
const [s, setS] = useState({});
const [t, setT] = useState({});
const [theme, setTheme] = useState();
const setDarkTheme = () => {
console.debug('setDarkTheme');
setI({inverted: true});
setS({style: {backgroundColor: '#1B1C1D', color: '#dddddd'}});
setT({style: {color: '#dddddd'}});
setTheme(darkTheme);
document.body.style.background = '#1B1C1D';
}
const setLightTheme = () => {
console.debug('setLightTheme');
setI({inverted: undefined});
setS({});
setT({});
setTheme(lightTheme);
document.body.style.background = '#FFFFFF';
}
useEffect(() => {
window.matchMedia('(prefers-color-scheme: dark)').matches && setDarkTheme();
window.matchMedia('(prefers-color-scheme: dark)').addEventListener(
'change', (e) => e.matches && setDarkTheme());
window.matchMedia('(prefers-color-scheme: light)').addEventListener(
'change', (e) => e.matches && setLightTheme());
}, []);
const themeValue = {i, s, t, theme, setDarkTheme, setLightTheme};
return (
<ThemeContext.Provider value={themeValue}>
<ThemeWrapper>
<SettingsContext.Provider value={settings}>
<header>
<NavBar/>
@ -177,6 +143,6 @@ export default function App() {
<SemanticToastContainer position="top-right"/>
<Footer/>
</SettingsContext.Provider>
</ThemeContext.Provider>
</ThemeWrapper>
);
}

View file

@ -25,13 +25,12 @@ import {
import TimezoneSelect from 'react-timezone-select';
import {
DisableDownloadsToggle,
ExternalCardLink,
HelpPopup,
HotspotToggle,
humanFileSize,
LoadStatistic,
PageContainer,
secondsToDate,
secondsToElapsedPopup,
secondsToFrequency,
TabLinks,
textEllipsis,
@ -540,11 +539,13 @@ class DownloadRow extends React.Component {
return <TableRow positive={positive}>
<TableCell>
<ExternalCardLink to={url}>{textEllipsis(url, 50)}</ExternalCardLink>
<a href={location || url} target='_blank' rel='noopener noreferrer'>
{textEllipsis(url, 50)}
</a>
</TableCell>
<TableCell>{secondsToFrequency(frequency)}</TableCell>
<TableCell>{last_successful_download ? secondsToDate(last_successful_download) : null}</TableCell>
<TableCell>{secondsToDate(next_download)}</TableCell>
<TableCell>{last_successful_download ? secondsToElapsedPopup(last_successful_download) : null}</TableCell>
<TableCell>{secondsToElapsedPopup(next_download)}</TableCell>
<TableCell>{error && errorModal}{location && <a href={location}>View</a>}</TableCell>
</TableRow>
}
@ -595,7 +596,7 @@ class StoppableRow extends React.Component {
let {url, last_successful_download, status, location, error} = this.props;
let {stopOpen, startOpen, errorModalOpen} = this.state;
let completedAtCell = last_successful_download ? secondsToDate(last_successful_download) : null;
let completedAtCell = last_successful_download ? secondsToElapsedPopup(last_successful_download) : null;
let buttonCell = <TableCell/>;
let positive = false;
let negative = false;
@ -676,9 +677,8 @@ class StoppableRow extends React.Component {
return (
<TableRow positive={positive} negative={negative} warning={warning}>
<TableCell>
<ExternalCardLink to={url}>{textEllipsis(url, 50)}</ExternalCardLink>
<a href={location || url} target='_blank' rel='noopener noreferrer'>{textEllipsis(url, 50)}</a>
</TableCell>
<TableCell>{status}</TableCell>
<TableCell>{completedAtCell}</TableCell>
{buttonCell}
</TableRow>
@ -700,7 +700,6 @@ function Downloads() {
<TableHeader>
<TableRow>
<TableHeaderCell>URL</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell>Completed At</TableHeaderCell>
<TableHeaderCell>Control</TableHeaderCell>
</TableRow>
@ -712,8 +711,8 @@ function Downloads() {
<TableRow>
<TableHeaderCell>URL</TableHeaderCell>
<TableHeaderCell>Download Frequency</TableHeaderCell>
<TableHeaderCell>Last Successful Download</TableHeaderCell>
<TableHeaderCell>Next Download</TableHeaderCell>
<TableHeaderCell>Completed At</TableHeaderCell>
<TableHeaderCell>Next</TableHeaderCell>
<TableHeaderCell>View</TableHeaderCell>
</TableRow>
</TableHeader>

View file

@ -50,7 +50,7 @@ function ArchivePage() {
if (archiveFile === undefined) {
return <>
<Header as='h2'>Unknown archive</Header>
This archive does not exist
<Header as='h4'>This archive does not exist</Header>
</>
}

View file

@ -30,6 +30,56 @@ export function Paginator({activePage, onPageChange, totalPages, showFirstAndLas
/>)
}
export function divmod(x, y) {
return [Math.floor(x / y), x % y];
}
export function secondsElapsed(seconds) {
// Convert the provided seconds into a human-readable string of the time elapsed between the provided timestamp
// and now.
if (!seconds || seconds < 0) {
return null;
}
let years;
let days;
let hours;
let minutes;
seconds = Math.abs((new Date().getTime() / 1000) - seconds);
[years, seconds] = divmod(seconds, secondsToYears);
[days, seconds] = divmod(seconds, secondsToDays);
[hours, seconds] = divmod(seconds, secondsToHours);
[minutes, seconds] = divmod(seconds, secondsToMinutes);
seconds = Math.floor(seconds);
if (years > 0 && days > 30) {
return `${years}y${days}d`;
} else if (years > 0) {
return `${years}y`;
} else if (days > 0) {
return `${days}d`;
} else if (hours > 0) {
return `${hours}h`;
} else if (minutes > 0) {
return `${minutes}m`;
}
return `${seconds}s`
}
export function secondsToElapsedPopup(seconds) {
// Return a Popup which allows the user see a more detailed timestamp when hovering.
const elapsed = secondsElapsed(seconds);
if (!elapsed) {
return <></>;
}
return <Popup
content={secondsToTimestamp(seconds)}
on='hover'
trigger={<span>{elapsed}</span>}
/>
}
export function secondsToDuration(video) {
let duration = video.duration;
let hours = Math.floor(duration / 3600);
@ -249,25 +299,25 @@ const secondsToDays = 86400;
const secondsToHours = 3600;
const secondsToMinutes = 60;
export function secondsToString(seconds) {
export function secondsToFullDuration(seconds) {
let s = '';
let numYears = Math.floor(seconds / secondsToYears);
if (numYears) {
s = `${numYears}Y`;
seconds -= numYears * secondsToYears;
let years, days, hours, minutes;
[years, seconds] = divmod(seconds, secondsToYears);
[days, seconds] = divmod(seconds, secondsToDays);
[hours, seconds] = divmod(seconds, secondsToHours);
[minutes] = divmod(seconds, secondsToMinutes);
if (years > 0) {
s = `${years}Y`;
}
let numDays = Math.floor(seconds / secondsToDays);
if (numDays) {
s = `${s} ${numDays}D`;
seconds -= numDays * secondsToDays;
if (days > 0) {
s = `${s} ${days}D`;
}
let numHours = Math.floor(seconds / secondsToHours);
if (numHours) {
seconds -= numHours * secondsToHours;
if (hours > 0 || minutes > 0) {
hours = String(hours).padStart(2, '0');
minutes = String(minutes).padStart(2, '0');
s = `${s} ${hours}:${minutes}`;
}
let numMinutes = Math.floor(seconds / secondsToMinutes);
s = `${s} ${numHours}:${numMinutes}H`;
return s;
}
@ -278,7 +328,11 @@ export function secondsToDate(seconds) {
export function secondsToTimestamp(seconds) {
let d = new Date(seconds * 1000);
return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()} ${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}`;
const day = String(d.getDate()).padStart(2, '0');
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
const sec = String(d.getSeconds()).padStart(2, '0');
return `${d.getFullYear()}-${d.getMonth() + 1}-${day} ${hours}:${minutes}:${sec}`;
}
export function humanFileSize(bytes, si = false, dp = 1) {

View file

@ -36,7 +36,7 @@ import {CardPlacholder} from "./Placeholder";
import {ArchiveCard} from "./Archive";
import Grid from "semantic-ui-react/dist/commonjs/collections/Grid";
import {ThemeContext} from "../contexts/contexts";
import {Button, Icon, Placeholder, Segment, Table} from "./Theme";
import {Button, Header, Icon, Placeholder, Segment, Table} from "./Theme";
const icons = {
File: <Icon name='file'/>,
@ -209,7 +209,7 @@ export function FileCards({files}) {
{files.map((i) => <FileCard key={i['path']} file={i}/>)}
</CardGroupCentered>
} else if (files && files.length === 0) {
return <p>No files found.</p>
return <Header as='h4'>No files found.</Header>
} else {
return <CardGroupCentered><CardPlacholder/></CardGroupCentered>
}

View file

@ -1,5 +1,5 @@
import React, {useContext} from 'react';
import {ThemeContext} from "../contexts/contexts";
import React, {useContext, useEffect, useState} from 'react';
import {darkTheme, lightTheme, ThemeContext} from "../contexts/contexts";
import {
Accordion as ACCORDION,
Button as BUTTON,
@ -23,6 +23,46 @@ import {
TextArea as TEXTAREA
} from "semantic-ui-react";
export function ThemeWrapper(props) {
const [i, setI] = useState({});
const [s, setS] = useState({});
const [t, setT] = useState({});
const [theme, setTheme] = useState();
const setDarkTheme = () => {
console.debug('setDarkTheme');
setI({inverted: true});
setS({style: {backgroundColor: '#1B1C1D', color: '#dddddd'}});
setT({style: {color: '#dddddd'}});
setTheme(darkTheme);
document.body.style.background = '#1B1C1D';
}
const setLightTheme = () => {
console.debug('setLightTheme');
setI({inverted: undefined});
setS({});
setT({});
setTheme(lightTheme);
document.body.style.background = '#FFFFFF';
}
useEffect(() => {
window.matchMedia('(prefers-color-scheme: dark)').matches && setDarkTheme();
window.matchMedia('(prefers-color-scheme: dark)').addEventListener(
'change', (e) => e.matches && setDarkTheme());
window.matchMedia('(prefers-color-scheme: light)').addEventListener(
'change', (e) => e.matches && setLightTheme());
}, []);
const themeValue = {i, s, t, theme, setDarkTheme, setLightTheme};
return <ThemeContext.Provider value={themeValue}>
{props.children}
</ThemeContext.Provider>
}
// Simple wrappers for Semantic UI elements to use the current theme.
const invertedNull = (props) => {

View file

@ -1,5 +1,5 @@
import React, {useContext, useState} from 'react';
import {deleteVideos, favoriteVideo} from "../api";
import {deleteVideos} from "../api";
import {Link, useNavigate, useParams} from "react-router-dom";
import {humanFileSize, humanNumber, PageContainer, secondsToTimestamp, uploadDate, useTitle, VideoCard} from "./Common";
import {Confirm} from "semantic-ui-react";
@ -13,7 +13,7 @@ import {Button, Header, Icon, Segment, Tab, TabPane} from "./Theme";
const MEDIA_PATH = '/media';
function VideoPage({videoFile, prevFile, nextFile, ...props}) {
function VideoPage({videoFile, prevFile, nextFile, setFavorite, ...props}) {
const {theme} = useContext(ThemeContext);
const navigate = useNavigate();
@ -33,11 +33,9 @@ function VideoPage({videoFile, prevFile, nextFile, ...props}) {
return <VideoPlaceholder/>;
}
if (videoFile === undefined) {
return <PageContainer><p>Could not find video</p></PageContainer>;
return <PageContainer><Header as='h4'>Could not find video</Header></PageContainer>;
}
let favorite = video.favorite;
const handleDeleteVideo = async (video_id) => {
try {
await deleteVideos(video_id)
@ -60,22 +58,22 @@ function VideoPage({videoFile, prevFile, nextFile, ...props}) {
viewCount = viewCount || video.info_json['view_count'];
}
const favorite = video && video.favorite;
let handleFavorite = async (e) => {
e.preventDefault();
favorite = await favoriteVideo(video.id, !!!favorite);
if (video) {
setFavorite(!favorite);
}
}
let favorite_button;
if (favorite) {
favorite_button = (<Button color='green' style={{'margin': '0.5em'}} onClick={handleFavorite}>
const favorite_button = (
<Button
color='green'
style={{'margin': '0.5em'}}
onClick={handleFavorite}
>
<Icon name='heart'/>
Unfavorite
{favorite ? 'Unfavorite' : 'Favorite'}
</Button>);
} else {
favorite_button = (<Button basic color='green' style={{'margin': '0.5em'}} onClick={handleFavorite}>
<Icon name='heart'/>
Favorite
</Button>);
}
let descriptionPane = {
menuItem: 'Description', render: () => <TabPane>

View file

@ -22,10 +22,11 @@ import {deleteVideos} from "../api";
export function VideoWrapper() {
const {videoId} = useParams();
const {videoFile, prevFile, nextFile} = useVideo(videoId);
const {videoFile, prevFile, nextFile, setFavorite} = useVideo(videoId);
scrollToTop();
return <VideoPage videoFile={videoFile} prevFile={prevFile} nextFile={nextFile} autoplay={true}/>
return <VideoPage videoFile={videoFile} prevFile={prevFile} nextFile={nextFile}
setFavorite={setFavorite} autoplay={true}/>
}
function Videos({filter}) {

View file

@ -1,5 +1,6 @@
import {useEffect, useState} from "react";
import {
favoriteVideo,
fetchDomains,
filesSearch,
getArchive,
@ -25,7 +26,7 @@ import {
} from "../api";
import {createSearchParams, useSearchParams} from "react-router-dom";
import {toast} from "react-semantic-toasts";
import {enumerate, humanFileSize, secondsToString} from "../components/Common";
import {enumerate, humanFileSize, secondsToFullDuration} from "../components/Common";
const calculatePage = (offset, limit) => {
return offset && limit ? Math.round((offset / limit) + 1) : 1;
@ -265,11 +266,24 @@ export const useVideo = (videoId) => {
}
}
const setFavorite = async (value) => {
if (!videoFile) {
console.error('No video to favorite');
return
}
try {
await favoriteVideo(videoFile.video.id, value);
fetchVideo();
} catch (e) {
console.error(e);
}
}
useEffect(() => {
fetchVideo();
}, [videoId]);
return {videoFile, prevFile, nextFile};
return {videoFile, prevFile, nextFile, setFavorite};
}
export const useChannel = (channel_id) => {
@ -648,7 +662,7 @@ export const useVideoStatistics = () => {
const fetchStatistics = async () => {
try {
let stats = await getStatistics();
stats.videos.sum_duration = secondsToString(stats.videos.sum_duration);
stats.videos.sum_duration = secondsToFullDuration(stats.videos.sum_duration);
stats.videos.sum_size = humanFileSize(stats.videos.sum_size, true);
stats.videos.max_size = humanFileSize(stats.videos.max_size, true);
stats.historical.average_size = humanFileSize(stats.historical.average_size, true);