folder menu clicking on last breadcrumb
Massimo Melina committed
May 7, 2023 at 19:20 UTC
85ec24af660166f50bfb15f2baf0efae2eaedcc6
9 files changed
+120
-76
frontend/fontello-config.json
+12
-6
@@ -114,12 +114,6 @@
114
"code": 59402,
115
"src": "fontawesome"
116
},
117
- {
118
- "uid": "09feb4465d9bd1364f4e301c9ddbaa92",
119
- "css": "retweet",
120
- "code": 59407,
121
- "src": "fontawesome"
122
- },
117
{
118
"uid": "186dec7a13156bbe2550790c158fb85d",
119
"css": "crown",
@@ -197,6 +191,18 @@
191
"css": "left",
192
"code": 59414,
193
"src": "fontawesome"
194
+ },
195
+ {
196
+ "uid": "3e1bb74572a30486bd647304d9fcaff6",
197
+ "css": "lock",
198
+ "code": 59415,
199
+ "src": "typicons"
200
+ },
201
+ {
202
+ "uid": "f9c3205df26e7778abac86183aefdc99",
203
+ "css": "ccw",
204
+ "code": 59416,
205
+ "src": "fontawesome"
206
}
207
]
208
}
\ No newline at end of file
frontend/public/fontello.css
+4
-3
@@ -1,6 +1,6 @@
1
@font-face {
2
font-family: 'fontello';
3
- src: url('fontello.woff2?19258766') format('woff2'); font-weight: normal;
3
+ src: url('fontello.woff2?26713191') format('woff2');
4
font-weight: normal;
5
font-style: normal;
6
}
@@ -10,7 +10,7 @@
10
@media screen and (-webkit-min-device-pixel-ratio:0) {
11
@font-face {
12
font-family: 'fontello';
13
- src: url('../font/fontello.svg?19258766#fontello') format('svg');
13
+ src: url('../font/fontello.svg?26713191#fontello') format('svg');
14
}
15
}
16
*/
@@ -64,7 +64,6 @@
64
.fa-key:before { content: '\e80c'; } /* '' */
65
.fa-trash:before { content: '\e80d'; } /* '' */
66
.fa-to_start:before { content: '\e80e'; } /* '' */
67
-.fa-retweet:before { content: '\e80f'; } /* '' */
67
.fa-to_end:before { content: '\e810'; } /* '' */
68
.fa-picture:before { content: '\e811'; } /* '' */
69
.fa-camera:before { content: '\e812'; } /* '' */
@@ -72,6 +71,8 @@
71
.fa-logout:before { content: '\e814'; } /* '' */
72
.fa-video:before { content: '\e815'; } /* '' */
73
.fa-left:before { content: '\e816'; } /* '' */
74
+.fa-lock:before { content: '\e817'; } /* '' */
75
+.fa-reload:before { content: '\e818'; } /* '' */
76
.fa-spin6:before { content: '\e839'; } /* '' */
77
.fa-crown:before { content: '\e844'; } /* '' */
78
.fa-download:before { content: '\f02e'; } /* '' */
frontend/public/fontello.woff2
Binary files a/frontend/public/fontello.woff2 and b/frontend/public/fontello.woff2 differ
frontend/src/Breadcrumbs.ts
+18
-8
@@ -6,6 +6,7 @@ import { getPrefixUrl, hIcon } from './misc'
6
import { state } from './state'
7
import { usePath, reloadList } from './useFetchList'
8
import { useI18N } from './i18n'
9
+import { openFileMenu } from './fileMenu'
10
11
export function Breadcrumbs() {
12
const base = getPrefixUrl() + '/'
@@ -13,7 +14,7 @@ export function Breadcrumbs() {
14
const parent = base + currentPath.slice(0, currentPath.lastIndexOf('/') + 1)
15
let prev = base
16
const breadcrumbs = currentPath ? currentPath.split('/').map(x => [prev += x + '/', decodeURIComponent(x)]) : []
16
- const {t} = useI18N()
17
+ const {t} = useI18N()
18
return h(Fragment, {},
19
h(Breadcrumb, { label: hIcon('parent', { title: t`parent folder` }), path: parent }),
20
h(Breadcrumb, { label: hIcon('home', { title: t`home` }), path: base, current: !currentPath }),
@@ -30,16 +31,25 @@ export function Breadcrumbs() {
31
function Breadcrumb({ path, label, current }:{ current?: boolean, path?: string, label?: string | ReactElement }) {
32
const PAD = '\u00A0\u00A0' // make small elements easier to tap. Don't use min-width 'cause it requires display-inline that breaks word-wrapping
33
if (typeof label === 'string' && label.length < 3)
33
- label = PAD+label+PAD
34
+ label = PAD + label + PAD
35
+ const {t} = useI18N()
36
return h(Link, {
37
className: 'breadcrumb',
38
to: path || '/',
37
- async onClick() {
38
- if (current) {
39
- state.remoteSearch = ''
40
- state.stopSearch?.()
41
- reloadList()
42
- }
39
+ async onClick(ev) {
40
+ if (!current) return
41
+ const asEntry = { n: path!, uri: path!, name: label as string, ext: '', isFolder: true, cantOpen: false }
42
+ openFileMenu(asEntry, ev as any as MouseEvent, [
43
+ {
44
+ label: t`Reload`,
45
+ icon: 'reload',
46
+ onClick() {
47
+ state.remoteSearch = ''
48
+ state.stopSearch?.()
49
+ reloadList()
50
+ }
51
+ }
52
+ ])
53
}
54
}, label)
55
}
frontend/src/BrowseFiles.ts
+17
-56
@@ -4,7 +4,6 @@ import { Link } from 'react-router-dom'
4
import {
5
createElement as h,
6
Fragment,
7
- isValidElement,
7
memo,
8
useCallback,
9
useEffect,
@@ -12,7 +11,7 @@ import {
11
useRef,
12
useState
13
} from 'react'
15
-import { domOn, formatBytes, ErrorMsg, hIcon, isMobile, newDialog, hfsEvent, getHFS, dontBotherWithKeys } from './misc'
14
+import { domOn, formatBytes, ErrorMsg, hIcon, isMobile, getHFS } from './misc'
15
import { Checkbox, CustomCode, Spinner } from './components'
16
import { Head } from './Head'
17
import { state, useSnapState } from './state'
@@ -23,12 +22,13 @@ import { acceptDropFiles, enqueue } from './upload'
22
import _ from 'lodash'
23
import { t, useI18N } from './i18n'
24
import { deleteFiles } from './menu'
25
+import { openFileMenu } from './fileMenu'
26
27
export interface DirEntry { n:string, s?:number, m?:string, c?:string, p?:string,
28
name: string, uri: string, ext:string, isFolder:boolean, t?:Date, cantOpen: boolean } // we memoize these value for speed
29
export type DirList = DirEntry[]
30
31
-const MISSING_PERM = "Missing permission"
31
+export const MISSING_PERM = "Missing permission"
32
33
export function BrowseFiles() {
34
useFetchList()
@@ -192,9 +192,9 @@ const Entry = memo(({ entry, midnight, separator }: EntryProps) => {
192
className += ' cant-open'
193
if (separator)
194
className += ' ' + PAGE_SEPARATOR_CLASS
195
- const ico = hIcon(isFolder ? 'folder' : 'file')
195
+ const ico = getEntryIcon(entry)
196
const menuOnLink = getHFS().fileMenuOnLink
197
- const onClick = menuOnLink && openFileMenu || undefined
197
+ const onClick = menuOnLink && fileMenu || undefined
198
const mobile = isMobile()
199
return h('li', { className, label: separator },
200
showFilter && h(Checkbox, {
@@ -212,7 +212,7 @@ const Entry = memo(({ entry, midnight, separator }: EntryProps) => {
212
onMouseLeave(){ setMenuBtn(false) },
213
} || {},
214
h(Link, { to: base + uri }, ico, entry.n.slice(0,-1)),
215
- menuBtn && h('button', { className: 'popup-menu-button', onClick: openFileMenu }, hIcon('menu'), t`Menu`)
215
+ menuBtn && h('button', { className: 'popup-menu-button', onClick: fileMenu }, hIcon('menu'), t`Menu`)
216
)
217
: h(Fragment, {},
218
containerDir && h(Link, { to: base + containerDir, className:'container-folder' }, ico, containerName),
@@ -221,67 +221,28 @@ const Entry = memo(({ entry, midnight, separator }: EntryProps) => {
221
h(CustomCode, { name: 'afterEntryName', props: { entry } }),
222
h('div', { className: 'entry-panel' },
223
h(EntryDetails, { entry, midnight }),
224
- (!menuOnLink || isFolder && mobile) && h('button', { className: 'file-menu-button', onClick: openFileMenu }, hIcon('menu')),
224
+ (!menuOnLink || isFolder && mobile) && h('button', { className: 'file-menu-button', onClick: fileMenu }, hIcon('menu')),
225
),
226
h('div'),
227
)
228
229
- function openFileMenu(ev: MouseEvent) {
229
+ function fileMenu(ev: MouseEvent) {
230
if (ev.altKey || ev.ctrlKey || ev.metaKey) return
231
ev.preventDefault()
232
- const cantDownload = entry.cantOpen || isFolder && entry.p?.includes('r') // folders needs both list and read
233
- const OPEN_ICON = 'play'
234
- const OPEN_LABEL = t('file_open', "Open")
235
- const menu = [
232
+ const open = { icon: 'play', label: t('file_open', "Open"), href: uri, target: isFolder ? undefined : '_blank' }
233
+ openFileMenu(entry, ev, [
234
menuOnLink && !entry.cantOpen && (
237
- isFolder ? h(Link, { to: base + uri, onClick: () => close() }, hIcon(OPEN_ICON), OPEN_LABEL)
238
- : { label: OPEN_LABEL, href: uri, target: isFolder ? undefined : '_blank', icon: OPEN_ICON }),
239
- !cantDownload && { label: t`Download`, href: uri + (isFolder ? '?get=zip' : '?dl'), icon: 'download' },
235
+ !isFolder ? open : h(Link, { to: base + uri, onClick: () => close() }, hIcon(open.icon), open.label) ),
236
can_delete && { label: t`Delete`, icon: 'trash', onClick: () => deleteFiles([uri], base) }
241
- ]
242
- const props = [
243
- [t(`Name`), entry.name]
244
- ]
245
- const res = hfsEvent('fileMenu', { entry, menu, props })
246
- if (res)
247
- menu.push(...res.flat())
248
- const close = newDialog({
249
- title: isFolder ? t`Folder menu` : t`File menu`,
250
- className: 'file-dialog',
251
- icon: () => ico,
252
- position: isMobile() ? undefined
253
- : [ev.pageX, ev.pageY - window.scrollY] as [number, number],
254
- Content() {
255
- const {t} = useI18N()
256
- return h(Fragment, {},
257
- h('dl', { className: 'file-dialog-properties' },
258
- dontBotherWithKeys(props.map(prop => isValidElement(prop) ? prop
259
- : Array.isArray(prop) ? h(Fragment, {}, h('dt', {}, prop[0]), h('dd', {}, prop[1]))
260
- : null
261
- ))
262
- ),
263
- entry.cantOpen && h(Fragment, {}, hIcon('password', { style: { marginRight: '.5em' } }), t(MISSING_PERM)),
264
- h('div', { className: 'file-menu' },
265
- dontBotherWithKeys(menu.map((e: any, i) =>
266
- isValidElement(e) ? e
267
- : !e?.label ? null :
268
- h('a', {
269
- key: i,
270
- href: e.href || '#',
271
- ..._.omit(e, ['label', 'icon', 'href', 'onClick']),
272
- async onClick() {
273
- if ((await e.onClick?.()) !== false)
274
- close()
275
- }
276
- }, hIcon(e.icon || 'file'), e.label )
277
- ))
278
- )
279
- )
280
- }
281
- })
237
+ ])
238
}
239
+
240
})
241
242
+export function getEntryIcon(entry: DirEntry) {
243
+ return hIcon(entry.isFolder ? 'folder' : 'file')
244
+}
245
+
246
const EntryDetails = memo(({ entry, midnight }: { entry: DirEntry, midnight: Date }) => {
247
const { t: time, s } = entry
248
const today = time && time > midnight
frontend/src/fileMenu.ts
new
+63
@@ -0,0 +1,63 @@
1
+import { t, useI18N } from './i18n'
2
+import { dontBotherWithKeys, hfsEvent, hIcon, isMobile, newDialog } from './misc'
3
+import { createElement as h, Fragment, isValidElement, MouseEventHandler, ReactNode } from 'react'
4
+import _ from 'lodash'
5
+import { DirEntry, getEntryIcon, MISSING_PERM } from './BrowseFiles'
6
+
7
+interface FileMenuEntry {
8
+ label: ReactNode
9
+ href?: string
10
+ icon?: string
11
+ onClick?: MouseEventHandler
12
+}
13
+
14
+export function openFileMenu(entry: DirEntry, ev: MouseEvent, addToMenu: FileMenuEntry[]) {
15
+ const { uri, isFolder } = entry
16
+ const cantDownload = entry.cantOpen || isFolder && entry.p?.includes('r') // folders needs both list and read
17
+ const menu = [
18
+ !cantDownload && { label: t`Download`, href: uri + (isFolder ? '?get=zip' : '?dl'), icon: 'download' },
19
+ ...addToMenu,
20
+ ]
21
+ const props = [
22
+ [t(`Name`), entry.name]
23
+ ]
24
+ const res = hfsEvent('fileMenu', { entry, menu, props })
25
+ if (res)
26
+ menu.push(...res.flat())
27
+ const ico = getEntryIcon(entry)
28
+ const close = newDialog({
29
+ title: isFolder ? t`Folder menu` : t`File menu`,
30
+ className: 'file-dialog',
31
+ icon: () => ico,
32
+ position: isMobile() ? undefined
33
+ : [ev.pageX, ev.pageY - window.scrollY] as [number, number],
34
+ Content() {
35
+ const {t} = useI18N()
36
+ return h(Fragment, {},
37
+ h('dl', { className: 'file-dialog-properties' },
38
+ dontBotherWithKeys(props.map(prop => isValidElement(prop) ? prop
39
+ : Array.isArray(prop) ? h(Fragment, {}, h('dt', {}, prop[0]), h('dd', {}, prop[1]))
40
+ : null
41
+ ))
42
+ ),
43
+ entry.cantOpen && h(Fragment, {}, hIcon('password', { style: { marginRight: '.5em' } }), t(MISSING_PERM)),
44
+ h('div', { className: 'file-menu' },
45
+ dontBotherWithKeys(menu.map((e: any, i) =>
46
+ isValidElement(e) ? e
47
+ : !e?.label ? null :
48
+ h('a', {
49
+ key: i,
50
+ href: e.href || '#',
51
+ ..._.omit(e, ['label', 'icon', 'href', 'onClick']),
52
+ async onClick() {
53
+ if ((await e.onClick?.()) !== false)
54
+ close()
55
+ }
56
+ }, hIcon(e.icon || 'file'), e.label )
57
+ ))
58
+ )
59
+ )
60
+ }
61
+ })
62
+}
63
+
frontend/src/icons.ts
+2
-1
@@ -21,7 +21,8 @@ const SYS_ICONS = {
21
password: '🗝️:key',
22
download: '⬇️',
23
upload: '⬆️',
24
- invert: '🙃:retweet',
24
+ reload: '🔄:reload',
25
+ lock: '🔒:lock',
26
admin: '👑:crown',
27
check: '✔️',
28
to_start: '◀',
src/langs/hfs-lang-en.json
+2
-1
@@ -109,6 +109,7 @@
109
"Name": "Name",
110
"file_open": "Open",
111
"Download": "Download",
112
- "Missing permission": "Missing permission"
112
+ "Missing permission": "Missing permission",
113
+ "Reload": "Reload"
114
}
115
}
src/langs/hfs-lang-it.json
+2
-1
@@ -103,6 +103,7 @@
103
"Name": "Nome",
104
"file_open": "Apri",
105
"Download": "Download",
106
- "Missing permission": "Permesso mancante"
106
+ "Missing permission": "Permesso mancante",
107
+ "Reload": "Ricarica"
108
}
109
}