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