hfs 0.26.8 → 0.27.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +15 -2
  2. package/admin/assets/index-509bb1d6.js +415 -0
  3. package/admin/assets/index-60a380a7.css +1 -0
  4. package/admin/assets/sha512-738f0943.js +8 -0
  5. package/admin/index.html +3 -1
  6. package/admin/{public/logo.svg → logo.svg} +0 -0
  7. package/frontend/assets/index-6e178dfd.css +1 -0
  8. package/frontend/assets/index-aea7654e.js +85 -0
  9. package/frontend/assets/sha512-bf915587.js +8 -0
  10. package/frontend/{public/fontello.css → fontello.css} +0 -0
  11. package/frontend/{public/fontello.woff2 → fontello.woff2} +0 -0
  12. package/frontend/index.html +4 -2
  13. package/package.json +2 -6
  14. package/plugins/vhosting/plugin.js +23 -20
  15. package/src/QuickZipStream.js +285 -0
  16. package/src/ThrottledStream.js +93 -0
  17. package/src/adminApis.js +169 -0
  18. package/src/api.accounts.js +59 -0
  19. package/src/api.auth.js +128 -0
  20. package/src/api.file_list.js +110 -0
  21. package/src/api.helpers.js +32 -0
  22. package/src/api.monitor.js +104 -0
  23. package/src/api.plugins.js +128 -0
  24. package/src/api.vfs.js +167 -0
  25. package/src/apiMiddleware.js +123 -0
  26. package/src/block.js +34 -0
  27. package/src/commands.js +125 -0
  28. package/src/config.js +168 -0
  29. package/src/connections.js +57 -0
  30. package/src/const.js +94 -0
  31. package/src/crypt.js +21 -0
  32. package/src/debounceAsync.js +49 -0
  33. package/src/events.js +9 -0
  34. package/src/frontEndApis.js +38 -0
  35. package/src/github.js +104 -0
  36. package/src/index.js +57 -0
  37. package/src/listen.js +235 -0
  38. package/src/log.js +137 -0
  39. package/src/middlewares.js +195 -0
  40. package/src/misc.js +160 -0
  41. package/src/pbkdf2.js +74 -0
  42. package/src/perm.js +183 -0
  43. package/src/plugins.js +343 -0
  44. package/src/serveFile.js +105 -0
  45. package/src/serveGuiFiles.js +113 -0
  46. package/src/sse.js +30 -0
  47. package/src/throttler.js +91 -0
  48. package/src/update.js +70 -0
  49. package/src/util-files.js +163 -0
  50. package/src/util-generators.js +31 -0
  51. package/src/util-http.js +32 -0
  52. package/src/vfs.js +232 -0
  53. package/src/watchLoad.js +73 -0
  54. package/src/zip.js +73 -0
  55. package/admin/.DS_Store +0 -0
  56. package/admin/.eslintrc +0 -8
  57. package/admin/.gitignore +0 -23
  58. package/admin/package.json +0 -67
  59. package/admin/src/AccountForm.ts +0 -92
  60. package/admin/src/AccountsPage.ts +0 -143
  61. package/admin/src/App.ts +0 -83
  62. package/admin/src/ArrayField.ts +0 -84
  63. package/admin/src/ConfigPage.ts +0 -279
  64. package/admin/src/FileField.ts +0 -52
  65. package/admin/src/FileForm.ts +0 -148
  66. package/admin/src/FilePicker.ts +0 -166
  67. package/admin/src/HomePage.ts +0 -96
  68. package/admin/src/InstalledPlugins.ts +0 -158
  69. package/admin/src/LoginRequired.ts +0 -75
  70. package/admin/src/LogoutPage.ts +0 -27
  71. package/admin/src/LogsPage.ts +0 -75
  72. package/admin/src/MainMenu.ts +0 -74
  73. package/admin/src/MenuButton.ts +0 -38
  74. package/admin/src/MonitorPage.ts +0 -200
  75. package/admin/src/OnlinePlugins.ts +0 -101
  76. package/admin/src/PermField.ts +0 -80
  77. package/admin/src/PluginsPage.ts +0 -27
  78. package/admin/src/VfsMenuBar.ts +0 -58
  79. package/admin/src/VfsPage.ts +0 -124
  80. package/admin/src/VfsTree.ts +0 -95
  81. package/admin/src/addFiles.ts +0 -59
  82. package/admin/src/api.ts +0 -246
  83. package/admin/src/dialog.ts +0 -203
  84. package/admin/src/index.css +0 -21
  85. package/admin/src/index.ts +0 -10
  86. package/admin/src/md.ts +0 -31
  87. package/admin/src/misc.ts +0 -141
  88. package/admin/src/react-app-env.d.ts +0 -1
  89. package/admin/src/reportWebVitals.ts +0 -15
  90. package/admin/src/setupTests.ts +0 -5
  91. package/admin/src/state.ts +0 -40
  92. package/admin/src/theme.ts +0 -37
  93. package/admin/tsconfig.json +0 -26
  94. package/admin/vite.config.ts +0 -32
  95. package/frontend/.DS_Store +0 -0
  96. package/frontend/.eslintrc +0 -8
  97. package/frontend/.gitignore +0 -23
  98. package/frontend/package.json +0 -51
  99. package/frontend/src/App.ts +0 -25
  100. package/frontend/src/Breadcrumbs.ts +0 -43
  101. package/frontend/src/BrowseFiles.ts +0 -141
  102. package/frontend/src/Head.ts +0 -45
  103. package/frontend/src/UserPanel.ts +0 -52
  104. package/frontend/src/api.ts +0 -78
  105. package/frontend/src/components.ts +0 -54
  106. package/frontend/src/dialog.css +0 -76
  107. package/frontend/src/dialog.ts +0 -105
  108. package/frontend/src/icons.ts +0 -46
  109. package/frontend/src/index.scss +0 -307
  110. package/frontend/src/index.ts +0 -10
  111. package/frontend/src/login.ts +0 -50
  112. package/frontend/src/menu.ts +0 -188
  113. package/frontend/src/misc.ts +0 -54
  114. package/frontend/src/options.ts +0 -52
  115. package/frontend/src/react-app-env.d.ts +0 -1
  116. package/frontend/src/reportWebVitals.ts +0 -15
  117. package/frontend/src/setupTests.ts +0 -5
  118. package/frontend/src/state.ts +0 -82
  119. package/frontend/src/useAuthorized.ts +0 -17
  120. package/frontend/src/useFetchList.ts +0 -144
  121. package/frontend/src/useTheme.ts +0 -23
  122. package/frontend/tsconfig.json +0 -26
  123. package/frontend/vite.config.ts +0 -21
  124. package/src/QuickZipStream.ts +0 -279
  125. package/src/ThrottledStream.ts +0 -98
  126. package/src/adminApis.ts +0 -161
  127. package/src/api.accounts.ts +0 -78
  128. package/src/api.auth.ts +0 -131
  129. package/src/api.file_list.ts +0 -102
  130. package/src/api.helpers.ts +0 -30
  131. package/src/api.monitor.ts +0 -106
  132. package/src/api.plugins.ts +0 -139
  133. package/src/api.vfs.ts +0 -182
  134. package/src/apiMiddleware.ts +0 -124
  135. package/src/block.ts +0 -35
  136. package/src/commands.ts +0 -122
  137. package/src/config.ts +0 -166
  138. package/src/connections.ts +0 -60
  139. package/src/const.ts +0 -57
  140. package/src/crypt.ts +0 -16
  141. package/src/debounceAsync.ts +0 -51
  142. package/src/events.ts +0 -6
  143. package/src/frontEndApis.ts +0 -17
  144. package/src/github.ts +0 -102
  145. package/src/index.ts +0 -53
  146. package/src/listen.ts +0 -220
  147. package/src/log.ts +0 -128
  148. package/src/middlewares.ts +0 -176
  149. package/src/misc.ts +0 -149
  150. package/src/pbkdf2.ts +0 -83
  151. package/src/perm.ts +0 -194
  152. package/src/plugins.ts +0 -342
  153. package/src/serveFile.ts +0 -104
  154. package/src/serveGuiFiles.ts +0 -95
  155. package/src/sse.ts +0 -29
  156. package/src/throttler.ts +0 -106
  157. package/src/update.ts +0 -67
  158. package/src/util-files.ts +0 -137
  159. package/src/util-generators.ts +0 -29
  160. package/src/util-http.ts +0 -29
  161. package/src/vfs.ts +0 -258
  162. package/src/watchLoad.ts +0 -75
  163. package/src/zip.ts +0 -69
