UNPKG

@xuda.io/xuda-widget-plugin-xuda-drive

Version:

Xuda Drive widget plugin

1,079 lines (920 loc) 57.2 kB
import * as FilePond from 'filepond'; import 'filepond/dist/filepond.min.css'; window.FilePond = FilePond; import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type'; import * as pintura from './vendors/pintura/pintura/pintura.js'; import './vendors/pintura/pintura/pintura.css'; import FilePondPluginImageEditor from './vendors/pintura/filepond-plugin-image-editor'; import moment from 'moment'; import { createApp } from 'vue/dist/vue.esm-bundler'; import './vendors/tailwind.cdn.js'; import Swiper from 'swiper/bundle'; import 'swiper/css/bundle'; import 'swiper/css/navigation'; import 'swiper/css'; import FloatingVue from 'floating-vue'; import 'floating-vue/dist/style.css'; import * as fs from './vendors/fs-lightbox.js'; import { useDropZone } from '@vueuse/core'; export async function upload(fields, e) { const container = e.container_node; if (!container) { e.report_error?.('xuda-drive: upload skipped — host provided no container_node (element not yet in DOM, e.g. a collapsed section)', true); return; } return new Promise(async (resolve, reject) => { var overrideAction = 'replace'; var existingFiles = {}; fields.method = e.method; var ret_obj = { app_files: [], addedArr: [], deletedArr: [] }; var init_files = []; var widget_cssClass = fields.widget_cssClass; var pond = undefined; var init_files_fn = (files) => { Object.entries(files || {}).forEach(function ([key, val]) { if (val?.server_fileName) val = val.server_fileName; val = func.utils.get_drive_url(e.SESSION_ID, val).value; ret_obj.app_files.push(val); init_files.push({ source: val, options: { type: 'local', }, }); }); }; // init files from predefined array if (!_.isEmpty(fields.file_upload_init_file_array)) { if (typeof fields.file_upload_init_file_array === 'string') { fields.file_upload_init_file_array = eval(fields.file_upload_init_file_array); } if (fields.file_upload_init_file_array?.length) { init_files_fn(fields.file_upload_init_file_array); } } // init files from bind var initialBindValue; if (!_.isEmpty(fields['xu-bind'])) { initialBindValue = await e.api_utils.get_field_value(fields['xu-bind']); let value = initialBindValue; if (typeof value === 'string') { value = value.split(',').filter((e) => e); } if (value?.length) { init_files_fn(value); } } //////////////////////////////////// const do_callback = function (is_remove) { setTimeout(async () => { var pond_files = pond.getFiles(); //$(`#${input_id}`).filepond("getFiles"); ret_obj.app_files = []; pond_files.forEach((file) => { var files = { data: {} }; // if (file.serverId) { if (is_remove) { files.data.server_fileName = file.serverId; } else { files.data = JSON.parse(file.serverId).data; } ret_obj.app_files.push(files.data); // } }); if (!_.isEmpty(fields['xu-bind'])) { var ret = ret_obj.app_files; if (typeof initialBindValue === 'string') ret = ret_obj.app_files.map((f) => f.server_fileName).join(','); await e.api_utils.set_field_value(fields['xu-bind'], ret); } if (is_remove && fields.event_name_for_successful_remove_callback) { await e.api_utils.invoke_event(fields.event_name_for_successful_remove_callback); } if (!is_remove && fields.event_name_for_successful_callback) { await e.api_utils.invoke_event(fields.event_name_for_successful_callback); } if (fields.clear_on_file_upload) { pond.removeFiles(); e.api_utils.set_field_value(fields['xu-bind'], []); } }, 100); }; var input_id = new Date().valueOf(); var pallet_wrapper = document.createElement('div'); pallet_wrapper.className = 'image_wrapper'; pallet_wrapper.style.setProperty('border', 'none', 'important'); var imgInp = document.createElement('input'); imgInp.className = `filepond ${widget_cssClass || ''}`.trim(); imgInp.id = String(input_id); pallet_wrapper.appendChild(imgInp); container.replaceChildren(pallet_wrapper); FilePond.registerPlugin(FilePondPluginImageEditor); FilePond.registerPlugin(FilePondPluginFileValidateType); const mask_doka = (root, setInnerHTML) => { setInnerHTML( root, ` <mask id="my-mask"> <rect x="0" y="0" width="100%" height="100%" fill="white"/> <circle cx="50%" cy="50%" r="50%" fill="black"/> </mask> <rect fill="rgba(255,255,255,.3125)" x="0" y="0" width="100%" height="100%" mask="url(#my-mask)"/> <circle cx="50%" cy="50%" r="50%" fill="transparent" stroke-width="1" stroke="#fff"/> `, ); }; const axios_ajax = function (url, data) { const body = new URLSearchParams(); Object.entries(data || {}).forEach(function ([key, val]) { body.append(key, val); }); fetch(url, { method: 'POST', body, }).then((res) => { console.log('Request complete! response:', res); }); }; var opt = { allowProcess: true, allowRemove: true, allowRevert: true, maxParallelUploads: 25, imageCropAspectRatio: 1, imageResizeTargetWidth: 200, // allowProcess: false, imageEditInstantEdit: false, credits: false, allowMultiple: false, imageEditorSupportImage: (file) => { // return /^image/.test(file); return file.type !== 'image/svg+xml' && file.type !== 'image/gif' && !file.type.includes('audio') && !file.type.includes('video'); }, imageEditor: { createEditor: pintura.openEditor, imageReader: [pintura.createDefaultImageReader], imageWriter: [pintura.createDefaultImageWriter], imageProcessor: pintura.processImage, editorOptions: { ...pintura.getEditorDefaults(), }, legacyDataToImageState: pintura.legacyDataToImageState, }, server: { load: (source, load, error, progress, abort, headers) => { // you would get the file data from your server here try { fetch(source) .then(function (response) { try { if (response.ok) return response.blob(); throw new Error('bad image'); } catch (err) { error({}); } }) .then(load) .catch((e) => { error({}); }); } catch (err) { error({}); } return { abort: () => { abort(); }, }; }, process: (fieldName, file, metadata, load, error, progress, abort, transfer, options) => { let formData = new FormData(); formData.append('file', file, file.name); const { app_id, gtp_token, app_token, api_key } = e._session; formData.append('app_id_reference', APP_OBJ?.[app_id]?.app_id_reference); formData.append('app_id', app_id); formData.append('gtp_token', gtp_token); formData.append('app_token', app_token); formData.append('drive_type', 'workspace'); if (api_key) formData.append('api_key', api_key); if (fields.file_upload_folder) formData.append('folder', fields.file_upload_folder); if (fields.public_file) formData.append('make_public', fields.public_file); let tags = fields.assign_file_tags || []; let file_tags = []; if (fields.auto_tag_generator) { let identifier = fields.auto_tag_identifier || ' '; const filename = file.name.split('.').slice(0, -1).join('.'); file_tags = filename.split(identifier); if (fields.auto_tag_translation) { for (let tag of file_tags) { if (fields.auto_tag_translation[tag]) { tag = fields.auto_tag_translation[tag]; } } } } formData.append('tags', [...tags, ...file_tags]); const request = new XMLHttpRequest(); let _domain = e._session.domain; if (e._session.is_deployment) { _domain = e._session.opt.regional_server === 'dev.xuda.ai' ? 'dev.xuda.ai' : 'xuda.ai'; } if (overrideAction === 'replace' && file.name in existingFiles) { formData.append('file_path', '/' + file.name); formData.append('file_name', file.name); request.open('POST', `https://${_domain}/cpi/update_drive_file_workspace`); } else { request.open('POST', `https://${_domain}/cpi/upload_drive_file_workspace`); } request.setRequestHeader('xu-gtp-token', gtp_token); request.setRequestHeader('xu-app-token', app_token); // request.open("POST", `https://${_domain}/rpi/runtime_upload_file`); request.upload.onprogress = (e) => { progress(e.lengthComputable, e.loaded, e.total); }; request.onload = async function () { var res = {}; try { res = JSON.parse(request.responseText); } catch (error) {} if (request.status >= 200 && request.status < 300 && res.code > 0) { // the load method accepts either a string (id) or an object load(request.responseText); var key = res.data; ret_obj.addedArr.push(key); delete res.code; ret_obj.app_files.push(key); // if (fields.instant_file_upload === "N" ? false : true) do_callback(); } else { error(res.data); // Can call the error method if something is wrong, should exit after // e.report_error(res.data); } }; request.onerror = () => { var res = JSON.parse(request.responseText); error(res.data); this.$emit('error', error); }; request.send(formData); // Should expose an abort method so the request can be cancelled return { abort: () => { // This function is entered if the user has tapped the cancel button request.abort(); // Let FilePond know the request has been cancelled abort(); }, }; }, revert: (uniqueFileId, load, error) => { var res = JSON.parse(uniqueFileId); var key = res.data; ret_obj.deletedArr.push(key); var myIndex = ret_obj.app_files.indexOf(key.server_fileName); if (myIndex !== -1) { ret_obj.app_files.splice(myIndex, 1); } let app_id = e._session.app_id; let app_id_reference = APP_OBJ[app_id].app_id_reference; axios_ajax(func.common.get_url(e.SESSION_ID, 'rpi', 'runtime_delete_file_bulk'), { app_id, server_files: ret_obj.deletedArr, app_id_reference: app_id_reference, gtp_token: e._session.gtp_token, api_key, }); load(); }, remove: (source, load, error) => { var source_clean = source.split('/').pop(); ret_obj.deletedArr.push(source_clean); var myIndex = ret_obj.app_files.indexOf((file) => file.server_fileName === source); if (myIndex !== -1) { ret_obj.app_files.splice(myIndex, 1); } ////////////// CALLBACK if (fields.instant_file_upload === 'N' ? false : true) do_callback(true); // Can call the error method if something is wrong, should exit after // error('oh my goodness'); // Should call the load method when done, no parameters required load(); }, }, }; if (fields.file_upload_drop_area_label) { opt.labelIdle = fields.file_upload_drop_area_label; } if (fields.file_upload_allow_multiple_files) { opt.allowMultiple = true; } if (init_files) { opt.files = init_files; } opt.instantUpload = fields.instant_file_upload === 'N' ? false : true; if (fields.file_upload_mime_type_preset) { switch (fields.file_upload_mime_type_preset) { case 'image': opt.acceptedFileTypes = ['image/*']; break; case 'video': opt.acceptedFileTypes = ['video/*']; break; case 'audio': opt.acceptedFileTypes = ['audio/*']; break; default: opt.acceptedFileTypes = ['image/*']; } } else { if (fields.file_upload_custom_mime_types) { opt.acceptedFileTypes = fields.file_upload_custom_mime_types.split(','); } else { opt.allowFileTypeValidation = false; } } if (fields.file_upload_mask === 'circle') { opt.imageCropAspectRatio = '1:1'; opt.imageResizeTargetWidth = 200; opt.imageResizeTargetHeight = 200; opt.stylePanelLayout = 'compact circle'; opt.styleLoadIndicatorPosition = 'center bottom'; opt.styleProgressIndicatorPosition = 'right bottom'; opt.styleButtonRemoveItemPosition = 'left bottom'; opt.styleButtonProcessItemPosition = 'right bottom'; } pond = FilePond.create(imgInp, opt); pond.on('processfiles', (e) => { do_callback(); }); function processFiles(fileId, params = {}) { overrideAction = params.overrideAction; existingFiles = params.existingFiles; if (fileId) { pond.processFiles(fileId); } else { pond.processFiles(); } } resolve({ pond, processFiles, }); }); } export async function viewer(fields, e) { const { app_id, gtp_token, app_token, api_key } = e._session; if (!e.container_node) { e.report_error?.('xuda-drive: viewer skipped — host provided no container_node (element not yet in DOM, e.g. a collapsed section)', true); return; } var sepcialLoader = { template: ` <div class="relative isolate [--spinner-dots-stroke-dasharray:0_3.315] [--spinner-stroke:1] [--spinner-thumb-animation-duration:1.5s] *:size-full"> <span class="sr-only">Loading…</span> <span class="absolute grid place-items-center inset-0 font-semibold"> <slot> </slot> </span> <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <g class="motion-safe:origin-[--spinner-origin] motion-safe:animate-spin motion-safe:will-change-transform motion-safe:[animation-direction:reverse] motion-safe:[animation-duration:12s]" fill="currentColor" opacity="0.5"> <circle cx="8" cy="2.75" r="0.5"></circle> <circle cx="8" cy="13.25" r="0.5"></circle> <circle cx="3.01001" cy="6.35999" r="0.5"></circle> <circle cx="12.98" cy="6.35999" r="0.5"></circle> <circle cx="3.01001" cy="9.62" r="0.5"></circle> <circle cx="12.99" cy="9.62" r="0.5"></circle> <circle cx="4.91998" cy="3.75" r="0.5"></circle> <circle cx="11.08" cy="3.75" r="0.5"></circle> <circle cx="4.91998" cy="12.25" r="0.5"></circle> <circle cx="11.08" cy="12.25" r="0.5"></circle> </g> <circle class="motion-safe:origin-[--spinner-origin] motion-safe:animate-spin motion-safe:will-change-transform motion-safe:[animation-duration:--spinner-thumb-animation-duration]" cx="8" cy="8" r="5.25" pathLength="360" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-dashoffset="100" stroke-dasharray="90 270" stroke-width="var(--spinner-stroke,1.5)" ></circle> </svg> </div> `, }; var segmentComponent = { props: ['options', 'modelValue'], computed: { model: { get() { var value = this.modelValue; return value; }, set(v) { this.$emit('update:modelValue', v); }, }, segmentActive() { // debugger; var ret = _.findIndex(this.options, (value) => { return value.value === this.model; }); // debugger; return ret; }, }, template: ` <div class="flex relative items-center rounded-lg bg-gray-100 [&>button]:px-4 [&>button]:py-2"> <button v-for="option in options" @click="model = option.value" type="button" class="flex items-center gap-2 text-gray-500 focus:outline-none relative z-20 text-sm"> <span v-html="option.icon"/> <div class="truncate empty:hidden">{{ option.label }}</div> <span class="sr-only">Use {{option.label}} view</span> </button> <div :style="{ left: segmentActive > 0 ? (Number(segmentActive) / options.length) * 100 + '%' : 0, width: 100 / options.length + '%' }" class="after:absolute transition-all duration-300 after:inset-[3px] after:bg-white after:rounded-lg after:shadow absolute h-full rounded"></div> </div> `, }; var imagePreview = { props: ['file', 'size'], template: ` <div class="aspect-[3/2] group relative block w-full overflow-hidden rounded-lg bg-gray-100 border"> <div v-if="size === 'sm' && (file.extension === 'mp3' || file.extension === 'mp4')"> <svg class="h-5 w-10" v-if="file.extension === 'mp3'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path fill="#dc2625" d="M12,20a1.00061,1.00061,0,0,1-.625-.21875L6.64941,16H3a.99943.99943,0,0,1-1-1V9A.99943.99943,0,0,1,3,8H6.64941L11.375,4.21875A1.00025,1.00025,0,0,1,13,5V19a.9993.9993,0,0,1-1,1Z" /> <path fill="#f1a8a7" d="M18.501,18.3623a.99995.99995,0,0,1-.64356-1.76562,6.00231,6.00231,0,0,0,.38574-8.83984.9999.9999,0,0,1,1.41407-1.41407,8.00064,8.00064,0,0,1-.51465,11.78516A.99536.99536,0,0,1,18.501,18.3623Z" /> <path fill="#ea7c7c" d="M15.92969,15.29785a1,1,0,0,1-.64356-1.7666A1.99114,1.99114,0,0,0,16,12a2.01438,2.01438,0,0,0-.58594-1.41406.99989.99989,0,0,1,1.41406-1.41406,3.99974,3.99974,0,0,1-.25683,5.89257A.99968.99968,0,0,1,15.92969,15.29785Z" /> </svg> <svg class="h-5 w-10" v-else-if="file.extension === 'mp4'" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24"> <path fill="#ea7c7c" d="M14,18H5c-1.65611-0.00181-2.99819-1.34389-3-3V9c0.00181-1.65611,1.34389-2.99819,3-3h9c1.65611,0.00181,2.99819,1.34389,3,3v6C16.99819,16.65611,15.65611,17.99819,14,18z" /> <path fill="#dc2625" d="M21.89465,7.55359c-0.24683-0.49432-0.8476-0.69495-1.34192-0.44812l-3.56421,1.7821C16.98999,8.92572,16.99994,8.96149,17,9v6c-0.00006,0.03851-0.01001,0.07428-0.01147,0.11243l3.56421,1.7821C20.69165,16.96381,20.84479,16.99994,21,17c0.55212-0.00037,0.99969-0.44788,1-1V8C21.99994,7.84503,21.96387,7.6922,21.89465,7.55359z" /> </svg> </div> <template v-else-if="size !== 'sm' && (file.extension === 'mp3' || file.extension === 'mp4')"> <div v-if="file.extension === 'mp3'" class="flex h-full flex-col items-center justify-center bg-light-50"> <audio class="w-[90%]" controls> <source :src="file.access_link" type="audio/ogg" /> <source :src="file.access_link" type="audio/mpeg" /> <source :src="file.access_link" type="audio/mp3" /> </audio> </div> <video v-else-if="file.extension === 'mp4'" controls class="h-full object-cover"> <source :src="file.access_link" type="video/mp4" /> <source :src="file.access_link" type="video/ogg" /> </video> </template> <a v-else-if="file.extension === 'svg' || file.extension === 'png' || file.extension === 'jpg' || file.extension === 'jpeg' || file.extension === 'webp'" data-fslightbox="xuda-drive-preview" :href="file.access_link"> <img :src="file.access_link" :alt="file.name" class="pointer-events-none object-cover checker-bg" /> </a> <div v-else-if="file.type === 'directory'"> <div class="flex h-full cursor-pointer flex-col items-center justify-center gap-4"> <svg class="h-24 w-24 text-light-800 dark:text-newDark-700" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"></path></svg> </div> </div> <div class="flex items-center justify-center bg-light-50 dark:bg-newDark-950" v-else> <h1 :class="[size === 'sm' ? 'text-xs' : 'text-2xl sm:text-5xl', 'bg-gradient-to-r from-xudaTint-300 to-xudaTint-600 bg-clip-text py-2 font-extrabold text-transparent']">{{ file.extension }}</h1> </div> <div v-tooltip="file.tags.join(', ')" class="rounded-full flex items-center gap-2 absolute top-3 right-3 px-3 py-1 text-sm bg-white border" v-if="file.tags?.length"> <svg class="size-4" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"> <g fill="#878787"> <path d="m240 128l-45.62 68.44a8 8 0 0 1-6.66 3.56H40a8 8 0 0 1-8-8V64a8 8 0 0 1 8-8h147.72a8 8 0 0 1 6.66 3.56Z" opacity=".2" /> <path d="M246.66 123.56L201 55.13A15.94 15.94 0 0 0 187.72 48H40a16 16 0 0 0-16 16v128a16 16 0 0 0 16 16h147.72a16 16 0 0 0 13.28-7.12l45.63-68.44a8 8 0 0 0 .03-8.88M187.72 192H40V64h147.72l42.66 64Z" /> </g> </svg> {{ file.tags.length }} tags </div> </div> `, }; const html = ` <div class="flex flex-col divide-y overflow-hidden h-full" dir="ltr"> <div v-if="options.show_toolbar !== false" class="flex items-center py-2 px-6"> <div class="flex gap-2 items-center flex-1"> <h1 class="text-2xl font-bold text-gray-900 capitalize" v-text="options.toolbar_label || view"></h1> </div> <div class="flex items-center gap-4"> <div class="relative"> <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> <svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd"></path> </svg> </div> <VDropdown popperClass="w-96" :triggers="[]" :disposeTimeout="10" :shown="showSearchPopper" :autoHide="false" placement="bottom"> <ul class="focus:ring-2 focus:ring-offset-2 focus:ring-neutral-400 relative min-h-[38px] flex items-center justify-between w-full py-2 px-3 text-left bg-white border rounded-md shadow-sm cursor-default border-neutral-200/70 focus:outline-none text-sm flex items-center gap-2 px-2"> <li v-for="(chip, i) in searchTags" :class="[removeChipCount && i == searchTags.length - 1 ? 'bg-neutral-400' : 'bg-neutral-200 ']" class="rounded-lg flex items-stretch text-sm overflow-hidden"> <div :class="[removeChipCount && i == searchTags.length - 1 ? 'bg-neutral-500/20' : 'bg-neutral-300']" class="uppercase flex items-center text-[0.65rem] px-2">{{ chip.split(':')[0] }}</div> <div class="px-2 flex items-center">{{ chip.split(':')[1] }}</div> </li> <li> <input type="text" ref="search" placeholder="Search" @focus="search.length ? (showSearchPopper = true) : ''" @blur="hideSearchPopper" class="border-0 ring-0 focus:ring-0 py-0 px-1" v-model.trim="search" @keyup.delete="removeLastSearch" /> </li> <div class="absolute right-4 flex items-center gap-2 empty:hidden"> <sepcial-loader class="size-5" v-if="loadingSearch" /> <template v-if="search.length"> <button @click="search = '' "> <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path> </svg> </button> </template> </div> </ul> <template #popper> <div class="space-y-3 divide-y xuda-divide p-4"> <div v-for="option in searchOptions" class="search-option [&:not(:first-child)]:pt-3"> <div class="search-option-label text-xs font-semibold mb-1 text-light-800">{{ option.label }}</div> <button class="search-option-action hover:bg-black px-2 -mx-2 py-1 block w-full text-left rounded-md hover:text-white text-sm" @click="search ? ((search = option.tag+':'+ search), $refs.search.focus()) : ''">{{option.label}} {{option.modifier}} "{{ search }}"</button> </div> </div> </template> </VDropdown> </div> <segment-component :options="sortDirOptions" v-model="opts.sort_dir" /> <segment-component :options="sortOptions" v-model="opts.sort_by" /> <segment-component :options="viewOptions" v-model="view" /> <div> <button @click="uploadModal = true" type="button" class="inline-flex items-center justify-center px-4 py-2 text-sm font-medium tracking-wide text-white transition-colors duration-200 rounded-md bg-neutral-950 hover:bg-neutral-900 focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900 focus:shadow-outline focus:outline-none" > Upload </button> </div> </div> </div> <!-- Tabs --> <div ref="dropZoneRef" class="flex-1 overflow-auto size-full relative"> <div v-if="isOverDropZone && view !== 'slider'" class="absolute inset-0 z-10 px-2 py-2 animate"> <div class="size-full flex flex-col justify-between items-center !border-black border-2 rounded-2xl bg-black/20"> <div></div> <div></div> </div> </div> <template v-if="view === 'gallery' || view === 'list'"> <div class="flex-1 overflow-auto" :class="{ 'grid gap-x-4 gap-y-4 grid-cols-2 sm:grid-cols-3 sm:gap-x-6 md:grid-cols-4 lg:grid-cols-4 xl:grid-cols-5 xl:gap-x-6 p-4': view === 'gallery', 'flex flex-col divide-y': view === 'list' }"> <div :class="{ 'md:col-span-4 xl:col-span-5 grid gap-x-4 gap-y-3 grid-cols-2 sm:grid-cols-3 sm:gap-x-6 md:grid-cols-4 lg:grid-cols-4 xl:grid-cols-5 sm:gap-x-6': view === 'gallery'}"> <template v-for="folder in folders"> <div v-if="view === 'gallery'" class="flex items-center justify-between px-3 group gap-2 rounded-lg bg-gray-100 border py-3"> <div class="flex items-center gap-2 truncate flex-1"> <svg class="h-7 w-7 shrink-0" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"></path></svg> <div>{{folder.name}}</div> </div> <div class="flex items-center justify-between gap-2"> <div class="hidden font-normal sm:block">{{ folder.size }}</div> </div> </div> </template> </div> <template v-for="file in (view === 'list' ? [...folders, ...files] : files)"> <div v-if="view === 'gallery'" class="relative"> <imagePreview :file="file" /> <div class="mt-2 block truncate font-medium text-sm text-gray-900" v-text="file.name"></div> <div class="flex items-center justify-between text-xs text-gray-500"> <div class="truncate" v-text="file.size"></div> <div v-tooltip="{content: (file.date_modified ? '<div> Date modified: '+ moment(file.date_modified).format('lll') + '</div>' : '') +'<div> Date created: '+ moment(file.date_created).format('lll') +'</div>', html: true}" class="truncate"> {{ file.date_modified ? moment(file.date_modified).format('lll') : moment(file.date_created).format('lll') }} </div> </div> </div> <div v-if="view === 'list'" class="flex group items-center justify-between py-2 px-4 hover:bg-neutral-100 transition-all"> <div class="flex items-center gap-4 truncate max-w-6xl"> <a v-if="file.type==='file'" data-fslightbox="xuda-drive-preview" :href="file.access_link" class="group block shrink-0 overflow-hidden"> <img :src="file.access_link" :alt="file.name" class="pointer-events-none object-cover checker-bg flex w-10 h-8 items-center" /> </a> <div class="w-10 h-8 grid place-items-center shrink-0 rounded-lg bg-gray-100 border" v-if="file.type==='directory'"> <svg class="size-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"></path></svg> </div> <div class="pointer-events-none block w-56 truncate text-left font-medium 2xl:w-full" v-text="file.name"></div> </div> <div class="flex items-center gap-3 truncate text-gray-500"> <div class="flex flex-wrap gap-1"> <div class="inline-flex bg-neutral-100 group-hover:bg-neutral-200 py-1 px-2 text-xs rounded-full" v-for="tag in file.tags">{{ tag }}</div> </div> <div class="truncate" v-text="file.size"></div> <div class="truncate" v-text="moment(file.date_created).format('lll')"></div> </div> </div> </template> </div> <div v-if="!isEndOfList && (!loadingSearch || loadingSearch && allLoading) && files.length" class="sticky text-center col-span-5 py-6 bottom-0 left-0 right-0"> <button :class="[allLoading ? 'px-1.5' : 'px-4']" class="px-4 bg-black/80 rounded-full py-1.5 transition-all text-white font-semibold" @click="refreshDirectory(true)"> <template v-if="allLoading"> <sepcial-loader class="size-6" /> </template> <template v-else> Load all </template> </button> </div> </template> <div v-if="view === 'slider'" class="relative flex items-center justify-between py-4 px-4 flex-1 h-full overflow-hidden"> <div v-if="loadingSearch" class="absolute bg-white/80 inset-0 grid place-items-center size-full"> <sepcial-loader class="size-8"/> </div> <div v-if="options.navigation" class="absolute inset-y-0 left-6 z-10 flex items-center"> <button @click="options.direction === 'rtl' ? swiper.slideNext() : swiper.slidePrev()" class="bg-white -ml-2 lg:-ml-4 flex justify-center items-center w-10 h-10 rounded-full shadow focus:outline-none"> <svg viewBox="0 0 20 20" fill="currentColor" class="chevron-left w-6 h-6"><path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"></path></svg> </button> </div> <div class="swiper-container flex-1 w-full h-full" :dir="options.direction || 'ltr'" ref="swiper-container"> <div class="swiper-wrapper"> <!-- Slides --> <template v-for="file in files" hidden> <div class="swiper-slide"> <a data-fslightbox="xuda-drive-preview" :href="file.access_link" class="flex-shrink-0"> <img class="h-full w-full object-cover" loading="lazy" :src="file.access_link" :alt="file.name" /> </a> </div> </template> </div> </div> <div v-if="options.navigation" class="absolute inset-y-0 right-6 z-10 flex items-center"> <button @click="options.direction === 'rtl' ? swiper.slidePrev() : swiper.slideNext()" class="bg-white -mr-2 lg:-mr-4 flex justify-center items-center w-10 h-10 rounded-full shadow focus:outline-none"> <svg viewBox="0 0 20 20" fill="currentColor" class="chevron-right w-6 h-6"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path></svg> </button> </div> </div> </div> <teleport to="body"> <div @keydown.escape.window="uploadModal = false" v-show="uploadModal" class="fixed top-0 left-0 z-[99] w-screen h-screen" x-cloak> <div v-show="uploadModal" @click="uploadModal=false" class="absolute inset-0 w-full h-full bg-white backdrop-blur-sm bg-opacity-70"></div> <transition key="wrapper" enter-from-class="opacity-0" enter-active-class="ease-out duration-300" enter-to-class="opacity-100" leave-from-class="opacity-100" leave-active-class="ease-in duration-300" leave-to-class="opacity-0"> <div class="flex items-center justify-center size-full" v-show="uploadModal"> <transition key="inner" enter-active-class="ease-out duration-300" enter-from-class="opacity-0 -translate-y-2 sm:scale-95" enter-to-class="opacity-100 translate-y-0 sm:scale-100" leave-active-class="ease-in duration-200" leave-from-class="opacity-100 translate-y-0 sm:scale-100" leave-to-class="opacity-0 -translate-y-2 sm:scale-95" > <div v-show="uploadModal" class="relative w-full py-6 bg-white border shadow-lg px-7 border-neutral-200 sm:max-w-lg sm:rounded-lg overflow-hidden"> <div class="flex items-center justify-between pb-3"> <h3 class="text-lg font-semibold" v-text="'Upload file' + (options.file_upload_allow_multiple_files ? 's' : '')"></h3> <button @click="uploadModal=false" class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 text-gray-600 rounded-full hover:text-gray-800 hover:bg-gray-50"> <svg class="w-5 h-5" 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> </button> </div> <div id="imageUploaderComponent" class="relative w-auto pb-8 h-48 [&>div]:h-full [&>div>div]:h-full"></div> <div class="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2"> <button @click="uploadModal=false" type="button" class="inline-flex gap-2 items-center justify-center h-10 px-4 py-2 text-sm font-medium transition-colors border rounded-md focus:outline-none focus:ring-2 focus:ring-neutral-100 focus:ring-offset-2"> <div>Dismiss</div> </button> <button @click="uploadFiles(false)" :disabled="uploaderLoading || checkingFiles" type="button" class="inline-flex items-center gap-2 justify-center h-10 px-4 py-2 text-sm font-medium text-white transition-colors border border-transparent rounded-md focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-2 bg-neutral-950 hover:bg-neutral-900 disabled:opacity-50 disabled:cursor-not-allowed" > <template v-if="checkingFiles"> <sepcial-loader class="size-5" /> Checking files... </template> <template v-else-if="uploaderLoading"> <sepcial-loader class="size-5" /> Uploading </template> <template v-else>Upload</template> </button> </div> <div v-show="showExistModal" class="absolute inset-0 bg-white py-6 px-7 flex flex-col"> <div class="flex items-center justify-between pb-3"> <h3 class="text-lg font-semibold">Upload options</h3> <button @click="showExistModal=false" class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 text-gray-600 rounded-full hover:text-gray-800 hover:bg-gray-50"> <svg class="w-5 h-5" 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> </button> </div> <div class="flex-1"> <p class="text-gray-800">One or more items already exists in this location. Do you want to replace the existing items with a new version or keep both items? Replacing the items won't change sharing settings.</p> <fieldset> <div class="mt-6 space-y-5 pl-2"> <div v-for="option in replaceOptions" :key="option.value"> <label :for="option.value" class="flex items-center cursor-pointer"> <input :id="option.value" name="notification-method" type="radio" :value="option.value" v-model="overrideAction" :checked="option.value === overrideAction" class="option-radio peer" /> <div class="ml-3 block font-medium peer-checked:text-black text-light-700 cursor-pointer">{{ option.label }}</div> </label> </div> </div> </fieldset> </div> <div class="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2"> <button @click="showExistModal=false" type="button" class="inline-flex gap-2 items-center justify-center h-10 px-4 py-2 text-sm font-medium transition-colors border rounded-md focus:outline-none focus:ring-2 focus:ring-neutral-100 focus:ring-offset-2"> <div>Dismiss</div> </button> <button @click="uploadFiles(true)" :disabled="uploaderLoading" type="button" class="inline-flex items-center gap-2 justify-center h-10 px-4 py-2 text-sm font-medium text-white transition-colors border border-transparent rounded-md focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-2 bg-neutral-950 hover:bg-neutral-900 disabled:opacity-50 disabled:cursor-not-allowed" > Upload </button> </div> </div> <div v-show="checkerFailedModal" class="absolute inset-0 bg-white py-6 px-7 flex flex-col"> <div class="flex items-center justify-between pb-3"> <h3 class="text-lg font-semibold">Checker failed</h3> <button @click="checkerFailedModal=false" class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 text-gray-600 rounded-full hover:text-gray-800 hover:bg-gray-50"> <svg class="w-5 h-5" 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> </button> </div> <div class="flex-1"> <p class="text-gray-800">We couldn't complete your file upload due to a network error. This might be due to a temporary connection issue. </p> </div> <div class="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2"> <button @click="checkerFailedModal=false" type="button" class="inline-flex gap-2 items-center justify-center h-10 px-4 py-2 text-sm font-medium transition-colors border rounded-md focus:outline-none focus:ring-2 focus:ring-neutral-100 focus:ring-offset-2"> <div>Dismiss</div> </button> <button @click="uploadFiles(false), (checkerFailedModal = false)" :disabled="uploaderLoading" type="button" class="inline-flex items-center gap-2 justify-center h-10 px-4 py-2 text-sm font-medium text-white transition-colors border border-transparent rounded-md focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-2 bg-neutral-950 hover:bg-neutral-900 disabled:opacity-50 disabled:cursor-not-allowed" > Retry </button> </div> </div> </div> </transition> </div> </transition> </div> </teleport> </div> `; const fetcher = async function (service, opt) { let _domain = e._session.domain; // if (e._session.is_deployment) { // _domain = e._session.opt.regional_server === "dev.xuda.io" ? "dev.xuda.io" : "xuda.io"; // } opt.app_id = app_id; opt.api_key = api_key; const response = await fetch(`https://${_domain}/cpi/${service}`, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'xu-gtp-token': gtp_token, 'xu-app-token': app_token, }, body: JSON.stringify(opt), }); const json = await response.json(); if (!response.ok) { throw json; } return json; }; const formatBytes = function (bytes) { if (bytes < 1024) { return bytes + ' Bytes'; // Less than 1 KB } else if (bytes < 1024 * 1024) { return (bytes / 1024).toFixed(2) + ' KB'; // Less than 1 MB } else if (bytes < 1024 * 1024 * 1024) { return (bytes / (1024 * 1024)).toFixed(2) + ' MB'; // Less than 1 GB } else { return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; // 1 GB or more } }; var containerId = crypto.randomUUID(); e.container_node.setAttribute('widget-id', containerId); var app = createApp({ template: html, components: { segmentComponent, sepcialLoader, imagePreview, }, data() { return { replaceOptions: [ { label: 'Replace existing items', value: 'replace' }, { label: 'Keep all items', value: 'keep' }, ], sortDirOptions: [ { label: 'Ascending', value: 'asc', }, { label: 'Descending', value: 'desc', }, ], sortOptions: [ { label: 'Date', value: 'date', }, { label: 'Name', value: 'name', }, ], viewOptions: [ { icon: ` <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <path fill-rule="evenodd" d="M4.25 2A2.25 2.25 0 002 4.25v2.5A2.25 2.25 0 004.25 9h2.5A2.25 2.25 0 009 6.75v-2.5A2.25 2.25 0 006.75 2h-2.5zm0 9A2.25 2.25 0 002 13.25v2.5A2.25 2.25 0 004.25 18h2.5A2.25 2.25 0 009 15.75v-2.5A2.25 2.25 0 006.75 11h-2.5zm9-9A2.25 2.25 0 0011 4.25v2.5A2.25 2.25 0 0013.25 9h2.5A2.25 2.25 0 0018 6.75v-2.5A2.25 2.25 0 0015.75 2h-2.5zm0 9A2.25 2.25 0 0011 13.25v2.5A2.25 2.25 0 0013.25 18h2.5A2.25 2.25 0 0018 15.75v-2.5A2.25 2.25 0 0015.75 11h-2.5z" clip-rule="evenodd" ></path> </svg>`, label: '', value: 'gallery', }, { icon: ` <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <path fill-rule="evenodd" d="M2 3.75A.75.75 0 012.75 3h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 3.75zm0 4.167a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75zm0 4.166a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75zm0 4.167a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75z" clip-rule="evenodd" ></path> </svg>`, label: '', value: 'list', }, { icon: ` <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"> <path d="M200,64V192a8,8,0,0,1-8,8H64a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H192A8,8,0,0,1,200,64Z" opacity="0.2"></path> <path d="M192,48H64A16,16,0,0,0,48,64V192a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64A16,16,0,0,0,192,48Zm0,144H64V64H192V192ZM240,56V200a8,8,0,0,1-16,0V56a8,8,0,0,1,16,0ZM32,56V200a8,8,0,0,1-16,0V56a8,8,0,0,1,16,0Z"></path> </svg>`, label: '', value: 'slider', }, ], options: fields, folders: [], files: [], view: fields.default_view_type || 'gallery', uploadModal: false, uploaderLoading: false, checkingFiles: false, loadingSearch: false, fetcher, formatBytes, initUploader: upload, swiper: null, existingTags: '', showSearchPopper: false, isOverDropZone: false, removeChipCount: 0, searchOptions: [ { label: 'Filename', tag: 'name', modifier: 'contains', }, { label: 'Tag', tag: 'tag', modifier: 'equals', }, { label: 'Content', tag: 'any', modifier: 'contains', }, ], opts: { app_id, path: '/', search_string: '', sort_by: fields.sort_by || 'date', sort_dir: fields.sort_dir || 'asc', ...(fields.filter_tags?.length ? { tags: fields.filter_tags } : {}), // ...filter_tags }, limit: 50, isEndOfList: false, existingFiles: {}, allLoading: false, showExistModal: false, checkerFailedModal: false, overrideAction: 'replace', pond: null, processFiles: null, }; }, methods: { async refreshDirectory(all, bypassFolders = false) { if (this.view === 'slider') bypassFolders = true; this.loadingSearch = true; if (all) { this.allLoading = true; } else { this.isEndOfList = false; } if (!bypassFolders) { const folders = await this.fetcher('get_drive_files_workspace', { ...this.opts, type: 'directory', }); this.folders = folders.data.children || []; } const files = await this.fetcher('get_drive_files_workspace', { ...this.opts, ...(all ? {} : { f