mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Moving main ThemeContext.Provider into Theme module. Using live elapsed timers on Downloads page.
This commit is contained in:
parent
854cdf597a
commit
fdfd1e98fc
9 changed files with 163 additions and 91 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue