HFS.onEvent('additionalEntryProps')

Massimo Melina committed Jan 12, 2022 at 18:46 UTC a3b5a4130aa71dcc95f6cac0501735d1119dfd2c
7 files changed +122 -48
README.md
+35 -6
@@ -118,21 +118,50 @@ but nothing is preventing a single plug-in from doing both tasks.
118
119 ## For plug-in makers
120
121 +You should find some examples within your installation.
122 +
123 A plug-in must have a `plugin.js` file in its own folder.
124 This file is javascript module that is supposed to expose one or more of the supported keys:
125
124 -- `frontend_css` path to one or more css files that you want the frontend to load. These are to be placed in the `public` folder (refer below).
125 -
126 -- `frontend_js` path to one or more js files that you want the frontend to load. These are to be placed in the `public` folder (refer below).
127 -
128 -- `middleware: function(Context): undefined | true` a function that will be used as a middleware: it can interfere with http activity.
126 +- `frontend_css: string | string[]` path to one or more css files that you want the frontend to load. These are to be placed in the `public` folder (refer below).
127 +- `frontend_js: string | string[]` path to one or more js files that you want the frontend to load. These are to be placed in the `public` folder (refer below).
128 +- `middleware: (Context) => void | true` a function that will be used as a middleware: it can interfere with http activity.
129
130 To know what the Context object contains please refer to [Koa's documentation](https://github.com/koajs/koa/blob/master/docs/api/context.md).
131 You don't get the `next` parameter as in standard Koa's middlewares because this is different, but we are now explaining how to achieve the same results.
132 To interrupt other middlewares on this http request, return `true`.
133 If you want to execute something in the "upstream" of middlewares, return a function.
134 +
135 +- `unload: () => void` callback called when unloading a plugin. This is a good place for example to clearInterval().
136 +- `onDirEntry: ({ entry: DirEntry, listPath: string }) => void | false` by providing this callback you can manipulate the record
137 + that is sent to the frontend (`entry`), or you can return false to exclude this entry from the results.
138
139 Each plug-in can have a `public` folder, and its files will be accessible at `/~/plugins/PLUGIN_NAME/FILENAME`.
140
137 -If your plugin need to get some configuration, it should require the `getPluginConfig(pluginName:string)` function.
141 +If your plugin needs to get some configuration, it should require the `getPluginConfig(pluginName:string)` function.
142 The content will be read from the main config file, under the `plugins_config` property.
143 +
144 +### Front-end specific
145 +
146 +The following information applies to the default front-end, and may not apply to a custom one.
147 +
148 +#### Javascript
149 +Once your script is loaded into the frontend (via `frontend_js`), you will have access to the `HFS` object in the global scope.
150 +There you'll find `HFS.onEvent` function that is the base of communication.
151 +
152 +`onEvent(eventName:string, callback: (object) => any)` your callback will be called on the specified event.
153 +Depending on the event you'll have an object with parameters in it, and may return some output. Refer to the specific event for further information.
154 +
155 +This is a list of available frontend events, with respective parameters and output.
156 +
157 +- `additionalEntryProps`
158 + - parameters `{ entry: Entry }`
159 +
160 + The `Entry` type is an object with the following properties:
161 + - `n: string` name of the entry, including relative path in some cases.
162 + - `s?: number` size of the entry, in bytes. It may be missing, for example for folders.
163 + - `t?: Date` generic timestamp, combination of creation-time and modified-time.
164 + - `c?: Date` creation-time.
165 + - `m?: Date` modified-time.
166 + - output `string | void`
167 + - you receive each entry of the list, and optionally produce HTML code that will be added in the `entry-props` container.
config.yaml
+3
@@ -6,6 +6,9 @@
6 mime:
7 "*.jpg|*.png|*.mp3|*.txt": auto
8 disable_plugins: [ 'theme-example', 'middleware-example' ]
9 +plugins_config:
10 + middleware-example:
11 + message: ciao
12 vfs:
13 children:
14 - name: f1
frontend/src/App.tsx
+22 -11
@@ -1,5 +1,5 @@
1 import { BrowserRouter, Route, Routes } from "react-router-dom"
2 -import { createElement as h, Fragment, useEffect } from 'react'
2 +import { createElement as h, Fragment, useEffect, useState } from 'react'
3 import { BrowseFiles } from "./BrowseFiles";
4 import { Dialogs } from './dialog'
5 import { useApi } from "./api";
@@ -8,16 +8,8 @@ import useTheme from "./useTheme";
8 function App() {
9 const extras = useApi('extras_to_load')
10 useTheme()
11 -
12 - const imp = extras?.js
13 - useEffect(() => {
14 - for (const url of imp||[]) {
15 - const el = document.createElement('script')
16 - el.src = url
17 - el.async = true
18 - document.body.appendChild(el)
19 - }
20 - }, [imp])
11 + if (!useImportJs(extras))
12 + return null
13 return h(Fragment, {},
14 h(BrowserRouter, {},
15 h(Routes, {},
@@ -31,3 +23,22 @@ function App() {
23 }
24
25 export default App;
26 +
27 +// return true when all is loaded
28 +function useImportJs(extras:{ js?: string[] }) {
29 + const [ready, setReady] = useState(false)
30 + useEffect(() => {
31 + if (!extras) return
32 + const toImport = extras.js
33 + let missing = toImport?.length || 0
34 + if (!missing)
35 + return setReady(true)
36 + for (const url of toImport!) {
37 + const el = document.createElement('script')
38 + el.src = url
39 + el.onload = ()=> setReady(!--missing)
40 + document.body.appendChild(el)
41 + }
42 + }, [extras])
43 + return ready
44 +}
frontend/src/BrowseFiles.ts
+29 -20
@@ -1,6 +1,6 @@
1 import { Link, useLocation } from 'react-router-dom'
2 -import { createContext, createElement as h, Fragment, useContext, useEffect } from 'react'
3 -import { formatBytes, hError, hIcon, useForceUpdate } from './misc'
2 +import { createContext, createElement as h, Fragment, useContext, useEffect, useMemo } from 'react'
3 +import { formatBytes, hError, hIcon, Html, useForceUpdate, hfsEvent } from './misc'
4 import { Spinner } from './components'
5 import { Head } from './Head'
6 import { state, useSnapState } from './state'
@@ -36,7 +36,7 @@ function FilesList() {
36 const ret = h('ul', { className: 'dir' },
37 !list.length ? (!loading && (snap.stoppedSearch ? 'Stopped before finding anything' : 'Nothing here'))
38 : list.map((entry: DirEntry) =>
39 - h(File, { key: entry.n, midnight, hidden: filter && !filter.test(entry.n) || !++n, ...entry })),
39 + h(Entry, { 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
@@ -58,34 +58,20 @@ function isMobile() {
58 return window.innerWidth < 800
59 }
60
61 -function File({ n, t, s, hidden, isFolder, midnight }: DirEntry & { hidden:boolean, midnight: Date }) {
61 +function Entry(entry: DirEntry & { hidden:boolean, midnight: Date }) {
62 + let { n, hidden, isFolder } = entry
63 const base = usePath()
64 const containerDir = isFolder ? '' : n.substring(0, n.lastIndexOf('/')+1)
65 if (containerDir)
66 n = n.substring(containerDir.length)
67 const href = fix(containerDir + n)
67 - const today = t && t > midnight
68 - const shortTs = isMobile()
68 return h('li', { className:isFolder ? 'folder' : 'file', style:hidden ? { display:'none' } : null },
69 isFolder ? h(Link, { to: base+href }, hIcon('folder'), n)
70 : h(Fragment, {},
71 containerDir && h(Link, { to: base+fix(containerDir), className:'container-folder' }, hIcon('file'), containerDir ),
72 h('a', { href }, !containerDir && hIcon('file'), n)
73 ),
75 - h('div', { className:'entry-props' },
76 - s !== undefined && h(Fragment, {},
77 - h('span', { className:'entry-size' }, formatBytes(s)),
78 - hIcon('download'),
79 - ),
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 - ),
74 + h(EntryProps, entry),
75 h('div', { style:{ clear:'both' } })
76 )
77 }
@@ -93,3 +79,26 @@ function File({ n, t, s, hidden, isFolder, midnight }: DirEntry & { hidden:boole
79 function fix(s:string) {
80 return s.replace(/#/g, encodeURIComponent)
81 }
82 +
83 +function EntryProps(entry: DirEntry & { midnight: Date }) {
84 + const { t, s } = entry
85 + const today = t && t > entry.midnight
86 + const shortTs = isMobile()
87 + const code = useMemo(()=> hfsEvent('additionalEntryProps', { entry }).join(''),
88 + [entry])
89 + return h('div', { className:'entry-props' },
90 + h(Html, { code, className:'add-props' }),
91 + s !== undefined && h(Fragment, {},
92 + h('span', { className:'entry-size' }, formatBytes(s)),
93 + hIcon('download'),
94 + ),
95 + t && h('span', {
96 + className: 'entry-ts',
97 + title: today || !shortTs ? null : t.toLocaleString(),
98 + onClick() { // mobile has no hover
99 + if (shortTs)
100 + alertDialog('Full timestamp:\n' + t.toLocaleString()).then()
101 + }
102 + }, !shortTs ? t.toLocaleString() : today ? t.toLocaleTimeString() : t.toLocaleDateString()),
103 + )
104 +}
frontend/src/api.ts
+5 -9
@@ -1,10 +1,10 @@
1 import { useEffect, useState } from 'react';
2 -import { Falsy, getCookie, working } from './misc'
2 +import { Dict, Falsy, getCookie, working } from './misc'
3
4 const PREFIX = '/~/api/'
5
6 interface ApiCallOptions { noModal?:true }
7 -export function apiCall(cmd: string, params?: Record<string,any>, options: ApiCallOptions={}) : Promise<any> {
7 +export function apiCall(cmd: string, params?: Dict, options: ApiCallOptions={}) : Promise<any> {
8 const stop = options.noModal ? undefined : working()
9 params = addCsrf(params)
10 return fetch(PREFIX+cmd, {
@@ -42,7 +42,7 @@ export function useApi(cmd: string | Falsy, params?: object) : any {
42
43 type EventHandler = (type:string, data?:any) => void
44
45 -export function apiEvents(cmd: string, params: Record<string,any>, cb:EventHandler) {
45 +export function apiEvents(cmd: string, params: Dict, cb:EventHandler) {
46 const processed: Record<string,string> = {}
47 for (const k in params) {
48 const v = params[k]
@@ -66,10 +66,6 @@ export function apiEvents(cmd: string, params: Record<string,any>, cb:EventHandl
66 return source
67 }
68
69 -function addCsrf(params?: Record<string,any>) {
70 - const csrf = getCookie('csrf')
71 - if (!csrf)
72 - return params
73 - console.log({ csrf })
74 - return { csrf, ...params }
69 +function addCsrf(params?: Dict) {
70 + return { csrf: getCookie('csrf'), ...params }
71 }
frontend/src/misc.ts
+27 -1
@@ -1,10 +1,12 @@
1 -import { createElement as h, useCallback, useState } from 'react'
1 +import { createElement as h, HTMLAttributes, useCallback, useMemo, useState } from 'react'
2 import { Spinner } from './components'
3 import { newDialog } from './dialog'
4 import { Icon } from './icons'
5
6 export type Falsy = false | null | undefined | '' | 0
7
8 +export type Dict = Record<string, any>
9 +
10 export function hIcon(name: string, props?:any) {
11 return h(Icon, { name, ...props })
12 }
@@ -91,3 +93,27 @@ export function getCookie(name: string) {
93 }
94 return ''
95 }
96 +
97 +export function Html({ code, ...rest }:{ code:string } & HTMLAttributes<any>) {
98 + const o = useMemo(() => ({ __html: code }), [code])
99 + if (!code)
100 + return null
101 + return h('span', { ...rest, dangerouslySetInnerHTML: o })
102 +}
103 +
104 +export function hfsEvent(name: string, params?:Dict) {
105 + const output: any[] = []
106 + document.dispatchEvent(new CustomEvent('hfs.'+name, { detail:{ params, output } }))
107 + return output
108 +}
109 +
110 +const HFS: any = (window as any).HFS = {}
111 +
112 +HFS.onEvent = (name: string, cb: (params:any, output:any) => any) => {
113 + document.addEventListener('hfs.' + name, ev => {
114 + const { params, output } = (ev as CustomEvent).detail
115 + const res = cb(params, output)
116 + if (res !== undefined && Array.isArray(output))
117 + output.push(res)
118 + })
119 +}
src/index.ts
+1 -1
@@ -19,7 +19,7 @@ import { pluginsMiddleware } from './plugins'
19 import { throttler } from './throttler'
20 import { getAccount, getCurrentUsername } from './perm'
21
22 -const BUILD_TIMESTAMP = ""
22 +const BUILD_TIMESTAMP = "-"
23
24 export const SESSION_DURATION = 30*60_000
25