admin/options/file: copy including custom_html
Massimo Melina committed
Nov 14, 2024 at 10:59 UTC
4d757b3911679ec5b1d52d25cb2fb0876e2f1b70
6 files changed
+31
-27
admin/src/ConfigFilePage.ts
+6
-4
@@ -3,7 +3,7 @@
3
import { createElement as h, Fragment, useEffect, useState } from 'react';
4
import { apiCall, useApiEx } from './api'
5
import { Alert, Box } from '@mui/material'
6
-import { copyTextToClipboard, focusSelector, isCtrlKey, KeepInScreen } from './misc'
6
+import { copyTextToClipboard, focusSelector, isCtrlKey, KeepInScreen, md } from './misc'
7
import { Btn, Flex, IconBtn, reloadBtn } from './mui';
8
import { Save, ContentCopy, Edit } from '@mui/icons-material'
9
import { TextEditor } from './TextEditor';
@@ -61,8 +61,10 @@ export default function ConfigFilePage() {
61
}
62
63
function copy() {
64
- if (!text) return
65
- copyTextToClipboard(text.replace(/^(\s*(\w*password(?!_change)\w*|srp):\s*).+\n/gm, '$1removed\n'))
66
- toast("copied")
64
+ const s = (text || '').replace(/^(\s*(\w*password(?!_change)\w*|srp):\s*).+\n/gm, '$1removed\n')
65
+ + 'custom_html: | # this is currently ignored by hfs, just here for reference\n' + data.customHtml.replace(/^/gm, ' ')
66
+ if (!s) return
67
+ copyTextToClipboard(s)
68
+ toast(md`Copied!\ncustom.html included`)
69
}
70
}
src/adminApis.ts
+5
-4
@@ -27,7 +27,7 @@ import { cloudflareDetected, getProxyDetected } from './middlewares'
27
import { writeFile } from 'fs/promises'
28
import { execFile } from 'child_process'
29
import { promisify } from 'util'
30
-import { customHtmlSections, customHtmlState, saveCustomHtml } from './customHtml'
30
+import { customHtmlSections, customHtml, saveCustomHtml } from './customHtml'
31
import _ from 'lodash'
32
import { autoCheckUpdateResult, getUpdates, localUpdateAvailable, update, updateSupported } from './update'
33
import { resolve } from 'path'
@@ -72,6 +72,7 @@ export const adminApis = {
72
path: configFile.getPath(),
73
fullPath: resolve(configFile.getPath()),
74
text: configFile.getText(),
75
+ customHtml: customHtml.getText(),
76
}
77
},
78
set_config_text: ({ text }) => configFile.save(text, { reparse: true }),
@@ -96,9 +97,9 @@ export const adminApis = {
97
get_custom_html() {
98
return {
99
sections: Object.fromEntries([
99
- ...customHtmlSections.concat(getErrorSections()).map(k => [k,'']),
100
- ...customHtmlState.sections
101
- ])
100
+ ...customHtmlSections.concat(getErrorSections()).map(k => [k,'']), // be sure to output all sections
101
+ ...customHtml.sections // override entries above
102
+ ]),
103
}
104
},
105
src/customHtml.ts
+9
-11
@@ -11,15 +11,13 @@ const FILE = 'custom.html'
11
export const customHtmlSections: ReadonlyArray<string> = ['style', 'beforeHeader', 'afterHeader', 'afterMenuBar', 'afterList',
12
'footer', 'top', 'bottom', 'afterEntryName', 'beforeLogin', 'unauthorized', 'htmlHead', 'userPanelAfterInfo']
13
14
-export const customHtmlState = proxy({
15
- sections: watchLoadCustomHtml().state
16
-})
14
+export const customHtml = watchLoadCustomHtml()
15
16
export function watchLoadCustomHtml(folder='') {
19
- const state = new Map<string, string>()
17
+ const sections = new Map<string, string>()
18
const res = watchLoad(prefix('', folder, '/') + FILE, data => {
19
const re = /^\[([^\]]+)] *$/gm
22
- state.clear()
20
+ sections.clear()
21
if (!data) return
22
let name: string | undefined = 'top'
23
do {
@@ -27,21 +25,21 @@ export function watchLoadCustomHtml(folder='') {
25
const match = re.exec(data)
26
const content = data.slice(last, !match ? undefined : re.lastIndex - (match?.[0]?.length || 0)).trim()
27
if (content)
30
- state.set(name, content)
28
+ sections.set(name, content)
29
name = match?.[1]
30
} while (name)
31
})
34
- return Object.assign(res, { state })
32
+ return Object.assign(res, { sections })
33
}
34
35
export function getSection(name: string) {
38
- return (customHtmlState.sections.get(name) || '')
36
+ return (customHtml.sections.get(name) || '')
37
+ mapPlugins(pl => pl.getData().getCustomHtml()[name]).join('\n')
38
}
39
40
export function getAllSections() {
41
const keys = mapPlugins(pl => Object.keys(pl.getData().getCustomHtml()))
44
- keys.push(Array.from(customHtmlState.sections.keys()))
42
+ keys.push(Array.from(customHtml.sections.keys()))
43
const all = _.uniq(keys.flat())
44
return Object.fromEntries(all.map(x => [x, getSection(x)]))
45
}
@@ -49,8 +47,8 @@ export function getAllSections() {
47
export async function saveCustomHtml(sections: Dict<string>) {
48
const text = Object.entries(sections).filter(([k,v]) => v?.trim()).map(([k,v]) => `[${k}]\n${v}\n\n`).join('')
49
await writeFile(FILE, text)
52
- customHtmlState.sections.clear()
50
+ customHtml.sections.clear()
51
for (const [k,v] of Object.entries(sections))
52
if (v)
55
- customHtmlState.sections.set(k, v)
53
+ customHtml.sections.set(k, v)
54
}
\ No newline at end of file
src/plugins.ts
+2
-2
@@ -488,9 +488,9 @@ function watchPlugin(id: string, path: string) {
488
},
489
})
490
const folder = dirname(module)
491
- const { state, unwatch } = watchLoadCustomHtml(folder)
491
+ const { sections, unwatch } = watchLoadCustomHtml(folder)
492
pluginData.getCustomHtml = () =>
493
- Object.assign(Object.fromEntries(state), callable(pluginData.customHtml) || {})
493
+ Object.assign(Object.fromEntries(sections), callable(pluginData.customHtml) || {})
494
495
const plugin = new Plugin(id, folder, pluginData, async () => {
496
unwatch()
src/serveGuiFiles.ts
+3
-4
@@ -11,8 +11,7 @@ import { ApiError } from './apiMiddleware'
11
import { join, extname } from 'path'
12
import { CFG, debounceAsync, FRONTEND_OPTIONS, isPrimitive, newObj, onlyTruthy, parseFile } from './misc'
13
import { favicon, title } from './adminApis'
14
-import { subscribe } from 'valtio/vanilla'
15
-import { customHtmlState, getAllSections, getSection } from './customHtml'
14
+import { customHtml, getAllSections, getSection } from './customHtml'
15
import _ from 'lodash'
16
import { defineConfig, getConfig } from './config'
17
import { getLangData } from './lang'
@@ -28,8 +27,8 @@ const DEV_STATIC = process.env.DEV ? 'dist/' : ''
27
function serveStatic(uri: string): Koa.Middleware {
28
const folder = uri.slice(2,-1) // we know folder is very similar to uri
29
let cache: Record<string, Promise<string>> = {}
31
- subscribe(customHtmlState, () => cache = {}) // reset cache at every change
32
- return async (ctx) => {
30
+ customHtml.emitter.on('change', () => cache = {}) // reset cache at every change
31
+ return async ctx => {
32
if (!logGui.get())
33
ctx.state.dontLog = true
34
if(ctx.method === 'OPTIONS') {
src/watchLoad.ts
+6
-2
@@ -3,27 +3,30 @@
3
import { FSWatcher, watch } from 'fs'
4
import fs from 'fs/promises'
5
import { debounceAsync, readFileBusy } from './misc'
6
+import { BetterEventEmitter } from './events'
7
8
export type WatchLoadCanceller = () => void
9
10
interface Options { failedOnFirstAttempt?: ()=>void, immediateFirst?: boolean }
11
12
type WriteFile = (data: string, options?: { reparse: boolean }) => Promise<void>
12
-interface WatchLoadReturn { unwatch:WatchLoadCanceller, save: WriteFile, getText: () => string | undefined, getPath: () => string }
13
+interface WatchLoadReturn { unwatch:WatchLoadCanceller, save: WriteFile, emitter: BetterEventEmitter, getText: () => string | undefined, getPath: () => string }
14
export function watchLoad(path:string, parser:(data:any)=>void|Promise<void>, { failedOnFirstAttempt, immediateFirst }:Options={}): WatchLoadReturn {
15
let doing = false
16
let watcher: FSWatcher | undefined
17
const debounced = debounceAsync(load, { wait: 500, maxWait: 1000 })
18
let retry: NodeJS.Timeout
19
let last: string | undefined
20
+ const emitter = new BetterEventEmitter()
21
install(true)
22
const save = debounceAsync(async (data: string, { reparse=false }={}) => {
23
await fs.writeFile(path, data, 'utf8')
24
last = data
25
if (reparse)
26
await parser(data)
27
+ emitter.emit('change', last)
28
})
26
- return { unwatch, save, getText: () => last, getPath: () => path }
29
+ return { unwatch, save, emitter, getText: () => last, getPath: () => path }
30
31
function install(first=false) {
32
try {
@@ -60,6 +63,7 @@ export function watchLoad(path:string, parser:(data:any)=>void|Promise<void>, {
63
if (text === last)
64
return
65
last = text
66
+ emitter.emit('change', last)
67
console.debug('loaded', path)
68
unwatch(); install() // reinstall, as the original file could have been renamed. We watch by the name.
69
await parser(text)