@samitouri / QOSami-HFS / commits / 85ec24af

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 }