diff --git a/README.md b/README.md index 74a698d2..d93dd0bc 100644 --- a/README.md +++ b/README.md @@ -32,24 +32,52 @@ app.use(VueFinder) app.mount('#app') ``` -Html -```html +Vue Template +```vue ...
- +
... + + ``` ### Props -| Prop | Value | Default | Description | -|---------------|:-------:|---------|:---------------------------------------| -| id | string | _null_ | required | -| url | string | _null_ | required - backend url | -| locale | string | en | optional - default language code | -| dark | boolean | false | optional - makes theme dark as default | -| max-file-size | string | 10mb | optional - client side max file upload | +| Prop | Value | Default | Description | +|---------------|:-------:|---------|:----------------------------------------------------| +| id | string | _null_ | required | +| request | object | _null_ | required - backend url or request object, see above | +| locale | string | en | optional - default language code | +| dark | boolean | false | optional - makes theme dark as default | +| max-file-size | string | 10mb | optional - client side max file upload | ### Features - Multi adapter/storage (see https://github.com/thephpleague/flysystem) @@ -86,13 +114,13 @@ Html - PHP: [VueFinder Php Library](https://github.com/n1crack/vuefinder-php) ### Roadmap +- [x] restyle the modals +- [x] add more languages (only en/tr/ru at the moment. PRs are welcomed.) +- [x] emit select event, with @select get selected files for online editors like tinymce/ckeditor - [ ] code refactoring (cleanup the duplications, make reusable components) -- [ ] restyle the modals -- [ ] add more languages (only en/tr/ru at the moment. PRs are welcomed.) - [ ] copy/move to a folder (modal, treeview) - [ ] transfer items between filesystem adapters - [ ] show/hide components (toolbar/statusbar etc.) -- [ ] emit select event, with @select get selected files for online editors like tinymce/ckeditor - [ ] drag&drop on folders at address bar - [ ] update DragSelect plugin for using its dropzone support diff --git a/src/App.vue b/src/App.vue index e43cd614..53274611 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,18 +2,75 @@
+ + + +
+

Selected Files

+
    +
  • + {{ file.path }} +
  • +
