@samitouri / QOSami-HFS / commits / 4d757b39

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)