@samitouri / QOSami-HFS / commits / 8d8694ed

plugins: allow custom fileShow component to override errors handler

Massimo Melina committed Dec 16, 2024 at 15:15 UTC 8d8694ed0a1a0f863e781c5a0d325969eaeb9d4c
2 files changed +36 -33
dev-plugins.md
+10 -8
@@ -119,17 +119,19 @@ used must be strictly JSON (thus, no single quotes, only double quotes for strin
119 ```js
120 exports.config = { message: {} }
121 ```
122 -
123 - Once the admin has chosen a value for it, the value will be saved in the main config file, under the `plugins_config` property.
122 + This will produce a configuration form in the admin-panel.
123 + Once the admin has customized the value, the latter will be saved in the main config file, under the `plugins_config` property.
124 ```yaml
125 plugins_config:
126 name_of_the_plugin:
127 message: Hi there!
128 ```
129 - When necessary your plugin will read its value using `api.getConfig('message')`.
129
130 + When necessary your plugin will read its value using `api.getConfig('message')` in the backend,
131 + or `HFS.getPluginConfig('message')` in the frontend, but the latter must be enabled using the `frontend` flag in the config.
132 +
133 - `configDialog: DialogOptions` object to override dialog options. Please refer to sources for details.
132 -- `onFrontendConfig: (config: object) => void | object` manipulate config values exposed to front-end.
134 +- `onFrontendConfig: (config: object) => void | object` manipulate config values exposed to frontend.
135 - `customHtml: object | () => object` return custom-html sections programmatically.
136 - `customRest: { [name]: (parameters: object) => any }` declare backend functions to be called by frontend with `HFS.customRestCall`
137 - `customApi: { [name]: (parameters) => any }` declare functions to be called by other plugins (only backend, not frontend) using `api.customApiCall` (documented below)
@@ -242,9 +244,9 @@ The `api` object you get as parameter of the `init` contains the following:
244 These are not documented, probably never will, and are subject to change without notifications,
245 but you can study the sources if you are interested in using them. It's just a shorter version of `api.require('./misc')`
246
245 -## Front-end specific
247 +## Frontend specific
248
247 -The following information applies to the default front-end, and may not apply to a custom one.
249 +The following information applies to the default frontend, and may not apply to a custom one.
250
251 Once your script is loaded into the frontend (via `frontend_js`), you will have access to the `HFS` object in the global scope.
252
@@ -301,7 +303,7 @@ The following properties are accessible only immediately at top-level; don't cal
303 - `getPluginConfig()` returns object of all config keys that are declared frontend-accessible by this plugin.
304 - `getPluginPublic()` returns plugin's public folder, with final slash. Useful to point to public files.
305
304 -### Front-end API events
306 +### Frontend API events
307
308 API at this level is done with frontend-events, that you can handle by calling
309
@@ -436,7 +438,7 @@ This is a list of available frontend-events, with respective object parameter an
438 - `unauthorized` displayed behind the login dialog accessing a protected folder
439 - `userPanelAfterInfo` visible to logged-in users, after the click on the button with their username, between user-info and buttons
440
439 -## Back-end events
441 +## Backend events
442
443 These events happen in the server, and not in the browser.
444 You can listen to these events accessing `api.events` in the `init` function of the plugin.
frontend/src/show.ts
+26 -25
@@ -107,7 +107,7 @@ export function fileShow(entry: DirEntry, { startPlaying=false, startShuffle=fal
107 const showElement = getShowElement()
108 if (!autoPlaying || !showElement) return
109 if (showElement instanceof HTMLMediaElement) {
110 - showElement.play().catch(curFailed)
110 + showElement.play().catch(playFailed)
111 return domOn('ended', goNext, { target: showElement as any })
112 }
113 // we are supposedly showing an image
@@ -124,7 +124,7 @@ export function fileShow(entry: DirEntry, { startPlaying=false, startShuffle=fal
124 const failOnce = useRef<typeof cur>()
125 useEffect(() => {
126 if (component || failOnce.current === cur) return
127 - curFailed()
127 + onError()
128 failOnce.current = cur
129 }, [cur, component])
130 return h(FlexV, {
@@ -188,7 +188,7 @@ export function fileShow(entry: DirEntry, { startPlaying=false, startShuffle=fal
188 lastGood.current = cur
189 setLoading(false)
190 },
191 - onError: curFailed,
191 + onError,
192 async onPlay() {
193 const covers = !isAudio ? [] : state.list.filter(x => folder === dirname(x.n) // same folder
194 && x.name.match(/(?:folder|cover|front|albumart.*)\.jpe?g$/i))
@@ -232,28 +232,7 @@ export function fileShow(entry: DirEntry, { startPlaying=false, startShuffle=fal
232
233 function goNext() { go(+1) }
234
235 - function curFailed(err?: Error) {
236 - console.debug(err)
237 - if (err?.name === 'NotAllowedError') { // browser won't allow automatic audio playing without user interaction...
238 - if (!playMsgOnce) return
239 - playMsgOnce = false
240 - const el = getShowElement()
241 - if (!(el instanceof HTMLMediaElement)) return
242 - const mel = el as HTMLMediaElement
243 - const dlg = newDialog({ // ...so we offer a simple dialog with a button
244 - onClose: () => playMsgOnce = true,
245 - Content: () => h(Btn, {
246 - autoFocus: true,
247 - icon: 'play',
248 - label: "Click here to play",
249 - onClick: () => {
250 - mel.play().catch(curFailed)
251 - dlg.close()
252 - }
253 - })
254 - })
255 - return
256 - }
235 + function onError() {
236 const mediaError = (document.querySelector('.showing-container .showing') as any)?.error?.code // only present in video/audio elements
237 if (mediaError === 2) return // happens when chrome fails to fetch cover for videos. We don't skip the file for this reason. Tested on chrome129/windows
238 if (cur !== lastGood.current)
@@ -262,6 +241,28 @@ export function fileShow(entry: DirEntry, { startPlaying=false, startShuffle=fal
241 setFailed(cur.n)
242 }
243
244 + function playFailed(err?: Error) {
245 + console.debug(err)
246 + if (err?.name !== 'NotAllowedError') return // browser won't allow automatic audio playing without user interaction...
247 + if (!playMsgOnce) return
248 + playMsgOnce = false
249 + const el = getShowElement()
250 + if (!(el instanceof HTMLMediaElement)) return
251 + const mel = el as HTMLMediaElement
252 + const dlg = newDialog({ // ...so we offer a simple dialog with a button
253 + onClose: () => playMsgOnce = true,
254 + Content: () => h(Btn, {
255 + autoFocus: true,
256 + icon: 'play',
257 + label: "Click here to play",
258 + onClick: () => {
259 + mel.play().catch(playFailed)
260 + dlg.close()
261 + }
262 + })
263 + })
264 + }
265 +
266 function go(dir=1) {
267 if (getCur() !== cur) return // this was fired with a stale state (closure), cancel. To reproduce: hold right-arrow on the keyboard
268 const { list } = state