@xuda.io/xuda-widget-plugin-xuda-drive
Version:
Xuda Drive widget plugin
1,131 lines (964 loc) • 54.1 kB
JavaScript
import * as FilePond from 'filepond';
import 'filepond/dist/filepond.min.css';
window.FilePond = FilePond;
import FilePondPluginFilePoster from 'filepond-plugin-file-poster';
import 'filepond-plugin-file-poster/dist/filepond-plugin-file-poster.css';
import FilePondPluginMediaPreview from 'filepond-plugin-media-preview';
import 'filepond-plugin-media-preview/dist/filepond-plugin-media-preview.css';
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, useDebounceFn } from '@vueuse/core';
export async function upload(fields, e) {
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) => {
$.each(files, 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 $pallet_wrapper = $(`<div class="image_wrapper" style="border: none !important;">`);
var input_id = new Date().valueOf();
var $imgInp = $(`<input class="filepond ${widget_cssClass}" id="${input_id}" />`).appendTo($pallet_wrapper);
e.$containerP.html($pallet_wrapper);
FilePond.registerPlugin(FilePondPluginImageEditor);
FilePond.registerPlugin(FilePondPluginMediaPreview);
FilePond.registerPlugin(FilePondPluginFilePoster);
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 e = new URLSearchParams();
$.each(data, function (key, val) {
e.append(key, val);
});
fetch(url, {
method: 'POST',
body: e,
}).then((res) => {
console.log('Request complete! response:', res);
});
};
var opt = {
allowProcess: true,
allowRemove: true,
allowRevert: true,
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.io' ? 'dev.xuda.io' : 'xuda.io';
}
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.imagePreviewHeight = 170;
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[0], 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;
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="gallery" :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="gallery" :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="gallery" :href="file.access_link" class="flex-shrink-0">
<img class="h-full w-full object-cover" :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"
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="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>
</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.$containerP.attr('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,
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,
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 ? {} : { from: 0, to: this.limit }),
type: 'file',
});
this.files = files.data.children || [];
if (all) {
this.allLoading = false;
this.isEndOfList = true;
}
if (this.files.length < this.limit) {
this.allLoading = false;
this.isEndOfList = true;
}
this.loadingSearch = false;
this.$nextTick(() => {
refreshFsLightbox();
});
},
uploadFiles(is_after_confirm) {
this.uploaderLoading = true;
if (is_after_confirm) {
this.showExistModal = false;
// this.processCb();
this.processFiles(null, {
overrideAction: this.overrideAction,
existingFiles: this.existingFiles,
});
// this.pond.on("processfile", (e) => {
// this.uploaderLoading = false;
// this.uploadModal = false;
// this.pond.removeFiles();
// this.refreshDirectory();
// });
} else {
let promises = [];
this.existingFiles = {};
var filesMapped = this.pond.getFiles().map((e) => ({ id: e.id, filename: e.file.name }));
filesMapped.forEach(({ filename }) => {
promises.push(
new Promise((resolve, reject) => {
this.fetcher(`check_drive_file_workspace`, {
file_path: '/' + filename,
file_name: filename,
type: 'file',
})
.then((e) => {
resolve(e);
})
.catch((err) => {
this.uploaderLoading = false;
if (err.code === -764) {
this.existingFiles[filename] = err.duplicates;
resolve(err);
return;
}
reject(err);
});
}),
);
});
Promise.all(promises)
.then((e) => {
// debugger;
if (Object.keys(this.existingFiles).length) {
this.showExistModal = true;
}
filesMapped
.filter((e) => !Object.keys(this.existingFiles).includes(e.filename))
.forEach((e) => {
this.processFiles(e.id);
});
})
.catch((e) => {
debugger;
this.showExistModal = true;
// debugger;
});
}
},
removeLastSearch($event) {
if (!$event.target.value) {
if (this.removeChipCount === 1) {
this.opts.search_string = this.opts.search_string
.replaceAll(
this.op