main
ts 171 lines 6.51 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 React, { createElement as h } from 'react'
4 import { Btn, iconBtn, Spinner } from './components'
5 import { newDialog, toast } from './dialog'
6 import { Icon, IconProps } from './icons'
7 import { Callback, Dict, domOn, getHFS, getOrSet, Html, HTTP_MESSAGES, prefix, urlParams, useBatch } from '@hfs/shared'
8 import * as cross from '../../src/cross'
9 import * as shared from '@hfs/shared'
10 import { apiCall, getNotifications, useApi } from '@hfs/shared/api'
11 import { DirEntry, state, useSnapState } from './state'
12 import * as dialogLib from './dialog'
13 import _ from 'lodash'
14 import { reloadList } from './useFetchList'
15 import { logout } from './login'
16 import { subscribeKey } from 'valtio/utils'
17 import { uploadState } from './uploadQueue'
18 import { fileShow, Video, Audio, getShowComponent } from './show'
19 import { ANY_LANGUAGE } from '../../src/i18n'
20 import { debounceAsync } from '../../src/debounceAsync'
21 export * from '@hfs/shared'
22 import i18n from './i18n'
23 const { t, getLangs } = i18n
24
25 export function err2msg(err: number | Error) {
26 return typeof err === 'number' ? HTTP_MESSAGES[err]
27 : (HTTP_MESSAGES[(err as any).code] || err.message || String(err))
28 }
29
30 export function hIcon(name: string, props?: Omit<IconProps, 'name'>) {
31 return h(Icon, { name, ...props })
32 }
33
34 export function ErrorMsg({ err }: { err: any }) {
35 return err ? h('div', { className:'error-msg' }, err?.message || _.isString(err) && err || `${t`Error`}:${err}`)
36 : null
37 }
38
39 let isWorking = false // we want the 'working' thing to be singleton
40 export function working() {
41 if (isWorking)
42 return ()=>{} // noop
43 isWorking = true
44 const { close } = newDialog({
45 closable: false,
46 noFrame: true,
47 Content: Spinner,
48 reserveClosing: true,
49 className: 'working',
50 onClose(){
51 isWorking = false
52 }
53 })
54 return close
55 }
56
57 export function hfsEvent(name: string, params?:Dict) {
58 const output: any[] = []
59 const order: number[] = []
60 const detail = { params, output, order }
61 let ev = new CustomEvent('hfs.'+name, { detail, cancelable: true })
62 document.dispatchEvent(ev)
63 if (!ev.defaultPrevented) {
64 ev = new CustomEvent('hfs.'+name+':after', { detail, cancelable: true })
65 document.dispatchEvent(ev)
66 }
67 const sortedOutput = order.length && _.sortBy(output.map((x, i) => [order[i] || 0, x]), '0').map(x => x[1])
68 return Object.assign(sortedOutput || output, {
69 isDefaultPrevented: () => ev.defaultPrevented,
70 })
71 }
72
73 type HfsEventCallback = (params:any, extra: { output: any[], setOrder: Callback<number>, preventDefault: Callback }) => any
74 export function onHfsEvent(pluginId: string, name: string, cb: HfsEventCallback, options?: { once?: boolean }) {
75 if (!_.isFunction(cb)) return
76 const key = 'hfs.' + name
77 document.addEventListener(key, wrapper, options)
78 return () => document.removeEventListener(key, wrapper)
79
80 function wrapper(ev: Event) {
81 const { params, output, order } = (ev as CustomEvent).detail
82 let thisOrder
83 try {
84 const res = cb(params, {
85 output,
86 setOrder(x) { thisOrder = x },
87 preventDefault: () => ev.preventDefault()
88 })
89 if (res === undefined) return
90 if (Array.isArray(output)) {
91 output.push(res instanceof Promise ? res.catch(printError) : res)
92 if (thisOrder)
93 order[output.length - 1] = thisOrder
94 }
95 }
96 catch(e) {
97 printError(e)
98 }
99
100 function printError(e: any) {
101 console.error(`plugin ${pluginId} on event ${name}: ${e}`)
102 }
103 }
104 }
105
106 export function formatTimestamp(x: number | string | Date, options?: Intl.DateTimeFormatOptions) {
107 const cached = getOrSet(formatTimestamp as any, 'langs', () => {
108 const ret = getLangs()
109 const def = urlParams.lang || navigator.language
110 return !ret.length || def.startsWith(ret[0]) ? def : ret // eg: if i'm ar-EG, and first translation is ar, keep ar-EG
111 })
112 return !x ? '' : (x instanceof Date ? x : new Date(x)).toLocaleString(cached, options)
113 }
114
115 import * as thisModule from './misc'
116 Object.assign(getHFS(), {
117 h, React, state, t, _, dialogLib, apiCall, useApi, reloadList, logout, Icon, hIcon, iconBtn, useBatch, fileShow,
118 toast, domOn, getNotifications, debounceAsync, useSnapState, DirEntry, Btn, i18n,
119 fileShowComponents: { Video, Audio },
120 isVideoComponent, markVideoComponent, isAudioComponent, markAudioComponent,
121 isShowSupported: getShowComponent,
122 misc: { ...cross, ...shared, ...thisModule },
123 emit: hfsEvent,
124 onEvent: (...args: Parameters<typeof onHfsEvent> extends [any, ...infer R] ? R : []) =>
125 onHfsEvent(getHFS().getPluginKey(true) || '???', ...args),
126 watchState(k: string, cb: (v: any) => void, callNow=false) {
127 const up = k.split('upload.')[1]
128 const thisState = up ? uploadState : state as any
129 if (callNow)
130 cb(thisState[k])
131 return subscribeKey(thisState, up || k, cb, true)
132 },
133 customRestCall(name: string, ...rest: any[]) {
134 return apiCall(cross.PLUGIN_CUSTOM_REST_PREFIX + name, ...rest)
135 },
136 html: (html: string) => h(Html, {}, html),
137 elementToEntry(el: any) {
138 if (!(el instanceof HTMLElement)) return
139 const a = el.closest('li')?.querySelector('.link-wrapper a')
140 if (!(a instanceof HTMLAnchorElement)) return
141 try { return _.find(state.list, { uri: new URL(a.href).pathname }) }
142 catch {}
143 },
144 customizeText(mods: Dict<string>, lang=ANY_LANGUAGE) {
145 const o = i18n.state.translations
146 if (lang === ANY_LANGUAGE || lang === cross.EMBEDDED_LANGUAGE) // these are missing at the start, but we need them if we want to customize content
147 o[lang] ??= { translate: {} }
148 Object.assign(o[lang]?.translate || {}, mods)
149 }
150 })
151
152 markVideoComponent(Video)
153 markAudioComponent(Audio)
154 function isVideoComponent(Component: any) {
155 return Boolean(Component?.hfs_show_video)
156 }
157 function markVideoComponent(Component: any) {
158 Component.hfs_show_video = true
159 return Component
160 }
161 function isAudioComponent(Component: any) {
162 return Boolean(Component?.hfs_show_audio)
163 }
164 function markAudioComponent(Component: any) {
165 Component.hfs_show_audio = true
166 return Component
167 }
168
169 export function operationSuccessful() {
170 return toast(t`Operation successful`, 'success')
171 }