Browse Source

Merge branch 'develop' into languages/es

# Conflicts:
#	dist/index.html
#	languages/en.js
#	languages/es.js
#	languages/ru-RU.js
#	languages/zh-CN.js
#	src/utils/domain.js
#	src/views/ncs-orginazition/components/partInfoEdit.vue
vothin 8 months ago
parent
commit
13466e15e0
100 changed files with 12796 additions and 104 deletions
  1. 2 1
      .gitignore
  2. 2 2
      Dockerfile
  3. BIN
      dist/favicon.ico
  4. 0 1
      dist/index.html
  5. BIN
      dist/static/fonts/element-icons.535877f5.woff
  6. BIN
      dist/static/fonts/element-icons.732389de.ttf
  7. BIN
      dist/static/img/401.089007e7.gif
  8. BIN
      dist/static/img/404.a57b6f31.png
  9. BIN
      dist/static/img/404_cloud.0f4bc32b.png
  10. 77 0
      languages/editorjs/en.js
  11. 77 0
      languages/editorjs/es.js
  12. 77 0
      languages/editorjs/ru-RU.js
  13. 78 0
      languages/editorjs/zh-CN.js
  14. 127 52
      languages/en.js
  15. 84 9
      languages/es.js
  16. 66 0
      languages/message-center/en.js
  17. 66 0
      languages/message-center/es.js
  18. 66 0
      languages/message-center/ru-RU.js
  19. 66 0
      languages/message-center/zh-CN.js
  20. 81 7
      languages/ru-RU.js
  21. 92 14
      languages/zh-CN.js
  22. 1 1
      nginx.conf
  23. 30 2
      package.json
  24. 13 11
      public/domain.js
  25. 11 0
      public/index.html
  26. 2 1
      set-envs.sh
  27. 58 0
      src/api/message_template.js
  28. 59 0
      src/api/message_type.js
  29. 58 0
      src/api/messages.js
  30. 10 2
      src/api/ncs_board.js
  31. 9 0
      src/api/ncs_customer.js
  32. 56 0
      src/api/ncs_customer_affair.js
  33. 42 1
      src/api/ncs_device.js
  34. 8 0
      src/api/ncs_entrace_guard_user.js
  35. 9 0
      src/api/ncs_event.js
  36. 67 0
      src/api/template_paramer.js
  37. 15 0
      src/api/upload.js
  38. 64 0
      src/components/AgGridCellRender/TagsCellRender.vue
  39. 34 0
      src/components/EditorjsTools/AudioTool/index.css
  40. 145 0
      src/components/EditorjsTools/AudioTool/index.js
  41. 77 0
      src/components/EditorjsTools/ImageTool/index.css
  42. 211 0
      src/components/EditorjsTools/ImageTool/index.js
  43. 33 0
      src/components/EditorjsTools/VideoTool/index.css
  44. 172 0
      src/components/EditorjsTools/VideoTool/index.js
  45. 74 0
      src/components/FileManager/ServiceContainer.js
  46. 287 0
      src/components/FileManager/VueFinder.vue
  47. 3549 0
      src/components/FileManager/assets/css/a.css
  48. 119 0
      src/components/FileManager/assets/css/index.css
  49. 353 0
      src/components/FileManager/assets/css/preflight.css
  50. 41 0
      src/components/FileManager/components/ActionMessage.vue
  51. 239 0
      src/components/FileManager/components/Breadcrumb.vue
  52. 441 0
      src/components/FileManager/components/ContextMenu.vue
  53. 428 0
      src/components/FileManager/components/Explorer.vue
  54. 45 0
      src/components/FileManager/components/Message.vue
  55. 82 0
      src/components/FileManager/components/Statusbar.vue
  56. 251 0
      src/components/FileManager/components/Toolbar.vue
  57. 156 0
      src/components/FileManager/components/modals/ModalAbout.vue
  58. 84 0
      src/components/FileManager/components/modals/ModalArchive.vue
  59. 128 0
      src/components/FileManager/components/modals/ModalDelete.vue
  60. 52 0
      src/components/FileManager/components/modals/ModalLayout.vue
  61. 52 0
      src/components/FileManager/components/modals/ModalMessage.vue
  62. 157 0
      src/components/FileManager/components/modals/ModalMove.vue
  63. 74 0
      src/components/FileManager/components/modals/ModalNewFile.vue
  64. 118 0
      src/components/FileManager/components/modals/ModalNewFolder.vue
  65. 196 0
      src/components/FileManager/components/modals/ModalPreview.vue
  66. 130 0
      src/components/FileManager/components/modals/ModalRename.vue
  67. 78 0
      src/components/FileManager/components/modals/ModalUnarchive.vue
  68. 837 0
      src/components/FileManager/components/modals/ModalUpload.vue
  69. 51 0
      src/components/FileManager/components/previews/Audio.vue
  70. 33 0
      src/components/FileManager/components/previews/Default.vue
  71. 168 0
      src/components/FileManager/components/previews/Image.vue
  72. 61 0
      src/components/FileManager/components/previews/Pdf.vue
  73. 259 0
      src/components/FileManager/components/previews/Text.vue
  74. 49 0
      src/components/FileManager/components/previews/Video.vue
  75. 31 0
      src/components/FileManager/composables/useDebouncedRef.js
  76. 47 0
      src/components/FileManager/composables/useI18n.js
  77. 51 0
      src/components/FileManager/composables/useStorage.js
  78. 55 0
      src/components/FileManager/composables/useTheme.js
  79. 17 0
      src/components/FileManager/features.js
  80. 262 0
      src/components/FileManager/filemaps.js
  81. 32 0
      src/components/FileManager/index.js
  82. 89 0
      src/components/FileManager/locales/de.js
  83. 89 0
      src/components/FileManager/locales/en.js
  84. 89 0
      src/components/FileManager/locales/fa.js
  85. 90 0
      src/components/FileManager/locales/fr.js
  86. 89 0
      src/components/FileManager/locales/he.js
  87. 89 0
      src/components/FileManager/locales/hi.js
  88. 89 0
      src/components/FileManager/locales/ru.js
  89. 89 0
      src/components/FileManager/locales/sv.js
  90. 89 0
      src/components/FileManager/locales/tr.js
  91. 90 0
      src/components/FileManager/locales/zhCN.js
  92. 89 0
      src/components/FileManager/locales/zhTW.js
  93. 11 0
      src/components/FileManager/modals.js
  94. 243 0
      src/components/FileManager/utils/ajax.js
  95. 1 0
      src/components/FileManager/utils/datetimestring.js
  96. 30 0
      src/components/FileManager/utils/filesize.js
  97. 5 0
      src/components/FileManager/utils/title_shorten.js
  98. 292 0
      src/components/ReCropperPreview/index.vue
  99. 1 0
      src/icons/svg/audio.svg
  100. 0 0
      src/icons/svg/full-screen-cancel.svg

+ 2 - 1
.gitignore

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

+ 2 - 2
Dockerfile

@@ -1,4 +1,4 @@
-FROM nginx:alpine
+FROM nginx:stable-alpine
 MAINTAINER wuyunfeng
 
 RUN mkdir -p /app/
@@ -7,7 +7,7 @@ COPY ./nginx.conf /etc/nginx/nginx.conf
 COPY ./set-envs.sh /app/set-envs.sh
 EXPOSE 443 80
 
-ENV OnlineSystemUrl=http://api.base.wdklian.com serverUrl=http://172.28.100.100:8005 DeviceUrl=http://172.28.100.100:8006 mediaUrl=http://172.28.100.100:8004 apiMode=dev uiVersion=1 enableBroadcast=false enableMobile=false enableEntraceguard=false enableNBiot=false enableCustomerDevice=false enableSosDevice=false enable485=false enableLinux=false
+ENV OnlineSystemUrl=http://api.base.wdklian.com serverUrl=http://172.28.100.100:8005  gateWayUrl=http://172.28.100.100:7030 DeviceUrl=http://172.28.100.100:8006 mediaUrl=http://172.28.100.100:8004 apiMode=dev uiVersion=1 enableBroadcast=false enableMobile=false enableEntraceguard=false enableNBiot=false enableCustomerDevice=false enableSosDevice=false enable485=false enableLinux=false
 RUN echo 'USERNAME=' $serverUrl ',DeviceUrl=' $DeviceUrl ',mediaUrl=' $mediaUrl',apiMode=' $apiMode ',uiVersion=' $uiVersion
 #CMD sh -c "sed -i 's/^.*8006.*$/serverUrl=$serverUrl' domain.js"
 RUN ["chmod", "777", "/app/domain.js"]

BIN
dist/favicon.ico


File diff suppressed because it is too large
+ 0 - 1
dist/index.html


BIN
dist/static/fonts/element-icons.535877f5.woff


BIN
dist/static/fonts/element-icons.732389de.ttf


BIN
dist/static/img/401.089007e7.gif


BIN
dist/static/img/404.a57b6f31.png


BIN
dist/static/img/404_cloud.0f4bc32b.png


+ 77 - 0
languages/editorjs/en.js

@@ -0,0 +1,77 @@
+export default {
+
+  messages: {
+    'ui': {
+      'blockTunes': {
+        'toggler': {
+          'Click to tune': 'Click to tune',
+          'or drag to move': 'or drag to move'
+        }
+      },
+      'inlineToolbar': {
+        'converter': {
+          'Convert to': 'Convert to'
+        }
+      },
+      'toolbar': {
+        'toolbox': {
+          'Add': 'Add'
+        }
+      },
+      'popover': {
+        'Filter': 'Filter',
+        'Nothing found': 'Nothing found'
+      }
+    },
+
+    blockTunes: {
+      spoiler: {
+        'Hide content': 'Hide content'
+      },
+      'delete': {
+        'Delete': 'Delete block',
+        'Click to delete': 'Click to delete'
+      },
+      'moveUp': {
+        'Move up': 'Move up'
+      },
+      'moveDown': {
+        'Move down': 'Move down'
+      },
+      'indentTune': {
+        'Add Border': 'Add Border'
+      }
+    },
+    toolNames: {
+      'Text': 'Text',
+      'Heading': 'Heading',
+      'Title': 'Title',
+      'List': 'List',
+      'Warning': 'Warning',
+      'Checklist': 'Checklist',
+      'Quote': 'Quote',
+      'Code': 'Code',
+      'Delimiter': 'Delimiter',
+      'Raw HTML': 'Raw HTML',
+      'Table': 'Table',
+      'Link': 'Link',
+      'Marker': 'Marker',
+      'Bold': 'Bold',
+      'Italic': 'Italic',
+      'Alert': 'Alert',
+      'InlineCode': 'Inline Code',
+      'ImageTool': 'Image',
+      'AudioPlayer': 'Audio',
+      'VideoPlayer': 'Video'
+    },
+    tools: {
+      image: {
+        'Stretch Image': 'Stretch Image',
+        'Add Background': 'Add Background',
+        'Add Border': 'Add Border',
+        'Reselection Image': 'Reselect Image'
+      }
+    }
+  }
+
+}

+ 77 - 0
languages/editorjs/es.js

@@ -0,0 +1,77 @@
+export default {
+
+  messages: {
+    'ui': {
+      'blockTunes': {
+        'toggler': {
+          'Click to tune': 'Haga clic para ajustar',
+          'or drag to move': 'o arrastre para mover'
+        }
+      },
+      'inlineToolbar': {
+        'converter': {
+          'Convert to': 'Convertir a'
+        }
+      },
+      'toolbar': {
+        'toolbox': {
+          'Add': 'Agregar'
+        }
+      },
+      'popover': {
+        'Filter': 'Filtrar',
+        'Nothing found': 'No se encontró nada'
+      }
+    },
+
+    blockTunes: {
+      spoiler: {
+        'Hide content': 'Ocultar contenido'
+      },
+      'delete': {
+        'Delete': 'Eliminar bloque',
+        'Click to delete': 'Haga clic para eliminar'
+      },
+      'moveUp': {
+        'Move up': 'Mover hacia arriba'
+      },
+      'moveDown': {
+        'Move down': 'Mover hacia abajo'
+      },
+      'indentTune': {
+        'Add Border': 'Agregar borde'
+      }
+    },
+    toolNames: {
+      'Text': 'Texto',
+      'Heading': 'Encabezado',
+      'Title': 'Título',
+      'List': 'Lista',
+      'Warning': 'Advertencia',
+      'Checklist': 'Lista de verificación',
+      'Quote': 'Cita',
+      'Code': 'Código',
+      'Delimiter': 'Delimitador',
+      'Raw HTML': 'HTML crudo',
+      'Table': 'Tabla',
+      'Link': 'Enlace',
+      'Marker': 'Marcador',
+      'Bold': 'Negrita',
+      'Italic': 'Cursiva',
+      'Alert': 'Alerta',
+      'InlineCode': 'Código en línea',
+      'ImageTool': 'Imagen',
+      'AudioPlayer': 'Audio',
+      'VideoPlayer': 'Video'
+    },
+    tools: {
+      image: {
+        'Stretch Image': 'Estirar imagen',
+        'Add Background': 'Agregar fondo',
+        'Add Border': 'Agregar borde',
+        'Reselection Image': 'Reseleccionar imagen'
+      }
+    }
+  }
+
+}

