This commit is contained in:
2026-05-17 21:09:32 +08:00
commit 1870400c47
1564 changed files with 544713 additions and 0 deletions

View File

@@ -0,0 +1,625 @@
@charset "utf-8";
/*-------------------------
File manager
-------------------------*/
#MediaModal .modal-body, .filemanager {
position: static;
}
#MediaModal .modal-body {
min-height:50vh;
}
/*-------------------------
Breadcrumps
-------------------------*/
.filemanager .breadcrumbs {
color: var(--bs-primary-text);
//font-size: 1.2rem;
//font-weight: 500;
line-height: 35px;
}
.filemanager .breadcrumbs a:link, .breadcrumbs a:visited {
color: var(--bs-primary-text);
text-decoration: none;
}
.filemanager .breadcrumbs a:hover {
color: var(--bs-link-color);
}
.filemanager .breadcrumbs .arrow {
color: var(--bs-secondary-border-subtle);
//font-size: 24px;
font-weight: 500;
line-height: 20px;
}
/*-------------------------
Search box
-------------------------*/
.filemanager .top-right {
right: 0;
font-size: 17px;
background-color: var(--bs-body-bg);
color: var(--bs-primary-text);
display: block;
position: sticky;
top: 0;
padding: 1rem;
z-index:2;
}
.filemanager .search {
float:right;
position:relative;
cursor: pointer;
}
.filemanager .upload {
margin:3rem;
padding:0rem;
padding:1rem;
border:3px dashed var(--bs-border-color);
position:relative;
min-height:200px;
}
.filemanager .upload h3 {
padding:1rem 4rem;
}
.filemanager .upload button#upload-close {
position:absolute;
right:1rem;
z-index:1000
}
.filemanager .upload input[type=file] {
position:absolute;
z-index:100;
top:0;
left:0;
width:100%;
height:100%;
padding:7rem 5rem 5rem;
display:block !important;
font-size:1rem;
}
.filemanager .upload input[type=file]:hover::before {
border-color: black;
}
.filemanager .upload input[type=file]:active {
outline: 0;
}
.filemanager .upload input[type=file]:active::before {
background: -webkit-linear-gradient(top, #0a58ca, #0d6efd);
}
.filemanager .search:before {
content: '';
position: absolute;
margin-top:7px;
width: 14px;
height: 14px;
border-radius: 50%;
border: 2px solid var(--bs-border-color);
right: 10px;
}
.filemanager .search:after {
content: '';
width: 3px;
height: 10px;
background-color: var(--bs-border-color);
border-radius: 2px;
position: absolute;
top: 18px;
right: 9px;
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
}
.filemanager .search input[type=search] {
border-radius: 2px;
color: #4D535E;
background-color: #f5f5f5;
width: 250px;
height: 35px;
padding-left: 20px;
text-decoration-color: #4d535e;
font-size: 14px;
font-weight: 400;
line-height: 20px;
display: none;
outline: none;
border: none;
padding-right: 10px;
-webkit-appearance: none;
}
::-webkit-input-placeholder { /* WebKit browsers */
color: #4d535e;
}
:-moz-placeholder { /* Mozilla Firefox 4 to 18 */
color: #4d535e;
opacity: 1;
}
::-moz-placeholder { /* Mozilla Firefox 19+ */
color: #4d535e;
opacity: 1;
}
:-ms-input-placeholder { /* Internet Explorer 10+ */
color: #4d535e;
}
/*-------------------------
Content area
-------------------------*/
.filemanager .data {
margin: 0px;
padding: 0px;
z-index: -3;
}
.filemanager .data.animated {
-webkit-animation: showSlowlyElement 700ms; /* Chrome, Safari, Opera */
animation: showSlowlyElement 700ms; /* Standard syntax */
}
.filemanager .data li {
border-radius: 3px;
/*background-color: #fafafa;*/
border: 1px solid var(--bs-border-color);
width: 300px;
height: 118px;
list-style-type: none;
margin: 10px;
display: inline-block;
/*
position: relative;
*/
overflow: hidden;
padding: 0.3em;
z-index: 1;
cursor: pointer;
box-sizing: border-box;
transition: 0.3s background-color;
padding:0;
}
.filemanager .data li:hover, .filemanager .data li.active {
padding:0px;
border-color: #007bff;
border-width: 1px;
outline: 1px solid #007bff;
box-shadow:0px 0px 1px 3px rgba(0, 0, 0, 0.07);
}
.filemanager .data li.folders {
padding-top:1.5rem;
padding-left:1.5rem;
}
.filemanager .data li a {
text-decoration:none;
}
.filemanager .data li label.form-check {
font-size:1.2rem;
height:100%;
padding:0;
}
.filemanager .data li label.form-check .form-check-input {
border-color: #ddd;
float:right;
}
.filemanager .data li .info {
//margin-top:1rem;
//margin-top: 15px;
}
.filemanager .data li .name {
font-size: 1rem;
font-weight: 500;
line-height: 1.4rem;
max-height:3rem;
overflow: hidden;
text-overflow: ellipsis;
}
.filemanager .data li .details {
color:var(--bs-secondary-text);
font-size: 13px;
font-weight: 400;
white-space: nowrap;
display:block;
}
.filemanager .nothingfound {
/*background-color: rgba(var(--bs-primary-rgb), 0.1);*/
border:1px solid var(--bs-primary-bg-subtle);
width: 23em;
height: 21em;
margin: 0 auto;
border-radius:20px;
text-align:center;
-webkit-animation: showSlowlyElement 700ms; /* Chrome, Safari, Opera */
animation: showSlowlyElement 700ms; /* Standard syntax */
}
.filemanager .nothingfound .nofiles {
margin: 30px auto;
border-radius: 50%;
position:relative;
background-color: var(--bs-primary-bg-subtle);
width: 11em;
height: 11em;
line-height: 15em;
}
.filemanager .nothingfound .nofiles i {
font-size: 7rem;
transform: rotate(270deg);
line-height: 1rem;
}
/*
.filemanager .nothingfound .nofiles:after {
content: 'x';
position: absolute;
color: var(--bs-body-bg);
font-size: 12em;
margin-right: 2rem;
line-height: 0.76;
font-weight: 600;
right: 0;
}
*/
.filemanager .nothingfound span {
margin: 0 auto auto;
//color: var(--bs-body-bg);
font-size: 16px;
font-weight: 500;
line-height: 20px;
height: 13px;
position: relative;
top: 2em;
}
@media all and (max-width:965px) {
.filemanager .data li {
width: 100%;
margin: 5px 0;
}
}
/* Chrome, Safari, Opera */
@-webkit-keyframes showSlowlyElement {
100% { transform: scale(1); opacity: 1; }
0% { transform: scale(0.9); opacity: 0; }
}
/* Standard syntax */
@keyframes showSlowlyElement {
100% { transform: scale(1); opacity: 1; }
0% { transform: scale(0.9); opacity: 0; }
}
/*-------------------------
Icons
-------------------------*/
.files .image
{
display:inline-block;
margin:0px 10px 0px 0px;
max-width:200px;
max-height:120px;
background-position: center center;
background-size: 100%;
background-repeat:no-repeat;
float:left;
}
.files .preview
{
display:none;
position:absolute;
top:0;
right:0;
z-index:10000;
max-width:50%;
max-height:100%;
background-color:var(--bs-body-bg);
border-left:1px solid var(--bs-border-color);
border-bottom:1px solid var(--bs-border-color);
}
.files .preview img {
max-width:100%;
}
.files .preview-link:hover + .preview,
.files .preview-link:focus + .preview,
.files .preview-link + .preview:hover {
display:block;
}
.files .preview > img {
display:block;
margin:auto;
}
.files .preview > div {
padding: 2rem;
}
.icon {
font-size: 23px;
float:left;
}
.icon.folder {
display: inline-block;
margin:0px 20px 0px 5px;
background-color: transparent;
overflow: hidden;
}
.icon.folder:before {
content: '';
float: left;
//background-color: rgba(var(--bs-primary-rgb), 0.5);
background:linear-gradient(var(--bs-primary-bg-subtle), rgba(var(--bs-primary-rgb), 0.4));
background:linear-gradient(var(--bs-primary-border-subtle), rgba(var(--bs-primary-rgb), 0.6));
width: 1.5em;
height: 0.45em;
margin-left: 0.07em;
margin-bottom: -0.07em;
border-top-left-radius: 0.1em;
border-top-right-radius: 0.1em;
box-shadow: 1.25em 0.25em 0 0em rgba(var(--bs-primary-rgb), 0.6);
}
.icon.folder:after {
content: '';
float: left;
clear: left;
//background-color: rgba(var(--bs-primary-rgb), 1);
//background-color: var(--bs-primary-border-subtle);
background:linear-gradient(var(--bs-primary-border-subtle), rgba(var(--bs-primary-rgb), 0.6));
width: 3em;
height: 2.25em;
border-radius: 0.2em;
}
.icon.folder.full:before {
height: 0.55em;
}
.icon.folder.full:after {
height: 2.15em;
box-shadow: 0 -0.12em 0 0 var(--bs-body-bg);
}
.icon.file {
width: 2.5em;
height: 3em;
line-height: 3em;
text-align: center;
border-radius: 0.25em;
color: var(--bs-body-bg);
display: inline-block;
margin: 15px 20px 0px 5px;
position: relative;
overflow: hidden;
box-shadow: 1.74em -2.1em 0 0 #A4A7AC inset;
}
.icon.file:first-line {
font-size: 13px;
font-weight: 500;
}
.icon.file:after {
content: '';
position: absolute;
z-index: -1;
border-width: 0;
border-bottom: 2.6em solid #DADDE1;
border-right: 2.22em solid rgba(0, 0, 0, 0);
top: -34.5px;
right: -4px;
}
.icon.file.f-avi,
.icon.file.f-flv,
.icon.file.f-mkv,
.icon.file.f-mov,
.icon.file.f-mpeg,
.icon.file.f-mpg,
.icon.file.f-mp4,
.icon.file.f-m4v,
.icon.file.f-wmv {
box-shadow: 1.74em -2.1em 0 0 #7e70ee inset;
}
.icon.file.f-avi:after,
.icon.file.f-flv:after,
.icon.file.f-mkv:after,
.icon.file.f-mov:after,
.icon.file.f-mpeg:after,
.icon.file.f-mpg:after,
.icon.file.f-mp4:after,
.icon.file.f-m4v:after,
.icon.file.f-wmv:after {
border-bottom-color: #5649c1;
}
.icon.file.f-mp2,
.icon.file.f-mp3,
.icon.file.f-m3u,
.icon.file.f-wma,
.icon.file.f-xls,
.icon.file.f-xlsx {
box-shadow: 1.74em -2.1em 0 0 #5bab6e inset;
}
.icon.file.f-mp2:after,
.icon.file.f-mp3:after,
.icon.file.f-m3u:after,
.icon.file.f-wma:after,
.icon.file.f-xls:after,
.icon.file.f-xlsx:after {
border-bottom-color: #448353;
}
.icon.file.f-doc,
.icon.file.f-docx,
.icon.file.f-psd{
box-shadow: 1.74em -2.1em 0 0 #03689b inset;
}
.icon.file.f-doc:after,
.icon.file.f-docx:after,
.icon.file.f-psd:after {
border-bottom-color: #2980b9;
}
.icon.file.f-gif,
.icon.file.f-jpg,
.icon.file.f-jpeg,
.icon.file.f-pdf,
.icon.file.f-png {
box-shadow: 1.74em -2.1em 0 0 #e15955 inset;
}
.icon.file.f-gif:after,
.icon.file.f-jpg:after,
.icon.file.f-jpeg:after,
.icon.file.f-pdf:after,
.icon.file.f-png:after {
border-bottom-color: #c6393f;
}
.icon.file.f-deb,
.icon.file.f-dmg,
.icon.file.f-gz,
.icon.file.f-rar,
.icon.file.f-zip,
.icon.file.f-7z {
box-shadow: 1.74em -2.1em 0 0 #867c75 inset;
}
.icon.file.f-deb:after,
.icon.file.f-dmg:after,
.icon.file.f-gz:after,
.icon.file.f-rar:after,
.icon.file.f-zip:after,
.icon.file.f-7z:after {
border-bottom-color: #685f58;
}
.icon.file.f-html,
.icon.file.f-rtf,
.icon.file.f-xml,
.icon.file.f-xhtml {
box-shadow: 1.74em -2.1em 0 0 #a94bb7 inset;
}
.icon.file.f-html:after,
.icon.file.f-rtf:after,
.icon.file.f-xml:after,
.icon.file.f-xhtml:after {
border-bottom-color: #d65de8;
}
.icon.file.f-js {
box-shadow: 1.74em -2.1em 0 0 #d0c54d inset;
}
.icon.file.f-js:after {
border-bottom-color: #a69f4e;
}
.icon.file.f-css,
.icon.file.f-saas,
.icon.file.f-scss {
box-shadow: 1.74em -2.1em 0 0 #44afa6 inset;
}
.icon.file.f-css:after,
.icon.file.f-saas:after,
.icon.file.f-scss:after {
border-bottom-color: #30837c;
}
.upload-collapse {
margin: 0 1rem 2rem;
padding: 0rem;
border: 1px dashed var(--bs-border-color);
border-radius: 4px;
background: var(--bs-light-bg-subtle);
position: relative;
min-height: 180px;
}
.upload-collapse h3, .upload-collapse .h3 {
padding: 2rem 4rem;
}
.upload-collapse button#upload-close {
position: absolute;
right: 1rem;
top: 1rem;
z-index: 1000;
}
.upload-collapse input[type=file] {
position: absolute;
z-index: 100;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 6rem 4rem 4rem;
display: block !important;
font-size: 1rem;
color:var(--bs-primary);
}
.upload-collapse input[type=file]::-webkit-file-upload-button {
visibility: hidden;
}
.upload-collapse input[type=file]::before {
content: 'Choose files';
color: white;
display: inline-block;
background: gradient(top, rgba(var(--bs-link-color-rgb), 0.85), var(--bs-link-color));
background: -webkit-linear-gradient(top, rgba(var(--bs-link-color-rgb), 0.85), var(--bs-link-color));
border: 1px solid var(--bs-link-color-rgb);
border-radius: 4px;
padding: 0.7rem 1.8rem;
outline: none;
white-space: nowrap;
-webkit-user-select: none;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
box-shadow: 1px 1px 2px 1px rgba(var(--bs-body-color-rgb), 0.07), -1px 1px 2px 0px rgba(var(--bs-body-bg-rgb), 0.15) inset;
}
.upload-collapse input[type=file]:hover::before {
border-color: rgba(var(--bs-link-color-rgb), 0.7);
}
.upload-collapse input[type=file]:active {
outline: 0;
}
.upload-collapse input[type=file]:active::before {
background: gradient(top, var(--bs-link-color), rgba(var(--bs-link-color-rgb), 0.85));
background: -webkit-linear-gradient(top, var(--bs-link-color), rgba(var(--bs-link-color-rgb), 0.85));
}

