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(){