+ 77 - 0
languages/editorjs/ru-RU.js

@@ -0,0 +1,77 @@
+export default {
+
+  messages: {
+    'ui': {
+      'blockTunes': {
+        'toggler': {
+          'Click to tune': 'Нажмите, чтобы настроить',
+          'or drag to move': 'или перетащите, чтобы переместить'
+        }
+      },
+      'inlineToolbar': {
+        'converter': {
+          'Convert to': 'Преобразовать в'
+        }
+      },
+      'toolbar': {
+        'toolbox': {
+          'Add': 'Добавить'
+        }
+      },
+      'popover': {
+        'Filter': 'Фильтр',
+        'Nothing found': 'Ничего не найдено'
+      }
+    },
+
+    blockTunes: {
+      spoiler: {
+        'Hide content': 'Скрыть содержимое'
+      },
+      'delete': {
+        'Delete': 'Удалить блок',
+        'Click to delete': 'Нажмите, чтобы удалить'
+      },
+      'moveUp': {
+        'Move up': 'Переместить вверх'
+      },
+      'moveDown': {
+        'Move down': 'Переместить вниз'
+      },
+      'indentTune': {
+        'Add Border': 'Добавить границу'
+      }
+    },
+    toolNames: {
+      'Text': 'Текст',
+      'Heading': 'Заголовок',
+      'Title': 'Название',
+      'List': 'Список',
+      'Warning': 'Предупреждение',
+      'Checklist': 'Контрольный список',
+      'Quote': 'Цитата',
+      'Code': 'Код',
+      'Delimiter': 'Разделитель',
+      'Raw HTML': 'HTML код',
+      'Table': 'Таблица',
+      'Link': 'Ссылка',
+      'Marker': 'Маркер',
+      'Bold': 'Жирный',
+      'Italic': 'Курсив',
+      'Alert': 'Оповещение',
+      'InlineCode': 'Встроенный код',
+      'ImageTool': 'Изображение',
+      'AudioPlayer': 'Аудио',
+      'VideoPlayer': 'Видео'
+    },
+    tools: {
+      image: {
+        'Stretch Image': 'Растянуть изображение',
+        'Add Background': 'Добавить фон',
+        'Add Border': 'Добавить границу',
+        'Reselection Image': 'Перевыбрать изображение'
+      }
+    }
+  }
+
+}

+ 78 - 0
languages/editorjs/zh-CN.js

@@ -0,0 +1,78 @@
+export default {
+
+  messages: {
+    'ui': {
+      'blockTunes': {
+        'toggler': {
+          'Click to tune': '打开调节器',
+          'or drag to move': '或拖拽移动'
+        }
+      },
+      'inlineToolbar': {
+        'converter': {
+          'Convert to': '转换为'
+        }
+      },
+      'toolbar': {
+        'toolbox': {
+          'Add': '添加'
+        }
+      },
+      'popover': {
+        'Filter': '过滤',
+        'Nothing found': '无匹配'
+      }
+    },
+
+    blockTunes: {
+      spoiler: {
+        'Hide content': '隐藏内容'
+      },
+      'delete': {
+        'Delete': '删除模块',
+        'Click to delete': '点击删除'
+      },
+      'moveUp': {
+        'Move up': '上移'
+      },
+      'moveDown': {
+        'Move down': '下移'
+      },
+      'indentTune': {
+        'Add Border': '添加边框'
+      }
+    },
+    toolNames: {
+      'Text': '段落',
+      'Heading': '标题',
+      'Title': '标题',
+      'List': '列表',
+      'Warning': '提示',
+      'Checklist': '可选列表',
+      'Quote': '引用',
+      'Code': '代码',
+      'Delimiter': '分隔符',
+      'Raw HTML': 'HTML代码',
+      'Table': '表格',
+      'Link': '链接',
+      'Marker': '标记',
+      'Bold': '加粗',
+      'Italic': '斜体',
+      'Alert': '警示',
+      'InlineCode': '内连代码',
+      'ImageTool': '图片',
+      'AudioPlayer': '音频',
+      'VideoPlayer': '视频'
+
+    },
+    tools: {
+      image: {
+        'Stretch Image': '拉升图片',
+        'Add Background': '添加背景',
+        'Add Border': '添加边框',
+        'Reselection Image': '重选图片'
+      }
+    }
+  }
+
+}

+ 127 - 52
languages/en.js

