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