+
@@ -31,7 +31,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});