diff --git a/src/files/templates/includes/file_pswp_caption.html b/src/files/templates/includes/file_pswp_caption.html index 7026b29b..819101d8 100644 --- a/src/files/templates/includes/file_pswp_caption.html +++ b/src/files/templates/includes/file_pswp_caption.html @@ -1,10 +1,19 @@
-

{{ file.title }}

+

{{ file.title }}

+ {{ file.attribution }} + {% if file.description %} {{ file.description }} {% endif %} + + {% if file.license == "CC_ZERO_1_0" %}{% endif %} + {% if file.license == "CC_BY_4_0" %}{% endif %} + {% if file.license == "CC_BY_SA_4_0" %}{% endif %} + {{ file.license_name }} + + {% if file.filetype == "image" %} {% if file.get_exif_camera %} diff --git a/src/files/templates/includes/file_thumbnail_pswp.html b/src/files/templates/includes/file_thumbnail_pswp.html index b95b5d8a..ada3a13f 100644 --- a/src/files/templates/includes/file_thumbnail_pswp.html +++ b/src/files/templates/includes/file_thumbnail_pswp.html @@ -1,4 +1,4 @@ - + {% if record.filetype == "image" %} { - return slide.data.element.parentElement.querySelector("div.pswp-caption-content").innerHTML + return slide.data.element.parentElement.querySelector(`.pswp-caption-content[data-bma-file-uuid="${slide.data.element.dataset.bmaFileUuid}"]`).innerHTML }, }); @@ -180,3 +180,13 @@ if (location.hash && location.hash.substring(0, 10) == "#lightbox=") { let slide = document.querySelector("a.gallerya[data-bma-file-uuid='" + uuid + "']"); slide.click(); } + +//Disable license link click propagation +$(".license-link").bind("click", function(e) { + e.stopPropagation(); +}); + +//Disable attribution link click propagation +$(".attribution-link").bind("click", function(e) { + e.stopPropagation(); +}); diff --git a/src/utils/templates/thumbnail.html b/src/utils/templates/thumbnail.html index c581d193..8bb4a868 100644 --- a/src/utils/templates/thumbnail.html +++ b/src/utils/templates/thumbnail.html @@ -1,4 +1,12 @@ diff --git a/src/utils/templatetags/bma_utils.py b/src/utils/templatetags/bma_utils.py index 876abe87..38b765eb 100644 --- a/src/utils/templatetags/bma_utils.py +++ b/src/utils/templatetags/bma_utils.py @@ -92,6 +92,7 @@ def thumbnail(basefile: "BaseFile", width: int, ratio: str, mimetype: str = "ima "width": width, "height": t.height, "title": title, + "file": basefile, "alt": alt, } ) diff --git a/src/widgets/templates/gallery.html b/src/widgets/templates/gallery.html new file mode 100644 index 00000000..1ef3eeb0 --- /dev/null +++ b/src/widgets/templates/gallery.html @@ -0,0 +1,17 @@ +{% extends "includes/iframe.html" %} +{% load humanize static %} +{% block extra_head %} + + +{% endblock extra_head %} + +{% block main_content %} +
+ {% csrf_token %} + +
+{% endblock main_content %} diff --git a/src/widgets/templates/includes/base.js b/src/widgets/templates/includes/base.js new file mode 100644 index 00000000..5c6a7ff5 --- /dev/null +++ b/src/widgets/templates/includes/base.js @@ -0,0 +1,79 @@ +// config (rendered serverside) +const uuid = "{{ uuid }}"; +const host = "{{ host }}"; +const count = "{{ count }}"; + +const templateFiles = "{{ files|escapejs }}"; + +// custom error class +class BmaNotFoundError extends Error { + constructor(message) { + super(message); + this.name = "BmaNotFoundError"; + } +} +class BmaApiError extends Error { + constructor(message) { + super(message); + this.name = "BmaApiError"; + } +} +class BmaPermissionError extends Error { + constructor(message) { + super(message); + this.name = "BmaPermissionError"; + } +} + +const main_loader = document.createElement('div'); +main_loader.id = "photoswipe-" + count + "-loader" +main_loader.innerHTML = `
Loading Gallery....`; +// A reference to the currently running script +const bma_script = document.scripts[document.scripts.length - 1]; +bma_script.parentElement.insertBefore(main_loader, bma_script); + + +async function getFileMetadata(file_uuid) { + const response = fetch("//" + host + "/api/v1/json/files/" + file_uuid + "/", {mode: 'cors'}) + .then((x) => { + if (!x.ok) { + // handle non-2xx x code + if (x.status === 404) { + throw new BmaNotFoundError("File UUID " + file_uuid + " not found!"); + } else if (x.status === 403) { + throw new BmaPermissionError("No permission for file UUID " + file_uuid + "!"); + } else { + throw new BmaApiError("BMA API returned unexpected x code " + x.status); + } + } + return x.json() + }) + .then((x) => ({[file_uuid]: x['bma_response']})) + .catch((response) => { + console.log(response); + }); + return response +} + +async function getAlbumMetadata(album_uuid) { + const response = fetch("//" + host + "/api/v1/json/albums/" + album_uuid + "/", {mode: 'cors'}) + .then((response) => { + if (!response.ok) { + // handle non-2xx response code + if (response.status === 404) { + throw new BmaNotFoundError("Album UUID " + album_uuid + " not found!"); + } else if (response.status === 403) { + throw new BmaPermissionError("No permission for album UUID " + album_uuid + "!"); + } else { + throw new BmaApiError("BMA API returned unexpected response code " + response.status); + } + } + return response.json(); + }) + .then((data) => data["bma_response"]) + .catch((response) => { + console.log(response); + }); + return response; +} + diff --git a/src/widgets/templates/includes/iframe.html b/src/widgets/templates/includes/iframe.html new file mode 100644 index 00000000..cd10e059 --- /dev/null +++ b/src/widgets/templates/includes/iframe.html @@ -0,0 +1,52 @@ +{% load static %} +{% load django_bootstrap5 %} +{% load django_htmx %} +{% load bma_utils %} + + + + + + {% block title %}Untitled page{% endblock %} - BornHack Media Archive + + + {% bootstrap_css %} + + + + + + + + + + + + + + {% bootstrap_javascript %} + + + + + + {{ bma_version|json_script:"bma-version" }} + + {% block extra_head %}{% endblock %} + + + + {% block body %} +
+
+ {% bootstrap_messages %} + + {% block main_content %} +

This is the default content of the block 'body' in the iframe.html template of the BMA widget. Please replace in templates inheriting from iframe.html

+ {% endblock main_content %} +
+
+ {% endblock body %} + + + diff --git a/src/widgets/templates/photoswipe-module.js b/src/widgets/templates/photoswipe-module.js new file mode 100644 index 00000000..b723ee4c --- /dev/null +++ b/src/widgets/templates/photoswipe-module.js @@ -0,0 +1,160 @@ +{% load static %} + +const count = "{{ count }}"; +const uuid = "{{ uuid }}"; + +// https://github.com/dimsemenov/PhotoSwipe +import PhotoSwipeLightbox from "{% static 'js/vendor/photoswipe-lightbox-v5.4.4.esm.min.js' %}"; + +// https://github.com/dimsemenov/photoswipe-video-plugin +import PhotoSwipeVideoPlugin from '{% static 'js/vendor/photoswipe-video-plugin-v1.0.2.esm.min.js' %}'; + +// https://github.com/dimsemenov/photoswipe-dynamic-caption-plugin +import PhotoSwipeDynamicCaption from '{% static 'js/vendor/photoswipe-dynamic-caption-plugin-v1.2.7.esm.js' %}'; + +// https://github.com/junkfix/photoswipe-slideshow +import PhotoSwipeSlideshow from '{% static 'js/vendor/photoswipe-slideshow.21b9b68e9ffa5bbd370d57888ebf001dd08e36e2.esm.js' %}'; + +// https://github.com/arnowelzel/photoswipe-auto-hide-ui +import PhotoSwipeAutoHideUI from '{% static 'js/vendor/photoswipe-auto-hide-ui.v1.0.1.esm.js' %}'; + +// https://github.com/arnowelzel/photoswipe-fullscreen +import PhotoSwipeFullscreen from '{% static 'js/vendor/photoswipe-fullscreen.v1.0.5.esm.js' %}'; + +/////////////////////////////////////////////////////////////////////////////// +export const lightbox = new PhotoSwipeLightbox({ + gallery: `#photoswipe-${count}-${uuid}-main`, + children: `a.gallery-${count}-${uuid}`, + pswpModule: () => import('/static/js/vendor/photoswipe-v5.4.4.esm.min.js') +}); + +// enable videoplugin +const videoPlugin = new PhotoSwipeVideoPlugin(lightbox, { + // no options for now +}); + +// enable captionplugin +const captionPlugin = new PhotoSwipeDynamicCaption(lightbox, { + // Plugins options + type: 'auto', + captionContent: (slide) => { + return slide.data.element.parentElement.querySelector("div.pswp-caption-content").innerHTML + }, +}); + +// slideshow plugin +const slideshowPlugin = new PhotoSwipeSlideshow(lightbox, { + // Plugin options + defaultDelayMs: 5000, + progressBarPosition: 'bottom', +}); + +// autohideui plugin +const autoHideUI = new PhotoSwipeAutoHideUI(lightbox, { + // Plugin options + idleTime: 4000 // ms +}); + +// fullscreen plugin +const fullscreenPlugin = new PhotoSwipeFullscreen(lightbox); + +/////////////////////////////////////////////////////////////////////////////// +// bullets +lightbox.on('uiRegister', function() { + lightbox.pswp.ui.registerElement({ + name: 'bulletsIndicator', + className: 'pswp__bullets-indicator d-none d-sm-flex', + appendTo: 'wrapper', + onInit: (el, pswp) => { + // skip bullets if there is only 1 file + if (pswp.getNumItems() == 1) { + return; + }; + const bullets = []; + let bullet; + let prevIndex = -1; + + for (let i = 0; i < pswp.getNumItems(); i++) { + bullet = document.createElement('div'); + bullet.className = 'pswp__bullet'; + bullet.onclick = (e) => { + pswp.goTo(bullets.indexOf(e.target)); + }; + el.appendChild(bullet); + bullets.push(bullet); + } + + pswp.on('change', (a,) => { + if (prevIndex >= 0) { + bullets[prevIndex].classList.remove('pswp__bullet--active'); + } + bullets[pswp.currIndex].classList.add('pswp__bullet--active'); + prevIndex = pswp.currIndex; + }); + } + }); +}); + +/////////////////////////////////////////////////////////////////////////////// +// add download button +lightbox.on('uiRegister', function() { + lightbox.pswp.ui.registerElement({ + name: 'download-button', + order: 8, + isButton: true, + tagName: 'a', + html: { + isCustomSVG: true, + inner: '', + outlineID: 'pswp__icn-download' + }, + onInit: (el, pswp) => { + el.setAttribute('download', ''); + el.setAttribute('rel', 'noopener'); + pswp.on('change', () => { + el.href = pswp.currSlide.data.element.dataset.bmaFileOrigUrl; + el.title = "Download original"; + }); + } + }); +}); + + +/////////////////////////////////////////////////////////////////////////////// +// parse data-bma-file-orig-url attribute +lightbox.addFilter('itemData', (itemData, _index) => { + const bmaFileOrigUrl = itemData.element.dataset.bmaFileOrigUrl; + if (bmaFileOrigUrl) { + itemData.bmaFileOrigUrl = bmaFileOrigUrl; + } + return itemData; +}); + +// override slide content +lightbox.on('contentLoad', (e) => { + const { content } = e; + if (content.type === 'document') { + console.log("DOCUMENT"); + // prevent the deafult behavior + e.preventDefault(); + + // Create a container for iframe + // and assign it to the `content.element` property + content.element = document.createElement('div'); + content.element.className = 'pswp__document-container'; + + const iframe = document.createElement('iframe'); + //iframe.setAttribute('allowfullscreen', ''); + iframe.src = content.data.bmaFileOrigUrl; + content.element.appendChild(iframe); + } +}); + + +// support autoplay +lightbox.on('openingAnimationEnd', () => { + if (location.hash.includes("autoplay")) { + slideshowPlugin.setSlideshowState(); + autoHideUI.hideUI(); + } +}); diff --git a/src/widgets/templates/photoswipe.js b/src/widgets/templates/photoswipe.js new file mode 100644 index 00000000..d88c7f8a --- /dev/null +++ b/src/widgets/templates/photoswipe.js @@ -0,0 +1,237 @@ +{% load static %} + +(async function(){ + {% include "includes/base.js" %} + + // load photoswipe css and js, which in turn calls init() when it is done loading + await loadPhotoswipe(); + + /** + * Load PhotoSwipe + */ + async function loadPhotoswipe() { + // load photoswipe JS + const {lightbox} = await import(`${window.location.protocol}//${host}/widgets/photoswipe-module/${count}/${uuid}/`); + + // load photoswipe CSS + let photoswipe_css = document.createElement( "link" ); + photoswipe_css.href = "//" + host + "{% static 'css/vendor/photoswipe-v5.4.4.css' %}"; + photoswipe_css.type = "text/css"; + photoswipe_css.rel = "stylesheet"; + photoswipe_css.media = "screen,print"; + document.head.appendChild(photoswipe_css); + + // load photoswipe widget CSS + let photoswipe_widget_css = document.createElement( "link" ); + photoswipe_widget_css.href = "//" + host + "{% static 'css/photoswipe-widget.css' %}"; + photoswipe_widget_css.type = "text/css"; + photoswipe_widget_css.rel = "stylesheet"; + photoswipe_widget_css.media = "screen,print"; + document.head.appendChild(photoswipe_widget_css); + + // load custom css + let custom_css = document.createElement( "link" ); + custom_css.href = "//" + host + "{% static 'css/vendor/photoswipe-dynamic-caption-plugin-v1.2.7.css' %}"; + custom_css.type = "text/css"; + custom_css.rel = "stylesheet"; + custom_css.media = "screen,print"; + document.head.appendChild(custom_css); + + await init(); + lightbox.init(); + } + + /** + * Render the source set + * @param {object} metadata - file metadata record + * @param {string} source - Download or Thumbnail sources + * @param {string} aspect_ration - the required aspect ratio + * @returns {string} + */ + function PswpSourceSet(metadata, source, aspect_ratio) { + let urls = metadata["links"][source][aspect_ratio]; + if (!urls) { + console.log("Source set error", metadata, source, aspect_ratio); + return ""; + } + let srcset = ""; + for (const [size, url] of Object.entries(urls)) { + const sizes = size.split("*"); + srcset = srcset + "//" + host + url + " " + sizes[0] + "w, "; + } + return srcset; + } + + /** + * Creates a photoswipe thumbnail caption + * @param {object} record - file record + * @returns {string} + */ + function createThumbnailCaption(file) { + let caption = `
+ ${file.title}`; + + caption += ` ${file.attribution}`; + caption += `${createLicenseIcon(file.license)} ${file.license_name}`; + if (file.description) + caption += ` ${file.description}`; + if (file.filetype === "image" && file.exif) { + if (file.exif.Image) { + if (file.exif.Image.Make && file.exif.Image.Model) + caption += ` ${file.exif.Image.Make} ${file.exif.Image.Model}`; + } + if (file.exif.EXIF) { + if (file.exif.EXIF.LensModel) + caption += ` ${file.exif.EXIF.LensModel}` + if (file.exif.EXIF.FocalLength) + caption += ` ${file.exif.EXIF.FocalLength}mm`; + + if (file.exif.EXIF.ExposureTime) + caption += ` ${file.exif.EXIF.ExposureTime} s`; + + if (file.exif.EXIF.ISOSpeedRatings) + caption += ` ISO ${file.exif.EXIF.ISOSpeedRatings}`; + + if (file.exif.EXIF.FNumber) + caption += ` ${file.exif.EXIF.FNumber}`; + + if (file.exif.EXIF.Orientation) + caption += ` ${file.exif.EXIF.Orientation}`; + + if (file.exif.EXIF.DateTimeOriginal) { + caption += ` ${file.exif.EXIF.DateTimeOriginal}`; + } + + } + } + caption += "
"; + return caption; + } + + /** + * Creates license icon + * @param {string} license - License name + * @returns {string} + */ + function createLicenseIcon(license) { + switch(license) { + case "CC_ZERO_1_0": + return ''; + case "CC_BY_4_0": + return ''; + case "CC_BY_SA_4_0": + return ''; + default: + return license; + } + } + + /** + * Creates a photoswipe thumbnail + * @param {object} record - file record + * @returns {string} + */ + function createThumbnailPswp(record) { + let thumb = ``; + if (record.filetype === "image") { + thumb += ` +
+ +
+
${createThumbnailCaption(record)}`; + } + else if (record.filetype === "video") { + thumb += ` +
+ + +
+
${createThumbnailCaption(record)}`; + } + else if (record.filetype === "audio") { + thumb += ` +
+ + +
+
${createThumbnailCaption(record)}`; + } + else if (record.filetype === "document") { + thumb += ` +
+ + +
+
${createThumbnailCaption(record)}`; + } else { + console.log("Filetype not found", record) + } + thumb += ``; + thumb += "
" + return thumb; + } + + async function createPhotoswipe(files) { + const photoswipe_main_div = document.createElement('div'); + + // begin main photoswipe + photoswipe_main_div.className = "row"; + photoswipe_main_div.innerHTML = `