Manage albums admin page, and more!

Resolves #194.

Added pagination for Manage your albums page.

Albums sidebar will now only list 9 albums at most.
Use Manage your albums page to view the rest.
Albums in the list will now have View uploads button after all.

Delete album button for albums renamed to Disable album.
Since techincally the server would've always been disabling the albums
instead of deleting them.
It was something upstream dev's decided, and I haven't bothered changing
its behavior.

I'll work on actual Delete album feature some other days.

As the title says, added Manage albums admin page.

Viewing uploads of an album will hook into albumid: filter key.

I'll work on filter and bulk operations some other days.

Updated styling for disabled albums and users.
Instead of havine a line through them, they will be greyed out.
Disable public page of albums will still use line through however.

Links to album's disabled public page are now clickable.

Added a new button styling is-dangerish.
It'll be orange.

Renamed /api/albums/delete to /api/albums/disable.
For backwards compatibility, /api/albums/delete will still work
but automatically re-routed to /api/albums/disable.

/api/uploads/list will no longer print SQLite errors for moderators
or higher when encountering them.
It was originally used to inform moderators of non-existing colum names
when used for sorting.
But on one of the recent commits, I had added a check for allowed colum
names.

Improved some caching in dashboard page.

Added new entries to cookie policy.

Some other small things.

Bumped v1 version string and rebuilt client assets.
This commit is contained in:
Bobby Wibowo 2020-06-01 11:44:16 +07:00
parent 255a5992b5
commit 5e5d5c5647
No known key found for this signature in database
GPG Key ID: 51C3A1E1E22D26CF
13 changed files with 544 additions and 181 deletions

View File