@@ -5,7 +5,7 @@ module.exports = {
     home: 'Home',
     add: 'Add',
     edit: 'Edit',
-    more: 'More',
+    more: 'Saber mais',
     view: 'View',
     delete: 'Delete',
     enabled: 'Enable',
@@ -66,7 +66,7 @@ module.exports = {
     FontSize: 'Font size',
     chooseLang: 'Choose language',
     SynchronizeHISInformation: 'Synchronize HIS information',
-    perpetualLicence: 'Perpetual License',
+    perpetualLicence: 'Licença Perpétua',
     licenseValidity: 'The license is valid until: ',
     licenseRemainsValid: 'The remaining validity period of the license:',
     getLicense: 'days, please contact after-sales to obtain authorization',
@@ -127,7 +127,7 @@ module.exports = {
     filters: 'filter',
     loadingOoo: 'Loading...',
     noRowsToShow: 'No data to show',
-    enabled: 'open',
+    // enabled: 'open',
     pinColumn: 'fixed column',
     pinLeft: 'Fixed to the left',
     pinRight: 'Fixed to the right',
@@ -280,10 +280,10 @@ module.exports = {
     passNo: 'Employee No'
   },
   home: {
-    todayTask: "Today's task",
-    recentNote: 'Recent Notes',
-    recentInteract: 'Recent Interact',
-    recentRemarks: 'Recent user notes'
+    todayTask: 'A tarefa de hoje',
+    recentNote: 'Notas recentes',
+    recentInteract: 'Interação recente',
+    recentRemarks: 'Notas de utilizador recentes'
   },
   frameManage: {
     frameManage: 'Space position',
@@ -658,8 +658,8 @@ module.exports = {
   remark: {
     remarkAdd: 'New note',
     remarkContent: 'Note content:',
-    remarkCreateTime: 'Create time:',
-    remarkCreateName: 'Created by:'
+    remarkCreateTime: 'Tempo de criação:',
+    remarkCreateName: 'Criado por:'
   },
   task: {
     all: 'All',
@@ -683,10 +683,10 @@ module.exports = {
   },
   interaction: {
     interactionKeywords: 'Please enter the initiator or receiver',
-    fromMemberName: 'Originator',
-    toMemberName: 'Receiver',
-    actionType: 'Interaction type',
-    actionEnd: 'Interaction result',
+    fromMemberName: 'Originador',
+    toMemberName: 'Recetor',
+    actionType: 'Tipo de interação',
+    actionEnd: 'Resultado da interação',
     actionTime: 'Number of interactions',
     actionTime2: 'Number of calls',
     success: 'Success',
@@ -699,9 +699,9 @@ module.exports = {
     unSuccessTime2: 'Unconnected times',
     failedInteraction: 'Failed to hang up the interaction normally',
     failedInteraction2: 'The call did not hang up normally',
-    notOperated: 'Not responding',
+    notOperated: 'Não está respondendo',
     data: 'Interaction data',
-    createDate: 'Interaction time',
+    createDate: 'Tempo de interação',
     responseTime: 'Response time',
     fromDevice: 'Initiating device',
     toDevice: 'Receive device',
@@ -1076,12 +1076,12 @@ module.exports = {
     hisNurseOptionsKeyval: 'Nursing item classification his PK'
   },
   tab: {
-    home: 'Home',
-    frameManage: 'Space position',
-    deviceManage: 'All devices',
-    clerkManage: 'Employee management',
+    home: 'Casa',
+    frameManage: 'Localização espacial',
+    deviceManage: 'Todos os dispositivos',
+    clerkManage: 'Gestão de colaboradores',
     clerkCalendar: 'Employee clock in calendar',
-    patientManage: 'Patient management',
+    patientManage: 'Gestão de doentes',
     customerManage: 'User Management',
     customerQrCode: 'User QR code',
     customerAdvice: 'Doctor\'s advice',
@@ -1090,21 +1090,21 @@ module.exports = {
     sosDeviceSettingManage: 'Alarm device',
     channelManage: 'Intercom channel',
     channelImHistory: 'Channel message history',
-    remarkManage: 'Note management',
-    taskManage: 'Task management',
-    interaction: 'Interaction',
+    remarkManage: 'Gestão de notas',
+    taskManage: 'Gestão de tarefas',
+    interaction: 'Interação',
     interactionHistory: 'Interaction History',
     frameGroupManage: 'Region management',
     frameGroupEdit: 'Edit area information',
     watchFrameManage: 'Mobile device space',
-    broadcastManage: 'Broadcast settings',
+    broadcastManage: 'Configurações de transmissão',
     broadcastEdit: 'Edit broadcast information',
     nurseConfig: 'Nursing parameters',
     boardManage: 'Information board settings',
     eventManage: 'Button event management',
     hisManage: 'His query',
     interactionChars: 'Interaction statistics',
-    settings: 'settings',
+    settings: 'Configurações',
     partSettings: 'Organization settings',
     functionRoleMapping: 'Component Permissions',
     countdownConfig: 'Countdown Component Configuration',
@@ -1125,7 +1125,7 @@ module.exports = {
     deviceFrame: 'Device space',
     ledDeviceManager: 'LED dot matrix screen management',
     ledDevice: 'LED dot matrix screen control',
-    entraceguardUser: 'Passage setting',
+    entraceguardUser: 'Configurações de acesso',
     customBoardManage: 'Custom Board Screen',
     customBoardDesigner: 'Designer Board Screen',
     staffManageFrames: 'Staff Serve Structure',
@@ -1142,22 +1142,22 @@ module.exports = {
     convenientDataSync: 'Convenient Data Sync'
   },
   deviceType: {
-    NURSE_HOST: 'Nurse Host',
-    DOCTOR_HOST: 'Doctor Host',
-    DOOR_DEVICE: 'Door Station',
+    NURSE_HOST: 'Nurse Host Machine',
+    DOCTOR_HOST: 'Doctor Machine',
+    DOOR_DEVICE: 'Doorway Screen',
     DIGIT_BED_DEVICE: 'Bed Extension',
     LCD_SCREEN: 'LCD Corridor Screen',
     LED_SCREEN: 'LED Dot Corridor Screen',
-    NURSE_WATCH: 'Nurse Moves',
-    WORKER_WATCH: 'The Carer Moves',
-    USER_WATCH: 'User Move',
+    NURSE_WATCH: 'Nurse Phone',
+    WORKER_WATCH: 'worker Phone',
+    USER_WATCH: 'BT Bracelet',
     CELL_PHONE: 'Mobile App',
     TRANSFER_DEVICE: 'Bus Conversion Box',
-    SIMULATE_BED_DEVICE: 'Analog Extension',
+    SIMULATE_BED_DEVICE: 'Analog Bed Unit',
     SIMULATE_EMERGENCY_BUTTON: 'Analog Emergency Button',
-    SIMULATE_DOOR_LIGHT: 'Analog Door Light',
+    SIMULATE_DOOR_LIGHT: 'Analog Door Lamp',
     REMOTE_CONTROL: 'Remote Control',
-    BEACON: 'Beacons',
+    BEACON: 'BT NetGate',
     INFORMATION_BOARD: 'Information Board',
     ENTRANCE_GUARD: 'Access Control Device',
     VISITATION: 'Visiting Machine',
@@ -1166,29 +1166,31 @@ module.exports = {
     RS485_DOOR: '485 Door Extension',
     ALARM_BODY_INDUCTIVE: 'Infrared Alarm',
     ALARM_WATER_OVERFLOW: 'Water Monitor',
-    ALARM_HOUSEHOLD_GAS: 'Household Fire Alarm',
-    ALARM_HOUSEHOLD_SMOKE: 'Household Smoke Alarm',
+    ALARM_HOUSEHOLD_GAS: 'Fire Alarm',
+    ALARM_HOUSEHOLD_SMOKE: 'Smoke Alarm',
     ALARM_BUTTON_SOS: 'One Button Alarm',
-    VITAL_SIGNS_DEVICE: 'Signs Devices',
+    VITAL_SIGNS_DEVICE: 'Vital Signs Devices',
     ALARM_RESTRAINT_BAND: 'Restriction Band Alarm',
     DOOR_LOCK: 'Door Magnetic Sensor',
     EMERGENCY_GATEWAY: 'Alarm Gateway',
     ALARM_433BUTTON: '433 Alarm',
     OTHER_HOST: 'Other Host',
-    BREASTPLATE: 'Breast Plate',
+    BREASTPLATE: 'Badge/bracelet',
     OWON_X5_GATEWAY: 'X5 Gateway',
     FALL_DETECTION_RADAR: 'Fall Detection Radar',
     HUMAN_DETECTION_RADAR: 'Sleep Detection Radar',
     ALARM_INFUSION: 'Infusion alarm',
     ELECTRONIC_FENCE: 'Electronic fence',
-    S433_DOOR_LAMP: 'Wireless door lamp',
-    S433_TRANSFER_BOX: 'Wireless conversion box',
+    S433_DOOR_LAMP: '433 Door Lamp',
+    S433_TRANSFER_BOX: '433 Lora Converter',
     S433_RECEIVER: 'Signal receiver',
     SLEEPMATTRESS: 'sleep mattress',
     S4G_INTERCOM: '4G intercom',
     WATCH_IW: 'Smart Watch - IW',
-    MULTIFUNCTIONAL_BUTTON: 'Multi function button',
-    S433_BJMD: 'Alarm door light'
+    MULTIFUNCTIONAL_BUTTON: '433 Button',
+    S433_BJMD: '433 Lamp',
+    SOS_VOICE_BUTTON: 'Voice Button',
+    PTT: 'ptt'
   },
   vitalSignsDeviceType: {
     BLOOD_SUGAR: 'Blood Pressure Meter',
@@ -1206,7 +1208,7 @@ module.exports = {
     CALLBACK: 'TCP Feedback',
     VOICE: 'Voice',
     VIDEO: 'Video',
-    SOS: 'Emergency Call',
+    SOS: 'Chamadas de emergência',
     REINFORCE: 'Reinforcements',
     IM: 'Message',
     DEVICE: 'Device',
@@ -1387,15 +1389,19 @@ module.exports = {
     SATISFACTION: 'Satisfaction survey'
   },
   entraceguardUser: {
-    named: 'User Name',
+    named: 'User name',
     idNo: 'ID number',
-    ic: 'IC Card No',
-    phone: 'cell-phone number',
-    password: 'Access code',
-    forbidden: 'No Entry',
-    refreshUser: 'Refresh Users',
-    yes: 'YES',
-    nop: 'NO'
+    ic: 'IC card number',
+    phone: 'Phone number',
+    password: 'Access password',
+    forbidden: 'Forbidden access',
+    refreshUser: 'Refresh available users',
+    yes: 'Yes',
+    nop: 'No',
+    expire: 'Expiration date',
+    face: 'Face recognition photo',
+    chooseImage: 'Choose image',
+    syncToDevice: 'Synchronize to device',
   },
   boardTitle: {
     add: 'Add information board screen',
@@ -1609,6 +1615,7 @@ module.exports = {
     MULTIFUNCTIONAL_BUTTON: 'Multi function button',
     SLEEP_MATTRESS: 'Sleep mattress',
     NFC: 'NFC functionality',
+    LORA_BUTTON: 'LoRa Button',
     linuxSerial1: 'Linux serial port 1',
     linuxSerial2: 'Linux serial port 2',
     linuxSerial3: 'Linux serial port 3',
@@ -1638,5 +1645,73 @@ module.exports = {
     deleteSqlMsg: 'The data is not recoverable after the deletion operation, are you sure you want to delete this data?',
     choiceTimeZone: 'Select Time Zone',
     choiceLanguage: 'Select language'
+
+  },
+  wu20240322: {
+    fileManager: 'Gestor de ficheiros'
+  },
+  zy20240530: {
+    customerAffair: 'user event',
+    customerAffairAdd: 'Add new event',
+    customerAffairConTent: 'Event content',
+    timeMsg: 'Please select a time',
+    planTime: 'Planned execution time',
+    doTime: 'Actual execution time'
+  },
+  wu20240530: {
+    nurseConfig: {
+      default_icon: 'Default Icon',
+      default_content_show: 'Default Content Display',
+      icon_text: 'Image + Text',
+      icon: 'Image Only',
+      text: 'Text Only'
+    },
+    nurseOption: {
+      icon_url: 'Icon',
+      content_show: 'Content Display Mode'
+    },
+    clear_icon: 'Clear Icon'
+  },
+  wnn20240322: {
+    SOS_VOICE_BUTTON: 'SOS voice button',
+    PTT: 'ptt'
+  },
+  zy20240611: {
+    boolTransfer: 'Enable managed interface',
+    hostDeviceLock: 'Enable card swiping privileges for the nurse\'s console',
+    broadcastEnable: 'Activate the broadcasting function',
+    learn: 'Learn',
+    learnAll: 'Learn All',
+    deleteAll: 'Delete All',
+    deviceFunction: 'Device Function',
+    deviceButtonFunctionStr: 'Device Button Function',
+    selectKey: 'Please select a key',
+    voiceCancel: 'Cancel Call',
+    SMART_LIFE_GATEWAY: 'Smart Life Gateway',
+    SMART_LIFE_VOICE_GATE: 'Smart Life Voice Gateway',
+    CURTAIN: 'Curtain',
+    SWITCH: 'Switch',
+    SIMULATE_BLUE_CODE: 'Simulate BLUE CODE',
+    switchType: 'Switch Type',
+    SINGLE_SWITCH: 'Single Key Switch',
+    TWO_KEY_SWITCH: 'Two Key Switch',
+    THREE_KEY_SWITCH: 'Three Key Switch',
+    MINUTE10: '10 minutes',
+    MINUTE15: '15 minutes',
+    MINUTE20: '20 minutes',
+    MINUTE30: '30 minutes',
+    MINUTE45: '45 minutes',
+    MINUTE60: '60 minutes',
+    MINUTE90: '90 minutes',
+    MINUTE120: '120 minutes'
+  },
+  wu20240928: {
+    editImage: 'Edit image',
+    preview: 'Preview',
+    reload: 'Reload',
+    comfirm: 'Confirm',
+    cancel: 'Cancel',
+    incorrectFormat: 'Not an image format file',
+    fileSizeLimit: 'Please upload an image less than {size}M'
   }
 }

+ 84 - 9
languages/es.js

@@ -127,7 +127,7 @@ module.exports = {
     filters: 'filtro',
     loadingOoo: 'Cargando...',
     noRowsToShow: 'No hay datos para mostrar',
-    enabled: 'abierto',
+    // enabled: 'abierto',
     pinColumn: 'columna fija',
     pinLeft: 'Fijo a la izquierda',
     pinRight: 'Fijo a la derecha',
@@ -214,7 +214,7 @@ module.exports = {
     pause: 'Pausa',
     noFile: 'El archivo no existe, no se puede reproducir',
     inputMsg: '¡Por favor, introduzca el contenido!',
-    year: 'Año',
+    year: 'Edad',
     month: 'Mes',
     day: 'Día',
     timeMsg: '¡Por favor, seleccione el intervalo de tiempo!',
@@ -1188,7 +1188,9 @@ module.exports = {
     S4G_INTERCOM: 'Walkie - talkie 4G',
     WATCH_IW: 'Reloj inteligente - IW',
     MULTIFUNCTIONAL_BUTTON: 'Botón multifuncional',
-    S433_BJMD: 'Luz de la puerta de alarma'
+    S433_BJMD: 'Luz de la puerta de alarma',
+    SOS_VOICE_BUTTON: 'Botón de voz sos',
+    PTT: 'ptt'
   },
   vitalSignsDeviceType: {
     BLOOD_SUGAR: 'Medidor de presión arterial',
@@ -1390,12 +1392,16 @@ module.exports = {
     named: 'Nombre de usuario',
     idNo: 'Número de identificación',
     ic: 'Número de tarjeta IC',
-    phone: 'Número de teléfono móvil',
-    password: 'Código de acceso',
-    forbidden: 'Entrada no autorizada',
-    refreshUser: 'Actualizar usuarios',
-    yes: 'Sí.',
-    nop: 'No.'
+    phone: 'Número de teléfono',
+    password: 'Contraseña de acceso',
+    forbidden: 'Acceso prohibido',
+    refreshUser: 'Actualizar usuarios disponibles',
+    yes: 'Sí',
+    nop: 'No',
+    expire: 'Fecha de vencimiento',
+    face: 'Foto de reconocimiento facial',
+    chooseImage: 'Elegir imagen',
+    syncToDevice: 'Sincronizar con el dispositivo'
   },
   boardTitle: {
     add: 'Añadir panel de visualización de información',
@@ -1601,6 +1607,7 @@ module.exports = {
     MULTIFUNCTIONAL_BUTTON: 'Botón multifuncional',
     S433_BJMD: 'Luz de la puerta de alarma'
   },
+
   zy20240205: {
     linuxPortraitDoor: 'Máquina de puerta de pantalla vertical de Linux',
     ENABLE: 'No abrir',
@@ -1609,6 +1616,7 @@ module.exports = {
     MULTIFUNCTIONAL_BUTTON: 'Botón multifuncional',
     SLEEP_MATTRESS: 'Colchón para dormir',
     NFC: 'Función NFC',
+    LORA_BUTTON: 'Botón LoRa',
     linuxSerial1: 'Puerto serie de Linux 1',
     linuxSerial2: 'Puerto serie de Linux 2',
     linuxSerial3: 'Puerto serie de Linux 3',
@@ -1638,5 +1646,72 @@ module.exports = {
     deleteSqlMsg: 'Los datos no se pueden recuperar después de la operación de eliminación, ¿está seguro de que desea eliminar estos datos?',
     choiceTimeZone: 'Elegir zona horaria',
     choiceLanguage: 'Elegir idioma'
+  },
+  wu20240322: {
+    fileManager: 'Administrador de Archivos'
+  },
+  zy20240530: {
+    customerAffair: 'Asuntos del usuario',
+    customerAffairAdd: 'Nuevos asuntos',
+    customerAffairConTent: 'Contenido de la transacción',
+    timeMsg: 'Por favor, elija la hora',
+    planTime: 'Tiempo de ejecución previsto',
+    doTime: 'Tiempo real de ejecución'
+  },
+  wu20240530: {
+    nurseConfig: {
+      default_icon: 'Icono Predeterminado',
+      default_content_show: 'Visualización de Contenido Predeterminada',
+      icon_text: 'Imagen + Texto',
+      icon: 'Solo Imagen',
+      text: 'Solo Texto'
+    },
+    nurseOption: {
+      icon_url: 'Icono',
+      content_show: 'Modo de Visualización del Contenido'
+    },
+    clear_icon: 'Limpiar Icono'
+  },
+  wnn20240322: {
+    SOS_VOICE_BUTTON: 'Botón de voz sos',
+    PTT: 'ptt'
+  },
+  zy20240611: {
+    boolTransfer: 'Habilitar interfaz gestionada',
+    hostDeviceLock: 'Habilitar privilegios de tarjeta para la consola de enfermería',
+    broadcastEnable: 'Activar la función de transmisión',
+    learn: 'Aprender',
+    learnAll: 'Aprender Todo',
+    deleteAll: 'Borrar Todo',
+    deviceFunction: 'Función del dispositivo',
+    deviceButtonFunctionStr: 'Función del botón del dispositivo',
+    selectKey: 'Por favor, seleccione una clave',
+    voiceCancel: 'Cancelar llamada',
+    SMART_LIFE_GATEWAY: 'Puerta de enlace de vida inteligente',
+    SMART_LIFE_VOICE_GATE: 'Pasarela de voz de vida inteligente',
+    CURTAIN: 'Cortina',
+    SWITCH: 'Interruptor',
+    SIMULATE_BLUE_CODE: 'Simular BLUE CODE',
+    switchType: 'Tipo de Interruptor',
+    SINGLE_SWITCH: 'Interruptor de un botón',
+    TWO_KEY_SWITCH: 'Interruptor de dos botones',
+    THREE_KEY_SWITCH: 'Interruptor de tres botones',
+    MINUTE10: '10 minutos',
+    MINUTE15: '15 minutos',
+    MINUTE20: '20 minutos',
+    MINUTE30: '30 minutos',
+    MINUTE45: '45 minutos',
+    MINUTE60: '60 minutos',
+    MINUTE90: '90 minutos',
+    MINUTE120: '120 minutos',
+  },
+  wu20240928: {
+    editImage: 'Editar imagen',
+    preview: 'Vista previa',
+    reload: 'Recargar',
+    comfirm: 'Confirmar',
+    cancel: 'Cancelar',
+    incorrectFormat: 'No es un archivo de formato de imagen',
+    fileSizeLimit: 'Por favor, suba una imagen de menos de {size}M'
   }
 }

+ 66 - 0
languages/message-center/en.js

@@ -0,0 +1,66 @@
+module.exports = {
+
+  mc: {
+    messageCenter: 'Centro de Mensagens',
+    messages: 'Message List',
+    messageType: 'Message Type',
+    messageTemplate: 'Message Template',
+    templateParamer: 'Template Parameter',
+    messageTypeModule: {
+      typeName: 'Type Name',
+
+      addType: 'Add Message Type',
+      editType: 'Edit Message Type',
+      typeNameRequired: 'Type Name is required',
+      MESSAGE_TYPE_DELETE_FAILED: 'The type is referenced by messages and cannot be deleted!'
+    },
+    messagesModule: {
+      addMessage: 'Add Message',
+      title: 'Message Title',
+      description: 'Message Description',
+      openType: 'Display Type',
+      titleRequired: 'Message Title is required',
+      openTypeRequired: 'Display Type is required',
+      messageTypeRequired: 'Message Type is required',
+      boolShared: 'Is Shared Message',
+      status: 'Message Status',
+      enabledStatus: 'Enabled Message',
+      disabled: 'Disabled',
+      enabled: 'Enabled'
+
+    },
+    messageTemplateModule: {
+      addMessageTemplate: 'Add Template',
+      templateTitle: 'Template Title',
+      templateContent: 'Template Content',
+      editMessageTemplate: 'Edit Template',
+      templateTitleRequired: 'Template Title is required',
+      templateContentRequired: 'Template Content is required',
+      boolShared: 'Is Shared Template',
+      templateContentError: 'Template parameter {0} is invalid, please use defined template parameters and avoid nesting parameters',
+      insertParamerTip: 'Right-click to insert template parameter!'
+    },
+    templateParamerModule: {
+      addTemplateParamer: 'Add Parameter',
+      paramerName: 'Parameter Name',
+      paramerPreInput: 'Preset Value',
+      allowInput: 'Allow Custom Input',
+      boolDate: 'Is Date Type',
+      boolTime: 'Is Time Type',
+      addValue: 'Add Value',
+      yes: 'Yes',
+      no: 'No',
+      paramNameRequired: 'Parameter Name is required',
+      editParamer: 'Edit Parameter',
+      addParamer: 'Add Parameter',
+      TEMPLATE_PARAMER_DELETE_FAILED: 'The parameter is referenced by template messages and cannot be deleted!'
+    },
+    openTypeEnum: {
+      video: 'Video',
+      audio: 'Audio',
+      images: 'Image Gallery',
+      article: 'Text and Image'
+    }
+  }
+
+}

+ 66 - 0
languages/message-center/es.js

@@ -0,0 +1,66 @@
+module.exports = {
+
+  mc: {
+    messageCenter: 'Centro de Mensajes',
+    messages: 'Lista de Mensajes',
+    messageType: 'Tipo de Mensaje',
+    messageTemplate: 'Plantilla de Mensaje',
+    templateParamer: 'Parámetro de Plantilla',
+    messageTypeModule: {
+      typeName: 'Nombre del Tipo',
+
+      addType: 'Agregar Tipo de Mensaje',
+      editType: 'Editar Tipo de Mensaje',
+      typeNameRequired: 'Nombre del Tipo es obligatorio',
+      MESSAGE_TYPE_DELETE_FAILED: '¡El tipo está referenciado por mensajes y no se puede eliminar!'
+    },
+    messagesModule: {
+      addMessage: 'Agregar Mensaje',
+      title: 'Título del Mensaje',
+      description: 'Descripción del Mensaje',
+      openType: 'Tipo de Visualización',
+      titleRequired: 'Título del Mensaje es obligatorio',
+      openTypeRequired: 'Tipo de Visualización es obligatorio',
+      messageTypeRequired: 'Tipo de Mensaje es obligatorio',
+      boolShared: '¿Es un Mensaje Compartido?',
+      status: 'Estado del Mensaje',
+      enabledStatus: 'Mensaje Habilitado',
+      disabled: 'Deshabilitado',
+      enabled: 'Habilitado'
+
+    },
+    messageTemplateModule: {
+      addMessageTemplate: 'Agregar Plantilla',
+      templateTitle: 'Título de la Plantilla',
+      templateContent: 'Contenido de la Plantilla',
+      editMessageTemplate: 'Editar Plantilla',
+      templateTitleRequired: 'Título de la Plantilla es obligatorio',
+      templateContentRequired: 'Contenido de la Plantilla es obligatorio',
+      boolShared: '¿Es una Plantilla Compartida?',
+      templateContentError: 'El parámetro de plantilla {0} no es válido, use parámetros de plantilla definidos y evite anidar parámetros',
+      insertParamerTip: '¡Haga clic derecho para insertar el parámetro de plantilla!'
+    },
+    templateParamerModule: {
+      addTemplateParamer: 'Agregar Parámetro',
+      paramerName: 'Nombre del Parámetro',
+      paramerPreInput: 'Valor Predefinido',
+      allowInput: 'Permitir Entrada Personalizada',
+      boolDate: '¿Es Tipo Fecha?',
+      boolTime: '¿Es Tipo Hora?',
+      addValue: 'Agregar Valor',
+      yes: 'Sí',
+      no: 'No',
+      paramNameRequired: 'Nombre del Parámetro es obligatorio',
+      editParamer: 'Editar Parámetro',
+      addParamer: 'Agregar Parámetro',
+      TEMPLATE_PARAMER_DELETE_FAILED: '¡El parámetro está referenciado por mensajes de plantilla y no se puede eliminar!'
+    },
+    openTypeEnum: {
+      video: 'Video',
+      audio: 'Audio',
+      images: 'Galería de Imágenes',
+      article: 'Texto e Imagen'
+    }
+  }
+
+}

+ 66 - 0
languages/message-center/ru-RU.js

@@ -0,0 +1,66 @@
+module.exports = {
+
+  mc: {
+    messageCenter: 'Центр Сообщений',
+    messages: 'Список Сообщений',
+    messageType: 'Тип Сообщения',
+    messageTemplate: 'Шаблон Сообщения',
+    templateParamer: 'Параметр Шаблона',
+    messageTypeModule: {
+      typeName: 'Название Типа',
+
+      addType: 'Добавить Тип Сообщения',
+      editType: 'Редактировать Тип Сообщения',
+      typeNameRequired: 'Название Типа обязательно',
+      MESSAGE_TYPE_DELETE_FAILED: 'Тип используется в сообщениях и не может быть удален!'
+    },
+    messagesModule: {
+      addMessage: 'Добавить Сообщение',
+      title: 'Заголовок Сообщения',
+      description: 'Описание Сообщения',
+      openType: 'Тип Отображения',
+      titleRequired: 'Заголовок Сообщения обязателен',
+      openTypeRequired: 'Тип Отображения обязателен',
+      messageTypeRequired: 'Тип Сообщения обязателен',
+      boolShared: 'Является Общим Сообщением',
+      status: 'Статус Сообщения',
+      enabledStatus: 'Включенное Сообщение',
+      disabled: 'Отключено',
+      enabled: 'Включено'
+
+    },
+    messageTemplateModule: {
+      addMessageTemplate: 'Добавить Шаблон',
+      templateTitle: 'Название Шаблона',
+      templateContent: 'Содержание Шаблона',
+      editMessageTemplate: 'Редактировать Шаблон',
+      templateTitleRequired: 'Название Шаблона обязательно',
+      templateContentRequired: 'Содержание Шаблона обязательно',
+      boolShared: 'Является Общим Шаблоном',
+      templateContentError: 'Параметр шаблона {0} недействителен, используйте определенные параметры шаблона и избегайте вложенности параметров',
+      insertParamerTip: 'Щелкните правой кнопкой мыши, чтобы вставить параметр шаблона!'
+    },
+    templateParamerModule: {
+      addTemplateParamer: 'Добавить Параметр',
+      paramerName: 'Название Параметра',
+      paramerPreInput: 'Предустановленное Значение',
+      allowInput: 'Разрешить Пользовательский Ввод',
+      boolDate: 'Является Типом Даты',
+      boolTime: 'Является Типом Времени',
+      addValue: 'Добавить Значение',
+      yes: 'Да',
+      no: 'Нет',
+      paramNameRequired: 'Название Параметра обязательно',
+      editParamer: 'Редактировать Параметр',
+      addParamer: 'Добавить Параметр',
+      TEMPLATE_PARAMER_DELETE_FAILED: 'Параметр используется в шаблонных сообщениях и не может быть удален!'
+    },
+    openTypeEnum: {
+      video: 'Видео',
+      audio: 'Аудио',
+      images: 'Галерея Изображений',
+      article: 'Текст и Изображение'
+    }
+  }
+
+}

+ 66 - 0
languages/message-center/zh-CN.js

@@ -0,0 +1,66 @@
+module.exports = {
+
+  mc: {
+    messageCenter: '消息中心',
+    messages: '消息列表',
+    messageType: '消息分类',
+    messageTemplate: '消息模板',
+    templateParamer: '消息模板参数',
+    messageTypeModule: {
+      typeName: '分类名称',
+
+      addType: '添加消息分类',
+      editType: '修改消息分类',
+      typeNameRequired: '分类名称必填',
+      MESSAGE_TYPE_DELETE_FAILED: '分类已被消息引用,不可删除!',
+    },
+    messagesModule: {
+      addMessage: '添加消息',
+      title: '消息标题',
+      description: '消息摘要',
+      openType: '展示类型',
+      titleRequired: '消息标题必填',
+      openTypeRequired: '展示类型必填',
+      messageTypeRequired: '消息分类必填',
+      boolShared: '是否共享消息',
+      status: '消息状态',
+      enabledStatus: '启用消息',
+      disabled: '无效',
+      enabled: '有效',
+
+    },
+    messageTemplateModule: {
+      addMessageTemplate: '添加模版',
+      templateTitle: '模版标题',
+      templateContent: '模版内容',
+      editMessageTemplate: '修改模版',
+      templateTitleRequired: '模版标题必填',
+      templateContentRequired: '模版内容必填',
+      boolShared: '是否共享模版',
+      templateContentError: '模版参数{0}无效,请使用已定义模版参数,且不要嵌套使用参数',
+      insertParamerTip: '点击鼠标右键可以插入模版参数!',
+    },
+    templateParamerModule: {
+      addTemplateParamer: '添加参数',
+      paramerName: '参数名称',
+      paramerPreInput: '参数预设值',
+      allowInput: '允许自定义输入',
+      boolDate: '是否日期类型',
+      boolTime: '是否时间类型',
+      addValue: '添加值',
+      yes: '是',
+      no: '否',
+      paramNameRequired: '参数名称必填',
+      editParamer: '修改参数',
+      addParamer: '添加参数',
+      TEMPLATE_PARAMER_DELETE_FAILED: '参数已被模版消息引用,不可删除!',
+    },
+    openTypeEnum: {
+      video: '视频',
+      audio: '音频',
+      images: '图集',
+      article: '图文'
+    }
+  }
+
+}

+ 81 - 7
languages/ru-RU.js

@@ -127,7 +127,7 @@ module.exports = {
     filters: 'фильтр',
     loadingOoo: 'Идет загрузка...',
     noRowsToShow: 'Нет данных для отображения',
-    enabled: 'открыть',
+    // enabled: 'открыть',
     pinColumn: 'фиксированный столбец',
     pinLeft: 'Зафиксировано слева',
     pinRight: 'Закреплено справа',
@@ -1188,7 +1188,9 @@ module.exports = {
     S4G_INTERCOM: 'Диалог 4G',
     WATCH_IW: 'Умные часы - IW',
     MULTIFUNCTIONAL_BUTTON: 'Многофункциональная кнопка',
-    S433_BJMD: 'Сигнализация дверей'
+    S433_BJMD: 'Сигнализация дверей',
+    SOS_VOICE_BUTTON: 'Звуковая кнопка SOS',
+    PTT: 'ptt'
   },
   vitalSignsDeviceType: {
     BLOOD_SUGAR: 'Измеритель сахара в крови',
@@ -1389,13 +1391,17 @@ module.exports = {
   entraceguardUser: {
     named: 'Имя пользователя',
     idNo: 'Номер удостоверения личности',
-    ic: 'Номер карты IC',
+    ic: 'Номер IC-карты',
     phone: 'Номер телефона',
     password: 'Пароль доступа',
-    forbidden: 'Запрет на передвижение',
-    refreshUser: 'Actualizar usuarios',
-    yes: 'Да.',
-    nop: 'Нет'
+    forbidden: 'Запретить доступ',
+    refreshUser: 'Обновить доступных пользователей',
+    yes: 'Да',
+    nop: 'Нет',
+    expire: 'Дата истечения срока действия',
+    face: 'Фото для распознавания лица',
+    chooseImage: 'Выбрать изображение',
+    syncToDevice: 'Синхронизировать с устройством'
   },
   boardTitle: {
     add: 'Добавить информационный экран',
@@ -1609,6 +1615,7 @@ module.exports = {
     MULTIFUNCTIONAL_BUTTON: 'Многофункциональная кнопка',
     SLEEP_MATTRESS: 'Матрас для сна',
     NFC: 'Функции NFC',
+    LORA_BUTTON: 'Кнопка LoRa',
     linuxSerial1: 'Установка Linux 1',
     linuxSerial2: 'Установка Linux 2',
     linuxSerial3: 'Установка Linux 3',
@@ -1638,5 +1645,72 @@ module.exports = {
     deleteSqlMsg: 'Данные не подлежат восстановлению после операции удаления, вы уверены, что хотите удалить эти данные?',
     choiceTimeZone: 'Выберите часовой пояс',
     choiceLanguage: 'Выбор языка'
+  },
+  wu20240322: {
+    fileManager: 'Файловый Менеджер'
+  },
+  zy20240530: {
+    customerAffair: 'Услуги пользователей',
+    customerAffairAdd: 'Дополнительные услуги',
+    customerAffairConTent: 'Содержание транзакций',
+    timeMsg: 'Выберите время',
+    planTime: 'Запланированные сроки осуществления',
+    doTime: 'Фактическое время осуществления'
+  },
+  wu20240530: {
+    nurseConfig: {
+      default_icon: 'Иконка по умолчанию',
+      default_content_show: 'Отображение контента по умолчанию',
+      icon_text: 'Изображение + Текст',
+      icon: 'Только изображение',
+      text: 'Только текст'
+    },
+    nurseOption: {
+      icon_url: 'Иконка',
+      content_show: 'Способ отображения контента'
+    },
+    clear_icon: 'Очистить иконку'
+  },
+  wnn20240322: {
+    SOS_VOICE_BUTTON: 'Звуковая кнопка SOS',
+    PTT: 'ptt'
+  },
+  zy20240611: {
+    boolTransfer: 'Включить управляемый интерфейс',
+    hostDeviceLock: 'Включить права использования карты для стационарного монитора медсестры',
+    broadcastEnable: 'Активировать функцию трансляции',
+    learn: 'Изучать',
+    learnAll: 'Изучать все',
+    deleteAll: 'Удалить все',
+    deviceFunction: 'Функция устройства',
+    deviceButtonFunctionStr: 'Функция кнопки устройства',
+    selectKey: 'Пожалуйста, выберите ключ',
+    voiceCancel: 'Отменить вызов',
+    SMART_LIFE_GATEWAY: 'Умный шлюз жизни',
+    SMART_LIFE_VOICE_GATE: 'Умный шлюз голосовой жизни',
+    CURTAIN: 'Шторка',
+    SWITCH: 'Переключатель',
+    SIMULATE_BLUE_CODE: 'Имитация_Blue_Code',
+    switchType: 'Тип Переключателя',
+    SINGLE_SWITCH: 'Переключатель с одним кнопкой',
+    TWO_KEY_SWITCH: 'Переключатель с двумя кнопками',
+    THREE_KEY_SWITCH: 'Переключатель с тремя кнопками',
+    MINUTE10: '10 минут',
+    MINUTE15: '15 минут',
+    MINUTE20: '20 минут',
+    MINUTE30: '30 минут',
+    MINUTE45: '45 минут',
+    MINUTE60: '60 минут',
+    MINUTE90: '90 минут',
+    MINUTE120: '120 минут'
+  },
+  wu20240928: {
+    editImage: 'Редактировать изображение',
+    preview: 'Предварительный просмотр',
+    reload: 'Перезагрузить',
+    comfirm: 'Подтвердить',
+    cancel: 'Отменить',
+    incorrectFormat: 'Файл не в формате изображения',
+    fileSizeLimit: 'Пожалуйста, загрузите изображение меньше {size}M'
   }
 }

+ 92 - 14
languages/zh-CN.js

@@ -127,7 +127,7 @@ module.exports = {
     filters: '筛选器',
     loadingOoo: '加载中...',
     noRowsToShow: '无数据显示',
-    enabled: '开启',
+    // enabled: '开启',
     pinColumn: '固定列',
     pinLeft: '固定到左边',
     pinRight: '固定到右边',
@@ -1029,16 +1029,16 @@ module.exports = {
     clerkNameHiddenOnDoor: '门口机员工姓名隐藏',
     hidden: '开启隐藏',
     channelImHistoryStoreDays: '频道留言保留天数',
-    recordEnabled: '开启录音录像功能',
-    recordAble: '开启录音录像功能',
-    screenLight: '开启按钮亮屏并触发功能',
-    roomCallBed: '开启门口机呼叫床位按钮',
-    boolAllDoorStatus: '开启所有门口机呼显示',
+    recordEnabled: '录音录像功能',
+    recordAble: '录音录像功能',
+    screenLight: '按钮亮屏并触发功能',
+    roomCallBed: '门口机呼叫床位按钮',
+    boolAllDoorStatus: '所有门口机呼显示',
     boolDooLightAlwaysOn: '门灯是否常亮',
     convenientServiceEnabled: '便民服务',
     ledServiceEnabled: '服务端控制点阵屏',
-    autoPositionEnabled: '开启自动定位',
-    boolDisplayNcTitle: '开启护理标题显示'
+    autoPositionEnabled: '自动定位',
+    boolDisplayNcTitle: '护理标题显示'
   },
   role: {
     roleName: '角色名称',
@@ -1188,7 +1188,9 @@ module.exports = {
     S4G_INTERCOM: '4G对讲器',
     WATCH_IW: '智能手表-IW',
     MULTIFUNCTIONAL_BUTTON: '多功能按钮',
-    S433_BJMD: '报警门灯'
+    S433_BJMD: '报警门灯',
+    SOS_VOICE_BUTTON: 'SOS语音按钮',
+    PTT: '一键通'
   },
   vitalSignsDeviceType: {
     BLOOD_SUGAR: '血糖仪',
@@ -1395,7 +1397,11 @@ module.exports = {
     forbidden: '禁止通行',
     refreshUser: '刷新可用用户',
     yes: '是',
-    nop: '否'
+    nop: '否',
+    expire: '截止有效时间',
+    face: '人脸识别照片',
+    chooseImage: '选择图片',
+    syncToDevice: '同步到设备'
   },
   boardTitle: {
     add: '添加看板屏幕',
@@ -1608,10 +1614,11 @@ module.exports = {
     S433_BUTTON: '433按钮',
     MULTIFUNCTIONAL_BUTTON: '多功能按钮',
     SLEEP_MATTRESS: '睡眠床垫',
-    NFC: 'nfc功能',
-    linuxSerial1: 'linux串口1',
-    linuxSerial2: 'linux串口2',
-    linuxSerial3: 'linux串口3',
+    NFC: 'NFC功能',
+    LORA_BUTTON: 'LoRa按钮',
+    linuxSerial1: 'Linux串口1',
+    linuxSerial2: 'Linux串口2',
+    linuxSerial3: 'Linux串口3',
     androidSerial1: '安卓串口1',
     androidSerial2: '安卓串口2',
     androidSerial3: '安卓串口3',
@@ -1638,5 +1645,76 @@ module.exports = {
     deleteSqlMsg: '删除操作后数据不可复原,您确定要删除此数据?',
     choiceTimeZone: '选择时区',
     choiceLanguage: '选择语言'
+  },
+  wu20240322: {
+    fileManager: '文件管理器'
+  },
+  zy20240530: {
+    customerAffair: '用户事务',
+    customerAffairAdd: '新增事务',
+    customerAffairConTent: '事务内容',
+    timeMsg: '请选择时间',
+    planTime: '计划执行时间',
+    doTime: '实际执行时间'
+  },
+  wu20240530: {
+    nurseConfig: {
+      default_icon: '默认图标',
+      default_content_show: '默认内容显示',
+      icon_text: '图片+文字',
+      icon: '仅图片',
+      text: '仅文字'
+    },
+    nurseOption: {
+      icon_url: '图标',
+      content_show: '内容显示方式'
+    },
+    clear_icon: '清除图标'
+  },
+
+  wu20240604: {
+    textDisplayHospitalModuleTitle: '院级统计信息'
+  },
+  wnn20240322: {
+    SOS_VOICE_BUTTON: 'SOS语音按钮',
+    PTT: '一键通'
+  },
+  zy20240611: {
+    boolTransfer: '托管界面',
+    hostDeviceLock: '护士主机刷卡权限',
+    broadcastEnable: '广播功能',
+    learn: '学习',
+    learnAll: '全部学习',
+    deleteAll: '全部删除',
+    deviceFunction: '设备功能',
+    deviceButtonFunctionStr: '设备按钮功能',
+    selectKey: '请选择键值',
+    voiceCancel: '取消呼叫',
+    SMART_LIFE_GATEWAY: '智慧生活网关',
+    SMART_LIFE_VOICE_GATE: '智慧生活语音网关',
+    CURTAIN: '窗帘',
+    SWITCH: '开关',
+    SIMULATE_BLUE_CODE: '模拟BLUE CODE',
+    switchType: '开关类型',
+    SINGLE_SWITCH: '单键开关',
+    TWO_KEY_SWITCH: '双键开关',
+    THREE_KEY_SWITCH: '三键开关',
+    MINUTE10: '10分钟',
+    MINUTE15: '15分钟',
+    MINUTE20: '20分钟',
+    MINUTE30: '30分钟',
+    MINUTE45: '45分钟',
+    MINUTE60: '60分钟',
+    MINUTE90: '90分钟',
+    MINUTE120: '120分钟'
+  },
+  wu20240928: {
+    editImage: '编辑图片',
+    preview: '预览',
+    reload: '重新上传',
+    comfirm: '确定',
+    cancel: '取消',
+    incorrectFormat: '不是图片格式文件',
+    fileSizeLimit: '请上传小于{size}M的图片'
   }
 }

+ 1 - 1
nginx.conf

@@ -29,7 +29,7 @@ http {
 
     #keepalive_timeout  0;
     keepalive_timeout  65;
-    client_max_body_size 10m;
+    client_max_body_size 20G;
     gzip on;
     gzip_min_length  5k;
     gzip_buffers     4 16k;

+ 30 - 2
package.json

@@ -15,10 +15,22 @@
     "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/core": "^3.11.3",
+    "@uppy/locales": "^3.5.2",
+    "@uppy/xhr-upload": "^3.6.6",
+    "@videojs/themes": "^1.0.1",
     "@wangeditor/editor": "^5.1.23",
     "@wangeditor/editor-for-vue": "^1.0.2",
+    "ace-builds": "^1.33.2",
     "ag-grid-community": "^25.0.0",
     "ag-grid-vue": "^25.0.0",
     "axios": "0.18.1",
@@ -26,11 +38,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",
@@ -40,6 +59,7 @@
     "jsonlint": "1.6.3",
     "jszip": "3.2.1",
     "jwt-decode": "^3.1.2",
+    "less-loader": "^12.2.0",
     "moment": "^2.29.4",
     "moment-timezone": "^0.5.43",
     "nanoid": "^4.0.2",
@@ -51,6 +71,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",
@@ -58,6 +80,7 @@
     "vue-baidu-map": "^0.21.22",
     "vue-class-component": "^7.2.6",
     "vue-count-to": "^1.0.13",
+    "vue-cropper": "^0.5.2",
     "vue-draggable-resizable": "^2.3.0",
     "vue-i18n": "^8.26.1",
     "vue-json-viewer": "^2.2.22",
@@ -84,15 +107,19 @@
     "chalk": "2.4.2",
     "chokidar": "2.1.5",
     "connect": "3.6.6",
+    "editorjs-inline-image": "^2.0.0",
     "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",
+    "postcss-preset-env": "^9.5.2",
     "runjs": "4.3.2",
     "sass": "1.26.2",
     "sass-loader": "8.0.2",
@@ -100,8 +127,9 @@
     "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",
+    "video.js": "^8.10.0",
+    "vue-template-compiler": "2.6.10"
   },
   "browserslist": [
     "> 1%",

+ 13 - 11
public/domain.js

@@ -1,17 +1,19 @@
 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://47.254.27.69:8005',
+  DeviceUrl: 'http://47.254.27.69:8006',
+  mediaUrl: 'http://47.254.27.69:8004',
   OnlineSystemUrl: 'http://api.base.wdklian.com',
+  gateWayUrl:'http://8.129.220.143:8014',
+  fileServer: 'http://8.129.220.143:8012',
   apiMode: 'dev',
   uiVersion: 1, // 1 医院版,2 月子中心版,3养老院版
-  enableBroadcast: false, //广播使能
-  enableMobile: false,  //手机使能
-  enableEntraceguard: false,  //门禁使能
-  enableNBiot: false,  //NB设备
-  enableCustomerDevice: false,  //用户设备
-  enableSosDevice: false, //报警设备
-  enable485: false,
-  enableLinux: false
+  enableBroadcast: true, //广播使能
+  enableMobile: true,  //手机使能
+  enableEntraceguard: true,  //门禁使能
+  enableNBiot: true,  //true
+  enableCustomerDevice: true,  //用户设备
+  enableSosDevice: true, //报警设备
+  enable485: true,
+  enableLinux: true
 }
 

+ 11 - 0
public/index.html

@@ -6,6 +6,17 @@
     <meta name="renderer" content="webkit">
     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
     <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <!-- City -->
+    <link href="https://unpkg.com/@videojs/themes@1/dist/city/index.css" rel="stylesheet">
+
+    <!-- Fantasy -->
+    <link href="https://unpkg.com/@videojs/themes@1/dist/fantasy/index.css" rel="stylesheet">
+
+    <!-- Forest -->
+    <link href="https://unpkg.com/@videojs/themes@1/dist/forest/index.css" rel="stylesheet">
+
+    <!-- Sea -->
+    <link href="https://unpkg.com/@videojs/themes@1/dist/sea/index.css" rel="stylesheet">
     <script src="/domain.js" ></script>
       <!--<script src="/janus.js" ></script>-->
       <!--<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/6.4.0/adapter.min.js" ></script>-->

+ 2 - 1
set-envs.sh

@@ -2,7 +2,8 @@ echo "const domain = {
   serverUrl: '${serverUrl}',
   DeviceUrl: '${DeviceUrl}',
   mediaUrl: '${mediaUrl}',
-  OnlineSystemUrl: '${API_MODEL}',
+  OnlineSystemUrl: '${OnlineSystemUrl}',
+  gateWayUrl: '${gateWayUrl}', //网关地址
   apiMode: '${apiMode}',
   uiVersion: ${uiVersion}, // 1 医院版,2 月子中心版,3养老院版
   enableBroadcast: ${enableBroadcast}, //广播使能

+ 58 - 0
src/api/message_template.js

@@ -0,0 +1,58 @@
+import request from '@/utils/request'
+
+/**
+ * 消息模版接口
+ * @param params
+ * @returns {*|Promise|Promise<unknown>}
+ */
+export function getList(params) {
+  return request({
+    baseURL: domain.gateWayUrl,
+    url: '/message-center/template-message-tpl/page',
+    method: 'POST',
+    loading: true,
+    data: params,
+    headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
+  })
+}
+
+/** 新增消息模版 */
+export function add(params) {
+  return request({
+    baseURL: domain.gateWayUrl,
+    url: '/message-center/template-message-tpl',
+    method: 'POST',
+    loading: true,
+    data: params
+  })
+}
+
+/** 删除消息模版 */
+export function remove(params) {
+  const ids = params.toString()
+  return request({
+    baseURL: domain.gateWayUrl,
+    url: `/message-center/template-message-tpl/${ids}`,
+    method: 'DELETE',
+    loading: true,
+    data: params
+  })
+}
+
+/** 更新消息模版 */
+export function update(id, params) {
+  return request({
+    baseURL: domain.gateWayUrl,
+    url: `/message-center/template-message-tpl/${id}`,
+    method: 'put',
+    data: params
+  })
+}
+/** 根据id查询消息模版 */
+export function get(id) {
+  return request({
+    baseURL: domain.gateWayUrl,
+    url: `/message-center/template-message-tpl/${id}`,
+    method: 'get'
+  })
+}

+ 59 - 0
src/api/message_type.js

@@ -0,0 +1,59 @@
+import request from '@/utils/request'
+
+/**
+ * 消息类型接口
+ * @param params
+ * @returns {*|Promise|Promise<unknown>}
+ */
+export function getList(params) {
+  return request({
+    baseURL: domain.gateWayUrl,
+    url: '/message-center/message-type/page',
+    method: 'POST',
+    loading: true,
+    data: params,
+    headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
+  })
+}
+
+/** 新增消息类型 */
+export function add(params) {
+  return request({
+    baseURL: domain.gateWayUrl,
+    url: '/message-center/message-type',
+    method: 'POST',
+    loading: true,
+    data: params
+  })
+}
+
+/** 删除消息类型 */
+export function remove(params) {
+  const ids = params.toString()
+  return request({
+    baseURL: domain.gateWayUrl,
+    url: `/message-center/message-type/${ids}`,
+    method: 'DELETE',
+    loading: true,
+    data: params
+  })
+}
+
+/** 更新消息类型 */
+export function update(id, params) {
+  return request({
+    baseURL: domain.gateWayUrl,
+    url: `/message-center/message-type/${id}`,
+    method: 'put',
+    data: params
+  })
+}
+
+/** 更新消息类型 */
+export function getAll() {
+  return request({
+    baseURL: domain.gateWayUrl,
+    url: `/message-center/message-type/all`,
+    method: 'get'
+  })
+}

+ 58 - 0
src/api/messages.js

@@ -0,0 +1,58 @@
+import request from '@/utils/request'
+
+/**
+ * 消息类型接口
+ * @param params
+ * @returns {*|Promise|Promise<unknown>}
+ */
+export function getList(params) {
+  return request({
+    baseURL: domain.gateWayUrl,
+    url: '/message-center/messages/page',
+    method: 'POST',
+    loading: true,
+    data: params,
+    headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
+  })
+}
+
+/** 新增消息 */
+export function add(params) {
+  return request({
+    baseURL: domain.gateWayUrl,
+    url: '/message-center/messages',
+    method: 'POST',
+    loading: true,
+    data: params
+  })
+}
+
+/** 删除消息 */
+export function remove(params) {
+  const ids = params.toString()
+  return request({
+    baseURL: domain.gateWayUrl,
+    url: `/message-center/messages/${ids}`,
+    method: 'DELETE',
+    loading: true,
+    data: params
+  })
+}
+
+/** 更新消息 */
+export function update(id, params) {
+  return request({
+    baseURL: domain.gateWayUrl,
+    url: `/message-center/messages/${id}`,
+    method: 'put',
+    data: params
+  })
+}
+/** 根据id查询消息 */
+export function get(id) {
+  return request({
+    baseURL: domain.gateWayUrl,
+    url: `/message-center/messages/${id}`,
+    method: 'get'
+  })
+}

+ 10 - 2
src/api/ncs_board.js

@@ -15,7 +15,6 @@ export function getBoardTitles(partid) {
 }
 
 
-
 export function getBedInfo(partid) {
   return request({
     baseURL:domain.DeviceUrl,
@@ -25,7 +24,6 @@ export function getBedInfo(partid) {
   })
 }
 
-
 export function getPartStatisticSummary(partid) {
   return request({
     baseURL:domain.DeviceUrl,
@@ -34,3 +32,13 @@ export function getPartStatisticSummary(partid) {
     loading: false
   })
 }
+
+/** 获取医院级联科室统计信息 */
+export function getHospitalStatisticsCascader(partId) {
+  return request({
+    baseURL:domain.DeviceUrl,
+    url: `/boardinfo/get-hospital-statistics-cascader/${partId}`,
+    method: 'get',
+    loading: false
+  })
+}

+ 9 - 0
src/api/ncs_customer.js

@@ -68,10 +68,19 @@ export function saveListByCustomer(partId) {
     loading: true
   })
 }
+
 export function getListByPartId(partId) {
   return request({
     url: `/ncs/customer/getListByPartId/${partId}`,
     method: 'GET',
     loading: true
   })
+}
+
+export function getCustomerAndFrameListByPartId(partId) {
+  return request({
+    url: `/ncs/customer/get_customer_and_frame_list_by_part_id/${partId}`,
+    method: 'GET',
+    loading: true
+  })
 }

+ 56 - 0
src/api/ncs_customer_affair.js

@@ -0,0 +1,56 @@
+import request from '@/utils/request'
+
+/**
+ * 用户事务相关接口
+ * @param params
+ * @returns {*|Promise|Promise<unknown>}
+ */
+export function getList(params) {
+  return request({
+    url: '/ncs/customer_affair/page',
+    method: 'POST',
+    loading: true,
+    data: params,
+    headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
+  })
+}
+
+/** 新增用户事务 */
+export function add(params) {
+  return request({
+    url: '/ncs/customer_affair',
+    method: 'POST',
+    loading: true,
+    data: params
+  })
+}
+
+/** 删除用户事务 */
+export function remove(params) {
+  const ids = params.toString()
+  return request({
+    url: `/ncs/customer_affair/${ids}`,
+    method: 'DELETE',
+    loading: true,
+    data: params
+  })
+}
+
+/** 更新用户事务 */
+export function update(id, params) {
+  return request({
+    url: `/ncs/customer_affair/${id}`,
+    method: 'put',
+    data: params
+  })
+}
+
+/** 查询用户事务 */
+export function get(id, params) {
+  return request({
+    url: `/ncs/customer_affair/${id}`,
+    method: 'get',
+    loading: false,
+    params
+  })
+}

+ 42 - 1
src/api/ncs_device.js

@@ -284,7 +284,7 @@ export function getLocationDeviceList(partid) {
     loading: false
   })
 }
-/** 查询定位设备 */
+
 export function getDeviceList(frameId, type) {
   return request({
     url: `/ncs/device/getDeviceList/${frameId}/${type}`,
@@ -293,3 +293,44 @@ export function getDeviceList(frameId, type) {
   })
 }
 
+/** 获取转换盒存储的按钮 */
+export function getLoraButtons(mac) {
+  return request({
+    url: `/ncs/device/get_lora_buttons/${mac}`,
+    method: 'get',
+    loading: false
+  })
+}
+/** 刷新转换盒存储的按钮 */
+export function refreshLoraButtons(mac) {
+  return request({
+    url: `/ncs/device/refresh_lora_buttons/${mac}`,
+    method: 'get',
+    loading: false
+  })
+}
+/** 转换盒添加按钮 */
+export function addLoraButtons(mac, lora_list) {
+  return request({
+    url: `/ncs/device/add_lora_buttons/${mac}/${lora_list}`,
+    method: 'get',
+    loading: false
+  })
+}
+/** 转换盒删除按钮 */
+export function delLoraButtons(mac, lora_mac) {
+  return request({
+    url: `/ncs/device/del_lora_buttons/${mac}/${lora_mac}`,
+    method: 'get',
+    loading: false
+  })
+}
+/** 转换盒清空存储的按钮 */
+export function delAllLoraButtons(mac) {
+  return request({
+    url: `/ncs/device/del_all_lora_buttons/${mac}`,
+    method: 'get',
+    loading: false
+  })
+}
+

+ 8 - 0
src/api/ncs_entrace_guard_user.js

@@ -26,3 +26,11 @@ export function refreshUser(part_id) {
     method: 'post'
   })
 }
+
+export function syncUserToDevice(part_id) {
+  return request({
+    url: `/entrace-guard-user/sync-to-device/${part_id}`,
+    method: 'POST',
+    baseURL:domain.DeviceUrl
+  })
+}

+ 9 - 0
src/api/ncs_event.js

@@ -47,3 +47,12 @@ export function updateRoleId(params) {
     data: params
   })
 }
+
+/** 批量修改roleId */
+export function getEventListByPartId(part_id) {
+  return request({
+    url: `/ncs/event/get_event_list_by_part_id/${part_id}`,
+    method: 'get',
+    loading: false
+  })
+}

+ 67 - 0
src/api/template_paramer.js

@@ -0,0 +1,67 @@
+import request from '@/utils/request'
+
+/**
+ * 模版参数接口
+ * @param params
+ * @returns {*|Promise|Promise<unknown>}
+ */
+export function getList(params) {
+  return request({
+    baseURL: domain.gateWayUrl,
+    url: '/message-center/template-message-paramer/page',
+    method: 'POST',
+    loading: true,
+    data: params,
+    headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
+  })
+}
+
+/** 新增模版参数 */
+export function add(params) {
+  return request({
+    baseURL: domain.gateWayUrl,
+    url: '/message-center/template-message-paramer',
+    method: 'POST',
+    loading: true,
+    data: params
+  })
+}
+
+/** 删除模版参数 */
+export function remove(params) {
+  const ids = params.toString()
+  return request({
+    baseURL: domain.gateWayUrl,
+    url: `/message-center/template-message-paramer/${ids}`,
+    method: 'DELETE',
+    loading: true,
+    data: params
+  })
+}
+
+/** 更新模版参数 */
+export function update(id, params) {
+  return request({
+    baseURL: domain.gateWayUrl,
+    url: `/message-center/template-message-paramer/${id}`,
+    method: 'put',
+    data: params
+  })
+}
+
+/** 根据id查询模版参数 */
+export function get(id) {
+  return request({
+    baseURL: domain.gateWayUrl,
+    url: `/message-center/template-message-paramer/${id}`,
+    method: 'get'
+  })
+}
+
+export function getAll() {
+  return request({
+    baseURL: domain.gateWayUrl,
+    url: '/message-center/template-message-paramer/all',
+    method: 'get'
+  })
+}

+ 15 - 0
src/api/upload.js

@@ -12,3 +12,18 @@ export function upload(file) {
     ]
   })
 }
+
+export function uploadSenceFile(file, scene) {
+  console.log(file)
+  return request({
+    url: `ncs/upload/uploadFile?scene=${scene}`,
+    method: 'POST',
+    data: file,
+    headers: { 'Content-Type': 'multipart/form-data' },
+    transformRequest: [
+      function() {
+        return file
+      }
+    ]
+  })
+}

+ 64 - 0
src/components/AgGridCellRender/TagsCellRender.vue

@@ -0,0 +1,64 @@
+
+<template>
+  <div class="tag-wrap">
+    <el-tag
+        v-for="item in tagList"
+        :key="item"
+        :type="tagType"
+        :closable="closable"
+        :size="tagSize">
+      {{ item }}
+    </el-tag>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "TagsCellRender",
+  data() {
+    return {
+      tagType: 'success',
+      tagSize: 'mini',
+      closable: false,
+      tagList: []
+    }
+  },
+  beforeMount() {
+  },
+  mounted() {
+    const _this = this
+    _this.tagList = this.params.list
+    const { tagType, tagSize, closable,tagList } = this.params
+    if (tagType) {
+      this.tagType = tagType
+    }
+    if (tagSize) {
+      this.tagSize = tagSize
+    }
+    if (closable) {
+      this.closable = closable
+    }
+    this.tagList=tagList
+  },
+  methods: {
+  }
+}
+</script>
+
+
+
+<style scoped>
+.el-tag{
+  margin-right: 5px;
+  margin-top: 5px;
+}
+
+.tag-wrap{
+  display: flex;
+  width: 100%;
+  flex-wrap: wrap;
+  align-items: flex-start;
+  justify-content: flex-start;
+}
+
+</style>

+ 34 - 0
src/components/EditorjsTools/AudioTool/index.css

@@ -0,0 +1,34 @@
+.wrapper {
+     display: flex;
+     flex-direction: column;
+     align-items: center;
+     min-height: 3rem;
+     justify-content: space-around;
+ }
+.wrapper .input {
+    background-color: aliceblue;
+}
+
+.wrapper input {
+    width: 100%;
+    padding: 10px;
+    border: 1px solid #e4e4e4;
+    border-radius: 3px;
+    outline: none;
+    font-size: 14px;
+}
+.cdx-plus-holder{
+    background-color: #fbfdff;
+    border: 1px dashed #c0ccda;
+    border-radius: 6px;
+    box-sizing: border-box;
+    width: 100%;
+    height: 50px;
+    cursor: pointer;
+    text-align: center;
+    line-height: 50px;
+    vertical-align: top;
+}
+.cdx-plus-holder .el-icon-plus{
+    font-style: normal;
+}

File diff suppressed because it is too large
+ 145 - 0
src/components/EditorjsTools/AudioTool/index.js


+ 77 - 0
src/components/EditorjsTools/ImageTool/index.css

@@ -0,0 +1,77 @@
+.cdx-simple-image {
+}
+
+.el-loading-mask .el-loading-spinner{
+    font-size: 14px;
+}
+.cdx-plus-holder{
+    background-color: #fbfdff;
+    border: 1px dashed #c0ccda;
+    border-radius: 6px;
+    box-sizing: border-box;
+    width: 100%;
+    height: 50px;
+    cursor: pointer;
+    text-align: center;
+    line-height: 50px;
+    vertical-align: top;
+}
+.cdx-plus-holder .el-icon-plus{
+    font-style: normal;
+}
+.cdx-simple-image__picture {
+     max-width: 100%;
+     vertical-align: bottom;
+     display: block;
+    font-size: 0;
+ }
+.cdx-simple-image .cdx-loader {
+    min-height: 200px;
+}
+
+.cdx-simple-image .cdx-input {
+    margin-top: 10px;
+}
+
+.cdx-simple-image img {
+    max-width: 100%;
+    vertical-align: bottom;
+}
+
+.cdx-simple-image__caption[contentEditable=true][data-placeholder]:empty::before {
+    position: absolute;
+    content: attr(data-placeholder);
+    color: #707684;
+    font-weight: normal;
+    opacity: 0;
+}
+
+.cdx-simple-image__caption[contentEditable=true][data-placeholder]:empty::before {
+    opacity: 1;
+}
+
+.cdx-simple-image__caption[contentEditable=true][data-placeholder]:empty:focus::before {
+    opacity: 0;
+}
+
+
+.cdx-simple-image__picture--with-background {
+    background: #eff2f5;
+    padding: 10px;
+}
+
+.cdx-simple-image__picture--with-background img {
+    display: block;
+    max-width: 60%;
+    margin: 0 auto;
+}
+
+.cdx-simple-image__picture--with-border {
+    border: 1px solid #e8e8eb;
+    padding: 1px;
+}
+
+.cdx-simple-image__picture--stretched img {
+    max-width: none;
+    width: 100%;
+}

File diff suppressed because it is too large
+ 211 - 0
src/components/EditorjsTools/ImageTool/index.js


+ 33 - 0
src/components/EditorjsTools/VideoTool/index.css

@@ -0,0 +1,33 @@
+.wrapper {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: space-around;
+}
+.wrapper .input {
+    background-color: aliceblue;
+}
+
+.wrapper input {
+    width: 100%;
+    padding: 10px;
+    border: 1px solid #e4e4e4;
+    border-radius: 3px;
+    outline: none;
+    font-size: 14px;
+}
+.cdx-plus-holder{
+    background-color: #fbfdff;
+    border: 1px dashed #c0ccda;
+    border-radius: 6px;
+    box-sizing: border-box;
+    width: 100%;
+    height: 50px;
+    cursor: pointer;
+    text-align: center;
+    line-height: 50px;
+    vertical-align: top;
+}
+.cdx-plus-holder .el-icon-plus{
+    font-style: normal;
+}

+ 172 - 0
src/components/EditorjsTools/VideoTool/index.js

@@ -0,0 +1,172 @@
+import { IconPlay } from '@codexteam/icons'
+import './index.css'
+import videojs from 'video.js'
+import 'video.js/dist/video-js.css'
+export default class VideoTool {
+  constructor({ data, block, config, api }) {
+    /**
+         * Nodes cache
+         */
+
+    this.nodes = {
+      wrapper: null,
+      video: null,
+      plusHolder: null,
+      popOver: null,
+      videoSource: null
+    }
+    this.api = api
+    this.player = null
+    this.data = {
+      src: data.src || '',
+      withBorder: data.withBorder !== undefined ? data.withBorder : false,
+      withBackground: data.withBackground !== undefined ? data.withBackground : false,
+      stretched: data.stretched !== undefined ? data.stretched : false
+
+    }
+    // console.log('thisdata', this.data)
+    this.config = config
+    this.block = block
+  }
+
+  static get toolbox() {
+    return {
+      title: 'VideoPlayer',
+      icon: IconPlay
+    }
+  }
+
+  changeImageUrl(url) {
+    this.data = {
+      src: url
+    }
+  }
+  afterRender() {
+    // videojs(`vedio_${this.block.id}`)
+  }
+  render() {
+    this.nodes.wrapper = this._make('div', '', { style: 'height:100px;' })
+    this.nodes.video = this._make('video', 'video-js', { style: 'display:none;width:100%;', id: `vedio_${this.block.id}` })
+    this.nodes.videoSource = this._make('source', '', { type: 'video/mp4' })
+    this.nodes.video.appendChild(this.nodes.videoSource)
+    this.nodes.wrapper.classList.add('wrapper')
+    this.nodes.video.setAttribute('controls', '')
+    this.nodes.video.setAttribute('preload', '')
+    this.nodes.plusHolder = this._make('div', 'cdx-plus-holder', {
+      onclick: () => {
+        this.config.onChoose(this.block.id, this.api.blocks.getCurrentBlockIndex())
+      }
+    })
+    const plus = this._make('i', ['el-icon-plus'], {})
+    this.nodes.plusHolder.appendChild(plus)
+    // this.nodes.input.type = 'text'
+    // this.nodes.input.name = 'audioUrl'
+    // this.nodes.input.placeholder = 'Enter your url of audio file'
+    // this.nodes.input.value = this.data.src ? this.data.src : ''
+    // this.nodes.input.classList.add('input')
+    // this.nodes.input.addEventListener('change', () => {
+    //   const src = this.nodes.input?.value
+    //   if (src) {
+    //     this.nodes.audio.setAttribute('src', src)
+    //   } else {
+    //     this.nodes.audio.removeAttribute('src')
+    //   }
+    // })
+    if (this.data.src) {
+      this.nodes.videoSource.setAttribute('src', this.data.src)
+    }
+    // const { src } = this.data
+    // if (src) {
+    //
+    // }
+
+    // this.nodes.wrapper.appendChild(this.nodes.input)
+    this.nodes.wrapper.appendChild(this.nodes.video)
+    this.nodes.wrapper.appendChild(this.nodes.plusHolder)
+
+    if (this.data.src) {
+      this.nodes.videoSource.src = this.data.src
+      this.nodes.plusHolder.style.display = 'none'
+      this.nodes.video.style.display = 'block'
+      this.nodes.wrapper.style.height = '500px'
+      const id = this.block.id
+
+      setTimeout(() => {
+        this.player = videojs(`vedio_${id}`)
+      }, 500)
+      //
+    } else {
+      this.nodes.video.style.display = 'none'
+      this.nodes.wrapper.style.height = '100px'
+      this.nodes.plusHolder.style.display = 'block'
+    }
+
+    return this.nodes.wrapper
+  }
+
+  save(blockContent) {
+    const video = blockContent.querySelector('video')
+    const src = video.querySelector('source').src
+
+    return {
+      src
+    }
+  }
+
+  validate(savedData) {
+    if (savedData.src === '' && !savedData.src.trim()) {
+      return false
+    }
+
+    return true
+  }
+
+  _make(tagName, classNames = null, attributes = {}) {
+    const el = document.createElement(tagName)
+
+    if (Array.isArray(classNames)) {
+      el.classList.add(...classNames)
+    } else if (classNames) {
+      el.classList.add(classNames)
+    }
+
+    for (const attrName in attributes) {
+      el[attrName] = attributes[attrName]
+    }
+
+    return el
+  }
+
+  get data() {
+    return this._data
+  }
+  static get isReadOnlySupported() {
+    return true
+  }
+  set data(data) {
+    // console.log('setdata')
+    this._data = Object.assign({}, this.data, data)
+    if (this.nodes.video && this.nodes.videoSource) {
+      if (this._data.src) {
+        this.nodes.videoSource.src = this._data.src
+        this.nodes.plusHolder.style.display = 'none'
+        this.nodes.video.style.display = 'block'
+        this.nodes.wrapper.style.height = '500px'
+        const id = this.block.id
+        setTimeout(function() {
+          videojs(`vedio_${id}`)
+        }, 500)
+        // console.log(this.nodes.video)
+      } else {
+        this.nodes.video.style.display = 'none'
+        this.nodes.plusHolder.style.display = 'block'
+        this.nodes.wrapper.style.height = '100px'
+      }
+    }
+  }
+  destroy() {
+    if (this.player) {
+      this.player.dispose()
+    }
+  }
+}

+ 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: []
+  }
+}

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

@@ -0,0 +1,287 @@
+<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':(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'
+            },
+            height:{
+                type:Number,
+                default:null
+            },
+            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>

File diff suppressed because it is too large
+ 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('{0} item(s) selected.',[selectedItemCount]) : '' }}</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>

File diff suppressed because it is too large
+ 251 - 0
src/components/FileManager/components/Toolbar.vue


File diff suppressed because it is too large
+ 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>
+

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

@@ -0,0 +1,52 @@
+<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="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">{{  ServiceContainer.modal.data.title}}</h3>
+        <div class="mt-2">
+          <p class="text-sm text-gray-500">{{  ServiceContainer.modal.data.message}}</p>
+        </div>
+      </div>
+    </div>
+
+    <template v-slot:buttons>
+      <button type="button" @click="close" class="vf-btn vf-btn-secondary">
+        {{ t('Close') }}</button>
+    </template>
+  </modal-layout>
+</template>
+
+<script>
+    import ModalLayout from "./ModalLayout";
+export default {
+  name: 'ModalMessage',
+    components: {ModalLayout},
+    inject: ['ServiceContainer'],
+    data(){
+      return{
+
+      }
+    },
+    methods:{
+      t(str,...arg){
+          return this.$t(str,arg)
+      },
+        close(){
+        this.$bus.$emit('vf-modal-close')
+        }
+    }
+};
+</script>
+
+<!--<script setup>-->
+<!--import VFModalLayout from './ModalLayout.vue';-->
+<!--import {inject} from 'vue';-->
+<!--const app = inject('ServiceContainer');-->
+<!--const {t} = app.i18n;-->
+
+<!--</script>-->

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

@@ -0,0 +1,157 @@
+<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">{{ t('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">
+<!--        {{ t(''${items.length} item(s) selected.''  }}-->
+        {{ t('{0} item(s) selected.',[items.length])}}
+      </div>
+    </template>
+  </ModalLayout>
+</template>
+
+
+
+<script>
+
+  import ModalLayout from "./ModalLayout";
+  import Message from './ModalMessage'
+  export default {
+    name: "ModalMove",
+    components: {ModalLayout,Message},
+    inject: ['ServiceContainer'],
+    data(){
+      return{
+        items:this.ServiceContainer.modal.data.items.from,
+        toItem:this.ServiceContainer.modal.data.items.to,
+        message:'aaa'
+      }
+    },
+    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,...arg){
+        return this.$t(item,arg)
+      }
+
+    }
+  }
+
+</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('{0} 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('{0} 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('{0} 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>-->

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

@@ -0,0 +1,259 @@
+<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",
+    "{0} item(s) selected.": "{0} Element(e) ausgewählt.",
+    "{0} is renamed.": "{0} wurde umbenannt.",
+    "This is a readonly storage.": "Dies ist ein schreibgeschützter Speicher.",
+    "{0} is created.": "{0} 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",
+  "{0} item(s) selected.": "{0} item(s) selected.",
+  "{0} is renamed." : "{0} is renamed.",
+  "This is a readonly storage." : "This is a readonly storage.",
+  "{0} is created." : "{0} 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" : "نتیجه جستجو برای",
+  "{0} item(s) selected.": " آیتم(های) انتخاب شده{0}",
+  "{0} is renamed." : "تغییر نام برای %s صورت گرفت.",
+  "This is a readonly storage." : "این فضا فقط قابل خواندن است!",
+  "{0} is created." : "{0} ساخته شد!",
+  "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",
+  "{0} item(s) selected.": "{0} élément(s) sélectionné(s).",
+  "{0} is renamed." : "{0} est renommé.",
+  "This is a readonly storage." : "C'est un stockage en lecture seule.",
+  "{0} is created." : "{0} 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": "תוצאות חיפוש עבור",
+  "{0} item(s) selected.": "{0} פריט(ים) נבחרו.",
+  "{0} is renamed.": "השם של %s השתנה.",
+  "This is a readonly storage.": "זהו אחסון לקריאה בלבד.",
+  "{0} is created.": "{0} נוצר",
+  "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": "के लिए खोज परिणाम",
+  "{0} item(s) selected.": "{0} आइटम(आइटम) चयनित।",
+  "{0} is renamed.": "{0} का नाम बदला गया है।",
+  "This is a readonly storage.": "यह एक केवल पठनीय संग्रह है।",
+  "{0} is created.": "{0} बनाया गया है।",
+  "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": "Результаты поиска по",
+  "{0} item(s) selected.": "{0} выбраны.",
+  "{0} is renamed.": "{0} переименован.",
+  "This is a readonly storage.": "Данное хранилище только для чтения.",
+  "{0} is created.": "{0} создан.",
+  "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",
+  "{0} item(s) selected.": "{0} objekt valda.",
+  "{0} is renamed." : "{0} har fått ett nytt namn.",
+  "This is a readonly storage." : "Detta är en skrivskyddad lagring.",
+  "{0} is created." : "{0} 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ı:",
+  "{0} item(s) selected.": "{0} dosya seçildi.",
+  "{0} is renamed." : "{0} yeniden adlandırılmıştır.",
+  "This is a readonly storage." : "Bu salt okunur bir depolama alanıdır.",
+  "{0} is created." : "{0} 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,
+}

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

@@ -0,0 +1,90 @@
+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': '搜索结果为',
+  '{0} item(s) selected.': '{0} 个文件 已选择。',
+  '{0} is renamed.': '{0} 已重命名。',
+  'This is a readonly storage.': '这是只读存储。',
+  '{0} is created.': '{0} 已创建。',
+  '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 {0}': '关于 {0}',
+  '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,
+  'Select': '选择'
+}

+ 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" : "搜尋結果為",
+  "{0} item(s) selected.": "{0} 個檔案 已選取。",
+  "{0} is renamed." : "{0} 已重新命名。",
+  "This is a readonly storage." : "這是只讀存儲器。",
+  "{0} is created." : "{0} 已創建。",
+  "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');
+};

+ 292 - 0
src/components/ReCropperPreview/index.vue

@@ -0,0 +1,292 @@
+<template>
+  <el-dialog
+      :title="this.$t('wu20240928.editImage')"
+      class="cropper-dialog"
+      :close-on-click-modal="false"
+      :visible="dialogVisible"
+      center
+      @close="close"
+  >
+    <div class="cropper-wrap">
+      <div
+          class="cropper-box"
+          :style="cropperStyle"
+      >
+        <vue-cropper
+            ref="cropper"
+            :img="option.img"
+            :output-size="option.size"
+            :output-type="option.outputType"
+            :info="option.info"
+            :full="option.full"
+            :canScale="option.canScale"
+            :can-move="option.canMove"
+            :can-move-box="option.canMoveBox"
+            :fixed="option.fixed"
+            :fixed-box="option.fixedBox"
+            :original="option.original"
+            :auto-crop="option.autoCrop"
+            :auto-crop-width="option.autoCropWidth"
+            :auto-crop-height="option.autoCropHeight"
+            :center-box="option.centerBox"
+            :high="option.high"
+            :info-true="option.infoTrue"
+            :max-img-size="option.maxImageSize"
+            :enlarge="option.enlarge"
+            :mode="option.mode"
+            :maxImgSize="option.maxImgSize"
+            @realTime="realTime"
+        />
+      </div>
+      <div class="preview-box">
+        <div class="preview-title">
+          <span>{{this.$t('wu20240928.preview')}}</span>
+          <span @click="upload" class="preveiw-upload">{{this.$t('wu20240928.reload')}}</span>
+        </div>
+        <input
+            ref="upload"
+            type="file"
+            style="position:absolute; clip:rect(0 0 0 0);"
+            accept="image/png, image/jpeg, image/jpg"
+            @change="uploadImg"
+        >
+        <div
+
+            class="preview-img"
+
+        >
+<!--          <div :class="preview.div" :style="previewStyle">-->
+<!--            <img-->
+<!--                :style="preview.img"-->
+<!--                :src="preview.url"-->
+<!--            />-->
+<!--          </div>-->
+
+          <div class="show-preview" :style="{'width': preview.w + 'px', 'height': preview.h + 'px',  'overflow': 'hidden', 'margin': '5px'}">
+            <div :style="preview.div">
+              <img :src="preview.url" :style="preview.img"/>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div slot="footer" class="dialog-footer">
+      <el-button type="success" icon="el-icon-refresh-right" @click="rotateRight" ></el-button>
+      <el-button type="success" icon="el-icon-refresh-left" @click="rotateLeft"></el-button>
+      <el-button @click="close">{{this.$t('wu20240928.cancel')}}</el-button>
+      <el-button type="primary" @click="finish" :loading="loading">{{this.$t('wu20240928.comfirm')}}</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import {VueCropper} from "vue-cropper";  // 引入vue-cropper
+
+export default {
+  name: 'ReCropperPreview',
+  components: {
+    VueCropper
+  },
+  data () {
+    return {
+      // 裁剪组件的基础配置option
+      option: {
+        img: '', // 裁剪图片的地址
+        outputSize: 1, // 裁剪生成图片的质量
+        outputType: 'jpg', // 裁剪生成图片的格式
+        full: true, // 是否输出原图比例的截图
+        info: true, // 图片大小信息
+        canScale: true, // 图片是否允许滚轮缩放
+        autoCrop: true, // 是否默认生成截图框
+        autoCropWidth: 200, // 默认生成截图框宽度
+        autoCropHeight: 150, // 默认生成截图框高度
+        canMove: true, // 上传图片是否可以移动
+        fixedBox: false, // 固定截图框大小 不允许改变
+        fixed: false, // 是否开启截图框宽高固定比例
+        canMoveBox: true, // 截图框能否拖动
+        original: false, // 上传图片按照原始比例渲染
+        centerBox: false, // 截图框是否被限制在图片里面
+        height: true,
+        infoTrue: false, // true 为展示真实输出图片宽高 false 展示看到的截图框宽高
+        enlarge: 1, // 图片根据截图框输出比例倍数
+        mode: 'container', // 图片默认渲染方式
+        maxImgSize: 375
+      },
+      // 防止重复提交
+      loading: false,
+      preview: {},
+      previewStyle: {}
+    }
+  },
+  props: {
+    dialogVisible: {
+      type: Boolean,
+      default: false
+    },
+    cropperOption: {
+      type: Object,
+      default: () => {}
+    },
+    cropperStyle: {
+      type: Object,
+      default: () => {}
+    },
+    fileSize: {
+      type: Number,
+      default: 2
+    },
+    // 裁剪预览图片缩放比例
+    zoom: {
+      type: Number,
+      default: 1
+    }
+  },
+  watch: {
+    cropperOption: {
+      handler (value) {
+        this.option = Object.assign(this.option, value)
+      },
+      immediate: true,
+      deep: true
+    }
+  },
+  methods: {
+    upload () { // 点击上传
+      this.$refs.upload.value = null
+      this.$refs.upload.click()
+    },
+    uploadImg (e) { // 上传图片
+      let file = e.target.files[0]
+      if (!/\.(jpg|jpeg|png|JPG|PNG)$/.test(e.target.value)) {
+        this.$message.error(`${file.name}${this.$t('wu20240928.incorrectFormat')}`)
+        return false
+      }
+      if (file.size > 1024 * 1024 * this.fileSize) { // 图片不大于2M
+        this.$message.error(`${this.$t('wu20240928.fileSize',this.fileSize)}`)
+        return false
+      }
+      let reader = new FileReader()
+      // 转化为blob
+      reader.readAsArrayBuffer(file)
+      reader.onload = e => {
+        let data
+        if (typeof e.target.result === 'object') {
+          // 把Array Buffer转化为blob 如果是base64不需要
+          data = window.URL.createObjectURL(new Blob([e.target.result]))
+        } else {
+          data = e.target.result
+        }
+        this.$set(this.option, 'img', data)
+      }
+    },
+    realTime (preview) { // 实时预览
+      this.preview = preview
+      console.log('preview',preview)
+      this.previewStyle = {
+        width: preview.w+'px',
+        height: preview.h + 'px',
+        overflow: 'hidden',
+        margin: '0',
+        zoom: this.zoom
+      }
+    },
+    // 将base64转换为文件
+    dataURLtoFile (dataurl, filename) {
+      let arr = dataurl.split(',')
+      let mime = arr[0].match(/:(.*?);/)[1]
+      let bstr = atob(arr[1])
+      let len = bstr.length
+      let u8arr = new Uint8Array(len)
+      while (len--) {
+        u8arr[len] = bstr.charCodeAt(len)
+      }
+      return new File([u8arr], filename, { type: mime })
+    },
+    // 将base64转换为png文件图片
+    finish () {
+      this.$refs.cropper.getCropData(data => {
+        let file = this.dataURLtoFile(data, 'images.png')
+        this.$emit('uploadCropper', file, data)
+      })
+    },
+    close () {
+      this.$emit('close')
+    },
+    rotateLeft() {
+      this.$refs.cropper.rotateLeft()
+    },
+    rotateRight() {
+      this.$refs.cropper.rotateRight()
+    },
+  }
+}
+</script>
+<style  scoped>
+.cropper-dialog .el-dialog {
+  width: max-content;
+}
+
+.cropper-dialog .el-dialog__body {
+  padding: 20px;
+}
+
+
+
+.cropper-wrap {
+  display: flex;
+}
+
+.cropper-wrap .cropper-box {
+  margin-right: 20px;
+  width: 375px;
+  height: 176px;
+}
+.cropper-wrap .preview-box{
+ flex: 1;
+}
+.cropper-wrap .preview-box .preview-title {
+  display: flex;
+  min-width: 100px;
+  justify-content: space-between;
+  align-items: center;
+  height: 32px;
+  color: rgba(30,35,48,1);
+}
+
+.cropper-wrap .preview-box .preview-title .preveiw-upload {
+  color: #0067ED;
+  cursor: pointer;
+}
+
+.cropper-wrap .preview-box .preview-img {
+  border-radius: 2px;
+}
+
+.fun-btn {
+  margin-top: 16px;
+}
+
+.fun-btn i {
+  margin-right: 16px;
+  font-size: 18px;
+  color: #8c8c8c;
+  cursor: pointer;
+}
+
+.fun-btn i:hover {
+  color: #0067ED;
+}
+
+.avatar-right-previews {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+}
+
+.fun-btn .reUpload {
+  margin-right: 16px;
+}
+.show-preview img{
+  max-width: none;
+}
+</style>

File diff suppressed because it is too large
+ 1 - 0
src/icons/svg/audio.svg


+ 0 - 0
src/icons/svg/full-screen-cancel.svg


Some files were not shown because too many files changed in this diff