admin/options: default value for all frontend options
Massimo Melina committed
Oct 28, 2023 at 19:33 UTC
dbeb416d9d22cc46f61257726da49cbecb2e144b
9 files changed
+87
-93
admin/src/OptionsPage.ts
+23
-17
@@ -1,13 +1,13 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import { Box, Button, FormHelperText } from '@mui/material';
3
+import { Box, Button, Divider, FormHelperText } from '@mui/material';
4
import { createElement as h, Fragment, useEffect, useRef } from 'react';
5
import { apiCall, useApiEx } from './api'
6
import { state, useSnapState } from './state'
7
import { Link as RouterLink } from 'react-router-dom'
8
import { CardMembership, EditNote, Refresh, Warning } from '@mui/icons-material'
9
-import { Dict, iconTooltip, InLink, LinkBtn, MAX_TILES_SIZE, modifiedSx, REPO_URL, ipLocalHost,
10
- wait, wikiLink, with_, try_, ipForUrl, useBreakpoint } from './misc'
9
+import { Dict, iconTooltip, InLink, LinkBtn, MAX_TILE_SIZE, modifiedSx, REPO_URL, ipLocalHost,
10
+ wait, wikiLink, with_, try_, ipForUrl, SORT_BY_OPTIONS, THEME_OPTIONS, useBreakpoint } from './misc'
11
import { Form, BoolField, NumberField, SelectField, FieldProps, Field, StringField } from '@hfs/mui-grid-form';
12
import { ArrayField } from './ArrayField'
13
import FileField from './FileField'
@@ -116,21 +116,18 @@ export default function OptionsPage() {
116
httpsEnabled && { k: 'private_key', comp: FileField, md: 4, label: "HTTPS private key file",
117
...with_(status?.https.error, e => isKeyError(e) ? { error: true, helperText: e } : null)
118
},
119
- { k: 'open_browser_at_start', comp: BoolField, label: "Open Admin-panel at start", md: 4,
119
+ { k: 'favicon', comp: FileField, placeholder: "None", fileMask: '*.png|*.ico|*.jpg|*.jpeg|*.gif|*.svg',
120
+ helperText: "The icon associated to your website" },
121
+ { k: 'allowed_referer', placeholder: "any", label: "Links from other websites", comp: AllowedReferer },
122
+ { k: 'open_browser_at_start', comp: BoolField, label: "Open Admin-panel at start",
123
helperText: "Browser is automatically launched with HFS"
124
},
122
- { k: 'localhost_admin', comp: BoolField, label: "Unprotected admin on localhost", md: 4,
125
+ { k: 'localhost_admin', comp: BoolField, label: "Unprotected admin on localhost",
126
getError: x => !x && admins?.length===0 && "First create at least one admin account",
127
helperText: "Access Admin-panel without entering credentials"
128
},
126
- { k: 'file_menu_on_link', comp: SelectField, label: "Access file menu", sm: 12, md: 4,
127
- options: { "by clicking on file name": true, "by dedicated button": false }
128
- },
129
{ k: 'max_kbps', ...maxSpeedDefaults, label: "Limit output", helperText: "Doesn't apply to localhost" },
130
{ k: 'max_kbps_per_ip', ...maxSpeedDefaults, label: "Limit output per-ip" },
131
- { k: 'title', helperText: "You can see this in the tab of your browser" },
132
- { k: 'favicon', comp: FileField, placeholder: "None", fileMask: '*.png|*.ico|*.jpg|*.jpeg|*.gif|*.svg',
133
- helperText: "The icon associated to your website" },
131
{ k: 'log', label: logLabels.log, md: 3, helperText: "Requests are logged here" },
132
{ k: 'error_log', label: logLabels.error_log, md: 3, placeholder: "errors go to main log",
133
helperText: "If you want errors in a different log"
@@ -141,13 +138,12 @@ export default function OptionsPage() {
138
{ k: 'dont_log_net', comp: NetmaskField, label: "Don't log address", md: 3, placeholder: "no exception",
139
helperText: h(WildcardsSupported)
140
},
144
- { k: 'log_gui', comp: BoolField, label: "Log interface loading" },
145
- { k: 'log_api', comp: BoolField, label: "Log API requests" },
141
+ { k: 'log_gui', sm: 3, comp: BoolField, label: "Log interface loading" },
142
+ { k: 'log_api', sm: 3, comp: BoolField, label: "Log API requests" },
143
{ k: 'proxies', comp: NumberField, min: 0, max: 9, sm: 6, label: "How many HTTP proxies between this server and users?",
144
error: proxyWarning(values, status),
145
helperText: "Wrong number will prevent detection of users' IP address"
146
},
150
- { k: 'allowed_referer', placeholder: "any", label: "Links from other websites", comp: AllowedReferer },
147
{ k: 'dont_overwrite_uploading', comp: BoolField, label: "Don't overwrite uploading",
148
helperText: "Files will be numbered to avoid overwriting" },
149
{ k: 'delete_unfinished_uploads_after', comp: NumberField, md: 3, min : 0, unit: "seconds", placeholder: "Never",
@@ -158,8 +154,7 @@ export default function OptionsPage() {
154
{ k: 'session_duration', comp: NumberField, sm: 3, min: 5, unit: "seconds", required: true },
155
{ k: 'zip_calculate_size_for_seconds', comp: NumberField, sm: 3, label: "Calculate ZIP size for", unit: "seconds",
156
helperText: "If time is not enough, the browser will not show download percentage" },
161
- { k: 'update_to_beta', comp: BoolField, md: 3, helperText: "Include betas searching updates" },
162
- { k: 'tiles_size', comp: NumberField, md: 3, min: 0, max: MAX_TILES_SIZE, label: "Default tiles size", helperText: "Zero for list mode" },
157
+ { k: 'update_to_beta', comp: BoolField, helperText: "Include betas searching updates" },
158
{ k: 'admin_net', comp: NetmaskField, label: "Admin-panel accessible from", placeholder: "any address",
159
helperText: h(Fragment, {}, "IP address of browser machine. ", h(WildcardsSupported))
160
},
@@ -190,7 +185,18 @@ export default function OptionsPage() {
185
},
186
{ k: 'server_code', comp: TextEditorField, sm: 12, getError: v => try_(() => new Function(v) && null, e => e.message),
187
helperText: md(`This code works similarly to [a plugin](${REPO_URL}blob/main/dev-plugins.md) (with some limitations)`)
193
- }
188
+ },
189
+ h(Divider, {}, "Front-end options", h(Box, { fontSize: 'small' }, "Following options affect only the front-end")),
190
+ { k: 'file_menu_on_link', comp: SelectField, label: "Access file menu", sm: 6, md: 4,
191
+ options: { "by clicking on file name": true, "by dedicated button": false }
192
+ },
193
+ { k: 'title', md: 8, helperText: "You can see this in the tab of your browser" },
194
+ { k: 'tile_size', comp: NumberField, xs: 6, sm: 4, min: 0, max: MAX_TILE_SIZE, label: "Default tiles size", helperText: "Zero = list mode" },
195
+ { k: 'theme', comp: SelectField, xs: 6, sm: 4, options: THEME_OPTIONS },
196
+ { k: 'sort_by', comp: SelectField, xs: 6, sm: 4, options: SORT_BY_OPTIONS },
197
+ { k: 'invert_order', comp: BoolField, xs: 6, sm: 4, },
198
+ { k: 'folders_first', comp: BoolField, xs: 6, sm: 4, },
199
+ { k: 'sort_numerics', comp: BoolField, xs: 6, sm: 4, label: "Sort numeric names" },
200
]
201
})
202
frontend/src/App.ts
+3
-3
@@ -13,14 +13,14 @@ import { Spinner } from "./components"
13
function App() {
14
useTheme()
15
const { ready } = useSnapshot(pageState) // wait for all plugins to be loaded
16
- const { messageOnly, tilesSize=0 } = useSnapState()
16
+ const { messageOnly, tile_size=0 } = useSnapState()
17
if (messageOnly)
18
return h('h1', { style: { textAlign: 'center'} }, messageOnly)
19
if (!ready)
20
return h(Spinner, { style: { margin: 'auto' } })
21
- const style = { '--tiles-size': tilesSize }
21
+ const style = { '--tile-size': tile_size }
22
return h(I18Nprovider, {},
23
- h('div', { className: tilesSize ? 'tiles-mode' : 'list-mode', style },
23
+ h('div', { className: tile_size ? 'tiles-mode' : 'list-mode', style },
24
h(BrowserRouter, {},
25
h(NavigationExtractor, {},
26
h(Routes, {},
frontend/src/BrowseFiles.ts
+9
-20
@@ -1,19 +1,9 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
import { Link } from 'react-router-dom'
4
-import {
5
- createElement as h,
6
- Fragment,
7
- memo,
8
- MouseEvent,
9
- useCallback,
10
- useEffect,
11
- useMemo,
12
- useRef,
13
- useState
14
-} from 'react'
4
+import { createElement as h, Fragment, memo, MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
5
import { useWindowSize } from 'usehooks-ts'
16
-import { domOn, formatBytes, ErrorMsg, hIcon, getHFS } from './misc'
6
+import { domOn, formatBytes, ErrorMsg, hIcon, onlyTruthy } from './misc'
7
import { Checkbox, CustomCode, Spinner } from './components'
8
import { Head } from './Head'
9
import { DirEntry, state, useSnapState } from './state'
@@ -184,7 +174,7 @@ const PAGE_SEPARATOR_CLASS = 'page-separator'
174
interface EntryProps { entry: DirEntry, midnight: Date, separator?: string }
175
const Entry = memo(({ entry, midnight, separator }: EntryProps) => {
176
const { uri, isFolder } = entry
187
- const { showFilter, selected } = useSnapState()
177
+ const { showFilter, selected, file_menu_on_link } = useSnapState()
178
const containerDir = isFolder ? '' : uri.substring(0, uri.lastIndexOf('/')+1)
179
const containerName = containerDir && entry.n.slice(0, -entry.name.length)
180
let className = isFolder ? 'folder' : 'file'
@@ -193,10 +183,9 @@ const Entry = memo(({ entry, midnight, separator }: EntryProps) => {
183
if (separator)
184
className += ' ' + PAGE_SEPARATOR_CLASS
185
const ico = getEntryIcon(entry)
196
- const menuOnLink = getHFS().fileMenuOnLink
197
- const onClick = !entry.web && menuOnLink && fileMenu || undefined
186
+ const onClick = !entry.web && file_menu_on_link && fileMenu || undefined
187
const small = useWindowSize().width < 800
199
- const showingButton = !menuOnLink || isFolder && small
188
+ const showingButton = !file_menu_on_link || isFolder && small
189
return h('li', { className, label: separator },
190
h(CustomCode, { name: 'entry', props: { entry }, ifEmpty: () => h(Fragment, {},
191
showFilter && h(Checkbox, {
@@ -211,7 +200,7 @@ const Entry = memo(({ entry, midnight, separator }: EntryProps) => {
200
isFolder ? h(Fragment, {},
201
h(Link, { to: uri }, ico, entry.n.slice(0,-1)),
202
// popup button is here to be able to detect link-wrapper:hover
214
- menuOnLink && !showingButton && h('button', { className: 'popup-menu-button', onClick: fileMenu }, hIcon('menu'), t`Menu`)
203
+ file_menu_on_link && !showingButton && h('button', { className: 'popup-menu-button', onClick: fileMenu }, hIcon('menu'), t`Menu`)
204
)
205
: containerDir ? h(Fragment, {},
206
h('a', { href: uri, onClick, tabIndex: -1 }, ico),
@@ -232,11 +221,11 @@ const Entry = memo(({ entry, midnight, separator }: EntryProps) => {
221
function fileMenu(ev: MouseEvent) {
222
if (ev.altKey || ev.ctrlKey || ev.metaKey) return
223
ev.preventDefault()
235
- openFileMenu(entry, ev, [
236
- menuOnLink && 'open',
224
+ openFileMenu(entry, ev, onlyTruthy([
225
+ file_menu_on_link && 'open',
226
'delete',
227
'show'
239
- ])
228
+ ]))
229
}
230
231
})
frontend/src/index.scss
+4
-4
@@ -499,10 +499,10 @@ button label {
499
#root>.tiles-mode {
500
max-width: none;
501
margin: 0 1em;
502
- --tiles-size: 5;
502
+ --tile-size: 5;
503
--name-lines: 3;
504
- --name-height: calc(4.7em + var(--tiles-size) * .4em + 1.2em * var(--name-lines));
505
- --tile-width: calc(5em + 1em * var(--tiles-size));
504
+ --name-height: calc(4.7em + var(--tile-size) * .4em + 1.2em * var(--name-lines));
505
+ --tile-width: calc(5em + 1em * var(--tile-size));
506
507
ul.dir {
508
display: grid;
@@ -531,7 +531,7 @@ button label {
531
a {
532
&:last-of-type { padding: 0; }
533
span { display: block; }
534
- .icon.icon { font-size: calc(1.2rem + .6rem * var(--tiles-size)); }
534
+ .icon.icon { font-size: calc(1.2rem + .6rem * var(--tile-size)); }
535
img.icon {
536
width: auto;
537
height: 1em;
frontend/src/options.ts
+16
-16
@@ -4,12 +4,12 @@ import { newDialog } from './dialog'
4
import { state, useSnapState } from './state'
5
import { createElement as h } from 'react'
6
import { Checkbox, FlexV, Select } from './components'
7
-import { hIcon, MAX_TILES_SIZE } from './misc'
7
+import { FRONTEND_OPTIONS, hIcon, MAX_TILE_SIZE, SORT_BY_OPTIONS, THEME_OPTIONS } from './misc'
8
import { MenuLink } from './menu'
9
import { t } from './i18n'
10
+import _ from 'lodash'
11
12
export function showOptions (){
12
- const options = ['name', 'extension', 'size', 'time']
13
newDialog({
14
title: t`Options`,
15
className: 'options-dialog',
@@ -27,48 +27,48 @@ export function showOptions (){
27
target: 'admin',
28
}),
29
h(Select, {
30
- options: options.map(x => ({
30
+ options: SORT_BY_OPTIONS.map(x => ({
31
value: x,
32
label: t("Sort by:", { by: t(x) }, t`Sort by` + ': ' + t(x))
33
})),
34
- value: snap.sortBy,
34
+ value: snap.sort_by,
35
onChange(v) {
36
- state.sortBy = v
36
+ state.sort_by = v
37
}
38
}),
39
h(Checkbox, {
40
- value: snap.invertOrder,
40
+ value: snap.invert_order,
41
onChange(v) {
42
- state.invertOrder = v
42
+ state.invert_order = v
43
}
44
}, t`Invert order`),
45
h(Checkbox, {
46
- value: snap.foldersFirst,
46
+ value: snap.folders_first,
47
onChange(v) {
48
- state.foldersFirst = v
48
+ state.folders_first = v
49
}
50
}, t`Folders first`),
51
h(Checkbox, {
52
- value: snap.sortNumerics,
52
+ value: snap.sort_numerics,
53
onChange(v) {
54
- state.sortNumerics = v
54
+ state.sort_numerics = v
55
}
56
}, t`Numeric names`),
57
58
h('div', {},
59
- h('div', {}, t`Tiles mode:`, ' ', state.tilesSize || t`off`),
59
+ h('div', {}, t`Tiles mode:`, ' ', state.tile_size || t`off`),
60
h('input', {
61
type: 'range',
62
- min: 0, max: MAX_TILES_SIZE,
63
- value: snap.tilesSize || 0,
62
+ min: 0, max: MAX_TILE_SIZE,
63
+ value: snap.tile_size,
64
onChange(ev: any) {
65
- state.tilesSize = Number(ev.target.value)
65
+ state.tile_size = Number(ev.target.value)
66
}
67
}),
68
),
69
70
h(Select, {
71
- options: ['', 'light', 'dark'].map(s => ({ label: t(["theme:", "Theme:", ]) + ' ' + t(s || "auto"), value: s })),
71
+ options: _.map(THEME_OPTIONS, (value, label) => ({ label: t(["theme:", "Theme:", ]) + ' ' + t(label), value })),
72
value: snap.theme,
73
onChange(v) {
74
state.theme = v
frontend/src/state.ts
+6
-15
@@ -3,9 +3,9 @@
3
import _ from 'lodash'
4
import { proxy, useSnapshot } from 'valtio'
5
import { subscribeKey } from 'valtio/utils'
6
-import { Dict, getHFS, hIcon } from './misc'
6
+import { FRONTEND_OPTIONS, getHFS, hIcon, objSameKeys, typedKeys } from './misc'
7
8
-export const state = proxy<{
8
+export const state = proxy<typeof FRONTEND_OPTIONS & {
9
stopSearch?: ()=>void,
10
stoppedSearch?: boolean,
11
iconsReady: boolean,
@@ -19,11 +19,6 @@ export const state = proxy<{
19
showFilter: boolean,
20
selected: { [uri:string]: true }, // by using an object instead of an array, Entry components are not rendered when others get selected
21
remoteSearch: string,
22
- sortBy: string,
23
- invertOrder: boolean,
24
- foldersFirst: boolean,
25
- sortNumerics: boolean,
26
- theme: string,
22
adminUrl?: string,
23
loginRequired?: boolean, // force user to login before proceeding
24
messageOnly?: string, // no gui, just show this message
@@ -34,12 +29,11 @@ export const state = proxy<{
29
can_archive?: boolean
30
can_comment?: boolean
31
}
37
- tilesSize: number
32
canChangePassword: boolean
33
}>({
34
canChangePassword: false,
35
props: {},
42
- tilesSize: getHFS().tilesSize || 0,
36
+ ...objSameKeys(FRONTEND_OPTIONS, (v,k) => getHFS()[k] ?? v),
37
iconsReady: false,
38
username: '',
39
list: [],
@@ -49,11 +43,6 @@ export const state = proxy<{
43
showFilter: false,
44
selected: {},
45
remoteSearch: '',
52
- sortBy: 'name',
53
- invertOrder: false,
54
- foldersFirst: true,
55
- sortNumerics: false,
56
- theme: '',
46
})
47
48
export function useSnapState() {
@@ -61,7 +50,9 @@ export function useSnapState() {
50
}
51
52
const SETTINGS_KEY = 'hfs_settings'
64
-const SETTINGS_TO_STORE: (keyof typeof state)[] = ['sortBy', 'sortNumerics', 'invertOrder', 'foldersFirst', 'theme', 'tilesSize']
53
+type StateKey = keyof typeof state
54
+const SETTINGS_WITHOUT_GUI: StateKey[] = ['file_menu_on_link']
55
+const SETTINGS_TO_STORE: StateKey[] = _.difference(typedKeys(FRONTEND_OPTIONS), SETTINGS_WITHOUT_GUI)
56
57
loadSettings()
58
for (const k of SETTINGS_TO_STORE)
frontend/src/useFetchList.ts
+9
-9
@@ -118,23 +118,23 @@ export function reloadList() {
118
state.listReloader = Date.now()
119
}
120
121
-const { compare:localCompare } = new Intl.Collator(navigator.language)
121
+const { compare: localCompare } = new Intl.Collator(navigator.language)
122
123
function sort(list: DirList) {
124
- const { sortBy, foldersFirst, sortNumerics } = state
124
+ const { sort_by, folders_first, sort_numerics } = state
125
// optimization: precalculate string comparisons
126
- const bySize = sortBy === 'size'
127
- const byExt = sortBy === 'extension'
128
- const byTime = sortBy === 'time'
129
- const invert = state.invertOrder ? -1 : 1
126
+ const bySize = sort_by === 'size'
127
+ const byExt = sort_by === 'extension'
128
+ const byTime = sort_by === 'time'
129
+ const invert = state.invert_order ? -1 : 1
130
return list.sort((a,b) =>
131
- foldersFirst && -compare(a.isFolder, b.isFolder)
131
+ folders_first && -compare(a.isFolder, b.isFolder)
132
|| invert * (bySize ? compare(a.s||0, b.s||0)
133
: byExt ? localCompare(a.ext, b.ext)
134
: byTime ? compare(a.t, b.t)
135
: 0
136
)
137
- || sortNumerics && (invert * compare(parseFloat(a.n), parseFloat(b.n)))
137
+ || sort_numerics && (invert * compare(parseFloat(a.n), parseFloat(b.n)))
138
|| invert * localCompare(a.n, b.n) // fallback to name/path
139
)
140
}
@@ -146,7 +146,7 @@ function compare(a:any, b:any) {
146
147
// update list on sorting criteria
148
const sortAgain = _.debounce(()=> state.list = sort(state.list), 100)
149
-for (const k of [ 'sortBy', 'invertOrder', 'foldersFirst', 'sortNumerics'] as const)
149
+for (const k of [ 'sort_by', 'invert_order', 'folders_first', 'sort_numerics'] as const)
150
subscribeKey(state, k, sortAgain)
151
152
subscribeKey(state, 'patternFilter', updateFilteredList)
src/cross.ts
+12
-1
@@ -9,7 +9,18 @@ export const WIKI_URL = REPO_URL + 'wiki/'
9
export const MINUTE = 60_000
10
export const HOUR = 60 * MINUTE
11
export const DAY = 24 * HOUR
12
-export const MAX_TILES_SIZE = 10
12
+export const MAX_TILE_SIZE = 10
13
+export const FRONTEND_OPTIONS = {
14
+ file_menu_on_link: true,
15
+ tile_size: 0,
16
+ sort_by: 'name',
17
+ invert_order: false,
18
+ folders_first: true,
19
+ sort_numerics: false,
20
+ theme: '',
21
+}
22
+export const SORT_BY_OPTIONS = ['name', 'extension', 'size', 'time']
23
+export const THEME_OPTIONS = { auto: '', light: 'light', dark: 'dark' }
24
25
export type Dict<T=any> = Record<string, T>
26
export type Falsy = false | null | undefined | '' | 0
src/serveGuiFiles.ts
+5
-8
@@ -17,16 +17,17 @@ import { getPluginConfigFields, getPluginInfo, mapPlugins, pluginsConfig } from
17
import { refresh_session } from './api.auth'
18
import { ApiError } from './apiMiddleware'
19
import { join, extname } from 'path'
20
-import { getOrSet, newObj, onlyTruthy } from './misc'
20
+import { FRONTEND_OPTIONS, getOrSet, newObj, onlyTruthy } from './misc'
21
import { favicon, title } from './adminApis'
22
import { subscribe } from 'valtio/vanilla'
23
import { customHtmlState, getSection } from './customHtml'
24
import _ from 'lodash'
25
-import { defineConfig } from './config'
25
+import { defineConfig, getConfig } from './config'
26
import { getLangData } from './lang'
27
import { MIME_AUTO } from './vfs'
28
29
const logGui = defineConfig('log_gui', false)
30
+_.each(FRONTEND_OPTIONS, (v,k) => defineConfig(k, v)) // define default values
31
32
// in case of dev env we have our static files within the 'dist' folder'
33
const DEV_STATIC = process.env.DEV ? 'dist/' : ''
@@ -107,9 +108,8 @@ async function treatIndex(ctx: Koa.Context, filesUri: string, body: string) {
108
plugins,
109
prefixUrl: ctx.state.revProxyPath,
110
customHtml: _.omit(Object.fromEntries(customHtmlState.sections),
110
- ['top','bottom']), // excluding sections we apply in this phase
111
- fileMenuOnLink: fileMenuOnLink.get(),
112
- tilesSize: tilesSize.get(),
111
+ ['top','bottom']), // exclude the sections we already apply in this phase
112
+ ...newObj(FRONTEND_OPTIONS, (v, k) => getConfig(k)),
113
lang
114
}, null, 4)
115
.replace(/<(\/script)/g, '<"+"$1') /*avoid breaking our script container*/}
@@ -166,6 +166,3 @@ function serveProxied(port: string | undefined, uri: string) { // used for devel
166
export function serveGuiFiles(proxyPort:string | undefined, uri:string) {
167
return serveProxied(proxyPort, uri) || serveStatic(uri)
168
}
169
-
170
-const fileMenuOnLink = defineConfig('file_menu_on_link', true)
171
-const tilesSize = defineConfig('tiles_size', 0)
\ No newline at end of file