View File

@@ -0,0 +1,838 @@
function ucFirst(str) {
if (!str) return str;
return str[0].toUpperCase() + str.slice(1);
}
if (typeof mediaScanUrl === "undefined") {
var mediaPath = "/media/";
var mediaScanUrl = "scan.php";
var uploadUrl = "upload.php";
}
class MediaModal {
constructor (modal = true)
{
this.isInit = false;
this.isModal = modal;
this.modalHtml =
`
<div class="modal fade modal-full" id="MediaModal" tabindex="-1" role="dialog" aria-labelledby="MediaModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-normal" id="MediaModalLabel">Media</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<!-- <span aria-hidden="true"><i class="la la-times la-lg"></i></span> -->
</button>
</div>
<div class="modal-body">
<div class="filemanager">
<div class="top-right d-flex justify-content-between">
<div class="">
<div class="breadcrumbs"></div>
</div>
<div class="">
<div class="search">
<input type="search" id="media-search-input" placeholder="Find a file.." />
</div>
<button class="btn btn-outline-secondary btn-sm btn-icon me-5 float-end"
data-bs-toggle="collapse"
data-bs-target=".upload-collapse"
aria-expanded="false"
>
<i class="la la-upload la-lg"></i>
Upload file
</button>
</div>
</div>
<div class="top-panel">
<div class="upload-collapse collapse">
<button id="upload-close" type="button" class="btn btn-sm btn-light" aria-label="Close" data-bs-toggle="collapse" data-bs-target=".upload-collapse" aria-expanded="true">
<span aria-hidden="true"><i class="la la-times la-lg"></i></span>
</button>
<h3>Drop or choose files to upload</h3>
<input type="file" multiple class="">
<div class="status"></div>
</div>
</div>
<div class="display-panel">
<ul class="data" id="media-files"></ul>
<div class="nothingfound">
<div class="nofiles">
<i class="la la-folder-open"></i>
</div>
<div>No files here.</div>
<div class="mt-4">
<button class="btn btn-outline-secondary btn-sm btn-icon" data-bs-toggle="collapse" data-bs-target=".upload-collapse" aria-expanded="false">
<i class="la la-upload la-lg"></i>
Upload file
</button>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer justify-content-between">
<div class="align-left">
</div>
<div class="align-right">
<button type="button" class="btn btn-secondary btn-icon me-1" data-bs-dismiss="modal">
<i class="la la-times"></i>
<span>Cancel</span>
</button>
<button type="button" class="btn btn-primary btn-icon save-btn">
<i class="la la-check"></i>
<span>Add selected</span>
</button>
</div>
</div>
</div>
</div>
</div>`;
this.response = [],
this.currentPath = '';
this.breadcrumbsUrls = [];
this.filemanager = null;
this.breadcrumbs = null;
this.fileList = null;
this.mediaPath = mediaPath;
this.type = "single";
this.container = document.getElementById("MediaModal");
}
getResponse(response) {
return this.response;
}
setResponse(response) {
this.response = response;
this.currentPath = '',
this.breadcrumbsUrls = [];
}
addModalHtml() {
if (this.isModal) document.body.append(generateElements(this.modalHtml)[0]);
this.container = document.getElementById("MediaModal");
this.container.querySelector(".save-btn").addEventListener("click", () => this.save());
}
showUploadLoading() {
this.container.querySelector(".upload-collapse .status").innerHTML = `
<div class="spinner-border" style="width: 5rem; height: 5rem;margin: 5rem auto; display:block" role="status">
<span class="visually-hidden">Loading...</span>
</div>`;
}
hideUploadLoading() {
this.container.querySelector(".upload-collapse .status").innerHTML = '';
}
save() {
let file = this.container.querySelector(".files input:checked").value ?? false;
let src = file;
if (!file) return;
if (file.indexOf("//") == -1) {
src = this.mediaPath + file;
}
if (this.targetThumb) {
document.querySelector(this.targetThumb).setAttribute("src", src);
}
if (this.callback) {
this.callback(src);
}
if (this.targetInput) {
let input = document.querySelector(this.targetInput);
input.value = file;
const e = new Event("change",{bubbles: true});
input.dispatchEvent(e);
//$(this.targetInput).val(file).trigger("change");
}
let modal = bootstrap.Modal.getOrCreateInstance(this.container);
if (this.isModal) modal.hide();
}
init() {
if (!this.isInit) {
if (this.isModal) this.addModalHtml();
let self = this;
this.initGallery();
this.isInit = true;
this.container.querySelector(".filemanager input[type=file]").addEventListener("change", this.onUpload);
this.container.querySelector(".filemanager").addEventListener("click", function (e) {
let element = e.target.closest(".btn-delete");
if (element) {
self.deleteFile(element);
} else {
element = e.target.closest(".btn-rename");
if (element) {
self.renameFile(element);
}
}
});
const event = new CustomEvent( "mediaModal:init", {detail: { type:this.type, targetInput:this.targetInput, targetThumb:this.targetThumb, callback:this.callback} });
window.dispatchEvent(event);
}
}
open(element, callback) {
if (element instanceof Element) {
this.targetInput = element.dataset.targetInput;
this.targetThumb = element.dataset.targetThumb;
if (element.dataset.type) {
this.type = element.dataset.type;
}
} else if (element) {
this.targetInput = element.targetInput;
this.targetThumb = element.targetThumb;
if (element.type) {
this.type = element.type;
}
}
this.callback = callback;
this.init();
let modal = bootstrap.Modal.getOrCreateInstance(this.container);
if (this.isModal) modal.show();
}
initGallery() {
this.filemanager = this.container.querySelector('.filemanager'),
this.breadcrumbs = this.container.querySelector('.breadcrumbs'),
this.fileList = this.filemanager.querySelector('.data');
let _this = this;
// Start by fetching the file data from scan.php with an AJAX request
if (!this.response.length) {//if response set by a plugin ignore fetch
fetch(mediaScanUrl)
.then((response) => {
if (!response.ok) { throw new Error(response) }
return response.json();
})
.then((data) => {
_this.response = [data],
_this.currentPath = '',
_this.breadcrumbsUrls = [];
let folders = [],
files = [];
window.dispatchEvent(new HashChangeEvent("hashchange"));
})
.catch(error => {
console.log(error.statusText);
displayToast("bg-danger", "Error", "Error loading media!");
});
} else {
this.goto("");
}
// This event listener monitors changes on the URL. We use it to
// capture back/forward navigation in the browser.
window.addEventListener('hashchange', function(){
_this.goto(window.location.hash);
// We are triggering the event. This will execute
// this function on page load, so that we show the correct folder:
});
// Hiding and showing the search box
let search = this.filemanager.querySelector('input[type=search]');
this.filemanager.querySelector('.search').addEventListener("click", function(){
let _search = this;
_search.querySelectorAll('span').forEach(function (el,i) { el.style.display = "none";});
search.style.display = "block";
search.focus();
});
// Listening for keyboard input on the search field.
// We are using the "input" event which detects cut and paste
// in addition to keyboard input.
search.addEventListener('input', function(e) {
let folders = [];
let files = [];
let value = this.value.trim();
if(value.length) {
_this.filemanager.classList.add('searching');
// Update the hash on every key stroke
window.location.hash = 'search=' + value.trim();
}
else {
_this.filemanager.classList.remove('searching');
window.location.hash = encodeURIComponent(_this.currentPath);
}
});
search.addEventListener('keyup', function(e) {
// Clicking 'ESC' button triggers focusout and cancels the search
let search = this;
if(e.keyCode == 27) {
search.trigger('focusout');
}
});
search.addEventListener("focusout", function(e) {
// Cancel the search
let search = this;
if(!search.value.trim().length) {
window.location.hash = encodeURIComponent(_this.currentPath);
search.style.display = 'none';
search.parentNode.querySelectorAll('span').style.display = '';
}
});
// Clicking on folders
this.fileList.addEventListener('click', function(e) {
let el = event.target.closest('li.folders');
if (el) {
e.preventDefault();
let nextDir = el.querySelector('a').getAttribute('href');
if(_this.filemanager.classList.contains('searching')) {
// Building the this.breadcrumbs
_this.breadcrumbsUrls = _this.generateBreadcrumbs(nextDir);
_this.filemanager.classList.remove('searching');
let search = _this.filemanager.querySelector('input[type=search]');
search.val('')
search.style.display = 'none';
_this.filemanager.querySelectorAll('span').forEach(e => e.style.display = '');
}
else {
_this.breadcrumbsUrls.push(nextDir);
}
window.location.hash = encodeURIComponent(nextDir);
_this.currentPath = nextDir;
}
});
// Clicking on this.breadcrumbs
this.breadcrumbs.addEventListener('click', function(e){
let el = event.target.closest('a');
if (el) {
e.preventDefault();
let index = [...el.parentNode.children].indexOf(el),
nextDir = _this.breadcrumbsUrls[index];
nextDir = el.getAttribute("href");
_this.breadcrumbsUrls.length = Number(index);
window.location.hash = encodeURIComponent(nextDir);
}
});
}
// Navigates to the given hash (path)
goto(hash) {
hash = decodeURIComponent(hash).slice(1).split('=');
let _this = this;
if (hash.length) {
let rendered = '';
// if hash has search in it
if (hash[0] === 'search') {
this.filemanager.classList.add('searching');
rendered = _this.searchData(_this.response, hash[1].toLowerCase());
if (rendered.length) {
this.currentPath = hash[0];
this.render(rendered);
}
else {
this.render(rendered);
}
}
// if hash is some path
else if (hash[0].trim().length) {
rendered = this.searchByPath(hash[0]);
if (rendered.length) {
this.currentPath = hash[0];
this.breadcrumbsUrls = this.generateBreadcrumbs(hash[0]);
this.render(rendered);
}
else {
this.currentPath = hash[0];
this.breadcrumbsUrls = this.generateBreadcrumbs(hash[0]);
this.render(rendered);
}
}
// if there is no hash
else {
this.currentPath = this.response[0]?.path ?? "";
this.breadcrumbsUrls.push(this.currentPath);
this.render(this.searchByPath(this.currentPath));
}
}
}
// Splits a file path and turns it into clickable breadcrumbs
_
generateBreadcrumbs(nextDir){
let path = nextDir.split('/').slice(0);
for(let i=1;i<path.length;i++){
path[i] = path[i-1]+ '/' +path[i];
}
return path;
}
// Locates a file by path
searchByPath(dir) {
let path = dir.split('/'),
demo = this.response,
flag = 0;
for(let i=0;i<path.length;i++){
for(let j=0;j<demo.length;j++){
if(demo[j].name === path[i]){
flag = 1;
demo = demo[j].items;
break;
}
}
}
//demo = flag ? demo : [];
return demo;
}
// Recursively search through the file tree
searchData(data, searchTerms) {
let _this = this;
let folders = [];
let files = [];
let _searchData = function (data, searchTerms) {
data.forEach(function(d){
if(d.type === 'folder') {
_searchData(d.items,searchTerms);
if(d.name.toLowerCase().indexOf(searchTerms) >= 0) {
folders.push(d);
}
}
else if(d.type === 'file') {
if(d.name.toLowerCase().indexOf(searchTerms) >= 0) {
files.push(d);
}
}
});
};
_searchData(data, searchTerms);
return {folders: folders, files: files};
}
onUpload(event) {
let file;
if (this.files && this.files[0]) {
Vvveb.MediaModal.showUploadLoading();
let reader = new FileReader();
reader.onload = imageIsLoaded;
reader.readAsDataURL(this.files[0]);
//reader.readAsBinaryString(this.files[0]);
file = this.files[0];
}
function imageIsLoaded(e) {
let image = e.target.result;
let formData = new FormData();
formData.append("file", file);
formData.append("mediaPath", Vvveb.MediaModal.mediaPath + Vvveb.MediaModal.currentPath);
formData.append("onlyFilename", true);
fetch(uploadUrl, {method: "POST", body: formData})
.then((response) => {
if (!response.ok) {
return Promise.resolve(response.text()).then((responseInText) => {
return Promise.reject([response, responseInText]);
});
}
//if (!response.ok) { throw new Error(response) }
return response.text()
})
.then((data) => {
let fileElement = Vvveb.MediaModal.addFile({
name:data,
type:"file",
path: Vvveb.MediaModal.currentPath + "/" + data,
size:1
},true);
fileElement.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});
Vvveb.MediaModal.hideUploadLoading();
})
.catch((error) => {
let [response, responseInText] = error;
let message = responseInText ?? error.statusText ?? "Error uploading!";
Vvveb.MediaModal.hideUploadLoading();
displayToast("bg-danger", "Error", message.substr(0, 200));
if (response.text && !response.bodyUsed) {
response.text().then( errorMessage => {
let message = errorMessage.substr(0, 200);
console.log(message);
displayToast("bg-danger", "Error", message);
});
}
});
}
}
deleteFile(el) {
let parent = el.closest("li");
let file = parent.querySelector('input[type ="hidden"]').value;
if (confirm(`Are you sure you want to delete "${file}"template?`)) {
fetch(deleteUrl, {method: "POST", body: new URLSearchParams({file})})
.then((response) => {
if (!response.ok) { return Promise.reject(response); }
return response.text();
})
.then((data) => {
let bg = "bg-success";
if (data.success) {
} else {
//bg = "bg-danger";
}
displayToast(bg, "Delete", data.message ?? data);
parent.remove();
})
.catch(error => {
console.log(error);
let message = error.statusText ?? "Error deleting file!";
displayToast("bg-danger", "Error", message);
error.text().then( errorMessage => {
let message = errorMessage.substr(0, 200);
displayToast("bg-danger", "Error", message);
})
});
}
}
renameFile(el) {
let parent = el.closest("li");
let file = parent.querySelector('input[type ="hidden"]').value;
let newfile = prompt(`Enter new file name for "${file}"`, file);
if (newfile) {
fetch(renameUrl, {method: "POST", body: {file, newfile}})
.then((response) => {
console.log(response);
if (!response.ok) { throw new Error(response) }
return response.text()
})
.then((data) => {
let bg = "bg-success";
if (data.success) {
} else {
//bg = "bg-danger";
}
displayToast(bg, "Save", data.message ?? data);
})
.catch(error => {
console.log(error);
displayToast("bg-danger", "Error", "Error renaming file!");
});
}
}
addFile(f, selected) {
let _this= this;
let isImage = false;
let actions = '';
let fileSize = _this.bytesToSize(f.size),
name = _this.escapeHTML(f.name),
fileType = name.split('.'),
icon = '<span class="icon file"></span>';
fileType = fileType[fileType.length-1];
if (fileType == "jpg" || fileType == "jpeg" || fileType == "png" || fileType == "gif" || fileType == "svg" || fileType == "webp") {
//icon = '<div class="image" style="background-image: url(' + _this.mediaPath + f.path + ');"></div>';
icon = '<img class="image" loading="lazy" src="' + _this.mediaPath + f.path + '">';
isImage = true;
} else {
icon = '<span class="icon file f-'+fileType+'">.'+fileType+'</span>';
}
//icon = '<span class="icon file f-'+fileType+'">.'+fileType+'</span>';
actions += '<a href="javascript:void(0);" title="Rename" class="btn btn-outline-primary btn-sm border-0 btn-rename"><i class="la la-edit"></i></a> <a href="javascript:void(0);" title="Delete" class="btn btn-outline-danger btn-sm border-0 btn-delete"><i class="la la-trash"></i></a>';
const event = new CustomEvent("mediaModal:fileActions", {detail: { file: _this.mediaPath + f.path, name, fileType, fileSize, isImage, fileType, actions} });
window.dispatchEvent(event);
if (isImage) actions += '<a href="javascript:void(0);" class="preview-link p-2"><i class="la la-search-plus"></i></a>';
let file = generateElements('<li class="files">\
<label class="form-check">\
<input type="hidden" value="' + _this.mediaPath + f.path + '" name="filename[]">\
<input type="' + ((_this.type == "single") ? "radio" : "checkbox") + '" class="form-check-input" value="' + f.path + '" name="file[]" ' + ((selected == "single") ? "checked" : "") + '><span class="form-check-label"></span>\
<div href="#\" class="files">'+icon+'<div class="info"><div class="name">'+ name +'</div><span class="details">'+fileSize+'</span>\
' + actions + '\
<div class="preview">\
<img src="' + _this.mediaPath + f.path + '">\
<div>\
<span class="name">'+ name +'</span><span class="details">'+fileSize+'</span>\
</div>\
</div>\
</div>\
</label>\
</li>')[0];
_this.fileList.append(file);
if (selected) {
file.querySelector("input[type ='radio'], input[type='checkbox']").checked = true;
}
return file;
}
render(data) {
let scannedFolders = [],
scannedFiles = [];
if(Array.isArray(data)) {
data.forEach(function (d) {
if (d.type === 'folder') {
scannedFolders.push(d);
}
else if (d.type === 'file') {
scannedFiles.push(d);
}
});
}
else if(typeof data === 'object') {
scannedFolders = data.folders;
scannedFiles = data.files;
}
// Empty the old result and make the new one
this.fileList.replaceChildren();//.style.display = 'none';
if(!scannedFolders.length && !scannedFiles.length) {
this.filemanager.querySelector('.nothingfound').style.display = '';
}
else {
this.filemanager.querySelector('.nothingfound').style.display = 'none';
}
let _this = this;
if(scannedFolders.length) {
scannedFolders.forEach(function(f) {
let itemsLength = f.items.length,
name = _this.escapeHTML(f.name),
icon = '<span class="icon folder"></span>';
if(itemsLength) {
icon = '<span class="icon folder full"></span>';
}
if(itemsLength == 1) {
itemsLength += ' item';
}
else if(itemsLength > 1) {
itemsLength += ' items';
}
else {
itemsLength = 'Empty';
}
let folder = generateElements('<li class="folders"><a href="'+ f.path +'" title="'+ f.path +'" class="folders">'+icon+'<div class="info"><span class="name">' + name + '</span> <span class="details">' + itemsLength + '</span></div></a></li>')[0];
_this.fileList.append(folder);
});
}
if(scannedFiles.length) {
scannedFiles.forEach(function(f) {
_this.addFile(f);
});
}
// Generate the breadcrumbs
let url = '';
if(this.filemanager.classList.contains('searching')){
url = '<span>Search results: </span>';
this.fileList.classList.remove('animated');
}
else {
this.fileList.classList.add('animated');
this.breadcrumbsUrls.forEach(function (u, i) {
let name = u.split('/');
if (i !== _this.breadcrumbsUrls.length - 1) {
url += '<a href="'+u+'"><span class="folderName">' + name[name.length-1] + '</span></a> <span class="arrow">→</span> ';
}
else {
url += '<span class="folderName">' + name[name.length-1] + '</span>';
}
});
}
this.breadcrumbs.replaceChildren();
this.breadcrumbs.appendChild(generateElements('<a href="/"><i class="la la-home"></i><span class="folderName">&ensp;home</span></a>')[0]);
this.breadcrumbs.appendChild(generateElements('<span>' + url + '</span>')[0]);
// Show the generated elements
this.fileList.animate({'display':'inline-block'});
}
// This function escapes special html characters in names
escapeHTML(text) {
return text.replace(/\&/g,'&amp;').replace(/\</g,'&lt;').replace(/\>/g,'&gt;');
}
// Convert file sizes from bytes to human readable units
bytesToSize(bytes) {
let sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
if (bytes == 0) return '0 Bytes';
let i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
}
}
/*
export {
MediaModal
}
*/

