Improved performance of /api/users/:id (admin's manage users).

Promisify fs.writeFile.

Improved performance of /api/stats.
By a lot in Linux, cause uploads size will be deferred to "du" binary.
In addition, total usage of whichever disk uploads path resides on will
also be queried using "df" binary.
Non-Linux will have to rely on manual calculation by querying DB
for each upload's size.
But logics related to uploads stats were also improved to be almost
twice as fast as before.

Improved parsing of /api/stats results on dashboard.js.
This allows ease of extending server's response by not having to update
dashboard.js by much, if at all.

Improved codes relating to item menus in dashboard's sidebar.
Finally much cleaner now 👍

No longer use /api/upload/delete API route from dashboard.
Single file deletion and bulk files deletion, both from uploads list or
by names, will now properly use a single function that will use
/api/upload/bulkdelete API route.

/api/upload/delete will still be kept indefinitely for backward support.

Fixed oddities with Select all checkbox.

Replaced all instances of modifying HTML element's style attribute with
adding/removing is-hidden CSS helper class.

Rephrased all instances of "files" to "uploads" in any display strings.

Fixed notice message when server is on private mode.

A few other improvements.
This commit is contained in:
Bobby Wibowo 2019-09-10 23:31:27 +07:00
parent df8cac0f0b
commit 264bd88e88
No known key found for this signature in database
GPG Key ID: 51C3A1E1E22D26CF
10 changed files with 657 additions and 537 deletions

View File

@ -205,33 +205,22 @@ self.listUsers = async (req, res, next) => {
.offset(25 * offset) .offset(25 * offset)
.select('id', 'username', 'enabled', 'permission') .select('id', 'username', 'enabled', 'permission')
const userids = [] const pointers = {}
for (const user of users) { for (const user of users) {
user.groups = perms.mapPermissions(user) user.groups = perms.mapPermissions(user)
delete user.permission delete user.permission
user.uploads = 0
userids.push(user.id) user.usage = 0
user.uploadsCount = 0 pointers[user.id] = user
user.diskUsage = 0
} }
const maps = {}
const uploads = await db.table('files') const uploads = await db.table('files')
.whereIn('userid', userids) .whereIn('userid', Object.keys(pointers))
.select('userid', 'size')
for (const upload of uploads) { for (const upload of uploads) {
if (maps[upload.userid] === undefined) pointers[upload.userid].uploads++
maps[upload.userid] = { count: 0, size: 0 } pointers[upload.userid].usage += parseInt(upload.size)
maps[upload.userid].count++
maps[upload.userid].size += parseInt(upload.size)
}
for (const user of users) {
if (!maps[user.id]) continue
user.uploadsCount = maps[user.id].count
user.diskUsage = maps[user.id].size
} }
return res.json({ success: true, users, count }) return res.json({ success: true, users, count })

View File

@ -16,7 +16,8 @@ const fsFuncs = [
'rename', 'rename',
'rmdir', 'rmdir',
'symlink', 'symlink',
'unlink' 'unlink',
'writeFile'
] ]
for (const fsFunc of fsFuncs) for (const fsFunc of fsFuncs)

View File

@ -312,12 +312,7 @@ self.actuallyUploadUrls = async (req, res, user, albumid, age) => {
const name = await self.getUniqueRandomName(length, extname) const name = await self.getUniqueRandomName(length, extname)
const destination = path.join(paths.uploads, name) const destination = path.join(paths.uploads, name)
await new Promise((resolve, reject) => { await paths.writeFile(destination, file)
fs.writeFile(destination, file, error => {
if (error) return reject(error)
return resolve()
})
})
downloaded.push(destination) downloaded.push(destination)
infoMap.push({ infoMap.push({

View File

@ -1,4 +1,5 @@
const { promisify } = require('util') const { promisify } = require('util')
const { spawn } = require('child_process')
const config = require('./../config') const config = require('./../config')
const db = require('knex')(config.database) const db = require('knex')(config.database)
const fetch = require('node-fetch') const fetch = require('node-fetch')
@ -33,6 +34,10 @@ const statsCache = {
cache: null, cache: null,
generating: false generating: false
}, },
disk: {
cache: null,
generating: false
},
albums: { albums: {
cache: null, cache: null,
generating: false, generating: false,
@ -500,153 +505,287 @@ self.stats = async (req, res, next) => {
const isadmin = perms.is(user, 'admin') const isadmin = perms.is(user, 'admin')
if (!isadmin) return res.status(403).end() if (!isadmin) return res.status(403).end()
const stats = {} try {
const stats = {}
// Re-use caches as long as they are still valid
if (!statsCache.system.cache && statsCache.system.generating) {
stats.system = false
} else if (statsCache.system.generating) {
stats.system = statsCache.system.cache
} else {
statsCache.system.generating = true
const os = await si.osInfo() const os = await si.osInfo()
const currentLoad = await si.currentLoad()
const mem = await si.mem()
stats.system = { // System info
platform: `${os.platform} ${os.arch}`, if (!statsCache.system.cache && statsCache.system.generating) {
distro: `${os.distro} ${os.release}`, stats.system = false
kernel: os.kernel, } else if (statsCache.system.generating) {
cpuLoad: `${currentLoad.currentload.toFixed(1)}%`, stats.system = statsCache.system.cache
cpusLoad: currentLoad.cpus.map(cpu => `${cpu.load.toFixed(1)}%`).join(', '), } else {
systemMemory: { statsCache.system.generating = true
used: mem.active,
total: mem.total const currentLoad = await si.currentLoad()
}, const mem = await si.mem()
memoryUsage: process.memoryUsage().rss,
nodeVersion: `${process.versions.node}` stats.system = {
_types: {
byte: ['memoryUsage'],
byteUsage: ['systemMemory']
},
platform: `${os.platform} ${os.arch}`,
distro: `${os.distro} ${os.release}`,
kernel: os.kernel,
cpuLoad: `${currentLoad.currentload.toFixed(1)}%`,
cpusLoad: currentLoad.cpus.map(cpu => `${cpu.load.toFixed(1)}%`).join(', '),
systemMemory: {
used: mem.active,
total: mem.total
},
memoryUsage: process.memoryUsage().rss,
nodeVersion: `${process.versions.node}`
}
// Update cache
statsCache.system.cache = stats.system
statsCache.system.generating = false
} }
// Update cache // Disk usage, only for Linux platform
statsCache.system.cache = stats.system if (os.platform === 'linux')
statsCache.system.generating = false if (!statsCache.disk.cache && statsCache.disk.generating) {
} stats.disk = false
} else if (statsCache.disk.generating) {
stats.disk = statsCache.disk.cache
} else {
statsCache.disk.generating = true
if (!statsCache.albums.cache && statsCache.albums.generating) { // We pre-assign the keys below to guarantee their order
stats.albums = false stats.disk = {
} else if ((statsCache.albums.invalidatedAt < statsCache.albums.generatedAt) || statsCache.albums.generating) { _types: {
stats.albums = statsCache.albums.cache byte: ['uploads', 'thumbs', 'zips', 'chunks'],
} else { byteUsage: ['drive']
statsCache.albums.generating = true },
stats.albums = { drive: null,
total: 0, uploads: 0,
active: 0, thumbs: 0,
downloadable: 0, zips: 0,
public: 0, chunks: 0
zips: 0 }
// Get size of directories in uploads path
await new Promise((resolve, reject) => {
const proc = spawn('du', [
'--apparent-size',
'--block-size=1',
'--dereference',
'--separate-dirs',
paths.uploads
])
proc.stdout.on('data', data => {
const formatted = String(data)
.trim()
.split(/\s+/)
if (formatted.length !== 2) return
const basename = path.basename(formatted[1])
stats.disk[basename] = parseInt(formatted[0])
// Add to types if necessary
if (!stats.disk._types.byte.includes(basename))
stats.disk._types.byte.push(basename)
})
const stderr = []
proc.stderr.on('data', data => stderr.push(data))
proc.on('exit', code => {
if (code !== 0) return reject(stderr)
resolve()
})
})
// Get disk usage of whichever disk uploads path resides on
await new Promise((resolve, reject) => {
const proc = spawn('df', [
'--block-size=1',
'--output=used,size',
paths.uploads
])
proc.stdout.on('data', data => {
// Only use the first valid line
if (stats.disk.drive !== null) return
const lines = String(data)
.trim()
.split('\n')
if (lines.length !== 2) return
for (const line of lines) {
const columns = line.split(/\s+/)
// Skip lines that have non-number chars
if (columns.some(w => !/^\d+$/.test(w))) continue
stats.disk.drive = {
used: parseInt(columns[0]),
total: parseInt(columns[1])
}
}
})
const stderr = []
proc.stderr.on('data', data => stderr.push(data))
proc.on('exit', code => {
if (code !== 0) return reject(stderr)
resolve()
})
})
// Update cache
statsCache.disk.cache = stats.system
statsCache.disk.generating = false
}
// Uploads
if (!statsCache.uploads.cache && statsCache.uploads.generating) {
stats.uploads = false
} else if ((statsCache.uploads.invalidatedAt < statsCache.uploads.generatedAt) || statsCache.uploads.generating) {
stats.uploads = statsCache.uploads.cache
} else {
statsCache.uploads.generating = true
stats.uploads = {
_types: {
number: ['total', 'images', 'videos', 'others']
},
total: 0,
images: 0,
videos: 0,
others: 0
}
if (os.platform !== 'linux') {
// If not Linux platform, rely on DB for total size
const uploads = await db.table('files')
.select('size')
stats.uploads.total = uploads.length
stats.uploads.sizeInDb = uploads.reduce((acc, upload) => acc + parseInt(upload.size), 0)
// Add type information for the new column
if (!Array.isArray(stats.uploads._types.byte))
stats.uploads._types.byte = []
stats.uploads._types.byte.push('sizeInDb')
} else {
stats.uploads.total = await db.table('files')
.count('id as count')
.then(rows => rows[0].count)
}
stats.uploads.images = await db.table('files')
.whereRaw(self.imageExts.map(ext => `\`name\` like '%${ext}'`).join(' or '))
.count('id as count')
.then(rows => rows[0].count)
stats.uploads.videos = await db.table('files')
.whereRaw(self.videoExts.map(ext => `\`name\` like '%${ext}'`).join(' or '))
.count('id as count')
.then(rows => rows[0].count)
stats.uploads.others = stats.uploads.total - stats.uploads.images - stats.uploads.videos
// Update cache
statsCache.uploads.cache = stats.uploads
statsCache.uploads.generatedAt = Date.now()
statsCache.uploads.generating = false
} }
const albums = await db.table('albums') // Users
stats.albums.total = albums.length if (!statsCache.users.cache && statsCache.users.generating) {
const identifiers = [] stats.users = false
for (const album of albums) } else if ((statsCache.users.invalidatedAt < statsCache.users.generatedAt) || statsCache.users.generating) {
if (album.enabled) { stats.users = statsCache.users.cache
stats.albums.active++ } else {
statsCache.users.generating = true
stats.users = {
_types: {
number: ['total', 'disabled']
},
total: 0,
disabled: 0
}
const permissionKeys = Object.keys(perms.permissions).reverse()
permissionKeys.forEach(p => {
stats.users[p] = 0
stats.users._types.number.push(p)
})
const users = await db.table('users')
stats.users.total = users.length
for (const user of users) {
if (user.enabled === false || user.enabled === 0)
stats.users.disabled++
// This may be inaccurate on installations with customized permissions
user.permission = user.permission || 0
for (const p of permissionKeys)
if (user.permission === perms.permissions[p]) {
stats.users[p]++
break
}
}
// Update cache
statsCache.users.cache = stats.users
statsCache.users.generatedAt = Date.now()
statsCache.users.generating = false
}
// Albums
if (!statsCache.albums.cache && statsCache.albums.generating) {
stats.albums = false
} else if ((statsCache.albums.invalidatedAt < statsCache.albums.generatedAt) || statsCache.albums.generating) {
stats.albums = statsCache.albums.cache
} else {
statsCache.albums.generating = true
stats.albums = {
_types: {
number: ['total', 'active', 'downloadable', 'public', 'generatedZip']
},
total: 0,
disabled: 0,
public: 0,
downloadable: 0,
zipGenerated: 0
}
const albums = await db.table('albums')
stats.albums.total = albums.length
const identifiers = []
for (const album of albums) {
if (!album.enabled) {
stats.albums.disabled++
continue
}
if (album.download) stats.albums.downloadable++ if (album.download) stats.albums.downloadable++
if (album.public) stats.albums.public++ if (album.public) stats.albums.public++
if (album.zipGeneratedAt) identifiers.push(album.identifier) if (album.zipGeneratedAt) identifiers.push(album.identifier)
} }
const zipsDir = path.join(paths.uploads, 'zips') for (const identifier of identifiers)
await Promise.all(identifiers.map(identifier => { try {
return new Promise(resolve => { await paths.access(path.join(paths.zips, `${identifier}.zip`))
const filePath = path.join(zipsDir, `${identifier}.zip`) stats.albums.zipGenerated++
fs.access(filePath, error => { } catch (error) {
if (!error) stats.albums.zips++ // Re-throw error
resolve(true) if (error.code !== 'ENOENT')
}) throw error
})
}))
// Update cache
statsCache.albums.cache = stats.albums
statsCache.albums.generatedAt = Date.now()
statsCache.albums.generating = false
}
if (!statsCache.users.cache && statsCache.users.generating) {
stats.users = false
} else if ((statsCache.users.invalidatedAt < statsCache.users.generatedAt) || statsCache.users.generating) {
stats.users = statsCache.users.cache
} else {
statsCache.users.generating = true
stats.users = {
total: 0,
disabled: 0
}
const permissionKeys = Object.keys(perms.permissions)
permissionKeys.forEach(p => {
stats.users[p] = 0
})
const users = await db.table('users')
stats.users.total = users.length
for (const user of users) {
if (user.enabled === false || user.enabled === 0)
stats.users.disabled++
// This may be inaccurate on installations with customized permissions
user.permission = user.permission || 0
for (const p of permissionKeys)
if (user.permission === perms.permissions[p]) {
stats.users[p]++
break
} }
// Update cache
statsCache.albums.cache = stats.albums
statsCache.albums.generatedAt = Date.now()
statsCache.albums.generating = false
} }
// Update cache return res.json({ success: true, stats })
statsCache.users.cache = stats.users } catch (error) {
statsCache.users.generatedAt = Date.now() logger.error(error)
statsCache.users.generating = false return res.status(500).json({ success: false, description: 'An unexpected error occurred. Try again?' })
} }
if (!statsCache.uploads.cache && statsCache.uploads.generating) {
stats.uploads = false
} else if ((statsCache.uploads.invalidatedAt < statsCache.uploads.generatedAt) || statsCache.uploads.generating) {
stats.uploads = statsCache.uploads.cache
} else {
statsCache.uploads.generating = true
stats.uploads = {
total: 0,
size: 0,
images: 0,
videos: 0,
others: 0
}
const uploads = await db.table('files')
stats.uploads.total = uploads.length
for (const upload of uploads) {
stats.uploads.size += parseInt(upload.size)
const extname = self.extname(upload.name)
if (self.imageExts.includes(extname))
stats.uploads.images++
else if (self.videoExts.includes(extname))
stats.uploads.videos++
else
stats.uploads.others++
}
// Update cache
statsCache.uploads.cache = stats.uploads
statsCache.uploads.generatedAt = Date.now()
statsCache.uploads.generating = false
}
return res.json({ success: true, stats })
} }
module.exports = self module.exports = self

View File

@ -3,9 +3,7 @@ body {
animation: none; animation: none;
} }
#auth,
#dashboard { #dashboard {
display: none;
-webkit-animation: fadeInOpacity .5s; -webkit-animation: fadeInOpacity .5s;
animation: fadeInOpacity .5s; animation: fadeInOpacity .5s;
} }
@ -102,15 +100,11 @@ li[data-action="page-ellipsis"] {
.no-touch .image-container .checkbox { .no-touch .image-container .checkbox {
opacity: .5; opacity: .5;
-webkit-transition: opacity .25s;
transition: opacity .25s;
} }
.no-touch .image-container .controls, .no-touch .image-container .controls,
.no-touch .image-container .details { .no-touch .image-container .details {
opacity: 0; opacity: 0;
-webkit-transition: opacity .25s;
transition: opacity .25s;
} }
.no-touch .image-container:hover .checkbox, .no-touch .image-container:hover .checkbox,

View File

@ -24,6 +24,9 @@ const page = {
username: null, username: null,
permissions: null, permissions: null,
// sidebar menus
menus: [],
currentView: null, currentView: null,
views: { views: {
// config of uploads view // config of uploads view
@ -75,7 +78,8 @@ const page = {
clipboardJS: null, clipboardJS: null,
lazyLoad: null, lazyLoad: null,
imageExtensions: ['.webp', '.jpg', '.jpeg', '.bmp', '.gif', '.png'], imageExts: ['.webp', '.jpg', '.jpeg', '.gif', '.png', '.tiff', '.tif', '.svg'],
videoExts: ['.webm', '.mp4', '.wmv', '.avi', '.mov', '.mkv'],
fadingIn: null fadingIn: null
} }
@ -115,77 +119,64 @@ page.verifyToken = function (token, reloadOnError) {
page.prepareDashboard = function () { page.prepareDashboard = function () {
page.dom = document.querySelector('#page') page.dom = document.querySelector('#page')
// Capture all click events
page.dom.addEventListener('click', page.domClick, true) page.dom.addEventListener('click', page.domClick, true)
// Capture all submit events
page.dom.addEventListener('submit', function (event) { page.dom.addEventListener('submit', function (event) {
const element = event.target // Prevent default if necessary
if (element && element.classList.contains('prevent-default')) if (event.target && event.target.classList.contains('prevent-default'))
return event.preventDefault() return event.preventDefault()
}, true) }, true)
document.querySelector('#dashboard').style.display = 'block' // All item menus in the sidebar
const itemMenus = [
{ selector: '#itemUploads', onclick: page.getUploads },
{ selector: '#itemDeleteUploadsByNames', onclick: page.deleteUploadsByNames },
{ selector: '#itemManageAlbums', onclick: page.getAlbums },
{ selector: '#itemManageToken', onclick: page.changeToken },
{ selector: '#itemChangePassword', onclick: page.changePassword },
{ selector: '#itemLogout', onclick: page.logout, inactive: true },
{ selector: '#itemManageUploads', onclick: page.getUploads, params: [{ all: true }], group: 'moderator' },
{ selector: '#itemStatistics', onclick: page.getStatistics, group: 'admin' },
{ selector: '#itemManageUsers', onclick: page.getUsers, group: 'admin' }
]
for (let i = 0; i < itemMenus.length; i++) {
// Skip item menu if not enough permission
if (itemMenus[i].group && !page.permissions[itemMenus[i].group])
continue
// Add onclick event listener
const item = document.querySelector(itemMenus[i].selector)
item.addEventListener('click', function () {
itemMenus[i].onclick.apply(null, itemMenus[i].params)
if (!itemMenus[i].inactive)
page.setActiveMenu(this)
})
item.classList.remove('is-hidden')
page.menus.push(item)
}
// If at least a moderator, show administration section
if (page.permissions.moderator) { if (page.permissions.moderator) {
document.querySelector('#itemLabelAdmin').style.display = 'block' document.querySelector('#itemLabelAdmin').classList.remove('is-hidden')
document.querySelector('#itemListAdmin').style.display = 'block' document.querySelector('#itemListAdmin').classList.remove('is-hidden')
const itemManageUploads = document.querySelector('#itemManageUploads')
itemManageUploads.addEventListener('click', function () {
page.setActiveMenu(this)
page.getUploads({ all: true })
})
} }
if (page.permissions.admin) { // Update text of logout button
const itemServerStats = document.querySelector('#itemServerStats') document.querySelector('#itemLogout').innerHTML = `Logout ( ${page.username} )`
itemServerStats.addEventListener('click', function () {
page.setActiveMenu(this)
page.getServerStats()
})
const itemManageUsers = document.querySelector('#itemManageUsers') // Finally display dashboard
itemManageUsers.addEventListener('click', function () { document.querySelector('#dashboard').classList.remove('is-hidden')
page.setActiveMenu(this)
page.getUsers()
})
} else {
document.querySelector('#itemServerStats').style.display = 'none'
document.querySelector('#itemManageUsers').style.display = 'none'
}
document.querySelector('#itemUploads').addEventListener('click', function () {
page.setActiveMenu(this)
page.getUploads({ all: false })
})
document.querySelector('#itemDeleteByNames').addEventListener('click', function () {
page.setActiveMenu(this)
page.deleteByNames()
})
document.querySelector('#itemManageGallery').addEventListener('click', function () {
page.setActiveMenu(this)
page.getAlbums()
})
document.querySelector('#itemTokens').addEventListener('click', function () {
page.setActiveMenu(this)
page.changeToken()
})
document.querySelector('#itemPassword').addEventListener('click', function () {
page.setActiveMenu(this)
page.changePassword()
})
const logoutBtn = document.querySelector('#itemLogout')
logoutBtn.addEventListener('click', function () {
page.logout()
})
logoutBtn.innerHTML = `Logout ( ${page.username} )`
// Load albums sidebar
page.getAlbumsSidebar() page.getAlbumsSidebar()
if (typeof page.prepareShareX === 'function') page.prepareShareX() if (typeof page.prepareShareX === 'function')
page.prepareShareX()
} }
page.logout = function () { page.logout = function () {
@ -234,22 +225,20 @@ page.domClick = function (event) {
return page.setUploadsView('thumbs', element) return page.setUploadsView('thumbs', element)
case 'clear-selection': case 'clear-selection':
return page.clearSelection() return page.clearSelection()
case 'add-selected-files-to-album': case 'add-selected-uploads-to-album':
return page.addSelectedFilesToAlbum() return page.addSelectedUploadsToAlbum()
case 'bulk-delete':
return page.deleteSelectedFiles()
case 'select': case 'select':
return page.select(element, event) return page.select(element, event)
case 'add-to-album':
return page.addSingleFileToAlbum(id)
case 'delete-file':
return page.deleteFile(id)
case 'select-all': case 'select-all':
return page.selectAll(element) return page.selectAll(element)
case 'add-to-album':
return page.addToAlbum(id)
case 'delete-upload':
return page.deleteUpload(id)
case 'bulk-delete-uploads':
return page.bulkDeleteUploads()
case 'display-thumbnail': case 'display-thumbnail':
return page.displayThumbnail(id) return page.displayThumbnail(id)
case 'delete-file-by-names':
return page.deleteFileByNames()
case 'submit-album': case 'submit-album':
return page.submitAlbum(element) return page.submitAlbum(element)
case 'edit-album': case 'edit-album':
@ -457,12 +446,12 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele
</span> </span>
</a> </a>
${all ? '' : ` ${all ? '' : `
<a class="button is-small is-warning" title="Add selected uploads to album" data-action="add-selected-files-to-album"> <a class="button is-small is-warning" title="Bulk add to album" data-action="add-selected-uploads-to-album">
<span class="icon"> <span class="icon">
<i class="icon-plus"></i> <i class="icon-plus"></i>
</span> </span>
</a>`} </a>`}
<a class="button is-small is-danger" title="Bulk delete" data-action="bulk-delete"> <a class="button is-small is-danger" title="Bulk delete" data-action="bulk-delete-uploads">
<span class="icon"> <span class="icon">
<i class="icon-trash"></i> <i class="icon-trash"></i>
</span> </span>
@ -472,8 +461,8 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele
</div> </div>
` `
// Set to true to tick "all files" checkbox in list view // Whether there are any unselected items
let allSelected = true let unselected = false
const hasExpiryDateColumn = files.some(file => file.expirydate !== undefined) const hasExpiryDateColumn = files.some(file => file.expirydate !== undefined)
@ -501,7 +490,7 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele
// Update selected status // Update selected status
files[i].selected = page.selected[page.currentView].includes(files[i].id) files[i].selected = page.selected[page.currentView].includes(files[i].id)
if (allSelected && !files[i].selected) allSelected = false if (!files[i].selected) unselected = true
// Appendix (display album or user) // Appendix (display album or user)
if (all) if (all)
@ -539,7 +528,7 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele
div.innerHTML = `<a class="image" href="${upload.file}" target="_blank" rel="noopener"><h1 class="title">${upload.extname || 'N/A'}</h1></a>` div.innerHTML = `<a class="image" href="${upload.file}" target="_blank" rel="noopener"><h1 class="title">${upload.extname || 'N/A'}</h1></a>`
div.innerHTML += ` div.innerHTML += `
<input type="checkbox" class="checkbox" title="Select this file" data-action="select"${upload.selected ? ' checked' : ''}> <input type="checkbox" class="checkbox" title="Select" data-action="select"${upload.selected ? ' checked' : ''}>
<div class="controls"> <div class="controls">
<a class="button is-small is-primary" title="View thumbnail" data-action="display-thumbnail"${upload.thumb ? '' : ' disabled'}> <a class="button is-small is-primary" title="View thumbnail" data-action="display-thumbnail"${upload.thumb ? '' : ' disabled'}>
<span class="icon"> <span class="icon">
@ -556,7 +545,7 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele
<i class="icon-plus"></i> <i class="icon-plus"></i>
</span> </span>
</a> </a>
<a class="button is-small is-danger" title="Delete file" data-action="delete-file"> <a class="button is-small is-danger" title="Delete" data-action="delete-upload">
<span class="icon"> <span class="icon">
<i class="icon-trash"></i> <i class="icon-trash"></i>
</span> </span>
@ -581,7 +570,7 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele
<table class="table is-narrow is-fullwidth is-hoverable"> <table class="table is-narrow is-fullwidth is-hoverable">
<thead> <thead>
<tr> <tr>
<th><input id="selectAll" class="checkbox" type="checkbox" title="Select all uploads" data-action="select-all"></th> <th><input id="selectAll" class="checkbox" type="checkbox" title="Select all" data-action="select-all"></th>
<th style="width: 20%">File</th> <th style="width: 20%">File</th>
<th>${all ? 'User' : 'Album'}</th> <th>${all ? 'User' : 'Album'}</th>
<th>Size</th> <th>Size</th>
@ -606,7 +595,7 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele
const tr = document.createElement('tr') const tr = document.createElement('tr')
tr.dataset.id = upload.id tr.dataset.id = upload.id
tr.innerHTML = ` tr.innerHTML = `
<td class="controls"><input type="checkbox" class="checkbox" title="Select this file" data-action="select"${upload.selected ? ' checked' : ''}></td> <td class="controls"><input type="checkbox" class="checkbox" title="Select" data-action="select"${upload.selected ? ' checked' : ''}></td>
<th><a href="${upload.file}" target="_blank" rel="noopener" title="${upload.file}">${upload.name}</a></th> <th><a href="${upload.file}" target="_blank" rel="noopener" title="${upload.file}">${upload.name}</a></th>
<th>${upload.appendix}</th> <th>${upload.appendix}</th>
<td>${upload.prettyBytes}</td> <td>${upload.prettyBytes}</td>
@ -630,7 +619,7 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele
<i class="icon-plus"></i> <i class="icon-plus"></i>
</span> </span>
</a>`} </a>`}
<a class="button is-small is-danger" title="Delete file" data-action="delete-file"> <a class="button is-small is-danger" title="Delete" data-action="delete-upload">
<span class="icon"> <span class="icon">
<i class="icon-trash"></i> <i class="icon-trash"></i>
</span> </span>
@ -642,13 +631,15 @@ page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, ele
page.checkboxes[page.currentView] = Array.from(table.querySelectorAll('.checkbox[data-action="select"]')) page.checkboxes[page.currentView] = Array.from(table.querySelectorAll('.checkbox[data-action="select"]'))
} }
} }
page.fadeAndScroll()
if (allSelected && files.length) { const selectAll = document.querySelector('#selectAll')
const selectAll = document.querySelector('#selectAll') if (selectAll && !unselected) {
if (selectAll) selectAll.checked = true selectAll.checked = true
selectAll.title = 'Unselect all'
} }
page.fadeAndScroll()
if (page.currentView === 'uploads') page.views.uploads.album = album if (page.currentView === 'uploads') page.views.uploads.album = album
if (page.currentView === 'uploadsAll') page.views.uploadsAll.filters = filters if (page.currentView === 'uploadsAll') page.views.uploadsAll.filters = filters
page.views[page.currentView].pageNum = files.length ? pageNum : 0 page.views[page.currentView].pageNum = files.length ? pageNum : 0
@ -693,27 +684,28 @@ page.displayThumbnail = function (id) {
const thumb = div.querySelector('#swalThumb') const thumb = div.querySelector('#swalThumb')
const exec = /.[\w]+(\?|$)/.exec(original) const exec = /.[\w]+(\?|$)/.exec(original)
const isimage = exec && exec[0] && page.imageExtensions.includes(exec[0].toLowerCase()) if (!exec || !exec[0]) return
if (isimage) { const extname = exec[0].toLowerCase()
if (page.imageExts.includes(extname)) {
thumb.src = file.original thumb.src = file.original
thumb.onload = function () { thumb.onload = function () {
button.style.display = 'none' button.classList.add('is-hidden')
document.body.querySelector('.swal-overlay .swal-modal:not(.is-expanded)').classList.add('is-expanded') document.body.querySelector('.swal-overlay .swal-modal:not(.is-expanded)').classList.add('is-expanded')
} }
thumb.onerror = function () { thumb.onerror = function () {
button.className = 'button is-danger' button.className = 'button is-danger'
button.innerHTML = 'Unable to load original' button.innerHTML = 'Unable to load original'
} }
} else { } else if (page.videoExts.includes(extname)) {
thumb.style.display = 'none' thumb.classList.add('is-hidden')
const video = document.createElement('video') const video = document.createElement('video')
video.id = 'swalVideo' video.id = 'swalVideo'
video.controls = true video.controls = true
video.src = file.original video.src = file.original
thumb.insertAdjacentElement('afterend', video) thumb.insertAdjacentElement('afterend', video)
button.style.display = 'none' button.classList.add('is-hidden')
document.body.querySelector('.swal-overlay .swal-modal:not(.is-expanded)').classList.add('is-expanded') document.body.querySelector('.swal-overlay .swal-modal:not(.is-expanded)').classList.add('is-expanded')
} }
}) })
@ -732,68 +724,76 @@ page.displayThumbnail = function (id) {
} }
page.selectAll = function (element) { page.selectAll = function (element) {
const checkboxes = page.checkboxes[page.currentView] for (let i = 0; i < page.checkboxes[page.currentView].length; i++) {
const selected = page.selected[page.currentView] const id = page.getItemID(page.checkboxes[page.currentView][i])
for (let i = 0; i < checkboxes.length; i++) {
const id = page.getItemID(checkboxes[i])
if (isNaN(id)) continue if (isNaN(id)) continue
if (checkboxes[i].checked !== element.checked) { if (page.checkboxes[page.currentView][i].checked !== element.checked) {
checkboxes[i].checked = element.checked page.checkboxes[page.currentView][i].checked = element.checked
if (checkboxes[i].checked) if (page.checkboxes[page.currentView][i].checked)
selected.push(id) page.selected[page.currentView].push(id)
else else
selected.splice(selected.indexOf(id), 1) page.selected[page.currentView].splice(page.selected[page.currentView].indexOf(id), 1)
} }
} }
localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(selected) if (page.selected[page.currentView].length)
page.selected[page.currentView] = selected localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(page.selected[page.currentView])
element.title = element.checked ? 'Unselect all uploads' : 'Select all uploads' else
delete localStorage[lsKeys.selected[page.currentView]]
element.title = element.checked ? 'Unselect all' : 'Select all'
} }
page.selectInBetween = function (element, lastElement) { page.selectInBetween = function (element, lastElement) {
if (!element || !lastElement) return if (!element || !lastElement || element === lastElement)
if (element === lastElement) return return
const checkboxes = page.checkboxes[page.currentView] if (!Array.isArray(page.checkboxes[page.currentView]) || !page.checkboxes[page.currentView].length)
if (!checkboxes || !checkboxes.length) return return
const thisIndex = checkboxes.indexOf(element) const thisIndex = page.checkboxes[page.currentView].indexOf(element)
const lastIndex = checkboxes.indexOf(lastElement) const lastIndex = page.checkboxes[page.currentView].indexOf(lastElement)
const distance = thisIndex - lastIndex const distance = thisIndex - lastIndex
if (distance >= -1 && distance <= 1) return if (distance >= -1 && distance <= 1)
return
for (let i = 0; i < checkboxes.length; i++) for (let i = 0; i < page.checkboxes[page.currentView].length; i++)
if ((thisIndex > lastIndex && i > lastIndex && i < thisIndex) || if ((thisIndex > lastIndex && i > lastIndex && i < thisIndex) ||
(thisIndex < lastIndex && i > thisIndex && i < lastIndex)) { (thisIndex < lastIndex && i > thisIndex && i < lastIndex)) {
checkboxes[i].checked = true // Check or uncheck depending on the state of the initial checkbox
page.selected[page.currentView].push(page.getItemID(checkboxes[i])) page.checkboxes[page.currentView][i].checked = lastElement.checked
const id = page.getItemID(page.checkboxes[page.currentView][i])
if (!page.selected[page.currentView].includes(id) && page.checkboxes[page.currentView][i].checked)
page.selected[page.currentView].push(id)
else if (page.selected[page.currentView].includes(id) && !page.checkboxes[page.currentView][i].checked)
page.selected[page.currentView].splice(page.selected[page.currentView].indexOf(id), 1)
} }
localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(page.selected[page.currentView])
page.checkboxes[page.currentView] = checkboxes
} }
page.select = function (element, event) { page.select = function (element, event) {
const lastSelected = page.lastSelected[page.currentView]
if (event.shiftKey && lastSelected)
page.selectInBetween(element, lastSelected)
else
page.lastSelected[page.currentView] = element
const id = page.getItemID(element) const id = page.getItemID(element)
if (isNaN(id)) return if (isNaN(id)) return
const selected = page.selected[page.currentView] const lastSelected = page.lastSelected[page.currentView]
if (!selected.includes(id) && element.checked) if (event.shiftKey && lastSelected) {
selected.push(id) page.selectInBetween(element, lastSelected)
else if (selected.includes(id) && !element.checked) // Check or uncheck depending on the state of the initial checkbox
selected.splice(selected.indexOf(id), 1) element.checked = lastSelected.checked
} else {
page.lastSelected[page.currentView] = element
}
localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(selected) if (!page.selected[page.currentView].includes(id) && element.checked)
page.selected[page.currentView] = selected page.selected[page.currentView].push(id)
else if (page.selected[page.currentView].includes(id) && !element.checked)
page.selected[page.currentView].splice(page.selected[page.currentView].indexOf(id), 1)
// Update local storage
if (page.selected[page.currentView].length)
localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(page.selected[page.currentView])
else
delete localStorage[lsKeys.selected[page.currentView]]
} }
page.clearSelection = function () { page.clearSelection = function () {
@ -830,7 +830,7 @@ page.filtersHelp = function (element) {
const content = document.createElement('div') const content = document.createElement('div')
content.style = 'text-align: left' content.style = 'text-align: left'
content.innerHTML = ` content.innerHTML = `
This supports 3 filter keys, namely <b>user</b> (username), <b>ip</b> and <b>name</b> (file name). This supports 3 filter keys, namely <b>user</b> (username), <b>ip</b> and <b>name</b> (upload name).
Each key can be specified more than once. Each key can be specified more than once.
Backlashes should be used if the usernames have spaces. Backlashes should be used if the usernames have spaces.
There are also 2 additional flags, namely <b>-user</b> and <b>-ip</b>, which will match uploads by non-registered users and have no IPs respectively. There are also 2 additional flags, namely <b>-user</b> and <b>-ip</b>, which will match uploads by non-registered users and have no IPs respectively.
@ -847,7 +847,7 @@ page.filtersHelp = function (element) {
Uploads from users with username either "John Doe" OR "demo": Uploads from users with username either "John Doe" OR "demo":
<code>user:John\\ Doe user:demo</code> <code>user:John\\ Doe user:demo</code>
Uploads from IP "127.0.0.1" AND which file names match "*.rar" OR "*.zip": Uploads from IP "127.0.0.1" AND which upload names match "*.rar" OR "*.zip":
<code>ip:127.0.0.1 name:*.rar name:*.zip</code> <code>ip:127.0.0.1 name:*.rar name:*.zip</code>
Uploads from user with username "test" OR from non-registered users: Uploads from user with username "test" OR from non-registered users:
@ -864,56 +864,133 @@ page.filterUploads = function (element) {
page.viewUserUploads = function (id) { page.viewUserUploads = function (id) {
const user = page.cache.users[id] const user = page.cache.users[id]
if (!user) return if (!user) return
page.setActiveMenu(document.querySelector('#itemManageUploads'))
page.getUploads({ all: true, filters: `user:${user.username.replace(/ /g, '\\ ')}` }) page.getUploads({ all: true, filters: `user:${user.username.replace(/ /g, '\\ ')}` })
page.setActiveMenu(document.querySelector('#itemManageUploads'))
} }
page.deleteFile = function (id) { page.deleteUpload = function (id) {
// TODO: Share function with bulk delete, just like 'add selected uploads to album' and 'add single file to album' page.postBulkDeleteUploads({
swal({ field: 'id',
title: 'Are you sure?', values: [id],
text: 'You won\'t be able to recover the file!', cb (failed) {
icon: 'warning', // Remove from remembered checkboxes if necessary
dangerMode: true, if (!failed.length && page.selected[page.currentView].includes(id))
buttons: { page.selected[page.currentView].splice(page.selected[page.currentView].indexOf(id), 1)
cancel: true,
confirm: {
text: 'Yes, delete it!',
closeModal: false
}
}
}).then(function (proceed) {
if (!proceed) return
axios.post('api/upload/delete', { id }).then(function (response) { // Update local storage
if (!response) return if (page.selected[page.currentView].length)
localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(page.selected[page.currentView])
if (response.data.success === false) else
if (response.data.description === 'No token provided') { delete localStorage[lsKeys.selected[page.currentView]]
return page.verifyToken(page.token)
} else {
return swal('An error occurred!', response.data.description, 'error')
}
swal('Deleted!', 'The file has been deleted.', 'success')
// Reload upload list
const views = Object.assign({}, page.views[page.currentView]) const views = Object.assign({}, page.views[page.currentView])
views.autoPage = true views.autoPage = true
page.getUploads(views) page.getUploads(views)
}).catch(function (error) { }
console.error(error)
return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error')
})
}) })
} }
page.deleteSelectedFiles = function () { page.bulkDeleteUploads = function () {
const count = page.selected[page.currentView].length const count = page.selected[page.currentView].length
if (!count) if (!count)
return swal('An error occurred!', 'You have not selected any uploads.', 'error') return swal('An error occurred!', 'You have not selected any uploads.', 'error')
const suffix = `upload${count === 1 ? '' : 's'}` page.postBulkDeleteUploads({
let text = `You won't be able to recover ${count} ${suffix}!` field: 'id',
values: page.selected[page.currentView],
cb (failed) {
// Update state of checkboxes
if (failed.length)
page.selected[page.currentView] = page.selected[page.currentView]
.filter(function (id) {
return failed.includes(id)
})
else
page.selected[page.currentView] = []
// Update local storage
if (page.selected[page.currentView].length)
localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(page.selected[page.currentView])
else
delete localStorage[lsKeys.selected[page.currentView]]
// Reload uploads list
const views = Object.assign({}, page.views[page.currentView])
views.autoPage = true
page.getUploads(views)
}
})
}
page.deleteUploadsByNames = function () {
let appendix = ''
if (page.permissions.moderator)
appendix = '<br>As a staff, you can use this feature to delete uploads from other users.'
page.dom.innerHTML = `
<form class="prevent-default">
<div class="field">
<label class="label">Upload names:</label>
<div class="control">
<textarea id="bulkDeleteNames" class="textarea"></textarea>
</div>
<p class="help">Separate each entry with a new line.${appendix}</p>
</div>
<div class="field">
<div class="control">
<button type="submit" id="submitBulkDelete" class="button is-danger is-fullwidth">
<span class="icon">
<i class="icon-trash"></i>
</span>
<span>Bulk delete</span>
</button>
</div>
</div>
</form>
`
page.fadeAndScroll()
document.querySelector('#submitBulkDelete').addEventListener('click', function () {
const textArea = document.querySelector('#bulkDeleteNames')
// Clean up
const seen = {}
const names = textArea.value
.split(/\r?\n/)
.map(function (name) {
const trimmed = name.trim()
return /^[^\s]+$/.test(trimmed)
? trimmed
: ''
})
.filter(function (name) {
// Filter out invalid and duplicate names
return (!name || Object.prototype.hasOwnProperty.call(seen, name))
? false
: (seen[name] = true)
})
// Update textarea with cleaned names
textArea.value = names.join('\n')
if (!names.length)
return swal('An error occurred!', 'You have not entered any upload names.', 'error')
page.postBulkDeleteUploads({
field: 'name',
values: names,
cb (failed) {
textArea.value = failed.join('\n')
}
})
})
}
page.postBulkDeleteUploads = function ({ field, values, cb } = {}) {
const count = values.length
const objective = `${values.length} upload${count === 1 ? '' : 's'}`
let text = `You won't be able to recover ${objective}!`
if (page.currentView === 'uploadsAll') if (page.currentView === 'uploadsAll')
text += '\nBe aware, you may be nuking uploads by other users!' text += '\nBe aware, you may be nuking uploads by other users!'
@ -925,41 +1002,32 @@ page.deleteSelectedFiles = function () {
buttons: { buttons: {
cancel: true, cancel: true,
confirm: { confirm: {
text: `Yes, nuke the ${suffix}!`, text: `Yes, nuke ${values.length === 1 ? 'it' : 'them'}!`,
closeModal: false closeModal: false
} }
} }
}).then(function (proceed) { }).then(function (proceed) {
if (!proceed) return if (!proceed) return
axios.post('api/upload/bulkdelete', { axios.post('api/upload/bulkdelete', { field, values }).then(function (response) {
field: 'id', if (!response) return
values: page.selected[page.currentView]
}).then(function (bulkdelete) {
if (!bulkdelete) return
if (bulkdelete.data.success === false) if (response.data.success === false)
if (bulkdelete.data.description === 'No token provided') { if (response.data.description === 'No token provided') {
return page.verifyToken(page.token) return page.verifyToken(page.token)
} else { } else {
return swal('An error occurred!', bulkdelete.data.description, 'error') return swal('An error occurred!', response.data.description, 'error')
} }
if (Array.isArray(bulkdelete.data.failed) && bulkdelete.data.failed.length) { const failed = Array.isArray(response.data.failed) ? response.data.failed : []
page.selected[page.currentView] = page.selected[page.currentView].filter(function (id) { if (failed.length === values.length)
return bulkdelete.data.failed.includes(id) swal('An error occurred!', `Unable to delete any of the ${objective}.`, 'error')
}) else if (failed.length && failed.length < values.length)
localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(page.selected[page.currentView]) swal('Warning!', `From ${objective}, unable to delete ${failed.length} of them.`, 'warning')
swal('An error ocurrred!', `From ${count} ${suffix}, unable to delete ${bulkdelete.data.failed.length} of them.`, 'error') else
} else { swal('Deleted!', `${objective} ${count === 1 ? 'has' : 'have'} been deleted.`, 'success')
page.selected[page.currentView] = []
delete localStorage[lsKeys.selected[page.currentView]]
swal('Deleted!', `${count} ${suffix} ${count === 1 ? 'has' : 'have'} been deleted.`, 'success')
}
const views = Object.assign({}, page.views[page.currentView]) if (typeof cb === 'function') cb(failed)
views.autoPage = true
page.getUploads(views)
}).catch(function (error) { }).catch(function (error) {
console.error(error) console.error(error)
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')
@ -967,94 +1035,7 @@ page.deleteSelectedFiles = function () {
}) })
} }
page.deleteByNames = function () { page.addSelectedUploadsToAlbum = function () {
let appendix = ''
if (page.permissions.moderator)
appendix = '<br>As a staff, you can use this feature to delete uploads by other users.'
page.dom.innerHTML = `
<h2 class="subtitle">Delete by names</h2>
<div class="field">
<label class="label">File names:</label>
<div class="control">
<textarea id="names" class="textarea"></textarea>
</div>
<p class="help">Separate each entry with a new line.${appendix}</p>
</div>
<div class="field">
<div class="control">
<a class="button is-danger is-fullwidth" data-action="delete-file-by-names">
<span class="icon">
<i class="icon-trash"></i>
</span>
<span>Bulk delete</span>
</a>
</div>
</div>
`
page.fadeAndScroll()
}
page.deleteFileByNames = function () {
const names = document.querySelector('#names').value
.split(/\r?\n/)
.filter(function (n) {
return n.trim().length
})
const count = names.length
if (!count)
return swal('An error occurred!', 'You have not entered any file names.', 'error')
const suffix = `file${count === 1 ? '' : 's'}`
swal({
title: 'Are you sure?',
text: `You won't be able to recover ${count} ${suffix}!`,
icon: 'warning',
dangerMode: true,
buttons: {
cancel: true,
confirm: {
text: `Yes, nuke the ${suffix}!`,
closeModal: false
}
}
}).then(function (proceed) {
if (!proceed) return
axios.post('api/upload/bulkdelete', {
field: 'name',
values: names
}).then(function (bulkdelete) {
if (!bulkdelete) return
if (bulkdelete.data.success === false)
if (bulkdelete.data.description === 'No token provided') {
return page.verifyToken(page.token)
} else {
return swal('An error occurred!', bulkdelete.data.description, 'error')
}
if (Array.isArray(bulkdelete.data.failed) && bulkdelete.data.failed.length) {
page.selected[page.currentView] = page.selected[page.currentView].filter(function (id) {
return bulkdelete.data.failed.includes(id)
})
localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(page.selected[page.currentView])
swal('An error ocurrred!', `From ${count} ${suffix}, unable to delete ${bulkdelete.data.failed.length} of them.`, 'error')
} else {
page.selected[page.currentView] = []
delete localStorage[lsKeys.selected[page.currentView]]
swal('Deleted!', `${count} ${suffix} ${count === 1 ? 'has' : 'have'} been deleted.`, 'success')
}
document.querySelector('#names').value = bulkdelete.data.failed.join('\n')
}).catch(function (error) {
console.error(error)
swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error')
})
})
}
page.addSelectedFilesToAlbum = function () {
if (page.currentView !== 'uploads') if (page.currentView !== 'uploads')
return return
@ -1062,7 +1043,7 @@ page.addSelectedFilesToAlbum = function () {
if (!count) if (!count)
return swal('An error occurred!', 'You have not selected any uploads.', 'error') return swal('An error occurred!', 'You have not selected any uploads.', 'error')
page.addFilesToAlbum(page.selected[page.currentView], function (failed) { page.addUploadsToAlbum(page.selected[page.currentView], function (failed) {
if (!failed) return if (!failed) return
if (failed.length) if (failed.length)
page.selected[page.currentView] = page.selected[page.currentView].filter(function (id) { page.selected[page.currentView] = page.selected[page.currentView].filter(function (id) {
@ -1076,21 +1057,21 @@ page.addSelectedFilesToAlbum = function () {
}) })
} }
page.addSingleFileToAlbum = function (id) { page.addToAlbum = function (id) {
page.addFilesToAlbum([id], function (failed) { page.addUploadsToAlbum([id], function (failed) {
if (!failed) return if (!failed) return
page.getUploads(page.views[page.currentView]) page.getUploads(page.views[page.currentView])
}) })
} }
page.addFilesToAlbum = function (ids, callback) { page.addUploadsToAlbum = function (ids, callback) {
const count = ids.length const count = ids.length
const content = document.createElement('div') const content = document.createElement('div')
content.innerHTML = ` content.innerHTML = `
<div class="field has-text-centered"> <div class="field has-text-centered">
<p>You are about to add <b>${count}</b> file${count === 1 ? '' : 's'} to an album.</p> <p>You are about to add <b>${count}</b> upload${count === 1 ? '' : 's'} to an album.</p>
<p><b>If a file is already in an album, it will be moved.</b></p> <p><b>If an upload is already in an album, it will be moved.</b></p>
</div> </div>
<div class="field"> <div class="field">
<div class="control"> <div class="control">
@ -1140,7 +1121,7 @@ page.addFilesToAlbum = function (ids, callback) {
if (add.data.failed && add.data.failed.length) if (add.data.failed && add.data.failed.length)
added -= add.data.failed.length added -= add.data.failed.length
const suffix = `file${ids.length === 1 ? '' : 's'}` const suffix = `upload${ids.length === 1 ? '' : 's'}`
if (!added) if (!added)
return swal('An error occurred!', `Could not add the ${suffix} to the album.`, 'error') return swal('An error occurred!', `Could not add the ${suffix} to the album.`, 'error')
@ -1469,9 +1450,17 @@ page.getAlbumsSidebar = function () {
} }
const albumsContainer = document.querySelector('#albumsContainer') const albumsContainer = document.querySelector('#albumsContainer')
albumsContainer.innerHTML = ''
if (response.data.albums === undefined) return // Clear albums sidebar if necessary
const oldAlbums = albumsContainer.querySelectorAll('li > a')
if (oldAlbums.length) {
for (let i = 0; i < oldAlbums.length; i++)
page.menus.splice(page.menus.indexOf(oldAlbums[i]), 1)
albumsContainer.innerHTML = ''
}
if (response.data.albums === undefined)
return
for (let i = 0; i < response.data.albums.length; i++) { for (let i = 0; i < response.data.albums.length; i++) {
const album = response.data.albums[i] const album = response.data.albums[i]
@ -1481,8 +1470,10 @@ page.getAlbumsSidebar = function () {
a.innerHTML = album.name a.innerHTML = album.name
a.addEventListener('click', function () { a.addEventListener('click', function () {
page.getAlbum(this) page.getUploads({ album: this.id })
page.setActiveMenu(this)
}) })
page.menus.push(a)
li.appendChild(a) li.appendChild(a)
albumsContainer.appendChild(li) albumsContainer.appendChild(li)
@ -1493,11 +1484,6 @@ page.getAlbumsSidebar = function () {
}) })
} }
page.getAlbum = function (album) {
page.setActiveMenu(album)
page.getUploads({ album: album.id })
}
page.changeToken = function () { page.changeToken = function () {
axios.get('api/tokens').then(function (response) { axios.get('api/tokens').then(function (response) {
if (response.data.success === false) if (response.data.success === false)
@ -1508,7 +1494,6 @@ page.changeToken = function () {
} }
page.dom.innerHTML = ` page.dom.innerHTML = `
<h2 class="subtitle">Manage your token</h2>
<div class="field"> <div class="field">
<label class="label">Your current token:</label> <label class="label">Your current token:</label>
<div class="field"> <div class="field">
@ -1567,7 +1552,6 @@ page.getNewToken = function (element) {
page.changePassword = function () { page.changePassword = function () {
page.dom.innerHTML = ` page.dom.innerHTML = `
<h2 class="subtitle">Change your password</h2>
<form class="prevent-default"> <form class="prevent-default">
<div class="field"> <div class="field">
<label class="label">New password:</label> <label class="label">New password:</label>
@ -1634,13 +1618,11 @@ page.sendNewPassword = function (pass, element) {
}) })
} }
page.setActiveMenu = function (activeItem) { page.setActiveMenu = function (element) {
const menu = document.querySelector('#menu') for (let i = 0; i < page.menus.length; i++)
const items = menu.getElementsByTagName('a') page.menus[i].classList.remove('is-active')
for (let i = 0; i < items.length; i++)
items[i].classList.remove('is-active')
activeItem.classList.add('is-active') element.classList.add('is-active')
} }
page.getUsers = function ({ pageNum } = {}, element) { page.getUsers = function ({ pageNum } = {}, element) {
@ -1717,7 +1699,8 @@ page.getUsers = function ({ pageNum } = {}, element) {
</div> </div>
` `
let allSelected = true // Whether there are any unselected items
let unselected = false
page.dom.innerHTML = ` page.dom.innerHTML = `
${pagination} ${pagination}
@ -1727,7 +1710,7 @@ page.getUsers = function ({ pageNum } = {}, element) {
<table class="table is-narrow is-fullwidth is-hoverable"> <table class="table is-narrow is-fullwidth is-hoverable">
<thead> <thead>
<tr> <tr>
<th><input id="selectAll" class="checkbox" type="checkbox" title="Select all users" data-action="select-all"></th> <th><input id="selectAll" class="checkbox" type="checkbox" title="Select all" data-action="select-all"></th>
<th>ID</th> <th>ID</th>
<th style="width: 20%">Username</th> <th style="width: 20%">Username</th>
<th>Uploads</th> <th>Uploads</th>
@ -1749,7 +1732,7 @@ page.getUsers = function ({ pageNum } = {}, element) {
for (let i = 0; i < response.data.users.length; i++) { for (let i = 0; i < response.data.users.length; i++) {
const user = response.data.users[i] const user = response.data.users[i]
const selected = page.selected.users.includes(user.id) const selected = page.selected.users.includes(user.id)
if (!selected && allSelected) allSelected = false if (!selected) unselected = true
let displayGroup = null let displayGroup = null
const groups = Object.keys(user.groups) const groups = Object.keys(user.groups)
@ -1770,11 +1753,11 @@ page.getUsers = function ({ pageNum } = {}, element) {
const tr = document.createElement('tr') const tr = document.createElement('tr')
tr.dataset.id = user.id tr.dataset.id = user.id
tr.innerHTML = ` tr.innerHTML = `
<td class="controls"><input type="checkbox" class="checkbox" title="Select this user" data-action="select"${selected ? ' checked' : ''}></td> <td class="controls"><input type="checkbox" class="checkbox" title="Select" data-action="select"${selected ? ' checked' : ''}></td>
<th>${user.id}</th> <th>${user.id}</th>
<th${enabled ? '' : ' class="is-linethrough"'}>${user.username}</td> <th${enabled ? '' : ' class="is-linethrough"'}>${user.username}</td>
<th>${user.uploadsCount}</th> <th>${user.uploads}</th>
<td>${page.getPrettyBytes(user.diskUsage)}</td> <td>${page.getPrettyBytes(user.usage)}</td>
<td>${displayGroup}</td> <td>${displayGroup}</td>
<td class="controls" style="text-align: right"> <td class="controls" style="text-align: right">
<a class="button is-small is-primary" title="Edit user" data-action="edit-user"> <a class="button is-small is-primary" title="Edit user" data-action="edit-user">
@ -1803,13 +1786,15 @@ page.getUsers = function ({ pageNum } = {}, element) {
table.appendChild(tr) table.appendChild(tr)
page.checkboxes.users = Array.from(table.querySelectorAll('.checkbox[data-action="select"]')) page.checkboxes.users = Array.from(table.querySelectorAll('.checkbox[data-action="select"]'))
} }
page.fadeAndScroll()
if (allSelected && response.data.users.length) { const selectAll = document.querySelector('#selectAll')
const selectAll = document.querySelector('#selectAll') if (selectAll && !unselected) {
if (selectAll) selectAll.checked = true selectAll.checked = true
selectAll.title = 'Unselect all'
} }
page.fadeAndScroll()
page.views.users.pageNum = response.data.users.length ? pageNum : 0 page.views.users.pageNum = response.data.users.length ? pageNum : 0
}).catch(function (error) { }).catch(function (error) {
if (element) page.isLoading(element, false) if (element) page.isLoading(element, false)
@ -2008,7 +1993,7 @@ page.paginate = function (totalItems, itemsPerPage, currentPage) {
` `
} }
page.getServerStats = function (element) { page.getStatistics = function (element) {
if (!page.permissions.admin) if (!page.permissions.admin)
return swal('An error occurred!', 'You can not do this!', 'error') return swal('An error occurred!', 'You can not do this!', 'error')
@ -2016,7 +2001,6 @@ page.getServerStats = function (element) {
Please wait, this may take a while\u2026 Please wait, this may take a while\u2026
<progress class="progress is-breeze" max="100" style="margin-top: 10px"></progress> <progress class="progress is-breeze" max="100" style="margin-top: 10px"></progress>
` `
page.fadeAndScroll()
const url = 'api/stats' const url = 'api/stats'
axios.get(url).then(function (response) { axios.get(url).then(function (response) {
@ -2040,20 +2024,31 @@ page.getServerStats = function (element) {
` `
else else
try { try {
const types = response.data.stats[keys[i]]._types || {}
const valKeys = Object.keys(response.data.stats[keys[i]]) const valKeys = Object.keys(response.data.stats[keys[i]])
for (let j = 0; j < valKeys.length; j++) { for (let j = 0; j < valKeys.length; j++) {
const _value = response.data.stats[keys[i]][valKeys[j]] // Skip keys that starts with an underscore
let value = _value if (/^_/.test(valKeys[j]))
if (['albums', 'users', 'uploads'].includes(keys[i])) continue
value = _value.toLocaleString()
if (['memoryUsage', 'size'].includes(valKeys[j])) const value = response.data.stats[keys[i]][valKeys[j]]
value = page.getPrettyBytes(_value) let parsed = value
if (valKeys[j] === 'systemMemory')
value = `${page.getPrettyBytes(_value.used)} / ${page.getPrettyBytes(_value.total)} (${Math.round(_value.used / _value.total * 100)}%)` // Parse values with some preset formatting
if ((types.number || []).includes(valKeys[j]))
parsed = value.toLocaleString()
if ((types.byte || []).includes(valKeys[j]))
parsed = page.getPrettyBytes(value)
if ((types.byteUsage || []).includes(valKeys[j]))
parsed = `${page.getPrettyBytes(value.used)} / ${page.getPrettyBytes(value.total)} (${Math.round(value.used / value.total * 100)}%)`
const string = valKeys[j]
.replace(/([A-Z])/g, ' $1')
.toUpperCase()
rows += ` rows += `
<tr> <tr>
<th>${valKeys[j].replace(/([A-Z])/g, ' $1').toUpperCase()}</th> <th>${string}</th>
<td>${value}</td> <td>${parsed}</td>
</tr> </tr>
` `
} }
@ -2084,11 +2079,16 @@ page.getServerStats = function (element) {
` `
} }
page.dom.innerHTML = ` page.dom.innerHTML = content
<h2 class="subtitle">Statistics</h2>
${content}
`
page.fadeAndScroll() page.fadeAndScroll()
}).catch(function (error) {
console.error(error)
const description = error.response.data && error.response.data.description
? error.response.data && error.response.data.description
: 'There was an error with the request, please check the console for more information.'
page.dom.innerHTML = `<p>${description}</p>`
page.fadeAndScroll()
return swal('An error occurred!', description, 'error')
}) })
} }

View File

@ -55,11 +55,12 @@ page.checkIfPublic = function () {
page.preparePage() page.preparePage()
}).catch(function (error) { }).catch(function (error) {
console.error(error) console.error(error)
document.querySelector('#albumDiv').style.display = 'none' document.querySelector('#albumDiv').classList.add('is-hidden')
document.querySelector('#tabs').style.display = 'none' document.querySelector('#tabs').classList.add('is-hidden')
const button = document.querySelector('#loginToUpload') const button = document.querySelector('#loginToUpload')
button.classList.remove('is-loading')
button.innerText = 'Error occurred. Reload the page?' button.innerText = 'Error occurred. Reload the page?'
button.classList.remove('is-loading')
button.classList.remove('is-hidden')
return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error')
}) })
} }
@ -74,9 +75,9 @@ page.preparePage = function () {
button.classList.remove('is-loading') button.classList.remove('is-loading')
if (page.enableUserAccounts) if (page.enableUserAccounts)
button.innerText = 'Anonymous upload is disabled. Log in to page.' button.innerText = 'Anonymous upload is disabled. Log in to upload.'
else else
button.innerText = 'Running in private mode. Log in to page.' button.innerText = 'Running in private mode. Log in to upload.'
} }
else else
return page.prepareUpload() return page.prepareUpload()
@ -120,13 +121,13 @@ page.prepareUpload = function () {
page.prepareAlbums() page.prepareAlbums()
// Display the album selection // Display the album selection
document.querySelector('#albumDiv').style.display = 'flex' document.querySelector('#albumDiv').classList.remove('is-hidden')
} }
page.prepareUploadConfig() page.prepareUploadConfig()
document.querySelector('#maxSize').innerHTML = `Maximum upload size per file is ${page.getPrettyBytes(page.maxSizeBytes)}` document.querySelector('#maxSize').innerHTML = `Maximum upload size per file is ${page.getPrettyBytes(page.maxSizeBytes)}`
document.querySelector('#loginToUpload').style.display = 'none' document.querySelector('#loginToUpload').classList.add('is-hidden')
if (!page.token && page.enableUserAccounts) if (!page.token && page.enableUserAccounts)
document.querySelector('#loginLinkText').innerHTML = 'Create an account and keep track of your uploads' document.querySelector('#loginLinkText').innerHTML = 'Create an account and keep track of your uploads'
@ -158,7 +159,7 @@ page.prepareUpload = function () {
page.setActiveTab(this.dataset.id) page.setActiveTab(this.dataset.id)
}) })
page.setActiveTab('tab-files') page.setActiveTab('tab-files')
tabs.style.display = 'flex' tabs.classList.remove('is-hidden')
} }
page.prepareAlbums = function () { page.prepareAlbums = function () {
@ -203,10 +204,10 @@ page.setActiveTab = function (tabId) {
const id = page.tabs[i].dataset.id const id = page.tabs[i].dataset.id
if (id === tabId) { if (id === tabId) {
page.tabs[i].classList.add('is-active') page.tabs[i].classList.add('is-active')
document.querySelector(`#${id}`).style.display = 'block' document.querySelector(`#${id}`).classList.remove('is-hidden')
} else { } else {
page.tabs[i].classList.remove('is-active') page.tabs[i].classList.remove('is-active')
document.querySelector(`#${id}`).style.display = 'none' document.querySelector(`#${id}`).classList.add('is-hidden')
} }
} }
page.activeTab = tabId page.activeTab = tabId
@ -268,7 +269,7 @@ page.prepareDropzone = function () {
} }
} }
}).then(function (response) { }).then(function (response) {
file.previewElement.querySelector('.progress').style.display = 'none' file.previewElement.querySelector('.progress').classList.add('is-hidden')
if (response.data.success === false) if (response.data.success === false)
file.previewElement.querySelector('.error').innerHTML = response.data.description file.previewElement.querySelector('.error').innerHTML = response.data.description
@ -285,7 +286,7 @@ page.prepareDropzone = function () {
// Set active tab to file uploads // Set active tab to file uploads
page.setActiveTab('tab-files') page.setActiveTab('tab-files')
// Add file entry // Add file entry
tabDiv.querySelector('.uploads').style.display = 'block' tabDiv.querySelector('.uploads').classList.remove('is-hidden')
file.previewElement.querySelector('.name').innerHTML = file.name file.previewElement.querySelector('.name').innerHTML = file.name
}) })
@ -308,7 +309,7 @@ page.prepareDropzone = function () {
page.dropzone.on('success', function (file, response) { page.dropzone.on('success', function (file, response) {
if (!response) return if (!response) return
file.previewElement.querySelector('.progress').style.display = 'none' file.previewElement.querySelector('.progress').classList.add('is-hidden')
if (response.success === false) if (response.success === false)
file.previewElement.querySelector('.error').innerHTML = response.description file.previewElement.querySelector('.error').innerHTML = response.description
@ -324,7 +325,7 @@ page.prepareDropzone = function () {
error = `File too large (${page.getPrettyBytes(file.size)}).` error = `File too large (${page.getPrettyBytes(file.size)}).`
page.updateTemplateIcon(file.previewElement, 'icon-block') page.updateTemplateIcon(file.previewElement, 'icon-block')
file.previewElement.querySelector('.progress').style.display = 'none' file.previewElement.querySelector('.progress').classList.add('is-hidden')
file.previewElement.querySelector('.name').innerHTML = file.name file.previewElement.querySelector('.name').innerHTML = file.name
file.previewElement.querySelector('.error').innerHTML = error.description || error file.previewElement.querySelector('.error').innerHTML = error.description || error
}) })
@ -362,7 +363,7 @@ page.uploadUrls = function (button) {
// eslint-disable-next-line prefer-promise-reject-errors // eslint-disable-next-line prefer-promise-reject-errors
return done('You have not entered any URLs.') return done('You have not entered any URLs.')
tabDiv.querySelector('.uploads').style.display = 'block' tabDiv.querySelector('.uploads').classList.remove('is-hidden')
const files = urls.map(function (url) { const files = urls.map(function (url) {
const previewTemplate = document.createElement('template') const previewTemplate = document.createElement('template')
previewTemplate.innerHTML = page.previewTemplate.trim() previewTemplate.innerHTML = page.previewTemplate.trim()
@ -377,7 +378,7 @@ page.uploadUrls = function (button) {
return done() return done()
function posted (result) { function posted (result) {
files[i].previewElement.querySelector('.progress').style.display = 'none' files[i].previewElement.querySelector('.progress').classList.add('is-hidden')
if (result.success) { if (result.success) {
page.updateTemplate(files[i], result.files[0]) page.updateTemplate(files[i], result.files[0])
} else { } else {
@ -408,7 +409,7 @@ page.updateTemplateIcon = function (templateElement, iconClass) {
const iconElement = templateElement.querySelector('.icon') const iconElement = templateElement.querySelector('.icon')
if (!iconElement) return if (!iconElement) return
iconElement.classList.add(iconClass) iconElement.classList.add(iconClass)
iconElement.style.display = '' iconElement.classList.remove('is-hidden')
} }
page.updateTemplate = function (file, response) { page.updateTemplate = function (file, response) {
@ -417,19 +418,19 @@ page.updateTemplate = function (file, response) {
const a = file.previewElement.querySelector('.link > a') const a = file.previewElement.querySelector('.link > a')
const clipboard = file.previewElement.querySelector('.clipboard-mobile > .clipboard-js') const clipboard = file.previewElement.querySelector('.clipboard-mobile > .clipboard-js')
a.href = a.innerHTML = clipboard.dataset.clipboardText = response.url a.href = a.innerHTML = clipboard.dataset.clipboardText = response.url
clipboard.parentElement.style.display = 'block' clipboard.parentElement.classList.remove('is-hidden')
const exec = /.[\w]+(\?|$)/.exec(response.url) const exec = /.[\w]+(\?|$)/.exec(response.url)
if (exec && exec[0] && page.imageExtensions.includes(exec[0].toLowerCase())) { if (exec && exec[0] && page.imageExtensions.includes(exec[0].toLowerCase())) {
const img = file.previewElement.querySelector('img') const img = file.previewElement.querySelector('img')
img.setAttribute('alt', response.name || '') img.setAttribute('alt', response.name || '')
img.dataset.src = response.url img.dataset.src = response.url
img.style.display = '' img.classList.remove('is-hidden')
img.onerror = function () { img.onerror = function () {
// Hide image elements that fail to load // Hide image elements that fail to load
// Consequently include WEBP in browsers that do not have WEBP support (Firefox/IE) // Consequently include WEBP in browsers that do not have WEBP support (Firefox/IE)
this.style.display = 'none' this.classList.add('is-hidden')
file.previewElement.querySelector('.icon').style.display = '' file.previewElement.querySelector('.icon').classList.remove('is-hidden')
} }
page.lazyLoad.update(file.previewElement.querySelectorAll('img')) page.lazyLoad.update(file.previewElement.querySelectorAll('img'))
} else { } else {
@ -439,7 +440,7 @@ page.updateTemplate = function (file, response) {
if (response.expirydate) { if (response.expirydate) {
const expiryDate = file.previewElement.querySelector('.expiry-date') const expiryDate = file.previewElement.querySelector('.expiry-date')
expiryDate.innerHTML = `Expiry date: ${page.getPrettyDate(new Date(response.expirydate * 1000))}` expiryDate.innerHTML = `Expiry date: ${page.getPrettyDate(new Date(response.expirydate * 1000))}`
expiryDate.style.display = 'block' expiryDate.classList.remove('is-hidden')
} }
} }
@ -572,7 +573,7 @@ page.prepareUploadConfig = function () {
page.fileLength = stored page.fileLength = stored
} }
fileLengthDiv.style.display = 'block' fileLengthDiv.classList.remove('is-hidden')
fileLengthDiv.querySelector('.help').innerHTML = helpText fileLengthDiv.querySelector('.help').innerHTML = helpText
} }
@ -597,7 +598,7 @@ page.prepareUploadConfig = function () {
page.uploadAge = stored page.uploadAge = stored
} }
} }
uploadAgeDiv.style.display = 'block' uploadAgeDiv.classList.remove('is-hidden')
} }
const tabContent = document.querySelector('#tab-config') const tabContent = document.querySelector('#tab-config')
@ -664,8 +665,9 @@ window.addEventListener('paste', function (event) {
const item = items[index[i]] const item = items[index[i]]
if (item.kind === 'file') { if (item.kind === 'file') {
const blob = item.getAsFile() const blob = item.getAsFile()
const file = new File([blob], `pasted-image.${blob.type.match(/(?:[^/]*\/)([^;]*)/)[1]}`) const file = new File([blob], `pasted-image.${blob.type.match(/(?:[^/]*\/)([^;]*)/)[1]}`, {
file.type = blob.type type: blob.type
})
page.dropzone.addFile(file) page.dropzone.addFile(file)
} }
} }

View File

@ -16,7 +16,7 @@
v3: CSS and JS files (libs such as bulma, lazyload, etc). v3: CSS and JS files (libs such as bulma, lazyload, etc).
v4: Renders in /public/render/* directories (to be used by render.js). v4: Renders in /public/render/* directories (to be used by render.js).
#} #}
{% set v1 = "o59f6p3y1F" %} {% set v1 = "1QupbESWeT" %}
{% set v2 = "hiboQUzAzp" %} {% set v2 = "hiboQUzAzp" %}
{% set v3 = "tWLiAlAX5i" %} {% set v3 = "tWLiAlAX5i" %}
{% set v4 = "S3TAWpPeFS" %} {% set v4 = "S3TAWpPeFS" %}

View File

@ -20,7 +20,7 @@
{% block content %} {% block content %}
{{ super() }} {{ super() }}
<section id="dashboard" class="section"> <section id="dashboard" class="section is-hidden">
<div id="panel" class="container"> <div id="panel" class="container">
<h1 class="title"> <h1 class="title">
Dashboard Dashboard
@ -42,28 +42,28 @@
<a id="itemUploads">Uploads</a> <a id="itemUploads">Uploads</a>
</li> </li>
<li> <li>
<a id="itemDeleteByNames">Delete by names</a> <a id="itemDeleteUploadsByNames">Delete uploads by names</a>
</li> </li>
</ul> </ul>
<p class="menu-label">Albums</p> <p class="menu-label">Albums</p>
<ul class="menu-list"> <ul class="menu-list">
<li> <li>
<a id="itemManageGallery">Manage your albums</a> <a id="itemManageAlbums">Manage your albums</a>
</li> </li>
<li> <li>
<ul id="albumsContainer"></ul> <ul id="albumsContainer"></ul>
</li> </li>
</ul> </ul>
<p id="itemLabelAdmin" class="menu-label" style="display: none">Administration</p> <p id="itemLabelAdmin" class="menu-label is-hidden">Administration</p>
<ul id="itemListAdmin" class="menu-list" style="display: none"> <ul id="itemListAdmin" class="menu-list is-hidden">
<li> <li>
<a id="itemServerStats">Statistics</a> <a id="itemStatistics" class="is-hidden">Statistics</a>
</li> </li>
<li> <li>
<a id="itemManageUploads">Manage uploads</a> <a id="itemManageUploads" class="is-hidden">Manage uploads</a>
</li> </li>
<li> <li>
<a id="itemManageUsers">Manage users</a> <a id="itemManageUsers" class="is-hidden">Manage users</a>
</li> </li>
</ul> </ul>
<p class="menu-label">Configuration</p> <p class="menu-label">Configuration</p>
@ -72,10 +72,10 @@
<a id="ShareX">ShareX user profile</a> <a id="ShareX">ShareX user profile</a>
</li> </li>
<li> <li>
<a id="itemTokens">Manage your token</a> <a id="itemManageToken">Manage your token</a>
</li> </li>
<li> <li>
<a id="itemPassword">Change your password</a> <a id="itemChangePassword">Change your password</a>
</li> </li>
<li> <li>
<a id="itemLogout">Logout</a> <a id="itemLogout">Logout</a>

View File

@ -43,8 +43,8 @@
<div class="columns is-gapless"> <div class="columns is-gapless">
<div class="column is-hidden-mobile"></div> <div class="column is-hidden-mobile"></div>
<div class="column"> <div class="column">
<a id="loginToUpload" class="button is-danger is-loading" style="display: flex"></a> <button id="loginToUpload" class="button is-danger is-fullwidth is-loading"></button>
<div id="albumDiv" class="field has-addons" style="display: none"> <div id="albumDiv" class="field has-addons is-hidden">
<div class="control is-expanded"> <div class="control is-expanded">
<div class="select is-fullwidth"> <div class="select is-fullwidth">
<select id="albumSelect"></select> <select id="albumSelect"></select>
@ -56,7 +56,7 @@
</a> </a>
</div> </div>
</div> </div>
<div id="tabs" class="tabs is-centered is-boxed" style="display: none"> <div id="tabs" class="tabs is-centered is-boxed is-hidden">
<ul> <ul>
<li data-id="tab-files" class="is-active"> <li data-id="tab-files" class="is-active">
<a> <a>
@ -80,12 +80,12 @@
</li> </li>
</ul> </ul>
</div> </div>
<div id="tab-files" class="tab-content" style="display: none"> <div id="tab-files" class="tab-content is-hidden">
<div class="field dz-container"></div> <div class="field dz-container"></div>
<div class="field uploads"></div> <div class="field uploads"></div>
</div> </div>
{% if urlMaxSize -%} {% if urlMaxSize -%}
<div id="tab-urls" class="tab-content" style="display: none"> <div id="tab-urls" class="tab-content is-hidden">
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<textarea id="urls" class="textarea" rows="2"></textarea> <textarea id="urls" class="textarea" rows="2"></textarea>
@ -119,7 +119,7 @@
<div class="field uploads"></div> <div class="field uploads"></div>
</div> </div>
{%- endif %} {%- endif %}
<div id="tab-config" class="tab-content" style="display: none"> <div id="tab-config" class="tab-content is-hidden">
<form> <form>
<div class="field"> <div class="field">
<label class="label">File size display</label> <label class="label">File size display</label>
@ -133,7 +133,7 @@
</div> </div>
<p class="help">This will be used in our homepage, dashboard, and album public pages.</p> <p class="help">This will be used in our homepage, dashboard, and album public pages.</p>
</div> </div>
<div id="fileLengthDiv" class="field" style="display: none"> <div id="fileLengthDiv" class="field is-hidden">
<label class="label">File identifier length</label> <label class="label">File identifier length</label>
<div class="control is-expanded"> <div class="control is-expanded">
<input id="fileLength" class="input is-fullwidth" type="number" min="0"> <input id="fileLength" class="input is-fullwidth" type="number" min="0">
@ -141,7 +141,7 @@
<p class="help"></p> <p class="help"></p>
</div> </div>
{%- if temporaryUploadAges %} {%- if temporaryUploadAges %}
<div id="uploadAgeDiv" class="field" style="display: none"> <div id="uploadAgeDiv" class="field is-hidden">
<label class="label">Upload age</label> <label class="label">Upload age</label>
<div class="control is-expanded"> <div class="control is-expanded">
<div class="select is-fullwidth"> <div class="select is-fullwidth">
@ -185,18 +185,18 @@
<div class="column is-hidden-mobile"></div> <div class="column is-hidden-mobile"></div>
</div> </div>
<div id="tpl" style="display: none"> <div id="tpl" class="is-hidden">
<div class="field"> <div class="field">
<i class="icon" style="display: none"></i> <i class="icon is-hidden"></i>
<img class="is-unselectable" style="display: none"> <img class="is-unselectable is-hidden">
<p class="name is-unselectable"></p> <p class="name is-unselectable"></p>
<progress class="progress is-small is-danger" max="100" value="0"></progress> <progress class="progress is-small is-danger" max="100" value="0"></progress>
<p class="error"></p> <p class="error"></p>
<p class="link"> <p class="link">
<a target="_blank" rel="noopener"></a> <a target="_blank" rel="noopener"></a>
</p> </p>
<p class="help expiry-date" style="display: none"></p> <p class="help expiry-date is-hidden"></p>
<p class="clipboard-mobile" style="display: none"> <p class="clipboard-mobile is-hidden">
<a class="button is-small is-info is-outlined clipboard-js" style="display: flex"> <a class="button is-small is-info is-outlined clipboard-js" style="display: flex">
<span class="icon"> <span class="icon">
<i class="icon-clipboard-1"></i> <i class="icon-clipboard-1"></i>