file menu

Massimo Melina committed Mar 25, 2023 at 00:09 UTC 6eb5f024fb831eb668ac22d81215435df87efc7a
5 files changed +71 -10
dev-plugins.md
+11 -1
@@ -188,7 +188,17 @@ This is a list of available frontend-events, with respective object parameter an
188 - `beforeHeader` & `afterHeader`
189 - use this to produce content that should go right before/after the `header` part
190 - output `Html`
191 -
191 +- `fileMenu`
192 + - add your entries to the menu.
193 + - parameter `{ entry: Entry }`
194 + - output `FileMenuEntry | FileMenuEntry[]`
195 + ```typescript
196 + interface FileMenuEntry {
197 + label: ReactNode, icon?: string, href?: string,
198 + onClick?: () => (Promisable<boolean>) // return false to not close menu dialog
199 + //...rest is transfered to <a> element
200 + }
201 + ```
202 ## Publish your plug-in
203
204 Suggested method for publishing is to have a dedicated repository on GitHub, with topic `hfs-plugin`.
frontend/src/BrowseFiles.ts
+40 -6
@@ -11,7 +11,7 @@ import {
11 useRef,
12 useState
13 } from 'react'
14 -import { domOn, formatBytes, ErrorMsg, hIcon, isMobile } from './misc'
14 +import { domOn, formatBytes, ErrorMsg, hIcon, isMobile, newDialog, hfsEvent } from './misc'
15 import { Checkbox, CustomCode, Spinner } from './components'
16 import { Head } from './Head'
17 import { state, useSnapState } from './state'
@@ -21,6 +21,7 @@ import { useAuthorized } from './login'
21 import { acceptDropFiles, enqueue } from './upload'
22 import _ from 'lodash'
23 import { useI18N } from './i18n'
24 +import { deleteFiles } from './menu'
25
26 export function usePath() {
27 return decodeURI(useLocation().pathname)
@@ -182,13 +183,14 @@ const PAGE_SEPARATOR_CLASS = 'page-separator'
183 const Entry = memo((entry: DirEntry & { midnight: Date, separator?: string }) => {
184 let { n: relativePath, isFolder, separator } = entry
185 const base = useLocation().pathname
185 - const { showFilter, selected } = useSnapState()
186 + const { showFilter, selected, can_delete } = useSnapState()
187 const href = fixUrl(relativePath)
188 const containerDir = isFolder ? '' : relativePath.substring(0, relativePath.lastIndexOf('/')+1)
189 const name = relativePath.substring(containerDir.length)
190 let className = isFolder ? 'folder' : 'file'
191 if (separator)
192 className += ' ' + PAGE_SEPARATOR_CLASS
193 + const ico = hIcon(isFolder ? 'folder' : 'file')
194 return h('li', { className, label: separator },
195 showFilter && h(Checkbox, {
196 value: selected[relativePath],
@@ -198,16 +200,48 @@ const Entry = memo((entry: DirEntry & { midnight: Date, separator?: string }) =>
200 delete state.selected[relativePath]
201 },
202 }),
201 - isFolder ? h(Link, { to: base+href }, hIcon('folder'), relativePath.slice(0,-1))
203 + isFolder ? h(Link, { to: base+href }, ico, relativePath.slice(0,-1))
204 : h(Fragment, {},
203 - h('a', { href: href + '?dl' }, hIcon('download')),
204 - containerDir && h(Link, { to: base + fixUrl(containerDir), className:'container-folder' }, containerDir),
205 - h('a', { href }, name)
205 + containerDir && h(Link, { to: base+fixUrl(containerDir), className:'container-folder' }, ico, containerDir ),
206 + h('a', { href, onClick: openFileMenu }, !containerDir && ico, name)
207 ),
208 h(CustomCode, { name: 'afterEntryName', props: { entry } }),
209 h(EntryProps, entry),
210 h('div'),
211 )
212 +
213 + function openFileMenu(ev: Event) {
214 + ev.preventDefault()
215 + const menu = [
216 + { label: "Open", href, target: '_blank', icon: 'play' },
217 + { label: "Download", href: href + '?dl', icon: 'download' },
218 + can_delete && { label: "Delete", icon: 'trash', onClick: () => deleteFiles([href], base) }
219 + ]
220 + hfsEvent('fileMenu', { entry, menu })
221 + const close = newDialog({
222 + title: "File menu",
223 + icon: () => ico,
224 + Content() {
225 + const {t} = useI18N()
226 + return h(Fragment, {},
227 + t("Name: {name}", { name }),
228 + h('div', { className: 'file-menu' },
229 + menu.map((e: any, i) => !e?.label ? null :
230 + h('a', {
231 + key: i,
232 + href: e.href || '#',
233 + ..._.omit(e, ['label', 'icon', 'href', 'onClick']),
234 + async onClick() {
235 + if ((await e.onClick?.()) !== false)
236 + close()
237 + }
238 + }, hIcon(e.icon || 'file'), e.label )
239 + )
240 + )
241 + )
242 + }
243 + })
244 + }
245 })
246
247 function fixUrl(s:string) {
frontend/src/index.scss
+16
@@ -11,6 +11,7 @@
11 --button-text: #eaeaea;
12 --focus-color: #468;
13 --separator: " – ";
14 + .highlightedText { color: #0006; text-shadow: 0 0 3px #0006; }
15 .theme-dark {
16 --bg: #000;
17 --text: #999;
@@ -18,6 +19,7 @@
19 --button-bg: #345;
20 --button-text: #999;
21 color-scheme: dark;
22 + .highlightedText { color: #fff; text-shadow: 0 0 3px #fff; }
23 a { color:#8ac; }
24 .dialog-closer { background: #633 }
25 .dialog-icon { color: #ccc;
@@ -352,6 +354,20 @@ button label {
354 }
355 }
356
357 +.file-menu {
358 + padding-top: .5em;
359 + border-top: 1px solid var(--faint-contrast);
360 + margin-top: 1em;
361 +
362 + display: flex;
363 + flex-direction: column;
364 + & a {
365 + padding: .5em 0;
366 + & .icon { margin-right: 0.5em; }
367 + &:hover { @extend .highlightedText }
368 + }
369 +}
370 +
371 @media (min-width: 42em) {
372 body {
373 /* Works on Firefox */
frontend/src/menu.ts
+3 -2
@@ -197,13 +197,14 @@ function LoginButton() {
197 })
198 }
199
200 -async function deleteFiles(uris: string[], root: string) {
200 +export async function deleteFiles(uris: string[], root: string='') {
201 const n = uris.length
202 if (!n) {
203 alertDialog(t('delete_select', "Select something to delete")).then()
204 return
205 }
206 - if (!await confirmDialog(t('delete_confirm', {n}, "Delete {n,plural, one{# item} other{# items}}?"))) return
206 + if (!await confirmDialog(t('delete_confirm', {n}, "Delete {n,plural, one{# item} other{# items}}?")))
207 + return false
208 const errors = onlyTruthy(await Promise.all(uris.map(uri =>
209 apiCall('del', { path: root + uri }).then(() => null, err => ({ uri, err }))
210 )))
src/const.ts
+1 -1
@@ -17,7 +17,7 @@ export const VERSION = pkg.version
17 export const DAY = 86_400_000
18 export const SESSION_DURATION = DAY
19
20 -export const API_VERSION = 6 // config.frontend
20 +export const API_VERSION = 7 // fileMenu event
21 export const COMPATIBLE_API_VERSION = 1 // while changes in the api are not breaking, this number stays the same, otherwise is made equal to API_VERSION
22
23 export const SPECIAL_URI = '/~/'