package/admin/src/misc.ts DELETED
@@ -1,141 +0,0 @@
1
- // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
-
3
- import { createElement as h, FC, ReactNode } from 'react'
4
- import { Box, Breakpoint, CircularProgress, IconButton, Link, Tooltip, useMediaQuery } from '@mui/material'
5
- import { Link as RouterLink } from 'react-router-dom'
6
- import { SxProps } from '@mui/system'
7
- import { SvgIconComponent } from '@mui/icons-material'
8
- import { alertDialog, confirmDialog } from './dialog'
9
- import { apiCall } from './api'
10
- import { onlyTruthy, useStateMounted } from '@hfs/shared'
11
- export * from '@hfs/shared'
12
-
13
- export function spinner() {
14
- return h(CircularProgress)
15
- }
16
-
17
- export function isWindowsDrive(s?: string) {
18
- return s && /^[a-zA-Z]:$/.test(s)
19
- }
20
-
21
- export function isEqualLax(a: any,b: any): boolean {
22
- return a == b //eslint-disable-line
23
- || (a && b && typeof a === 'object' && typeof b === 'object'
24
- && Object.entries(a).every(([k,v]) => isEqualLax(v, b[k])) )
25
- }
26
-
27
- export function modifiedSx(is: boolean) {
28
- return is ? { outline: '2px solid' } : undefined
29
- }
30
-
31
- interface IconBtnProps {
32
- title?: ReactNode
33
- icon: SvgIconComponent
34
- disabled?: boolean | string
35
- progress?: boolean | number
36
- link?: string
37
- confirm?: string
38
- [rest: string]: any
39
- }
40
-
41
- export function IconBtn({ title, icon, onClick, disabled, progress, link, tooltipProps, confirm, ...rest }: IconBtnProps) {
42
- const [loading, setLoading] = useStateMounted(false)
43
- if (typeof disabled === 'string')
44
- title = disabled
45
- if (link)
46
- onClick = () => window.open(link)
47
- let ret: ReturnType<FC> = h(IconButton, {
48
- disabled: Boolean(loading || progress || disabled),
49
- ...rest,
50
- async onClick() {
51
- if (confirm && !await confirmDialog(confirm)) return
52
- const ret = onClick?.apply(this,arguments)
53
- if (ret && ret instanceof Promise) {
54
- setLoading(true)
55
- ret.catch(alertDialog).finally(()=> setLoading(false))
56
- }
57
- }
58
- }, h(icon))
59
- if ((progress || loading) && progress !== false) // false is also useful to inhibit behavior with loading
60
- ret = h(Box, { position:'relative', display: 'inline-block' },
61
- h(CircularProgress, {
62
- ...(typeof progress === 'number' ? { value: progress*100, variant: 'determinate' } : null),
63
- style: { position:'absolute', top: 4, left: 4, width: 32, height: 32 }
64
- }),
65
- ret
66
- )
67
- if (title)
68
- ret = h(Tooltip, { title, ...tooltipProps, children: h('span',{},ret) })
69
- return ret
70
- }
71
-
72
- export function iconTooltip(icon: SvgIconComponent, tooltip: string, sx?: SxProps) {
73
- return h(Tooltip, { title: tooltip, children: h(icon, { sx }) })
74
- }
75
-
76
- export function InLink(props:any) {
77
- return h(Link, { component: RouterLink, ...props })
78
- }
79
-
80
- export function Center(props: any) {
81
- return h(Box, { display:'flex', height:'100%', width:'100%', justifyContent:'center', alignItems:'center', ...props })
82
- }
83
-
84
- export async function manipulateConfig(k: string, work:(data:any) => any) {
85
- const cfg = await apiCall('get_config', { only: [k] })
86
- const was = cfg[k]
87
- const will = await work(was)
88
- if (JSON.stringify(was) !== JSON.stringify(will))
89
- await apiCall('set_config', { values: { [k]: will } })
90
- }
91
-
92
- export function typedKeys<T extends {}>(o: T) {
93
- return Object.keys(o) as (keyof T)[]
94
- }
95
-
96
- export function dirname(s: string) {
97
- let i = s.lastIndexOf('/')
98
- if (i < 0)
99
- i = s.lastIndexOf('\\')
100
- return i < 0 ? '' : s.slice(0, i)
101
- }
102
-
103
- export function isAbsolutePath(s: string) {
104
- return s && (s[0] === '/' || isWindowsDrive(s.slice(0,2)))
105
- }
106
-
107
- export function pathJoin(...args: any[]) {
108
- const delimiter = findFirst(args, x => /\\|\//.exec('\\a/b')?.[0])
109
- const good = onlyTruthy(args.map(x => x == null ? '' : String(x)))
110
- return good.map((x, i) => i === good.length-1 || x.endsWith('\\') || x.endsWith('/') ? x : x + delimiter)
111
- .join('')
112
- }
113
-
114
- export function findFirst<I=any, O=any>(a: I[], cb:(v:I)=>O): any {
115
- for (const x of a) {
116
- const ret = cb(x)
117
- if (ret !== undefined)
118
- return ret
119
- }
120
- }
121
-
122
- export function xlate(input: any, table: Record<string, any>) {
123
- return table[input] ?? input
124
- }
125
-
126
- // return true if same size or larger
127
- export function useBreakpoint(breakpoint: Breakpoint) {
128
- return useMediaQuery((theme: any) => theme.breakpoints.up(breakpoint), { noSsr:true }) // without noSsr, first execution always returns false
129
- }
130
-
131
- export function err2msg(code: string) {
132
- return {
133
- ENOENT: "Not found",
134
- ENOTDIR: "Not a folder",
135
- }[code] || code
136
- }
137
-
138
- export function wantArray<T>(x?: void | T | T[]) {
139
- return x == null ? [] : Array.isArray(x) ? x : [x]
140
- }
141
-
@@ -1 +0,0 @@
1
- /// <reference types="react-scripts" />
@@ -1,15 +0,0 @@
1
- import { ReportHandler } from 'web-vitals';
2
-
3
- const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4
- if (onPerfEntry && onPerfEntry instanceof Function) {
5
- import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6
- getCLS(onPerfEntry);
7
- getFID(onPerfEntry);
8
- getFCP(onPerfEntry);
9
- getLCP(onPerfEntry);
10
- getTTFB(onPerfEntry);
11
- });
12
- }
13
- };
14
-
15
- export default reportWebVitals;
@@ -1,5 +0,0 @@
1
- // jest-dom adds custom jest matchers for asserting on DOM nodes.
2
- // allows you to do things like:
3
- // expect(element).toHaveTextContent(/react/i)
4
- // learn more: https://github.com/testing-library/jest-dom
5
- import '@testing-library/jest-dom';
@@ -1,40 +0,0 @@
1
- // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
-
3
- import { proxy, useSnapshot } from 'valtio'
4
- import { Dict } from './misc'
5
- import { VfsNode } from './VfsPage'
6
- import _ from 'lodash'
7
- import { subscribeKey } from 'valtio/utils'
8
-
9
- const STORAGE_KEY = 'admin_state'
10
- export const state = proxy<{
11
- title: string
12
- config: Dict
13
- vfs: VfsNode | undefined
14
- selectedFiles: VfsNode[]
15
- loginRequired: boolean
16
- username: string
17
- onlinePluginsColumns: Dict<boolean>
18
- }>(Object.assign({
19
- title: '',
20
- config: {},
21
- selectedFiles: [],
22
- vfs: undefined,
23
- loginRequired: false,
24
- username: '',
25
- onlinePluginsColumns: {
26
- version: false,
27
- pushed_at: false,
28
- license: false,
29
- }
30
- }, JSON.parse(localStorage[STORAGE_KEY]||null)))
31
-
32
- const SETTINGS_TO_STORE: (keyof typeof state)[] = ['onlinePluginsColumns']
33
- const storeSettings = _.debounce(() =>
34
- localStorage[STORAGE_KEY] = JSON.stringify(_.pick(state, SETTINGS_TO_STORE)), 500, { maxWait: 1000 })
35
- for (const k of SETTINGS_TO_STORE)
36
- subscribeKey(state, k, storeSettings)
37
-
38
- export function useSnapState() {
39
- return useSnapshot(state)
40
- }
@@ -1,37 +0,0 @@
1
- // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
-
3
- import { createTheme, useMediaQuery } from '@mui/material'
4
- import { useMemo } from 'react'
5
-
6
- const EMPTY = {}
7
- export function useMyTheme() {
8
- const lightMode = useMediaQuery('(prefers-color-scheme: dark)') ? null : EMPTY
9
- return useMemo(() => createTheme({
10
- palette: lightMode || {
11
- mode: 'dark',
12
- text: { primary: '#bbb' },
13
- primary: { main: '#469' },
14
- },
15
- typography: {
16
- fontFamily: 'Roboto, "Noto sans", "Segoe UI", "San Francisco", "Helvetica Neue", Arial, sans-serif'
17
- },
18
- components: {
19
- MuiTextField: {
20
- defaultProps: { variant: 'filled' },
21
- styleOverrides: lightMode || {
22
- root: { '& label.Mui-focused': { color: '#ccc' } } // our primary.main is too dark for mui's dark theme, and when input element is :-webkit-autofill it will make not enough contrast
23
- }
24
- },
25
- MuiButton: {
26
- defaultProps: { variant: 'outlined' },
27
- styleOverrides: lightMode || {
28
- root({ ownerState }) {
29
- return ownerState.color === 'primary' && {
30
- color: ownerState.variant === 'contained' ? '#ddd' : '#68c'
31
- }
32
- }
33
- }
34
- }
35
- }
36
- }), [lightMode])
37
- }
@@ -1,26 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "es2017",
4
- "lib": [
5
- "dom",
6
- "dom.iterable",
7
- "esnext"
8
- ],
9
- "allowJs": true,
10
- "skipLibCheck": true,
11
- "esModuleInterop": true,
12
- "allowSyntheticDefaultImports": true,
13
- "strict": true,
14
- "forceConsistentCasingInFileNames": true,
15
- "noFallthroughCasesInSwitch": true,
16
- "module": "esnext",
17
- "moduleResolution": "node",
18
- "resolveJsonModule": true,
19
- "isolatedModules": true,
20
- "noEmit": true,
21
- "jsx": "react-jsx"
22
- },
23
- "include": [
24
- "src"
25
- ]
26
- }
@@ -1,32 +0,0 @@
1
- import { defineConfig } from 'vite'
2
- import vitePluginImport from 'vite-plugin-babel-import';
3
-
4
- // https://vitejs.dev/config/
5
- export default defineConfig({
6
- build: {
7
- outDir: '../dist/admin',
8
- emptyOutDir: true,
9
- target: "es2015",
10
- },
11
- plugins: [
12
- vitePluginImport([] || [
13
- { // this is (currently) speeding up build process, by bringing "modules transformed" from 11k+ down to 1.5k+
14
- libraryName: '@mui/icons-material',
15
- libraryDirectory: '',
16
- libraryChangeCase: "camelCase",
17
- ignoreStyles: [],
18
- },
19
- ])
20
- ],
21
- server: {
22
- port: 3006,
23
- proxy: {
24
- '/~/': {
25
- target: 'http://localhost',
26
- proxyTimeout: 2000,
27
- changeOrigin: true,
28
- ws: true,
29
- }
30
- }
31
- }
32
- })
Binary file
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "react-app",
3
- "rules": {
4
- "no-mixed-operators": 0,
5
- "no-ex-assign": 0,
6
- "no-throw-literal": 0
7
- }
8
- }
@@ -1,23 +0,0 @@
1
- # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
-
3
- # dependencies
4
- /node_modules
5
- /.pnp
6
- .pnp.js
7
-
8
- # testing
9
- /coverage
10
-
11
- # production
12
- /build
13
-
14
- # misc
15
- .DS_Store
16
- .env.local
17
- .env.development.local
18
- .env.test.local
19
- .env.production.local
20
-
21
- npm-debug.log*
22
- yarn-debug.log*
23
- yarn-error.log*
@@ -1,51 +0,0 @@
1
- {
2
- "name": "@hfs/frontend",
3
- "private": true,
4
- "proxy": "http://localhost",
5
- "scripts": {
6
- "start": "vite",
7
- "build": "tsc && vite build",
8
- "preview": "vite preview",
9
- "test-dep": "npm audit --production"
10
- },
11
- "dependencies": {
12
- "@hfs/shared": "*",
13
- "js-sha512": "^0.8.0",
14
- "lodash": "^4.17.21",
15
- "react": "^18.2.0",
16
- "react-dom": "^18.2.0",
17
- "react-router-dom": "^6.1.1",
18
- "tssrp6a": "^3.0.0",
19
- "use-debounce": "^7.0.1",
20
- "usehooks-ts": "^2.6.0",
21
- "valtio": "^1.2.7",
22
- "web-vitals": "^2.1.4"
23
- },
24
- "devDependencies": {
25
- "@types/lodash": "^4.14.178",
26
- "@types/node": "^16.11.21",
27
- "@types/react": "^18.0.15",
28
- "@types/react-dom": "^18.0.6",
29
- "cross-env": "^7.0.3",
30
- "sass": "^1.54.5",
31
- "vite": "^3.0.0"
32
- },
33
- "eslintConfig": {
34
- "extends": [
35
- "react-app",
36
- "react-app/jest"
37
- ]
38
- },
39
- "browserslist": {
40
- "production": [
41
- ">0.2%",
42
- "not dead",
43
- "not op_mini all"
44
- ],
45
- "development": [
46
- "last 1 chrome version",
47
- "last 1 firefox version",
48
- "last 1 safari version"
49
- ]
50
- }
51
- }
@@ -1,25 +0,0 @@
1
- // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
-
3
- import { BrowserRouter, Route, Routes } from "react-router-dom"
4
- import { createElement as h, Fragment } from 'react'
5
- import { BrowseFiles } from "./BrowseFiles"
6
- import { Dialogs } from './dialog'
7
- import useTheme from "./useTheme"
8
- import { useSnapState } from './state'
9
-
10
- function App() {
11
- useTheme()
12
- const { messageOnly } = useSnapState()
13
- if (messageOnly)
14
- return h('h1', { style: { textAlign: 'center'} }, messageOnly)
15
- return h(Fragment, {},
16
- h(BrowserRouter, {},
17
- h(Routes, {},
18
- h(Route, { path:'*', element: h(BrowseFiles) })
19
- )
20
- ),
21
- h(Dialogs)
22
- )
23
- }
24
-
25
- export default App;
@@ -1,43 +0,0 @@
1
- // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
-
3
- import { Link, useLocation } from 'react-router-dom'
4
- import { createElement as h, Fragment, ReactElement } from 'react'
5
- import { hIcon } from './misc'
6
- import { state } from './state'
7
- import { reloadList } from './useFetchList'
8
-
9
- export function Breadcrumbs() {
10
- const currentPath = useLocation().pathname.slice(1,-1)
11
- let prev = ''
12
- const parent = currentPath.split('/').slice(0,-1).join('/')+'/'
13
- const breadcrumbs = currentPath ? currentPath.split('/').map(x => [prev = prev + x + '/', decodeURIComponent(x)]) : []
14
- return h(Fragment, {},
15
- h(Breadcrumb, { label: hIcon('parent', { alt:'parent folder' }), path: parent }),
16
- h(Breadcrumb, { current: !currentPath, label: hIcon('home', { alt:'home' }) }),
17
- breadcrumbs.map(([path,label]) =>
18
- h(Breadcrumb, {
19
- key: path,
20
- path,
21
- label,
22
- current: path === currentPath+'/',
23
- }) )
24
- )
25
- }
26
-
27
- function Breadcrumb({ path, label, current }:{ current?: boolean, path?: string, label?: string | ReactElement }) {
28
- const PAD = '\u00A0\u00A0' // make small elements easier to tap. Don't use min-width 'cause it requires display-inline that breaks word-wrapping
29
- if (typeof label === 'string' && label.length < 3)
30
- label = PAD+label+PAD
31
- return h(Link, {
32
- className: 'breadcrumb',
33
- to: path || '/',
34
- async onClick() {
35
- if (current) {
36
- state.remoteSearch = ''
37
- state.stopSearch?.()
38
- reloadList()
39
- }
40
- }
41
- }, label)
42
- }
43
-
@@ -1,141 +0,0 @@
1
- // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
-
3
- import { Link, useLocation } from 'react-router-dom'
4
- import { createElement as h, Fragment, memo, useEffect, useMemo, useState } from 'react'
5
- import { formatBytes, hError, hfsEvent, hIcon, isMobile } from './misc'
6
- import { Checkbox, Html, Spinner } from './components'
7
- import { Head } from './Head'
8
- import { state, useSnapState } from './state'
9
- import { alertDialog } from './dialog'
10
- import useFetchList from './useFetchList'
11
- import useAuthorized from './useAuthorized'
12
-
13
- export function usePath() {
14
- return decodeURI(useLocation().pathname)
15
- }
16
-
17
- export interface DirEntry { n:string, s?:number, m?:string, c?:string,
18
- ext:string, isFolder:boolean, t?:Date } // we memoize these value for speed
19
- export type DirList = DirEntry[]
20
-
21
- export function BrowseFiles() {
22
- useFetchList()
23
- const { error, list, serverConfig } = useSnapState()
24
- return useAuthorized() && h(Fragment, {},
25
- h(Html, { code: serverConfig?.custom_header }),
26
- h(Head),
27
- hError(error)
28
- || h(list ? FilesList : Spinner))
29
- }
30
-
31
- function FilesList() {
32
- const { filteredList, list, loading, stoppedSearch } = useSnapState()
33
- const midnight = useMidnight() // as an optimization we calculate this only once per list and pass it down
34
- const pageSize = 100
35
- const [page, setPage] = useState(0)
36
- const offset = page * pageSize
37
- const theList = filteredList || list
38
- const total = theList.length
39
-
40
- useEffect(() => setPage(0), [theList])
41
- useEffect(() => document.scrollingElement?.scrollTo(0,0), [page])
42
-
43
- return h(Fragment, {},
44
- h('ul', { className: 'dir' },
45
- !list.length ? (!loading && (stoppedSearch ? "Stopped before finding anything" : "Nothing here"))
46
- : filteredList && !filteredList.length ? "No match for this filter"
47
- : theList.slice(offset, offset + pageSize).map((entry: DirEntry) =>
48
- h(Entry, { key: entry.n, midnight, ...entry })),
49
- loading && h(Spinner),
50
- ),
51
- total > pageSize && h(Paging, { total, current:page, pageSize, pageChange:setPage })
52
- )
53
- }
54
-
55
- interface PagingProps {
56
- total: number
57
- current: number
58
- pageSize: number
59
- pageChange:(newPage:number) => void
60
- }
61
- function Paging({ total, current, pageSize, pageChange }: PagingProps) {
62
- const nPages = Math.ceil(total / pageSize)
63
- const pages = []
64
- for (let i=0; i<nPages; i++)
65
- pages.push(h('button', {
66
- ...i===current && { className:'toggled' },
67
- onClick(){
68
- pageChange(i)
69
- }
70
- }, i*pageSize || 1))
71
- return h('div', { id:'paging' }, ...pages)
72
- }
73
-
74
- function useMidnight() {
75
- const [midnight, setMidnight] = useState(calcMidnight)
76
- useEffect(() => {
77
- setTimeout(()=> setMidnight(calcMidnight()), 10 * 60_000) // refresh every 10 minutes
78
- }, [])
79
- return midnight
80
-
81
- function calcMidnight() {
82
- const recent = new Date()
83
- recent.setHours(recent.getHours() - 6)
84
- const midnight = new Date()
85
- midnight.setHours(0,0,0,0)
86
- return recent < midnight ? recent : midnight
87
- }
88
- }
89
-
90
- const Entry = memo(function(entry: DirEntry & { midnight: Date }) {
91
- let { n: relativePath, isFolder } = entry
92
- const base = usePath()
93
- const { showFilter, selected } = useSnapState()
94
- const href = fixUrl(relativePath)
95
- const containerDir = isFolder ? '' : relativePath.substring(0, relativePath.lastIndexOf('/')+1)
96
- const name = relativePath.substring(containerDir.length)
97
- return h('li', { className: isFolder ? 'folder' : 'file' },
98
- showFilter && h(Checkbox, {
99
- value: selected[relativePath],
100
- onChange(v){
101
- if (v)
102
- return state.selected[relativePath] = true
103
- delete state.selected[relativePath]
104
- },
105
- }),
106
- isFolder ? h(Link, { to: base+href }, hIcon('folder'), relativePath)
107
- : h(Fragment, {},
108
- containerDir && h(Link, { to: base+fixUrl(containerDir), className:'container-folder' }, hIcon('file'), containerDir ),
109
- h('a', { href }, !containerDir && hIcon('file'), name)
110
- ),
111
- h(EntryProps, entry),
112
- h('div', { style:{ clear:'both' } })
113
- )
114
- })
115
-
116
- function fixUrl(s:string) {
117
- return s.replace(/#/g, encodeURIComponent)
118
- }
119
-
120
- const EntryProps = memo(function(entry: DirEntry & { midnight: Date }) {
121
- const { t, s } = entry
122
- const today = t && t > entry.midnight
123
- const shortTs = isMobile()
124
- const code = useMemo(()=> hfsEvent('additionalEntryProps', { entry }).join(''),
125
- [entry])
126
- return h('div', { className:'entry-props' },
127
- h(Html, { code, className:'add-props' }),
128
- s !== undefined && h(Fragment, {},
129
- h('span', { className:'entry-size' }, formatBytes(s)),
130
- " — ",
131
- ),
132
- t && h('span', {
133
- className: 'entry-ts',
134
- title: today || !shortTs ? null : t.toLocaleString(),
135
- onClick() { // mobile has no hover
136
- if (shortTs)
137
- alertDialog("Full timestamp:\n" + t.toLocaleString()).then()
138
- }
139
- }, !shortTs ? t.toLocaleString() : today ? t.toLocaleTimeString() : t.toLocaleDateString()),
140
- )
141
- })
@@ -1,45 +0,0 @@
1
- // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
-
3
- import { createElement as h, useMemo} from 'react'
4
- import { formatBytes, hIcon, prefix } from './misc'
5
- import { Spinner } from './components'
6
- import { useSnapState } from './state'
7
- import { MenuPanel } from './menu'
8
- import { Breadcrumbs } from './Breadcrumbs'
9
-
10
- export function Head() {
11
- return h('header', {},
12
- h(MenuPanel),
13
- h(Breadcrumbs),
14
- h(FolderStats),
15
- h('div', { style:{ clear:'both' }}),
16
- )
17
- }
18
-
19
- function FolderStats() {
20
- const { list, loading, filteredList, selected, stoppedSearch } = useSnapState()
21
- const stats = useMemo(() =>{
22
- let files = 0, folders = 0, size = 0
23
- for (const x of list) {
24
- if (x.isFolder)
25
- ++folders
26
- else
27
- ++files
28
- size += x.s||0
29
- }
30
- return { files, folders, size }
31
- }, [list])
32
- const sel = Object.keys(selected).length
33
- const fil = filteredList?.length
34
- return h('div', { id:'folder-stats' },
35
- stoppedSearch ? hIcon('interrupted', { title:'Search was interrupted' })
36
- : list.length>0 && loading && h(Spinner),
37
- [
38
- prefix('', stats.files,' file(s)'),
39
- prefix('', stats.folders, ' folder(s)'),
40
- stats.size ? formatBytes(stats.size) : '',
41
- sel && sel+' selected',
42
- fil !== undefined && fil < list.length && fil+' displayed',
43
- ].filter(Boolean).join(', ')
44
- )
45
- }