@ -4,6 +4,7 @@ const path = require('path')
const randomstring = require('randomstring') const randomstring = require('randomstring')
const Zip = require('jszip') const Zip = require('jszip')
const paths = require('./pathsController') const paths = require('./pathsController')
const perms = require('./permissionController')
const utils = require('./utilsController') const utils = require('./utilsController')
const config = require('./../config') const config = require('./../config')
const logger = require('./../logger') const logger = require('./../logger')
@ -76,39 +77,102 @@ self.list = async (req, res, next) => {
const user = await utils.authorize(req, res) const user = await utils.authorize(req, res)
if (!user) return if (!user) return
let fields = ['id', 'name'] const all = req.headers.all === '1'
if (req.params.sidebar === undefined) const sidebar = req.headers.sidebar
fields = fields.concat(['timestamp', 'identifier', 'editedAt', 'download', 'public', 'description']) const ismoderator = perms.is(user, 'moderator')
if (all && !ismoderator)
return res.status(403).end()
const albums = await db.table('albums') const filter = function () {
.select(fields) if (!all)
.where({ this.where({
enabled: 1, enabled: 1,
userid: user.id userid: user.id
}) })
if (req.params.sidebar !== undefined)
return res.json({ success: true, albums })
const albumids = {}
for (const album of albums) {
album.download = album.download !== 0
album.public = album.public !== 0
album.files = 0
// Map by IDs
albumids[album.id] = album
} }
const files = await db.table('files') try {
.whereIn('albumid', Object.keys(albumids)) // Query albums count for pagination
.select('albumid') const count = await db.table('albums')
.where(filter)
.count('id as count')
.then(rows => rows[0].count)
if (!count)
return res.json({ success: true, albums: [], count })
// Increment files count let fields = ['id', 'name']
for (const file of files)
if (albumids[file.albumid])
albumids[file.albumid].files++
return res.json({ success: true, albums, homeDomain }) let albums
if (sidebar) {
albums = await db.table('albums')
.where(filter)
.limit(9)
.select(fields)
return res.json({ success: true, albums, count })
} else {
let offset = Number(req.params.page)
if (isNaN(offset)) offset = 0
else if (offset < 0) offset = Math.max(0, Math.ceil(count / 25) + offset)
fields = fields.concat(['identifier', 'enabled', 'timestamp', 'editedAt', 'download', 'public', 'description'])
if (all)
fields.push('userid')
albums = await db.table('albums')
.where(filter)
.limit(25)
.offset(25 * offset)
.select(fields)
}
const albumids = {}
for (const album of albums) {
album.download = album.download !== 0
album.public = album.public !== 0
album.uploads = 0
// Map by IDs
albumids[album.id] = album
}
const uploads = await db.table('files')
.whereIn('albumid', Object.keys(albumids))
.select('albumid')
for (const upload of uploads)
if (albumids[upload.albumid])
albumids[upload.albumid].uploads++
// If we are not listing all albums, send response
if (!all)
return res.json({ success: true, albums, count, homeDomain })
// Otherwise proceed to querying usernames
const userids = albums
.map(album => album.userid)
.filter((v, i, a) => {
return v !== null && v !== undefined && v !== '' && a.indexOf(v) === i
})
// If there are no albums attached to a registered user, send response
if (userids.length === 0)
return res.json({ success: true, albums, count, homeDomain })
// Query usernames of user IDs from currently selected files
const usersTable = await db.table('users')
.whereIn('id', userids)
.select('id', 'username')
const users = {}
for (const user of usersTable)
users[user.id] = user.username
return res.json({ success: true, albums, count, users, homeDomain })
} catch (error) {
logger.error(error)
return res.status(500).json({ success: false, description: 'An unexpected error occurred. Try again?' })
}
} }
self.create = async (req, res, next) => { self.create = async (req, res, next) => {
@ -161,6 +225,11 @@ self.create = async (req, res, next) => {
} }
self.delete = async (req, res, next) => { self.delete = async (req, res, next) => {
// Map /delete requests to /disable route
return self.disable(req, res, next)
}
self.disable = async (req, res, next) => {
const user = await utils.authorize(req, res) const user = await utils.authorize(req, res)
if (!user) return if (!user) return
@ -218,6 +287,8 @@ self.edit = async (req, res, next) => {
const user = await utils.authorize(req, res) const user = await utils.authorize(req, res)
if (!user) return if (!user) return
const ismoderator = perms.is(user, 'moderator')
const id = parseInt(req.body.id) const id = parseInt(req.body.id)
if (isNaN(id)) if (isNaN(id))
return res.json({ success: false, description: 'No album specified.' }) return res.json({ success: false, description: 'No album specified.' })
@ -229,13 +300,19 @@ self.edit = async (req, res, next) => {
if (!name) if (!name)
return res.json({ success: false, description: 'No name specified.' }) return res.json({ success: false, description: 'No name specified.' })
const filter = function () {
this.where('id', id)
if (!ismoderator)
this.andWhere({
enabled: 1,
userid: user.id
})
}
try { try {
const album = await db.table('albums') const album = await db.table('albums')
.where({ .where(filter)
id,
userid: user.id,
enabled: 1
})
.first() .first()
if (!album) if (!album)
@ -256,14 +333,14 @@ self.edit = async (req, res, next) => {
: '' : ''
} }
if (ismoderator)
update.enabled = Boolean(req.body.enabled)
if (req.body.requestLink) if (req.body.requestLink)
update.identifier = await self.getUniqueRandomName() update.identifier = await self.getUniqueRandomName()
await db.table('albums') await db.table('albums')
.where({ .where(filter)
id,
userid: user.id
})
.update(update) .update(update)
utils.invalidateAlbumsCache([id]) utils.invalidateAlbumsCache([id])

View File

@ -1332,23 +1332,8 @@ self.list = async (req, res) => {
return res.json({ success: true, files, count, users, albums, basedomain }) return res.json({ success: true, files, count, users, albums, basedomain })
} catch (error) { } catch (error) {
// If moderator, capture SQLITE_ERROR and use its error message for the response's description logger.error(error)
let errorString return res.status(500).json({ success: false, description: 'An unexpected error occurred. Try again?' })
if (ismoderator && error.code === 'SQLITE_ERROR') {
const match = error.message.match(/SQLITE_ERROR: .*$/)
errorString = match && match[0]
}
// If not proper SQLITE_ERROR, log to console
if (!errorString) {
logger.error(error)
res.status(500) // Use 500 status code
}
return res.json({
success: false,
description: errorString || 'An unexpected error occurred. Try again?'
})
} }
} }

2
dist/css/style.css vendored
View File

@ -1,2 +1,2 @@
html{background-color:#000;overflow-y:auto}body{color:#eff0f1;-webkit-animation:fadeInOpacity .5s;animation:fadeInOpacity .5s}@-webkit-keyframes fadeInOpacity{0%{opacity:0}to{opacity:1}}@keyframes fadeInOpacity{0%{opacity:0}to{opacity:1}}a{color:#209cee}a:hover{color:#67c3ff}hr{background-color:#585858}.message-body code,code{background-color:#000;border-radius:5px;font-size:1rem}.subtitle,.subtitle strong{color:#bdc3c7}.subtitle.is-brighter,.subtitle.is-brighter strong,.title{color:#eff0f1}.input,.select select,.textarea{color:#eff0f1;border-color:#585858;background-color:#000}.input::-moz-placeholder,.textarea::-moz-placeholder{color:#bdc3c7}.input::-webkit-input-placeholder,.textarea::-webkit-input-placeholder{color:#bdc3c7}.input:-moz-placeholder,.textarea:-moz-placeholder{color:#bdc3c7}.input:-ms-input-placeholder,.textarea:-ms-input-placeholder{color:#bdc3c7}.input.is-active,.input.is-focused,.input:active,.input:focus,.input:not([disabled]):hover,.select fieldset:not([disabled]) select:hover,.select select:not([disabled]):hover,.textarea.is-active,.textarea.is-focused,.textarea:active,.textarea:focus,.textarea:not([disabled]):hover,fieldset:not([disabled]) .input:hover,fieldset:not([disabled]) .select select:hover,fieldset:not([disabled]) .textarea:hover{border-color:#209cee}.input[disabled],.select fieldset[disabled] select,.select select[disabled],.textarea[disabled],fieldset[disabled] .input,fieldset[disabled] .select select,fieldset[disabled] .textarea{border-color:#585858;background-color:#2f2f2f}.label{color:#eff0f1;font-weight:400}.help{color:#bdc3c7}.progress{background-color:#585858}.button.is-info.is-hovered [class*=" icon-"]:before,.button.is-info.is-hovered [class^=icon-]:before,.button.is-info:hover [class*=" icon-"]:before,.button.is-info:hover [class^=icon-]:before{fill:#fff}.button.is-wrappable{white-space:break-spaces;min-height:2.25em;height:auto}.checkbox:hover,.radio:hover{color:#7f8c8d}.select:not(.is-multiple):not(.is-loading):after,.select:not(.is-multiple):not(.is-loading):hover:after{border-color:#eff0f1}.select select[disabled]:hover,fieldset[disabled] .select select:hover{border-color:#585858}.message{background-color:#2f2f2f}.message-body{color:#eff0f1;border:0}.table{color:#bdc3c7;background-color:#000}.table.is-narrow{font-size:.75rem}.table.is-hoverable tbody tr:not(.is-selected):hover{background-color:#2f2f2f}.table td,.table th{white-space:nowrap;vertical-align:middle;border-bottom:1px solid #585858}.table th{color:#eff0f1;height:2.25em;font-weight:400}.table th.capitalize{text-transform:capitalize}.table thead td,.table thead th{color:#eff0f1;background-color:#383838;border-bottom:0;height:31px}.table tbody tr:last-child td,.table tbody tr:last-child th{border-bottom-width:1px}.table .cell-indent{padding-left:2.25em}.cc-window{font-family:inherit!important}.cc-link{padding:0!important}.section.has-extra-bottom-padding{padding-bottom:6.5rem}a.floating-home-button{display:flex;position:fixed;right:1.5rem;bottom:1.5rem;border-radius:100%;background-color:#209cee;color:#fff;width:3.5rem;height:3.5rem;justify-content:center;align-items:center;transition:background-color .25s}a.floating-home-button:hover{background-color:#67c3ff;color:#fff}a.floating-home-button>.icon{margin-top:-2px}.hero.is-fullheight>.hero-body{min-height:100vh;height:100%}.hero.is-fullheight>.hero-body>.container{width:100%} html{background-color:#000;overflow-y:auto}body{color:#eff0f1;-webkit-animation:fadeInOpacity .5s;animation:fadeInOpacity .5s}@-webkit-keyframes fadeInOpacity{0%{opacity:0}to{opacity:1}}@keyframes fadeInOpacity{0%{opacity:0}to{opacity:1}}a{color:#209cee}a:hover{color:#67c3ff}hr{background-color:#585858}.message-body code,code{background-color:#000;border-radius:5px;font-size:1rem}.subtitle,.subtitle strong{color:#bdc3c7}.subtitle.is-brighter,.subtitle.is-brighter strong,.title{color:#eff0f1}.input,.select select,.textarea{color:#eff0f1;border-color:#585858;background-color:#000}.input::-moz-placeholder,.textarea::-moz-placeholder{color:#bdc3c7}.input::-webkit-input-placeholder,.textarea::-webkit-input-placeholder{color:#bdc3c7}.input:-moz-placeholder,.textarea:-moz-placeholder{color:#bdc3c7}.input:-ms-input-placeholder,.textarea:-ms-input-placeholder{color:#bdc3c7}.input.is-active,.input.is-focused,.input:active,.input:focus,.input:not([disabled]):hover,.select fieldset:not([disabled]) select:hover,.select select:not([disabled]):hover,.textarea.is-active,.textarea.is-focused,.textarea:active,.textarea:focus,.textarea:not([disabled]):hover,fieldset:not([disabled]) .input:hover,fieldset:not([disabled]) .select select:hover,fieldset:not([disabled]) .textarea:hover{border-color:#209cee}.input[disabled],.select fieldset[disabled] select,.select select[disabled],.textarea[disabled],fieldset[disabled] .input,fieldset[disabled] .select select,fieldset[disabled] .textarea{border-color:#585858;background-color:#2f2f2f}.label{color:#eff0f1;font-weight:400}.help{color:#bdc3c7}.progress{background-color:#585858}.button.is-info.is-hovered [class*=" icon-"]:before,.button.is-info.is-hovered [class^=icon-]:before,.button.is-info:hover [class*=" icon-"]:before,.button.is-info:hover [class^=icon-]:before{fill:#fff}.button.is-dangerish{background-color:#ff7043;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-dangerish.is-hovered,.button.is-dangerish:not([disabled]):hover{background-color:#ff8a65;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-dangerish.is-active,.button.is-dangerish:not([disabled]):active{background-color:#ff5722;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-dangerish.is-outlined{background-color:transparent;border-color:#ff7043;color:#ff7043}.button.is-dangerish.is-outlined.is-focused,.button.is-dangerish.is-outlined.is-hovered,.button.is-dangerish.is-outlined:not([disabled]):focus,.button.is-dangerish.is-outlined:not([disabled]):hover{background-color:#ff7043;border-color:#ff7043;color:rgba(0,0,0,.7)}.button.is-wrappable{white-space:break-spaces;min-height:2.25em;height:auto}.checkbox:hover,.radio:hover{color:#7f8c8d}.select:not(.is-multiple):not(.is-loading):after,.select:not(.is-multiple):not(.is-loading):hover:after{border-color:#eff0f1}.select select[disabled]:hover,fieldset[disabled] .select select:hover{border-color:#585858}.message{background-color:#2f2f2f}.message-body{color:#eff0f1;border:0}.table{color:#bdc3c7;background-color:#000}.table.is-narrow{font-size:.75rem}.table.is-hoverable tbody tr:not(.is-selected):hover{background-color:#2f2f2f}.table td,.table th{white-space:nowrap;vertical-align:middle;border-bottom:1px solid #585858}.table th{color:#eff0f1;height:2.25em;font-weight:400}.table th.capitalize{text-transform:capitalize}.table thead td,.table thead th{color:#eff0f1;background-color:#383838;border-bottom:0;height:31px}.table tbody tr:last-child td,.table tbody tr:last-child th{border-bottom-width:1px}.table .cell-indent{padding-left:2.25em}.cc-window{font-family:inherit!important}.cc-link{padding:0!important}.section.has-extra-bottom-padding{padding-bottom:6.5rem}a.floating-home-button{display:flex;position:fixed;right:1.5rem;bottom:1.5rem;border-radius:100%;background-color:#209cee;color:#fff;width:3.5rem;height:3.5rem;justify-content:center;align-items:center;transition:background-color .25s}a.floating-home-button:hover{background-color:#67c3ff;color:#fff}a.floating-home-button>.icon{margin-top:-2px}.hero.is-fullheight>.hero-body{min-height:100vh;height:100%}.hero.is-fullheight>.hero-body>.container{width:100%}
/*# sourceMappingURL=style.css.map */ /*# sourceMappingURL=style.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -35,10 +35,11 @@ routes.get('/album/zip/:identifier', (req, res, next) => albumsController.genera
routes.get('/album/:id', (req, res, next) => uploadController.list(req, res, next)) routes.get('/album/:id', (req, res, next) => uploadController.list(req, res, next))
routes.get('/album/:id/:page', (req, res, next) => uploadController.list(req, res, next)) routes.get('/album/:id/:page', (req, res, next) => uploadController.list(req, res, next))
routes.get('/albums', (req, res, next) => albumsController.list(req, res, next)) routes.get('/albums', (req, res, next) => albumsController.list(req, res, next))
routes.get('/albums/:sidebar', (req, res, next) => albumsController.list(req, res, next)) routes.get('/albums/:page', (req, res, next) => albumsController.list(req, res, next))
routes.post('/albums', (req, res, next) => albumsController.create(req, res, next)) routes.post('/albums', (req, res, next) => albumsController.create(req, res, next))
routes.post('/albums/addfiles', (req, res, next) => albumsController.addFiles(req, res, next)) routes.post('/albums/addfiles', (req, res, next) => albumsController.addFiles(req, res, next))
routes.post('/albums/delete', (req, res, next) => albumsController.delete(req, res, next)) routes.post('/albums/delete', (req, res, next) => albumsController.delete(req, res, next))
routes.post('/albums/disable', (req, res, next) => albumsController.disable(req, res, next))
routes.post('/albums/edit', (req, res, next) => albumsController.edit(req, res, next)) routes.post('/albums/edit', (req, res, next) => albumsController.edit(req, res, next))
routes.post('/albums/rename', (req, res, next) => albumsController.rename(req, res, next)) routes.post('/albums/rename', (req, res, next) => albumsController.rename(req, res, next))
routes.get('/albums/test', (req, res, next) => albumsController.test(req, res, next)) routes.get('/albums/test', (req, res, next) => albumsController.test(req, res, next))

View File

@ -141,6 +141,41 @@ fieldset[disabled] .textarea {
fill: #fff fill: #fff
} }
.button.is-dangerish {
background-color: #ff7043;
border-color: transparent;
color: rgba(0, 0, 0, 0.7)
}
.button.is-dangerish.is-hovered,
.button.is-dangerish:not([disabled]):hover {
background-color: #ff8a65;
border-color: transparent;
color: rgba(0, 0, 0, 0.7)
}
.button.is-dangerish.is-active,
.button.is-dangerish:not([disabled]):active {
background-color: #ff5722;
border-color: transparent;
color: rgba(0, 0, 0, 0.7)
}
.button.is-dangerish.is-outlined {
background-color: transparent;
border-color: #ff7043;
color: #ff7043
}
.button.is-dangerish.is-outlined.is-focused,
.button.is-dangerish.is-outlined.is-hovered,
.button.is-dangerish.is-outlined:not([disabled]):focus,
.button.is-dangerish.is-outlined:not([disabled]):hover {
background-color: #ff7043;
border-color: #ff7043;
color: rgba(0, 0, 0, 0.7)
}
.button.is-wrappable { .button.is-wrappable {
white-space: break-spaces; white-space: break-spaces;
min-height: 2.25em; min-height: 2.25em;

View File

@ -9,6 +9,8 @@ const lsKeys = {
selected: { selected: {
uploads: 'selectedUploads', uploads: 'selectedUploads',
uploadsAll: 'selectedUploadsAll', uploadsAll: 'selectedUploadsAll',
albums: 'selectedAlbums',
albumsAll: 'selectedAlbumsAll',
users: 'selectedUsers' users: 'selectedUsers'
} }
} }
@ -32,52 +34,53 @@ const page = {
currentView: null, currentView: null,
views: { views: {
// config of uploads view // params of uploads view
uploads: { uploads: {
type: localStorage[lsKeys.viewType.uploads], type: localStorage[lsKeys.viewType.uploads],
album: null, // album's id album: null, // album's id
pageNum: null // page num pageNum: null
}, },
// config of uploads view (all) // params of uploads view (all)
uploadsAll: { uploadsAll: {
type: localStorage[lsKeys.viewType.uploadsAll], type: localStorage[lsKeys.viewType.uploadsAll],
filters: null, // uploads' filters filters: null,
pageNum: null, // page num pageNum: null,
all: true all: true
}, },
// config of users view // params of albums view
albums: {
filters: null,
pageNum: null
},
// params of albums view (all)
albumsAll: {
filters: null,
pageNum: null,
all: true
},
// params of users view
users: { users: {
filters: null, // users' filters filters: null,
pageNum: null pageNum: null
} }
}, },
// id of selected items (shared across pages and will be synced with localStorage) // ids of selected items (shared across pages and will be synced with localStorage)
selected: { selected: {
uploads: [], uploads: [],
uploadsAll: [], uploadsAll: [],
albums: [],
albumsAll: [],
users: [] users: []
}, },
checkboxes: { checkboxes: [],
uploads: [], lastSelected: [],
uploadsAll: [],
users: []
},
lastSelected: {
upload: null,
uploadsAll: null,
user: null
},
// select album dom for dialogs/modals // select album dom for dialogs/modals
selectAlbumContainer: null, selectAlbumContainer: null,
// cache for dialogs/modals // cache for dialogs/modals
cache: { cache: {},
uploads: {},
albums: {},
users: {}
},
clipboardJS: null, clipboardJS: null,
lazyLoad: null, lazyLoad: null,
@ -208,11 +211,12 @@ page.prepareDashboard = () => {
const itemMenus = [ const itemMenus = [
{ selector: '#itemUploads', onclick: page.getUploads }, { selector: '#itemUploads', onclick: page.getUploads },
{ selector: '#itemDeleteUploadsByNames', onclick: page.deleteUploadsByNames }, { selector: '#itemDeleteUploadsByNames', onclick: page.deleteUploadsByNames },
{ selector: '#itemManageAlbums', onclick: page.getAlbums }, { selector: '#itemManageYourAlbums', onclick: page.getAlbums },
{ selector: '#itemManageToken', onclick: page.changeToken }, { selector: '#itemManageToken', onclick: page.changeToken },
{ selector: '#itemChangePassword', onclick: page.changePassword }, { selector: '#itemChangePassword', onclick: page.changePassword },
{ selector: '#itemLogout', onclick: page.logout }, { selector: '#itemLogout', onclick: page.logout },
{ selector: '#itemManageUploads', onclick: page.getUploads, params: { all: true }, group: 'moderator' }, { selector: '#itemManageUploads', onclick: page.getUploads, params: { all: true }, group: 'moderator' },
{ selector: '#itemManageAlbums', onclick: page.getAlbums, params: { all: true }, group: 'moderator' },
{ selector: '#itemStatistics', onclick: page.getStatistics, group: 'admin' }, { selector: '#itemStatistics', onclick: page.getStatistics, group: 'admin' },
{ selector: '#itemManageUsers', onclick: page.getUsers, group: 'admin' } { selector: '#itemManageUsers', onclick: page.getUsers, group: 'admin' }
] ]
@ -357,8 +361,10 @@ page.domClick = event => {
return page.submitAlbum(element) return page.submitAlbum(element)
case 'edit-album': case 'edit-album':
return page.editAlbum(id) return page.editAlbum(id)
case 'delete-album': case 'disable-album':
return page.deleteAlbum(id) return page.disableAlbum(id)
case 'view-album-uploads':
return page.viewAlbumUploads(id, element)
// Manage users // Manage users
case 'create-user': case 'create-user':
return page.createUser() return page.createUser()
@ -370,12 +376,6 @@ page.domClick = event => {
return page.deleteUser(id) return page.deleteUser(id)
case 'view-user-uploads': case 'view-user-uploads':
return page.viewUserUploads(id, element) return page.viewUserUploads(id, element)
/* // WIP
case 'user-filters-help':
return page.userFiltersHelp(element)
case 'filter-users':
return page.filterUsers(element)
*/
// Others // Others
case 'get-new-token': case 'get-new-token':
return page.getNewToken(element) return page.getNewToken(element)
@ -416,6 +416,30 @@ page.fadeAndScroll = disableFading => {
}) })
} }
page.getByView = (view, get) => {
switch (view) {
case 'uploads':
case 'uploadsAll':
return {
type: 'uploads',
func: page.getUploads
}[get]
case 'albums':
case 'albumsAll':
return {
type: 'albums',
func: page.getAlbums
}[get]
case 'users':
return {
type: 'users',
func: page.getUsers
}[get]
default:
return null
}
}
page.switchPage = (action, element) => { page.switchPage = (action, element) => {
if (page.isSomethingLoading) if (page.isSomethingLoading)
return page.warnSomethingLoading() return page.warnSomethingLoading()
@ -425,7 +449,7 @@ page.switchPage = (action, element) => {
trigger: element trigger: element
}) })
const func = page.currentView === 'users' ? page.getUsers : page.getUploads const func = page.getByView(page.currentView, 'func')
switch (action) { switch (action) {
case 'page-prev': case 'page-prev':
@ -508,11 +532,13 @@ page.getUploads = (params = {}) => {
} }
page.currentView = params.all ? 'uploadsAll' : 'uploads' page.currentView = params.all ? 'uploadsAll' : 'uploads'
page.cache.uploads = {} page.cache = {}
const albums = response.data.albums const albums = response.data.albums
const users = response.data.users const users = response.data.users
const basedomain = response.data.basedomain const basedomain = response.data.basedomain
if (params.pageNum < 0) params.pageNum = Math.max(0, pages + params.pageNum)
const pagination = page.paginate(response.data.count, 25, params.pageNum) const pagination = page.paginate(response.data.count, 25, params.pageNum)
const filter = ` const filter = `
@ -520,7 +546,7 @@ page.getUploads = (params = {}) => {
<form class="prevent-default"> <form class="prevent-default">
<div class="field has-addons"> <div class="field has-addons">
<div class="control is-expanded"> <div class="control is-expanded">
<input id="filters" class="input is-small" type="text" placeholder="Filters" value="${page.escape(params.filters || '')}"> <input id="filters" class="input is-small" type="text" placeholder="Filter uploads" value="${page.escape(params.filters || '')}">
</div> </div>
<div class="control"> <div class="control">
<button type="button" class="button is-small is-primary is-outlined" title="Help?" data-action="upload-filters-help"${params.all ? ' data-all="true"' : ''}> <button type="button" class="button is-small is-primary is-outlined" title="Help?" data-action="upload-filters-help"${params.all ? ' data-all="true"' : ''}>
@ -633,7 +659,7 @@ page.getUploads = (params = {}) => {
files[i].type = 'video' files[i].type = 'video'
// Cache bare minimum data for thumbnails viewer // Cache bare minimum data for thumbnails viewer
page.cache.uploads[files[i].id] = { page.cache[files[i].id] = {
name: files[i].name, name: files[i].name,
thumb: files[i].thumb, thumb: files[i].thumb,
original: files[i].file, original: files[i].file,
@ -723,7 +749,7 @@ page.getUploads = (params = {}) => {
` `
table.appendChild(div) table.appendChild(div)
page.checkboxes[page.currentView] = table.querySelectorAll('.checkbox[data-action="select"]') page.checkboxes = table.querySelectorAll('.checkbox[data-action="select"]')
} }
} else { } else {
const allAlbums = params.all && params.filters && params.filters.includes('albumid:') const allAlbums = params.all && params.filters && params.filters.includes('albumid:')
@ -765,7 +791,7 @@ page.getUploads = (params = {}) => {
<td class="controls"><input type="checkbox" class="checkbox" title="Select" data-index="${i}" data-action="select"${upload.selected ? ' checked' : ''}></td> <td class="controls"><input type="checkbox" class="checkbox" title="Select" data-index="${i}" data-action="select"${upload.selected ? ' checked' : ''}></td>
<th><a href="${upload.file}" target="_blank" title="${upload.file}">${upload.name}</a></th> <th><a href="${upload.file}" target="_blank" title="${upload.file}">${upload.name}</a></th>
${params.album === undefined ? `<th>${upload.appendix}</th>` : ''} ${params.album === undefined ? `<th>${upload.appendix}</th>` : ''}
${allAlbums ? `<th>${files[i].albumid ? (albums[files[i].albumid] || '') : ''}</th>` : ''} ${allAlbums ? `<th>${upload.albumid ? (albums[upload.albumid] || '') : ''}</th>` : ''}
<td>${upload.prettyBytes}</td> <td>${upload.prettyBytes}</td>
${params.all ? `<td>${upload.ip || ''}</td>` : ''} ${params.all ? `<td>${upload.ip || ''}</td>` : ''}
<td>${upload.prettyDate}</td> <td>${upload.prettyDate}</td>
@ -796,7 +822,7 @@ page.getUploads = (params = {}) => {
` `
table.appendChild(tr) table.appendChild(tr)
page.checkboxes[page.currentView] = table.querySelectorAll('.checkbox[data-action="select"]') page.checkboxes = table.querySelectorAll('.checkbox[data-action="select"]')
} }
} }
@ -830,8 +856,13 @@ page.setUploadsView = (view, element) => {
if (page.isSomethingLoading) if (page.isSomethingLoading)
return page.warnSomethingLoading() return page.warnSomethingLoading()
localStorage[lsKeys.viewType[page.currentView]] = view if (view === 'list') {
page.views[page.currentView].type = view delete localStorage[lsKeys.viewType[page.currentView]]
page.views[page.currentView].type = undefined
} else {
localStorage[lsKeys.viewType[page.currentView]] = view
page.views[page.currentView].type = view
}
// eslint-disable-next-line compat/compat // eslint-disable-next-line compat/compat
page.getUploads(Object.assign(page.views[page.currentView], { page.getUploads(Object.assign(page.views[page.currentView], {
@ -840,7 +871,7 @@ page.setUploadsView = (view, element) => {
} }
page.displayPreview = id => { page.displayPreview = id => {
const file = page.cache.uploads[id] const file = page.cache[id]
if (!file.thumb) return if (!file.thumb) return
const div = document.createElement('div') const div = document.createElement('div')
@ -927,12 +958,12 @@ page.displayPreview = id => {
} }
page.selectAll = element => { page.selectAll = element => {
for (let i = 0; i < page.checkboxes[page.currentView].length; i++) { for (let i = 0; i < page.checkboxes.length; i++) {
const id = page.getItemID(page.checkboxes[page.currentView][i]) const id = page.getItemID(page.checkboxes[i])
if (isNaN(id)) continue if (isNaN(id)) continue
if (page.checkboxes[page.currentView][i].checked !== element.checked) { if (page.checkboxes[i].checked !== element.checked) {
page.checkboxes[page.currentView][i].checked = element.checked page.checkboxes[i].checked = element.checked
if (page.checkboxes[page.currentView][i].checked) if (page.checkboxes[i].checked)
page.selected[page.currentView].push(id) page.selected[page.currentView].push(id)
else else
page.selected[page.currentView].splice(page.selected[page.currentView].indexOf(id), 1) page.selected[page.currentView].splice(page.selected[page.currentView].indexOf(id), 1)
@ -955,12 +986,12 @@ page.selectInBetween = (element, lastElement) => {
if (distance < 2) if (distance < 2)
return return
for (let i = 0; i < page.checkboxes[page.currentView].length; i++) for (let i = 0; i < page.checkboxes.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)) {
// Check or uncheck depending on the state of the initial checkbox // Check or uncheck depending on the state of the initial checkbox
const checked = page.checkboxes[page.currentView][i].checked = lastElement.checked const checked = page.checkboxes[i].checked = lastElement.checked
const id = page.getItemID(page.checkboxes[page.currentView][i]) const id = page.getItemID(page.checkboxes[i])
if (!page.selected[page.currentView].includes(id) && checked) if (!page.selected[page.currentView].includes(id) && checked)
page.selected[page.currentView].push(id) page.selected[page.currentView].push(id)
else if (page.selected[page.currentView].includes(id) && !checked) else if (page.selected[page.currentView].includes(id) && !checked)
@ -972,13 +1003,12 @@ page.select = (element, event) => {
const id = page.getItemID(element) const id = page.getItemID(element)
if (isNaN(id)) return if (isNaN(id)) return
const lastSelected = page.lastSelected[page.currentView] if (event.shiftKey && page.lastSelected) {
if (event.shiftKey && lastSelected) { page.selectInBetween(element, page.lastSelected)
page.selectInBetween(element, lastSelected)
// Check or uncheck depending on the state of the initial checkbox // Check or uncheck depending on the state of the initial checkbox
element.checked = lastSelected.checked element.checked = page.lastSelected.checked
} else { } else {
page.lastSelected[page.currentView] = element page.lastSelected = element
} }
if (!page.selected[page.currentView].includes(id) && element.checked) if (!page.selected[page.currentView].includes(id) && element.checked)
@ -995,7 +1025,7 @@ page.select = (element, event) => {
page.clearSelection = () => { page.clearSelection = () => {
const selected = page.selected[page.currentView] const selected = page.selected[page.currentView]
const type = page.currentView === 'users' ? 'users' : 'uploads' const type = page.getByView(page.currentView, 'type')
const count = selected.length const count = selected.length
if (!count) if (!count)
return swal('An error occurred!', `You have not selected any ${type}.`, 'error') return swal('An error occurred!', `You have not selected any ${type}.`, 'error')
@ -1008,7 +1038,7 @@ page.clearSelection = () => {
}).then(proceed => { }).then(proceed => {
if (!proceed) return if (!proceed) return
const checkboxes = page.checkboxes[page.currentView] const checkboxes = page.checkboxes
for (let i = 0; i < checkboxes.length; i++) for (let i = 0; i < checkboxes.length; i++)
if (checkboxes[i].checked) if (checkboxes[i].checked)
checkboxes[i].checked = false checkboxes[i].checked = false
@ -1134,7 +1164,7 @@ page.filterUploads = element => {
} }
page.viewUserUploads = (id, element) => { page.viewUserUploads = (id, element) => {
const user = page.cache.users[id] const user = page.cache[id]
if (!user) return if (!user) return
element.classList.add('is-loading') element.classList.add('is-loading')
// Wrap username in quotes if it contains whitespaces // Wrap username in quotes if it contains whitespaces
@ -1148,6 +1178,20 @@ page.viewUserUploads = (id, element) => {
}) })
} }
page.viewAlbumUploads = (id, element) => {
if (!page.cache[id]) return
element.classList.add('is-loading')
// eslint-disable-next-line compat/compat
const all = page.currentView === 'albumsAll' && page.permissions.moderator
page.getUploads({
all,
filters: `albumid:${id}`,
trigger: all
? document.querySelector('#itemManageUploads')
: document.querySelector('#itemUploads')
})
}
page.deleteUpload = id => { page.deleteUpload = id => {
page.postBulkDeleteUploads({ page.postBulkDeleteUploads({
all: page.currentView === 'uploadsAll', all: page.currentView === 'uploadsAll',
@ -1462,9 +1506,24 @@ page.addUploadsToAlbum = (ids, callback) => {
} }
page.getAlbums = (params = {}) => { page.getAlbums = (params = {}) => {
if (params && params.all && !page.permissions.moderator)
return swal('An error occurred!', 'You cannot do this!', 'error')
if (page.isSomethingLoading)
return page.warnSomethingLoading()
page.updateTrigger(params.trigger, 'loading') page.updateTrigger(params.trigger, 'loading')
axios.get('api/albums').then(response => { if (typeof params.pageNum !== 'number')
params.pageNum = 0
const headers = {}
if (params.all)
headers.all = '1'
const url = `api/albums/${params.pageNum}`
axios.get(url, { headers }).then(response => {
if (!response) return if (!response) return
if (response.data.success === false) if (response.data.success === false)
@ -1475,9 +1534,115 @@ page.getAlbums = (params = {}) => {
return swal('An error occurred!', response.data.description, 'error') return swal('An error occurred!', response.data.description, 'error')
} }
page.cache.albums = {} const pages = Math.ceil(response.data.count / 25)
const albums = response.data.albums
if (params.pageNum && (albums.length === 0))
if (params.autoPage) {
params.pageNum = pages - 1
return page.getAlbums(params)
} else {
page.updateTrigger(params.trigger)
return swal('An error occurred!', `There are no more albums to populate page ${params.pageNum + 1}.`, 'error')
}
page.dom.innerHTML = ` page.currentView = params.all ? 'albumsAll' : 'albums'
page.cache = {}
const users = response.data.users
const homeDomain = response.data.homeDomain
if (params.pageNum < 0) params.pageNum = Math.max(0, pages + params.pageNum)
const pagination = page.paginate(response.data.count, 25, params.pageNum)
const filter = `
<div class="column">
<form class="prevent-default">
<div class="field has-addons">
<div class="control is-expanded">
<input id="filters" class="input is-small" type="text" placeholder="Filter albums (WIP)" value="${page.escape(params.filters || '')}" disabled>
</div>
<div class="control">
<button type="button" class="button is-small is-primary is-outlined" title="Help? (WIP)" data-action="album-filters-help" disabled>
<span class="icon">
<i class="icon-help-circled"></i>
</span>
</button>
</div>
<div class="control">
<button type="submit" class="button is-small is-info is-outlined" title="Filter albums (WIP)" data-action="filter-albums" disabled>
<span class="icon">
<i class="icon-filter"></i>
</span>
</button>
</div>
</div>
</form>
</div>
`
const extraControls = `
<div class="columns">
${filter}
<div class="column is-one-quarter">
<form class="prevent-default">
<div class="field has-addons">
<div class="control is-expanded">
<input id="jumpToPage" class="input is-small" type="number" min="1" max="${pages}" value="${params.pageNum + 1}"${pages === 1 ? ' disabled' : ''}>
</div>
<div class="control">
<button type="submit" class="button is-small is-info is-outlined" title="Jump to page" data-action="jump-to-page">
<span class="icon">
<i class="icon-paper-plane"></i>
</span>
</button>
</div>
</div>
</form>
</div>
</div>
`
const controls = `
<div class="columns">
<div class="column is-hidden-mobile"></div>
<div class="column bulk-operations has-text-right">
<a class="button is-small is-info is-outlined" title="Clear selection" data-action="clear-selection">
<span class="icon">
<i class="icon-cancel"></i>
</span>
</a>
<a class="button is-small is-dangerish is-outlined" title="Bulk disable (WIP)" data-action="bulk-disable-albums" disabled>
<span class="icon">
<i class="icon-trash"></i>
</span>
${!params.all ? '<span>Bulk disable</span>' : ''}
</a>
${params.all
? `<a class="button is-small is-danger is-outlined" title="Bulk delete (WIP)" data-action="bulk-delete-albums" disabled>
<span class="icon">
<i class="icon-trash"></i>
</span>
<span>Bulk delete</span>
</a>`
: ''}
</div>
</div>
`
// Do some string replacements for bottom controls
const bottomFiltersId = 'bFilters'
const bottomJumpId = 'bJumpToPage'
const bottomExtraControls = extraControls
.replace(/id="filters"/, `id="${bottomFiltersId}"`)
.replace(/(data-action="filter-uploads")/, `$1 data-filtersid="${bottomFiltersId}"`)
.replace(/id="jumpToPage"/, `id="${bottomJumpId}"`)
.replace(/(data-action="jump-to-page")/g, `$1 data-jumpid="${bottomJumpId}"`)
const bottomPagination = pagination
.replace(/(data-action="page-ellipsis")/g, `$1 data-jumpid="${bottomJumpId}"`)
// Whether there are any unselected items
let unselected = false
const createNewAlbum = `
<h2 class="subtitle">Create new album</h2> <h2 class="subtitle">Create new album</h2>
<form class="prevent-default"> <form class="prevent-default">
<div class="field"> <div class="field">
@ -1504,14 +1669,22 @@ page.getAlbums = (params = {}) => {
</div> </div>
</form> </form>
<hr> <hr>
<h2 class="subtitle">List of albums</h2> `
page.dom.innerHTML = `
${!params.all ? createNewAlbum : ''}
${pagination}
${extraControls}
${controls}
<div class="table-container"> <div class="table-container">
<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" data-action="select-all"></th>
<th>ID</th> <th>ID</th>
<th>Name</th> <th>Name</th>
<th>Files</th> ${params.all ? '<th>User</th>' : ''}
<th>Uploads</th>
<th>Created at</th> <th>Created at</th>
<th>Public link</th> <th>Public link</th>
<th></th> <th></th>
@ -1521,38 +1694,54 @@ page.getAlbums = (params = {}) => {
</tbody> </tbody>
</table> </table>
</div> </div>
${controls}
${bottomExtraControls}
${bottomPagination}
` `
const homeDomain = response.data.homeDomain
const table = document.querySelector('#table') const table = document.querySelector('#table')
for (let i = 0; i < response.data.albums.length; i++) { for (let i = 0; i < albums.length; i++) {
const album = response.data.albums[i] const album = albums[i]
const albumUrl = `${homeDomain}/a/${album.identifier}` const albumUrl = `${homeDomain}/a/${album.identifier}`
const selected = page.selected[page.currentView].includes(album.id)
if (!selected) unselected = true
// Prettify // Prettify
album.prettyDate = page.getPrettyDate(new Date(album.timestamp * 1000)) album.prettyDate = page.getPrettyDate(new Date(album.timestamp * 1000))
page.cache.albums[album.id] = { // Server-side explicitly expect this value to consider an album as disabled
const enabled = album.enabled !== 0
page.cache[album.id] = {
name: album.name, name: album.name,
download: album.download, download: album.download,
public: album.public, public: album.public,
description: album.description description: album.description,
enabled
} }
const tr = document.createElement('tr') const tr = document.createElement('tr')
tr.dataset.id = album.id
tr.innerHTML = ` tr.innerHTML = `
<td class="controls"><input type="checkbox" class="checkbox" title="Select" data-index="${i}" data-action="select"${selected ? ' checked' : ''}></td>
<th>${album.id}</th> <th>${album.id}</th>
<th>${album.name}</th> <th${enabled ? '' : ' class="has-text-grey"'}>${album.name}</td>
<th>${album.files}</th> ${params.all ? `<th>${album.userid ? (users[album.userid] || '') : ''}</th>` : ''}
<th>${album.uploads}</th>
<td>${album.prettyDate}</td> <td>${album.prettyDate}</td>
<td><a ${album.public ? `href="${albumUrl}"` : 'class="is-linethrough"'} target="_blank">${albumUrl}</a></td> <td><a ${album.public ? '' : 'class="is-linethrough" '}href="${albumUrl}" target="_blank">${albumUrl}</a></td>
<td class="has-text-right" data-id="${album.id}"> <td class="has-text-right" data-id="${album.id}">
<a class="button is-small is-primary is-outlined" title="Edit album" data-action="edit-album"> <a class="button is-small is-primary is-outlined" title="Edit album" data-action="edit-album">
<span class="icon is-small"> <span class="icon is-small">
<i class="icon-pencil"></i> <i class="icon-pencil"></i>
</span> </span>
</a> </a>
<a class="button is-small is-info is-outlined" title="${album.uploads ? 'View uploads' : 'Album doesn\'t have uploads'}" data-action="view-album-uploads" ${album.uploads ? '' : 'disabled'}>
<span class="icon">
<i class="icon-docs"></i>
</span>
</a>
<a class="button is-small is-info is-outlined clipboard-js" title="Copy link to clipboard" ${album.public ? `data-clipboard-text="${albumUrl}"` : 'disabled'}> <a class="button is-small is-info is-outlined clipboard-js" title="Copy link to clipboard" ${album.public ? `data-clipboard-text="${albumUrl}"` : 'disabled'}>
<span class="icon is-small"> <span class="icon is-small">
<i class="icon-clipboard"></i> <i class="icon-clipboard"></i>
@ -1563,7 +1752,7 @@ page.getAlbums = (params = {}) => {
<i class="icon-download"></i> <i class="icon-download"></i>
</span> </span>
</a> </a>
<a class="button is-small is-danger is-outlined" title="Delete album" data-action="delete-album"> <a class="button is-small is-dangerish is-outlined" title="Disable album" data-action="disable-album">
<span class="icon is-small"> <span class="icon is-small">
<i class="icon-trash"></i> <i class="icon-trash"></i>
</span> </span>
@ -1572,9 +1761,21 @@ page.getAlbums = (params = {}) => {
` `
table.appendChild(tr) table.appendChild(tr)
page.checkboxes = table.querySelectorAll('.checkbox[data-action="select"]')
} }
const selectAll = document.querySelector('#selectAll')
if (selectAll && !unselected) {
selectAll.checked = true
selectAll.title = 'Unselect all'
}
page.fadeAndScroll() page.fadeAndScroll()
page.updateTrigger(params.trigger, 'active') page.updateTrigger(params.trigger, 'active')
if (page.currentView === 'albumsAll')
page.views[page.currentView].filters = params.filters
page.views[page.currentView].pageNum = albums.length ? params.pageNum : 0
}).catch(error => { }).catch(error => {
page.updateTrigger(params.trigger) page.updateTrigger(params.trigger)
page.onAxiosError(error) page.onAxiosError(error)
@ -1582,7 +1783,7 @@ page.getAlbums = (params = {}) => {
} }
page.editAlbum = id => { page.editAlbum = id => {
const album = page.cache.albums[id] const album = page.cache[id]
if (!album) return if (!album) return
const div = document.createElement('div') const div = document.createElement('div')
@ -1599,6 +1800,16 @@ page.editAlbum = id => {
</div> </div>
<p class="help">Max length is ${page.albumDescMaxLength} characters.</p> <p class="help">Max length is ${page.albumDescMaxLength} characters.</p>
</div> </div>
${page.currentView === 'albumsAll' && page.permissions.moderator
? `<div class="field">
<div class="control">
<label class="checkbox">
<input id="swalEnabled" type="checkbox" ${album.enabled ? 'checked' : ''}>
Enabled
</label>
</div>
</div>`
: ''}
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<label class="checkbox"> <label class="checkbox">
@ -1638,14 +1849,19 @@ page.editAlbum = id => {
}).then(value => { }).then(value => {
if (!value) return if (!value) return
axios.post('api/albums/edit', { const post = {
id, id,
name: document.querySelector('#swalName').value.trim(), name: document.querySelector('#swalName').value.trim(),
description: document.querySelector('#swalDescription').value.trim(), description: document.querySelector('#swalDescription').value.trim(),
download: document.querySelector('#swalDownload').checked, download: document.querySelector('#swalDownload').checked,
public: document.querySelector('#swalPublic').checked, public: document.querySelector('#swalPublic').checked,
requestLink: document.querySelector('#swalRequestLink').checked requestLink: document.querySelector('#swalRequestLink').checked
}).then(response => { }
if (page.currentView === 'albumsAll' && page.permissions.moderator)
post.enabled = document.querySelector('#swalEnabled').checked
axios.post('api/albums/edit', post).then(response => {
if (!response) return if (!response) return
if (response.data.success === false) if (response.data.success === false)
@ -1656,35 +1872,39 @@ page.editAlbum = id => {
} }
if (response.data.identifier) if (response.data.identifier)
swal('Success!', `Your album's new identifier is: ${response.data.identifier}.`, 'success') swal('Success!', `The album's new identifier is: ${response.data.identifier}.`, 'success')
else if (response.data.name !== album.name) else if (response.data.name !== album.name)
swal('Success!', `Your album was renamed to: ${response.data.name}.`, 'success') swal('Success!', `The album was renamed to: ${response.data.name}.`, 'success')
else else
swal('Success!', 'Your album was edited!', 'success', { swal('Success!', 'The album was edited.', 'success', {
buttons: false, buttons: false,
timer: 1500 timer: 1500
}) })
page.getAlbumsSidebar() page.getAlbumsSidebar()
page.getAlbums() // Reload albums list
// eslint-disable-next-line compat/compat
page.getAlbums(Object.assign(page.views[page.currentView], {
autoPage: true
}))
}).catch(page.onAxiosError) }).catch(page.onAxiosError)
}) })
} }
page.deleteAlbum = id => { page.disableAlbum = id => {
swal({ swal({
title: 'Are you sure?', title: 'Are you sure?',
text: 'This won\'t delete your uploads, only the album!', text: 'This won\'t delete the uploads associated with the album!',
icon: 'warning', icon: 'warning',
dangerMode: true, dangerMode: true,
buttons: { buttons: {
cancel: true, cancel: true,
confirm: { confirm: {
text: 'Yes, delete it!', text: 'Yes, disable it!',
closeModal: false closeModal: false
}, },
purge: { purge: {
text: 'Umm, delete the uploads too please?', text: 'Umm, delete the uploads too, please?',
value: 'purge', value: 'purge',
className: 'swal-button--danger', className: 'swal-button--danger',
closeModal: false closeModal: false
@ -1693,7 +1913,7 @@ page.deleteAlbum = id => {
}).then(proceed => { }).then(proceed => {
if (!proceed) return if (!proceed) return
axios.post('api/albums/delete', { axios.post('api/albums/disable', {
id, id,
purge: proceed === 'purge' purge: proceed === 'purge'
}).then(response => { }).then(response => {
@ -1710,12 +1930,17 @@ page.deleteAlbum = id => {
return swal('An error occurred!', response.data.description, 'error') return swal('An error occurred!', response.data.description, 'error')
} }
swal('Deleted!', 'Your album has been deleted.', 'success', { swal('Deleted!', 'Your album has been disabled.', 'success', {
buttons: false, buttons: false,
timer: 1500 timer: 1500
}) })
page.getAlbumsSidebar() page.getAlbumsSidebar()
page.getAlbums() // Reload albums list
// eslint-disable-next-line compat/compat
page.getAlbums(Object.assign(page.views[page.currentView], {
autoPage: true
}))
}).catch(page.onAxiosError) }).catch(page.onAxiosError)
}) })
} }
@ -1742,7 +1967,9 @@ page.submitAlbum = element => {
timer: 1500 timer: 1500
}) })
page.getAlbumsSidebar() page.getAlbumsSidebar()
page.getAlbums() page.getAlbums({
pageNum: -1
})
}).catch(error => { }).catch(error => {
page.updateTrigger(element) page.updateTrigger(element)
page.onAxiosError(error) page.onAxiosError(error)
@ -1750,7 +1977,7 @@ page.submitAlbum = element => {
} }
page.getAlbumsSidebar = () => { page.getAlbumsSidebar = () => {
axios.get('api/albums/sidebar').then(response => { axios.get('api/albums', { headers: { sidebar: '1' } }).then(response => {
if (!response) return if (!response) return
if (response.data.success === false) if (response.data.success === false)
@ -1760,6 +1987,8 @@ page.getAlbumsSidebar = () => {
return swal('An error occurred!', response.data.description, 'error') return swal('An error occurred!', response.data.description, 'error')
} }
const albums = response.data.albums
const count = response.data.count
const albumsContainer = document.querySelector('#albumsContainer') const albumsContainer = document.querySelector('#albumsContainer')
// Clear albums sidebar if necessary // Clear albums sidebar if necessary
@ -1770,16 +1999,16 @@ page.getAlbumsSidebar = () => {
albumsContainer.innerHTML = '' albumsContainer.innerHTML = ''
} }
if (response.data.albums === undefined) if (albums === undefined)
return return
for (let i = 0; i < response.data.albums.length; i++) { for (let i = 0; i < albums.length; i++) {
const album = response.data.albums[i] const album = albums[i]
const li = document.createElement('li') const li = document.createElement('li')
const a = document.createElement('a') const a = document.createElement('a')
a.id = album.id a.id = album.id
a.innerHTML = album.name
a.className = 'is-relative' a.className = 'is-relative'
a.innerHTML = album.name
a.addEventListener('click', event => { a.addEventListener('click', event => {
page.getUploads({ page.getUploads({
@ -1792,6 +2021,23 @@ page.getAlbumsSidebar = () => {
li.appendChild(a) li.appendChild(a)
albumsContainer.appendChild(li) albumsContainer.appendChild(li)
} }
if (count > albums.length) {
const li = document.createElement('li')
const a = document.createElement('a')
a.className = 'is-relative'
a.innerHTML = '...'
a.title = `You have ${count} albums, but the sidebar can only list your first ${albums.length} albums.`
a.addEventListener('click', event => {
page.getAlbums({
trigger: document.querySelector('#itemManageYourAlbums')
})
})
li.appendChild(a)
albumsContainer.appendChild(li)
}
}).catch(page.onAxiosError) }).catch(page.onAxiosError)
} }
@ -1939,7 +2185,7 @@ page.getUsers = (params = {}) => {
page.updateTrigger(params.trigger, 'loading') page.updateTrigger(params.trigger, 'loading')
if (params.pageNum === undefined) if (typeof params.pageNum !== 'number')
params.pageNum = 0 params.pageNum = 0
const url = `api/users/${params.pageNum}` const url = `api/users/${params.pageNum}`
@ -1953,7 +2199,8 @@ page.getUsers = (params = {}) => {
} }
const pages = Math.ceil(response.data.count / 25) const pages = Math.ceil(response.data.count / 25)
if (params.pageNum && (response.data.users.length === 0)) const users = response.data.users
if (params.pageNum && (users.length === 0))
if (params.autoPage) { if (params.autoPage) {
params.pageNum = pages - 1 params.pageNum = pages - 1
return page.getUsers(params) return page.getUsers(params)
@ -1963,7 +2210,7 @@ page.getUsers = (params = {}) => {
} }
page.currentView = 'users' page.currentView = 'users'
page.cache.users = {} page.cache = {}
if (params.pageNum < 0) params.pageNum = Math.max(0, pages + params.pageNum) if (params.pageNum < 0) params.pageNum = Math.max(0, pages + params.pageNum)
const pagination = page.paginate(response.data.count, 25, params.pageNum) const pagination = page.paginate(response.data.count, 25, params.pageNum)
@ -1973,7 +2220,7 @@ page.getUsers = (params = {}) => {
<form class="prevent-default"> <form class="prevent-default">
<div class="field has-addons"> <div class="field has-addons">
<div class="control is-expanded"> <div class="control is-expanded">
<input id="filters" class="input is-small" type="text" placeholder="Filters (WIP)" value="${page.escape(params.filters || '')}" disabled> <input id="filters" class="input is-small" type="text" placeholder="Filter users (WIP)" value="${page.escape(params.filters || '')}" disabled>
</div> </div>
<div class="control"> <div class="control">
<button type="button" class="button is-small is-primary is-outlined" title="Help? (WIP)" data-action="user-filters-help" disabled> <button type="button" class="button is-small is-primary is-outlined" title="Help? (WIP)" data-action="user-filters-help" disabled>
@ -2031,7 +2278,7 @@ page.getUsers = (params = {}) => {
<i class="icon-cancel"></i> <i class="icon-cancel"></i>
</span> </span>
</a> </a>
<a class="button is-small is-warning is-outlined" title="Bulk disable (WIP)" data-action="bulk-disable-users" disabled> <a class="button is-small is-dangerish is-outlined" title="Bulk disable (WIP)" data-action="bulk-disable-users" disabled>
<span class="icon"> <span class="icon">
<i class="icon-hammer"></i> <i class="icon-hammer"></i>
</span> </span>
@ -2089,9 +2336,9 @@ page.getUsers = (params = {}) => {
const table = document.querySelector('#table') const table = document.querySelector('#table')
for (let i = 0; i < response.data.users.length; i++) { for (let i = 0; i < users.length; i++) {
const user = response.data.users[i] const user = users[i]
const selected = page.selected.users.includes(user.id) const selected = page.selected[page.currentView].includes(user.id)
if (!selected) unselected = true if (!selected) unselected = true
let displayGroup = null let displayGroup = null
@ -2103,7 +2350,7 @@ page.getUsers = (params = {}) => {
// Server-side explicitly expects either of these two values to consider a user as disabled // Server-side explicitly expects either of these two values to consider a user as disabled
const enabled = user.enabled !== false && user.enabled !== 0 const enabled = user.enabled !== false && user.enabled !== 0
page.cache.users[user.id] = { page.cache[user.id] = {
username: user.username, username: user.username,
groups: user.groups, groups: user.groups,
enabled, enabled,
@ -2121,7 +2368,7 @@ page.getUsers = (params = {}) => {
tr.dataset.id = user.id tr.dataset.id = user.id
tr.innerHTML = ` tr.innerHTML = `
<td class="controls"><input type="checkbox" class="checkbox" title="Select" data-index="${i}" data-action="select"${selected ? ' checked' : ''}></td> <td class="controls"><input type="checkbox" class="checkbox" title="Select" data-index="${i}" data-action="select"${selected ? ' checked' : ''}></td>
<th${enabled ? '' : ' class="is-linethrough"'}>${user.username}</td> <th${enabled ? '' : ' class="has-text-grey"'}>${user.username}</td>
<th>${user.uploads}</th> <th>${user.uploads}</th>
<td>${page.getPrettyBytes(user.usage)}</td> <td>${page.getPrettyBytes(user.usage)}</td>
<td>${displayGroup}</td> <td>${displayGroup}</td>
@ -2138,7 +2385,7 @@ page.getUsers = (params = {}) => {
<i class="icon-docs"></i> <i class="icon-docs"></i>
</span> </span>
</a> </a>
<a class="button is-small is-warning is-outlined" title="${enabled ? 'Disable user' : 'User is disabled'}" data-action="disable-user" ${enabled ? '' : 'disabled'}> <a class="button is-small is-dangerish is-outlined" title="${enabled ? 'Disable user' : 'User is disabled'}" data-action="disable-user" ${enabled ? '' : 'disabled'}>
<span class="icon"> <span class="icon">
<i class="icon-hammer"></i> <i class="icon-hammer"></i>
</span> </span>
@ -2152,7 +2399,7 @@ page.getUsers = (params = {}) => {
` `
table.appendChild(tr) table.appendChild(tr)
page.checkboxes.users = table.querySelectorAll('.checkbox[data-action="select"]') page.checkboxes = table.querySelectorAll('.checkbox[data-action="select"]')
} }
const selectAll = document.querySelector('#selectAll') const selectAll = document.querySelector('#selectAll')
@ -2164,7 +2411,7 @@ page.getUsers = (params = {}) => {
page.fadeAndScroll() page.fadeAndScroll()
page.updateTrigger(params.trigger, 'active') page.updateTrigger(params.trigger, 'active')
page.views.users.pageNum = response.data.users.length ? params.pageNum : 0 page.views[page.currentView].pageNum = users.length ? params.pageNum : 0
}).catch(error => { }).catch(error => {
page.updateTrigger(params.trigger) page.updateTrigger(params.trigger)
page.onAxiosError(error) page.onAxiosError(error)
@ -2252,7 +2499,7 @@ page.createUser = () => {
} }
page.editUser = id => { page.editUser = id => {
const user = page.cache.users[id] const user = page.cache[id]
if (!user) return if (!user) return
const groupOptions = Object.keys(page.permissions).map((g, i, a) => { const groupOptions = Object.keys(page.permissions).map((g, i, a) => {
@ -2366,12 +2613,12 @@ page.editUser = id => {
} }
page.disableUser = id => { page.disableUser = id => {
const user = page.cache.users[id] const user = page.cache[id]
if (!user || !user.enabled) return if (!user || !user.enabled) return
const content = document.createElement('div') const content = document.createElement('div')
content.innerHTML = ` content.innerHTML = `
<p>You will be disabling a user named <b>${page.cache.users[id].username}</b>.</p> <p>You will be disabling a user named <b>${page.cache[id].username}</b>.</p>
<p>Their files will remain.</p> <p>Their files will remain.</p>
` `
@ -2399,7 +2646,7 @@ page.disableUser = id => {
else else
return swal('An error occurred!', response.data.description, 'error') return swal('An error occurred!', response.data.description, 'error')
swal('Success!', `${page.cache.users[id].username} has been disabled.`, 'success', { swal('Success!', `${page.cache[id].username} has been disabled.`, 'success', {
buttons: false, buttons: false,
timer: 1500 timer: 1500
}) })
@ -2409,12 +2656,12 @@ page.disableUser = id => {
} }
page.deleteUser = id => { page.deleteUser = id => {
const user = page.cache.users[id] const user = page.cache[id]
if (!user) return if (!user) return
const content = document.createElement('div') const content = document.createElement('div')
content.innerHTML = ` content.innerHTML = `
<p>You will be deleting a user named <b>${page.cache.users[id].username}</b>.<p> <p>You will be deleting a user named <b>${page.cache[id].username}</b>.<p>
<p>Their files will remain, unless you choose otherwise.</p> <p>Their files will remain, unless you choose otherwise.</p>
` `
@ -2458,7 +2705,7 @@ page.deleteUser = id => {
return swal('An error occurred!', response.data.description, 'error') return swal('An error occurred!', response.data.description, 'error')
} }
swal('Success!', `${page.cache.users[id].username} has been deleted.`, 'success', { swal('Success!', `${page.cache[id].username} has been deleted.`, 'success', {
buttons: false, buttons: false,
timer: 1500 timer: 1500
}) })
@ -2647,7 +2894,7 @@ window.onload = () => {
if (!('ontouchstart' in document.documentElement)) if (!('ontouchstart' in document.documentElement))
document.documentElement.classList.add('no-touch') document.documentElement.classList.add('no-touch')
const selectedKeys = ['uploads', 'uploadsAll', 'users'] const selectedKeys = ['uploads', 'uploadsAll', 'albums', 'albumsAll', 'users']
for (let i = 0; i < selectedKeys.length; i++) { for (let i = 0; i < selectedKeys.length; i++) {
const ls = localStorage[lsKeys.selected[selectedKeys[i]]] const ls = localStorage[lsKeys.selected[selectedKeys[i]]]
if (ls) page.selected[selectedKeys[i]] = JSON.parse(ls) if (ls) page.selected[selectedKeys[i]] = JSON.parse(ls)

View File

@ -1,5 +1,5 @@
{ {
"1": "1590695426", "1": "1590986654",
"2": "1589010026", "2": "1589010026",
"3": "1581416390", "3": "1581416390",
"4": "1581416390", "4": "1581416390",

View File

@ -54,7 +54,7 @@
<h2 class='subtitle is-brighter'>What information do we know about you?</h2> <h2 class='subtitle is-brighter'>What information do we know about you?</h2>
<article class="message"> <article class="message">
<div class="message-body"> <div class="message-body">
We dont request or require you to provide personal information to access our web site.<br> We dont request or require you to provide personal information to access our website.<br>
However, we may receive your IP address and user agent automatically. However, we may receive your IP address and user agent automatically.
</div> </div>
</article> </article>
@ -124,22 +124,34 @@
<td>Personalization</td> <td>Personalization</td>
<td>LocalStorage</td> <td>LocalStorage</td>
</tr> </tr>
<tr>
<th>selectedAlbums</th>
<td>{{ globals.root_domain }}</td>
<td>Personalization</td>
<td>LocalStorage</td>
</tr>
<tr>
<th>selectedAlbumsAll</th>
<td>{{ globals.root_domain }}</td>
<td>Personalization</td>
<td>LocalStorage</td>
</tr>
<tr> <tr>
<th>selectedUploads</th> <th>selectedUploads</th>
<td>{{ globals.root_domain }}</td> <td>{{ globals.root_domain }}</td>
<td>Necessary</td> <td>Personalization</td>
<td>LocalStorage</td> <td>LocalStorage</td>
</tr> </tr>
<tr> <tr>
<th>selectedUploadsAll</th> <th>selectedUploadsAll</th>
<td>{{ globals.root_domain }}</td> <td>{{ globals.root_domain }}</td>
<td>Necessary</td> <td>Personalization</td>
<td>LocalStorage</td> <td>LocalStorage</td>
</tr> </tr>
<tr> <tr>
<th>selectedUsers</th> <th>selectedUsers</th>
<td>{{ globals.root_domain }}</td> <td>{{ globals.root_domain }}</td>
<td>Necessary</td> <td>Personalization</td>
<td>LocalStorage</td> <td>LocalStorage</td>
</tr> </tr>
<tr> <tr>
@ -175,18 +187,21 @@
<tr> <tr>
<th>viewTypeUploads</th> <th>viewTypeUploads</th>
<td>{{ globals.root_domain }}</td> <td>{{ globals.root_domain }}</td>
<td>Necessary</td> <td>Personalization</td>
<td>LocalStorage</td> <td>LocalStorage</td>
</tr> </tr>
<tr> <tr>
<th>viewTypeUploadsAll</th> <th>viewTypeUploadsAll</th>
<td>{{ globals.root_domain }}</td> <td>{{ globals.root_domain }}</td>
<td>Necessary</td> <td>Personalization</td>
<td>LocalStorage</td> <td>LocalStorage</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
Only Analytics keys will be set on first page load regardless of your consent.<br>
The rest will only be set as you use features of our website.<br>
However, all Personalization keys should delete themselves once you set back their default values.
</div> </div>
</article> </article>

View File

@ -58,7 +58,7 @@
<p class="menu-label">Albums</p> <p class="menu-label">Albums</p>
<ul class="menu-list is-unselectable"> <ul class="menu-list is-unselectable">
<li> <li>
<a id="itemManageAlbums" class="is-relative">Manage your albums</a> <a id="itemManageYourAlbums" class="is-relative">Manage your albums</a>
</li> </li>
<li> <li>
<ul id="albumsContainer"></ul> <ul id="albumsContainer"></ul>
@ -72,6 +72,9 @@
<li> <li>
<a id="itemManageUploads" class="is-relative is-hidden">Manage uploads</a> <a id="itemManageUploads" class="is-relative is-hidden">Manage uploads</a>
</li> </li>
<li>
<a id="itemManageAlbums" class="is-relative is-hidden">Manage albums</a>
</li>
<li> <li>
<a id="itemManageUsers" class="is-relative is-hidden">Manage users</a> <a id="itemManageUsers" class="is-relative is-hidden">Manage users</a>
</li> </li>

View File

@ -275,7 +275,7 @@
The logs contain each visitor's IP address.<br> The logs contain each visitor's IP address.<br>
This is required by our automated system, that checks logs in real time to detect and punish abuse, such as DDoS attempts, scanners, and so on.<br> This is required by our automated system, that checks logs in real time to detect and punish abuse, such as DDoS attempts, scanners, and so on.<br>
Logs are rotated daily, and the server will only keep the last 10 logs.<br> Logs are rotated daily, and the server will only keep the last 10 logs.<br>
So you can be reassured that the logs will only be kept for <strong>10 days at most</strong>. So you can be assured that the logs will only be kept for <strong>10 days at most</strong>.
</div> </div>
</article> </article>