Forráskód Böngészése

文件管理器,文章编辑器

wuyunfeng 1 éve
szülő
commit
91c662fce7
72 módosított fájl, 10539 hozzáadás és 19 törlés
  1. 2 1
      .gitignore
  2. 3 0
      languages/zh-CN.js
  3. 22 2
      package.json
  4. 5 5
      public/domain.js
  5. 74 0
      src/components/FileManager/ServiceContainer.js
  6. 283 0
      src/components/FileManager/VueFinder.vue
  7. 3549 0
      src/components/FileManager/assets/css/a.css
  8. 119 0
      src/components/FileManager/assets/css/index.css
  9. 353 0
      src/components/FileManager/assets/css/preflight.css
  10. 41 0
      src/components/FileManager/components/ActionMessage.vue
  11. 239 0
      src/components/FileManager/components/Breadcrumb.vue
  12. 441 0
      src/components/FileManager/components/ContextMenu.vue
  13. 428 0
      src/components/FileManager/components/Explorer.vue
  14. 45 0
      src/components/FileManager/components/Message.vue
  15. 82 0
      src/components/FileManager/components/Statusbar.vue
  16. 251 0
      src/components/FileManager/components/Toolbar.vue
  17. 156 0
      src/components/FileManager/components/modals/ModalAbout.vue
  18. 84 0
      src/components/FileManager/components/modals/ModalArchive.vue
  19. 128 0
      src/components/FileManager/components/modals/ModalDelete.vue
  20. 52 0
      src/components/FileManager/components/modals/ModalLayout.vue
  21. 36 0
      src/components/FileManager/components/modals/ModalMessage.vue
  22. 155 0
      src/components/FileManager/components/modals/ModalMove.vue
  23. 74 0
      src/components/FileManager/components/modals/ModalNewFile.vue
  24. 118 0
      src/components/FileManager/components/modals/ModalNewFolder.vue
  25. 196 0
      src/components/FileManager/components/modals/ModalPreview.vue
  26. 130 0
      src/components/FileManager/components/modals/ModalRename.vue
  27. 78 0
      src/components/FileManager/components/modals/ModalUnarchive.vue
  28. 837 0
      src/components/FileManager/components/modals/ModalUpload.vue
  29. 51 0
      src/components/FileManager/components/previews/Audio.vue
  30. 33 0
      src/components/FileManager/components/previews/Default.vue
  31. 168 0
      src/components/FileManager/components/previews/Image.vue
  32. 61 0
      src/components/FileManager/components/previews/Pdf.vue
  33. 258 0
      src/components/FileManager/components/previews/Text.vue
  34. 49 0
      src/components/FileManager/components/previews/Video.vue
  35. 31 0
      src/components/FileManager/composables/useDebouncedRef.js
  36. 47 0
      src/components/FileManager/composables/useI18n.js
  37. 51 0
      src/components/FileManager/composables/useStorage.js
  38. 55 0
      src/components/FileManager/composables/useTheme.js
  39. 17 0
      src/components/FileManager/features.js
  40. 262 0
      src/components/FileManager/filemaps.js
  41. 32 0
      src/components/FileManager/index.js
  42. 89 0
      src/components/FileManager/locales/de.js
  43. 89 0
      src/components/FileManager/locales/en.js
  44. 89 0
      src/components/FileManager/locales/fa.js
  45. 90 0
      src/components/FileManager/locales/fr.js
  46. 89 0
      src/components/FileManager/locales/he.js
  47. 89 0
      src/components/FileManager/locales/hi.js
  48. 89 0
      src/components/FileManager/locales/ru.js
  49. 89 0
      src/components/FileManager/locales/sv.js
  50. 89 0
      src/components/FileManager/locales/tr.js
  51. 89 0
      src/components/FileManager/locales/zhCN.js
  52. 89 0
      src/components/FileManager/locales/zhTW.js
  53. 11 0
      src/components/FileManager/modals.js
  54. 243 0
      src/components/FileManager/utils/ajax.js
  55. 1 0
      src/components/FileManager/utils/datetimestring.js
  56. 30 0
      src/components/FileManager/utils/filesize.js
  57. 5 0
      src/components/FileManager/utils/title_shorten.js
  58. 1 0
      src/icons/svg/audio.svg
  59. 1 0
      src/icons/svg/full-screen-cancel.svg
  60. 1 0
      src/icons/svg/full-screen.svg
  61. 1 0
      src/icons/svg/grid.svg
  62. 1 0
      src/icons/svg/list-view.svg
  63. 1 0
      src/icons/svg/text-file.svg
  64. 1 0
      src/icons/svg/unknow-file.svg
  65. 1 0
      src/icons/svg/zip-file.svg
  66. 12 5
      src/main.js
  67. 27 0
      src/router/index.js
  68. 8 4
      src/utils/i18n.js
  69. 68 0
      src/views/article-manager/index.vue
  70. 1 1
      src/views/ncs-device/components/deviceManager.vue
  71. 33 0
      src/views/ncs-file-manager/index.vue
  72. 16 1
      vue.config.js

+ 2 - 1
.gitignore

@@ -1,5 +1,6 @@
 /node_modules/
-/dist/
+/dist/*
+dist
 .DS_Store
 .idea
 package-lock.json

+ 3 - 0
languages/zh-CN.js

@@ -1599,5 +1599,8 @@ module.exports = {
   wnn20240126: {
     MULTIFUNCTIONAL_BUTTON: '多功能按钮',
     S433_BJMD: '报警门灯'
+  },
+  wu20240322: {
+    fileManager: '文件管理器'
   }
 }

+ 22 - 2
package.json

@@ -15,10 +15,19 @@
     "test:ci": "npm run lint && npm run test:unit"
   },
   "dependencies": {
+    "@editorjs/editorjs": "^2.29.1",
+    "@editorjs/header": "^2.8.1",
+    "@editorjs/image": "^2.9.0",
+    "@editorjs/list": "^1.9.0",
+    "@editorjs/simple-image": "^1.6.0",
+    "@editorjs/text-variant-tune": "^1.0.1",
+    "@editorjs/warning": "^1.4.0",
     "@moefe/vue-aplayer": "^2.0.0-beta.5",
     "@toast-ui/editor": "^3.1.3",
+    "@uppy/locales": "^3.5.2",
     "@wangeditor/editor": "^5.1.23",
     "@wangeditor/editor-for-vue": "^1.0.2",
+    "ace-builds": "^1.32.7",
     "ag-grid-community": "^25.0.0",
     "ag-grid-vue": "^25.0.0",
     "axios": "0.18.1",
@@ -26,11 +35,18 @@
     "clipboard": "2.0.4",
     "codemirror": "5.45.0",
     "core-js": "3.6.5",
+    "cropperjs": "^1.6.1",
+    "dragselect": "^2.3.1",
     "driver.js": "0.9.5",
     "dropzone": "5.5.1",
     "echarts": "^5.2.2",
+    "editorjs-alert": "^1.1.3",
+    "editorjs-header-with-alignment": "^1.0.1",
+    "editorjs-indent-tune": "^1.2.1",
+    "editorjs-paragraph-with-alignment": "^3.0.0",
     "element-ui": "^2.15.6",
     "eslint-plugin-html": "^6.2.0",
+    "esm": "^3.2.25",
     "file-saver": "2.0.1",
     "fuse.js": "3.4.4",
     "janus-gateway": "1.1.4",
@@ -51,6 +67,8 @@
     "screenfull": "4.2.0",
     "script-loader": "0.7.2",
     "sortablejs": "1.8.4",
+    "title-editorjs": "^1.0.2",
+    "ts-loader": "^9.5.1",
     "v-viewer": "^1.4.0",
     "vant": "^2.12.54",
     "vcolorpicker": "^1.0.3",
@@ -86,11 +104,13 @@
     "connect": "3.6.6",
     "eslint": "^5.16.0",
     "eslint-plugin-vue": "^7.20.0",
+    "exports-loader": "^1.1.0",
     "html-webpack-plugin": "3.2.0",
     "husky": "1.3.1",
     "js-cookie": "2.2.0",
     "js-md5": "^0.7.3",
     "lint-staged": "8.1.5",
+    "microtip": "^0.2.2",
     "mockjs": "1.0.1-beta3",
     "plop": "2.3.0",
     "runjs": "4.3.2",
@@ -100,8 +120,8 @@
     "serve-static": "1.13.2",
     "svg-sprite-loader": "4.1.3",
     "svgo": "1.2.0",
-    "vue-template-compiler": "2.6.10",
-    "exports-loader": "^1.1.0"
+    "tailwindcss": "^3.1.8",
+    "vue-template-compiler": "2.6.10"
   },
   "browserslist": [
     "> 1%",

+ 5 - 5
public/domain.js

@@ -1,12 +1,12 @@
 const domain = {
-  serverUrl: 'http://172.28.100.100:8005',
-  DeviceUrl: 'http://172.28.100.100:8006',
-  mediaUrl: 'http://172.28.100.100:8004',
+  serverUrl: 'http://192.168.1.198:8005',
+  DeviceUrl: 'http://192.168.1.198:8006',
+  mediaUrl: 'http://192.168.1.198:8004',
   OnlineSystemUrl: 'http://api.base.wdklian.com',
   apiMode: 'dev',
   uiVersion: 1, // 1 医院版,2 月子中心版,3养老院版
-  enableBroadcast: false, //广播使能
-  enableMobile: false,  //手机使能
+  enableBroadcast: true, //广播使能
+  enableMobile: true,  //手机使能
   enableEntraceguard: false,  //门禁使能
   enableNBiot: false,  //NB设备
   enableCustomerDevice: false,  //用户设备

+ 74 - 0
src/components/FileManager/ServiceContainer.js

@@ -0,0 +1,74 @@
+
+import { buildRequester } from './utils/ajax.js'
+import { format as filesizeDefault, metricFormat as filesizeMetric } from './utils/filesize.js'
+import { useStorage } from './composables/useStorage.js'
+import { FEATURE_ALL_NAMES, FEATURES } from './features.js'
+import useTheme from './composables/useTheme.js'
+export default (props, options) => {
+  const storage = useStorage(props.id)
+  const metricUnits = storage.getStore('metricUnits', true)
+  const theme = useTheme(storage, props.theme)
+  // const supportedLocales = options.i18n
+  // const initialLang = props.locale ?? options.locale
+
+  const setFeatures = (features) => {
+    if (Array.isArray(features)) {
+      return features
+    }
+    return FEATURE_ALL_NAMES
+  }
+
+  const path = props.persist ? storage.getStore('path', props.path) : props.path
+
+  return {
+    // app version
+    version: '1.0.1',
+    // root element
+    root: null,
+    // app id
+    debug: props.debug,
+    // // Event Bus
+    // emitter: emitter,
+    // active features
+    features: setFeatures(FEATURE_ALL_NAMES),
+    // http object
+    requester: buildRequester(props.request),
+    // theme state
+    theme: theme,
+    // view state
+    view: storage.getStore('viewport', 'grid'),
+    // fullscreen state
+    fullScreen: storage.getStore('full-screen', props.fullScreen),
+    // selectButton state
+    selectButton: props.selectButton,
+    // unit state - for example: GB or GiB
+    metricUnits: metricUnits,
+    // human readable file sizes
+    filesize: metricUnits ? filesizeMetric : filesizeDefault,
+    // max file size
+    maxFileSize: props.maxFileSize,
+    // loading state
+    loading: false,
+    // default locale
+    // i18n: i18n,
+    // modal state
+    modal: {
+      active: false,
+      type: 'delete',
+      data: {}
+    },
+    // main storage adapter
+    adapter: storage.getStore('adapter'),
+    // main storage adapter
+    path: path,
+    // persist state
+    persist: props.persist,
+    // storage
+    storage: storage,
+    breadcrumb: [],
+    // fetched items
+    data: { adapter: storage.getStore('adapter'), storages: [], dirname: path, files: [], paths: [] },
+    // selected items
+    selectedItems: []
+  }
+}

+ 283 - 0
src/components/FileManager/VueFinder.vue

@@ -0,0 +1,283 @@
+<template>
+    <div class="vuefinder" ref="root">
+        <div :class="app.theme === 'dark' ? 'dark': ''">
+            <div
+                    :class="app.fullScreen ? 'fixed w-screen inset-0 z-20' : 'relative rounded-md'"
+                    :style="{'height':(this.mainAreaHeight)+'px'}"
+                    class="border flex flex-col bg-white dark:bg-gray-800 text-gray-700 dark:text-neutral-400 border-neutral-300 dark:border-gray-900 min-w-min select-none"
+                    @mousedown="mouseDown"
+                    @touchstart="mouseUp">
+                <toolbar/>
+                <breadcrumb/>
+                <Explorer/>
+                <!--                <v-f-explorer/>-->
+                 <statusbar/>
+            </div>
+
+            <Transition name="fade">
+              <component v-if="app.modal.active" :is="'modal-'+ app.modal.type"/>
+            </Transition>
+
+                        <context-menu/>
+        </div>
+    </div>
+</template>
+
+<script>
+    import ServiceContainer from "./ServiceContainer";
+    import Breadcrumb from "./components/Breadcrumb";
+    import Toolbar from "./components/Toolbar";
+    import Statusbar from "./components/Statusbar";
+    import Explorer from "./components/Explorer";
+    import ContextMenu from './components/ContextMenu.vue';
+
+    export default {
+        name: "VueFinder",
+        components: {Explorer, Statusbar, Toolbar, Breadcrumb,ContextMenu},
+        // inject:['VueFinderOptions'],
+
+        props: {
+            id: {
+                type: String,
+                default: 'vf'
+            },
+            request: {
+                type: [String, Object],
+                required: true,
+            },
+            persist: {
+                type: Boolean,
+                default: false,
+            },
+            path: {
+                type: String,
+                default: '.',
+            },
+            features: {
+                type: [Array, Boolean],
+                default: true,
+            },
+            debug: {
+                type: Boolean,
+                default: false,
+            },
+            theme: {
+                type: String,
+                default: 'system',
+            },
+            // locale: {
+            //     type: String,
+            //     default: ''
+            // },
+            maxHeight: {
+                type: String,
+                default: '600px'
+            },
+            maxFileSize: {
+                type: String,
+                default: '10mb'
+            },
+            fullScreen: {
+                type: Boolean,
+                default: false
+            },
+            selectButton: {
+                type: Object,
+                default(rawProps) {
+                    return {
+                        active: false,
+                        multiple: true,
+                        filters:[],
+                        click: (items) => {
+                            // items is an array of selected items
+                            //
+                        },
+                        ...rawProps,
+                    }
+                },
+            }
+        },
+        data() {
+
+            return {
+                app: ServiceContainer(this.$props, {}),
+                controller:null
+            }
+        },
+        provide() {
+            return {
+                ServiceContainer: this.app
+            }
+        },
+        methods: {
+            mouseDown() {
+                this.$bus.$emit('vf-contextmenu-hide')
+            },
+            mouseUp() {
+                this.$bus.$emit('vf-contextmenu-hide')
+            },
+           updateItems(data){
+                if(data){
+                if(data.paths.length>0){
+                    data.paths.forEach(item=>{
+                        Object.assign(item, {'href':`${data.href=='/'?'':data.href}/${item.name}`,'ext':this.getExt(item.name).toLocaleLowerCase()});
+                    })
+                }
+                Object.assign(this.app.data, data);
+                }
+                console.log('getdata',this.app.data,data)
+                this.$bus.$emit('vf-nodes-selected', {});
+                this.$bus.$emit('vf-explorer-update');
+            },
+            getExt(e){
+                const i = e.lastIndexOf('.');
+                return i === -1 ? '' : e.substring(i + 1, e.length);
+            }
+        },
+        mounted() {
+            let pathExists = {};
+            if (this.app.path.includes("://")) {
+                pathExists = {
+                    adapter: this.app.path.split("://")[0],
+                    path: this.app.path,
+                };
+            }
+
+            this.$bus.$on('vf-nodes-selected', (items) => {
+                console.log('node selected')
+                this.app.selectedItems = items;
+                this.$bus.$emit('select', items);
+            })
+
+            this.$bus.$on('vf-download', (url) => {
+                const filename = url.slice(url.lastIndexOf('/')+1)
+                fetch(url)
+                    .then((response) => {
+                        if (response.status === 200) {
+                            // 进度条相关
+                            // 获取 Response 对象的 body 属性的读取器(reader)。body 属性是一个可读的流(ReadableStream),可以用来读取响应体的数据。
+                            const reader = response.body.getReader();
+                            const contentLength = +response.headers.get('Content-Length');
+                            let receivedLength = 0;
+                            let chunks = [];
+                            // 读取一个数据块。这个方法返回一个 Promise,当一个数据块被读取时,这个 Promise 就会解析为一个包含 done 和 value 属性的对象。done 属性表示是否已经读取完所有数据,value 属性是一个 Uint8Array,包含了读取到的数据块。
+                            return reader.read().then(function processChunk({ done, value }) {
+                                if (done) {
+                                    console.log('下载完成');
+                                    // 将 chunks 数组中的所有数据块复制到一个新的 Uint8Array 中,然后使用这个 Uint8Array 创建一个 Blob 对象
+                                    let data = new Uint8Array(receivedLength);
+                                    let position = 0;
+                                    for(let chunk of chunks) {
+                                        data.set(chunk, position);
+                                        position += chunk.length;
+                                    }
+                                    return new Blob([data]);
+                                }
+                                // 将读取到的数据块添加到 chunks 数组中。
+                                chunks.push(value);
+                                // 更新已接收的数据长度。
+                                receivedLength += value.length;
+                                console.log(`已下载:${receivedLength},总大小:${contentLength}`);
+                                // 递归调用 reader.read(),直到读取完所有数据。
+                                return reader.read().then(processChunk);
+                            });
+                        } else {
+                            console.log('下载失败')
+                        }
+                    })
+                    .then((blob) => {
+                        const blobUrl = window.URL.createObjectURL(blob)
+                        const aEl = document.createElement('a')
+                        aEl.href = blobUrl
+                        aEl.download = filename
+                        aEl.click()
+                        window.URL.revokeObjectURL(blobUrl)
+                        console.log("下载成功")
+                    })
+                    .finally(() => {
+                        // xxx
+                    })
+
+                // const $a = document.createElement('a');
+                // $a.style.display = 'none';
+                // $a.target = '_blank';
+                // // $a.href = url;
+                // // Cross-origin this doesn't work, but at least this does bring up a new window.
+                // $a.download = url;
+                // this.$refs.root.appendChild($a);
+                // $a.click();
+                // $a.remove();
+            });
+            this.$bus.$on('vf-fetch', ({params,url='',headers=null,responseType='json', method=null, body = null, onSuccess = null, onError = null, noCloseModal = false})=>{
+                console.log('v-fetch-method',method)
+                if (params&&['index', 'search'].includes(params.q)) {
+                    if (this.controller) {
+                        this.controller.abort();
+                    }
+                    this.app.loading = true;
+                }
+
+                this.controller = new AbortController();
+                const signal = this.controller.signal;
+                this.app.requester.send({
+                    url: url,
+                    method: method || 'get',
+                    params,
+                    body,
+                    headers,
+                    responseType,
+                    abortSignal: signal,
+                }).then(data => {
+                    console.log('movedata',data)
+                    // this.app.adapter = data.adapter;
+                    if(method!=='MOVE'&&method!=='DELETE'&&method!=='MKCOL'){
+                        this.app.data.dirname=url
+                    }
+
+                    // if (this.app.persist) {
+                    //     this.app.path = data.href;
+                    //     console.log('thispath',this.app.path)
+                    //     const {setStore} = this.app.storage;
+                    //     setStore('path', app.path);
+                    // }
+
+                    if (params&&['index', 'search'].includes(params.q)) {
+                        this.app.loading = false;
+                    }
+                    if (!noCloseModal) {
+                        this.$bus.$emit('vf-modal-close');
+                    }
+                    this.updateItems(data);
+                    if (onSuccess) {
+                        onSuccess(data);
+                    }
+                }).catch((e) => {
+                    console.error(e)
+                    if (onError) {
+                        onError(e);
+                    }
+                });
+            })
+
+            this.$bus.$emit('vf-fetch', {params: {json:'',q: '', adapter: this.app.adapter, ...pathExists}});
+
+            this.$bus.$on('vf-modal-show', (item) => {
+                console.log('modalshow',item)
+                this.app.modal.active = true;
+                this.app.modal.type = item.type;
+                this.app.modal.data = item;
+            })
+            this.$bus.$on('vf-modal-close', () => {
+                this.app.modal.active = false;
+            });
+            this.app.root=this.$refs.root
+        }
+    }
+</script>
+
+<style scoped>
+    /*@import url("microtip/microtip.css");*/
+    @import url("./assets/css/a.css");
+    @import url("./assets/css/index.css");
+
+</style>

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 3549 - 0
src/components/FileManager/assets/css/a.css


+ 119 - 0
src/components/FileManager/assets/css/index.css

