diff --git a/app/src/App.js b/app/src/App.js
index 9f325a60..1e8c224f 100644
--- a/app/src/App.js
+++ b/app/src/App.js
@@ -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 (
-
+
@@ -177,6 +143,6 @@ export default function App() {
-
+
);
}
diff --git a/app/src/components/Admin.js b/app/src/components/Admin.js
index 8a0560a9..63170008 100644
--- a/app/src/components/Admin.js
+++ b/app/src/components/Admin.js
@@ -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
- {textEllipsis(url, 50)}
+
+ {textEllipsis(url, 50)}
+
{secondsToFrequency(frequency)}
- {last_successful_download ? secondsToDate(last_successful_download) : null}
- {secondsToDate(next_download)}
+ {last_successful_download ? secondsToElapsedPopup(last_successful_download) : null}
+ {secondsToElapsedPopup(next_download)}
{error && errorModal}{location && View}
}
@@ -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 = ;
let positive = false;
let negative = false;
@@ -676,9 +677,8 @@ class StoppableRow extends React.Component {
return (
- {textEllipsis(url, 50)}
+ {textEllipsis(url, 50)}
- {status}
{completedAtCell}
{buttonCell}
@@ -700,7 +700,6 @@ function Downloads() {
URL
- Status
Completed At
Control
@@ -712,8 +711,8 @@ function Downloads() {
URL
Download Frequency
- Last Successful Download
- Next Download
+ Completed At
+ Next
View
diff --git a/app/src/components/Archive.js b/app/src/components/Archive.js
index 83e4011f..c388de9e 100644
--- a/app/src/components/Archive.js
+++ b/app/src/components/Archive.js
@@ -50,7 +50,7 @@ function ArchivePage() {
if (archiveFile === undefined) {
return <>
- This archive does not exist
+ This archive does not exist
>
}
diff --git a/app/src/components/Common.js b/app/src/components/Common.js
index 1415460e..588c39cb 100644
--- a/app/src/components/Common.js
+++ b/app/src/components/Common.js
@@ -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 {elapsed}}
+ />
+}
+
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) {
diff --git a/app/src/components/Files.js b/app/src/components/Files.js
index c35b95ac..73ad69c9 100644
--- a/app/src/components/Files.js
+++ b/app/src/components/Files.js
@@ -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: ,
@@ -209,7 +209,7 @@ export function FileCards({files}) {
{files.map((i) => )}
} else if (files && files.length === 0) {
- return No files found.
+ return
} else {
return
}
diff --git a/app/src/components/Theme.js b/app/src/components/Theme.js
index 55cba488..03f86711 100644
--- a/app/src/components/Theme.js
+++ b/app/src/components/Theme.js
@@ -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
+ {props.children}
+
+}
+
// Simple wrappers for Semantic UI elements to use the current theme.
const invertedNull = (props) => {
diff --git a/app/src/components/VideoPlayer.js b/app/src/components/VideoPlayer.js
index 6bc64618..efe9cdc8 100644
--- a/app/src/components/VideoPlayer.js
+++ b/app/src/components/VideoPlayer.js
@@ -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 ;
}
if (videoFile === undefined) {
- return Could not find video
;
+ return ;
}
- 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 = (