View File

@@ -0,0 +1,329 @@
class OpenVerse {
constructor ()
{
//register your key at https://api.openverse.engineering/v1/ and replace client_id, client_secret and name bellow
this.key = {
"client_secret" : "YhVjvIBc7TuRJSvO2wIi344ez5SEreXLksV7GjalLiKDpxfbiM8qfUb5sNvcwFOhBUVzGNdzmmHvfyt6yU3aGrN6TAbMW8EOkRMOwhyXkN1iDetmzMMcxLVELf00BR2e",
"client_id" : "pm8GMaIXIhkjQ4iDfXLOvVUUcIKGYRnMlZYApbda",
"name" : "My amazing project",
"grant_type" : "client_credentials"
};
this.accessToken = {
"access_token" : "DLBYIcfnKfolaXKcmMC8RIDCavc2hW",
"scope" : "read write groups",
"expires_in" : 36000,
"token_type" : "Bearer"
};
this.filters = {
"license" :["BY-ND", "PDM", "BY-NC", "BY-NC-SA", "BY-NC-ND", "BY-SA", "BY", "CC0"],
"license_type" :["all", "all-cc", "commercial", "modification"],
"categories" :["illustration", "photograph", "digitized_artwork"],
"aspect_ratio" :["tall", "wide", "square"],
"size" :["small", "medium", "large"],
"source" :["woc_tech", "wikimedia", "wellcome_collection", "thorvaldsensmuseum", "thingiverse", "svgsilh", "statensmuseum", "spacex", "smithsonian_zoo_and_conservation", "smithsonian_postal_museum", "smithsonian_portrait_gallery", "smithsonian_national_museum_of_natural_history", "smithsonian_libraries", "smithsonian_institution_archives", "smithsonian_hirshhorn_museum", "smithsonian_gardens", "smithsonian_freer_gallery_of_art", "smithsonian_cooper_hewitt_museum", "smithsonian_anacostia_museum", "smithsonian_american_indian_museum", "smithsonian_american_history_museum", "smithsonian_american_art_museum", "smithsonian_air_and_space_museum", "smithsonian_african_art_museum", "smithsonian_african_american_history_museum", "sketchfab", "sciencemuseum", "rijksmuseum", "rawpixel", "phylopic", "nypl", "nasa", "museumsvictoria", "met", "mccordmuseum", "iha", "geographorguk", "floraon", "flickr", "europeana", "eol", "digitaltmuseum", "deviantart", "clevelandmuseum", "brooklynmuseum", "bio_diversity", "behance", "animaldiversity", "WoRMS", "CAPL", "500px"]
},
this.baseUrl = 'https://api.openverse.engineering/v1/images?format=json&filter_dead=true&';
this.currentUrl = this.baseUrl;
this.filtersParameters = "";
}
authenticate() {
let url = "https://api.openverse.engineering/v1/auth_tokens/token/";
let self = this;
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: this.key
})
.then((response) => {
if (!response.ok) { throw new Error(response) }
return response.text()
})
.then((data) => {
this.accessToken = data;
console.log('OpenVerse Authentication:' , data);
})
.catch(error => {
console.log(error.statusText);
displayToast("bg-danger", "Error", "Openverse authentication failed!");
});
}
setFiltersParams(filtersParameters) {
this.filtersParameters = filtersParameters;
}
getResults(callback) {
this.currentUrl = this.baseUrl + this.filtersParameters;
fetch(this.currentUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
'Authorization': 'Bearer ' + this.accessToken.access_token,
},
})
.then((response) => {
if (!response.ok) { throw new Error(response) }
return response.text()
})
.then((data) => {
callback(data);
})
.catch(error => {
console.log(error.statusText);
displayToast("bg-danger", "Error", "Openverse error!");
});
}
}
class OpenVerseDisplay extends OpenVerse {
constructor ()
{
super();
}
getFiltersHtml() {
let html = "";
for (name in this.filters) {
let values = this.filters[name];
html += "<div class='col-md-3'>";
html += "<label>" + ucFirst(name.replaceAll("_", " ")) + "</label>";
html += "<select class='form-select' name="+ name +"><option value=''>All</option>";
for (let i=0;i< values.length;i++) {
let value = values[i];
let valueName = ucFirst(value.replaceAll("_", " "));
html += "<option value="+ value + ">"+ valueName +"</option>";
}
html += "</select>";
html += "</div>";
}
return html;
}
showLoading() {
document.getElementById("openverse-results").innerHTML = generateElements(`
<div class="spinner-border" style="width: 5rem; height: 5rem;margin: 5rem auto; display:block" role="status">
<span class="visually-hidden">Loading...</span>
</div>`)[0];
}
setFilters() {
this.filtersParameters = document.querySelector("#openverse-form").serialize();
//this.setFiltersParams(filters);
}
displayResults(data) {
var items = [];
data['results'].forEach( key =>{
let value = data['results'][key];
let item =
`<li class="files">
<label class="form-check">
<input type="radio" class="form-check-input" value="${val['thumbnail']}" name="file[]">
<span class="form-check-label">
</span>
<div href="#" class="files">
<img class="image" loading="lazy" src="${val['thumbnail']}" title="${val['title']}">
<div class="info">
<div class="name">${val['title']}</div>
<a href="javascript:void(0);" class="preview-link"><i class="la la-search-plus"></i></a>
<div class="preview">
<img loading="lazy" src="${val['thumbnail']}">
<div class="details">
<a href="${val['license_url']}" target="_blank">${val['license']} ${val['license_version']}</a><br/>
<a href="${val['creator_url']}" target="_blank">${val['creator']}</a><br/>
<a href="${val['foreign_landing_url']}" target="_blank">${val['source']}</a><br/>
<span>${val['attribution']}</span>
</div>
</div>
<span class="details">
<a href="${val['foreign_landing_url']}" target="_blank">${val['source']}</a><br/>
<a href="${val['license_url']}" target="_blank">${val['license']} ${val['license_version']}</a><br/>
<a href="${val['creator_url']}" target="_blank">${val['creator']}</a><br/>
</span>
</div>
</div>
</label>
</li>`;
items.push( item );
});
document.getElementById("openverse-results").innerHTML = items.join( "" );
//pagination
const maxpages = 15;
let pages = data['page_count'];
let visiblePages = 5;
let pagenum = openverse.pageNo ? openverse.pageNo : 1;
let pageStop = 1;
let currentPage = openverse.pageNo;
if (pages > maxpages)
{
if (pagenum > visiblePages)
{
if ((pagenum + visiblePages) > pages) {
currentPage = pages - maxpages - 1;
pageStop = pages;
} else {
currentPage = pagenum - visiblePages;
pageStop = pagenum + visiblePages;
}
} else {
currentPage = 1;
pageStop = maxpages;
}
}
let pagination = '';
let active = '';
//next
let prev = Math.max(pagenum - 1, 1);
pagination += `<li class="page-item"><button type="button" name="page" value="${prev}" class="me-1 page-link" onclick="openverse.page(${prev});return false;">Prev</button></li>`;
for (let i = currentPage; i <= pageStop; i++) {
if (i == pagenum) {
active = "active";
} else {
active = "";
}
pagination += `<li class="page-item ${active}"><button type="button" name="page" value="${i}" class="page-link" onclick="openverse.page(${i});return false;">${i}</button></li>`;
}
//next
let next = Math.min(pagenum + 1, pages);
pagination += `<li class="page-item"><button type="button" name="page" value="${next}" class="ms-1 page-link" onclick="openverse.page(${next});return false;">Next</button></li>`;
pagination += `<div class="p-2"> total pages ${data['page_count']}</div>`;
document.getElementById("openverse-results").innerHTML = generateElements('<div>' + pagination + '</div>')[0];
}
page(pageNo) {
this.pageNo = pageNo;
this.filtersParameters = (new FormData("openverse-form").toString()) + "&page=" + pageNo;
this.showLoading();
this.getResults(this.displayResults);
}
search() {
this.pageNo = 1;
this.setFilters();
this.showLoading();
this.getResults(this.displayResults);
}
toggleBtn() {
return generateElements(`
<button class="btn btn-outline-secondary btn-sm btn-icon me-3 float-end border-secondary-subtle" id="openverse-toggle"
data-bs-toggle="collapse"
data-bs-target="#openverse-form"
aria-expanded="false"
>
<i class="la la-search-plus la-lg"></i>
OpenVerse Search
</button>
`)[0];
}
displayPanel() {
return generateElements(`<ul class="data" id="openverse-results"></ul>`)[0];
}
paginationContainer() {
return generateElements(`<div class="pagination" id="openverse-pagination">
</div>`)[0];
}
topPanel() {
return generateElements(`
<form id="openverse-form" class="collapse p-4">
<div class="input-group">
<input id="openverse" name="q" class="form-control w-50">
<button class="btn btn-primary px-4" id="openverse-search-btn">Search</button>
<a class="btn btn-outline-secondary btn-sm btn-icon"
data-bs-toggle="collapse"
data-bs-target="#openverse-filters"
aria-expanded="false"
>
<i class="la la-filter la-lg"></i>
Filters
</a>
</div>
<div id="openverse-filters" class="row collapse">
<div class="col-md-3">
<label class="w-100">Results per page
<input name="page_size" type="number" value="20" step="10" class="form-control">
</label>
</div>
<div class="col-md-3 d-flex flex-column-reverse">
<label class="form-check-label">
<input name="mature" type="checkbox" value="false" class="form-check-input">
Mature content
</label>
<label class="form-check-label">
<input name="qa" type="checkbox" value="false" class="form-check-input">
Quality assurance
</label>
</div>
</div>
<!-- div class="pagination">
<button type="button" name="page" value="1" class="btn btn-primary me-1">1</button>
<button type="button" name="page" value="2" class="btn btn-secondary me-1">2</button>
</div -->
</form>`)[0];;
}
init() {
let self = this ;
this.authenticate();
document.querySelector("#MediaModal .top-panel").append(self.topPanel());
document.querySelector("#MediaModal .display-panel").append(self.displayPanel());
document.querySelector("#MediaModal .top-right .align-right").append(self.toggleBtn());
document.querySelector("#MediaModal .modal-footer .align-left").append(self.paginationContainer());
document.querySelector("#openverse-filters").prepend(self.getFiltersHtml());
document.querySelector("#openverse-search-btn").click(function (e) { self.search();e.preventDefault(); } );
//if openverse enabled hide media images
document.querySelector("#openverse-form").on("show.bs.collapse", function (e){
if (e.target.id == "openverse-form") {
document.querySelector("#MediaModal #openverse-results").show();
document.querySelector("#MediaModal #openverse-pagination").show();
document.querySelector("#MediaModal #media-files").hide();
}
});
document.querySelector("#openverse-form").on("hide.bs.collapse", function (e){
if (e.target.id == "openverse-form") {
document.querySelector("#MediaModal #openverse-results").hide();
document.querySelector("#MediaModal #openverse-pagination").hide();
document.querySelector("#MediaModal #media-files").show();
}
});
}
}
let openverse = new OpenVerseDisplay();
window.addEventListener("mediaModal:init", () => openverse.init());