-
-
Notifications
You must be signed in to change notification settings - Fork 239
/
state.ts
178 lines (164 loc) · 6.2 KB
/
state.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
import _ from 'lodash'
import { proxy, useSnapshot } from 'valtio'
import { subscribeKey } from 'valtio/utils'
import { FRONTEND_OPTIONS, getHFS, hfsEvent, hIcon, objSameKeys, pathEncode, StringifyProps, typedKeys } from './misc'
import { DirEntry as ServerDirEntry } from '../../src/api.get_file_list'
export const state = proxy<typeof FRONTEND_OPTIONS & {
stopSearch?: ()=>void,
searchManuallyInterrupted?: boolean,
iconsReady: boolean,
username: string,
accountExp?: string,
list: DirList,
filteredList?: DirList,
clip: DirList
loading: boolean,
error?: string,
listReloader: number,
patternFilter: string,
showFilter: boolean,
selected: { [uri:string]: true }, // by using an object instead of an array, Entry components are not rendered when others get selected
remoteSearch: string,
adminUrl?: string,
loginRequired?: boolean, // force user to login before proceeding
messageOnly?: string, // no gui, just show this message
props?: {
can_upload?: boolean
accept?: string
can_delete?: boolean
can_archive?: boolean
can_comment?: boolean
can_overwrite?: boolean
comment?: string
}
canChangePassword: boolean
uri: string
uploadOnExisting: 'skip' | 'overwrite' | 'rename'
searchOptions: any
expandedUsername: string[]
}>({
expandedUsername: [],
searchOptions: { wild: true },
uploadOnExisting: getHFS().dontOverwriteUploading ? 'rename' : 'skip',
uri: '',
canChangePassword: false,
props: {},
...objSameKeys(FRONTEND_OPTIONS, (v,k) => getHFS()[k] ?? v),
iconsReady: false,
username: '',
list: [],
clip: [],
loading: false,
listReloader: 0,
patternFilter: '',
showFilter: false,
selected: {},
remoteSearch: '',
})
export function useSnapState() {
return useSnapshot(state)
}
const SETTINGS_KEY = 'hfs_settings'
type StateKey = keyof typeof state
const SETTINGS_WITHOUT_GUI: StateKey[] = ['file_menu_on_link']
const SETTINGS_TO_STORE: StateKey[] = _.difference(typedKeys(FRONTEND_OPTIONS), SETTINGS_WITHOUT_GUI)
.concat(['uploadOnExisting']) // not adding this to FRONTEND_OPTIONS, as its possible values vary with the user permissions, but still makes sense to save on a single browser, supposedly for a single user
loadSettings()
for (const k of SETTINGS_TO_STORE)
subscribeKey(state, k, storeSettings)
function loadSettings() {
const json = localStorage.getItem(SETTINGS_KEY)
if (!json) return
let read
try { read = JSON.parse(json) }
catch {
console.error('invalid settings stored', json)
return
}
for (const k of SETTINGS_TO_STORE) {
const v = read[k]
if (v !== undefined) // @ts-ignore
state[k] = v
}
}
function storeSettings() {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(_.pick(state, SETTINGS_TO_STORE)))
}
export class DirEntry implements StringifyProps<ServerDirEntry> {
static FORBIDDEN = 'FORBIDDEN'
public readonly n: string
public readonly s?: number
public readonly m?: string
public readonly c?: string
public readonly p?: string
public readonly icon?: string | true
public readonly web?: true
public readonly url?: string
public readonly target?: string
public comment?: string
// we memoize these value for speed
public readonly name: string
public readonly uri: string
public readonly ext: string = ''
public readonly isFolder: boolean
public readonly t?:Date
public readonly cantOpen?: true | typeof DirEntry.FORBIDDEN
public readonly key?: string
constructor(n: string, rest?: any) {
Object.assign(this, rest) // we actually allow any custom property to be memorized
this.n = n // must do it after rest to avoid overwriting
this.uri = rest?.url || ((n[0] === '/' ? '' : location.pathname) + pathEncode(this.n))
this.isFolder = this.n.endsWith('/')
if (!this.isFolder) {
const i = this.n.lastIndexOf('.') + 1
this.ext = i ? this.n.substring(i).toLowerCase() : ''
}
const t = this.m || this.c
if (t)
this.t = new Date(t)
this.name = this.isFolder ? this.n.slice(this.n.lastIndexOf('/', this.n.length - 2) + 1, -1)
: this.n.slice(this.n.lastIndexOf('/') + 1)
const x = this.isFolder && !this.web ? 'L' : 'R' // to open we need list for folders and read for files
this.cantOpen = this.p?.match(x) ? true : this.p?.match(x.toLowerCase()) ? DirEntry.FORBIDDEN : undefined
}
getNext() {
return this.getSibling(+1)
}
getPrevious() {
return this.getSibling(-1)
}
getNextFiltered() {
return this.getSibling(+1, state.filteredList)
}
getPreviousFiltered() {
return this.getSibling(-1, state.filteredList)
}
getSibling(ofs: number, list: DirList=state.list) { // i'd rather make this private, but valtio is messing with types, causing problems in FilesList()
return list[ofs + list.findIndex(x => x.n === this.n)]
}
getDefaultIcon() {
return hIcon(this.icon === true ? `${this.n}?get=icon` : (this.icon ?? (this.isFolder || this.web ? 'folder' : this.url ? 'link' : ext2type(this.ext) || 'file')))
}
canArchive() {
return this.p?.includes('A') || state.props?.can_archive && !this.p?.includes('a')
}
canDelete() {
return this.p?.includes('D') || state.props?.can_delete && !this.p?.includes('d')
}
canSelect() {
if (this.url) return false
return this.canArchive() || this.canDelete() // selection is used only by zip and delete, but consider custom logic from plugins
|| hfsEvent('enableEntrySelection', { entry: this }).some(Boolean)
}
}
export type DirList = DirEntry[]
const exts = {
image: ['jpeg','jpg','gif','png','webp','svg'],
audio: ['mp3','wav','m4a','ogg','flac'],
video: ['mp4','mpeg','mpg','webm','mov','m4v','mkv'],
archive: ['zip', 'rar', 'gz', 'tgz'],
}
export function ext2type(ext: string) {
return _.findKey(exts, arr => arr.includes(ext))
}