@@ -0,0 +1,119 @@
+@tailwind base;
+
+@tailwind components;
+
+@layer components {
+    .vuefinder {
+        position: relative;
+    }
+
+    .vuefinder * {
+        touch-action: manipulation;
+    }
+
+    .vuefinder .vf-btn {
+        @apply
+        inline-flex justify-center
+        w-full px-4 py-2 rounded-md border shadow-sm
+        text-base font-medium tracking-wide
+        focus:outline-none focus:ring-2 focus:ring-offset-2
+        mt-0.5 sm:mx-1 sm:w-auto sm:text-sm;
+    }
+
+    .vuefinder .vf-btn-primary {
+        @apply
+        border-transparent
+        text-white
+        bg-blue-600 hover:bg-blue-700
+        focus:ring-indigo-500
+        dark:text-gray-50 dark:border-gray-800 dark:bg-gray-700 dark:hover:bg-gray-500;
+    }
+    .vuefinder .vf-btn-primary.disabled{
+        @apply
+        hover:bg-blue-600
+        dark:hover:bg-gray-700
+    }
+
+    .vuefinder .vf-btn-secondary {
+        @apply
+        border-gray-300
+        text-gray-700
+        bg-white hover:bg-gray-50
+        focus:ring-gray-500
+        dark:text-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-500;
+    }
+
+    .vuefinder .vf-btn-secondary.disabled{
+        @apply
+        hover:bg-gray-50
+        dark:hover:bg-gray-800
+    }
+
+    .vuefinder .vf-btn-danger {
+        @apply
+        border-transparent
+        text-white
+        bg-red-600 hover:bg-red-700
+        focus:ring-red-200 focus:ring-1 focus:ring-offset-1
+        dark:text-gray-50 dark:border-red-800 dark:bg-red-700 dark:hover:bg-red-500;
+    }
+
+    .vuefinder .disabled {
+        @apply
+        opacity-50 cursor-not-allowed
+    }
+
+
+
+    .vf-explorer-selected {
+        @apply bg-neutral-100 border border-neutral-300 dark:bg-slate-700 dark:border-gray-900 dark:border-slate-800 !important;
+    }
+
+    .vf-explorer-selector {
+        @apply border border-slate-500 bg-slate-300 opacity-50 !important;
+    }
+
+    .vuefinder.dark {
+        color-scheme: dark;
+    }
+
+    /* width */
+    .vf-scrollbar::-webkit-scrollbar {
+        width: 12px;
+    }
+
+    /* Track */
+    .vf-scrollbar::-webkit-scrollbar-track-piece {
+        @apply bg-gray-100 dark:bg-slate-900/50;
+    }
+
+    /* Handle */
+    .vf-scrollbar::-webkit-scrollbar-thumb {
+        @apply bg-gray-300 dark:bg-slate-700;
+    }
+
+    /* Handle on hover */
+    .vf-scrollbar::-webkit-scrollbar-thumb:hover {
+        @apply bg-gray-400 dark:bg-slate-600;
+    }
+
+    .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;
+

+ 353 - 0
src/components/FileManager/assets/css/preflight.css

@@ -0,0 +1,353 @@
+/*
+1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
+2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
+*/
+
+*,
+::before,
+::after {
+  box-sizing: border-box; /* 1 */
+  border-width: 0; /* 2 */
+  border-style: solid; /* 2 */
+  border-color: theme('borderColor.DEFAULT', currentColor); /* 2 */
+}
+
+::before,
+::after {
+  --tw-content: '';
+}
+
+/*
+1. Add the correct height in Firefox.
+2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
+3. Ensure horizontal rules are visible by default.
+*/
+
+hr {
+  height: 0; /* 1 */
+  color: inherit; /* 2 */
+  border-top-width: 1px; /* 3 */
+}
+
+/*
+Add the correct text decoration in Chrome, Edge, and Safari.
+*/
+
+abbr:where([title]) {
+  text-decoration: underline dotted;
+}
+
+/*
+Remove the default font size and weight for headings.
+*/
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  font-size: inherit;
+  font-weight: inherit;
+}
+
+/*
+Reset links to optimize for opt-in styling instead of opt-out.
+*/
+
+a {
+  color: inherit;
+  text-decoration: inherit;
+}
+
+/*
+Add the correct font weight in Edge and Safari.
+*/
+
+b,
+strong {
+  font-weight: bolder;
+}
+
+/*
+1. Use the user's configured `mono` font-family by default.
+2. Use the user's configured `mono` font-feature-settings by default.
+3. Use the user's configured `mono` font-variation-settings by default.
+4. Correct the odd `em` font sizing in all browsers.
+*/
+
+code,
+kbd,
+samp,
+pre {
+  font-family: theme('fontFamily.mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); /* 1 */
+  font-feature-settings: theme('fontFamily.mono[1].fontFeatureSettings', normal); /* 2 */
+  font-variation-settings: theme('fontFamily.mono[1].fontVariationSettings', normal); /* 3 */
+  font-size: 1em; /* 4 */
+}
+
+/*
+Add the correct font size in all browsers.
+*/
+
+small {
+  font-size: 80%;
+}
+
+/*
+Prevent `sub` and `sup` elements from affecting the line height in all browsers.
+*/
+
+sub,
+sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+
+sub {
+  bottom: -0.25em;
+}
+
+sup {
+  top: -0.5em;
+}
+
+/*
+1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
+2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
+3. Remove gaps between table borders by default.
+*/
+
+table {
+  text-indent: 0; /* 1 */
+  border-color: inherit; /* 2 */
+  border-collapse: collapse; /* 3 */
+}
+
+/*
+1. Change the font styles in all browsers.
+2. Remove the margin in Firefox and Safari.
+3. Remove default padding in all browsers.
+*/
+
+button,
+input,
+optgroup,
+select,
+textarea {
+  font-family: inherit; /* 1 */
+  font-feature-settings: inherit; /* 1 */
+  font-variation-settings: inherit; /* 1 */
+  font-size: 100%; /* 1 */
+  font-weight: inherit; /* 1 */
+  line-height: inherit; /* 1 */
+  color: inherit; /* 1 */
+  margin: 0; /* 2 */
+  padding: 0; /* 3 */
+}
+
+/*
+Remove the inheritance of text transform in Edge and Firefox.
+*/
+
+button,
+select {
+  text-transform: none;
+}
+
+/*
+1. Correct the inability to style clickable types in iOS and Safari.
+2. Remove default button styles.
+*/
+
+button,
+[type='button'],
+[type='reset'],
+[type='submit'] {
+  -webkit-appearance: button; /* 1 */
+  background-color: transparent; /* 2 */
+  background-image: none; /* 2 */
+}
+
+/*
+Use the modern Firefox focus style for all focusable elements.
+*/
+
+:-moz-focusring {
+  outline: auto;
+}
+
+/*
+Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
+*/
+
+:-moz-ui-invalid {
+  box-shadow: none;
+}
+
+/*
+Add the correct vertical alignment in Chrome and Firefox.
+*/
+
+progress {
+  vertical-align: baseline;
+}
+
+/*
+Correct the cursor style of increment and decrement buttons in Safari.
+*/
+
+::-webkit-inner-spin-button,
+::-webkit-outer-spin-button {
+  height: auto;
+}
+
+/*
+1. Correct the odd appearance in Chrome and Safari.
+2. Correct the outline style in Safari.
+*/
+
+[type='search'] {
+  -webkit-appearance: textfield; /* 1 */
+  outline-offset: -2px; /* 2 */
+}
+
+/*
+Remove the inner padding in Chrome and Safari on macOS.
+*/
+
+::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+/*
+1. Correct the inability to style clickable types in iOS and Safari.
+2. Change font properties to `inherit` in Safari.
+*/
+
+::-webkit-file-upload-button {
+  -webkit-appearance: button; /* 1 */
+  font: inherit; /* 2 */
+}
+
+/*
+Add the correct display in Chrome and Safari.
+*/
+
+summary {
+  display: list-item;
+}
+
+/*
+Removes the default spacing and border for appropriate elements.
+*/
+
+blockquote,
+dl,
+dd,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+hr,
+figure,
+p,
+pre {
+  margin: 0;
+}
+
+fieldset {
+  margin: 0;
+  padding: 0;
+}
+
+legend {
+  padding: 0;
+}
+
+ol,
+ul,
+menu {
+  list-style: none;
+  margin: 0;
+  padding: 0;
+}
+
+/*
+Reset default styling for dialogs.
+*/
+dialog {
+  padding: 0;
+}
+
+/*
+Prevent resizing textareas horizontally by default.
+*/
+
+textarea {
+  resize: vertical;
+}
+
+/*
+1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
+2. Set the default placeholder color to the user's configured gray 400 color.
+*/
+
+input::placeholder,
+textarea::placeholder {
+  opacity: 1; /* 1 */
+  color: theme('colors.gray.400', #9ca3af); /* 2 */
+}
+
+/*
+Set the default cursor for buttons.
+*/
+
+button,
+[role="button"] {
+  cursor: pointer;
+}
+
+/*
+Make sure disabled buttons don't get the pointer cursor.
+*/
+:disabled {
+  cursor: default;
+}
+
+/*
+1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
+2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
+   This can trigger a poorly considered lint error in some tools but is included by design.
+*/
+
+img,
+svg,
+video,
+canvas,
+audio,
+iframe,
+embed,
+object {
+  display: block; /* 1 */
+  vertical-align: middle; /* 2 */
+}
+
+/*
+Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
+*/
+
+img,
+video {
+  max-width: 100%;
+  height: auto;
+}
+
+/* Make elements with the HTML hidden attribute stay hidden by default */
+[hidden] {
+  display: none;
+}

+ 41 - 0
src/components/FileManager/components/ActionMessage.vue

@@ -0,0 +1,41 @@
+<template>
+    <div class="text-sm text-green-600 dark:text-green-600 transition-opacity duration-500 ease-out"
+         :class="[{ 'opacity-0': !shown }]">
+        <slot v-if="$slots.default"/>
+        <span v-else>{{ t('Saved.') }}</span>
+    </div>
+</template>
+
+<script>
+    export default {
+        name: "ActionMessage",
+        props: {
+            on: {type: String, required: true},
+        },
+        data() {
+            return {
+                shown: false,
+                timeout:null
+            }
+        },
+        methods: {
+            handleEvent() {
+                clearTimeout(this.timeout);
+                this.shown.value = true;
+                this.timeout = setTimeout(() => {
+                    this.shown.value = false;
+                }, 2000);
+            }
+        },
+        mounted() {
+           this.$bus.$on(this.props.on,handleEvent)
+        },
+        destroyed() {
+            this.$bus.$off(this.props.on)
+        }
+    }
+</script>
+
+<style scoped>
+
+</style>

+ 239 - 0
src/components/FileManager/components/Breadcrumb.vue

@@ -0,0 +1,239 @@
+<template>
+    <div class="flex p-1.5 bg-neutral-100 dark:bg-gray-800 border-t border-b border-neutral-300 dark:border-gray-700/50 items-center select-none text-sm">
+    <span :aria-label="this.$t('Go up a directory')" data-microtip-position="bottom-right" role="tooltip">
+
+        <el-button type="plain" icon="el-icon-top" circle @dragover="handleDragOver($event)"
+                   :disabled="!isGoUpAvailable"
+                   @dragleave="handleDragLeave($event)"
+                   @drop="handleDropZone($event)"
+                   @click="goUp"></el-button>
+
+
+    </span>
+
+        <span :aria-label="this.$t('Refresh')" data-microtip-position="bottom-right" role="tooltip" v-if="!this.ServiceContainer.loading">
+             <el-button type="plain" icon="el-icon-refresh-right" circle @click="refresh"></el-button>
+
+
+    </span>
+        <span :aria-label="this.$t('Cancel')" data-microtip-position="bottom-right" role="tooltip" v-else>
+            <el-button type="plain" icon="el-icon-close" circle @click="this.$bus.$on('vf-fetch-abort')"></el-button>
+
+    </span>
+
+        <div v-if="!searchMode" class="group flex bg-white dark:bg-gray-700 items-center rounded p-1 ml-2 w-full"
+             @click.self="enterSearchMode">
+
+<!--            <el-button type="plain" icon="el-icon-s-home" circle  class="h-6 w-6 p-1 rounded text-slate-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-gray-800 cursor-pointer"-->
+<!--                       @dragover="handleDragOver($event)"-->
+<!--                       @dragleave="handleDragLeave($event)"-->
+<!--                       @drop="handleDropZone($event, 0)"-->
+<!--                       @click="changeDir({title:'',href:'/'})"></el-button>-->
+            <i class="el-icon-s-home h-6 w-6 p-1 rounded text-slate-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-gray-800 cursor-pointer" @dragover="handleDragOver($event)"
+               @dragleave="handleDragLeave($event)"
+               @drop="handleDropZone($event, -1)"
+               @click="changeDir({title:'',href:'/'})"></i>
+
+
+            <div class="flex leading-6">
+                <div v-for="(item, index) in breadcrumb" :key="index">
+                    <span class="text-neutral-300 dark:text-gray-600 mx-0.5">/</span>
+                    <span
+                            @dragover="(index === breadcrumb.length - 1) || handleDragOver($event)"
+                            @dragleave="(index === breadcrumb.length - 1) || handleDragLeave($event)"
+                            @drop="(index === breadcrumb.length - 1) || handleDropZone($event, index)"
+                            class="px-1.5 py-1 text-slate-700 dark:text-slate-200 hover:bg-neutral-100 dark:hover:bg-gray-800 rounded cursor-pointer"
+                            :title="item.href"
+                            @click="changeDir(item)">{{ item.title }}</span>
+                </div>
+            </div>
+
+
+            <span class="animate-spin p-1 h-6 w-6 text-white ml-auto" v-if="this.ServiceContainer.loading" v-loading="this.ServiceContainer.loading"></span>
+
+        </div>
+        <div v-else
+             class="relative flex bg-white dark:bg-gray-700 justify-between items-center rounded p-1 ml-2 w-full">
+            <i class="el-icon-s-home h-6 w-6 p-1 rounded text-slate-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-gray-800 cursor-pointer"/>
+<!--                <el-button type="plain" icon="el-icon-s-home" circle class="h-6 w-6 p-1 m-auto stroke-gray-400 fill-gray-100 dark:stroke-gray-400 dark:fill-gray-400/20"></el-button>-->
+
+
+            <input
+                    ref="searchInput"
+                    @keydown.esc="exitSearchMode"
+                    @blur="handleBlur"
+                    v-model="query"
+                    :placeholder="this.$t('Search anything..')"
+                    class="w-full pb-0 px-1 border-0 text-base ring-0 outline-0 text-gray-600 focus:ring-transparent focus:border-transparent dark:focus:ring-transparent dark:focus:border-transparent dark:text-gray-300 bg-transparent"
+                    type="text" />
+            <svg
+                    class="w-6 h-6 cursor-pointer"
+                    @click="exitSearchMode"
+                    xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
+                    stroke="currentColor">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
+            </svg>
+        </div>
+    </div>
+</template>
+
+<script>
+    import {FEATURES} from "../features.js";
+    export default {
+        name: "Breadcrumb",
+        data() {
+            return {
+                dirname: [],
+                breadcrumb: [],
+                searchMode: false,
+                // searchInput: null,
+                query: ''
+            }
+        },
+        inject:['ServiceContainer'],
+        computed:{
+            isGoUpAvailable(){
+                return this.breadcrumb.length && !this.searchMode;
+            }
+        },
+        watch:{
+          query(newQuery){
+              console.log('query',newQuery)
+              this.$bus.$emit('vf-toast-clear');
+              this.$bus.$emit('vf-search-query', {newQuery});
+          }
+        },
+        mounted() {
+            this.$bus.$on('vf-search-exit', () => {
+                this.exitSearchMode();
+            })
+            this.$bus.$on('vf-explorer-update', () => {
+                let items = [], links = [];
+                this.dirname = this.ServiceContainer.data.dirname
+               console.log('update-dirname',this.dirname)
+                // if (this.dirname.length == 0) {
+                //     links = [{title: '/', href: '/'}];
+                // }
+
+                let h = '/';
+                console.log('removeSuffix',this.removeSuffix(this.dirname, '/').split('/').slice(1))
+                for (const p of this.removeSuffix(this.dirname, '/').split('/').slice(1)) {
+                    h += p+'/'
+                    links.push({title: p, href: h.slice(0, -1)});
+                }
+                // this.dirname
+                //     .forEach(function (item) {
+                //         items.push(item);
+                //         if (items.join('') != '') {
+                //             links.push({
+                //                 'basename': item,
+                //                 'name': item,
+                //                 'path': items,
+                //                 'type': 'dir'
+                //             });
+                //         }
+                //     });
+
+                // if (links.length > 4) {
+                //     links = links.slice(-5);
+                //     // links[0].name = '..';
+                // }
+
+                this.breadcrumb = links;
+                this.ServiceContainer.breadcrumb=links
+                console.log('links',links)
+            })
+        },
+        methods: {
+            exitSearchMode() {
+                this.searchMode = false
+                this.query = ''
+            },
+            enterSearchMode() {
+                if (!this.ServiceContainer.features.includes(FEATURES.SEARCH)) {
+                    return
+                }
+                this.searchMode = true
+                this.$nextTick(() => this.$refs.searchInput.focus())
+            },
+            handleDropZone(e, index = null) {
+                e.preventDefault();
+                console.log('draggedItems',draggedItems)
+                this.handleDragLeave(e);
+                if(index!==-1){
+                index ??= this.breadcrumb.length - 2;
+                }
+
+                let draggedItems = JSON.parse(e.dataTransfer.getData('items'));
+                console.log('index',this.breadcrumb[index])
+
+                // if (draggedItems.find(item => item.storage !== app.adapter)) {
+                //     alert('Moving items between different storages is not supported yet.');
+                //     return;
+                // }
+
+                this.$bus.$emit('vf-modal-show', {
+                    type: 'move',
+                    items: {from: draggedItems, to: this.breadcrumb[index] ?? {href: '/'}}
+                })
+            },
+            handleDragOver(e){
+                console.log('breadcrumb-dragover')
+                e.preventDefault();
+
+
+                if (this.isGoUpAvailable) {
+                    e.dataTransfer.dropEffect = 'copy';
+                    e.currentTarget.classList.add('bg-blue-200','dark:bg-slate-500');
+                } else {
+                    e.dataTransfer.dropEffect = 'none';
+                    e.dataTransfer.effectAllowed = 'none';
+                }
+            },
+            handleDragLeave(e){
+                e.preventDefault();
+                e.currentTarget.classList.remove('bg-blue-200','dark:bg-slate-500');
+                if (this.isGoUpAvailable) {
+                    e.currentTarget.classList.remove('bg-blue-200','dark:bg-slate-500');
+                }
+            },
+            handleBlur(){
+                if (this.query ==='') {
+                    this.exitSearchMode();
+                }
+            },
+            removeSuffix(s, p){return s.endsWith(p) ? s.substring(0, s.length - p.length) : s},
+            changeDir(item){
+                console.log('item',item)
+                this.$bus.$emit('vf-fetch', {url:item.href,params:{json:'', adapter: '' }})
+            },
+            goUp(){
+                const dirname = this.ServiceContainer.data.dirname
+                let topdir = dirname.slice(0,dirname.lastIndexOf('/'))
+                if(topdir===''){
+                    topdir='/'
+                }
+                this.$bus.$emit('vf-fetch', {url:topdir,params:{json:'', adapter: '' }})
+
+            },
+            refresh(){
+                if(!this.searchMode){
+                    this.changeDir({href:this.ServiceContainer.data.dirname,title:''})
+                }else{
+                    this.$bus.$emit('vf-search-query', {'newQuery':this.query});
+                }
+            }
+        }
+    }
+</script>
+
+<style scoped>
+/deep/ .el-button--medium{
+    font-size: 1.1rem;
+    padding:6px
+}
+.el-icon-s-home{
+    font-size: 1.1rem;
+    line-height: 1rem;
+}
+</style>

+ 441 - 0
src/components/FileManager/components/ContextMenu.vue

@@ -0,0 +1,441 @@
+<template>
+  <ul class="z-30 absolute text-xs bg-neutral-50 dark:bg-gray-800 text-gray-700 dark:text-gray-200 border border-neutral-300 dark:border-gray-600 shadow rounded select-none" ref="contextmenu" v-if="context.active" :style="context.positions">
+    <li class="px-2 py-1.5 cursor-pointer hover:bg-neutral-200 dark:hover:bg-gray-700"
+        v-for="(item) in filteredItems" :key="item.title()" @click="run(item)">
+      <template v-if="item.link">
+        <a target="_blank" :href="item.link" :download="item.link">
+          <span class="px-1"></span>
+          <span>{{ item.title() }}</span>
+        </a>
+      </template>
+      <template v-else>
+        <span class="px-1"></span>
+        <span>{{ item.title() }}</span>
+      </template>
+    </li>
+  </ul>
+</template>
+
+<script>
+  import {FEATURES} from "../features.js";
+export default {
+  name: 'ContextMenu',
+  inject: ['ServiceContainer'],
+  data(){
+    return{
+      FEATURES,
+      context:{
+        active: false,
+        items: [],
+        positions: {
+          left: 0,
+          top: 0
+        }
+      },
+      searchQuery:null,
+      menuItems:null,
+      selectedItems:[],
+
+    }
+  },
+  mounted(){
+    const _this=this
+    this.menuItems={
+      newfolder: {
+        key: FEATURES.NEW_FOLDER,
+        title: () => _this.$t('New Folder'),
+        action: () => {
+
+          let selectItem = null
+          const lastBreadcrumb = _this.ServiceContainer.breadcrumb[_this.ServiceContainer.breadcrumb.length-1]
+          console.log('lastBreadcrumb',lastBreadcrumb)
+          if(lastBreadcrumb){
+            selectItem=lastBreadcrumb
+          }else{
+            selectItem={'href':'/'}
+          }
+
+          _this.$bus.$emit('vf-modal-show', {type:'new-folder',items: selectItem});
+        },
+      },
+      delete: {
+        key: FEATURES.DELETE,
+        title: () => _this.t('Delete'),
+        action: () => {
+          _this.$bus.$emit('vf-modal-show', {type:'delete', items: _this.selectedItems});
+        },
+      },
+      refresh: {
+        title: () =>  _this.t('Refresh'),
+        action: () => {
+          _this.$bus.$emit('vf-fetch',{url:`${this.ServiceContainer.data.dirname==='/'?'':this.ServiceContainer.data.dirname}`,params:{json:''}} );
+        },
+      },
+      upload:{
+        title: () =>  _this.t('Upload'),
+        action: () => {
+          let selectItem = null
+          // console.log('selectItem',selectItem)
+
+          const lastBreadcrumb = _this.ServiceContainer.breadcrumb[_this.ServiceContainer.breadcrumb.length-1]
+          if(lastBreadcrumb){
+            selectItem=lastBreadcrumb
+          }else{
+            selectItem={'href':'/'}
+          }
+
+          _this.$bus.$emit('vf-modal-show', {type:'upload', items: selectItem})
+          // _this.$bus.$emit('vf-fetch',{url:`${this.ServiceContainer.data.dirname==='/'?'':this.ServiceContainer.data.dirname}`,params:{json:''}} );
+        },
+      },
+      preview: {
+        key: FEATURES.PREVIEW,
+        title: () =>  _this.t('Preview'),
+        action: () => {
+          _this.$bus.$emit('vf-modal-show', {type:'preview', adapter:_this.ServiceContainer.data.adapter, item: _this.selectedItems[0]});
+        },
+      },
+      open: {
+        title: () =>  _this.t('Open'),
+        action: () => {
+          _this.$bus.$emit('vf-search-exit');
+
+          _this.$bus.$emit('vf-fetch', {url:`${this.ServiceContainer.data.dirname==='/'?'':this.ServiceContainer.data.dirname}/${_this.selectedItems[0].name}`,params:{json:''}});
+          // _this.$bus.$emit('vf-fetch', {params:{q: 'index', adapter: _this.ServiceContainer.data.adapter, path:_this.selectedItems[0].path}});
+        },
+      },
+      openDir: {
+        title: () =>  _this.t('Open containing folder'),
+        action: () => {
+          const name =_this.selectedItems[0].href
+          let dir=''
+          if(_this.selectedItems[0].path_type==='Dir'){
+             dir=_this.selectedItems[0].href
+          }else{
+             dir =name.slice(0,name.lastIndexOf('/'))
+          }
+
+          console.log('dir',dir)
+          _this.ServiceContainer.data.dirname=dir
+          // _this.$bus.$emit('vf-fetch', {url:`${dir}`,params:{json:''}});
+          _this.$bus.$emit('vf-search-exit');
+          // _this.$bus.$emit('vf-fetch', {params:{q: 'index', adapter: _this.ServiceContainer.data.adapter, path: (_this.selectedItems[0].dir)}});
+        },
+      },
+      download: {
+        key: FEATURES.DOWNLOAD,
+        link:"" ,// _this.ServiceContainer.requester.getDownloadUrl(_this.ServiceContainer.data.adapter, _this.selectedItems[0]),
+        title: () =>  _this.t('Download'),
+        action: () => {
+          const url = _this.ServiceContainer.requester.getDownloadUrl(null, _this.selectedItems[0]);
+          _this.$bus.$emit('vf-download', url);
+        },
+      },
+      archive: {
+        key: FEATURES.ARCHIVE,
+        title: () =>  _this.t('Archive'),
+        action: () => {
+          _this.$bus.$emit('vf-modal-show', {type:'archive', items: _this.selectedItems});
+        },
+      },
+      unarchive: {
+        key: FEATURES.UNARCHIVE,
+        title: () => _this.t('Unarchive'),
+        action: () => {
+          _this.$bus.$emit('vf-modal-show', {type:'unarchive', items: _this.selectedItems});
+        },
+      },
+      rename: {
+        key: FEATURES.RENAME,
+        title: () =>  _this.t('Rename'),
+        action: () => {
+
+          _this.$bus.$emit('vf-modal-show', {type:'rename', items: this.selectedItems[0]});
+        },
+      }
+    }
+    this.$bus.$on('vf-context-selected', (items) => {
+      this.selectedItems = items;
+    })
+    this.$bus.$on('vf-search-query', ({newQuery}) => {
+      this.searchQuery = newQuery;
+    })
+    this.$bus.$on('vf-contextmenu-show', ({event, area, items,  target = null}) => {
+      this.context.items = [];
+      console.log('target',target)
+      if (this.searchQuery) {
+        if (target) {
+          this.context.items.push(this.menuItems.openDir);
+          this.$bus.$emit('vf-context-selected', [target]);
+          // console.log('search item selected');
+        } else {
+          return;
+        }
+      } else if (!target && !this.searchQuery) {
+        this.context.items.push(this.menuItems.refresh);
+        this.context.items.push(this.menuItems.newfolder);
+        this.context.items.push(this.menuItems.upload);
+        this.$bus.$emit('vf-context-selected', []);
+        // console.log('no files selected');
+      } else if (items.length > 1 && items.some(el => el.path === target.path)) {
+        this.context.items.push(this.menuItems.refresh);
+        // this.context.items.push(this.menuItems.archive);
+        this.context.items.push(this.menuItems.delete);
+        this.$bus.$emit('vf-context-selected', items);
+        // console.log(items.length + ' selected (more than 1 item.)');
+      } else {
+        if (target.path_type == 'Dir') {
+          this.context.items.push(this.menuItems.open);
+        } else {
+          this.context.items.push(this.menuItems.preview);
+          this.context.items.push(this.menuItems.download);
+        }
+        this.context.items.push(this.menuItems.rename);
+
+        // if (target.mime_type == 'application/zip') {
+        //   this.context.items.push(this.menuItems.unarchive);
+        // } else {
+        //   this.context.items.push(this.menuItems.archive);
+        // }
+        this.context.items.push(this.menuItems.delete);
+        this.$bus.$emit('vf-context-selected', [target]);
+        // console.log(target.type + ' is selected');
+      }
+      this.context.active = true;
+      this.showContextMenu(event, area)
+    })
+    this.$bus.$on('vf-contextmenu-hide', () => {
+      this.context.active = false;
+    })
+  },
+  computed:{
+    filteredItems(){
+
+      const {features}=this.ServiceContainer
+      console.log(this.context.items)
+      return  this.context.items.filter(item => item.key == null || features.includes(item.key))
+      // console.log('features',this.ServiceContainer.features,this.context.items,[...features])
+      // return this.context.items.filter(item => item.key !== null)
+    }
+  },
+  methods:{
+    run(item){
+      this.$bus.$emit('vf-contextmenu-hide');
+      item.action();
+    },
+    t(str){
+      return this.$t(str)
+    },
+    showContextMenu(event, area){
+
+
+      this.$nextTick(() => {
+        console.log('root',this.ServiceContainer.root)
+        const rootBbox = this.ServiceContainer.root.getBoundingClientRect();
+        const areaContainer = area.getBoundingClientRect();
+
+        let left = event.pageX - rootBbox.left;
+        let top = event.pageY - rootBbox.top;
+        console.log(
+                'ctxmenu',
+                this.$refs.contextmenu
+        )
+        const refContextMenu = this.$refs.contextmenu
+        let menuHeight = refContextMenu?refContextMenu.offsetHeight:0;
+        let menuWidth = refContextMenu?refContextMenu.offsetWidth:0;
+
+        left = (areaContainer.right - event.pageX + window.scrollX) < menuWidth ? left - menuWidth : left;
+        top = (areaContainer.bottom - event.pageY + window.scrollY) < menuHeight ? top - menuHeight : top;
+
+        this.context.positions = {
+          left: left + 'px',
+          top: top + 'px'
+        };
+      });
+    }
+  }
+};
+</script>
+
+<!--<script setup>-->
+<!--import {computed, inject, nextTick, reactive, ref} from 'vue';-->
+<!--import {FEATURES} from "./features.js";-->
+
+<!--const app = inject('ServiceContainer');-->
+<!--const {t} = app.i18n-->
+
+<!--const contextmenu = ref(null);-->
+<!--const selectedItems = ref([]);-->
+<!--const searchQuery = ref('');-->
+
+<!--const context = reactive({-->
+<!--  active: false,-->
+<!--  items: [],-->
+<!--  positions: {-->
+<!--    left: 0,-->
+<!--    top: 0-->
+<!--  }-->
+<!--});-->
+
+<!--const filteredItems = computed(() => {-->
+<!--  return context.items.filter(item => item.key == null || app.features.includes(item.key))-->
+<!--});-->
+
+<!--app.emitter.on('vf-context-selected', (items) => {-->
+<!--  selectedItems.value = items;-->
+<!--})-->
+
+<!--const menuItems = {-->
+<!--  newfolder: {-->
+<!--    key: FEATURES.NEW_FOLDER,-->
+<!--    title: () => t('New Folder'),-->
+<!--    action: () => {-->
+<!--      app.emitter.emit('vf-modal-show', {type:'new-folder'});-->
+<!--    },-->
+<!--  },-->
+<!--  delete: {-->
+<!--    key: FEATURES.DELETE,-->
+<!--    title: () => t('Delete'),-->
+<!--    action: () => {-->
+<!--      app.emitter.emit('vf-modal-show', {type:'delete', items: selectedItems});-->
+<!--    },-->
+<!--  },-->
+<!--  refresh: {-->
+<!--    title: () =>  t('Refresh'),-->
+<!--    action: () => {-->
+<!--      app.emitter.emit('vf-fetch',{params:{q: 'index', adapter: app.data.adapter, path: app.data.dirname}} );-->
+<!--    },-->
+<!--  },-->
+<!--  preview: {-->
+<!--    key: FEATURES.PREVIEW,-->
+<!--    title: () =>  t('Preview'),-->
+<!--    action: () => {-->
+<!--      app.emitter.emit('vf-modal-show', {type:'preview', adapter:app.data.adapter, item: selectedItems.value[0]});-->
+<!--    },-->
+<!--  },-->
+<!--  open: {-->
+<!--    title: () =>  t('Open'),-->
+<!--    action: () => {-->
+<!--      app.emitter.emit('vf-search-exit');-->
+<!--      app.emitter.emit('vf-fetch', {params:{q: 'index', adapter: app.data.adapter, path:selectedItems.value[0].path}});-->
+<!--    },-->
+<!--  },-->
+<!--  openDir: {-->
+<!--    title: () =>  t('Open containing folder'),-->
+<!--    action: () => {-->
+<!--      app.emitter.emit('vf-search-exit');-->
+<!--      app.emitter.emit('vf-fetch', {params:{q: 'index', adapter: app.data.adapter, path: (selectedItems.value[0].dir)}});-->
+<!--    },-->
+<!--  },-->
+<!--  download: {-->
+<!--    key: FEATURES.DOWNLOAD,-->
+<!--    link: computed(() => app.requester.getDownloadUrl(app.data.adapter, selectedItems.value[0])),-->
+<!--    title: () =>  t('Download'),-->
+<!--    action: () => {-->
+<!--      const url = app.requester.getDownloadUrl(app.data.adapter, selectedItems.value[0]);-->
+<!--      app.emitter.emit('vf-download', url);-->
+<!--    },-->
+<!--  },-->
+<!--  archive: {-->
+<!--    key: FEATURES.ARCHIVE,-->
+<!--    title: () =>  t('Archive'),-->
+<!--    action: () => {-->
+<!--      app.emitter.emit('vf-modal-show', {type:'archive', items: selectedItems});-->
+<!--    },-->
+<!--  },-->
+<!--  unarchive: {-->
+<!--    key: FEATURES.UNARCHIVE,-->
+<!--    title: () => t('Unarchive'),-->
+<!--    action: () => {-->
+<!--      app.emitter.emit('vf-modal-show', {type:'unarchive', items: selectedItems});-->
+<!--    },-->
+<!--  },-->
+<!--  rename: {-->
+<!--    key: FEATURES.RENAME,-->
+<!--    title: () =>  t('Rename'),-->
+<!--    action: () => {-->
+<!--      app.emitter.emit('vf-modal-show', {type:'rename', items: selectedItems});-->
+<!--    },-->
+<!--  }-->
+<!--};-->
+
+<!--const run = (item) =>{-->
+<!--  app.emitter.emit('vf-contextmenu-hide');-->
+<!--  item.action();-->
+<!--};-->
+
+
+<!--app.emitter.on('vf-search-query', ({newQuery}) => {-->
+<!--  searchQuery.value = newQuery;-->
+<!--});-->
+
+<!--app.emitter.on('vf-contextmenu-show', ({event, area, items,  target = null}) => {-->
+<!--  context.items = [];-->
+
+<!--  if (searchQuery.value) {-->
+<!--    if (target) {-->
+<!--      context.items.push(menuItems.openDir);-->
+<!--      app.emitter.emit('vf-context-selected', [target]);-->
+<!--      // console.log('search item selected');-->
+<!--    } else {-->
+<!--      return;-->
+<!--    }-->
+<!--  } else if (!target && !searchQuery.value) {-->
+<!--    context.items.push(menuItems.refresh);-->
+<!--    context.items.push(menuItems.newfolder);-->
+<!--    app.emitter.emit('vf-context-selected', []);-->
+<!--    // console.log('no files selected');-->
+<!--  } else if (items.length > 1 && items.some(el => el.path === target.path)) {-->
+<!--    context.items.push(menuItems.refresh);-->
+<!--    context.items.push(menuItems.archive);-->
+<!--    context.items.push(menuItems.delete);-->
+<!--    app.emitter.emit('vf-context-selected', items);-->
+<!--    // console.log(items.length + ' selected (more than 1 item.)');-->
+<!--  } else {-->
+<!--    if (target.type == 'dir') {-->
+<!--      context.items.push(menuItems.open);-->
+<!--    } else {-->
+<!--      context.items.push(menuItems.preview);-->
+<!--      context.items.push(menuItems.download);-->
+<!--    }-->
+<!--    context.items.push(menuItems.rename);-->
+
+<!--    if (target.mime_type == 'application/zip') {-->
+<!--      context.items.push(menuItems.unarchive);-->
+<!--    } else {-->
+<!--      context.items.push(menuItems.archive);-->
+<!--    }-->
+<!--    context.items.push(menuItems.delete);-->
+<!--    app.emitter.emit('vf-context-selected', [target]);-->
+<!--    // console.log(target.type + ' is selected');-->
+<!--  }-->
+<!--  showContextMenu(event, area)-->
+<!--})-->
+
+<!--app.emitter.on('vf-contextmenu-hide', () => {-->
+<!--  context.active = false;-->
+<!--})-->
+
+<!--const showContextMenu = (event, area) => {-->
+<!--  context.active = true;-->
+
+<!--  nextTick(() => {-->
+<!--    const rootBbox = app.root.getBoundingClientRect();-->
+<!--    const areaContainer = area.getBoundingClientRect();-->
+
+<!--    let left = event.pageX - rootBbox.left;-->
+<!--    let top = event.pageY - rootBbox.top;-->
+<!--    let menuHeight = contextmenu.value.offsetHeight;-->
+<!--    let menuWidth = contextmenu.value.offsetWidth;-->
+
+<!--    left = (areaContainer.right - event.pageX + window.scrollX) < menuWidth ? left - menuWidth : left;-->
+<!--    top = (areaContainer.bottom - event.pageY + window.scrollY) < menuHeight ? top - menuHeight : top;-->
+
+<!--    context.positions = {-->
+<!--      left: left + 'px',-->
+<!--      top: top + 'px'-->
+<!--    };-->
+<!--  });-->
+<!--};-->
+
+<!--</script>-->

+ 428 - 0
src/components/FileManager/components/Explorer.vue

@@ -0,0 +1,428 @@
+<template>
+    <div class="relative flex-auto flex flex-col overflow-hidden">
+        <div v-if="ServiceContainer.view=='list' || searchQuery.length"
+             class="grid grid-cols-12 border-b border-neutral-300 border-gray-200 dark:border-gray-700 text-xs select-none">
+            <div @click="sortBy('basename')"
+                 class="col-span-7 py-1 leading-6 hover:bg-neutral-100 bg-neutral-50 dark:bg-gray-800 dark:hover:bg-gray-700/10 flex items-center pl-1">
+                {{ this.$t('Name') }}
+<!--                <v-f-sort-icon :direction="sort.order=='asc'? 'down': 'up'"-->
+<!--                               v-show="sort.active && sort.column=='basename'"/>-->
+            </div>
+            <div v-if="!searchQuery.length" @click="sortBy('file_size')"
+                 class="col-span-2 py-1 leading-6 hover:bg-neutral-100 bg-neutral-50 dark:bg-gray-800 dark:hover:bg-gray-700/10 flex items-center justify-center border-l border-r dark:border-gray-700">
+                {{ this.$t('Size') }}
+<!--                <v-f-sort-icon :direction="sort.order=='asc'? 'down': 'up'"-->
+<!--                               v-show="sort.active && sort.column=='file_size'"/>-->
+            </div>
+            <div v-if="!searchQuery.length" @click="sortBy('last_modified')"
+                 class="col-span-3 py-1 leading-6 hover:bg-neutral-100 bg-neutral-50 dark:bg-gray-800 dark:hover:bg-gray-700/10 flex items-center justify-center">
+                {{ this.$t('Date') }}
+<!--                <v-f-sort-icon :direction="sort.order=='asc'? 'down': 'up'"-->
+<!--                               v-show="sort.active && sort.column=='last_modified'"/>-->
+            </div>
+            <div v-if="searchQuery.length" @click="sortBy('path')"
+                 class="col-span-5 py-1 leading-6 hover:bg-neutral-100 bg-neutral-50 dark:bg-gray-800 dark:hover:bg-gray-700/10 flex items-center justify-center border-l dark:border-gray-700">
+                {{ this.$t('Filepath') }}
+<!--                <v-f-sort-icon :direction="sort.order=='asc'? 'down': 'up'"-->
+<!--                               v-show="sort.active && sort.column=='path'"/>-->
+            </div>
+        </div>
+
+        <div class="absolute">
+            <div ref="dragImage" class="absolute -z-50 -top-96">
+                <svg xmlns="http://www.w3.org/2000/svg"
+                     class="absolute h-6 w-6 md:h-12 md:w-12 m-auto stroke-neutral-500 fill-white dark:fill-gray-700 dark:stroke-gray-600 z-10"
+                     fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
+                    <path stroke-linecap="round" stroke-linejoin="round"
+                          d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>
+                </svg>
+                <div class="text-neutral-700 dark:text-neutral-300 p-1 absolute text-center top-4 right-[-2rem] md:top-5 md:right-[-2.4rem] z-20 text-xs">
+                    {{ selectedCount }}
+                </div>
+            </div>
+        </div>
+
+        <div
+                @touchstart="handleTouchStart"
+                @contextmenu.self.prevent="contextMenu($event,null)"
+                :class="ServiceContainer.fullScreen ? '' : 'resize-y'"
+                class="h-full w-full text-xs vf-selector-area vf-scrollbar min-h-[150px] overflow-auto p-1 z-0"
+                ref="selectorArea">
+
+            <div
+                    v-if="searchQuery.length"
+                    @dblclick="openItem(item)"
+                    @touchstart="delayedOpenItem($event)"
+                    @touchend="clearTimeOut()"
+                    @contextmenu.prevent="contextMenu($event,item)"
+                    :class="'vf-item-' + randId"
+                    class="grid grid-cols-1 border hover:bg-neutral-50 dark:hover:bg-gray-700 border-transparent my-0.5 w-full select-none"
+                    v-for="(item, index) in getItems()" :data-type="item.type" :data-item="JSON.stringify(item)"
+                    :data-index="index">
+                <div class="grid grid-cols-12 items-center">
+                    <div class="flex col-span-7 items-center">
+                        <svg v-if="item.path_type === 'Dir'" xmlns="http://www.w3.org/2000/svg"
+                             class="h-5 w-5 text-neutral-500 fill-sky-500 stroke-sky-500 dark:fill-slate-500 dark:stroke-slate-500"
+                             fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
+                            <path stroke-linecap="round" stroke-linejoin="round"
+                                  d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
+                        </svg>
+                        <svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-neutral-500" fill="none"
+                             viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
+                            <path stroke-linecap="round" stroke-linejoin="round"
+                                  d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>
+                        </svg>
+                        <span class="overflow-ellipsis overflow-hidden whitespace-nowrap">{{getFileName(item.href) }}</span>
+                    </div>
+                    <div class="col-span-5 overflow-ellipsis overflow-hidden whitespace-nowrap">{{ item.href }}</div>
+                </div>
+            </div>
+
+            <div draggable="true"
+                 v-if="ServiceContainer.view==='list' && !searchQuery.length"
+                 @dblclick="openItem(item)"
+                 @touchstart="delayedOpenItem($event)"
+                 @touchend="clearTimeOut()"
+                 @contextmenu.prevent="contextMenu($event,item)"
+                 @dragstart="handleDragStart($event,item)"
+                 @dragover="handleDragOver($event,item)"
+                 @drop="handleDropZone($event,item)"
+                 :class="'vf-item-' + randId"
+                 class="grid grid-cols-1 border hover:bg-neutral-50 dark:hover:bg-gray-700 border-transparent my-0.5 w-full  select-none"
+                 v-for="(item, index) in getItems()" :data-type="item.type" :data-item="JSON.stringify(item)"
+                 :data-index="index">
+                <div class="grid grid-cols-12 items-center">
+                    <div class="flex col-span-7 items-center">
+                        <svg v-if="item.path_type === 'Dir'" xmlns="http://www.w3.org/2000/svg"
+                             class="h-5 w-5 text-neutral-500 fill-sky-500 stroke-sky-500 dark:fill-slate-500 dark:stroke-slate-500"
+                             fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
+                            <path stroke-linecap="round" stroke-linejoin="round"
+                                  d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
+                        </svg>
+                        <svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-neutral-500" fill="none"
+                             viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
+                            <path stroke-linecap="round" stroke-linejoin="round"
+                                  d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>
+                        </svg>
+                        <span class="overflow-ellipsis overflow-hidden whitespace-nowrap">{{item.name }}</span>
+                    </div>
+                    <div class="col-span-2 text-center">{{ item.size ? ServiceContainer.filesize(item.size) : '' }}</div>
+                    <div class="col-span-3 overflow-ellipsis overflow-hidden whitespace-nowrap">{{
+                        datetimestring(item.mtime) }}
+                    </div>
+                </div>
+            </div>
+
+            <div draggable="true"
+                 v-if="ServiceContainer.view==='grid' && !searchQuery.length"
+                 @dblclick="openItem(item)"
+                 @touchstart="delayedOpenItem($event)"
+                 @touchend="clearTimeOut()"
+                 @contextmenu.prevent="contextMenu($event,item)"
+                 @dragstart="handleDragStart($event,item)"
+                 @dragover="handleDragOver($event,item)"
+                 @drop="handleDropZone($event,item)"
+                 :class="'vf-item-' + randId"
+                 class="border border-transparent hover:bg-neutral-50 m-1 dark:hover:bg-gray-700 inline-flex w-[5.5rem] h-20 md:w-24 text-center  justify-center select-none"
+                 v-for="(item, index) in getItems(false)" :data-type="item.type" :data-item="JSON.stringify(item)"
+                 :data-index="index">
+                <div>
+                    <div class="relative">
+                        <svg v-if="item.path_type === 'Dir'" xmlns="http://www.w3.org/2000/svg"
+                             class="h-10 w-10 md:h-12 md:w-12 m-auto fill-sky-500 stroke-sky-500 dark:fill-slate-500 dark:stroke-slate-500"
+                             fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
+                            <path stroke-linecap="round" stroke-linejoin="round"
+                                  d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
+                        </svg>
+                        <img class="lazy h-10 md:h-12 m-auto" v-else-if="(new Set(['jpg', 'jpeg', 'gif', 'png', 'webp', 'avif', 'svg']).has(item.ext))"
+                             :src="(ServiceContainer.requester.getPreviewUrl(null, item))" :alt="item.name">
+                        <video class="lazy h-10 md:h-12 m-auto"   v-else-if="((new Set(['mp4', 'webm', 'ogv','avi'])).has(item.ext))">
+                            <source :src="(ServiceContainer.requester.getPreviewUrl(null, item))"/>
+                        </video>
+                        <svg-icon icon-class="audio" class-name="h-10 w-10 md:h-12 md:w-12 m-auto text-neutral-500" v-else-if="((new Set(['mp3', 'm4a', 'ogg', 'weba', 'oga', 'flac', 'opus'])).has(item.ext))">
+
+                        </svg-icon>
+<!--                        <audio class="lazy h-10 md:h-12 m-auto"  v-else-if="((new Set(['mp3', 'm4a', 'ogg', 'weba', 'oga', 'flac', 'opus'])).has(item.ext))">-->
+<!--                            <source :src="(ServiceContainer.requester.getPreviewUrl(null, item))"/>-->
+<!--
+
+                                </audio>-->
+                        <svg-icon icon-class="pdf" class-name="h-10 w-10 md:h-12 md:w-12 m-auto text-neutral-500" v-else-if="((new Set(['pdf'])).has(item.ext))">
+
+                        </svg-icon>
+                        <svg-icon icon-class="zip-file" class-name="h-10 w-10 md:h-12 md:w-12 m-auto text-neutral-500" v-else-if="((new Set(['zip','rar','tar'])).has(item.ext))"/>
+                        <svg-icon icon-class="text-file" class-name="h-10 w-10 md:h-12 md:w-12 m-auto text-neutral-500" v-else-if="isTextFile(item)"/>
+                        <svg-icon icon-class="unknow-file" class-name="h-10 w-10 md:h-12 md:w-12 m-auto text-neutral-500" v-else>
+
+                        </svg-icon>
+<!--                        <svg v-else xmlns="http://www.w3.org/2000/svg"-->
+<!--                             class="h-10 w-10 md:h-12 md:w-12 m-auto text-neutral-500" fill="none" viewBox="0 0 24 24"-->
+<!--                             stroke="currentColor" stroke-width="1">-->
+<!--                            <path stroke-linecap="round" stroke-linejoin="round"-->
+<!--                                  d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>-->
+<!--                        </svg>-->
+<!--                        <div class="absolute hidden md:block top-1/2 w-full text-center text-neutral-500"-->
+<!--                             v-if="!(new Set(['jpg', 'jpeg', 'gif', 'png', 'webp', 'avif', 'svg','mp3', 'm4a', 'ogg', 'weba', 'oga', 'flac', 'opus','pdf','mp4', 'webm', 'ogv','avi','zip','rar','tar','txt','yml','md','yaml','html','js','css','xml']).has(item.ext))&&item.path_type != 'Dir'">{{-->
+<!--                            item.ext }}-->
+<!--                        </div>-->
+                    </div>
+                    <span class="break-all">{{ item.name }}</span>
+                </div>
+            </div>
+
+        </div>
+<!--        <v-f-toast/>-->
+    </div>
+</template>
+
+<script>
+    import {FEATURES} from "../features.js";
+    import datetimestring from '../utils/datetimestring.js'
+    import title_shorten from "../utils/title_shorten.js"
+    const randId = Math.floor(Math.random() * 2 ** 32);
+    import DragSelect from 'dragselect';
+    import Video from "./previews/Video";
+    import Audio from "./previews/Audio";
+    import {MODES} from "../filemaps";
+
+    export default {
+        name: "Explorer",
+        components: {Audio, Video},
+        inject: ['ServiceContainer'],
+        data() {
+            return {
+                FEATURES,
+                // selectorArea: null,
+                selectedCount: 0,
+                ds: null,
+                randId,
+                searchQuery: '',
+                dragAndDrop:true,
+                touchTimeOut:null,
+                sort:{active: false, column: '', order: ''}
+
+            }
+        },
+        mounted() {
+            this.$bus.$on('vf-fullscreen-toggle', () => {
+                this.$refs.selectorArea.style.height = null
+            })
+            this.$bus.$on('vf-search-query', ({newQuery}) => {
+                this.searchQuery = newQuery
+                if(newQuery){
+                    this.$bus.$emit('vf-fetch',{
+                        url:`${this.ServiceContainer.data.dirname==='/'?'':this.ServiceContainer.data.dirname}`,
+                        params: {
+                            json:'',
+                            q: newQuery,
+                            // adapter: app.data.adapter,
+                            // path: app.data.dirname,
+                            // filter: newQuery
+                        },
+                        onSuccess: (data) => {
+                            if (!data.paths.length) {
+                                this.$bus.$emit('vf-toast-push', {label: this.$t('No search result found.')});
+                            }
+                        }
+                    })
+                }else{
+                     this.$bus.$emit('vf-fetch', {url:`${this.ServiceContainer.data.dirname==='/'?'':this.ServiceContainer.data.dirname}`,params:{json:''}})
+                }
+            })
+            this.setDragSelect()
+        },
+        methods:{
+            handleTouchStart(event){
+                if (event.touches.length > 1) {
+                    if (!dragAndDrop) {
+                        this.ds.start();
+                        this.$bus.$emit('vf-toast-push', {label: t('Drag&Drop: on')});
+                        this.$bus.$emit('vf-explorer-update');
+                    } else {
+                        this.ds.stop();
+                        this.$bus.$emit('vf-toast-push', {label: t('Drag&Drop: off')});
+                    }
+                    this.dragAndDrop = !this.dragAndDrop;
+                }
+            },
+            delayedOpenItem($event){
+                this.touchTimeOut = setTimeout(() =>  {
+                    const cmEvent = new MouseEvent("contextmenu", {
+                        bubbles: true,
+                        cancelable: false,
+                        view: window,
+                        button: 2,
+                        buttons: 0,
+                        clientX: $event.target.getBoundingClientRect().x,
+                        clientY: $event.target.getBoundingClientRect().y
+                    });
+                    $event.target.dispatchEvent(cmEvent);
+
+                } ,500)
+            },
+            openItem(item){
+                if (item.path_type === 'Dir') {
+                    console.log(this.ServiceContainer.data.dirname)
+                    console.log('openitem',item)
+                    this.$bus.$emit('vf-search-exit');
+                    this.$bus.$emit('vf-fetch', {url:`${this.ServiceContainer.data.dirname==='/'?'':this.ServiceContainer.data.dirname}/${item.name}`,params:{json:''}});
+                } else {
+                    this.$bus.$emit('vf-modal-show', {type: 'preview', item});
+                }
+            },
+            getItems(sorted=true){
+                 if(!this.ServiceContainer.data.paths){
+                     return []
+                 }
+                let files = [...this.ServiceContainer.data.paths],
+                    column = this.sort.column,
+                    order = this.sort.order == 'asc' ? 1 : -1;
+
+                if (!sorted) {
+                    return files;
+                }
+
+                const compare = (a, b) => {
+                    if (typeof a === 'string' && typeof b === 'string') {
+                        return a.toLowerCase().localeCompare(b.toLowerCase());
+                    }
+                    if (a < b) return -1;
+                    if (a > b) return 1;
+                    return 0;
+                };
+
+                if (this.sort.active) {
+                    files = files.slice().sort((a, b) => compare(a[column], b[column]) * order);
+                }
+
+                return files;
+
+            },
+            sortBy(column){
+                if (this.sort.active && this.sort.column == column) {
+                    this.sort.active =  this.sort.order == 'asc'
+                    this.sort.column = column
+                    this.sort.order = 'desc'
+                } else {
+                    this.sort.active =  true
+                    this.sort.column = column
+                    this.sort.order = 'asc'
+                }
+            },
+            // getSelectedItems(){
+            //     console.log(this.ds)
+            //     return this.ds.getSelection().map(el=>JSON.parse(el.dataset.item))
+            // },
+            handleDragStart(e,item){
+                if (e.altKey || e.ctrlKey || e.metaKey) {
+                    e.preventDefault();
+                    return false;
+                }
+
+                e.dataTransfer.setDragImage(this.$refs.dragImage, 0, 15);
+                e.dataTransfer.effectAllowed = 'all';
+                e.dataTransfer.dropEffect = 'copy';
+                console.log('dragstart',this.getSelectedItems())
+                e.dataTransfer.setData('items', JSON.stringify(this.getSelectedItems()))
+            },
+            handleDropZone(e,item){
+                e.preventDefault();
+                console.log('dropzone',item)
+                let draggedItems = JSON.parse(e.dataTransfer.getData('items'));
+
+                if (draggedItems.find(item => item.storage !== app.adapter)) {
+                    alert('Moving items between different storages is not supported yet.');
+                    return;
+                }
+
+                this.$bus.$emit('vf-modal-show', {type:'move', items: {from: draggedItems, to: item}});
+            },
+            getSelectedItems(){
+                // console.log('ds',this.ds)
+                // console.log('ds-selection',this.ds.getSelection())
+               return  this.ds.getSelection().map((el) => JSON.parse(el.dataset.item))
+            },
+            handleDragOver(e,item){
+                 // console.log('dragOver')
+                e.preventDefault();
+                if (!item || item.path_type !== 'Dir' || this.ds.getSelection().find(el => el == e.currentTarget)) {
+                    e.dataTransfer.dropEffect = 'none';
+                    e.dataTransfer.effectAllowed = 'none';
+                } else {
+                    e.dataTransfer.dropEffect = 'copy';
+                }
+            },
+            setDragSelect(){
+                this.ds = new DragSelect({
+                    area: this.$refs.selectorArea,
+                    keyboardDrag: false,
+                    selectedClass: 'vf-explorer-selected',
+                    selectorClass: 'vf-explorer-selector',
+                });
+
+                this.$bus.$on('vf-explorer-update', () => this.$nextTick(() => {
+                    console.log('vf-explorer-update')
+                    this.ds.clearSelection();
+                    this.ds.setSettings({
+                        selectables: document.getElementsByClassName('vf-item-' + this.randId ),
+                    })
+                }));
+
+                this.ds.subscribe('predragstart', ({event, isDragging}) => {
+                    // apply custom drag event
+                    if (isDragging) {
+                        this.selectedCount = this.ds.getSelection().length
+                        this.ds.break();
+                    } else {
+                        // if resizing selectable area at the corner dont start selection.
+                        const offsetX = event.target.offsetWidth - event.offsetX;
+                        const offsetY = event.target.offsetHeight - event.offsetY;
+                        if (offsetX < 15 && offsetY < 15) {
+                            this.ds.clearSelection();
+                            this.ds.break();
+                        }
+                    }
+                });
+
+                this.ds.subscribe('predragmove', ({isDragging}) => {
+                    if (isDragging) {
+                        this.ds.break();
+                    }
+                });
+
+                this.ds.subscribe("callback", (	{ items, event, isDragging}) => {
+                    console.log('selectitems',this.getSelectedItems())
+                    this.$bus.$emit('vf-nodes-selected', this.getSelectedItems());
+                    this.selectedCount = this.ds.getSelection().length;
+                })
+            },
+            ext(item){return item?.substring(0, 3)},
+            title_shorten(item){
+                title_shorten(item)
+            },
+            datetimestring(str){
+                return datetimestring(str)
+            },
+            contextMenu($event,item){
+                this.$bus.$emit('vf-contextmenu-show', {event: $event, area: this.$refs.selectorArea, items: this.getSelectedItems(), target: item })
+            },
+            getFileName(name){
+                return name.slice(name.lastIndexOf('/')+1)
+            },
+            isTextFile(item){
+                const filename = item.name
+                const txtfile = MODES.some(p => p.extRe.test(filename))
+                return txtfile
+            }
+
+
+        }
+    }
+</script>
+
+<style scoped>
+    .flex-auto{
+        flex: 1 1 auto;
+    }
+
+</style>

+ 45 - 0
src/components/FileManager/components/Message.vue

@@ -0,0 +1,45 @@
+<template>
+    <div>
+        <div
+                v-if="!hidden"
+                ref="strMessage" class="flex mt-2 p-1 px-2 rounded text-sm break-all dark:opacity-75"
+                :class="error ? 'bg-red-100 text-red-600 ' : 'bg-emerald-100 text-emerald-600'">
+            <!--        :class="error ? 'bg-red-100 text-red-600 dark:bg-red-950' : 'bg-emerald-100 text-emerald-600 dark:bg-emerald-950'">-->
+            <slot></slot>
+            <div class="ml-auto cursor-pointer" @click="hide"
+                 :aria-label="this.$t('Close')" data-microtip-position="top-left" role="tooltip">
+                <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
+                    <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
+                </svg>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+    export default {
+        name: "Message",
+        inject: ['ServiceContainer'],
+        props:{
+            error: {
+                type: Boolean,
+                default: false
+            }
+        },
+        data(){
+            return{
+                hidden:false
+            }
+        },
+        methods:{
+            hide(){
+                this.$emit('hidden');
+                this.hidden = true;
+            }
+        }
+    }
+</script>
+
+<style scoped>
+
+</style>

+ 82 - 0
src/components/FileManager/components/Statusbar.vue

@@ -0,0 +1,82 @@
+<template>
+    <div class="p-1 text-xs border-t border-neutral-300 dark:border-gray-700/50 flex justify-between select-none">
+        <div class="flex leading-5 items-center">
+<!--            <div class="mx-2" :aria-label="t('Storage')" data-microtip-position="top-right" role="tooltip">-->
+<!--                <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"-->
+<!--                     stroke="currentColor" stroke-width="1">-->
+<!--                    <path stroke-linecap="round" stroke-linejoin="round"-->
+<!--                          d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/>-->
+<!--                </svg>-->
+<!--            </div>-->
+<!--            <select v-model="app.adapter" @change="handleStorageSelect"-->
+<!--                    class="py-0.5 text-sm text-slate-500 border dark:border-gray-600 dark:text-neutral-50 dark:bg-gray-700 rounded pl-2 pr-8">-->
+<!--                <option v-for="storage in app.data.storages" :value="storage">-->
+<!--                    {{ storage }}-->
+<!--                </option>-->
+<!--            </select>-->
+
+            <div class="ml-3">
+                <span v-if="searchQuery.length">{{ this.ServiceContainer.data.paths.length }} items found. </span>
+                <span class="ml-1">{{ selectedItemCount > 0 ? this.$t(selectedItemCount+' item(s) selected.') : '' }}</span>
+            </div>
+        </div>
+        <div class="flex leading-5 items-center justify-end">
+
+            <button class="vf-btn py-0 vf-btn-primary"
+                    :class="{disabled: !isSelectButtonActive}"
+                    :disabled="!isSelectButtonActive"
+                    v-if="this.ServiceContainer.selectButton.active" @click="ServiceContainer.selectButton.click(ServiceContainer.selectedItems, $event)">{{
+                this.$t("Select") }}
+            </button>
+
+            <span class="mr-1" :aria-label="this.$t('About')" data-microtip-position="top-left" role="tooltip"
+                  @click="this.$bus.$emit('vf-modal-show', {type:'about'})">
+        <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 stroke-slate-500 cursor-pointer" fill="none"
+             viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
+          <path stroke-linecap="round" stroke-linejoin="round"
+                d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
+        </svg>
+      </span>
+        </div>
+    </div>
+</template>
+
+<script>
+    import {FEATURES} from "../features.js";
+
+    export default {
+        name: "Statusbar",
+        inject: ['ServiceContainer'],
+        data() {
+            return {
+                FEATURES,
+                searchQuery: '',
+                selectedItemCount: 0
+            }
+        },
+        methods: {},
+        mounted() {
+            this.$bus.$on('vf-search-query', ({newQuery}) => {
+                this.searchQuery = newQuery
+            })
+            this.$bus.$on('vf-nodes-selected', (items) => {
+                this.selectedItemCount = items.length
+            })
+        },
+        computed: {
+            isSelectButtonActive() {
+                let selectionAllowed = this.ServiceContainer.selectButton.multiple ? this.ServiceContainer.selectedItems.length > 0 : this.ServiceContainer.selectedItems.length === 1;
+                const filters = this.ServiceContainer.selectButton.filters
+                // console.log(this.ServiceContainer.selectedItems.some(p=>!filters.includes(p.ext)))
+                if(this.ServiceContainer.selectedItems.length&&filters.length){
+                    selectionAllowed=selectionAllowed && !this.ServiceContainer.selectedItems.some(p=>!filters.includes(p.ext))
+                }
+                return this.ServiceContainer.selectButton.active && selectionAllowed;
+            }
+        }
+    }
+</script>
+
+<style scoped>
+
+</style>

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 251 - 0
src/components/FileManager/components/Toolbar.vue


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 156 - 0
src/components/FileManager/components/modals/ModalAbout.vue


+ 84 - 0
src/components/FileManager/components/modals/ModalArchive.vue

@@ -0,0 +1,84 @@
+<template>
+  <v-f-modal-layout>
+    <div class="sm:flex sm:items-start">
+      <div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-50 dark:bg-gray-500 sm:mx-0 sm:h-10 sm:w-10">
+        <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 stroke-blue-600 dark:stroke-blue-100" fill="none" viewBox="0 0 24 24" stroke="none" stroke-width="1.5">
+          <path stroke-linecap="round" stroke-linejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" />
+        </svg>
+      </div>
+      <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
+        <h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-400" id="modal-title">{{ t('Archive the files') }}</h3>
+        <div class="mt-2">
+          <div class="text-gray-500 text-sm mb-1 overflow-auto vf-scrollbar" style="max-height: 200px;">
+            <p v-for="item in items" class="flex text-sm text-gray-800 dark:text-gray-400">
+              <svg v-if="item.type === 'dir'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-neutral-500 fill-sky-500 stroke-sky-500 dark:fill-slate-500 dark:stroke-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
+              </svg>
+              <svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-neutral-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
+              </svg>
+              <span class="ml-1.5">{{ item.basename }}</span>
+            </p>
+          </div>
+          <input v-model="name" @keyup.enter="archive"
+                 class="my-1 px-2 py-1 border rounded  dark:bg-gray-700/25 dark:focus:ring-gray-600 dark:focus:border-gray-600 dark:text-gray-100 w-full"
+                 :placeholder="t('Archive name. (.zip file will be created)')" type="text">
+
+          <message v-if="message.length" @hidden="message=''" error>{{ message }}</message>
+        </div>
+      </div>
+    </div>
+
+    <template v-slot:buttons>
+      <button type="button" @click="archive" class="vf-btn vf-btn-primary">
+        {{ t('Archive') }}</button>
+      <button type="button" @click="app.emitter.emit('vf-modal-close')" class="vf-btn vf-btn-secondary">
+        {{ t('Cancel') }}</button>
+    </template>
+  </v-f-modal-layout>
+</template>
+
+<script>
+export default {
+  name: 'VFModalArchive'
+};
+</script>
+
+
+<script setup>
+import VFModalLayout from './ModalLayout.vue';
+import {inject, ref} from 'vue';
+import Message from '../Message.vue';
+const app = inject('ServiceContainer');
+const {getStore} = app.storage;
+const {t} = app.i18n;
+
+const name = ref('');
+const message = ref('');
+
+const items = ref(app.modal.data.items);
+
+const archive = () => {
+  if (items.value.length) {
+    app.emitter.emit('vf-fetch', {
+      params: {
+        q: 'archive',
+        m: 'post',
+        adapter: app.adapter,
+        path: app.data.dirname,
+      },
+      body: {
+        items: items.value.map(({path, type}) => ({path, type})),
+        name: name.value,
+      },
+      onSuccess: () => {
+        app.emitter.emit('vf-toast-push', {label: t('The file(s) archived.')});
+      },
+      onError: (e) => {
+        message.value = t(e.message);
+      }
+    });
+  }
+};
+
+</script>

+ 128 - 0
src/components/FileManager/components/modals/ModalDelete.vue

@@ -0,0 +1,128 @@
+<template>
+  <modal-layout>
+    <div class="sm:flex sm:items-start">
+      <div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-gray-500 sm:mx-0 sm:h-10 sm:w-10">
+        <svg xmlns="http://www.w3.org/2000/svg"
+                 class="h-6 w-6 stroke-red-600 dark:stroke-red-200" fill="none" viewBox="0 0 24 24" stroke="none" stroke-width="1.5">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
+          </svg>
+      </div>
+      <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
+        <h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-400" id="modal-title">{{ t('Delete files') }}</h3>
+        <div class="mt-2">
+          <p class="text-sm text-gray-500">{{ t('Are you sure you want to delete these files?') }}</p>
+          <div class="text-gray-500 text-sm mb-1 overflow-auto vf-scrollbar" style="max-height: 200px;">
+            <p v-for="item in items" class="flex text-sm text-gray-800 dark:text-gray-400">
+              <svg v-if="item.path_type === 'Dir'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-neutral-500 fill-sky-500 stroke-sky-500 dark:fill-slate-500 dark:stroke-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
+              </svg>
+              <svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-neutral-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
+              </svg>
+              <span class="ml-1.5">{{ item.name }}</span>
+            </p>
+          </div>
+          <message v-if="message.length" @hidden="message=''" error>{{ message }}</message>
+        </div>
+      </div>
+    </div>
+
+    <template v-slot:buttons>
+      <button type="button" @click="remove" class="vf-btn vf-btn-danger">
+        {{ t('Yes, Delete!') }}</button>
+      <button type="button" @click="close" class="vf-btn vf-btn-secondary">
+        {{ t('Cancel') }}</button>
+      <div class="m-auto font-bold text-red-500 text-sm dark:text-red-200 text-center">{{ t('This action cannot be undone.') }}</div>
+    </template>
+  </modal-layout>
+</template>
+
+<script>
+  import ModalLayout from "./ModalLayout";
+  import Message from "../Message";
+export default {
+  name: 'ModalDelete',
+  components: {ModalLayout,Message},
+  inject: ['ServiceContainer'],
+  data(){
+    return{
+      message:'',
+      items:this.ServiceContainer.modal.data.items,
+
+    }
+  },
+  methods:{
+   remove(){
+     if (this.items.length) {
+      this.items.forEach((item,index)=>{
+        this.$bus.$emit('vf-fetch', {
+          url:item.href,
+          method: 'DELETE',
+          params: null,
+          responseType:'text',
+          // body: {
+          //   items: this.items.map(({href, type}) => ({href, type})),
+          // },
+          onSuccess: () => {
+            if(index===this.items.length-1){
+              this.$bus.$emit('vf-toast-push', {label: this.$t('Files deleted.')});
+              this.$bus.$emit('vf-fetch', {url:`${this.ServiceContainer.data.dirname==='/'?'':this.ServiceContainer.data.dirname}`,params:{json:''}});
+            }
+
+          },
+          onError: (e) => {
+            this.message = this.$t(e.message);
+          }
+        })
+      })
+
+     }
+   },
+    t(str){
+     return this.$t(str)
+    },
+    close(){
+     console.log('delete',this.items)
+      this.$bus.$emit('vf-modal-close')
+    }
+  }
+};
+</script>
+
+
+<!--<script setup>-->
+<!--import VFModalLayout from './ModalLayout.vue';-->
+<!--import {inject, ref} from 'vue';-->
+<!--import Message from '../Message.vue';-->
+
+<!--const app = inject('ServiceContainer');-->
+<!--const {getStore} = app.storage;-->
+<!--const {t} = app.i18n;-->
+
+<!--const items = ref(app.modal.data.items);-->
+<!--const message = ref('');-->
+
+<!--const remove = () => {-->
+
+<!--  if (items.value.length) {-->
+<!--    app.emitter.emit('vf-fetch', {-->
+<!--      params: {-->
+<!--        q: 'delete',-->
+<!--        m: 'post',-->
+<!--        adapter: app.adapter,-->
+<!--        path: app.data.dirname,-->
+<!--      },-->
+<!--      body: {-->
+<!--        items: items.value.map(({path, type}) => ({path, type})),-->
+<!--      },-->
+<!--      onSuccess: () => {-->
+<!--        app.emitter.emit('vf-toast-push', {label: t('Files deleted.')});-->
+<!--      },-->
+<!--      onError: (e) => {-->
+<!--        message.value = t(e.message);-->
+<!--      }-->
+<!--    });-->
+<!--  }-->
+<!--};-->
+
+<!--</script>-->

+ 52 - 0
src/components/FileManager/components/modals/ModalLayout.vue

@@ -0,0 +1,52 @@
+<template>
+  <div class="v-f-modal relative z-30" aria-labelledby="modal-title" role="dialog" aria-modal="true"
+       @keyup.esc="closePreview" tabindex="0">
+    <div class="fixed inset-0 bg-gray-500 dark:bg-gray-600 dark:bg-opacity-75 bg-opacity-75 transition-opacity"></div>
+
+    <div class="fixed z-10 inset-0 overflow-hidden">
+      <div class="flex items-end sm:items-center justify-center min-h-full p-4 text-center sm:p-0" @mousedown.self="closePreview">
+        <div class="relative bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8  sm:max-w-4xl md:max-w-2xl lg:max-w-3xl xl:max-w-5xl w-full">
+          <div class="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
+
+            <slot />
+
+          </div>
+          <div class="bg-gray-50 dark:bg-gray-800 dark:border-t dark:border-gray-700 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
+            <slot name="buttons"/>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+
+  export default {
+    name: "ModalLayout",
+    inject: ['ServiceContainer'],
+    mounted() {
+      const inputElements = document.querySelector('.v-f-modal input')
+      if (inputElements) {
+        inputElements.focus();
+      }
+    },
+    methods:{
+      closePreview(){
+        this.$bus.$emit('vf-modal-close')
+      }
+    }
+  }
+
+// import {inject, onMounted} from 'vue';
+//
+// const app = inject('ServiceContainer')
+//
+// onMounted(() => {
+//   const inputElements = document.querySelector('.v-f-modal input')
+//   if (inputElements) {
+//     inputElements.focus();
+//   }
+// })
+</script>
+

+ 36 - 0
src/components/FileManager/components/modals/ModalMessage.vue

@@ -0,0 +1,36 @@
+<template>
+ <v-f-modal-layout>
+    <div class="sm:flex sm:items-start ">
+      <div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-50 dark:bg-gray-500 sm:mx-0 sm:h-10 sm:w-10">
+        <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 stroke-blue-600 dark:stroke-blue-100" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
+          <path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
+        </svg>
+      </div>
+      <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
+        <h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-400" id="modal-title">{{  app.modal.data?.title ?? 'Title' }}</h3>
+        <div class="mt-2">
+          <p class="text-sm text-gray-500">{{  app.modal.data?.message ?? 'Message' }}</p>
+        </div>
+      </div>
+    </div>
+
+    <template v-slot:buttons>
+      <button type="button" @click="app.emitter.emit('vf-modal-close')" class="vf-btn vf-btn-secondary">
+        {{ t('Close') }}</button>
+    </template>
+  </v-f-modal-layout>
+</template>
+
+<script>
+export default {
+  name: 'VFModalMessage'
+};
+</script>
+
+<script setup>
+import VFModalLayout from './ModalLayout.vue';
+import {inject} from 'vue';
+const app = inject('ServiceContainer');
+const {t} = app.i18n;
+
+</script>

+ 155 - 0
src/components/FileManager/components/modals/ModalMove.vue

@@ -0,0 +1,155 @@
+<template>
+  <ModalLayout>
+    <div class="sm:flex sm:items-start">
+      <div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-50 dark:bg-gray-500 sm:mx-0 sm:h-10 sm:w-10">
+        <svg class="h-6 w-6 stroke-blue-600 dark:stroke-blue-100" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
+             stroke-width="2" stroke="currentColor" aria-hidden="true">
+          <path stroke-linecap="round" stroke-linejoin="round"
+                d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
+        </svg>
+      </div>
+      <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full ">
+        <h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-400" id="modal-title">{{ t('Move files') }}</h3>
+        <p class="text-sm text-gray-500 pb-1">{{ t('Are you sure you want to move these files?') }}</p>
+        <div class="max-h-[200px] overflow-y-auto vf-scrollbar text-left">
+          <div v-for="node in items" class="flex text-sm text-gray-800 dark:text-gray-400">
+            <div>
+             <svg v-if="node.path_type === 'Dir'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-neutral-500 fill-sky-500 stroke-sky-500 dark:fill-slate-500 dark:stroke-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
+              </svg>
+              <svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-neutral-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
+              </svg>
+            </div>
+            <div class="ml-1.5">{{ node.name }}</div>
+          </div>
+        </div>
+          <h4 class="font-bold text-xs text-gray-700 dark:text-gray-500 mt-3 tracking-wider">{{ 'Target Directory'}}</h4>
+          <p class="flex text-sm text-gray-800 dark:text-gray-400 border dark:border-gray-700 p-1 rounded">
+             <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-neutral-500 fill-sky-500 stroke-sky-500 dark:fill-slate-500 dark:stroke-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
+              </svg>
+            <span class="ml-1.5 overflow-auto">{{ ServiceContainer.modal.data.items.to.href }}</span>
+          </p>
+<!--          <message v-if="message.length" @hidden="message=''" error>{{ message }}</message>-->
+      </div>
+    </div>
+
+
+    <template v-slot:buttons>
+      <button type="button" @click="move" class="vf-btn vf-btn-primary">
+        {{ t('Yes, Move!') }}</button>
+      <button type="button" @click="cancel" class="vf-btn vf-btn-secondary">
+        {{ t('Cancel') }}</button>
+
+      <div class="m-1 mr-auto font-bold text-gray-500 text-sm dark:text-gray-200 self-center">
+        {{ `${items.length} item(s) selected.`  }}
+      </div>
+    </template>
+  </ModalLayout>
+</template>
+
+
+
+<script>
+
+  import ModalLayout from "./ModalLayout";
+  export default {
+    name: "ModalMove",
+    components: {ModalLayout},
+    inject: ['ServiceContainer'],
+    data(){
+      return{
+        items:this.ServiceContainer.modal.data.items.from,
+        toItem:this.ServiceContainer.modal.data.items.to,
+        message:''
+      }
+    },
+    mounted() {
+      const inputElements = document.querySelector('.v-f-modal input')
+      if (inputElements) {
+        inputElements.focus();
+      }
+    },
+    methods:{
+      move(){
+         console.log('items',this.items)
+        if (this.items.length) {
+          this.items.forEach((item,index)=>{
+            this.$bus.$emit('vf-fetch', {
+              url:`${this.ServiceContainer.data.dirname==='/'?'':this.ServiceContainer.data.dirname}/${item.name}`,
+              params:null,
+              method:'MOVE',
+              // body: {
+              //   items: this.items.map(({path, type}) => ({path, type})),
+              //   item: this.ServiceContainer.modal.data.items.to.path
+              // },
+              headers:{'Destination':encodeURI(`${this.toItem.href}/${item.name}`)},
+              responseType:'text',
+              onSuccess: () => {
+                console.log('success')
+                // this.$bus.$emit('vf-toast-push', {label: this.$t('Files moved.', this.ServiceContainer.modal.data.items.to.name)});
+                if(index===this.items.length-1){
+                  this.$bus.$emit('vf-fetch', {url:`${this.ServiceContainer.data.dirname==='/'?'':this.ServiceContainer.data.dirname}`,params:{json:''}});
+                }
+
+              },
+              onError: (e) => {
+                this.message = this.$t(e.message);
+              }
+            });
+          })
+
+        }
+      },
+      cancel(){
+        console.log(this)
+        this.$bus.$emit('vf-modal-close')
+      },
+      t(item){
+        return this.$t(item)
+      }
+
+    }
+  }
+
+</script>
+
+
+<!--<script setup>-->
+<!--import VFModalLayout from './ModalLayout.vue';-->
+<!--import {inject, ref} from 'vue';-->
+<!--import Message from '../Message.vue';-->
+
+<!--const app = inject('ServiceContainer');-->
+<!--const {t} = app.i18n;-->
+<!--const {getStore} = app.storage;-->
+
+<!--const items = ref(app.modal.data.items.from);-->
+<!--const message = ref('');-->
+
+<!--const move = () => {-->
+
+<!--  if (items.value.length) {-->
+<!--    app.emitter.emit('vf-fetch', {-->
+<!--      params: {-->
+<!--        q: 'move',-->
+<!--        m: 'post',-->
+<!--        adapter: app.adapter,-->
+<!--        path: app.data.dirname,-->
+<!--      },-->
+<!--      body: {-->
+<!--        items: items.value.map(({path, type}) => ({path, type})),-->
+<!--        item: app.modal.data.items.to.path-->
+<!--      },-->
+<!--      onSuccess: () => {-->
+<!--        app.emitter.emit('vf-toast-push', {label: t('Files moved.', app.modal.data.items.to.name)});-->
+<!--      },-->
+<!--      onError: (e) => {-->
+<!--        message.value = t(e.message);-->
+<!--      }-->
+<!--    });-->
+<!--  }-->
+<!--};-->
+
+<!--</script>-->

+ 74 - 0
src/components/FileManager/components/modals/ModalNewFile.vue

@@ -0,0 +1,74 @@
+<template>
+  <v-f-modal-layout>
+    <div class="sm:flex sm:items-start">
+      <div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-50 dark:bg-gray-500 sm:mx-0 sm:h-10 sm:w-10">
+        <svg xmlns="http://www.w3.org/2000/svg"
+            class="h-6 w-6 stroke-blue-600 dark:stroke-blue-100" fill="none" viewBox="0 0 24 24" stroke="none" stroke-width="1.5">
+            <path stroke-linecap="round" stroke-linejoin="round" d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
+        </svg>
+      </div>
+      <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
+        <h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-400" id="modal-title">{{ t('New File') }}</h3>
+        <div class="mt-2">
+          <p class="text-sm text-gray-500">{{ t('Create a new file') }}</p>
+          <input v-model="name" @keyup.enter="createFile"
+                 class="px-2 py-1 border rounded dark:bg-gray-700/25 dark:focus:ring-gray-600 dark:focus:border-gray-600 dark:text-gray-100 w-full" :placeholder="t('File Name')" type="text">
+          <message v-if="message.length" @hidden="message=''" error>{{ message }}</message>
+
+        </div>
+      </div>
+    </div>
+
+    <template v-slot:buttons>
+      <button type="button" @click="createFile" class="vf-btn vf-btn-primary">
+        {{ t('Create') }}
+      </button>
+      <button type="button" @click="app.emitter.emit('vf-modal-close')" class="vf-btn vf-btn-secondary">
+        {{ t('Cancel') }}
+      </button>
+    </template>
+  </v-f-modal-layout>
+</template>
+
+<script>
+export default {
+  name: 'VFModalNewFile'
+};
+</script>
+
+
+<script setup>
+import VFModalLayout from './ModalLayout.vue';
+import {inject, ref} from 'vue';
+import Message from '../Message.vue';
+
+const app = inject('ServiceContainer');
+const {getStore} = app.storage;
+const {t} = app.i18n;
+
+const name = ref('');
+const message = ref('');
+
+const createFile = () => {
+  if (name.value != '') {
+    app.emitter.emit('vf-fetch', {
+      params: {
+        q: 'newfile',
+        m: 'post',
+        adapter: app.adapter,
+        path: app.data.dirname,
+      },
+      body: {
+        name: name.value
+      },
+      onSuccess: () => {
+        app.emitter.emit('vf-toast-push', {label: t('%s is created.', name.value)});
+      },
+      onError: (e) => {
+        message.value = t(e.message);
+      }
+    });
+  }
+};
+
+</script>

+ 118 - 0
src/components/FileManager/components/modals/ModalNewFolder.vue

@@ -0,0 +1,118 @@
+<template>
+  <modal-layout>
+    <div class="sm:flex sm:items-start">
+      <div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-50 dark:bg-gray-500 sm:mx-0 sm:h-10 sm:w-10">
+         <svg xmlns="http://www.w3.org/2000/svg"
+               class="h-6 w-6 stroke-blue-600 dark:stroke-blue-100" fill="none" viewBox="0 0 24 24" stroke="none" stroke-width="1.5">
+              <path stroke-linecap="round" stroke-linejoin="round" d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
+          </svg>
+      </div>
+      <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
+        <h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-400" id="modal-title">{{ t('New Folder') }}</h3>
+        <div class="mt-2">
+          <p class="text-sm text-gray-500">{{ t('Create a new folder') }}</p>
+          <input v-model="name" @keyup.enter="createFolder"
+                 class="px-2 py-1 border rounded dark:bg-gray-700/25 dark:focus:ring-gray-600 dark:focus:border-gray-600 dark:text-gray-100 w-full" :placeholder="t('Folder Name')" type="text">
+          <message v-if="message.length" @hidden="message=''" error>{{ message }}</message>
+        </div>
+      </div>
+    </div>
+
+    <template v-slot:buttons>
+      <button type="button" @click="createFolder" class="vf-btn vf-btn-primary">
+        {{ t('Create') }}</button>
+      <button type="button" @click="close" class="vf-btn vf-btn-secondary">
+        {{ t('Cancel') }}</button>
+    </template>
+  </modal-layout>
+</template>
+
+<script>
+  import Message from '../Message.vue';
+  import ModalLayout from "./ModalLayout";
+export default {
+  name: 'ModalNewFolder',
+  components:{
+    ModalLayout,
+    Message
+  },
+  inject: ['ServiceContainer'],
+  data(){
+    return{
+      name:'',
+      message:''
+    }
+  },
+  methods:{
+    t(str){
+      return this.$t(str)
+    },
+    close(){
+      this.$bus.$emit('vf-modal-close')
+    },
+    createFolder(){
+      if (this.name != '') {
+        this.$bus.$emit('vf-fetch', {
+          params:null,
+          method:'MKCOL',
+          responseType:'text',
+          url:`${this.ServiceContainer.modal.data.items.href}/${this.name}`,
+          // params: {
+          //   q: 'newfolder',
+          //   m: 'post',
+          //   adapter: app.adapter,
+          //   path: app.data.dirname,
+          // },
+          // body: {
+          //   name: name.value
+          // },
+          onSuccess: () => {
+            this.$bus.$emit('vf-toast-push', {label: this.t('%s is created.', name.value)});
+            this.$bus.$emit('vf-fetch', {url:`${this.ServiceContainer.data.dirname==='/'?'':this.ServiceContainer.data.dirname}`,params:{json:''}});
+          },
+          onError: (e) => {
+            message.value = t(e.message);
+          }
+        });
+      }
+    }
+  }
+};
+</script>
+
+
+<!--<script setup>-->
+<!--import VFModalLayout from './ModalLayout.vue';-->
+<!--import {inject, ref} from 'vue';-->
+
+
+<!--const app = inject('ServiceContainer');-->
+<!--const {getStore} = app.storage;-->
+<!--const {t} = app.i18n;-->
+
+<!--const name = ref('');-->
+<!--const message = ref('');-->
+
+<!--const createFolder = () => {-->
+<!--  if (name.value != '') {-->
+<!--    app.emitter.emit('vf-fetch', {-->
+<!--      params: {-->
+<!--        q: 'newfolder',-->
+<!--        m: 'post',-->
+<!--        adapter: app.adapter,-->
+<!--        path: app.data.dirname,-->
+<!--      },-->
+<!--      body: {-->
+<!--        name: name.value-->
+<!--      },-->
+<!--      onSuccess: () => {-->
+<!--        app.emitter.emit('vf-toast-push', {label: t('%s is created.', name.value)});-->
+<!--      },-->
+<!--      onError: (e) => {-->
+<!--        message.value = t(e.message);-->
+<!--      }-->
+<!--    });-->
+<!--  }-->
+<!--};-->
+
+<!--</script>-->

+ 196 - 0
src/components/FileManager/components/modals/ModalPreview.vue

@@ -0,0 +1,196 @@
+<template>
+    <modal-layout>
+        <div class="sm:flex sm:items-start">
+            <div class="mt-3 text-center sm:mt-0 sm:text-left w-full">
+                <div v-if="enabledPreview">
+                    <component :is="previewType"  @success="loaded = true"/>
+
+                    <!--                    <ImagePreview v-if="loadPreview(new Set(['jpg', 'jpeg', 'gif', 'png', 'webp', 'avif','svg']))" @success="loaded = true"/>-->
+                    <!--                    <Video v-else-if="loadPreview(new Set(['mp4', 'webm', 'ogv','avi']))" @success="loaded = true"/>-->
+                    <!--                    <Audio v-else-if="loadPreview(new Set(['mp3', 'm4a', 'ogg', 'weba', 'oga', 'flac', 'opus']))"-->
+                    <!--                           @success="loaded = true"/>-->
+                    <!--                    <Pdf v-else-if="loadPreview(new Set(['pdf']))" @success="loaded = true"/>-->
+                    <!--                  <TextPreview v-else-if="loadPreview" @success="loaded = true"/>-->
+                    <!--                              <Default  v-else @success="loaded = true"/>-->
+                </div>
+
+                <div class="text-gray-700 dark:text-gray-200 text-sm">
+                    <div class="flex leading-5" v-if="loaded === false">
+                        <svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg"
+                             fill="none" viewBox="0 0 24 24">
+                            <circle class="opacity-25 stroke-blue-900 dark:stroke-blue-100" cx="12" cy="12" r="10"
+                                    stroke="currentColor" stroke-width="4"></circle>
+                            <path class="opacity-75" fill="currentColor"
+                                  d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+                        </svg>
+                        <span>{{ t('Loading') }}</span>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <div class="py-2 flex font-normal break-all dark:text-gray-200 rounded text-xs">
+            <div><span class="font-bold">{{ t('File Size') }}: </span>{{
+                ServiceContainer.filesize(ServiceContainer.modal.data.item.size) }}
+            </div>
+            <div><span class="font-bold pl-2">{{ t('Last Modified') }}: </span> {{
+                datetimestring(ServiceContainer.modal.data.item.mtime) }}
+            </div>
+        </div>
+        <div class="text-xs text-gray-600 dark:text-gray-400"
+             v-if="ServiceContainer.features.includes(FEATURES.DOWNLOAD)">
+            <span>{{ t('Download doesn\'t work? You can try right-click "Download" button, select "Save link as...".') }}</span>
+        </div>
+
+        <template v-slot:buttons>
+            <button type="button" @click="close" class="vf-btn vf-btn-secondary">{{ t('Close') }}</button>
+            <button
+                    target="_blank"
+                    class="vf-btn vf-btn-primary"
+                    @click="download"
+                    href="void()"
+                    v-if="ServiceContainer.features.includes(FEATURES.DOWNLOAD)">{{ t('Download') }}
+            </button>
+        </template>
+    </modal-layout>
+</template>
+
+<script>
+    import ModalLayout from './ModalLayout.vue';
+    import TextPreview from '../previews/Text.vue';
+    import ImagePreview from '../previews/Image.vue';
+    import Default from '../previews/Default.vue';
+    import VideoPreview from '../previews/Video.vue';
+    import AudioPreview from '../previews/Audio.vue';
+    import Pdf from '../previews/Pdf.vue';
+    import datetimestring from '../../utils/datetimestring.js';
+    import {FEATURES} from "../../features.js";
+    import {MODES} from "../../filemaps";
+
+    export default {
+        name: 'ModalPreview',
+        inject: ['ServiceContainer'],
+        components: {
+            ModalLayout,
+            AudioPreview,
+            Pdf,
+            ImagePreview,
+            VideoPreview,
+            Default,
+            TextPreview
+        },
+        data() {
+            return {
+                enabledPreview: this.ServiceContainer.features.includes(FEATURES.PREVIEW),
+                loaded: false,
+                FEATURES,
+                MODES
+            }
+        },
+        computed: {
+            previewType() {
+                const filename = this.ServiceContainer.modal.data.item.name
+                const ext = this.ServiceContainer.modal.data.item.ext
+                const txtfile = MODES.some(p => p.extRe.test(filename))
+                console.log('txtfile', txtfile, filename)
+                if (txtfile) {
+                    return 'text-preview'
+                } else if (new Set(['jpg', 'jpeg', 'gif', 'png', 'webp', 'avif', 'svg']).has(ext)) {
+                    return 'image-preview'
+                } else if (new Set(['mp4', 'webm', 'ogv', 'avi']).has(ext)) {
+                    return 'video-preview'
+                } else if (new Set(['mp3', 'm4a', 'ogg', 'weba', 'oga', 'flac', 'opus']).has(ext)) {
+                    return 'audio-preview'
+                } else if (new Set(['pdf']).has(ext)) {
+                    return 'pdf'
+                } else {
+                    return 'default'
+                }
+            }
+        },
+        methods: {
+            // loadPreview(type) {
+            //     console.log('type', type)
+            //     if (!type) {
+            //         const filename = this.ServiceContainer.modal.data.item.name
+            //         //const modesMatch =this.MODES.filter(p=>p.extRe.test(filename))
+            //         // let modeMatch = false
+            //         // MODES.forEach(item => {
+            //         //     if (item.extRe.test(filename)) {
+            //         //         console.log('modem', item)
+            //         //         modeMatch = item
+            //         //         return
+            //         //     }
+            //         // })
+            //
+            //         const txtfile = MODES.some(p => p.extRe.test(filename))
+            //         console.log('txtfile', txtfile, filename, type)
+            //         if (txtfile) {
+            //             return true
+            //         } else {
+            //             return false
+            //         }
+            //     } else {
+            //         return type.has(this.ServiceContainer.modal.data.item.ext ?? '')
+            //     }
+            // },
+            download() {
+                const url = this.ServiceContainer.requester.getDownloadUrl(this.ServiceContainer.modal.data.adapter, this.ServiceContainer.modal.data.item);
+                this.$bus.$emit('vf-download', url);
+            },
+            datetimestring(date) {
+                return datetimestring(date)
+            },
+            t(str) {
+                return this.$t(str)
+            },
+            close() {
+                this.$bus.$emit('vf-modal-close')
+            }
+        },
+        mounted() {
+            const enabledPreview = this.ServiceContainer.features.includes(FEATURES.PREVIEW)
+            if (!enabledPreview) {
+                this.loaded = true
+            }
+          // const filename = this.ServiceContainer.modal.data.item.name
+          // console.log('filename',filename)
+          // const modes = [...MODES].findIndex(p => p.extRe.test(filename))
+          // if(modes>0){
+          //   this.mode=MODES[modes].mode
+          // }else{
+          //    this.mode='ace/mode/text'
+          // }
+          //   console.log('mode',modes, this.mode,[...MODES].some(p => RegExp(p.extRe).test(filename)))
+        }
+
+    };
+</script>
+
+<!--<script setup>-->
+<!--import {inject, ref} from 'vue';-->
+<!--import VFModalLayout from './ModalLayout.vue';-->
+<!--import Text from '../previews/Text.vue';-->
+<!--import Image from '../previews/Image.vue';-->
+<!--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 datetimestring from '../../utils/datetimestring.js';-->
+<!--import {FEATURES} from "../features.js";-->
+
+<!--const app = inject('ServiceContainer')-->
+<!--const {t} = app.i18n-->
+<!--const loaded = ref(false);-->
+<!--const loadPreview = (type) => (app.modal.data.item.mime_type ?? '').startsWith(type)-->
+
+<!--const download = () => {-->
+<!--  const url = app.requester.getDownloadUrl(app.modal.data.adapter, app.modal.data.item);-->
+<!--  app.emitter.emit('vf-download', url);-->
+<!--}-->
+
+<!--const enabledPreview = app.features.includes(FEATURES.PREVIEW)-->
+<!--if (!enabledPreview) {-->
+<!--  loaded.value = true;-->
+<!--}-->
+<!--</script>-->

+ 130 - 0
src/components/FileManager/components/modals/ModalRename.vue

@@ -0,0 +1,130 @@
+<template>
+  <modal-layout>
+    <div class="sm:flex sm:items-start">
+      <div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-50 dark:bg-gray-500 sm:mx-0 sm:h-10 sm:w-10">
+
+        <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 stroke-blue-600 dark:stroke-blue-100" fill="none" viewBox="0 0 24 24" stroke="none" stroke-width="1.5">
+          <path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
+        </svg>
+      </div>
+      <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
+        <h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-400" id="modal-title">{{ t('Rename') }}</h3>
+        <div class="mt-2">
+          <p class="flex text-sm text-gray-800 dark:text-gray-400 py-2">
+            <svg v-if="item.path_type === 'Dir'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-neutral-500 fill-sky-500 stroke-sky-500 dark:fill-slate-500 dark:stroke-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
+              </svg>
+              <svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-neutral-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
+              </svg>
+            <span class="ml-1.5">{{ item.name }}</span>
+          </p>
+          <input v-model="name" @keyup.enter="rename"
+                 class="px-2 py-1 border rounded  dark:bg-gray-700/25 dark:focus:ring-gray-600 dark:focus:border-gray-600 dark:text-gray-100 w-full" placeholder="Name" type="text">
+          <message v-if="message.length" @hidden="message=''" error>{{ message }}</message>
+        </div>
+      </div>
+    </div>
+
+    <template v-slot:buttons>
+      <button type="button" @click="rename" class="vf-btn vf-btn-primary">{{ t('Rename') }}</button>
+      <button type="button" @click="close" class="vf-btn vf-btn-secondary">{{ t('Cancel') }}</button>
+    </template>
+  </modal-layout>
+</template>
+
+<script>
+  import Message from '../Message.vue';
+  import ModalLayout from "./ModalLayout";
+export default {
+  name: 'ModalRename',
+  components:{
+    ModalLayout,
+    Message
+  },
+  inject: ['ServiceContainer'],
+  data(){
+    return{
+      name:'',
+      message:'',
+      item:this.ServiceContainer.modal.data.items
+    }
+  },
+  methods:{
+    t(str){
+      return this.$t(str)
+    },
+    close(){
+      this.$bus.$emit('vf-modal-close')
+    },
+    rename(){
+      if (this.name != '') {
+        this.$bus.$emit('vf-fetch', {
+          url:`${this.ServiceContainer.modal.data.items.href}`,
+          params:null,
+          method:'MOVE',
+          // params: {
+          //   q: 'rename',
+          //   m: 'post',
+          //   adapter: app.adapter,
+          //   path: app.data.dirname,
+          // },
+          // body: {
+          //   item: item.value.path,
+          //   name: name.value
+          // },
+          headers:{'Destination':encodeURI(`${this.ServiceContainer.modal.data.items.href.replace(this.ServiceContainer.modal.data.items.name,this.name)}`)},
+          responseType:'text',
+          onSuccess: () => {
+            this.$bus.$emit('vf-toast-push', {label: this.$t('%s is renamed.', name.value)});
+            this.$bus.$emit('vf-fetch', {url:`${this.ServiceContainer.data.dirname==='/'?'':this.ServiceContainer.data.dirname}`,params:{json:''}});
+          },
+          onError: (e) => {
+            this.message = this.$t(e.message);
+          }
+        });
+      }
+    }
+  }
+
+};
+</script>
+
+
+<!--<script setup>-->
+<!--import VFModalLayout from './ModalLayout.vue';-->
+<!--import {inject, ref} from 'vue';-->
+<!--import Message from '../Message.vue';-->
+
+<!--const app = inject('ServiceContainer');-->
+<!--const {getStore} = app.storage;-->
+<!--const {t} = app.i18n;-->
+
+<!--const item = ref(app.modal.data.items[0]);-->
+<!--const name = ref(app.modal.data.items[0].basename);-->
+<!--const message = ref('');-->
+
+<!--const rename = () => {-->
+<!--  if (name.value != '') {-->
+<!--    app.emitter.emit('vf-fetch', {-->
+<!--      params: {-->
+<!--        q: 'rename',-->
+<!--        m: 'post',-->
+<!--        adapter: app.adapter,-->
+<!--        path: app.data.dirname,-->
+<!--      },-->
+<!--      body: {-->
+<!--        item: item.value.path,-->
+<!--        name: name.value-->
+<!--      },-->
+<!--      onSuccess: () => {-->
+<!--        app.emitter.emit('vf-toast-push', {label: t('%s is renamed.', name.value)});-->
+<!--      },-->
+<!--      onError: (e) => {-->
+<!--        message.value = t(e.message);-->
+<!--      }-->
+<!--    });-->
+<!--  }-->
+<!--};-->
+
+<!--</script>-->

+ 78 - 0
src/components/FileManager/components/modals/ModalUnarchive.vue

@@ -0,0 +1,78 @@
+<template>
+  <v-f-modal-layout>
+    <div class="sm:flex sm:items-start">
+      <div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-50 dark:bg-gray-500 sm:mx-0 sm:h-10 sm:w-10">
+        <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 stroke-blue-600 dark:stroke-blue-100" fill="none" viewBox="0 0 24 24" stroke="none" stroke-width="1.5">
+          <path stroke-linecap="round" stroke-linejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5m6 4.125l2.25 2.25m0 0l2.25 2.25M12 13.875l2.25-2.25M12 13.875l-2.25 2.25M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" />
+        </svg>
+      </div>
+      <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
+        <h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-400" id="modal-title">{{ t('Unarchive') }}</h3>
+        <div class="mt-2">
+          <p v-for="item in items" class="flex text-sm text-gray-800 dark:text-gray-400">
+            <svg v-if="item.type === 'dir'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-neutral-500 fill-sky-500 stroke-sky-500 dark:fill-slate-500 dark:stroke-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
+              </svg>
+              <svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-neutral-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
+                <path stroke-linecap="round" stroke-linejoin="round" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
+              </svg>
+            <span class="ml-1.5">{{ item.basename }}</span>
+          </p>
+          <p class="my-1 text-sm text-gray-500">{{ t('The archive will be unarchived at')}} ({{current.dirname}})</p>
+
+          <message v-if="message.length" @hidden="message=''" error>{{ message }}</message>
+        </div>
+      </div>
+    </div>
+
+    <template v-slot:buttons>
+      <button type="button" @click="unarchive" class="vf-btn vf-btn-primary">{{ t('Unarchive') }}</button>
+      <button type="button" @click="app.emitter.emit('vf-modal-close')" class="vf-btn vf-btn-secondary">{{ t('Cancel') }}</button>
+    </template>
+  </v-f-modal-layout>
+</template>
+
+<script>
+export default {
+  name: 'VFModalUnarchive'
+};
+</script>
+
+
+<script setup>
+import VFModalLayout from './ModalLayout.vue';
+import {inject, ref} from 'vue';
+import Message from '../Message.vue';
+
+const app = inject('ServiceContainer');
+const {getStore} = app.storage;
+const {t} = app.i18n;
+
+const name = ref('');
+const item = ref(app.modal.data.items[0]);
+const message = ref('');
+
+// todo: get zip folder content
+const items = ref([]);
+
+const unarchive = () => {
+  app.emitter.emit('vf-fetch', {
+    params: {
+      q: 'unarchive',
+      m: 'post',
+      adapter: app.adapter,
+      path: app.data.dirname,
+    },
+    body: {
+      item: item.value.path
+    },
+    onSuccess: () => {
+      app.emitter.emit('vf-toast-push', {label: t('The file unarchived.')});
+    },
+    onError: (e) => {
+      message.value = t(e.message);
+    }
+  });
+};
+
+</script>

+ 837 - 0
src/components/FileManager/components/modals/ModalUpload.vue

@@ -0,0 +1,837 @@
+<template>
+  <ModalLayout>
+    <div class="sm:flex sm:items-start">
+      <div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-50 dark:bg-gray-500 sm:mx-0 sm:h-10 sm:w-10">
+         <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 stroke-blue-600 dark:stroke-blue-100" fill="none" viewBox="0 0 24 24" stroke="none" stroke-width="1.5">
+            <path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
+          </svg>
+      </div>
+      <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
+        <h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-400" id="modal-title">{{ t('Upload Files') }}</h3>
+        <div class="mt-2">
+          <div
+            ref="dropArea"
+            class="flex items-center justify-center text-lg mb-4 text-gray-500 border-2 border-gray-300 rounded border-dashed select-none cursor-pointer
+              dark:border-gray-600 h-[120px]"
+            @click="openFileSelector">
+            <div class="pointer-events-none" v-if="hasFilesInDropArea">
+              {{ t('Release to drop these files.') }}
+            </div>
+            <div class="pointer-events-none" v-else>
+              <!--
+              We can use uppy's localization here..
+              {{ uppy.i18n('dropPasteFiles', {browseFiles: uppy.i18n('browseFiles')}) }}
+              -->
+              {{ t('Drag and drop the files/folders to here or click here.') }}
+            </div>
+          </div>
+          <div ref="container" class="text-gray-500 mb-1">
+            <button ref="pickFiles" type="button" class="vf-btn vf-btn-secondary">
+              {{ t('Select Files') }}
+            </button>
+            <button ref="pickFolders" type="button" class="vf-btn vf-btn-secondary">
+              {{ t('Select Folders') }}
+            </button>
+            <button type="button" class="vf-btn vf-btn-secondary" :disabled="uploading" @click="clear(false)">
+              {{ t('Clear all') }}
+            </button>
+            <button type="button" class="vf-btn vf-btn-secondary" :disabled="uploading" @click="clear(true)">
+              {{ t('Clear only successful') }}
+            </button>
+          </div>
+          <div class="text-gray-500 text-sm mb-1 pr-1 max-h-[200px] overflow-y-auto vf-scrollbar">
+            <div class="flex   hover:bg-gray-100 dark:hover:bg-gray-800 dark:hover:text-gray-300" :key="entry.id" v-for="entry in queue">
+              <span class="rounded flex flex-shrink-0 w-6 h-6 border bg-gray-50 text-xs cursor-default
+                  dark:border-gray-700 dark:bg-gray-800 dark:text-gray-50
+                  ">
+                <span class="text-base m-auto"
+                      :class="getClassNameForEntry(entry)"
+                      v-text="getIconForEntry(entry)"></span>
+              </span>
+              <div class="ml-1 w-full h-fit">
+                <div class="text-left hidden md:block">{{ title_shorten(entry.name, 40) }} ({{ entry.size }})</div>
+                <div class="text-left md:hidden">{{ title_shorten(entry.name, 16) }} ({{ entry.size }})</div>
+                <div class="flex break-all text-left" :class="getClassNameForEntry(entry)"> {{ entry.statusName }}
+                  <b class="ml-auto" v-if="entry.status === definitions.UPLOADING">{{ entry.percent }}<span class="ml-3">{{ entry.speed }}</span></b>
+                </div>
+              </div>
+              <button
+                type="button"
+                class="rounded w-5 h-5 border-1 text-base leading-none font-medium
+                  focus:outline-none dark:border-gray-200 dark:text-gray-400 dark:hover:text-gray-200 dark:bg-gray-600
+                    ml-auto sm:text-xs hover:text-red-600"
+                :class="uploading ? 'disabled:bg-gray-100 text-white text-opacity-50' : 'bg-gray-100'"
+                :title="t('Delete')"
+                :disabled="uploading"
+                @click="remove(entry)">
+                <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path></svg>
+              </button>
+            </div>
+
+            <div class="py-2" v-if="!queue.length">{{ t('No files selected!') }}</div>
+          </div>
+<!-- No need this ? -->
+<!--          <message v-if="message.length" @hidden="message=''" error>{{ message }}</message>-->
+        </div>
+      </div>
+    </div>
+    <input ref="internalFileInput" type="file" multiple class="hidden">
+    <!--suppress HtmlUnknownAttribute -->
+    <input ref="internalFolderInput" type="file" multiple webkitdirectory class="hidden">
+
+    <template v-slot:buttons>
+      <button type="button" class="vf-btn vf-btn-primary" :disabled="uploading" @click.prevent="upload"
+        :class="uploading ? 'bg-blue-200 hover:bg-blue-200 dark:bg-gray-700/50 dark:hover:bg-gray-700/50 dark:text-gray-500' : 'bg-blue-600 hover:bg-blue-700 dark:bg-gray-700 dark:hover:bg-gray-500'">
+        {{ t('Upload') }}
+      </button>
+      <button type="button" class="vf-btn vf-btn-secondary" v-if="uploading" @click.prevent="cancel">{{ t('Cancel') }}</button>
+      <button type="button" class="vf-btn vf-btn-secondary" v-else @click.prevent="close">{{ t('Close') }}</button>
+    </template>
+  </ModalLayout>
+</template>
+
+<script>
+  import ModalLayout from "./ModalLayout";
+  import Uppy from '@uppy/core';
+  import XHR from '@uppy/xhr-upload';
+  import { parse } from '../../utils/filesize.js';
+  import title_shorten from "../../utils/title_shorten.js";
+ const QUEUE_ENTRY_STATUS = {
+   PENDING: 0,
+   CANCELED: 1,
+   UPLOADING: 2,
+   ERROR: 3,
+  DONE: 10,
+ }
+export default {
+  name: 'ModalUpload',
+  components: {ModalLayout},
+  inject: ['ServiceContainer'],
+  data(){
+    return{
+      definitions: {
+        PENDING: 0,
+        CANCELED: 1,
+        UPLOADING: 2,
+        ERROR: 3,
+        DONE: 10,
+      },
+      queue:[],
+      uploading:false,
+      hasFilesInDropArea:false,
+      uppy:null,
+      message:''
+
+    }
+  },
+  methods:{
+    t(item){
+      return this.$t(item)
+    },
+    title_shorten(arg1,arg2){
+      return title_shorten(arg1,arg2)
+    },
+    findQueueEntryIndexById(id) {
+      return this.queue.findIndex((item) => item.id === id);
+    },
+    addFile(file, name = null) {
+    name = name != null ? name : (file.webkitRelativePath || file.name);
+    this.uppy.addFile({
+      name,
+      type: file.type,
+      data: file,
+      source: 'Local',
+    });
+  },
+    getClassNameForEntry(entry) {
+      switch (entry.status) {
+        case QUEUE_ENTRY_STATUS.DONE:
+          return 'text-green-600'
+        case QUEUE_ENTRY_STATUS.ERROR:
+          return 'text-red-600';
+        case QUEUE_ENTRY_STATUS.CANCELED:
+          return 'text-red-600';
+        case QUEUE_ENTRY_STATUS.PENDING:
+        default:
+          return '';
+      }
+    },
+    getIconForEntry(entry){
+      switch (entry.status) {
+        case QUEUE_ENTRY_STATUS.DONE:
+          return '✓'
+        case QUEUE_ENTRY_STATUS.ERROR:
+        case QUEUE_ENTRY_STATUS.CANCELED:
+          return '!';
+        case QUEUE_ENTRY_STATUS.PENDING:
+        default:
+          return '...';
+      }
+    },
+    openFileSelector() {
+      this.$refs.pickFiles.click();
+    },
+    upload(){
+    if (this.uploading) {
+      return;
+    }
+    if (!this.queue.filter(entry => entry.status !== QUEUE_ENTRY_STATUS.DONE).length) {
+      this.message = this.t('Please select file to upload first.');
+      return;
+    }
+    this.message = '';
+    this.uppy.retryAll();
+    this.uppy.upload();
+  },
+    cancel() {
+      this.uppy.cancelAll({ reason: 'user' });
+      this.queue.forEach(entry => {
+        if (entry.status !== QUEUE_ENTRY_STATUS.DONE) {
+          entry.status = QUEUE_ENTRY_STATUS.CANCELED;
+          entry.statusName = this.t('Canceled');
+        }
+      });
+      this.uploading = false;
+    },
+    remove(file) {
+      if (this.uploading) {
+        return;
+      }
+      this.uppy.removeFile(file.id, 'removed-by-user');
+      this.queue.splice(this.findQueueEntryIndexById(file.id), 1);
+    },
+    clear(onlySuccessful) {
+    if (this.uploading) {
+      return;
+    }
+    this.uppy.cancelAll({ reason: 'user' });
+    if (onlySuccessful) {
+      const retryQueue = [];
+      this.queue.forEach(entry => {
+        if (entry.status !== QUEUE_ENTRY_STATUS.DONE) {
+          retryQueue.push(entry);
+        }
+      });
+      this.queue = [];
+      retryQueue.forEach(entry => {
+        this.addFile(entry.originalFile, entry.name);
+      });
+      return;
+    }
+    this.queue.splice(0);
+  },
+    close() {
+    this.$bus.$emit('vf-modal-close');
+  },
+
+  buildReqParams() {
+    console.log('modalData',this.ServiceContainer.modal.data)
+    const url =this.ServiceContainer.modal.data.items.href
+    return this.ServiceContainer.requester.transformRequestParams({
+      url: url,
+      method: 'PUT',
+      params: { },
+    });
+  }
+  },
+  mounted() {
+    const _this=this
+    this.uppy = new Uppy({
+      debug: this.ServiceContainer.debug,
+      restrictions: {
+        maxFileSize: parse(_this.ServiceContainer.maxFileSize),
+        //maxNumberOfFiles
+        //allowedFileTypes
+      },
+      locale: _this.$t('uppy'),
+      onBeforeFileAdded(file, files) {
+        console.log('beforeFileAdd',this)
+        const duplicated = files[file.id] != null;
+        if (duplicated) {
+          const i = this.findQueueEntryIndexById(file.id);
+          if (_this.queue[i].status === QUEUE_ENTRY_STATUS.PENDING) {
+            // Undocumented, as long as uppy don't change this we are good.
+            _this.message = _this.uppy.i18n('noDuplicates', { fileName: file.name });
+          }
+          _this.queue = _this.queue.filter(entry => entry.id !== file.id);
+        }
+        // We only push the file in the end of queue, so user just need scroll down to find the newly selected stuff.
+        _this.queue.push({
+          id: file.id,
+          name: file.name,
+          size: _this.ServiceContainer.filesize(file.size),
+          status: QUEUE_ENTRY_STATUS.PENDING,
+          statusName: _this.t('Pending upload'),
+          percent: null,
+          stime:null,
+          sloaded:0,
+          speed:null,
+          originalFile: file.data,
+        });
+        return true;
+        // Uppy would only upload that file once even you call .addFile() twice in one row, nice.
+      }
+    });
+
+    this.uppy.use(XHR, {
+      endpoint: 'WILL_BE_REPLACED_BEFORE_UPLOAD',
+      limit: 5,
+      timeout: 0,
+      getResponseError(responseText, _response) {
+        /** @type {String} */
+        let message;
+        try {
+          /** @type {*} */
+          const body = JSON.parse(responseText);
+          message = body.message;
+        } catch (e) {
+          message = _this.t('Cannot parse server response.');
+        }
+        return new Error(message);
+      },
+    });
+    this.uppy.on('restriction-failed', (upFile, error) => {
+      //remove the restricted file.
+      const entry = _this.queue[_this.findQueueEntryIndexById(upFile.id)];
+      remove(entry)
+      message.value = error.message;
+    });
+    this.uppy.on('upload', (data) => {
+      const reqParams = this.buildReqParams();
+      const xhrPlugin = this.uppy.getPlugin('XHRUpload');
+      xhrPlugin.opts.method = reqParams.method;
+      xhrPlugin.opts.formData=false
+      xhrPlugin.opts.headers = reqParams.headers;
+      if(data.fileIDs.length>0){
+        data.fileIDs.forEach((id)=>{
+          const entry = _this.queue[_this.findQueueEntryIndexById(id)];
+          //开始上传时间
+          entry.stime=new Date().getTime()
+          entry.sloaded=0
+          console.log('reqParams',entry)
+          this.uppy.setMeta({ body:entry.originalFile });
+          console.log(entry.name)
+
+          _this.uppy.setFileState(id, {
+            xhrUpload: { endpoint: reqParams.url+'/' +entry.name },
+          });
+          // xhrPlugin.opts.endpoint = ;
+          console.log(xhrPlugin.opts.endpoint)
+        })
+      }
+      _this.uploading = true;
+      _this.queue.forEach(file => {
+        if (file.status === QUEUE_ENTRY_STATUS.DONE) {
+          return;
+        }
+        file.percent = null;
+        file.status = QUEUE_ENTRY_STATUS.UPLOADING;
+        file.statusName = _this.t('Pending upload');
+      });
+
+    });
+    this.uppy.on('upload-progress', (upFile, progress) => {
+      // upFile.progress.percentage never updates itself during this callback, and progress param definition showed
+      // some non exist properties, weird.
+      const item =_this.queue[_this.findQueueEntryIndexById(upFile.id)]
+      const p = Math.floor(progress.bytesUploaded / progress.bytesTotal * 100);
+      let endtime = new Date().getTime();
+      if(endtime-item.stime>1000){
+      const speed = ((progress.bytesUploaded-item.sloaded) / 1024 / 1024).toFixed(2); // 转换为MB/s
+      console.log(`上传速度:${speed} MB/s`);
+
+      item.stime=new Date().getTime()
+        item.speed=`${speed} MB/s`
+      }
+      item.sloaded=progress.bytesUploaded
+      _this.queue[_this.findQueueEntryIndexById(upFile.id)].percent = `${p}%`;
+    });
+    this.uppy.on('upload-success',(upFile) => {
+      const entry = _this.queue[_this.findQueueEntryIndexById(upFile.id)];
+      entry.status = QUEUE_ENTRY_STATUS.DONE;
+      entry.statusName = _this.t('Done');
+    });
+    this.uppy.on('upload-error', (upFile, error) => {
+      const entry = _this.queue[_this.findQueueEntryIndexById(upFile.id)];
+      entry.percent = null;
+      entry.status = QUEUE_ENTRY_STATUS.ERROR;
+      // https://uppy.io/docs/uppy/#upload-error
+      // noinspection JSUnresolvedReference
+      if (error.isNetworkError) {
+        entry.statusName = _this.t(`Network Error, Unable establish connection to the server or interrupted.`);
+      } else {
+        entry.statusName = error ? error.message : _this.t('Unknown Error');
+      }
+    });
+    this.uppy.on('error', (error) => {
+      this.message = error.message;
+      this.uploading = false;
+     this.$bus.$emit('vf-fetch', {
+        params: { q: 'index' },
+        noCloseModal: true,
+      });
+    })
+    this.uppy.on('complete', () => {
+      const url =this.ServiceContainer.modal.data.items.href
+      this.uploading = false;
+      this.$bus.$emit('vf-fetch', {
+        url:url,
+        params: { json:''},
+        noCloseModal: true,
+      });
+    });
+
+    this.$refs.pickFiles.addEventListener('click', () => {
+      this.$refs.internalFileInput.click();
+    })
+    this.$refs.pickFolders.addEventListener('click', () => {
+      this.$refs.internalFolderInput.click();
+    });
+    this.$refs.dropArea.addEventListener('dragover', ev => {
+      ev.preventDefault();
+      this.$refs.hasFilesInDropArea = true;
+    });
+    this.$refs.dropArea.addEventListener('dragleave', ev => {
+      ev.preventDefault();
+      this.$refs.hasFilesInDropArea = false;
+    });
+    /**
+     * @callback ResultCallback
+     * @param {FileSystemEntry|FileSystemDirectoryEntry|FileSystemFileEntry} fileSystemEntry
+     * @param {File} file
+     */
+    /**
+     * Iterate through all dirs & files, will invoke resultCallback multiple times when read file
+     * @param {ResultCallback} resultCallback
+     * @param {FileSystemEntry|FileSystemDirectoryEntry|FileSystemFileEntry} item
+     */
+    function scanFiles(resultCallback, item) {
+      if (item.isFile) {
+        item.file(f => resultCallback(item, f));
+      }
+      if (item.isDirectory) {
+        item.createReader().readEntries((entries) => {
+          entries.forEach((entry) => {
+            scanFiles(resultCallback, entry);
+          });
+        });
+      }
+    }
+    this.$refs.dropArea.addEventListener('drop', ev => {
+      ev.preventDefault();
+      this.hasFilesInDropArea = false;
+      const trimFileName = /^[/\\](.+)/;
+      [...ev.dataTransfer.items].forEach(item => {
+        if (item.kind === "file") {
+          scanFiles((entry, file) => {
+            const matched = trimFileName.exec(entry.fullPath);
+            this.addFile(file, matched[1]);
+          }, item.webkitGetAsEntry());
+        }
+      });
+    });
+
+    /**
+     * File <input> change handler
+     * @param {Event} event
+     * @param {HTMLInputElement} event.target
+     */
+    const onFileInputChange = ({ target }) => {
+      const files = target.files;
+      for (const file of files) {
+        this.addFile(file);
+      }
+      target.value = '';
+    };
+    this.$refs.internalFileInput.addEventListener('change', onFileInputChange);
+    this.$refs.internalFolderInput.addEventListener('change', onFileInputChange);
+  }
+};
+</script>
+
+<!--<script setup>-->
+<!--import Uppy from '@uppy/core';-->
+<!--import XHR from '@uppy/xhr-upload';-->
+<!--import VFModalLayout from './ModalLayout.vue';-->
+<!--import { inject, onBeforeUnmount, onMounted, ref } from 'vue';-->
+<!--import Message from '../Message.vue';-->
+<!--import { parse } from '../../utils/filesize.js';-->
+<!--import title_shorten from "../../utils/title_shorten.js";-->
+
+<!--const app = inject('ServiceContainer');-->
+<!--const {t} = app.i18n;-->
+
+<!--const uppyLocale = t("uppy");-->
+
+<!--const QUEUE_ENTRY_STATUS = {-->
+<!--  PENDING: 0,-->
+<!--  CANCELED: 1,-->
+<!--  UPLOADING: 2,-->
+<!--  ERROR: 3,-->
+<!--  DONE: 10,-->
+<!--}-->
+<!--const definitions = ref({ QUEUE_ENTRY_STATUS })-->
+
+<!--/** @type {import('vue').Ref<HTMLDivElement>} */-->
+<!--const container = ref(null);-->
+<!--/** @type {import('vue').Ref<HTMLInputElement>} */-->
+<!--const internalFileInput = ref(null);-->
+<!--/** @type {import('vue').Ref<HTMLInputElement>} */-->
+<!--const internalFolderInput = ref(null);-->
+<!--/** @type {import('vue').Ref<HTMLButtonElement>} */-->
+<!--const pickFiles = ref(null);-->
+<!--/** @type {import('vue').Ref<HTMLButtonElement>} */-->
+<!--const pickFolders = ref(null);-->
+<!--/** @type {import('vue').Ref<HTMLDivElement>} */-->
+<!--const dropArea = ref(null);-->
+<!--/**-->
+<!-- * @typedef {Object} QueueEntry-->
+<!-- * @property {String} id-->
+<!-- * @property {String} name File name-->
+<!-- * @property {String} size Formatted size-->
+<!-- * @property {?String} percent 0 to 100 progress value with "%" suffix-->
+<!-- * @property {Number} status Status, See QUEUE_ENTRY_STATUS-->
+<!-- * @property {String} statusName Status name-->
+<!-- * @property {File} originalFile-->
+<!-- */-->
+<!--/** @type {import('vue').Ref<QueueEntry[]>} */-->
+<!--const queue = ref([]);-->
+<!--const message = ref('');-->
+<!--const uploading = ref(false);-->
+<!--const hasFilesInDropArea = ref(false);-->
+
+<!--/**-->
+<!-- * Uploader instance-->
+<!-- * @type {?Uppy}-->
+<!-- */-->
+<!--let uppy;-->
+
+<!--/**-->
+<!-- * Find queue entry index by id-->
+<!-- *-->
+<!-- * <p>Yes calling this function is slow, but nobody is gonna use our stuff to upload over 100k files.</p>-->
+<!-- * @param {String} id-->
+<!-- * @return number index in queue-->
+<!-- */-->
+<!--function findQueueEntryIndexById(id) {-->
+<!--  return queue.value.findIndex((item) => item.id === id);-->
+<!--}-->
+
+<!--/**-->
+<!-- * Add file to uppy-->
+<!-- * @param {File} file-->
+<!-- * @param {?String} name file name with full relative path (like "dirA/dirB/file.xlsx" or just "file.xlsx")-->
+<!-- */-->
+<!--function addFile(file, name = null) {-->
+<!--  name = name != null ? name : (file.webkitRelativePath || file.name);-->
+<!--  uppy.addFile({-->
+<!--    name,-->
+<!--    type: file.type,-->
+<!--    data: file,-->
+<!--    source: 'Local',-->
+<!--  });-->
+<!--}-->
+
+<!--/**-->
+<!-- * Get dom class for entry-->
+<!-- * @param {QueueEntry} entry-->
+<!-- */-->
+<!--function getClassNameForEntry(entry) {-->
+<!--  switch (entry.status) {-->
+<!--    case QUEUE_ENTRY_STATUS.DONE:-->
+<!--      return 'text-green-600'-->
+<!--    case QUEUE_ENTRY_STATUS.ERROR:-->
+<!--      return 'text-red-600';-->
+<!--    case QUEUE_ENTRY_STATUS.CANCELED:-->
+<!--      return 'text-red-600';-->
+<!--    case QUEUE_ENTRY_STATUS.PENDING:-->
+<!--    default:-->
+<!--      return '';-->
+<!--  }-->
+<!--}-->
+
+<!--const getIconForEntry = (entry) =>{-->
+<!--  switch (entry.status) {-->
+<!--    case QUEUE_ENTRY_STATUS.DONE:-->
+<!--      return '✓'-->
+<!--    case QUEUE_ENTRY_STATUS.ERROR:-->
+<!--    case QUEUE_ENTRY_STATUS.CANCELED:-->
+<!--      return '!';-->
+<!--    case QUEUE_ENTRY_STATUS.PENDING:-->
+<!--    default:-->
+<!--      return '...';-->
+<!--  }-->
+<!--}-->
+
+<!--/**-->
+<!-- * Open file selector-->
+<!-- */-->
+<!--function openFileSelector() {-->
+<!--  pickFiles.value.click();-->
+<!--}-->
+
+<!--/**-->
+<!-- * Begin upload-->
+<!-- */-->
+<!--function upload() {-->
+<!--  if (uploading.value) {-->
+<!--    return;-->
+<!--  }-->
+<!--  if (!queue.value.filter(entry => entry.status !== QUEUE_ENTRY_STATUS.DONE).length) {-->
+<!--    message.value = t('Please select file to upload first.');-->
+<!--    return;-->
+<!--  }-->
+<!--  message.value = '';-->
+<!--  uppy.retryAll();-->
+<!--  uppy.upload();-->
+<!--}-->
+
+<!--/**-->
+<!-- * Cancel upload-->
+<!-- */-->
+<!--function cancel() {-->
+<!--  uppy.cancelAll({ reason: 'user' });-->
+<!--  queue.value.forEach(entry => {-->
+<!--    if (entry.status !== QUEUE_ENTRY_STATUS.DONE) {-->
+<!--      entry.status = QUEUE_ENTRY_STATUS.CANCELED;-->
+<!--      entry.statusName = t('Canceled');-->
+<!--    }-->
+<!--  });-->
+<!--  uploading.value = false;-->
+<!--}-->
+
+<!--/**-->
+<!-- * Remove a file from queue-->
+<!-- * @param {QueueEntry} file-->
+<!-- */-->
+<!--function remove(file) {-->
+<!--  if (uploading.value) {-->
+<!--    return;-->
+<!--  }-->
+<!--  uppy.removeFile(file.id, 'removed-by-user');-->
+<!--  queue.value.splice(findQueueEntryIndexById(file.id), 1);-->
+<!--}-->
+
+<!--/**-->
+<!-- * Clear queue-->
+<!-- * @param {boolean} onlySuccessful-->
+<!-- */-->
+<!--function clear(onlySuccessful) {-->
+<!--  if (uploading.value) {-->
+<!--    return;-->
+<!--  }-->
+<!--  uppy.cancelAll({ reason: 'user' });-->
+<!--  if (onlySuccessful) {-->
+<!--    const retryQueue = [];-->
+<!--    queue.value.forEach(entry => {-->
+<!--      if (entry.status !== QUEUE_ENTRY_STATUS.DONE) {-->
+<!--        retryQueue.push(entry);-->
+<!--      }-->
+<!--    });-->
+<!--    queue.value = [];-->
+<!--    retryQueue.forEach(entry => {-->
+<!--      addFile(entry.originalFile, entry.name);-->
+<!--    });-->
+<!--    return;-->
+<!--  }-->
+<!--  queue.value.splice(0);-->
+<!--}-->
+
+<!--/**-->
+<!-- * Close upload modal-->
+<!-- */-->
+<!--function close() {-->
+<!--  app.emitter.emit('vf-modal-close');-->
+<!--}-->
+
+<!--function buildReqParams() {-->
+<!--  return app.requester.transformRequestParams({-->
+<!--    url: '',-->
+<!--    method: 'post',-->
+<!--    params: { q: 'upload', adapter: app.data.adapter, path: app.data.dirname },-->
+<!--  });-->
+<!--}-->
+
+<!--onMounted(async () => {-->
+<!--  uppy = new Uppy({-->
+<!--    debug: app.debug,-->
+<!--    restrictions: {-->
+<!--      maxFileSize: parse(app.maxFileSize),-->
+<!--      //maxNumberOfFiles-->
+<!--      //allowedFileTypes-->
+<!--    },-->
+<!--    locale: uppyLocale,-->
+<!--    onBeforeFileAdded(file, files) {-->
+<!--      const duplicated = files[file.id] != null;-->
+<!--      if (duplicated) {-->
+<!--        const i = findQueueEntryIndexById(file.id);-->
+<!--        if (queue.value[i].status === QUEUE_ENTRY_STATUS.PENDING) {-->
+<!--          // Undocumented, as long as uppy don't change this we are good.-->
+<!--          message.value = uppy.i18n('noDuplicates', { fileName: file.name });-->
+<!--        }-->
+<!--        queue.value = queue.value.filter(entry => entry.id !== file.id);-->
+<!--      }-->
+<!--      // We only push the file in the end of queue, so user just need scroll down to find the newly selected stuff.-->
+<!--      queue.value.push({-->
+<!--        id: file.id,-->
+<!--        name: file.name,-->
+<!--        size: app.filesize(file.size),-->
+<!--        status: QUEUE_ENTRY_STATUS.PENDING,-->
+<!--        statusName: t('Pending upload'),-->
+<!--        percent: null,-->
+<!--        originalFile: file.data,-->
+<!--      });-->
+<!--      return true;-->
+<!--      // Uppy would only upload that file once even you call .addFile() twice in one row, nice.-->
+<!--    }-->
+<!--  });-->
+
+<!--  uppy.use(XHR, {-->
+<!--    endpoint: 'WILL_BE_REPLACED_BEFORE_UPLOAD',-->
+<!--    limit: 5,-->
+<!--    timeout: 0,-->
+<!--    getResponseError(responseText, _response) {-->
+<!--      /** @type {String} */-->
+<!--      let message;-->
+<!--      try {-->
+<!--        /** @type {*} */-->
+<!--        const body = JSON.parse(responseText);-->
+<!--        message = body.message;-->
+<!--      } catch (e) {-->
+<!--        message = t('Cannot parse server response.');-->
+<!--      }-->
+<!--      return new Error(message);-->
+<!--    },-->
+<!--  });-->
+<!--  uppy.on('restriction-failed', (upFile, error) => {-->
+<!--    //remove the restricted file.-->
+<!--    const entry = queue.value[findQueueEntryIndexById(upFile.id)];-->
+<!--    remove(entry)-->
+<!--    message.value = error.message;-->
+<!--  });-->
+<!--  uppy.on('upload', () => {-->
+<!--    const reqParams = buildReqParams();-->
+<!--    uppy.setMeta({ ...reqParams.body });-->
+<!--    const xhrPlugin = uppy.getPlugin('XHRUpload');-->
+<!--    xhrPlugin.opts.method = reqParams.method;-->
+<!--    xhrPlugin.opts.endpoint = reqParams.url + '?' + new URLSearchParams(reqParams.params);-->
+<!--    xhrPlugin.opts.headers = reqParams.headers;-->
+<!--    uploading.value = true;-->
+<!--    queue.value.forEach(file => {-->
+<!--      if (file.status === QUEUE_ENTRY_STATUS.DONE) {-->
+<!--        return;-->
+<!--      }-->
+<!--      file.percent = null;-->
+<!--      file.status = QUEUE_ENTRY_STATUS.UPLOADING;-->
+<!--      file.statusName = t('Pending upload');-->
+<!--    });-->
+<!--  });-->
+<!--  uppy.on('upload-progress', (upFile, progress) => {-->
+<!--    // upFile.progress.percentage never updates itself during this callback, and progress param definition showed-->
+<!--    // some non exist properties, weird.-->
+<!--    const p = Math.floor(progress.bytesUploaded / progress.bytesTotal * 100);-->
+<!--    queue.value[findQueueEntryIndexById(upFile.id)].percent = `${p}%`;-->
+<!--  });-->
+<!--  uppy.on('upload-success',(upFile) => {-->
+<!--    const entry = queue.value[findQueueEntryIndexById(upFile.id)];-->
+<!--    entry.status = QUEUE_ENTRY_STATUS.DONE;-->
+<!--    entry.statusName = t('Done');-->
+<!--  });-->
+<!--  uppy.on('upload-error', (upFile, error) => {-->
+<!--    const entry = queue.value[findQueueEntryIndexById(upFile.id)];-->
+<!--    entry.percent = null;-->
+<!--    entry.status = QUEUE_ENTRY_STATUS.ERROR;-->
+<!--    // https://uppy.io/docs/uppy/#upload-error-->
+<!--    // noinspection JSUnresolvedReference-->
+<!--    if (error.isNetworkError) {-->
+<!--      entry.statusName = t(`Network Error, Unable establish connection to the server or interrupted.`);-->
+<!--    } else {-->
+<!--      entry.statusName = error ? error.message : t('Unknown Error');-->
+<!--    }-->
+<!--  });-->
+<!--  uppy.on('error', (error) => {-->
+<!--    message.value = error.message;-->
+<!--    uploading.value = false;-->
+<!--    app.emitter.emit('vf-fetch', {-->
+<!--      params: { q: 'index', adapter: app.data.adapter, path: app.data.dirname },-->
+<!--      noCloseModal: true,-->
+<!--    });-->
+<!--  })-->
+<!--  uppy.on('complete', () => {-->
+<!--    uploading.value = false;-->
+<!--    app.emitter.emit('vf-fetch', {-->
+<!--      params: { q: 'index', adapter: app.data.adapter, path: app.data.dirname },-->
+<!--      noCloseModal: true,-->
+<!--    });-->
+<!--  });-->
+
+<!--  pickFiles.value.addEventListener('click', () => {-->
+<!--    internalFileInput.value.click();-->
+<!--  })-->
+<!--  pickFolders.value.addEventListener('click', () => {-->
+<!--    internalFolderInput.value.click();-->
+<!--  });-->
+<!--  dropArea.value.addEventListener('dragover', ev => {-->
+<!--    ev.preventDefault();-->
+<!--    hasFilesInDropArea.value = true;-->
+<!--  });-->
+<!--  dropArea.value.addEventListener('dragleave', ev => {-->
+<!--    ev.preventDefault();-->
+<!--    hasFilesInDropArea.value = false;-->
+<!--  });-->
+<!--  /**-->
+<!--   * @callback ResultCallback-->
+<!--   * @param {FileSystemEntry|FileSystemDirectoryEntry|FileSystemFileEntry} fileSystemEntry-->
+<!--   * @param {File} file-->
+<!--   */-->
+<!--  /**-->
+<!--   * Iterate through all dirs & files, will invoke resultCallback multiple times when read file-->
+<!--   * @param {ResultCallback} resultCallback-->
+<!--   * @param {FileSystemEntry|FileSystemDirectoryEntry|FileSystemFileEntry} item-->
+<!--   */-->
+<!--  function scanFiles(resultCallback, item) {-->
+<!--    if (item.isFile) {-->
+<!--      item.file(f => resultCallback(item, f));-->
+<!--    }-->
+<!--    if (item.isDirectory) {-->
+<!--      item.createReader().readEntries((entries) => {-->
+<!--        entries.forEach((entry) => {-->
+<!--          scanFiles(resultCallback, entry);-->
+<!--        });-->
+<!--      });-->
+<!--    }-->
+<!--  }-->
+<!--  dropArea.value.addEventListener('drop', ev => {-->
+<!--    ev.preventDefault();-->
+<!--    hasFilesInDropArea.value = false;-->
+<!--    const trimFileName = /^[/\\](.+)/;-->
+<!--    [...ev.dataTransfer.items].forEach(item => {-->
+<!--      if (item.kind === "file") {-->
+<!--        scanFiles((entry, file) => {-->
+<!--          const matched = trimFileName.exec(entry.fullPath);-->
+<!--          addFile(file, matched[1]);-->
+<!--        }, item.webkitGetAsEntry());-->
+<!--      }-->
+<!--    });-->
+<!--  });-->
+
+<!--  /**-->
+<!--   * File <input> change handler-->
+<!--   * @param {Event} event-->
+<!--   * @param {HTMLInputElement} event.target-->
+<!--   */-->
+<!--  const onFileInputChange = ({ target }) => {-->
+<!--    const files = target.files;-->
+<!--    for (const file of files) {-->
+<!--      addFile(file);-->
+<!--    }-->
+<!--    target.value = '';-->
+<!--  };-->
+<!--  internalFileInput.value.addEventListener('change', onFileInputChange);-->
+<!--  internalFolderInput.value.addEventListener('change', onFileInputChange);-->
+<!--});-->
+
+<!--onBeforeUnmount(() => {-->
+<!--  uppy?.close({ reason: 'unmount' });-->
+<!--});-->
+<!--</script>-->

+ 51 - 0
src/components/FileManager/components/previews/Audio.vue

@@ -0,0 +1,51 @@
+<template>
+    <div>
+  <h3 class="mb-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-400" id="modal-title"
+         :aria-label="this.ServiceContainer.modal.data.item.path" data-microtip-position="bottom-right" role="tooltip">{{ this.ServiceContainer.modal.data.item.name }}</h3>
+  <div>
+      <audio class="w-full" controls>
+          <source :src="getAudioUrl()" type="audio/mpeg">
+          Your browser does not support the audio element.
+      </audio>
+  </div>
+    </div>
+</template>
+
+<script>
+    export default {
+        name:'Audio',
+        inject: ['ServiceContainer'],
+        data(){
+            return{
+
+            }
+        },
+        methods:{
+            getAudioUrl(){
+                return this.ServiceContainer.requester.getPreviewUrl(this.ServiceContainer.modal.data.adapter, this.ServiceContainer.modal.data.item)
+            }
+        },
+        mounted() {
+            this.$emit('success')
+        }
+
+    }
+</script>
+
+<!--<script setup>-->
+
+<!--import {inject, onMounted} from 'vue';-->
+
+<!--const emit = defineEmits(['success']);-->
+
+<!--const app = inject('ServiceContainer');-->
+
+<!--const getAudioUrl = () => {-->
+<!--  return app.requester.getPreviewUrl(app.modal.data.adapter, app.modal.data.item)-->
+<!--}-->
+
+<!--onMounted(() => {-->
+<!--  emit('success');-->
+<!--});-->
+
+<!--</script>-->

+ 33 - 0
src/components/FileManager/components/previews/Default.vue

@@ -0,0 +1,33 @@
+<template>
+  <div class="flex">
+    <h3 class="mb-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-400" id="modal-title"
+         :aria-label="ServiceContainer.modal.data.item.name" data-microtip-position="bottom-right" role="tooltip">{{ ServiceContainer.modal.data.item.name }}</h3>
+  </div>
+</template>
+
+
+<script>
+  export  default {
+    name:'Default',
+    inject: ['ServiceContainer'],
+    data(){
+      return{}
+    },
+    mounted() {
+      this.$emit('success')
+    }
+  }
+</script>
+
+<!--<script setup>-->
+
+<!--import {onMounted, inject} from 'vue';-->
+
+<!--const app = inject('ServiceContainer');-->
+
+<!--const emit = defineEmits(['success']);-->
+
+<!--onMounted(() => {-->
+<!--  emit('success');-->
+<!--});-->
+<!--</script>-->

+ 168 - 0
src/components/FileManager/components/previews/Image.vue

@@ -0,0 +1,168 @@
+<template>
+  <div>
+  <div class="flex">
+    <h3 class="mb-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-400" id="modal-title"
+         :aria-label="ServiceContainer.modal.data.item.href" data-microtip-position="bottom-right" role="tooltip">{{ ServiceContainer.modal.data.item.name }}</h3>
+    <div class="ml-auto mb-2">
+      <button @click="crop" class="ml-1 px-2 py-1 rounded border border-transparent shadow-sm bg-blue-700/75 hover:bg-blue-700 dark:bg-gray-700 dark:hover:bg-gray-700/50  text-base font-medium text-white sm:ml-3 sm:w-auto sm:text-sm" v-if="showEdit">
+        {{ t('Crop') }}</button>
+      <button class="ml-1 px-2 py-1  text-blue-500" @click="editMode()" v-if="ServiceContainer.features.includes(FEATURES.EDIT)">{{ showEdit ? t('Cancel'): t('Edit') }}</button>
+    </div>
+  </div>
+
+  <div class="w-full flex justify-center">
+    <img ref="image" class="max-w-[50vh] max-h-[50vh]" :src="ServiceContainer.requester.getPreviewUrl(ServiceContainer.modal.data.adapter, ServiceContainer.modal.data.item)" alt="">
+  </div>
+
+  <message v-if="message.length" @hidden="message=''" :error="isError">{{ message }}</message>
+  </div>
+</template>
+<script>
+  import 'cropperjs/dist/cropper.css';
+  import Cropper from 'cropperjs';
+  import {FEATURES} from "../../features.js";
+  import Message from '../Message.vue'
+  export default {
+name:'ImagePreview',
+    inject: ['ServiceContainer'],
+    components:{
+  Message
+    },
+    data(){
+  return{
+    cropper:null,
+    showEdit:false,
+    message:'',
+    isError:false,
+    FEATURES
+  }
+    },
+methods:{
+  editMode(){
+    this.showEdit = !this.showEdit;
+
+    if (this.showEdit) {
+      this.cropper = new Cropper(this.$refs.image, {
+        crop(event) {
+        },
+      });
+    } else {
+      this.cropper.destroy();
+    }
+  },
+  crop(){
+    this.cropper.getCroppedCanvas({
+              width: 795,
+              height: 341
+            })
+            .toBlob(
+                    blob => {
+                      this.message = '';
+                      this.isError = false;
+                      let body = new FormData();
+                      body.append('file', blob);
+                       console.log('blob',body.get('file'))
+                      this.ServiceContainer.requester.send({
+                        url: this.ServiceContainer.modal.data.item.href,
+                        method: 'PUT',
+                        responseType:'text',
+                        params: {
+                          // q: 'upload',
+                          // adapter: this.ServiceContainer.modal.data.adapter,
+                          // path: this.ServiceContainer.modal.data.item.path,
+                        },
+                        body
+                      })
+                              .then(data => {
+                                this.message = this.$t('Updated.');
+                                this.$refs.image.src = this.ServiceContainer.requester.getPreviewUrl(this.ServiceContainer.modal.data.adapter, this.ServiceContainer.modal.data.item);
+                                this.editMode();
+                                this.$emit('success');
+                              })
+                              .catch((e) => {
+                                this.message = this.$t(e.message);
+                                this.isError = true;
+                              });
+                    });
+  },
+  t(item){
+    return this.$t(item)
+  }
+},
+    mounted() {
+  this.$emit('success')
+    }
+  }
+</script>
+<!--<script setup>-->
+<!--import 'cropperjs/dist/cropper.css';-->
+<!--import Cropper from 'cropperjs';-->
+<!--import {inject, onMounted, ref} from 'vue';-->
+<!--import Message from '../Message.vue';-->
+<!--import {FEATURES} from "../features.js";-->
+
+<!--const emit = defineEmits(['success']);-->
+
+<!--const app = inject('ServiceContainer');-->
+
+<!--const {t} = app.i18n;-->
+
+<!--const image = ref(null);-->
+<!--const cropper = ref(null);-->
+<!--const showEdit = ref(false);-->
+<!--const message = ref('');-->
+<!--const isError = ref(false);-->
+
+<!--const editMode = () => {-->
+<!--  showEdit.value = !showEdit.value;-->
+
+<!--  if (showEdit.value) {-->
+<!--    cropper.value = new Cropper(image.value, {-->
+<!--      crop(event) {-->
+<!--      },-->
+<!--    });-->
+<!--  } else {-->
+<!--    cropper.value.destroy();-->
+<!--  }-->
+<!--};-->
+
+<!--const crop = () => {-->
+<!--  cropper.value-->
+<!--      .getCroppedCanvas({-->
+<!--        width: 795,-->
+<!--        height: 341-->
+<!--      })-->
+<!--      .toBlob(-->
+<!--          blob => {-->
+<!--            message.value = '';-->
+<!--            isError.value = false;-->
+<!--            const body = new FormData();-->
+<!--            body.set('file', blob);-->
+<!--            app.requester.send({-->
+<!--              url: '',-->
+<!--              method: 'post',-->
+<!--              params: {-->
+<!--                q: 'upload',-->
+<!--                adapter: app.modal.data.adapter,-->
+<!--                path: app.modal.data.item.path,-->
+<!--              },-->
+<!--              body,-->
+<!--            })-->
+<!--                .then(data => {-->
+<!--                  message.value = t('Updated.');-->
+<!--                  image.value.src = app.requester.getPreviewUrl(app.modal.data.adapter, app.modal.data.item);-->
+<!--                  editMode();-->
+<!--                  emit('success');-->
+<!--                })-->
+<!--                .catch((e) => {-->
+<!--                  message.value = t(e.message);-->
+<!--                  isError.value = true;-->
+<!--                });-->
+<!--          });-->
+<!--};-->
+
+<!--onMounted(() => {-->
+<!--  emit('success');-->
+<!--});-->
+
+<!--</script>-->

+ 61 - 0
src/components/FileManager/components/previews/Pdf.vue

@@ -0,0 +1,61 @@
+<template>
+  <div>
+  <h3 class="mb-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-400" id="modal-title"
+         :aria-label="ServiceContainer.modal.data.item.href" data-microtip-position="bottom-right" role="tooltip">{{ ServiceContainer.modal.data.item.name }}</h3>
+  <div>
+    <object class="h-[60vh]" :data="getPDFUrl()" type="application/pdf" width="100%" height="100%">
+      <iframe
+          class="border-0"
+          :src="getPDFUrl()"
+          width="100%"
+          height="100%"
+        >
+          <p>
+            Your browser does not support PDFs.
+            <a href="https://example.com/test.pdf">Download the PDF</a>
+            .
+          </p>
+        </iframe>
+    </object>
+  </div>
+  </div>
+</template>
+
+<script>
+  export default {
+    name:'Pdf',
+    inject: ['ServiceContainer'],
+    data(){
+      return{
+
+      }
+    },
+    methods:{
+      getPDFUrl(){
+        return this.ServiceContainer.requester.getPreviewUrl(this.ServiceContainer.modal.data.adapter, this.ServiceContainer.modal.data.item)
+      }
+    },
+    mounted() {
+      this.$emit('success')
+    }
+  }
+</script>
+
+<!--<script setup>-->
+
+<!--import {inject,onMounted} from 'vue';-->
+
+<!--const app = inject('ServiceContainer');-->
+
+<!--const emit = defineEmits(['success']);-->
+
+<!--const getPDFUrl = () => {-->
+<!--  return app.requester.getPreviewUrl(app.modal.data.adapter, app.modal.data.item)-->
+<!--}-->
+
+<!--onMounted(() => {-->
+<!--  emit('success');-->
+<!--});-->
+
+
+<!--</script>-->

+ 258 - 0
src/components/FileManager/components/previews/Text.vue

@@ -0,0 +1,258 @@
+<template>
+  <div>
+  <div class="flex">
+    <div class="mb-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-400" id="modal-title"
+         :aria-label="ServiceContainer.modal.data.item.name" data-microtip-position="bottom-right" role="tooltip">
+      {{ ServiceContainer.modal.data.item.name }}
+    </div>
+    <div class="ml-auto mb-2">
+      <button @click="save" class="ml-1 px-2 py-1 rounded border border-transparent shadow-sm bg-blue-700/75 hover:bg-blue-700 dark:bg-gray-700 dark:hover:bg-gray-700/50  text-base font-medium text-white sm:ml-3 sm:w-auto sm:text-sm" v-if="showEdit">
+        {{ t('Save') }}</button>
+      <button class="ml-1 px-2 py-1  text-blue-500" @click="editMode" v-if="ServiceContainer.features.includes(FEATURES.EDIT)">{{ showEdit ? t('Cancel'): t('Edit') }}</button>
+    </div>
+  </div>
+  <div>
+    <div class="ace-container" ref="ace"></div>
+<!--    <div v-else>-->
+<!--      <textarea-->
+<!--          ref="editInput"-->
+<!--          v-model="contentTemp"-->
+<!--          class="w-full p-2 rounded dark:bg-gray-700 dark:text-gray-200 dark:focus:ring-gray-600 dark:focus:border-gray-600 dark:selection:bg-gray-500 min-h-[200px] max-h-[60vh] text-xs" name="text" id="" cols="30" rows="10"></textarea>-->
+
+<!--    </div>-->
+    <message v-if="message.length" @hidden="message=''" :error="isError">{{ message }}</message>
+  </div>
+  </div>
+</template>
+
+<script>
+  import ace from 'ace-builds'
+  import 'ace-builds/webpack-resolver' // 在 webpack 环境中使用必须要导入
+  import 'ace-builds/src-noconflict/theme-monokai' // 默认设置的主题
+  import 'ace-builds/src-noconflict/mode-javascript' // 默认设置的语言模式
+  import Message from '../Message.vue'
+  import {FEATURES} from "../../features.js";
+  import {FILEMAPS,MODES} from "../../filemaps";
+
+  export  default {
+    name:'TextPreview',
+    inject: ['ServiceContainer'],
+    components:{
+      Message
+    },
+    data(){
+      return{
+        message:'',
+        showEdit:false,
+        isError:false,
+        FEATURES,
+        FILEMAPS,
+        MODES,
+        aceEditor:null,
+        themePath:'ace/theme/twilight',
+        modePath: 'ace/mode/markdown'
+      }
+    },
+    // computed:{
+    //   textMode(){
+    //     const filename = this.ServiceContainer.modal.data.item.name
+    //     console.log('filename',filename)
+    //     const modes = MODES.filter(p => p.extRe.test(filename))
+    //     if(modes.length){
+    //       return modes[0].mode
+    //     }else{
+    //       return 'ace/mode/text'
+    //     }
+    //   }
+    // },
+    methods:{
+      t(str){
+        return this.$t(str)
+      },
+     save() {
+        this.message = '';
+        this.isError = false;
+       let body = new FormData();
+       body.append('file', this.aceEditor.getValue());
+        this.ServiceContainer.requester.send({
+          url: this.ServiceContainer.modal.data.item.href,
+          method: 'PUT',
+          params: null,
+          body,
+          responseType: 'text',
+        })
+                .then(data => {
+                  this.message = this.$t('Updated.');
+                  this.$emit('success');
+                  this.showEdit = !this.showEdit;
+                  this.aceEditor.setReadOnly(true)
+                  this.aceEditor.session.getUndoManager().markClean()
+                })
+                .catch((e) => {
+                  this.message = this.$t(e.message);
+                  this.isError = true;
+                });
+      },
+      editMode(){
+        if(this.showEdit){
+          // if(!this.aceEditor.session.getUndoManager().isClean()&&this.aceEditor.session.getUndoManager().hasUndo()){
+          //   this.aceEditor.undo()
+          // }
+          this.getFile()
+          this.aceEditor.setReadOnly(true)
+        }else{
+          // this.getFile()
+          this.aceEditor.setReadOnly(false)
+        }
+        this.showEdit = !this.showEdit
+
+
+        // this.contentTemp = this.content;
+        // if (this.showEdit == true) {
+        //   // this.$nextTick(() => {
+        //   //   this.$refs.editInput.focus();
+        //   // });
+        // }
+      },
+      getFile(){
+        this.ServiceContainer.requester.send({
+          url: this.ServiceContainer.modal.data.item.href,
+          method: 'get',
+          params: null,
+          body:null,
+          responseType: 'text',
+        })
+                .then(data => {
+                  this.aceEditor.setValue(data)
+                  this.aceEditor.session.getUndoManager().markClean()
+
+                  console.log('editstate', this.aceEditor.session.getUndoManager().isClean(),this.aceEditor.session.getUndoManager().hasUndo())
+
+                });
+      }
+    },
+
+
+    // beforeUpdate(){
+    //   const filename =this.ServiceContainer.modal.data.item.name
+    //   //const modesMatch =this.MODES.filter(p=>p.extRe.test(filename))
+    //   let modeMatch=null
+    //   MODES.forEach(item=>{
+    //     if(item.extRe.test(filename)){
+    //       console.log('modem',item)
+    //       modeMatch=item
+    //       return
+    //     }
+    //   })
+    //   this.aceEditor.setOption({
+    //     mode: modeMatch?modeMatch.mode:'ace/mode/text'
+    //   })
+    // },
+    mounted() {
+      this.$emit('success')
+       console.log(this.textMode)
+      const filename =this.ServiceContainer.modal.data.item.name
+      const modesMatch =this.MODES.filter(p=>p.extRe.test(filename))
+      this.aceEditor = ace.edit(this.$refs.ace, {
+        maxLines: 20, // 最大行数,超过会自动出现滚动条
+        minLines: 10, // 最小行数,还未到最大行数时,编辑器会自动伸缩大小
+        fontSize: 14, // 编辑器内字体大小
+        theme: this.themePath, // 默认设置的主题
+        value:this.content,
+        readOnly:true,
+        cursorStyle:'ace',
+        selectionStyle:'line',
+        autoScrollEditorIntoView:true,
+        mode: modesMatch.length?modesMatch[0].mode:'ace/mode/text',//modeMatch?modeMatch.mode:'ace/mode/text',//'ace/mode/css', // 默认设置的语言模式
+        tabSize: 4 // 制表符设置为 4 个空格大小
+      })
+
+     this.getFile()
+    },
+    destroyed() {
+      this.aceEditor=null
+    }
+  }
+</script>
+
+<!--<script setup>-->
+
+<!--import {inject, nextTick, onMounted, ref} from 'vue';-->
+<!--import Message from '../Message.vue';-->
+<!--import {FEATURES} from "../features.js";-->
+
+<!--const emit = defineEmits(['success'])-->
+<!--const content = ref('');-->
+<!--const contentTemp = ref('');-->
+<!--const editInput = ref(null);-->
+<!--const showEdit = ref(false);-->
+
+<!--const message = ref('');-->
+<!--const isError = ref(false);-->
+
+<!--const app = inject('ServiceContainer');-->
+
+<!--const {t} = app.i18n;-->
+
+<!--onMounted(() => {-->
+<!--  app.requester.send({-->
+<!--    url: '',-->
+<!--    method: 'get',-->
+<!--    params: {q: 'preview', adapter: app.modal.data.adapter, path: app.modal.data.item.path},-->
+<!--    responseType: 'text',-->
+<!--  })-->
+<!--      .then(data => {-->
+<!--        content.value = data;-->
+<!--        emit('success');-->
+<!--      });-->
+<!--});-->
+
+<!--const editMode = () => {-->
+<!--  showEdit.value = !showEdit.value;-->
+<!--  contentTemp.value = content.value;-->
+<!--  if (showEdit.value == true) {-->
+<!--    nextTick(() => {-->
+<!--      editInput.value.focus();-->
+<!--    });-->
+<!--  }-->
+<!--};-->
+
+<!--const save = () => {-->
+<!--  message.value = '';-->
+<!--  isError.value = false;-->
+
+<!--  app.requester.send({-->
+<!--    url: '',-->
+<!--    method: 'post',-->
+<!--    params: {-->
+<!--      q: 'save',-->
+<!--      adapter: app.modal.data.adapter,-->
+<!--      path: app.modal.data.item.path,-->
+<!--    },-->
+<!--    body: {-->
+<!--      content: contentTemp.value-->
+<!--    },-->
+<!--    responseType: 'text',-->
+<!--  })-->
+<!--      .then(data => {-->
+<!--        message.value = t('Updated.');-->
+<!--        content.value = data;-->
+<!--        emit('success');-->
+<!--        showEdit.value = !showEdit.value;-->
+<!--      })-->
+<!--      .catch((e) => {-->
+<!--        message.value = t(e.message);-->
+<!--        isError.value = true;-->
+<!--      });-->
+<!--}-->
+
+<!--</script>-->
+<style scoped>
+   .ace-container{
+     min-height: 400px;
+   }
+   .ace-container  /deep/ .ace_cursor{
+    border-left: 2px solid ;
+    transform: translatez(0) ;
+  }
+</style>

+ 49 - 0
src/components/FileManager/components/previews/Video.vue

@@ -0,0 +1,49 @@
+<template>
+<div>
+    <h3 class="mb-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-400" id="modal-title"
+         :aria-label="ServiceContainer.modal.data.item.name" data-microtip-position="bottom-right" role="tooltip">{{ ServiceContainer.modal.data.item.name }}</h3>
+  <div>
+    <video class="w-full" preload controls>
+      <source :src="getVideoUrl()" type="video/mp4">
+      Your browser does not support the video tag.
+    </video>
+  </div>
+</div>
+</template>
+<script>
+
+  export default {
+    name:'Video',
+    inject: ['ServiceContainer'],
+    data(){
+      return{
+
+      }
+    },
+    methods:{
+      getVideoUrl(){
+        return this.ServiceContainer.requester.getPreviewUrl(null, this.ServiceContainer.modal.data.item)
+      }
+    },
+    mounted() {
+      this.$emit('success')
+    }
+  }
+</script>
+
+<!--<script setup>-->
+<!--import {inject, onMounted} from 'vue';-->
+
+<!--const app = inject("ServiceContainer");-->
+<!--const emit = defineEmits(['success']);-->
+
+<!--const getVideoUrl = () => {-->
+<!--  return app.requester.getPreviewUrl(app.modal.data.adapter, app.modal.data.item)-->
+<!--}-->
+
+<!--onMounted(() => {-->
+<!--  emit('success');-->
+<!--});-->
+
+<!--</script>-->
+

+ 31 - 0
src/components/FileManager/composables/useDebouncedRef.js

@@ -0,0 +1,31 @@
+const debounce = (fn, delay = 0, immediate = false) => {
+  let timeout
+  return (...args) => {
+    if (immediate && !timeout) fn(...args)
+    clearTimeout(timeout)
+
+    timeout = setTimeout(() => {
+      fn(...args)
+    }, delay)
+  }
+}
+
+const useDebouncedRef = (initialValue, delay, immediate) => {
+  let state = initialValue
+  return (track, trigger) => ({
+    get() {
+      track()
+      return state
+    },
+    set: debounce(
+      value => {
+        state = value
+        trigger()
+      },
+      delay,
+      immediate
+    )
+  })
+}
+
+export default useDebouncedRef

+ 47 - 0
src/components/FileManager/composables/useI18n.js

@@ -0,0 +1,47 @@
+export async function loadLocale(locale, supportedLocales) {
+  const localeData = supportedLocales[locale]
+  return typeof localeData === 'function' ? (await localeData()).default : localeData
+}
+
+export function useI18n(storage, initialLocale, emitter, supportedLocales) {
+  const { getStore, setStore } = storage
+  let translations = {}
+  let locale = getStore('locale', initialLocale)
+
+  const changeLocale = (newLocale, defaultLocale = initialLocale) => {
+    loadLocale(newLocale, supportedLocales).then((i18n) => {
+      translations.value = i18n
+      setStore('locale', newLocale)
+      locale.value = newLocale
+      setStore('translations', i18n)
+      if (Object.values(supportedLocales).length > 1) {
+        emitter.emit('vf-toast-push', { label: 'The language is set to ' + newLocale })
+        emitter.emit('vf-language-saved')
+      }
+    }).catch(e => {
+      if (defaultLocale) {
+        emitter.emit('vf-toast-push', { label: 'The selected locale is not yet supported!', type: 'error' })
+        changeLocale(defaultLocale, null)
+      } else {
+        emitter.emit('vf-toast-push', { label: 'Locale cannot be loaded!', type: 'error' })
+      }
+    })
+  }
+
+  if (!getStore('locale') && !supportedLocales.length) {
+    changeLocale(initialLocale)
+  } else {
+    translations.value = getStore('translations')
+  }
+  const sprintf = (str, ...argv) => !argv.length ? str : sprintf(str = str.replace('%s', argv.shift()), ...argv)
+
+  function t(key, ...params) {
+    if (translations.value && translations.value.hasOwnProperty(key)) {
+      return sprintf(translations.value[key], ...params)
+    }
+    return sprintf(key, ...params)
+  }
+
+  return { t, changeLocale, locale }
+}
+

+ 51 - 0
src/components/FileManager/composables/useStorage.js

@@ -0,0 +1,51 @@
+/** @param {String} key */
+// import { set } from 'ag-grid-community/dist/lib/utils/object'
+
+export function useStorage(key) {
+  let storedValues = localStorage.getItem(key + '_storage')
+
+  const storage = JSON.parse(storedValues ?? '{}')
+
+  function setItem() {
+    if (!Object.keys(storage).length) {
+      localStorage.removeItem(key + '_storage')
+    } else {
+      localStorage.setItem(key + '_storage', JSON.stringify(storage))
+    }
+  }
+
+  /**
+     * @param {String} key
+     * @param {*} value
+     */
+  function setStore(key, value) {
+    storage[key] = value
+    setItem()
+  }
+
+  /**
+     * @param {String} key
+     */
+  function removeStore(key) {
+    delete storage[key]
+    setItem()
+  }
+
+  function clearStore() {
+    Object.keys(storage).map(key => removeStore(key))
+    setItem()
+  }
+
+  /**
+     * @param {String} key
+     * @param {*} defaultValue
+     */
+  const getStore = (key, defaultValue = null) => {
+    if (storage.hasOwnProperty(key)) {
+      return storage[key]
+    }
+    return defaultValue
+  }
+
+  return { getStore, setStore, removeStore, clearStore }
+}

+ 55 - 0
src/components/FileManager/composables/useTheme.js

@@ -0,0 +1,55 @@
+
+const THEMES = {
+  SYSTEM: 'system',
+  LIGHT: 'light',
+  DARK: 'dark'
+}
+
+/**
+ * @typedef Theme
+ * @type {"system"|"light"|"dark"} theme
+ */
+
+export default function(storage, propTheme) {
+  let theme = storage.getStore('theme', propTheme ?? THEMES.SYSTEM)
+  let actualTheme = THEMES.LIGHT
+  // theme.value = storage.getStore('theme', propTheme ?? THEMES.SYSTEM);
+
+  const matcher = window.matchMedia('(prefers-color-scheme: dark)')
+
+  const updateActualTheme = (matcher) => {
+    if (theme === THEMES.DARK || (theme === THEMES.SYSTEM && matcher.matches)) {
+      actualTheme = THEMES.DARK
+    } else {
+      actualTheme = THEMES.LIGHT
+    }
+  }
+
+  updateActualTheme(matcher)
+  matcher.addEventListener('change', updateActualTheme)
+
+  return {
+    /**
+     * @type {import('vue').Ref<Theme>}
+     */
+    value: theme,
+
+    /**
+     * @type {import('vue').Ref<Theme>}
+     */
+    actualValue: actualTheme,
+
+    /**
+     * @param {Theme} value
+     */
+    set(value) {
+      theme = value
+      if (value !== THEMES.SYSTEM) {
+        storage.setStore('theme', value)
+      } else {
+        storage.removeStore('theme')
+      }
+      updateActualTheme(matcher)
+    }
+  }
+}

+ 17 - 0
src/components/FileManager/features.js

@@ -0,0 +1,17 @@
+export const FEATURES = {
+    EDIT: 'edit',
+    // NEW_FILE: 'newfile',
+    NEW_FOLDER: 'newfolder',
+    PREVIEW: 'preview',
+    ARCHIVE: 'archive',
+    UNARCHIVE: 'unarchive',
+    SEARCH: 'search',
+    RENAME: 'rename',
+    UPLOAD: 'upload',
+    DELETE: 'delete',
+    FULL_SCREEN: 'fullscreen',
+    DOWNLOAD: 'download',
+    LANGUAGE: 'language',
+}
+
+export const FEATURE_ALL_NAMES = Object.values(FEATURES)

+ 262 - 0
src/components/FileManager/filemaps.js

@@ -0,0 +1,262 @@
+var supportedModes = {
+  ABAP: ['abap'],
+  ABC: ['abc'],
+  ActionScript: ['as'],
+  ADA: ['ada|adb'],
+  Alda: ['alda'],
+  Apache_Conf: ['^htaccess|^htgroups|^htpasswd|^conf|htaccess|htgroups|htpasswd'],
+  Apex: ['apex|cls|trigger|tgr'],
+  AQL: ['aql'],
+  AsciiDoc: ['asciidoc|adoc'],
+  ASL: ['dsl|asl|asl.json'],
+  Assembly_ARM32: ['s'],
+  Assembly_x86: ['asm|a'],
+  Astro: ['astro'],
+  AutoHotKey: ['ahk'],
+  BatchFile: ['bat|cmd'],
+  BibTeX: ['bib'],
+  C_Cpp: ['cpp|c|cc|cxx|h|hh|hpp|ino'],
+  C9Search: ['c9search_results'],
+  Cirru: ['cirru|cr'],
+  Clojure: ['clj|cljs'],
+  Cobol: ['CBL|COB'],
+  coffee: ['coffee|cf|cson|^Cakefile'],
+  ColdFusion: ['cfm|cfc'],
+  Crystal: ['cr'],
+  CSharp: ['cs'],
+  Csound_Document: ['csd'],
+  Csound_Orchestra: ['orc'],
+  Csound_Score: ['sco'],
+  CSS: ['css'],
+  Curly: ['curly'],
+  Cuttlefish: ['conf'],
+  D: ['d|di'],
+  Dart: ['dart'],
+  Diff: ['diff|patch'],
+  Django: ['djt|html.djt|dj.html|djhtml'],
+  Dockerfile: ['^Dockerfile'],
+  Dot: ['dot'],
+  Drools: ['drl'],
+  Edifact: ['edi'],
+  Eiffel: ['e|ge'],
+  EJS: ['ejs'],
+  Elixir: ['ex|exs'],
+  Elm: ['elm'],
+  Erlang: ['erl|hrl'],
+  Flix: ['flix'],
+  Forth: ['frt|fs|ldr|fth|4th'],
+  Fortran: ['f|f90'],
+  FSharp: ['fsi|fs|ml|mli|fsx|fsscript'],
+  FSL: ['fsl'],
+  FTL: ['ftl'],
+  Gcode: ['gcode'],
+  Gherkin: ['feature'],
+  Gitignore: ['^.gitignore'],
+  Glsl: ['glsl|frag|vert'],
+  Gobstones: ['gbs'],
+  golang: ['go'],
+  GraphQLSchema: ['gql'],
+  Groovy: ['groovy'],
+  HAML: ['haml'],
+  Handlebars: ['hbs|handlebars|tpl|mustache'],
+  Haskell: ['hs'],
+  Haskell_Cabal: ['cabal'],
+  haXe: ['hx'],
+  Hjson: ['hjson'],
+  HTML: ['html|htm|xhtml|we|wpy'],
+  HTML_Elixir: ['eex|html.eex'],
+  HTML_Ruby: ['erb|rhtml|html.erb'],
+  INI: ['ini|conf|cfg|prefs'],
+  Io: ['io'],
+  Ion: ['ion'],
+  Jack: ['jack'],
+  Jade: ['jade|pug'],
+  Java: ['java'],
+  JavaScript: ['js|jsm|cjs|mjs'],
+  JEXL: ['jexl'],
+  JSON: ['json'],
+  JSON5: ['json5'],
+  JSONiq: ['jq'],
+  JSP: ['jsp'],
+  JSSM: ['jssm|jssm_state'],
+  JSX: ['jsx'],
+  Julia: ['jl'],
+  Kotlin: ['kt|kts'],
+  LaTeX: ['tex|latex|ltx|bib'],
+  Latte: ['latte'],
+  LESS: ['less'],
+  Liquid: ['liquid'],
+  Lisp: ['lisp'],
+  LiveScript: ['ls'],
+  Log: ['log'],
+  LogiQL: ['logic|lql'],
+  Logtalk: ['lgt'],
+  LSL: ['lsl'],
+  Lua: ['lua'],
+  LuaPage: ['lp'],
+  Lucene: ['lucene'],
+  Makefile: ['^Makefile|^GNUmakefile|^makefile|^OCamlMakefile|make'],
+  Markdown: ['md|markdown'],
+  Mask: ['mask'],
+  MATLAB: ['matlab'],
+  Maze: ['mz'],
+  MediaWiki: ['wiki|mediawiki'],
+  MEL: ['mel'],
+  MIPS: ['s|asm'],
+  MIXAL: ['mixal'],
+  MUSHCode: ['mc|mush'],
+  MySQL: ['mysql'],
+  Nasal: ['nas'],
+  Nginx: ['nginx|conf'],
+  Nim: ['nim'],
+  Nix: ['nix'],
+  NSIS: ['nsi|nsh'],
+  Nunjucks: ['nunjucks|nunjs|nj|njk'],
+  ObjectiveC: ['m|mm'],
+  OCaml: ['ml|mli'],
+  Odin: ['odin'],
+  PartiQL: ['partiql|pql'],
+  Pascal: ['pas|p'],
+  Perl: ['pl|pm'],
+  pgSQL: ['pgsql'],
+  PHP: ['php|inc|phtml|shtml|php3|php4|php5|phps|phpt|aw|ctp|module'],
+  PHP_Laravel_blade: ['blade.php'],
+  Pig: ['pig'],
+  PLSQL: ['plsql'],
+  Powershell: ['ps1'],
+  Praat: ['praat|praatscript|psc|proc'],
+  Prisma: ['prisma'],
+  Prolog: ['plg|prolog'],
+  Properties: ['properties'],
+  Protobuf: ['proto'],
+  PRQL: ['prql'],
+  Puppet: ['epp|pp'],
+  Python: ['py'],
+  QML: ['qml'],
+  R: ['r'],
+  Raku: ['raku|rakumod|rakutest|p6|pl6|pm6'],
+  Razor: ['cshtml|asp'],
+  RDoc: ['Rd'],
+  Red: ['red|reds'],
+  RHTML: ['Rhtml'],
+  Robot: ['robot|resource'],
+  RST: ['rst'],
+  Ruby: ['rb|ru|gemspec|rake|^Guardfile|^Rakefile|^Gemfile'],
+  Rust: ['rs'],
+  SaC: ['sac'],
+  SASS: ['sass'],
+  SCAD: ['scad'],
+  Scala: ['scala|sbt'],
+  Scheme: ['scm|sm|rkt|oak|scheme'],
+  Scrypt: ['scrypt'],
+  SCSS: ['scss'],
+  SH: ['sh|bash|^.bashrc'],
+  SJS: ['sjs'],
+  Slim: ['slim|skim'],
+  Smarty: ['smarty|tpl'],
+  Smithy: ['smithy'],
+  snippets: ['snippets'],
+  Soy_Template: ['soy'],
+  Space: ['space'],
+  SPARQL: ['rq'],
+  SQL: ['sql'],
+  SQLServer: ['sqlserver'],
+  Stylus: ['styl|stylus'],
+  SVG: ['svg'],
+  Swift: ['swift'],
+  Tcl: ['tcl'],
+  Terraform: ['tf', 'tfvars', 'terragrunt'],
+  Tex: ['tex'],
+  Text: ['txt'],
+  Textile: ['textile'],
+  Toml: ['toml'],
+  TSX: ['tsx'],
+  Turtle: ['ttl'],
+  Twig: ['twig|swig'],
+  Typescript: ['ts|mts|cts|typescript|str'],
+  Vala: ['vala'],
+  VBScript: ['vbs|vb'],
+  Velocity: ['vm'],
+  Verilog: ['v|vh|sv|svh'],
+  VHDL: ['vhd|vhdl'],
+  Visualforce: ['vfp|component|page'],
+  Vue: ['vue'],
+  Wollok: ['wlk|wpgm|wtest'],
+  XML: ['xml|rdf|rss|wsdl|xslt|atom|mathml|mml|xul|xbl|xaml'],
+  XQuery: ['xq'],
+  YAML: ['yaml|yml'],
+  Zeek: ['zeek|bro'],
+  Zig: ['zig']
+}
+class Mode {
+  /**
+     * @param {string} name
+     * @param {string} caption
+     * @param {string} extensions
+     */
+  constructor(name, caption, extensions) {
+    this.name = name
+    this.caption = caption
+    this.mode = 'ace/mode/' + name
+    this.extensions = extensions
+    var re
+    if (/\^/.test(extensions)) {
+      re = extensions.replace(/\|(\^)?/g, function(a, b) {
+        return '$|' + (b ? '^' : '^.*\\.')
+      }) + '$'
+    } else {
+      re = '^.*\\.(' + extensions + ')$'
+    }
+
+    this.extRe = new RegExp(re, 'i')
+  }
+
+  /**
+     * @param {string} filename
+     */
+  supportsFile(filename) {
+    return filename.match(this.extRe)
+  }
+}
+export const FILEMAPS = {
+  'html': 'ace/mode/html',
+  'htm': 'ace/mode/html',
+  'css': 'ace/mode/css',
+  'md': 'ace/mode/markdown',
+  'xml': 'ace/mode/xml',
+  'txt': 'ace/mode/text',
+  'yml': 'ace/mode/yml',
+  'yaml': 'ace/mode/yaml',
+  'ini': 'ace/mode/ini',
+  'java': 'ace/mode/java',
+  'sql': 'ace/mode/mysql',
+  'conf': 'ace/mode/nginx',
+  'php': 'ace/mode/php',
+  'cs': 'ace/mode/csharp',
+  'c_pp': 'ace/mode/c_pp',
+  'js': 'ace/mode/javascript',
+  'ts': 'ace/mode/javascript',
+  'kt': 'ace/mode/ktolin',
+  'less': 'ace/mode/less',
+  'sass': 'ace/mode/sass',
+  'vue': 'ace/mode/vue',
+  'py': 'ace/mode/python',
+  'json': 'ace/mode/json',
+  'sh': 'ace/mode/sh',
+  'hl7': 'ace/mode/text',
+  'log': 'ace/mode/text'
+
+}
+export const FILEEXTS = Object.keys(FILEMAPS)
+var modesByName = {};
+var modes = [];
+for (var name in supportedModes) {
+  var data = supportedModes[name]
+  var displayName = (name).replace(/_/g, ' ')
+  var filename = name.toLowerCase()
+  var mode = new Mode(filename, displayName, data[0])
+  modesByName[filename] = mode
+  modes.push(mode)
+}
+
+export const MODES = modes

+ 32 - 0
src/components/FileManager/index.js

@@ -0,0 +1,32 @@
+import VueFinder from './VueFinder.vue'
+import * as modals from './modals.js'
+
+export default {
+  /** @param {import('vue').App} app
+     * @param options
+     */
+  install(Vue, options = {}) {
+    // define main component
+    Vue.component('VueFinder', VueFinder)
+
+    // define modals
+    for (const modal of Object.values(modals)) {
+      Vue.component(modal.name, modal)
+    }
+
+    // define global properties with 'options'
+    options.i18n = options.i18n ?? {}
+    let [firstLanguage] = Object.keys(options.i18n)
+    options.locale = options.locale ?? firstLanguage ?? 'en'
+
+    // unique id for the app options
+    Vue.mixin({
+      data() {
+        return { ...options }
+      }
+
+    })
+    // Vue.provide('VueFinderOptions', options)
+  }
+}
+

+ 89 - 0
src/components/FileManager/locales/de.js

@@ -0,0 +1,89 @@
+import uppyLocaleDe from '@uppy/locales/lib/de_DE.js';
+
+export default {
+    "Language": "Sprache",
+    "Create": "Erstellen",
+    "Close": "Schließen",
+    "Cancel": "Abbrechen",
+    "Save": "Speichern",
+    "Edit": "Bearbeiten",
+    "Crop": "Zuschneiden",
+    "New Folder": "Neuer Ordner",
+    "New File": "Neue Datei",
+    "Rename": "Umbenennen",
+    "Delete": "Löschen",
+    "Upload": "Hochladen",
+    "Download": "Herunterladen",
+    "Archive": "Archivieren",
+    "Unarchive": "Dearchivieren",
+    "Open": "Öffnen",
+    "Open containing folder": "Enthaltenden Ordner öffnen",
+    "Refresh": "Aktualisieren",
+    "Preview": "Vorschau",
+    "Toggle Full Screen": "Vollbild umschalten",
+    "Change View": "Ansicht ändern",
+    "Storage": "Speicher",
+    "Go up a directory": "Ein Verzeichnis hochgehen",
+    "Search anything..": "Suche etwas..",
+    "Name": "Name",
+    "Size": "Größe",
+    "Date": "Datum",
+    "Filepath": "Dateipfad",
+    "About": "Über",
+    "Folder Name": "Ordnername",
+    "File Name": "Dateiname",
+    "Move files": "Dateien verschieben",
+    "Yes, Move!": "Ja, Verschieben!",
+    "Delete files": "Dateien löschen",
+    "Yes, Delete!": "Ja, Löschen!",
+    "Upload Files": "Dateien hochladen",
+    "No files selected!": "Keine Dateien ausgewählt!",
+    "Select Files": "Dateien auswählen",
+    "Archive the files": "Dateien archivieren",
+    "Unarchive the files": "Dateien dearchivieren",
+    "The archive will be unarchived at": "Das Archiv wird dearchiviert bei",
+    "Archive name. (.zip file will be created)": "Archivname. (.zip Datei wird erstellt)",
+    "Vuefinder is a file manager component for vue 3.": "Vuefinder ist eine Dateimanager-Komponente für Vue 3.",
+    "Create a new folder": "Einen neuen Ordner erstellen",
+    "Create a new file": "Eine neue Datei erstellen",
+    "Are you sure you want to delete these files?": "Sind Sie sicher, dass Sie diese Dateien löschen möchten?",
+    "This action cannot be undone.": "Diese Aktion kann nicht rückgängig gemacht werden.",
+    "Search results for": "Suchergebnisse für",
+    "%s item(s) selected.": "%s Element(e) ausgewählt.",
+    "%s is renamed.": "%s wurde umbenannt.",
+    "This is a readonly storage.": "Dies ist ein schreibgeschützter Speicher.",
+    "%s is created.": "%s wurde erstellt.",
+    "Files moved.": "Dateien verschoben.",
+    "Files deleted.": "Dateien gelöscht.",
+    "The file unarchived.": "Die Datei wurde dearchiviert.",
+    "The file(s) archived.": "Die Datei(en) wurden archiviert.",
+    "Updated.": "Aktualisiert.",
+    "No search result found.": "Keine Suchergebnisse gefunden.",
+    "Are you sure you want to move these files?": "Sind Sie sicher, dass Sie diese Dateien verschieben möchten?",
+    "File Size": "Dateigröße",
+    "Last Modified": "Zuletzt geändert",
+    "Drag&Drop: on": "Drag&Drop: an",
+    "Drag&Drop: off": "Drag&Drop: aus",
+    "Select Folders": "Ordner auswählen",
+    "Clear all": "Alles löschen",
+    "Clear only successful": "Nur erfolgreiche löschen",
+    "Drag and drop the files/folders to here or click here.": "Ziehen Sie die Dateien/Ordner hierher oder klicken Sie hier.",
+    "Release to drop these files.": "Lassen Sie los, um diese Dateien abzulegen.",
+    "Canceled": "Abgebrochen",
+    "Done": "Fertig",
+    "Network Error, Unable establish connection to the server or interrupted.": "Netzwerkfehler, Verbindung zum Server kann nicht hergestellt oder unterbrochen werden.",
+    "Pending upload": "Ausstehender",
+    "Please select file to upload first.": "Bitte wählen Sie zuerst eine Datei zum Hochladen aus.",
+    "About %s": "Über %s",
+    "Settings": "Einstellungen",
+    "Use Metric Units": "Use Metric Units",
+    "Saved.": "Gespeichert.",
+    "Reset Settings": "Einstellungen zurücksetzen",
+    "Download doesn\'t work? You can try right-click \"Download\" button, select \"Save link as...\".": "Der Download funktioniert nicht? Sie können versuchen, mit der rechten Maustaste auf die Schaltfläche \"Download\" zu klicken und \"Link speichern unter...\" auszuwählen.",
+    "Theme": "Thema",
+    "Dark": "Dunkel",
+    "Light": "Licht",
+    "System": "System",
+    "Target Directory" : "Zielverzeichnis",
+    "uppy" : uppyLocaleDe,
+}

+ 89 - 0
src/components/FileManager/locales/en.js

@@ -0,0 +1,89 @@
+import uppyLocaleEn from '@uppy/locales/lib/en_US.js';
+
+export default {
+  "Language": "Language",
+  "Create": "Create",
+  "Close": "Close",
+  "Cancel": "Cancel",
+  "Save": "Save",
+  "Edit": "Edit",
+  "Crop": "Crop",
+  "New Folder": "New Folder",
+  "New File": "New File",
+  "Rename": "Rename",
+  "Delete": "Delete",
+  "Upload": "Upload",
+  "Download": "Download",
+  "Archive": "Archive",
+  "Unarchive": "Unarchive",
+  "Open": "Open",
+  "Open containing folder": "Open containing folder",
+  "Refresh": "Refresh",
+  "Preview": "Preview",
+  "Toggle Full Screen": "Toggle Full Screen",
+  "Change View": "Change View",
+  "Storage" : "Storage",
+  "Go up a directory": "Go up a directory",
+  "Search anything..": "Search anything..",
+  "Name": "Name",
+  "Size": "Size",
+  "Date": "Date",
+  "Filepath": "Filepath",
+  "About": "About",
+  "Folder Name": "Folder Name",
+  "File Name": "File Name",
+  "Move files": "Move files",
+  "Yes, Move!": "Yes, Move!",
+  "Delete files": "Delete files",
+  "Yes, Delete!": "Yes, Delete!",
+  "Upload Files" : "Upload Files",
+  "No files selected!": "No files selected!",
+  "Select Files": "Select Files",
+  "Archive the files": "Archive the files",
+  "Unarchive the files": "Unarchive the files",
+  "The archive will be unarchived at": "The archive will be unarchived at",
+  "Archive name. (.zip file will be created)": "Archive name. (.zip file will be created)",
+  "Vuefinder is a file manager component for vue 3.": "Vuefinder is a file manager component for Vue 3.",
+  "Create a new folder": "Create a new folder",
+  "Create a new file": "Create a new file",
+  "Are you sure you want to delete these files?": "Are you sure you want to delete these files?",
+  "This action cannot be undone.": "This action cannot be undone.",
+  "Search results for" : "Search results for",
+  "%s item(s) selected.": "%s item(s) selected.",
+  "%s is renamed." : "%s is renamed.",
+  "This is a readonly storage." : "This is a readonly storage.",
+  "%s is created." : "%s is created.",
+  "Files moved." : "Files moved.",
+  "Files deleted." : "Files deleted.",
+  "The file unarchived." : "The file unarchived.",
+  "The file(s) archived." : "The file(s) archived.",
+  "Updated." : "Updated.",
+  "No search result found." : "No search result found.",
+  "Are you sure you want to move these files?" : "Are you sure you want to move these files?",
+  "File Size": "File Size",
+  "Last Modified": "Last Modified",
+  "Drag&Drop: on": "Drag&Drop: on",
+  "Drag&Drop: off": "Drag&Drop: off",
+  "Select Folders": "Select Folders",
+  "Clear all": "Clear all",
+  "Clear only successful": "Clear only successful",
+  "Drag and drop the files/folders to here or click here.": "Drag and drop the files/folders to here or click here.",
+  "Release to drop these files.": "Release to drop these files.",
+  "Canceled": "Canceled",
+  "Done": "Done",
+  "Network Error, Unable establish connection to the server or interrupted.": "Network Error, Unable establish connection to the server or interrupted.",
+  "Pending upload": "Pending",
+  "Please select file to upload first." : "Please select file to upload first.",
+  "About %s": "About %s",
+  "Settings": "Settings",
+  "Use Metric Units": "Use Metric Units",
+  "Saved.": "Saved.",
+  "Reset Settings": "Reset Settings",
+  "Download doesn\'t work? You can try right-click \"Download\" button, select \"Save link as...\".": "Download doesn\'t work? You can try right-click \"Download\" button, select \"Save link as...\".",
+  "Theme": "Theme",
+  "Dark": "Dark",
+  "Light": "Light",
+  "System": "System",
+  "Target Directory" : "Target Directory",
+  "uppy": uppyLocaleEn
+}

+ 89 - 0
src/components/FileManager/locales/fa.js

@@ -0,0 +1,89 @@
+import uppyLocaleFa from '@uppy/locales/lib/fa_IR.js';
+
+export default {
+  "Language": "زبان",
+  "Create": "ایجاد",
+  "Close": "بستن",
+  "Cancel": "انصراف",
+  "Save": "ذخیره",
+  "Edit": "ویرایش",
+  "Crop": "برش تصویر",
+  "New Folder": "پوشه جدید",
+  "New File": "فایل جدید",
+  "Rename": "تغییر نام",
+  "Delete": "حذف",
+  "Upload": "آپلود",
+  "Download": "دانلود",
+  "Archive": "فشرده سازی",
+  "Unarchive": "باز کردن فایل فشرده",
+  "Open": "باز کردن",
+  "Open containing folder": "محتوای پوشه را باز کن!",
+  "Refresh": "بارکذاری مجدد",
+  "Preview": "پیشنمایش",
+  "Toggle Full Screen": "تمام تصویر کردن",
+  "Change View": "تغییر نوع نمایش",
+  "Storage" : "فضا",
+  "Go up a directory": "برو به پوشه",
+  "Search anything..": "به دنبال چه چیزی هستید ؟ جستجو کنید ...",
+  "Name": "نام",
+  "Size": "سایز",
+  "Date": "تاریخ انتشار",
+  "Filepath": "مسیر فایل",
+  "About": "درباره",
+  "Folder Name": "نام پوشه",
+  "File Name": "نام فایل",
+  "Move files": "انتقال فایل ها",
+  "Yes, Move!": "بله، انتقال بده!",
+  "Delete files": "پاک کردن فایل ها",
+  "Yes, Delete!": "بله، پاک کن!",
+  "Upload Files" : "آپلود کردن فایل ها",
+  "No files selected!": "هیچ فایلی انتخاب نشده است.",
+  "Select Files": "انتخاب فایل ها",
+  "Archive the files": "فشرده سازی فایل ها",
+  "Unarchive the files": "باز کردن فایل های فشرده",
+  "The archive will be unarchived at": "فایل فشرده سازه در این مسیر باز میشود: ",
+  "Archive name. (.zip file will be created)": "نام فایل فشرده",
+  "Vuefinder is a file manager component for vue 3.": "Vuefinder یک کتابخانه مدیریت فایل ها برای Vue3 میباشد.",
+  "Create a new folder": "ایجاد پوشه جدید",
+  "Create a new file": "ایجاد فایل جدید",
+  "Are you sure you want to delete these files?": "آیا از حذف فایل ها مطمئن هستید ؟",
+  "This action cannot be undone.": "این تغییرات قابل بازگشت نیست!",
+  "Search results for" : "نتیجه جستجو برای",
+  "%s item(s) selected.": "%s آیتم(های) انتخاب شده",
+  "%s is renamed." : "تغییر نام برای %s صورت گرفت.",
+  "This is a readonly storage." : "این فضا فقط قابل خواندن است!",
+  "%s is created." : "%s ساخته شد!",
+  "Files moved." : "فایل(ها) انقال یافتند.",
+  "Files deleted." : "فایل(ها) حذف شدند.",
+  "The file unarchived." : "فایل فشرده شده باز شد.",
+  "The file(s) archived." : "فایل(ها) فشرده سازی شدند.",
+  "Updated." : "آپدیت شد.",
+  "No search result found." : "هیچ نتیجه ای یافت نشد.",
+  "Are you sure you want to move these files?" : "آیا برای انتقال فایل ها مطمئن هستید ؟",
+  "File Size": "سایز فایل",
+  "Last Modified": "آخرین ویرایش",
+  "Drag&Drop: on": "Drag&Drop: روشن",
+  "Drag&Drop: off": "Drag&Drop: خاموش",
+  "Select Folders": "Select Folders",
+  "Clear all": "پاک کردن همه",
+  "Clear only successful": "پاک کردن فقط موفق",
+  "Drag and drop the files/folders to here or click here.": "فایل ها/پوشه ها را به اینجا بکشید یا اینجا کلیک کنید.",
+  "Release to drop these files.": "رها کنید تا این فایل ها را رها کنید.",
+  "Canceled": "لغو شد",
+  "Done": "انجام شد",
+  "Network Error, Unable establish connection to the server or interrupted.": "خطای شبکه، اتصال به سرور برقرار نشد یا قطع شد.",
+  "Pending upload": "آپلود در حال انتظار",
+  "Please select file to upload first." : "لطفا ابتدا فایلی را برای آپلود انتخاب کنید.",
+  "About %s": "درباره %s",
+  "Settings": "تنظیمات",
+  "Use Metric Units": "استفاده از واحد های متریک",
+  "Saved.": "ذخیره شد.",
+  "Reset Settings": "بازنشانی تنظیمات",
+  "Download doesn\'t work? You can try right-click \"Download\" button, select \"Save link as...\".": "دانلود کار نمیکند؟ میتوانید روی دکمه \"دانلود\" راست کلیک کرده و \"ذخیره لینک به عنوان...\" را انتخاب کنید",
+  "Theme": "تم",
+  "Dark": "تاریک",
+  "Light": "روشن",
+  "System": "سیستم",
+  "Target Directory" : "پوشه مقصد",
+  "uppy": uppyLocaleFa
+}

+ 90 - 0
src/components/FileManager/locales/fr.js

@@ -0,0 +1,90 @@
+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",
+  "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",
+  "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",
+  "%s item(s) selected.": "%s é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é.",
+  "Reset Settings": "Réinitialiser les paramètres",
+  "Download doesn\'t work? You can try right-click \"Download\" button, select \"Save link as...\".": "Le téléchargement ne fonctionne pas? Vous pouvez essayer de cliquer avec le bouton droit sur le bouton \"Télécharger\", puis sélectionner \"Enregistrer le lien sous...\".",
+  "Theme": "Thème",
+  "Dark": "Sombre",
+  "Light": "Clair",
+  "System": "Système",
+  "Target Directory" : "Répertoire cible",
+  "uppy": uppyLocaleFr
+}

+ 89 - 0
src/components/FileManager/locales/he.js

@@ -0,0 +1,89 @@
+import uppyLocaleHe from '@uppy/locales/lib/he_IL.js';
+
+export default {
+  "Language": "שפה",
+  "Create": "יצירה",
+  "Close": "סגירה",
+  "Cancel": "ביטול",
+  "Save": "שמירה",
+  "Edit": "עריכה",
+  "Crop": "חיתוך",
+  "New Folder": "תיקייה חדשה",
+  "New File": "קובץ חדש",
+  "Rename": "שינוי שם",
+  "Delete": "מחיקה",
+  "Upload": "העלאה",
+  "Download": "הורדה",
+  "Archive": "לדחוס",
+  "Unarchive": "לחלץ",
+  "Open": "פתיחה",
+  "Open containing folder": "פתיחת מיקום קובץ",
+  "Refresh": "רענון",
+  "Preview": "תצוגה מקדימה",
+  "Toggle Full Screen": "שינוי מצב מסך מלא",
+  "Change View": "שינוי תצוגה",
+  "Storage": "אחסון",
+  "Go up a directory": "מעבר תיקייה אחת למעלה",
+  "Search anything..": "חיפוש...",
+  "Name": "שם",
+  "Size": "גודל",
+  "Date": "תאריך",
+  "Filepath": "נתיב קובץ",
+  "About": "אודות",
+  "Folder Name": "שם התיקייה",
+  "File Name": "שם הקובץ",
+  "Move files": "העברת קבצים",
+  "Yes, Move!": "כן, להעביר!",
+  "Delete files": "מחיקת קבצים",
+  "Yes, Delete!": "כן, למחוק!",
+  "Upload Files": "העלאת קבצים",
+  "No files selected!": "לא נבחרו קבצים",
+  "Select Files": "בחירת קבצים",
+  "Archive the files": "דחיסת קבצים אלו",
+  "Unarchive the files": "חילוץ קבצים אלו",
+  "The archive will be unarchived at": "הארכיןם יוחלץ ל:",
+  "Archive name. (.zip file will be created)": "שם הארכיון. (סיומת .zip תווסף)",
+  "Vuefinder is a file manager component for vue 3.": "Vuefinder הינו רכיב מנהל קבצים עבור vue 3",
+  "Create a new folder": "יצירת תיקייה חדשה",
+  "Create a new file": "יצירת קובץ חדש",
+  "Are you sure you want to delete these files?": "האם בטוח למחוק קבצים אלה?",
+  "This action cannot be undone.": "לא ניתן לבטל פעולה זו.",
+  "Search results for": "תוצאות חיפוש עבור",
+  "%s item(s) selected.": "%s פריט(ים) נבחרו.",
+  "%s is renamed.": "השם של %s השתנה.",
+  "This is a readonly storage.": "זהו אחסון לקריאה בלבד.",
+  "%s is created.": "%s נוצר",
+  "Files moved.": "הקבצים הועברו.",
+  "Files deleted.": "הקבצים נמחקו",
+  "The file unarchived.": "הקבצים חולצו.",
+  "The file(s) archived.": "הקובצים נדחסו.",
+  "Updated.": "התעדכן",
+  "No search result found.": "לא נמצאו תוצאות לחיפוש.",
+  "Are you sure you want to move these files?": "האם בטוח להעביר קבצים אלו?",
+  "File Size": "גודל קובץ",
+  "Last Modified": "תאריך שינוי",
+  "Drag&Drop: on": "מצב גרירה: פעיל",
+  "Drag&Drop: off": "מצב גרירה: כבוי",
+  "Select Folders": "Select Folders",
+  "Clear all": "נקה הכל",
+  "Clear only successful": "נקה רק הצלחות",
+  "Drag and drop the files/folders to here or click here.": "גרור ושחרר את הקבצים/תיקיות לכאן או לחץ כאן.",
+  "Release to drop these files.": "שחרר כדי לשחרר את הקבצים.",
+  "Canceled": "בוטל",
+  "Done": "הסתיים",
+  "Network Error, Unable establish connection to the server or interrupted.": "שגיאת רשת, לא ניתן להתחבר לשרת או החיבור נקטע.",
+  "Pending upload": "העלאה ממתינה",
+  "Please select file to upload first." : "בחר קובץ להעלאה תחילה.",
+  "About %s": "אודות %s",
+  "Settings": "הגדרות",
+  "Use Metric Units": "השתמש ביחידות מטריות",
+  "Saved.": "נשמר",
+  "Reset Settings": "איפוס הגדרות",
+  "Download doesn\'t work? You can try right-click \"Download\" button, select \"Save link as...\".": "ההורדה לא עובדה? ניתן לנסות ללחוץ ימינה על כפתור \"הורדה\", ולבחור \"שמור קישור בשם...\".",
+  "Theme": "ערכת נושא",
+  "Dark": "כהה",
+  "Light": "בהיר",
+  "System": "מערכת",
+  "Target Directory" : "תיקיית יעד",
+  "uppy": uppyLocaleHe
+}

+ 89 - 0
src/components/FileManager/locales/hi.js

@@ -0,0 +1,89 @@
+import uppyLocaleHi from '@uppy/locales/lib/hi_IN.js';
+
+export default {
+  "Language": "भाषा",
+  "Create": "बनाएँ",
+  "Close": "बंद करें",
+  "Cancel": "रद्द करें",
+  "Save": "सहेजें",
+  "Edit": "संपादित करें",
+  "Crop": "कटवा दें",
+  "New Folder": "नया फ़ोल्डर",
+  "New File": "नया फ़ाइल",
+  "Rename": "नाम बदलें",
+  "Delete": "हटाएं",
+  "Upload": "अपलोड करें",
+  "Download": "डाउनलोड करें",
+  "Archive": "आर्काइव",
+  "Unarchive": "अनआर्काइव",
+  "Open": "खोलें",
+  "Open containing folder": "धारक फोल्डर खोलें",
+  "Refresh": "ताजगी करें",
+  "Preview": "पूर्वावलोकन",
+  "Toggle Full Screen": "पूर्ण स्क्रीन टॉगल करें",
+  "Change View": "दृश्य बदलें",
+  "Storage" : "संग्रहण",
+  "Go up a directory": "एक निर्देशिका ऊपर जाएं",
+  "Search anything..": "कुछ भी खोजें..",
+  "Name": "नाम",
+  "Size": "आकार",
+  "Date": "तारीख",
+  "Filepath": "फ़ाइल पथ",
+  "About": "के बारे में",
+  "Folder Name": "फ़ोल्डर नाम",
+  "File Name": "फ़ाइल नाम",
+  "Move files": "फ़ाइलें ले जाएं",
+  "Yes, Move!": "हां, ले जाएं!",
+  "Delete files": "फ़ाइलें हटाएं",
+  "Yes, Delete!": "हां, हटाएं!",
+  "Upload Files" : "फ़ाइलें अपलोड करें",
+  "No files selected!": "कोई फ़ाइलें चयनित नहीं हैं!",
+  "Select Files": "फ़ाइलें चयनित करें",
+  "Archive the files": "फ़ाइलों को संग्रहित करें",
+  "Unarchive the files": "फ़ाइलों को संग्रहण से निकालें",
+  "The archive will be unarchived at": "संग्रहण इस तिथि पर से निकाला जाएगा",
+  "Archive name. (.zip file will be created)": "संग्रहण का नाम (.zip फ़ाइल बनाई जाएगी)",
+  "Vuefinder is a file manager component for vue 3.": "Vuefinder व्यू 3 के लिए एक फ़ाइल प्रबंधक घटक है।",
+  "Create a new folder": "नया फ़ोल्डर बनाएं",
+  "Create a new file": "नई फ़ाइल बनाएं",
+  "Are you sure you want to delete these files?": "क्या आप वाकई इन फ़ाइलों को हटाना चाहते हैं?",
+  "This action cannot be undone.": "इस क्रिया को पूर्वस्थित नहीं किया जा सकता है।",
+  "Search results for": "के लिए खोज परिणाम",
+  "%s item(s) selected.": "%s आइटम(आइटम) चयनित।",
+  "%s is renamed.": "%s का नाम बदला गया है।",
+  "This is a readonly storage.": "यह एक केवल पठनीय संग्रह है।",
+  "%s is created.": "%s बनाया गया है।",
+  "Files moved.": "फ़ाइलें मूव की गईं।",
+  "Files deleted.": "फ़ाइलें हटा दी गईं।",
+  "The file unarchived.": "फ़ाइल अनआर्काइव की गई है।",
+  "The file(s) archived.": "फ़ाइल(फ़ाइलें) आर्काइव की गई हैं।",
+  "Updated.": "अद्यतित।",
+  "No search result found.": "कोई खोज परिणाम नहीं मिले।",
+  "Are you sure you want to move these files?" : "क्या आप वाकई इन फ़ाइलों को स्थानांतरित करना चाहते हैं?",
+  "File Size": "फ़ाइल का आकार",
+  "Last Modified": "अंतिम संशोधित",
+  "Drag&Drop: on": "ड्रैग और ड्रॉप: चालू",
+  "Drag&Drop: off": "ड्रैग और ड्रॉप: बंद",
+  "Select Folders": "फ़ोल्डर चुनें",
+  "Clear all": "सब कुछ साफ़ करें",
+  "Clear only successful": "केवल सफल साफ़ करें",
+  "Drag and drop the files/folders to here or click here.": "फ़ाइलें/फ़ोल्डर्स यहाँ खींचें या यहाँ क्लिक करें।",
+  "Release to drop these files.": "इन फ़ाइलों को छोड़ने के लिए रिलीज़ करें।",
+  "Canceled": "रद्द कर दिया गया",
+  "Done": "हो गया",
+  "Network Error, Unable establish connection to the server or interrupted.": "नेटवर्क त्रुटि, सर्वर से कनेक्शन स्थापित नहीं कर सकता या रुका हुआ है।",
+  "Pending upload": "अपलोड प्रलंबित",
+  "Please select file to upload first." : "कृपया पहले अपलोड करने के लिए फ़ाइल का चयन करें।",
+  "About %s": "के बारे में %s",
+  "Settings": "सेटिंग्स",
+  "Use Metric Units": "मीट्रिक इकाइयों का उपयोग करें",
+  "Saved.": "सहेजा गया।",
+  "Reset Settings": "सेटिंग्स रीसेट करें",
+  "Download doesn\'t work? You can try right-click \"Download\" button, select \"Save link as...\".": "डाउनलोड काम नहीं करता? आप सीधे दायाँ क्लिक करके \"डाउनलोड\" बटन पर क्लिक करके \"लिंक को सहेजें...\" चुन सकते हैं।",
+  "Theme": "थीम",
+  "Dark": "डार्क",
+  "Light": "लाइट",
+  "System": "सिस्टम",
+  "Target Directory" : "लक्षित निर्देशिका",
+  "uppy": uppyLocaleHi
+}

+ 89 - 0
src/components/FileManager/locales/ru.js

@@ -0,0 +1,89 @@
+import uppyLocaleRu from '@uppy/locales/lib/ru_RU.js';
+
+export default {
+  "Language": "Язык",
+  "Create": "Создать",
+  "Close": "Закрыть",
+  "Cancel": "Отмена",
+  "Save": "Сохранить",
+  "Edit": "Изменить",
+  "Crop": "Обрезать",
+  "New Folder": "Новая папка",
+  "New File": "Новый файл",
+  "Rename": "Переименовать",
+  "Delete": "Удалить",
+  "Upload": "Загрузить",
+  "Download": "Скачать",
+  "Archive": "Архивировать",
+  "Unarchive": "Разархивировать",
+  "Open": "Открыть",
+  "Open containing folder": "Открыть расположение",
+  "Refresh": "Обновить",
+  "Preview": "Предпросмотр",
+  "Toggle Full Screen": "Полный экран",
+  "Change View": "Изменить вид",
+  "Storage" : "Хранилище",
+  "Go up a directory": "Вверх",
+  "Search anything..": "Поиск...",
+  "Name": "Название",
+  "Size": "Размер",
+  "Date": "Дата",
+  "Filepath": "Путь до файла",
+  "About": "О компоненте",
+  "Folder Name": "Название папки",
+  "File Name": "Название файла",
+  "Move files": "Переместить файлы",
+  "Yes, Move!": "Переместить",
+  "Delete files": "Удалить файлы",
+  "Yes, Delete!": "Удалить",
+  "Upload Files": "Загрузить файлы",
+  "No files selected!": "Файлы не выбраны!",
+  "Select Files": "Выбрать файлы",
+  "Archive the files": "Архивировать файлы",
+  "Unarchive the files": "Разархивировать файлы",
+  "The archive will be unarchived at": "Архив будет разархивирован в",
+  "Archive name. (.zip file will be created)": "Название архива. (будет создан .zip файл)",
+  "Vuefinder is a file manager component for vue 3.": "Vuefinder - файловый менеджер, компонент для vue 3.",
+  "Create a new folder": "Создать новую папку",
+  "Create a new file": "Создать новый файл",
+  "Are you sure you want to delete these files?": "Вы уверены, что хотите удалить эти файлы?",
+  "This action cannot be undone.": "Это действия нельзя отменить.",
+  "Search results for": "Результаты поиска по",
+  "%s item(s) selected.": "%s выбраны.",
+  "%s is renamed.": "%s переименован.",
+  "This is a readonly storage.": "Данное хранилище только для чтения.",
+  "%s is created.": "%s создан.",
+  "Files moved.": "Файлы перемещены.",
+  "Files deleted.": "Файлы удалены.",
+  "The file unarchived.": "Файл разархивирован",
+  "The file(s) archived.": "Файл(-ы) архивированы",
+  "Updated.": "Обновлено.",
+  "No search result found.": "Ничего не найдено",
+  "Are you sure you want to move these files?": "Вы уверены, что хотите переместить эти файлы?",
+  "File Size": "Размер файла",
+  "Last Modified": "Последнее изменение",
+  "Drag&Drop: on": "Drag&Drop: on",
+  "Drag&Drop: off": "Drag&Drop: off",
+  "Select Folders": "Выбрать папки",
+  "Clear all": "Очистить все",
+  "Clear only successful": "Очистить только успешные",
+  "Drag and drop the files/folders to here or click here.": "Перетащите файлы/папки сюда или нажмите здесь.",
+  "Release to drop these files.": "Отпустите, чтобы отпустить эти файлы.",
+  "Canceled": "Отменено",
+  "Done": "Готово",
+  "Network Error, Unable establish connection to the server or interrupted.": "Ошибка сети, не удалось установить соединение с сервером или соединение было прервано.",
+  "Pending upload": "Ожидание загрузки",
+  "Please select file to upload first." : "Пожалуйста, выберите файл для загрузки.",
+  "About %s": "О компоненте %s",
+  "Settings": "Настройки",
+  "Use Metric Units": "Использовать метрические единицы",
+  "Saved.": "Сохранено.",
+  "Reset Settings": "Сбросить настройки",
+  "Download doesn\'t work? You can try right-click \"Download\" button, select \"Save link as...\".": "Загрузка не работает? Вы можете попробовать щелкнуть правой кнопкой мыши по кнопке \"Скачать\", выбрать \"Сохранить ссылку как...\".",
+  "Theme": "Тема",
+  "Dark": "Темный",
+  "Light": "Светлый",
+  "System": "Система",
+  "Target Directory" : "Целевая директория",
+  "uppy": uppyLocaleRu
+}

+ 89 - 0
src/components/FileManager/locales/sv.js

@@ -0,0 +1,89 @@
+import uppyLocaleSv from '@uppy/locales/lib/sv_SE.js';
+
+export default {
+  "Language": "Språk",
+  "Create": "Skapa",
+  "Close": "Stäng",
+  "Cancel": "Avbryt",
+  "Save": "Spara",
+  "Edit": "Redigera",
+  "Crop": "Beskära",
+  "New Folder": "Ny mapp",
+  "New File": "Ny fil",
+  "Rename": "Byt namn",
+  "Delete": "Radera",
+  "Upload": "Ladda upp",
+  "Download": "Ladda ner",
+  "Archive": "Arkivera",
+  "Unarchive": "Avarkivera",
+  "Open": "Öppna",
+  "Open containing folder": "Öppna innehållande mapp",
+  "Refresh": "Uppdatera",
+  "Preview": "Förhandsgranska",
+  "Toggle Full Screen": "Växla till helskärm",
+  "Change View": "Ändra vy",
+  "Storage" : "Lagring",
+  "Go up a directory": "Gå upp en katalog",
+  "Search anything..": "Sök överallt..",
+  "Name": "Namn",
+  "Size": "Storlek",
+  "Date": "Datum",
+  "Filepath": "Filväg",
+  "About": "Om",
+  "Folder Name": "Mappnamn",
+  "File Name": "Filnamn",
+  "Move files": "Flytta filer",
+  "Yes, Move!": "Ja, flytta!",
+  "Delete files": "Radera filer",
+  "Yes, Delete!": "Ja, radera!",
+  "Upload Files" : "Ladda upp filer",
+  "No files selected!": "Inga filer valda!",
+  "Select Files": "Välj filer",
+  "Archive the files": "Arkivera filer",
+  "Unarchive the files": "Avarkivera filer",
+  "The archive will be unarchived at": "Arkivet kommer att avarkiveras i",
+  "Archive name. (.zip file will be created)": "Arkivnamn. (.zip-fil kommer att skapas)",
+  "Vuefinder is a file manager component for vue 3.": "Vuefinder är en filhanteringskomponent för Vue 3.",
+  "Create a new folder": "Skapa en ny mapp",
+  "Create a new file": "Skapa en ny fil",
+  "Are you sure you want to delete these files?": "Är du säker på att du vill radera dessa filer?",
+  "This action cannot be undone.": "Denna åtgärd kan inte ångras.",
+  "Search results for" : "Sökresultat för",
+  "%s item(s) selected.": "%s objekt valda.",
+  "%s is renamed." : "%s har fått ett nytt namn.",
+  "This is a readonly storage." : "Detta är en skrivskyddad lagring.",
+  "%s is created." : "%s har skapats.",
+  "Files moved." : "Filerna har flyttats.",
+  "Files deleted." : "Filerna har raderats.",
+  "The file unarchived." : "Filen har avarkiverats.",
+  "The file(s) archived." : "Filen/filerna har arkiverats.",
+  "Updated." : "Uppdaterad.",
+  "No search result found." : "Inget sökresultat hittades.",
+  "Are you sure you want to move these files?" : "Är du säker på att du vill flytta dessa filer?",
+  "File Size": "Filstorlek",
+  "Last Modified": "Senast ändrad",
+  "Drag&Drop: on": "Dra och släpp: på",
+  "Drag&Drop: off": "Dra och släpp: av",
+  "Select Folders": "Välj mappar",
+  "Clear all": "Rensa alla",
+  "Clear only successful": "Rensa endast lyckade",
+  "Drag and drop the files/folders to here or click here.": "Dra och släpp filerna/mapparna hit eller klicka här.",
+  "Release to drop these files.": "Släpp för att lägga till dessa filer.",
+  "Canceled": "Avbruten",
+  "Done": "Klar",
+  "Network Error, Unable establish connection to the server or interrupted.": "Nätverksfel, kan inte upprätta anslutning till servern eller avbruten.",
+  "Pending upload": "Väntar på uppladdning",
+  "Please select file to upload first." : "Välj fil att ladda upp först.",
+  "About %s": "Om %s",
+  "Settings": "Inställningar",
+  "Use Metric Units": "Använd metriska enheter",
+  "Saved.": "Sparad.",
+  "Reset Settings": "Återställ inställningar",
+  "Download doesn\'t work? You can try right-click \"Download\" button, select \"Save link as...\".": "Fungerar inte nedladdningen? Du kan högerklicka på \"Ladda ner\"-knappen, välja \"Spara länk som...\".",
+  "Theme": "Tema",
+  "Dark": "Mörk",
+  "Light": "Ljus",
+  "System": "System",
+  "Target Directory" : "Mål katalog",
+  "uppy": uppyLocaleSv
+}

+ 89 - 0
src/components/FileManager/locales/tr.js

@@ -0,0 +1,89 @@
+import uppyLocaleTr from '@uppy/locales/lib/tr_TR.js';
+
+export default {
+  "Language": "Dil",
+  "Create": "Oluştur",
+  "Close": "Kapat",
+  "Cancel": "İptal",
+  "Save": "Kaydet",
+  "Edit": "Düzenle",
+  "Crop": "Kes",
+  "New Folder": "Yeni Klasör",
+  "New File": "Yeni Dosya",
+  "Rename": "Yeniden Adlandır",
+  "Delete": "Sil",
+  "Upload": "Karşıya Yükle",
+  "Download": "İndir",
+  "Archive": "Arşivle",
+  "Unarchive": "Arşivden çıkar",
+  "Open": "Aç",
+  "Open containing folder": "İçeren klasörü aç",
+  "Refresh": "Yenile",
+  "Preview": "Önizleme",
+  "Toggle Full Screen": "Tam ekran",
+  "Change View": "Görünümü değiştir",
+  "Storage": "Depo",
+  "Go up a directory": "Yukarı git",
+  "Search anything..": "Herhangi bir şey ara..",
+  "Name": "Ad",
+  "Size": "Boyut",
+  "Date": "Tarih",
+  "Filepath": "Dosya yolu",
+  "About": "Hakkında",
+  "Folder Name": "Klasör Adı",
+  "File Name": "Dosya Adı",
+  "Move files": "Dosyaları taşı",
+  "Yes, Move!": "Evet, Taşı!",
+  "Delete files": "Dosyaları Sil",
+  "Yes, Delete!": "Evet, Sil!",
+  "Upload files": "Dosyaları Yükle",
+  "No files selected!": "Dosya Seçilmedi!",
+  "Select Files": "Dosyaları Seç",
+  "Archive the files": "Dosyaları arşivleyin",
+  "Unarchive the files": "Dosyaları arşivden çıkarın",
+  "The archive will be unarchived at": "Arşiv buraya çıkarılacak: ",
+  "Archive name. (.zip file will be created)": "Archive adı. (.zip dosyası oluşturulacak)",
+  "Vuefinder is a file manager component for vue 3.": "Vuefinder, vue 3 için bir web dosya yönetim bileşenidir.",
+  "Create a new folder": "Yeni bir klasör oluşturun",
+  "Create a new file": "Yeni bir dosya oluşturun",
+  "Are you sure you want to delete these files?": "Bu dosyaları silmek istediğinize emin misiniz?",
+  "This action cannot be undone.": "Bu işlem geri alınamaz.",
+  "Search results for": "Arama sonuçları:",
+  "%s item(s) selected.": "%s dosya seçildi.",
+  "%s is renamed." : "%s yeniden adlandırılmıştır.",
+  "This is a readonly storage." : "Bu salt okunur bir depolama alanıdır.",
+  "%s is created." : "%s başarıyla oluşturulmuştur.",
+  "Files moved." : "Dosyalar taşındı.",
+  "Files deleted." : "Dosyalar silindi.",
+  "The file is unarchived." : "Dosya arşivden çıkarıldı.",
+  "The file(s) is archived." : "Dosyalar arşivlendi.",
+  "Updated." : "Güncellendi.",
+  "No search result found." : "Arama sonucu bulunamadı.",
+  "Are you sure you want to move these files?" : "Bu dosyaları taşımak istediğinize emin misiniz?",
+  "File Size": "Dosya Boyutu",
+  "Last Modified": "Son Değişiklik",
+  "Drag&Drop: on": "Sürükle&Bırak: etkin",
+  "Drag&Drop: off": "Sürükle&Bırak: devre dışı",
+  "Select Folders": "Klasörleri Seç",
+  "Clear all": "Hepsini Temizle",
+  "Clear only successful": "Başarılı Olanları Temizle",
+  "Drag and drop the files/folders to here or click here.": "Dosyaları/klasörleri buraya sürükleyin veya buraya tıklayın.",
+  "Release to drop these files.": "Dosyaları eklemek için serbest bırakın.",
+  "Canceled": "İptal edildi",
+  "Done": "Tamamlandı",
+  "Network Error, Unable establish connection to the server or interrupted.": "Ağ Hatası, Sunucuya bağlantı kurulamıyor veya kesiliyor.",
+  "Pending upload": "Bekliyor",
+  "Please select file to upload first.": "Lütfen önce yüklenecek dosyayı seçin.",
+  "About %s": "%s Hakkında",
+  "Settings": "Ayarlar",
+  "Use Metric Units": "Metrik Birimleri Kullan",
+  "Saved.": "Kaydedildi.",
+  "Reset Settings": "Ayarları Sıfırla",
+  "Download doesn\'t work? You can try right-click \"Download\" button, select \"Save link as...\".": "Download çalışmıyor mu?  \"İndir\" düğmesine sağ tıklayarak, \"Farklı kaydet...\" seçebilirsiniz.",
+  "Theme": "Tema",
+  "Dark": "Koyu",
+  "Light": "Açık",
+  "System": "Sistem",
+  "Target Directory" : "Hedef Klasör",
+  "uppy": uppyLocaleTr,
+}

+ 89 - 0
src/components/FileManager/locales/zhCN.js

@@ -0,0 +1,89 @@
+import uppyLocalezhCN from '@uppy/locales/lib/zh_CN.js';
+
+export default {
+  "Language": "语言",
+  "Create": "创建",
+  "Close": "关闭",
+  "Cancel": "取消",
+  "Save": "保存",
+  "Edit": "编辑",
+  "Crop": "裁切",
+  "New Folder": "新文件夹",
+  "New File": "新文件",
+  "Rename": "重命名",
+  "Delete": "删除",
+  "Upload": "上传",
+  "Download": "下载",
+  "Archive": "压缩",
+  "Unarchive": "解压缩",
+  "Open": "打开",
+  "Open containing folder": "打开对应的文件夹",
+  "Refresh": "刷新",
+  "Preview": "预览",
+  "Toggle Full Screen": "切换到全屏",
+  "Change View": "切换视图",
+  "Storage" : "存储",
+  "Go up a directory": "上一级目录",
+  "Search anything..": "搜索..",
+  "Name": "名称",
+  "Size": "大小",
+  "Date": "日期",
+  "Filepath": "文件路径",
+  "About": "关于",
+  "Folder Name": "文件夹名称",
+  "File Name": "文件名称",
+  "Move files": "移动文件",
+  "Yes, Move!": "确定,移动!",
+  "Delete files": "删除文件",
+  "Yes, Delete!": "确定,删除!",
+  "Upload Files" : "上传文件",
+  "No files selected!": "未选择文件!",
+  "Select Files": "选择文件",
+  "Archive the files": "压缩文件",
+  "Unarchive the files": "解压缩文件",
+  "The archive will be unarchived at": "此压缩文件将解压到",
+  "Archive name. (.zip file will be created)": "压缩包名称。(将创建 .zip 文件)",
+  "Vuefinder is a file manager component for vue 3.": "Vuefinder 是 Vue 3 的一个文件管理组件。",
+  "Create a new folder": "创建一个新文件夹",
+  "Create a new file": "创建一个新文件",
+  "Are you sure you want to delete these files?": "您确定要删除这些文件吗?",
+  "This action cannot be undone.": "此操作不能撤销。",
+  "Search results for" : "搜索结果为",
+  "%s item(s) selected.": "%s 个文件 已选择。",
+  "%s is renamed." : "%s 已重命名。",
+  "This is a readonly storage." : "这是只读存储。",
+  "%s is created." : "%s 已创建。",
+  "Files moved." : "文件已移动。",
+  "Files deleted." : "文件已删除。",
+  "The file unarchived." : "文件已解压。",
+  "The file(s) archived." : "文件已压缩。",
+  "Updated." : "已更新。",
+  "No search result found." : "未找到搜索结果。",
+  "Are you sure you want to move these files?" : "您确定要移动这些文件吗?",
+  "File Size": "文件大小",
+  "Last Modified": "文件修改时间",
+  "Drag&Drop: on": "拖拽: 开",
+  "Drag&Drop: off": "拖拽: 关",
+  "Select Folders": "选择文件夹",
+  "Clear all": "清除全部",
+  "Clear only successful": "清除已成功上传的",
+  "Drag and drop the files/folders to here or click here.": "拖拽或点击此处上传文件/文件夹。",
+  "Release to drop these files.": "放开后添加这些文件。",
+  "Canceled": "已取消",
+  "Done": "已完成",
+  "Network Error, Unable establish connection to the server or interrupted.": "网络错误,无法连接到服务器或连接被意外中断。",
+  "Pending upload": "待上传",
+  "Please select file to upload first." : "请先选择要上传的文件。",
+  "About %s": "关于 %s",
+  "Settings": "设置",
+  "Use Metric Units": "使用公制单位",
+  "Saved.": "已保存。",
+  "Reset Settings": "重置设置",
+  "Download doesn\'t work? You can try right-click \"Download\" button, select \"Save link as...\".": "下载不管用?您可以尝试右键点击“下载”按钮,选择“链接另存为...”。",
+  "Theme": "主题",
+  "Dark": "深色",
+  "Light": "浅色",
+  "System": "系统",
+  "Target Directory": "目标目录",
+  "uppy": uppyLocalezhCN
+}

+ 89 - 0
src/components/FileManager/locales/zhTW.js

@@ -0,0 +1,89 @@
+import uppyLocalezhTW from '@uppy/locales/lib/zh_TW.js';
+
+export default {
+  "Language": "語言",
+  "Create": "建立",
+  "Close": "關閉",
+  "Cancel": "取消",
+  "Save": "储存",
+  "Edit": "編輯",
+  "Crop": "裁切",
+  "New Folder": "新資料夾",
+  "New File": "新檔案",
+  "Rename": "改名",
+  "Delete": "刪除",
+  "Upload": "上傳",
+  "Download": "下載",
+  "Archive": "壓縮",
+  "Unarchive": "解壓縮",
+  "Open": "打開",
+  "Open containing folder": "打開對應的資料夾",
+  "Refresh": "重新整理",
+  "Preview": "預覽",
+  "Toggle Full Screen": "切換全螢幕",
+  "Change View": "變更視圖",
+  "Storage" : "儲存",
+  "Go up a directory": "上一個目錄",
+  "Search anything..": "搜尋..",
+  "Name": "名稱",
+  "Size": "大小",
+  "Date": "日期",
+  "Filepath": "檔案路徑",
+  "About": "關於",
+  "Folder Name": "資料夾名稱",
+  "File Name": "檔案名称",
+  "Move files": "移動檔案",
+  "Yes, Move!": "確定,移動!",
+  "Delete files": "刪除檔案",
+  "Yes, Delete!": "確定,刪除!",
+  "Upload Files" : "上傳檔案",
+  "No files selected!": "未選取檔案!",
+  "Select Files": "選擇檔案",
+  "Archive the files": "壓縮檔案",
+  "Unarchive the files": "解壓縮檔案",
+  "The archive will be unarchived at": "此壓縮檔案將解壓到",
+  "Archive name. (.zip file will be created)": "壓縮檔案名稱。(將創建 .zip 檔案)",
+  "Vuefinder is a file manager component for vue 3.": "Vuefinder 是 Vue 3 的一個檔案管理員組件。",
+  "Create a new folder": "創建一個新資料夾",
+  "Create a new file": "創建一個新檔案",
+  "Are you sure you want to delete these files?": "您確定要刪除這些檔案嗎?",
+  "This action cannot be undone.": "此操作不能撤銷。",
+  "Search results for" : "搜尋結果為",
+  "%s item(s) selected.": "%s 個檔案 已選取。",
+  "%s is renamed." : "%s 已重新命名。",
+  "This is a readonly storage." : "這是只讀存儲器。",
+  "%s is created." : "%s 已創建。",
+  "Files moved." : "檔案已移動。",
+  "Files deleted." : "檔案已刪除。",
+  "The file unarchived." : "檔案已解壓。",
+  "The file(s) archived." : "檔案已壓縮。",
+  "Updated." : "已更新。",
+  "No search result found." : "未能搜尋到結果。",
+  "Are you sure you want to move these files?" : "您確定要移動這些檔案嗎?",
+  "File Size": "檔案大小",
+  "Last Modified": "檔案修改時間",
+  "Drag&Drop: on": "拖拽:開啟",
+  "Drag&Drop: off": "拖拽:關閉",
+  "Select Folders": "選擇資料夾",
+  "Clear all": "全部清除",
+  "Clear only successful": "僅清除成功上傳的",
+  "Drag and drop the files/folders to here or click here.": "拖曳或點擊此處上傳檔案/資料夾。",
+  "Release to drop these files.": "放開後添加這些文件。",
+  "Canceled": "已取消",
+  "Done": "已完成",
+  "Network Error, Unable establish connection to the server or interrupted.": "網路錯誤,無法連線到伺服器或連線被意外中斷。",
+  "Pending upload": "待上傳",
+  "Please select file to upload first." : "請先選擇要上傳的檔案。",
+  "About %s": "關於 %s",
+  "Settings": "設定",
+  "Use Metric Units": "使用公制單位",
+  "Saved.": "已儲存。",
+  "Reset Settings": "重置設定",
+  "Download doesn\'t work? You can try right-click \"Download\" button, select \"Save link as...\".": "下載不管用?您可以嘗試右鍵點擊“下載”按鈕,選擇“另存連結為...”。",
+  "Theme": "主題",
+  "Dark": "深色",
+  "Light": "淺色",
+  "System": "系統",
+  "Target Directory": "目標目錄",
+  "uppy": uppyLocalezhTW
+}

+ 11 - 0
src/components/FileManager/modals.js

@@ -0,0 +1,11 @@
+export { default as ModalDelete } from './components/modals/ModalDelete.vue';
+// export {default as ModalMessage} from './components/modals/ModalMessage.vue';
+export { default as ModalNewFolder } from './components/modals/ModalNewFolder.vue';
+// export {default as ModalNewFile} from './components/modals/ModalNewFile.vue';
+export { default as ModalPreview } from './components/modals/ModalPreview.vue';
+export { default as ModalRename } from './components/modals/ModalRename.vue';
+export { default as ModalUpload } from './components/modals/ModalUpload.vue';
+// export {default as ModalArchive} from './components/modals/ModalArchive.vue';
+// export {default as ModalUnarchive} from './components/modals/ModalUnarchive.vue';
+export { default as ModalMove } from './components/modals/ModalMove.vue'
+// export {default as ModalAbout} from './components/modals/ModalAbout.vue';

+ 243 - 0
src/components/FileManager/utils/ajax.js

@@ -0,0 +1,243 @@
+export const csrf = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
+
+/**
+ * @typedef RequestConfig
+ * @property {String} baseUrl
+ * @property {Record<String,String>=} headers Additional headers
+ * @property {Record<String,?String>=} params Additional query params
+ * @property {Record<String,*>=} 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<String,String>} headers
+ * @property {Record<String,?String>} params
+ * @property {Record<String,?String>|FormData|null} body
+ */
+/**
+ * @typedef RequestTransformResult
+ * @property {String=} url
+ * @property {'get'|'post'|'put'|'patch'|'delete'=} method
+ * @property {Record<String, String>=} headers
+ * @property {Record<String, ?String>=} params
+ * @property {Record<String,?String>|FormData=} body
+ */
+/**
+ * @typedef RequestTransformResultInternal
+ * @property {String} url
+ * @property {'get'|'post'|'put'|'patch'|'delete'} method
+ * @property {Record<String, String>} headers
+ * @property {Record<String, ?String>} params
+ * @property {Record<String,?String>|FormData=} body
+ */
+/**
+ * @callback RequestTransformer
+ * @param {RequestTransformParams} request
+ * @returns {RequestTransformResult}
+ */
+
+/**
+ * Base http requester
+ */
+export class Requester {
+    /** @type {RequestConfig} */
+    config
+
+    /** @param {RequestConfig} config */
+    constructor(config) {
+        this.config = config;
+    }
+
+    /** @type {RequestConfig} */
+    get config() {
+        return this.config;
+    }
+
+    /**
+     * Transform request params
+     * @param {Object} input
+     * @param {String} input.url
+     * @param {'get'|'post'|'put'|'patch'|'delete'} input.method
+     * @param {Record<String,String>=} input.headers
+     * @param {Record<String,?String>=} input.params
+     * @param {Record<String,?String>|FormData=} input.body
+     * @return {RequestTransformResultInternal}
+     */
+    transformRequestParams(input ) {
+        console.log('input',input)
+        const config = this.config;
+        const ourHeaders = {};
+        if (csrf != null && csrf !== '') {
+            ourHeaders[config.xsrfHeaderName] = csrf;
+        }
+        /** @type {Record<String, String>} */
+        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<String,*>|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
+    }
+
+    /**
+     * Get download url
+     * @param {String} adapter
+     * @param {String} node
+     * @param {String} node.path
+     * @param {String=} node.url
+     * @return {String}
+     */
+    getDownloadUrl(adapter, node) {
+        const config = this.config
+        if (node.href != null) {
+            return `${config.baseUrl}${node.href}`
+        }
+        return ''
+    }
+
+    /**
+     * Get preview url
+     * @param {String} adapter
+     * @param {String} node
+     * @param {String} node.path
+     * @param {String=} node.url
+     * @return {String}
+     */
+    getPreviewUrl(adapter, node) {
+        const config = this.config
+        if (node.href != null) {
+            return `${config.baseUrl}${node.href}?t=${Math.random()}`
+        }
+        return ''
+    }
+
+    /**
+     * Send request
+     * @param {Object} input
+     * @param {String} input.url
+     * @param {'get'|'post'|'put'|'patch'|'delete'} input.method
+     * @param {Record<String,String>=} input.headers
+     * @param {Record<String,?String>=} input.params
+     * @param {(Record<String,?String>|FormData|null)=} input.body
+     * @param {'arrayBuffer'|'blob'|'json'|'text'=} input.responseType
+     * @param {AbortSignal=} input.abortSignal
+     * @returns {Promise<(ArrayBuffer|Blob|Record<String,?String>|String|null)>}
+     * @throws {Record<String,?String>|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
+                if(input.body.has('file')){
+                    newBody=input.body.get('file')
+                }else{
+                    newBody = input.body;
+                }
+
+            }
+            init.body = newBody;
+        }
+        const response = await fetch(url, init);
+
+        if (response.ok) {
+             console.log('response',responseType)
+            const result = await response[responseType]();
+
+            return result
+        }
+        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 {}

+ 1 - 0
src/components/FileManager/utils/datetimestring.js

@@ -0,0 +1 @@
+export default (timestamp, locale = null) => new Date(timestamp).toLocaleString(locale ?? navigator.language ?? 'en-US')

+ 30 - 0
src/components/FileManager/utils/filesize.js

@@ -0,0 +1,30 @@
+// Sir I cannot understand what is going on with this function, add docs please.
+export function format(a, b, c, d, e)  {
+    return (b = Math, c = b.log, d = 1024, e = c(a) / c(d) | 0, a / b.pow(d, e)).toFixed(0) + ' ' + (e ? 'KMGTPEZY'[--e] + 'iB' : 'B');
+}
+
+export function metricFormat(a, b, c, d, e)  {
+    return (b = Math, c = b.log, d = 1000, e = c(a) / c(d) | 0, a / b.pow(d, e)).toFixed(0) + ' ' + (e ? 'KMGTPEZY'[--e]  + 'B' : 'B');
+}
+
+/**
+ * Convert human-readable size to bytes.
+ *
+ * <div>Example:
+ * ```javascript
+ * parse("50 MB") // 52428800
+ * parse("50gb")  // 53687091200
+ * parse("50G")   // 53687091200
+ * ```
+ * </div>
+ * @param {String} text
+ * @return {Number} how many bytes
+ */
+export function parse(text) {
+    const powers = {'k': 1, 'm': 2, 'g': 3, 't': 4};
+    const regex = /(\d+(?:\.\d+)?)\s?(k|m|g|t)?b?/i;
+
+    const res = regex.exec(text);
+
+    return res[1] * Math.pow(1024, powers[res[2].toLowerCase()]);
+}

+ 5 - 0
src/components/FileManager/utils/title_shorten.js

@@ -0,0 +1,5 @@
+export default function (title, length = 14) {
+    let pattern = `((?=([\\w\\W]{0,${length}}))([\\w\\W]{${length+1},})([\\w\\W]{8,}))`;
+
+    return title.replace(new RegExp(pattern), '$2..$4');
+};

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 0
src/icons/svg/audio.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 0
src/icons/svg/full-screen-cancel.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 0
src/icons/svg/full-screen.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 0
src/icons/svg/grid.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 0
src/icons/svg/list-view.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 0
src/icons/svg/text-file.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 0
src/icons/svg/unknow-file.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 0
src/icons/svg/zip-file.svg


+ 12 - 5
src/main.js

@@ -53,16 +53,20 @@ if (process.env.NODE_ENV === 'production') {
 // 全局注册echarts、jsonp
 import * as echarts from 'echarts'
 import 'echarts/theme/vintage.js'
-import Grid from 'vant/lib/grid';
+import Grid from 'vant/lib/grid'
 import GridItem from 'vant/lib/grid-item'
 import 'vant/lib/grid/style'
 import 'vant/lib/grid-item/style'
-
+// ace 编辑器
+import ace from 'ace-builds'
+Vue.use(ace)
+import VueFinder from '@/components/FileManager/index.js'
 
 Vue.prototype.$echarts = echarts
 Vue.component('AgGridVue', AgGridVue)
-Vue.component('Grid',Grid)
-Vue.component('GridItem',GridItem)
+Vue.component('Grid', Grid)
+Vue.component('GridItem', GridItem)
+Vue.use(VueFinder)
 Vue.use(vcolorpicker)
 Vue.use(Components)
 Vue.use(scroll)
@@ -91,5 +95,8 @@ new Vue({
   i18n, // 将VueI18n挂载到vue实例上
   router,
   store,
-  render: h => h(App)
+  render: h => h(App),
+  beforeCreate() {
+    Vue.prototype.$bus = this
+  }
 })

+ 27 - 0
src/router/index.js

@@ -281,6 +281,33 @@ export const partRoutes = [
       }
     ]
   },
+    // 文档管理
+  {
+    path: '/file-manager',
+    component: Layout,
+    redirect: '/file-manager/index',
+    children: [
+      {
+        path: 'index',
+        component: () => import('@/views/ncs-file-manager/index'),
+        name: 'file-manager',
+        meta: { title: i18n.t('wu20240322.fileManager'), icon: 'el-icon-folder-opened', noCache: true }
+      }
+    ]
+  },
+  {
+    path: '/article-manager',
+    component: Layout,
+    redirect: '/article-manager/index',
+    children: [
+      {
+        path: 'index',
+        component: () => import('@/views/article-manager/index'),
+        name: 'article-manager',
+        meta: { title: i18n.t('wu20240322.fileManager'), icon: 'el-icon-folder-opened', noCache: true }
+      }
+    ]
+  },
   // 任务
   {
     path: '/task',

+ 8 - 4
src/utils/i18n.js

@@ -3,6 +3,10 @@ import enLang from 'element-ui/lib/locale/lang/en'// 如果使用中文语言包
 import esLang from 'element-ui/lib/locale/lang/es'
 import zhLang from 'element-ui/lib/locale/lang/zh-CN'
 import ruLang from 'element-ui/lib/locale/lang/ru-RU'
+import finderZhLang from '@/components/FileManager/locales/zhCN'
+import finderEnLang from '@/components/FileManager/locales/en'
+import finderEsLang from '@/components/FileManager/locales/sv'
+import finderRuLang from '@/components/FileManager/locales/ru'
 import VueI18n from 'vue-i18n'
 import locale from 'element-ui/lib/locale'
 import Storage from '@/utils/storage'
@@ -14,10 +18,10 @@ const i18n = new VueI18n({
   locale: Storage.getItem('DefaultLanguage') == null ? 'zh' : Storage.getItem('DefaultLanguage'),
   // 添加多语言(每一个语言标示对应一个语言文件)
   messages: {
-    'zh': Object.assign(require('../../languages/zh-CN'), zhLang),
-    'en': Object.assign(require('../../languages/en'), enLang),
-    'es': Object.assign(require('../../languages/es'), esLang),
-    'ru': Object.assign(require('../../languages/ru-RU'), ruLang)
+    'zh': Object.assign(require('../../languages/zh-CN'), zhLang, finderZhLang),
+    'en': Object.assign(require('../../languages/en'), enLang, finderEnLang),
+    'es': Object.assign(require('../../languages/es'), esLang, finderEsLang),
+    'ru': Object.assign(require('../../languages/ru-RU'), ruLang, finderRuLang)
   }
 })
 locale.i18n((key, value) => i18n.t(key, value))

+ 68 - 0
src/views/article-manager/index.vue

@@ -0,0 +1,68 @@
+<template>
+<div >
+    <div id="editjs">
+
+    </div>
+
+
+</div>
+</template>
+
+<script>
+     import EditorJS from '@editorjs/editorjs';
+     import Header from 'editorjs-header-with-alignment';
+     import List from '@editorjs/list';
+     import SimpleImage from "@editorjs/simple-image";
+     // import ImageTool from '@editorjs/image'
+     import Warning from '@editorjs/warning'
+     import TextVariantTune from '@editorjs/text-variant-tune';
+     import Paragraph from 'editorjs-paragraph-with-alignment'
+     import Alert from 'editorjs-alert'
+     import Title from 'title-editorjs'
+     import IndentTune from 'editorjs-indent-tune'
+
+     export default {
+        name: "index",
+        data(){
+            return {
+                editjs:null
+            }
+        },
+        mounted() {
+            this.editjs=new EditorJS({
+                holder:'editjs',
+                placeholder:'点击输入内容...',
+                autofocus: true,
+                style:{
+                  'max-width':'800px'
+                },
+                tools:{
+                    header:{class:Header,inlineToolbar:true,
+                        tunes: ['textVariant']
+                    },
+                    indentTune: IndentTune,
+                    paragraph: { // apply only for the 'paragraph' tool
+                        class:Paragraph,
+                        tunes: ['indentTune','textVariant']
+                    },
+                    list:{class:List,inlineToolbar:true},
+                    image: {class:SimpleImage,inlineToolbar:true},
+                    warning:Warning,
+                    alert:Alert,
+                    textVariant: TextVariantTune,
+                    title:Title,
+                    blockTool:true
+                },
+
+                tunes: ['indentTune']
+
+            })
+
+        }
+    }
+</script>
+
+<style scoped>
+    /deep/.ce-block__content, /deep/ .ce-toolbar__content { max-width: 900px !important; }
+
+</style>

+ 1 - 1
src/views/ncs-device/components/deviceManager.vue

@@ -248,7 +248,7 @@
                     <el-col :span="8">
                         <el-form-item :label="this.$t('deviceManage.controlLineNumber')" prop="control_line_number">
                             <el-input v-model="deviceModel.control_line_number" clearable
-                                      :placeholder="this.$t('deviceManage.controlLineNumber')" type="number"/>
+                                      :placeholder="this.$t('deviceManage.controlLineNumber')" />
                         </el-form-item>
                     </el-col>
                 </el-row>

+ 33 - 0
src/views/ncs-file-manager/index.vue

@@ -0,0 +1,33 @@
+<template>
+    <VueFinder id="vuefinder" :select-button="handleSelectButton" theme="dark" :persist="true" max-file-size="50 GB" path="" request="http://192.168.1.54:5001"></VueFinder>
+</template>
+
+<script>
+    export default {
+        name: "index",
+        data(){
+            return{
+                handleSelectButton:{
+                    // show select button
+                    active: false,
+                    // allow multiple selection
+                    multiple: true,
+                    filters:['png'],
+                    // handle click event
+                    click: (items, event) => {
+                        if (!items.length) {
+                            alert('No item selected');
+                            return;
+                        }
+                        alert('Selected: ' + items[0].href);
+                        console.log(items, event);
+                    }
+                }
+            }
+        }
+    }
+</script>
+
+<style scoped>
+
+</style>

+ 16 - 1
vue.config.js

@@ -44,6 +44,7 @@ module.exports = {
     // it can be accessed in index.html to inject the correct title.
     name: name,
     resolve: {
+      extensions: ['.mjs', '.js', '.json', '.jsx', '.ts', '.tsx'],
       alias: {
         '@': resolve('src')
       }
@@ -65,6 +66,21 @@ module.exports = {
           options: {
             exports: 'Janus'
           }
+        },
+          // editorjs 使用ESM 导入,.mjs 需要转 es5处理,否则报错
+        {
+          test: /\.mjs$/,
+          use: {
+            loader: 'babel-loader'
+          },
+          include: /node_modules/
+        },
+        {
+          test: /bundle\.js$/,
+          use: {
+            loader: 'babel-loader'
+          },
+          include: /node_modules/
         }
       ]
     }
@@ -84,7 +100,6 @@ module.exports = {
 
     // when there are many pages, it will cause too many meaningless requests
     config.plugins.delete('prefetch')
-
     // set svg-sprite-loader
     config.module
       .rule('svg')