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 = '/~/'