* A bunch of refactors in public JS files (home.js, dashboard.js, etcetera).

* Added lazyload to home page (for thumbs of uploaded images), dashboard (for thumbs view) and albums' public link.
Albums' public link will silently fallback to loading all thumbs at once if JavaScript is disabled.

* A bunch of others code improvements. Honestly I'm too lazy to track all the changes.
This commit is contained in:
Bobby Wibowo 2018-04-29 19:47:24 +07:00
parent c51365adb5
commit 61e1896945
No known key found for this signature in database
GPG Key ID: 51C3A1E1E22D26CF
15 changed files with 524 additions and 436 deletions

View File

@ -163,13 +163,12 @@ albumsController.delete = async (req, res, next) => {
// Unlink zip archive of the album if it exists
const zipPath = path.join(zipsDir, `${identifier}.zip`)
return fs.access(zipPath, error => {
if (error) { return res.json({ success: true, failedids }) }
fs.unlink(zipPath, error => {
if (!error) { return res.json({ success: true, failedids }) }
fs.unlink(zipPath, error => {
if (error && error.code !== 'ENOENT') {
console.log(error)
res.json({ success: false, description: error.toString(), failedids })
})
return res.json({ success: false, description: error.toString(), failedids })
}
res.json({ success: true, failedids })
})
}

View File

@ -246,7 +246,9 @@ uploadsController.actuallyFinishChunks = async (req, res, user, albumid) => {
return new Promise((resolve, reject) => {
const chunkPath = path.join(uuidDir, chunkName)
fs.unlink(chunkPath, error => {
if (error) { return reject(error) }
if (error && error.code !== 'ENOENT') {
return reject(error)
}
resolve()
})
})
@ -366,7 +368,7 @@ uploadsController.writeFilesToDb = (req, res, user, infoMap) => {
timestamp: Math.floor(Date.now() / 1000)
})
} else {
utils.deleteFile(info.data.filename).then(() => {}).catch(error => console.log(error))
utils.deleteFile(info.data.filename).catch(console.error)
existingFiles.push(dbFile)
}
@ -447,6 +449,7 @@ uploadsController.delete = async (req, res) => {
// TODO: Wrap utils.bulkDeleteFilesByIds() instead
const user = await utils.authorize(req, res)
if (!user) { return }
const id = req.body.id
if (id === undefined || id === '') {
return res.json({ success: false, description: 'No file specified.' })
@ -461,21 +464,17 @@ uploadsController.delete = async (req, res) => {
})
.first()
try {
await utils.deleteFile(file.name).catch(error => {
// ENOENT is missing file, for whatever reason, then just delete from db anyways
if (error.code !== 'ENOENT') { throw error }
})
await db.table('files')
.where('id', id)
.del()
if (file.albumid) {
await db.table('albums')
.where('id', file.albumid)
.update('editedAt', Math.floor(Date.now() / 1000))
}
} catch (error) {
console.log(error)
const deleteFile = await utils.deleteFile(file.name).catch(console.error)
if (!deleteFile) { return }
await db.table('files')
.where('id', id)
.del()
if (file.albumid) {
await db.table('albums')
.where('id', file.albumid)
.update('editedAt', Math.floor(Date.now() / 1000))
}
return res.json({ success: true })

View File

@ -8,9 +8,17 @@ const path = require('path')
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const utilsController = {}
const uploadsDir = path.join(__dirname, '..', config.uploads.folder)
const thumbsDir = path.join(uploadsDir, 'thumbs')
utilsController.imageExtensions = ['.webp', '.jpg', '.jpeg', '.bmp', '.gif', '.png']
utilsController.videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov', '.mkv']
utilsController.mayGenerateThumb = extname => {
return (config.uploads.generateThumbnails.image && utilsController.imageExtensions.includes(extname)) ||
(config.uploads.generateThumbnails.video && utilsController.videoExtensions.includes(extname))
}
utilsController.getPrettyDate = date => {
return date.getFullYear() + '-' +
(date.getMonth() + 1) + '-' +
@ -48,72 +56,58 @@ utilsController.authorize = async (req, res) => {
const user = await db.table('users').where('token', token).first()
if (user) { return user }
res.status(401).json({ success: false, description: 'Invalid token.' })
}
utilsController.generateThumbs = (file, basedomain) => {
const ext = path.extname(file.name).toLowerCase()
const isVideoExt = utilsController.videoExtensions.includes(ext)
const isImageExt = utilsController.imageExtensions.includes(ext)
const extname = path.extname(file.name).toLowerCase()
if (!utilsController.mayGenerateThumb(extname)) { return }
if ((!isVideoExt && !isImageExt) ||
(isVideoExt && config.uploads.generateThumbnails.video !== true) ||
(isImageExt && config.uploads.generateThumbnails.image !== true)) {
return
}
const thumbname = path.join(__dirname, '..', config.uploads.folder, 'thumbs', file.name.slice(0, -ext.length) + '.png')
const thumbname = path.join(thumbsDir, file.name.slice(0, -extname.length) + '.png')
fs.access(thumbname, error => {
if (error && error.code === 'ENOENT') {
if (isVideoExt) {
ffmpeg(path.join(__dirname, '..', config.uploads.folder, file.name))
.thumbnail({
timestamps: ['1%'],
filename: '%b.png',
folder: path.join(__dirname, '..', config.uploads.folder, 'thumbs'),
size: '200x?'
})
.on('error', error => console.log('Error - ', error.message))
} else if (isImageExt) {
const size = {
width: 200,
height: 200
}
gm(path.join(__dirname, '..', config.uploads.folder, file.name))
.resize(size.width, size.height + '>')
.gravity('Center')
.extent(size.width, size.height)
.background('transparent')
.write(thumbname, error => {
if (error) { console.log('Error - ', error) }
})
}
// Only make thumbnail if it does not exist (ENOENT)
if (!error || error.code !== 'ENOENT') { return }
// If image extension
if (utilsController.imageExtensions.includes(extname)) {
const size = { width: 200, height: 200 }
return gm(path.join(__dirname, '..', config.uploads.folder, file.name))
.resize(size.width, size.height + '>')
.gravity('Center')
.extent(size.width, size.height)
.background('transparent')
.write(thumbname, error => {
if (error) { console.log('Error - ', error) }
})
}
// Otherwise video extension
ffmpeg(path.join(__dirname, '..', config.uploads.folder, file.name))
.thumbnail({
timestamps: ['1%'],
filename: '%b.png',
folder: path.join(__dirname, '..', config.uploads.folder, 'thumbs'),
size: '200x?'
})
.on('error', error => console.log('Error - ', error.message))
})
}
utilsController.deleteFile = file => {
const ext = path.extname(file).toLowerCase()
return new Promise((resolve, reject) => {
fs.stat(path.join(__dirname, '..', config.uploads.folder, file), (error, stats) => {
if (error) { return reject(error) }
fs.unlink(path.join(__dirname, '..', config.uploads.folder, file), error => {
if (error) { return reject(error) }
if (!utilsController.imageExtensions.includes(ext) && !utilsController.videoExtensions.includes(ext)) {
return resolve()
}
file = file.substr(0, file.lastIndexOf('.')) + '.png'
fs.stat(path.join(__dirname, '..', config.uploads.folder, 'thumbs/', file), (error, stats) => {
if (error) {
if (error.code !== 'ENOENT') { console.log(error) }
return resolve()
}
fs.unlink(path.join(__dirname, '..', config.uploads.folder, 'thumbs/', file), error => {
if (error) { return reject(error) }
return resolve()
})
const extname = path.extname(file).toLowerCase()
return fs.unlink(path.join(uploadsDir, file), error => {
if (error && error.code !== 'ENOENT') { return reject(error) }
if (utilsController.imageExtensions.includes(extname) || utilsController.videoExtensions.includes(extname)) {
const thumb = file.substr(0, file.lastIndexOf('.')) + '.png'
return fs.unlink(path.join(thumbsDir, thumb), error => {
if (error && error.code !== 'ENOENT') { return reject(error) }
resolve(true)
})
})
}
resolve(true)
})
})
}
@ -136,13 +130,10 @@ utilsController.bulkDeleteFilesByIds = async (ids, user) => {
await Promise.all(files.map(file => {
return new Promise(async resolve => {
const deleteFile = await utilsController.deleteFile(file.name)
.then(() => true)
.catch(error => {
if (error.code === 'ENOENT') { return true }
console.log(error)
failedids.push(file.id)
})
if (!deleteFile) { return resolve() }
await db.table('files')

View File

@ -140,10 +140,24 @@
}
}
img[data-dz-thumbnail] {
#uploads progress {
margin-bottom: 1rem;
}
#uploads img {
max-width: 200px;
}
.name {
font-size: 1rem;
color: #bdc3c7;
word-break: break-all;
}
.link > a {
word-break: break-all;
}
.clipboard-mobile {
margin-top: 5px;
}

7
public/js/album.js Normal file
View File

@ -0,0 +1,7 @@
/* global LazyLoad */
const page = {}
window.onload = () => {
page.lazyLoad = new LazyLoad()
}

View File

@ -1,62 +1,63 @@
/* global swal, axios */
const page = {}
const page = {
// user token
token: localStorage.token
}
page.do = dest => {
page.do = async dest => {
const user = document.getElementById('user').value
const pass = document.getElementById('pass').value
if (user === undefined || user === null || user === '') {
return swal('Error', 'You need to specify a username', 'error')
}
if (pass === undefined || pass === null || pass === '') {
if (!user) {
return swal('Error', 'You need to specify a username', 'error')
}
axios.post('api/' + dest, {
if (!pass) {
return swal('Error', 'You need to specify a username', 'error')
}
const response = await axios.post(`api/${dest}`, {
username: user,
password: pass
})
.then(response => {
if (response.data.success === false) {
return swal('Error', response.data.description, 'error')
}
localStorage.token = response.data.token
window.location = 'dashboard'
})
.catch(error => {
console.log(error)
return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error')
})
if (!response) { return }
if (response.data.success === false) {
return swal('Error', response.data.description, 'error')
}
localStorage.token = response.data.token
window.location = 'dashboard'
}
page.onkeypress = function (event, element) {
page.onkeypress = (event, element) => {
event = event || window.event
if (!event) { return }
if (event.keyCode === 13 || event.which === 13) {
return this.do('login')
}
if (event.keyCode === 13 || event.which === 13) { page.do('login') }
}
page.verify = () => {
page.token = localStorage.token
if (page.token === undefined) { return }
page.verify = async () => {
if (!page.token) { return }
axios.post('api/tokens/verify', {
const response = await axios.post('api/tokens/verify', {
token: page.token
})
.then(response => {
if (response.data.success === false) {
return swal('Error', response.data.description, 'error')
}
window.location = 'dashboard'
})
.catch(error => {
console.log(error)
return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error')
swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error')
})
if (!response) { return }
if (response.data.success === false) {
return swal('Error', response.data.description, 'error')
}
window.location = 'dashboard'
}
window.onload = () => {

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +1,34 @@
/* eslint-disable no-unused-expressions */
/* global swal, axios, Dropzone, ClipboardJS */
/* global swal, axios, Dropzone, ClipboardJS, LazyLoad */
const upload = {
private: undefined,
enableUserAccounts: undefined,
const page = {
// user token
token: localStorage.token,
maxFileSize: undefined,
chunkedUploads: undefined,
// Add the album let to the upload so we can store the album id in there
album: undefined,
dropzone: undefined,
clipboardJS: undefined
// configs from api/check
private: null,
enableUserAccounts: null,
maxFileSize: null,
chunkedUploads: null, // chunked uploads config
// store album id that will be used with upload requests
album: null,
dropzone: null,
clipboardJS: null,
lazyLoad: null,
lazyLoadTimestamp: null
}
const imageExtensions = ['.webp', '.jpg', '.jpeg', '.bmp', '.gif', '.png']
upload.checkIfPublic = () => {
page.checkIfPublic = () => {
axios.get('api/check')
.then(response => {
upload.private = response.data.private
upload.enableUserAccounts = response.data.enableUserAccounts
upload.maxFileSize = response.data.maxFileSize
upload.chunkedUploads = response.data.chunkedUploads
upload.preparePage()
page.private = response.data.private
page.enableUserAccounts = response.data.enableUserAccounts
page.maxFileSize = response.data.maxFileSize
page.chunkedUploads = response.data.chunkedUploads
page.preparePage()
})
.catch(error => {
console.log(error)
@ -33,27 +39,27 @@ upload.checkIfPublic = () => {
})
}
upload.preparePage = () => {
if (upload.private) {
if (upload.token) {
return upload.verifyToken(upload.token, true)
page.preparePage = () => {
if (page.private) {
if (page.token) {
return page.verifyToken(page.token, true)
} else {
const button = document.getElementById('loginToUpload')
button.href = 'auth'
button.className = button.className.replace(' is-loading', '')
if (upload.enableUserAccounts) {
button.innerText = 'Anonymous upload is disabled. Log in to upload.'
if (page.enableUserAccounts) {
button.innerText = 'Anonymous upload is disabled. Log in to page.'
} else {
button.innerText = 'Running in private mode. Log in to upload.'
button.innerText = 'Running in private mode. Log in to page.'
}
}
} else {
return upload.prepareUpload()
return page.prepareUpload()
}
}
upload.verifyToken = (token, reloadOnError) => {
page.verifyToken = (token, reloadOnError) => {
if (reloadOnError === undefined) { reloadOnError = false }
axios.post('api/tokens/verify', { token })
@ -73,8 +79,8 @@ upload.verifyToken = (token, reloadOnError) => {
}
localStorage.token = token
upload.token = token
return upload.prepareUpload()
page.token = token
return page.prepareUpload()
})
.catch(error => {
console.log(error)
@ -82,16 +88,16 @@ upload.verifyToken = (token, reloadOnError) => {
})
}
upload.prepareUpload = () => {
page.prepareUpload = () => {
// I think this fits best here because we need to check for a valid token before we can get the albums
if (upload.token) {
if (page.token) {
const select = document.getElementById('albumSelect')
select.addEventListener('change', () => {
upload.album = parseInt(select.value)
page.album = parseInt(select.value)
})
axios.get('api/albums', { headers: { token: upload.token } })
axios.get('api/albums', { headers: { token: page.token } })
.then(res => {
const albums = res.data.albums
@ -126,38 +132,38 @@ upload.prepareUpload = () => {
`
div.style.display = 'flex'
document.getElementById('maxFileSize').innerHTML = `Maximum upload size per file is ${upload.maxFileSize}`
document.getElementById('maxFileSize').innerHTML = `Maximum upload size per file is ${page.maxFileSize}`
document.getElementById('loginToUpload').style.display = 'none'
if (upload.token === undefined && upload.enableUserAccounts) {
if (!page.token && page.enableUserAccounts) {
document.getElementById('loginLinkText').innerHTML = 'Create an account and keep track of your uploads'
}
document.getElementById('uploadContainer').appendChild(div)
upload.prepareDropzone()
page.prepareDropzone()
}
upload.prepareDropzone = () => {
page.prepareDropzone = () => {
const previewNode = document.querySelector('#template')
previewNode.id = ''
const previewTemplate = previewNode.parentNode.innerHTML
previewNode.parentNode.removeChild(previewNode)
upload.dropzone = new Dropzone('div#dropzone', {
page.dropzone = new Dropzone('#dropzone', {
url: 'api/upload',
paramName: 'files[]',
maxFilesize: parseInt(upload.maxFileSize),
maxFilesize: parseInt(page.maxFileSize),
parallelUploads: 2,
uploadMultiple: false,
previewsContainer: 'div#uploads',
previewsContainer: '#uploads',
previewTemplate,
createImageThumbnails: false,
maxFiles: 1000,
autoProcessQueue: true,
headers: { token: upload.token },
chunking: upload.chunkedUploads.enabled,
chunkSize: parseInt(upload.chunkedUploads.chunkSize) * 1000000, // 1000000 B = 1 MB,
headers: { token: page.token },
chunking: page.chunkedUploads.enabled,
chunkSize: parseInt(page.chunkedUploads.chunkSize) * 1000000, // 1000000 B = 1 MB,
parallelChunkUploads: false, // when set to true, sometimes it often hangs with hundreds of parallel uploads
chunksUploaded: async (file, done) => {
file.previewElement.querySelector('.progress').setAttribute('value', 100)
@ -167,19 +173,17 @@ upload.prepareDropzone = () => {
const response = await axios.post(
'api/upload/finishchunks',
{
files: [
{
uuid: file.upload.uuid,
original: file.name,
size: file.size,
type: file.type,
count: file.upload.totalChunkCount,
albumid: upload.album
}
]
files: [{
uuid: file.upload.uuid,
original: file.name,
size: file.size,
type: file.type,
count: file.upload.totalChunkCount,
albumid: page.album
}]
},
{
headers: { token: upload.token }
headers: { token: page.token }
})
.then(response => response.data)
.catch(error => {
@ -195,32 +199,31 @@ upload.prepareDropzone = () => {
file.previewTemplate.querySelector('.error').innerHTML = response.description
}
if (response.files && response.files[0] && response.files[0].url) {
upload.appendLink(file, response.files[0].url)
upload.showThumbnail(file, response.files[0].url)
if (response.files && response.files[0]) {
page.updateTemplate(file, response.files[0])
}
return done()
}
})
upload.dropzone.on('addedfile', file => {
page.dropzone.on('addedfile', file => {
document.getElementById('uploads').style.display = 'block'
})
// Add the selected albumid, if an album is selected, as a header
upload.dropzone.on('sending', (file, xhr, formData) => {
page.dropzone.on('sending', (file, xhr, formData) => {
if (file.upload.chunked) { return }
if (upload.album) { xhr.setRequestHeader('albumid', upload.album) }
if (page.album) { xhr.setRequestHeader('albumid', page.album) }
})
// Update the total progress bar
upload.dropzone.on('uploadprogress', (file, progress, bytesSent) => {
page.dropzone.on('uploadprogress', (file, progress, bytesSent) => {
if (file.upload.chunked && progress === 100) { return }
file.previewElement.querySelector('.progress').setAttribute('value', progress)
file.previewElement.querySelector('.progress').innerHTML = `${progress}%`
})
upload.dropzone.on('success', (file, response) => {
page.dropzone.on('success', (file, response) => {
if (!response) { return }
file.previewTemplate.querySelector('.progress').style.display = 'none'
@ -228,37 +231,42 @@ upload.prepareDropzone = () => {
file.previewTemplate.querySelector('.error').innerHTML = response.description
}
if (response.files && response.files[0] && response.files[0].url) {
upload.appendLink(file, response.files[0].url)
upload.showThumbnail(file, response.files[0].url)
if (response.files && response.files[0]) {
page.updateTemplate(file, response.files[0])
}
})
upload.dropzone.on('error', (file, error) => {
page.dropzone.on('error', (file, error) => {
file.previewTemplate.querySelector('.progress').style.display = 'none'
file.previewTemplate.querySelector('.error').innerHTML = error
})
upload.prepareShareX()
page.prepareShareX()
}
upload.appendLink = (file, url) => {
page.updateTemplate = (file, response) => {
if (!response.url) { return }
const a = file.previewTemplate.querySelector('.link > a')
const clipboard = file.previewTemplate.querySelector('.clipboard-mobile > .clipboard-js')
a.href = a.innerHTML = clipboard.dataset['clipboardText'] = response.url
clipboard.parentElement.style.display = 'block'
a.href = a.innerHTML = clipboard.dataset['clipboardText'] = url
a.parentElement.style = clipboard.parentElement.style = ''
}
const name = file.previewTemplate.querySelector('.name')
name.innerHTML = file.name
upload.showThumbnail = (file, url) => {
const exec = /.[\w]+(\?|$)/.exec(url)
const exec = /.[\w]+(\?|$)/.exec(response.url)
if (exec && exec[0] && imageExtensions.includes(exec[0].toLowerCase())) {
upload.dropzone.emit('thumbnail', file, url)
const img = file.previewTemplate.querySelector('img')
img.setAttribute('alt', response.name || '')
img.dataset['src'] = response.url
img.onerror = function () { this.style.display = 'none' } // hide webp in firefox and ie
page.lazyLoad.update(file.previewTemplate.querySelectorAll('img'))
}
}
upload.prepareShareX = () => {
if (upload.token) {
page.prepareShareX = () => {
if (page.token) {
const sharexElement = document.getElementById('ShareX')
const sharexFile =
'{\r\n' +
@ -268,7 +276,7 @@ upload.prepareShareX = () => {
` "RequestURL": "${location.origin}/api/upload",\r\n` +
' "FileFormName": "files[]",\r\n' +
' "Headers": {\r\n' +
` "token": "${upload.token}"\r\n` +
` "token": "${page.token}"\r\n` +
' },\r\n' +
' "ResponseType": "Text",\r\n' +
' "URL": "$json:files[0].url$",\r\n' +
@ -291,22 +299,34 @@ window.addEventListener('paste', event => {
const file = new File([blob], `pasted-image.${blob.type.match(/(?:[^/]*\/)([^;]*)/)[1]}`)
file.type = blob.type
console.log(file)
upload.dropzone.addFile(file)
page.dropzone.addFile(file)
}
}
})
page.lazyLoadUpdate = () => {
// console.log(page.lazyLoadTimestamp)
if (!page.lazyLoadTimestamp) { page.lazyLoadTimestamp = 0 }
if (Date.now() - page.lazyLoadTimestamp >= 500) {
console.log('update() called')
page.lazyLoad.update()
page.lazyLoadTimestamp = Date.now()
}
}
window.onload = () => {
upload.checkIfPublic()
page.checkIfPublic()
upload.clipboardJS = new ClipboardJS('.clipboard-js')
page.clipboardJS = new ClipboardJS('.clipboard-js')
upload.clipboardJS.on('success', () => {
page.clipboardJS.on('success', () => {
return swal('Copied!', 'The link has been copied to clipboard.', 'success')
})
upload.clipboardJS.on('error', event => {
page.clipboardJS.on('error', event => {
console.error(event)
return swal('An error occurred!', 'There was an error when trying to copy the link to clipboard, please check the console for more information.', 'error')
})
page.lazyLoad = new LazyLoad()
}

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2015 Andrea Verlicchi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1
public/libs/lazyload/lazyload.min.js vendored Executable file
View File

@ -0,0 +1 @@
/*! lazyload v10.5.2 | Copyright (c) 2015 Andrea Verlicchi */var _extends=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},_typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};!function(e,t){"object"===("undefined"==typeof exports?"undefined":_typeof(exports))&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.LazyLoad=t()}(this,function(){"use strict";var e=function(e){var t={elements_selector:"img",container:document,threshold:300,data_src:"src",data_srcset:"srcset",class_loading:"loading",class_loaded:"loaded",class_error:"error",callback_load:null,callback_error:null,callback_set:null,callback_enter:null};return _extends({},t,e)},t=function(e,t){return e.getAttribute("data-"+t)},n=function(e,t,n){return e.setAttribute("data-"+t,n)},r=function(e){return e.filter(function(e){return!t(e,"was-processed")})},s=function(e,t){var n,r=new e(t);try{n=new CustomEvent("LazyLoad::Initialized",{detail:{instance:r}})}catch(e){(n=document.createEvent("CustomEvent")).initCustomEvent("LazyLoad::Initialized",!1,!1,{instance:r})}window.dispatchEvent(n)},o=function(e,n){var r=n.data_srcset,s=e.parentNode;if(s&&"PICTURE"===s.tagName)for(var o,a=0;o=s.children[a];a+=1)if("SOURCE"===o.tagName){var i=t(o,r);i&&o.setAttribute("srcset",i)}},a=function(e,n){var r=n.data_src,s=n.data_srcset,a=e.tagName,i=t(e,r);if("IMG"===a){o(e,n);var c=t(e,s);return c&&e.setAttribute("srcset",c),void(i&&e.setAttribute("src",i))}"IFRAME"!==a?i&&(e.style.backgroundImage='url("'+i+'")'):i&&e.setAttribute("src",i)},i="undefined"!=typeof window,c=i&&"IntersectionObserver"in window,l=i&&"classList"in document.createElement("p"),u=function(e,t){l?e.classList.add(t):e.className+=(e.className?" ":"")+t},d=function(e,t){l?e.classList.remove(t):e.className=e.className.replace(new RegExp("(^|\\s+)"+t+"(\\s+|$)")," ").replace(/^\s+/,"").replace(/\s+$/,"")},f=function(e,t){e&&e(t)},_=function(e,t,n){e.removeEventListener("load",t),e.removeEventListener("error",n)},v=function(e,t){var n=function n(s){m(s,!0,t),_(e,n,r)},r=function r(s){m(s,!1,t),_(e,n,r)};e.addEventListener("load",n),e.addEventListener("error",r)},m=function(e,t,n){var r=e.target;d(r,n.class_loading),u(r,t?n.class_loaded:n.class_error),f(t?n.callback_load:n.callback_error,r)},b=function(e,t){f(t.callback_enter,e),["IMG","IFRAME"].indexOf(e.tagName)>-1&&(v(e,t),u(e,t.class_loading)),a(e,t),n(e,"was-processed",!0),f(t.callback_set,e)},p=function(e){return e.isIntersecting||e.intersectionRatio>0},h=function(t,n){this._settings=e(t),this._setObserver(),this.update(n)};h.prototype={_setObserver:function(){var e=this;if(c){var t=this._settings,n={root:t.container===document?null:t.container,rootMargin:t.threshold+"px"};this._observer=new IntersectionObserver(function(t){t.forEach(function(t){if(p(t)){var n=t.target;b(n,e._settings),e._observer.unobserve(n)}}),e._elements=r(e._elements)},n)}},update:function(e){var t=this,n=this._settings,s=e||n.container.querySelectorAll(n.elements_selector);this._elements=r(Array.prototype.slice.call(s)),this._observer?this._elements.forEach(function(e){t._observer.observe(e)}):(this._elements.forEach(function(e){b(e,n)}),this._elements=r(this._elements))},destroy:function(){var e=this;this._observer&&(r(this._elements).forEach(function(t){e._observer.unobserve(t)}),this._observer=null),this._elements=null,this._settings=null}};var y=window.lazyLoadOptions;return i&&y&&function(e,t){if(t.length)for(var n,r=0;n=t[r];r+=1)s(e,n);else s(e,t)}(h,y),h});

View File

@ -42,25 +42,17 @@ routes.get('/a/:identifier', async (req, res, next) => {
for (const file of files) {
file.file = `${basedomain}/${file.name}`
file.size = utils.getPrettyBytes(parseInt(file.size))
file.extname = path.extname(file.name).toLowerCase()
const extname = path.extname(file.name).toLowerCase()
if ((config.uploads.generateThumbnails.image && utils.imageExtensions.includes(extname)) || (config.uploads.generateThumbnails.video && utils.videoExtensions.includes(extname))) {
file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -extname.length)}.png`
if (!utils.mayGenerateThumb(file.extname)) { continue }
file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -file.extname.length)}.png`
/*
If thumbnail for album is still not set, do it.
A potential improvement would be to let the user upload a specific image as an album cover
since embedding the first image could potentially result in nsfw content when pasting links.
*/
if (thumb === '') {
thumb = file.thumb
}
file.thumb = `<img alt="${file.name}" src="${file.thumb}"/>`
} else {
file.thumb = `<h1 class="title">${extname || 'N/A'}</h1>`
}
/*
If thumbnail for album is still not set, do it.
A potential improvement would be to let the user upload a specific image as an album cover
since embedding the first image could potentially result in nsfw content when pasting links.
*/
if (thumb === '') { thumb = file.thumb }
}
return res.render('album', {

View File

@ -12,7 +12,7 @@
v1: CSS and JS files.
v2: Images and config files (manifest.json, browserconfig.xml, etcetera).
#}
{% set v1 = "vixJhF5oKZ" %}
{% set v1 = "aP7qgfND6s" %}
{% set v2 = "MSEpgpfFIQ" %}
{#

View File

@ -7,6 +7,11 @@
<link rel="stylesheet" type="text/css" href="../css/album.css?v={{ globals.v1 }}">
{% endblock %}
{% block scripts %}
<script type="text/javascript" src="../libs/lazyload/lazyload.min.js?v={{ globals.v1 }}"></script>
<script type="text/javascript" src="../js/album.js?v={{ globals.v1 }}"></script>
{% endblock %}
{% block opengraph %}
<!-- Open Graph tags -->
<meta property="og:type" content="website" />
@ -65,7 +70,19 @@
<div id="table" class="columns is-multiline is-mobile is-centered has-text-centered">
{% for file in files %}
<div class="image-container column is-narrow">
<a class="image" href="{{ file.file }}" target="_blank" rel="noopener">{{ file.thumb | safe }}</a>
<a class="image" href="{{ file.file }}" target="_blank" rel="noopener">
{% if file.thumb -%}
<img alt="{{ file.name }}" data-src="{{ file.thumb }}">
{#-
This will kinda increase the overall page size,
but this will still benefit users with JavaScript enabled by lazyloading images,
and not causing those who have JavaScript disabled be unable to view the images.
#}
<noscript><img alt="{{ file.name }}" src="{{ file.thumb }}" style="display: none"></noscript>
{%- else -%}
<h1 class="title">{{ file.extname | default('N/A') }}</h1>
{%- endif %}
</a>
<div class="details">
<p><span class="name" title="{{ file.file }}">{{ file.name }}</span></p>
<p>{{ file.size }}</p>
@ -82,4 +99,12 @@
{%- endif %}
</div>
</section>
{# Hide lazyload img tags and show noscript img tags #}
<noscript>
<style>
img[data-src] { display: none; }
img[src] { display: block !important; }
</style>
</noscript>
{% endblock %}

View File

@ -12,6 +12,7 @@
<script type="text/javascript" src="libs/sweetalert/sweetalert.min.js?v={{ globals.v1 }}"></script>
<script type="text/javascript" src="libs/axios/axios.min.js?v={{ globals.v1 }}"></script>
<script type="text/javascript" src="libs/clipboard.js/clipboard.min.js?v={{ globals.v1 }}"></script>
<script type="text/javascript" src="libs/lazyload/lazyload.min.js?v={{ globals.v1 }}"></script>
<script type="text/javascript" src="js/dashboard.js?v={{ globals.v1 }}"></script>
{% endblock %}
@ -52,13 +53,13 @@
<a href=".">Frontpage</a>
</li>
<li>
<a id="itemUploads" onclick="panel.getUploads()">Uploads</a>
<a id="itemUploads" onclick="page.getUploads()">Uploads</a>
</li>
</ul>
<p class="menu-label">Albums</p>
<ul class="menu-list">
<li>
<a id="itemManageGallery" onclick="panel.getAlbums()">Manage your albums</a>
<a id="itemManageGallery" onclick="page.getAlbums()">Manage your albums</a>
</li>
<li>
<ul id="albumsContainer"></ul>
@ -67,16 +68,16 @@
<p class="menu-label">Administration</p>
<ul class="menu-list">
<li>
<a id="itemFileLength" onclick="panel.changeFileLength()">File name length</a>
<a id="itemFileLength" onclick="page.changeFileLength()">File name length</a>
</li>
<li>
<a id="itemTokens" onclick="panel.changeToken()">Manage your token</a>
<a id="itemTokens" onclick="page.changeToken()">Manage your token</a>
</li>
<li>
<a id="itemPassword" onclick="panel.changePassword()">Change your password</a>
<a id="itemPassword" onclick="page.changePassword()">Change your password</a>
</li>
<li>
<a id="itemLogout" onclick="panel.logout()">Logout</a>
<a id="itemLogout" onclick="page.logout()">Logout</a>
</li>
</ul>
</aside>
@ -89,13 +90,13 @@
</section>
<div id="modal" class="modal">
<div class="modal-background" onclick="panel.closeModal()"></div>
<div class="modal-background" onclick="page.closeModal()"></div>
<div class="modal-content">
<figure class="image">
<img id="modalImage" alt="modal-image">
</figure>
</div>
<button class="modal-close is-large" aria-label="close" onclick="panel.closeModal()"></button>
<button class="modal-close is-large" aria-label="close" onclick="page.closeModal()"></button>
</div>
{% include "_partial/noscript.njk" %}

View File

@ -13,6 +13,7 @@
<script type="text/javascript" src="libs/dropzone/dropzone.min.js?v={{ globals.v1 }}"></script>
<script type="text/javascript" src="libs/axios/axios.min.js?v={{ globals.v1 }}"></script>
<script type="text/javascript" src="libs/clipboard.js/clipboard.min.js?v={{ globals.v1 }}"></script>
<script type="text/javascript" src="libs/lazyload/lazyload.min.js?v={{ globals.v1 }}"></script>
<script type="text/javascript" src="js/home.js?v={{ globals.v1 }}"></script>
{% endblock %}
@ -51,18 +52,19 @@
<div class="column is-hidden-mobile"></div>
<div class="column">
<progress class="progress is-small is-danger" value="0" max="100"></progress>
<img class="is-unselectable" data-dz-thumbnail>
<img class="is-unselectable">
<p class="name is-unselectable"></p>
<p class="error"></p>
<p class="link" style="display: none">
<p class="link">
<a target="_blank" rel="noopener"></a>
</p>
<p class="clipboard-mobile is-hidden-desktop" style="display: none">
<a class="button is-info is-outlined clipboard-js" style="display: flex">
<span class="icon">
<i class="icon-clipboard-1"></i>
</span>
<span>Copy link to clipboard</span>
</a>
<span class="icon">
<i class="icon-clipboard-1"></i>
</span>
<span>Copy link to clipboard</span>
</a>
</p>
</div>
<div class="column is-hidden-mobile"></div>