shorter timestamp on mobile

Massimo Melina committed Jan 10, 2022 at 15:16 UTC e9c68139eed928059a88ca25ddb7dfb2ad32de2c
3 files changed +44 -11
frontend/src/BrowseFiles.ts
+32 -5
@@ -1,11 +1,12 @@
1 import { Link, useLocation } from 'react-router-dom'
2 -import { createContext, createElement as h, Fragment, useContext } from 'react'
3 -import { formatBytes, hError, hIcon } from './misc'
2 +import { createContext, createElement as h, Fragment, useContext, useEffect } from 'react'
3 +import { formatBytes, hError, hIcon, useForceUpdate } from './misc'
4 import { Spinner } from './components'
5 import { Head } from './Head'
6 import { state, useSnapState } from './state'
7 import _ from 'lodash'
8 import useFetchList from './useFetchList'
9 +import { alertDialog } from './dialog'
10
11 export function usePath() {
12 return decodeURI(useLocation().pathname)
@@ -28,24 +29,43 @@ export function BrowseFiles() {
29 function FilesList() {
30 const { list, loading } = useContext(ListContext)
31 const snap = useSnapState()
32 + const midnight = useMidnight() // as an optimization we calculate this only once per list
33 if (!list) return null
34 const filter = snap.listFilter > '' && new RegExp(_.escapeRegExp(snap.listFilter),'i')
35 let n = 0 // if I try to use directly the state as counter I get a "too many re-renders" error
36 const ret = h('ul', { className: 'dir' },
37 !list.length ? (!loading && (snap.stoppedSearch ? 'Stopped before finding anything' : 'Nothing here'))
38 : list.map((entry: DirEntry) =>
37 - h(File, { key: entry.n, hidden: filter && !filter.test(entry.n) || !++n, ...entry })),
39 + h(File, { key: entry.n, midnight, hidden: filter && !filter.test(entry.n) || !++n, ...entry })),
40 loading && h(Spinner))
41 state.filteredEntries = filter ? n : -1
42 return ret
43 }
44
43 -function File({ n, t, s, hidden, isFolder }: DirEntry & { hidden:boolean }) {
45 +function useMidnight() {
46 + const midnight = new Date()
47 + midnight.setHours(0,0,0,0)
48 + const [forceUpdate] = useForceUpdate()
49 + useEffect(()=>{
50 + const nextMidnight = new Date(midnight) // as an optimization we calculate this only once per list
51 + nextMidnight.setDate( 1 + nextMidnight.getDate() )
52 + setTimeout(forceUpdate, +nextMidnight - +midnight)
53 + },[])
54 + return midnight
55 +}
56 +
57 +function isMobile() {
58 + return window.innerWidth < 800
59 +}
60 +
61 +function File({ n, t, s, hidden, isFolder, midnight }: DirEntry & { hidden:boolean, midnight: Date }) {
62 const base = usePath()
63 const containerDir = isFolder ? '' : n.substring(0, n.lastIndexOf('/')+1)
64 if (containerDir)
65 n = n.substring(containerDir.length)
66 const href = fix(containerDir + n)
67 + const today = t && t > midnight
68 + const shortTs = isMobile()
69 return h('li', { className:isFolder ? 'folder' : 'file', style:hidden ? { display:'none' } : null },
70 isFolder ? h(Link, { to: base+href }, hIcon('folder'), n)
71 : h(Fragment, {},
@@ -57,7 +77,14 @@ function File({ n, t, s, hidden, isFolder }: DirEntry & { hidden:boolean }) {
77 h('span', { className:'entry-size' }, formatBytes(s)),
78 hIcon('download'),
79 ),
60 - t && h('span', { className:'entry-ts' }, t.toLocaleString()),
80 + t && h('span', {
81 + className: 'entry-ts',
82 + title: today || !shortTs ? null : t.toLocaleString(),
83 + onClick() { // mobile has no hover
84 + if (shortTs)
85 + alertDialog('Full timestamp:\n' + t.toLocaleString()).then()
86 + }
87 + }, !shortTs ? t.toLocaleString() : today ? t.toLocaleTimeString() : t.toLocaleDateString()),
88 ),
89 h('div', { style:{ clear:'both' } })
90 )
frontend/src/dialog.css
+4
@@ -43,6 +43,10 @@
43 line-height: 1.7em;
44 background-color: var(--color);
45 }
46 +.dialog-content p {
47 + white-space: pre;
48 + margin: .5em 0;
49 +}
50 .dialog-confirm button {
51 margin-top: 1em;
52 }
frontend/src/dialog.ts
+8 -6
@@ -1,4 +1,4 @@
1 -import { createElement as h, Fragment, FunctionComponent, ReactNode, useEffect, useRef } from 'react'
1 +import { createElement as h, Fragment, FunctionComponent, ReactElement, ReactNode, useEffect, useRef } from 'react'
2 import { proxy, useSnapshot } from 'valtio'
3 import './dialog.css'
4
@@ -35,7 +35,7 @@ function Dialog(d:DialogOptions) {
35 h('div', { className:'dialog '+(d.className||'') },
36 d.closable || d.closable===undefined && h('button', { className:'dialog-icon dialog-closer', onClick:()=> closeDialog() }, d.closableContent),
37 d.icon && h('div', { className:'dialog-icon dialog-type' }, d.icon),
38 - h('div', {}, h(d.Content || 'div'))
38 + h('div', { className:'dialog-content' }, h(d.Content || 'div'))
39 ))
40 }
41
@@ -88,7 +88,7 @@ export async function promptDialog(msg: string, { def, type }:PromptOptions={})
88 inp.value = def
89 },[])
90 return h('div', {},
91 - h('div', {}, msg),
91 + h('p', {}, msg),
92 h('input', {
93 ref,
94 type,
@@ -108,7 +108,7 @@ export async function promptDialog(msg: string, { def, type }:PromptOptions={})
108
109 type AlertType = 'error' | 'warning' | 'info'
110
111 -export async function alertDialog(msg: string | Error, type:AlertType='info') {
111 +export async function alertDialog(msg: ReactElement | string | Error, type:AlertType='info') {
112 if (msg instanceof Error) {
113 msg = String(msg)
114 type = 'error'
@@ -121,7 +121,9 @@ export async function alertDialog(msg: string | Error, type:AlertType='info') {
121 }))
122
123 function Content(){
124 - return h('span', {}, String(msg))
124 + if (typeof msg === 'string' || msg instanceof Error)
125 + msg = h('p', {}, String(msg))
126 + return msg
127 }
128 }
129
@@ -135,7 +137,7 @@ export async function confirmDialog(msg: string) : Promise<boolean> {
137
138 function Content() {
139 return h('div', {},
138 - h('div', {}, msg),
140 + h('p', {}, msg),
141 h('button', {
142 autoFocus: true,
143 onClick(){