+
+
diff --git a/src/assets/css/index.css b/src/assets/css/index.css index 719cc2bc..d31e1f19 100644 --- a/src/assets/css/index.css +++ b/src/assets/css/index.css @@ -89,6 +89,19 @@ .vf-scrollbar::-webkit-scrollbar-corner { @apply bg-transparent; } + + + /* modal fade effect */ + .vuefinder .fade-enter-active, + .vuefinder .fade-leave-active { + transition: opacity 0.2s ease; + } + + .vuefinder .fade-enter-from, + .vuefinder .fade-leave-to { + opacity: 0; + } + } @tailwind utilities; diff --git a/src/components/ActionMessage.vue b/src/components/ActionMessage.vue index 16343b7b..a613a6dd 100644 --- a/src/components/ActionMessage.vue +++ b/src/components/ActionMessage.vue @@ -2,7 +2,7 @@
- Saved. + {{ t('Saved.') }}
@@ -16,6 +16,8 @@ export default { setup(props, {emit, slots}) { const emitter = inject('emitter'); const shown = ref(false); + const {t} = inject('i18n'); + let timeout = null; const handleEvent = () => { @@ -35,7 +37,7 @@ export default { }); return { - shown, + shown, t }; }, }; diff --git a/src/components/Breadcrumb.vue b/src/components/Breadcrumb.vue index 5d2aa1b6..5c39c21a 100644 --- a/src/components/Breadcrumb.vue +++ b/src/components/Breadcrumb.vue @@ -79,15 +79,18 @@ export default { import {inject, nextTick, ref, watch} from 'vue'; import useDebouncedRef from '../composables/useDebouncedRef.js'; +import {FEATURES} from "./features.js"; const emitter = inject('emitter'); -const { getStore } = inject('storage'); const adapter = inject('adapter'); const dirname = ref(null); const breadcrumb = ref([]); const searchMode = ref(false); const searchInput = ref(null); +/** @type {import('vue').Ref} */ +const features = inject('features'); + const props = defineProps({ data: Object }); @@ -135,6 +138,9 @@ emitter.on('vf-search-exit', () => { }); const enterSearchMode = () => { + if (!features.value.includes(FEATURES.SEARCH)) { + return; + } searchMode.value = true; nextTick(() => searchInput.value.focus()) } diff --git a/src/components/ContextMenu.vue b/src/components/ContextMenu.vue index 10b41375..223a09a6 100644 --- a/src/components/ContextMenu.vue +++ b/src/components/ContextMenu.vue @@ -1,7 +1,7 @@ @@ -51,14 +51,17 @@ import Default from '../previews/Default.vue'; import Video from '../previews/Video.vue'; import Audio from '../previews/Audio.vue'; import Pdf from '../previews/Pdf.vue'; -import buildURLQuery from '../../utils/buildURLQuery.js'; -import {useApiUrl} from '../../composables/useApiUrl.js'; import datetimestring from '../../utils/datetimestring.js'; -const {apiUrl} = useApiUrl(); +import {FEATURES} from "../features.js"; + const emitter = inject('emitter') const {t} = inject('i18n') const loaded = ref(false); const filesize = inject("filesize") +/** @type {import('../../utils/ajax.js').Requester} */ +const requester = inject('requester'); +/** @type {import('vue').Ref} */ +const features = inject('features'); const setLoad = (bool) => loaded.value = bool; @@ -69,7 +72,12 @@ const props = defineProps({ const loadPreview = (type) => (props.selection.item.mime_type ?? '').startsWith(type) const download = () => { - const url = apiUrl.value + '?' + buildURLQuery({q:'download', adapter: props.selection.adapter, path: props.selection.item.path}); + const url = requester.getDownloadUrl(props.selection.adapter, props.selection.item) emitter.emit('vf-download', url) } + +const enabledPreview = features.value.includes(FEATURES.PREVIEW) +if (!enabledPreview) { + setLoad(true) +} diff --git a/src/components/modals/ModalRename.vue b/src/components/modals/ModalRename.vue index cb4b8c7d..030b0b31 100644 --- a/src/components/modals/ModalRename.vue +++ b/src/components/modals/ModalRename.vue @@ -64,8 +64,11 @@ const rename = () => { emitter.emit('vf-fetch', { params: { q: 'rename', + m: 'post', adapter: adapter.value, path: props.current.dirname, + }, + body: { item: item.value.path, name: name.value }, diff --git a/src/components/modals/ModalUnarchive.vue b/src/components/modals/ModalUnarchive.vue index b6c48760..8a125c78 100644 --- a/src/components/modals/ModalUnarchive.vue +++ b/src/components/modals/ModalUnarchive.vue @@ -64,8 +64,11 @@ const unarchive = () => { emitter.emit('vf-fetch', { params: { q: 'unarchive', + m: 'post', adapter: adapter.value, path: props.current.dirname, + }, + body: { item: item.value.path }, onSuccess: () => { diff --git a/src/components/modals/ModalUpload.vue b/src/components/modals/ModalUpload.vue index a1702fdb..93456ab9 100644 --- a/src/components/modals/ModalUpload.vue +++ b/src/components/modals/ModalUpload.vue @@ -101,21 +101,17 @@ import Uppy from '@uppy/core'; import XHR from '@uppy/xhr-upload'; import VFModalLayout from './ModalLayout.vue'; import { inject, onBeforeUnmount, onMounted, ref } from 'vue'; -import {useApiUrl} from '../../composables/useApiUrl.js'; -import buildURLQuery from '../../utils/buildURLQuery.js'; import Message from '../Message.vue'; -import {csrf} from '../../utils/ajax.js'; import { parse } from '../../utils/filesize.js'; import title_shorten from "../../utils/title_shorten.js"; -const {apiUrl} = useApiUrl(); +const debug = inject('debug'); const emitter = inject('emitter'); const {t} = inject('i18n'); const maxFileSize = inject('maxFileSize'); -const postData = inject('postData'); - -const filesize = inject("filesize") - +const filesize = inject("filesize"); +/** @type {import('../../utils/ajax.js').Requester} */ +const requester = inject('requester'); const props = defineProps({ current: Object @@ -306,7 +302,7 @@ function close() { onMounted(async () => { uppy = new Uppy({ - debug: process.env.NODE_ENV === 'development', + debug, restrictions: { maxFileSize: parse(maxFileSize), //maxNumberOfFiles @@ -337,12 +333,21 @@ onMounted(async () => { // Uppy would only upload that file once even you call .addFile() twice in one row, nice. } }); - uppy.use(XHR, { + const params = requester.transformRequestParams({ + url: '', method: 'post', - endpoint: apiUrl.value + '?' + buildURLQuery(Object.assign(postData, {q: 'upload', adapter: props.current.adapter, path: props.current.dirname })), - headers: { - ...(csrf && {'X-CSRF-Token' : csrf}), - }, + params: { q: 'upload', adapter: props.current.adapter, path: props.current.dirname }, + }); + if (debug) { + if (params.body != null && (params.body instanceof FormData || Object.keys(params.body).length > 0)) { + console.warn('Cannot set body on upload, make sure request.transformRequest didn\'t set body when upload.' + + '\nWill ignore for now.'); + } + } + uppy.use(XHR, { + method: params.method, + endpoint: params.url + '?' + new URLSearchParams(params.params), + headers: params.headers, limit: 5, timeout: 0, getResponseError(responseText, _response) { @@ -474,6 +479,7 @@ onMounted(async () => { for (const file of files) { addFile(file); } + target.value = ''; }; internalFileInput.value.addEventListener('change', onFileInputChange); internalFolderInput.value.addEventListener('change', onFileInputChange); diff --git a/src/components/previews/Audio.vue b/src/components/previews/Audio.vue index 5816847d..cc150e71 100644 --- a/src/components/previews/Audio.vue +++ b/src/components/previews/Audio.vue @@ -11,18 +11,17 @@ diff --git a/src/composables/useApiUrl.js b/src/composables/useApiUrl.js deleted file mode 100644 index f424b445..00000000 --- a/src/composables/useApiUrl.js +++ /dev/null @@ -1,11 +0,0 @@ -import {ref} from 'vue'; - -const apiUrl = ref(''); - -export function useApiUrl() { - function setApiUrl(url) { - apiUrl.value = url; - } - - return {apiUrl, setApiUrl}; -} diff --git a/src/composables/useI18n.js b/src/composables/useI18n.js index 653fa56f..f5272ce7 100644 --- a/src/composables/useI18n.js +++ b/src/composables/useI18n.js @@ -20,6 +20,7 @@ export function useI18n(id, locale, emitter) { active_locale.value = locale; setStore('translations', i18n); emitter.emit('vf-toast-push', {label: 'The language is set to ' + locale}); + emitter.emit('vf-language-saved'); }).catch(e => { if (defaultLocale) { emitter.emit('vf-toast-push', {label: 'The selected locale is not yet supported!', type: 'error'}); diff --git a/src/composables/useStorage.js b/src/composables/useStorage.js index e81404da..6853ea28 100644 --- a/src/composables/useStorage.js +++ b/src/composables/useStorage.js @@ -1,6 +1,7 @@ import {ref, watch} from 'vue'; +/** @param {String} key */ export function useStorage(key) { let storedValues = localStorage.getItem(key + '_storage'); @@ -16,6 +17,10 @@ export function useStorage(key) { } } + /** + * @param {String} key + * @param {*} value + */ function setStore(key, value) { storage.value = Object.assign({...storage.value}, {...{[key]: value}}); } @@ -24,6 +29,10 @@ export function useStorage(key) { storage.value = null; } + /** + * @param {String} key + * @param {*} defaultValue + */ const getStore = (key, defaultValue = null) => { if (storage.value === null || storage.value === '') { return defaultValue; diff --git a/src/index.js b/src/index.js index 24c47e56..a6ea4b26 100644 --- a/src/index.js +++ b/src/index.js @@ -1,13 +1,14 @@ -import components from './components'; +import components from './components.js'; import 'microtip/microtip.css' import './assets/css/index.css'; export default { - install(Vue) { + /** @param {import('vue').App} app */ + install(app) { for (const prop in components) { if (components.hasOwnProperty(prop)) { const component = components[prop]; - Vue.component(component.name, component); + app.component(component.name, component); } } } diff --git a/src/locales/fr.js b/src/locales/fr.js new file mode 100644 index 00000000..9b0cafb2 --- /dev/null +++ b/src/locales/fr.js @@ -0,0 +1,86 @@ +import uppyLocaleFr from '@uppy/locales/lib/fr_FR.js'; + +// todo : change the values from english to french +export default { + "Language": "Langue" , + "Create": "Créer", + "Close": "Fermer", + "Cancel": "Annuler", + "Save":"Enregistrer", + "Edit": "Modifier", + "Crop": "Recadrer", + "New Folder": "Nouveau dossier", + "New File": "Nouveau fichier", + "Rename": "Renommer", + "Delete": "Supprimer", + "Upload": "Télécharger", + "Download": "Télécharger", + "Archive": "Archiver", + "Unarchive": "Désarchiver", + "Open": "Ouvrir", + "Open containing folder": "Ouvrir le dossier contenant", + "Refresh": "Rafraîchir", + "Preview": "Aperçu", + "Dark Mode": "Mode sombre", + "Toggle Full Screen": "Basculer en plein écran", + "Change View": "Changer de vue", + "Storage" : "Stockage", + "Go up a directory": "Remonter d'un répertoire", + "Search anything..": "Rechercher...", + "Name": "Nom", + "Size": "Taille", + "Date": "Date", + "Filepath": "Chemin du fichier", + "About": "À propos", + "Folder Name": "Nom du dossier", + "File Name": "Nom du fichier", + "Move files": "Déplacer les fichiers", + "Are you sure you want to move these files to?": "Êtes-vous sûr de vouloir déplacer ces fichiers vers?", + "Yes, Move!": "Oui, déplacer!", + "Delete files": "Supprimer les fichiers", + "Yes, Delete!": "Oui, supprimer!", + "Upload Files" : "Télécharger des fichiers", + "No files selected!":"Aucun fichier sélectionné!", + "Select Files": "Sélectionner des fichiers", + "Archive the files": "Archiver les fichiers", + "Unarchive the files": "Désarchiver les fichiers", + "The archive will be unarchived at": "L'archive sera désarchivée à", + "Archive name. (.zip file will be created)": "Nom de l'archive. (un fichier .zip sera créé)", + "Vuefinder is a file manager component for vue 3.": "Vuefinder est un composant de gestionnaire de fichiers pour vue 3.", + "Create a new folder": "Créer un nouveau dossier", + "Create a new file": "Créer un nouveau fichier", + "Are you sure you want to delete these files?": "Êtes-vous sûr de vouloir supprimer ces fichiers?", + "This action cannot be undone.": "Cette action ne peut pas être annulée.", + "Search results for" : "Résultats de recherche pour", + "item(s) selected.": "élément(s) sélectionné(s).", + "%s is renamed." : "%s est renommé.", + "This is a readonly storage." : "C'est un stockage en lecture seule.", + "%s is created." : "%s est créé.", + "Files moved." : "Fichiers déplacés.", + "Files deleted." : "Fichiers supprimés.", + "The file unarchived." : "Le fichier désarchivé.", + "The file(s) archived." : "Le(s) fichier(s) archivé(s).", + "Updated." : "Mis à jour.", + "No search result found." : "Aucun résultat de recherche trouvé.", + "Are you sure you want to move these files?" : "Êtes-vous sûr de vouloir déplacer ces fichiers?", + "File Size": "Taille du fichier", + "Last Modified": "Dernière modification", + "Drag&Drop: on": "Drag&Drop: on", + "Drag&Drop: off": "Drag&Drop: off", + "Select Folders": "Sélectionner des dossiers", + "Clear all": "Tout effacer", + "Clear only successful": "Effacer uniquement les réussites", + "Drag and drop the files/folders to here or click here.": "Faites glisser les fichiers/dossiers ici ou cliquez ici.", + "Release to drop these files.": "Relâchez pour déposer ces fichiers.", + "Canceled": "Annulé", + "Done": "Terminé", + "Network Error, Unable establish connection to the server or interrupted.": "Erreur réseau, impossible d'établir une connexion avec le serveur ou interrompue.", + "Pending upload": "Téléchargement en attente", + "Please select file to upload first." : "Veuillez d'abord sélectionner le fichier à télécharger.", + "About %s": "À propos de %s", + "Settings": "Paramètres", + "Use Metric Units": "Utiliser les unités métriques", + "Saved.": "Enregistré.", + "Clear Local Storage": "Effacer le stockage local", + "uppy": uppyLocaleFr +} diff --git a/src/locales/zhCN.js b/src/locales/zhCN.js index d5baa821..6cdb70c3 100644 --- a/src/locales/zhCN.js +++ b/src/locales/zhCN.js @@ -35,7 +35,7 @@ export default { "File Name": "文件名称", "Move files": "移动文件", "Are you sure you want to move these files to?": "您确定要移动这些文件吗?", - "Yes, Move!": "确定,复制!", + "Yes, Move!": "确定,移动!", "Delete files": "删除文件", "Yes, Delete!": "确定,删除!", "Upload Files" : "上传文件", diff --git a/src/locales/zhTW.js b/src/locales/zhTW.js index 810fb54d..fee76c0c 100644 --- a/src/locales/zhTW.js +++ b/src/locales/zhTW.js @@ -35,7 +35,7 @@ export default { "File Name": "檔案名称", "Move files": "移動檔案", "Are you sure you want to move these files to?": "您確定要移動這些檔案嗎?", - "Yes, Move!": "確定,複製!", + "Yes, Move!": "確定,移動!", "Delete files": "刪除檔案", "Yes, Delete!": "確定,刪除!", "Upload Files" : "上傳檔案", diff --git a/src/utils/ajax.js b/src/utils/ajax.js index 05cd94f6..b20dbcfd 100644 --- a/src/utils/ajax.js +++ b/src/utils/ajax.js @@ -1,31 +1,241 @@ export const csrf = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); -export default (url, {method = 'get', params = {}, json = true, signal = null}) => { - const init = {method: method}; - init.signal = signal; +/** + * @typedef RequestConfig + * @property {String} baseUrl + * @property {Record=} headers Additional headers + * @property {Record=} params Additional query params + * @property {Record=} body Additional body key pairs + * @property {RequestTransformer=} transformRequest Transform request callback + * @property {String=} xsrfHeaderName The http header that carries the xsrf token value + */ +/** + * @typedef RequestTransformParams + * @property {String} url + * @property {'get'|'post'|'put'|'patch'|'delete'} method + * @property {Record} headers + * @property {Record} params + * @property {Record|FormData|null} body + */ +/** + * @typedef RequestTransformResult + * @property {String=} url + * @property {'get'|'post'|'put'|'patch'|'delete'=} method + * @property {Record=} headers + * @property {Record=} params + * @property {Record|FormData=} body + */ +/** + * @typedef RequestTransformResultInternal + * @property {String} url + * @property {'get'|'post'|'put'|'patch'|'delete'} method + * @property {Record} headers + * @property {Record} params + * @property {Record|FormData=} body + */ +/** + * @callback RequestTransformer + * @param {RequestTransformParams} request + * @returns {RequestTransformResult} + */ - if (method == 'get') { - url += '?' + new URLSearchParams(params); - } else { - init.headers = {}; +/** + * Base http requester + */ +export class Requester { + /** @type {RequestConfig} */ + #config - if (csrf) { - init.headers['X-CSRF-Token'] = csrf; - } + /** @param {RequestConfig} config */ + constructor(config) { + this.#config = config; + } - let formData = new FormData(); + /** @type {RequestConfig} */ + get config() { + return this.#config; + } - for (const [key, value] of Object.entries(params)) { - formData.append(key, value); + /** + * Transform request params + * @param {Object} input + * @param {String} input.url + * @param {'get'|'post'|'put'|'patch'|'delete'} input.method + * @param {Record=} input.headers + * @param {Record=} input.params + * @param {Record|FormData=} input.body + * @return {RequestTransformResultInternal} + */ + transformRequestParams(input ) { + const config = this.#config; + const ourHeaders = {}; + if (csrf != null && csrf !== '') { + ourHeaders[config.xsrfHeaderName] = csrf; + } + /** @type {Record} */ + const headers = Object.assign({}, config.headers, ourHeaders, input.headers); + const params = Object.assign({}, config.params, input.params); + const body = input.body; + const url = config.baseUrl + input.url; + const method = input.method; + let newBody + if (method !== 'get') { + /** @type {Record|FormData} */ + if (!(body instanceof FormData)) { + // JSON + newBody = { ...body }; + if (config.body != null) { + Object.assign(newBody, this.config.body); + } + } else { + // FormData + newBody = body; + if (config.body != null) { + Object.entries(this.config.body).forEach(([key, value]) => { + newBody.append(key, value); + }); + } + } } + /** @type {RequestTransformResultInternal} */ + const transformed = { + url, + method, + headers, + params, + body: newBody, + } + if (config.transformRequest != null) { + const transformResult = config.transformRequest({ + url, + method, + headers, + params, + body: newBody, + }); + if (transformResult.url != null) { + transformed.url = transformResult.url; + } + if (transformResult.method != null) { + transformed.method = transformResult.method; + } + if (transformResult.params != null) { + transformed.params = transformResult.params ?? {}; + } + if (transformResult.headers != null) { + transformed.headers = transformResult.headers ?? {}; + } + if (transformResult.body != null) { + transformed.body = transformResult.body; + } + } + return transformed + } - init.body = formData; + /** + * Get download url + * @param {String} adapter + * @param {String} node + * @param {String} node.path + * @param {String=} node.url + * @return {String} + */ + getDownloadUrl(adapter, node) { + if (node.url != null) { + return node.url + } + const transform = this.transformRequestParams({ + url: '', + method: 'get', + params: { q: 'download', adapter, path: node.path } + }); + return transform.url + '?' + new URLSearchParams(transform.params).toString() } - return fetch(url, init).then((response) => { + /** + * Get preview url + * @param {String} adapter + * @param {String} node + * @param {String} node.path + * @param {String=} node.url + * @return {String} + */ + getPreviewUrl(adapter, node) { + if (node.url != null) { + return node.url + } + const transform = this.transformRequestParams({ + url: '', + method: 'get', + params: { q: 'preview', adapter, path: node.path } + }); + return transform.url + '?' + new URLSearchParams(transform.params).toString() + } + + /** + * Send request + * @param {Object} input + * @param {String} input.url + * @param {'get'|'post'|'put'|'patch'|'delete'} input.method + * @param {Record=} input.headers + * @param {Record=} input.params + * @param {(Record|FormData|null)=} input.body + * @param {'arrayBuffer'|'blob'|'json'|'text'=} input.responseType + * @param {AbortSignal=} input.abortSignal + * @returns {Promise<(ArrayBuffer|Blob|Record|String|null)>} + * @throws {Record|null} resp json error + */ + async send(input) { + const reqParams = this.transformRequestParams(input); + const responseType = input.responseType || 'json'; + /** @type {RequestInit} */ + const init = { + method: input.method, + headers: reqParams.headers, + signal: input.abortSignal, + }; + const url = reqParams.url + '?' + new URLSearchParams(reqParams.params); + if (reqParams.method !== 'get' && reqParams.body != null) { + /** @type {String|FormData} */ + let newBody + if (!(reqParams.body instanceof FormData)) { + // JSON + newBody = JSON.stringify(reqParams.body); + init.headers['Content-Type'] = 'application/json'; + } else { + // FormData + newBody = input.body; + } + init.body = newBody; + } + const response = await fetch(url, init); if (response.ok) { - return json ? response.json() : response.text(); + return await response[responseType](); } - return response.json().then(Promise.reject.bind(Promise)); - }); + throw await response.json(); + } } + +/** + * Build requester from user config + * @param {String|RequestConfig} userConfig + * @return {Requester} + */ +export function buildRequester(userConfig) { + /** @type {RequestConfig} */ + const config = { + baseUrl: '', + headers: {}, + params: {}, + body: {}, + xsrfHeaderName: 'X-CSRF-Token', + }; + if (typeof userConfig === 'string') { + Object.assign(config, { baseUrl: userConfig }); + } else { + Object.assign(config, userConfig); + } + return new Requester(config); +} + +export {} diff --git a/src/utils/buildURLQuery.js b/src/utils/buildURLQuery.js deleted file mode 100644 index 58d8013e..00000000 --- a/src/utils/buildURLQuery.js +++ /dev/null @@ -1,6 +0,0 @@ -const buildURLQuery = obj => Object.entries(obj) - .map(pair => pair.map(encodeURIComponent).join('=')) - .join('&'); - - -export default buildURLQuery; diff --git a/src/utils/getImageUrl.js b/src/utils/getImageUrl.js deleted file mode 100644 index 5ac506d7..00000000 --- a/src/utils/getImageUrl.js +++ /dev/null @@ -1,5 +0,0 @@ -import {useApiUrl} from '../composables/useApiUrl.js'; -import buildURLQuery from './buildURLQuery.js'; -const {apiUrl} = useApiUrl(); - -export const getImageUrl = (adapter, path) => apiUrl.value + '?' + buildURLQuery({q: 'preview', adapter, path});