diff --git a/app/src/components/Archive.js b/app/src/components/Archive.js index 2de33524..311f0d73 100644 --- a/app/src/components/Archive.js +++ b/app/src/components/Archive.js @@ -26,6 +26,7 @@ import { findPosterPath, HelpHeader, humanFileSize, + isoDatetimeToAgoPopup, isoDatetimeToString, mimetypeColor, PageContainer, @@ -44,7 +45,7 @@ import {FileCards, FileRowTagIcon, FilesView} from "./Files"; import Grid from "semantic-ui-react/dist/commonjs/collections/Grid"; import _ from "lodash"; import {Media, ThemeContext} from "../contexts/contexts"; -import {Button, Card, CardIcon, darkTheme, Header, Loader, Placeholder, Segment, Tab, TabPane} from "./Theme"; +import {Button, Card, CardIcon, darkTheme, Header, Loader, Placeholder, Popup, Segment, Tab, TabPane} from "./Theme"; import {SortableTable} from "./SortableTable"; import {taggedImageLabel, TagsSelector} from "../Tags"; import {toast} from "react-semantic-toasts-2"; @@ -291,6 +292,9 @@ export function ArchiveCard({file}) { const domain = data ? data.domain : null; const domainUrl = `/archive?domain=${domain}`; + const title = file.title || data.url; + const header = {title}; + const dt = file.published_datetime || file.published_modified_datetime || file.modified; return
@@ -300,9 +304,9 @@ export function ArchiveCard({file}) { - - {file.title || data.url} - + {domain && @@ -310,9 +314,7 @@ export function ArchiveCard({file}) { {domain} } -

- {isoDatetimeToString(file.published_datetime)} -

+ {isoDatetimeToAgoPopup(dt, false)}
diff --git a/app/src/components/Common.js b/app/src/components/Common.js index 270cdd40..04481271 100644 --- a/app/src/components/Common.js +++ b/app/src/components/Common.js @@ -106,7 +106,7 @@ export function divmod(x, y) { return [Math.floor(x / y), x % y]; } -export function secondsElapsed(seconds) { +export function secondsElapsed(seconds, short = true) { // Convert the provided seconds into a human-readable string of the time elapsed between the provided timestamp // and now. if (!seconds || seconds < 0) { @@ -114,32 +114,47 @@ export function secondsElapsed(seconds) { } // Get seconds elapsed between now and `seconds` which is a UTC epoch. - const localNow = (new Date()).getTime(); + const localNow = (new Date()).getTime() / 1000; let years; let days; let hours; let minutes; - seconds = Math.abs((localNow / 1000) - seconds); + seconds = Math.abs(localNow - 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`; + if (short) { + 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` + } else { + if (years > 0 && days > 30) { + return `${years} years ${days} days`; + } else if (years > 0) { + return `${years} years`; + } else if (days > 0) { + return `${days} days`; + } else if (hours > 0) { + return `${hours} hours`; + } else if (minutes > 0) { + return `${minutes} minutes`; + } + return `${seconds} seconds` } - return `${seconds}s` } export function secondsToElapsedPopup(seconds) { @@ -160,6 +175,21 @@ export function isoDatetimeToElapsedPopup(dt) { return secondsToElapsedPopup(d.getTime() / 1000); } +export function isoDatetimeToAgoPopup(dt, short = true) { + const seconds = (new Date(dt)).getTime() / 1000; + // Return a Popup which allows the user see a more detailed timestamp when hovering. + const elapsed = secondsElapsed(seconds, short); + if (seconds === 0 || !elapsed) { + return <>; + } + const trigger = {isoDatetimeToString(dt)}; + return {elapsed} ago} + on='hover' + trigger={trigger} + /> +} + export function secondsToHMS(totalSeconds) { let hours = Math.floor(totalSeconds / 3600); totalSeconds -= hours * 3600; diff --git a/app/src/components/Files.js b/app/src/components/Files.js index 50eb551e..dcb9676c 100644 --- a/app/src/components/Files.js +++ b/app/src/components/Files.js @@ -24,6 +24,7 @@ import { findPosterPath, HandPointMessage, humanFileSize, + isoDatetimeToAgoPopup, isoDatetimeToString, mimetypeColor, PageContainer, @@ -57,6 +58,7 @@ import { ModalContent, ModalHeader, Placeholder, + Popup, Progress, Segment } from "./Theme"; @@ -77,14 +79,18 @@ function EbookCard({file}) { const viewerUrl = isEpub ? `/epub/epub.html?url=${downloadUrl}` : null; const color = mimetypeColor(file.mimetype); + const title = file.title || file.stem || file.name; + const header = + {title} + ; return - - {file.title || file.stem || file.name} - + @@ -100,18 +106,23 @@ function ImageCard({file}) { const {s} = useContext(ThemeContext); const url = `/media/${encodeMediaPath(file.primary_path)}`; + const title = file.title || file.stem || file.primary_path; + const header = +

{textEllipsis(title)}

+
; + const dt = file.published_datetime || file.published_modified_datetime || file.modified; return - -

{textEllipsis(file.title || file.stem || file.primary_path)}

-
+
-

{isoDatetimeToString(file.modified / 1000)}

+

{isoDatetimeToAgoPopup(dt, false)}

{humanFileSize(file.size)}

@@ -142,16 +153,21 @@ function FileCard({file}) { const color = mimetypeColor(file.mimetype); const size = file.size !== null && file.size !== undefined ? humanFileSize(file.size) : null; + const title = file.title || file.name || file.primary_path; + const header = + {title} + ; + const dt = file.published_datetime || file.published_modified_datetime || file.modified; return - - {file.title || file.name || file.primary_path} - + {author && {author}} -

{isoDatetimeToString(file.published_datetime) || isoDatetimeToString(file.modified)}

+

{isoDatetimeToAgoPopup(dt, false)}

{size}

diff --git a/app/src/components/Videos.js b/app/src/components/Videos.js index d2bbb5f6..93e4fd6c 100644 --- a/app/src/components/Videos.js +++ b/app/src/components/Videos.js @@ -12,7 +12,7 @@ import { FileIcon, findPosterPath, humanFileSize, - humanNumber, + humanNumber, isoDatetimeToAgoPopup, isoDatetimeToString, mimetypeColor, PageContainer, @@ -31,7 +31,6 @@ import { CardDescription, CardHeader, Container, - Dimmer, Image, PlaceholderHeader, PlaceholderLine, @@ -44,7 +43,7 @@ import {useChannel, useSearchOrder, useSearchVideos, useVideo, useVideoStatistic import {FileRowTagIcon, FilesView} from "./Files"; import Grid from "semantic-ui-react/dist/commonjs/collections/Grid"; import Icon from "semantic-ui-react/dist/commonjs/elements/Icon"; -import {Button, Card, Header, Loader, Placeholder, Segment, Statistic, StatisticGroup} from "./Theme"; +import {Button, Card, Header, Loader, Placeholder, Popup, Segment, Statistic, StatisticGroup} from "./Theme"; import {deleteVideos} from "../api"; import {Media, QueryContext, ThemeContext} from "../contexts/contexts"; import _ from "lodash"; @@ -309,7 +308,6 @@ export function VideoCard({file}) { // Default to video FilePreview for lone video files. let video_url; - const published_datetime = isoDatetimeToString(file.published_datetime); // A video may not have a channel. const channel = video.channel ? video.channel : null; let channel_url = null; @@ -324,8 +322,8 @@ export function VideoCard({file}) { let poster = ; - let header = {file.title || file.name || video.stem || video.name}; + const title = file.title || file.name || video.stem || video.name; + let header = {title}; if (video_url) { // Link to Channel-Video page or Video page. header = {header}; @@ -343,7 +341,9 @@ export function VideoCard({file}) { - {header} + @@ -351,7 +351,7 @@ export function VideoCard({file}) { {channel && {channel.name} } -

{published_datetime}

+

{isoDatetimeToAgoPopup(file.published_datetime, false)}