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 Zip = require('jszip')
const paths = require('./pathsController')
const perms = require('./permissionController')
const utils = require('./utilsController')
const config = require('./../config')
const logger = require('./../logger')
@ -76,39 +77,102 @@ self.list = async (req, res, next) => {
const user = await utils.authorize(req, res)
if (!user) return
let fields = ['id', 'name']
if (req.params.sidebar === undefined)
fields = fields.concat(['timestamp', 'identifier', 'editedAt', 'download', 'public', 'description'])
const all = req.headers.all === '1'
const sidebar = req.headers.sidebar
const ismoderator = perms.is(user, 'moderator')
if (all && !ismoderator)
return res.status(403).end()
const albums = await db.table('albums')
.select(fields)
.where({
enabled: 1,
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 filter = function () {
if (!all)
this.where({
enabled: 1,
userid: user.id
})
}
const files = await db.table('files')
.whereIn('albumid', Object.keys(albumids))
.select('albumid')
try {
// Query albums count for pagination
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
for (const file of files)
if (albumids[file.albumid])
albumids[file.albumid].files++
let fields = ['id', 'name']
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) => {
@ -161,6 +225,11 @@ self.create = 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)
if (!user) return
@ -218,6 +287,8 @@ self.edit = async (req, res, next) => {
const user = await utils.authorize(req, res)
if (!user) return
const ismoderator = perms.is(user, 'moderator')
const id = parseInt(req.body.id)
if (isNaN(id))
return res.json({ success: false, description: 'No album specified.' })
@ -229,13 +300,19 @@ self.edit = async (req, res, next) => {
if (!name)
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 {
const album = await db.table('albums')
.where({
id,
userid: user.id,
enabled: 1
})
.where(filter)
.first()
if (!album)
@ -256,14 +333,14 @@ self.edit = async (req, res, next) => {
: ''
}
if (ismoderator)
update.enabled = Boolean(req.body.enabled)
if (req.body.requestLink)
update.identifier = await self.getUniqueRandomName()
await db.table('albums')
.where({
id,
userid: user.id
})
.where(filter)
.update(update)
utils.invalidateAlbumsCache([id])

View File

@ -1332,23 +1332,8 @@ self.list = async (req, res) => {
return res.json({ success: true, files, count, users, albums, basedomain })
} catch (error) {
// If moderator, capture SQLITE_ERROR and use its error message for the response's description
let errorString
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?'
})
logger.error(error)
return res.status(500).json({ success: false, description: '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 */

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/:page', (req, res, next) => uploadController.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/addfiles', (req, res, next) => albumsController.addFiles(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/rename', (req, res, next) => albumsController.rename(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
}
.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 {
white-space: break-spaces;
min-height: 2.25em;

View File

@ -9,6 +9,8 @@ const lsKeys = {
selected: {
uploads: 'selectedUploads',
uploadsAll: 'selectedUploadsAll',
albums: 'selectedAlbums',
albumsAll: 'selectedAlbumsAll',
users: 'selectedUsers'
}
}
@ -32,52 +34,53 @@ const page = {
currentView: null,
views: {
// config of uploads view
// params of uploads view
uploads: {
type: localStorage[lsKeys.viewType.uploads],
album: null, // album's id
pageNum: null // page num
pageNum: null
},
// config of uploads view (all)
// params of uploads view (all)
uploadsAll: {
type: localStorage[lsKeys.viewType.uploadsAll],
filters: null, // uploads' filters
pageNum: null, // page num
filters: null,
pageNum: null,
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: {
filters: null, // users' filters
filters: 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: {
uploads: [],
uploadsAll: [],
albums: [],
albumsAll: [],
users: []
},
checkboxes: {
uploads: [],
uploadsAll: [],
users: []
},
lastSelected: {
upload: null,
uploadsAll: null,
user: null
},
checkboxes: [],
lastSelected: [],
// select album dom for dialogs/modals
selectAlbumContainer: null,
// cache for dialogs/modals
cache: {
uploads: {},
albums: {},
users: {}
},
cache: {},
clipboardJS: null,
lazyLoad: null,
@ -208,11 +211,12 @@ page.prepareDashboard = () => {
const itemMenus = [
{ selector: '#itemUploads', onclick: page.getUploads },
{ selector: '#itemDeleteUploadsByNames', onclick: page.deleteUploadsByNames },
{ selector: '#itemManageAlbums', onclick: page.getAlbums },
{ selector: '#itemManageYourAlbums', onclick: page.getAlbums },
{ selector: '#itemManageToken', onclick: page.changeToken },
{ selector: '#itemChangePassword', onclick: page.changePassword },
{ selector: '#itemLogout', onclick: page.logout },
{ 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: '#itemManageUsers', onclick: page.getUsers, group: 'admin' }
]
@ -357,8 +361,10 @@ page.domClick = event => {
return page.submitAlbum(element)
case 'edit-album':
return page.editAlbum(id)
case 'delete-album':
return page.deleteAlbum(id)
case 'disable-album':
return page.disableAlbum(id)
case 'view-album-uploads':
return page.viewAlbumUploads(id, element)
// Manage users
case 'create-user':
return page.createUser()
@ -370,12 +376,6 @@ page.domClick = event => {
return page.deleteUser(id)
case 'view-user-uploads':
return page.viewUserUploads(id, element)
/* // WIP
case 'user-filters-help':
return page.userFiltersHelp(element)
case 'filter-users':
return page.filterUsers(element)
*/
// Others
case 'get-new-token':
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) => {
if (page.isSomethingLoading)
return page.warnSomethingLoading()
@ -425,7 +449,7 @@ page.switchPage = (action, element) => {
trigger: element
})
const func = page.currentView === 'users' ? page.getUsers : page.getUploads
const func = page.getByView(page.currentView, 'func')
switch (action) {
case 'page-prev':
@ -508,11 +532,13 @@ page.getUploads = (params = {}) => {
}
page.currentView = params.all ? 'uploadsAll' : 'uploads'
page.cache.uploads = {}
page.cache = {}
const albums = response.data.albums
const users = response.data.users
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 filter = `
@ -520,7 +546,7 @@ page.getUploads = (params = {}) => {
<form class="prevent-default">
<div class="field has-addons">
<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 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"' : ''}>
@ -633,7 +659,7 @@ page.getUploads = (params = {}) => {
files[i].type = 'video'
// Cache bare minimum data for thumbnails viewer
page.cache.uploads[files[i].id] = {
page.cache[files[i].id] = {
name: files[i].name,
thumb: files[i].thumb,
original: files[i].file,
@ -723,7 +749,7 @@ page.getUploads = (params = {}) => {
`
table.appendChild(div)
page.checkboxes[page.currentView] = table.querySelectorAll('.checkbox[data-action="select"]')
page.checkboxes = table.querySelectorAll('.checkbox[data-action="select"]')
}
} else {
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>
<th><a href="${upload.file}" target="_blank" title="${upload.file}">${upload.name}</a></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>
${params.all ? `<td>${upload.ip || ''}</td>` : ''}
<td>${upload.prettyDate}</td>
@ -796,7 +822,7 @@ page.getUploads = (params = {}) => {
`
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)
return page.warnSomethingLoading()
localStorage[lsKeys.viewType[page.currentView]] = view
page.views[page.currentView].type = view
if (view === 'list') {
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
page.getUploads(Object.assign(page.views[page.currentView], {
@ -840,7 +871,7 @@ page.setUploadsView = (view, element) => {
}
page.displayPreview = id => {
const file = page.cache.uploads[id]
const file = page.cache[id]
if (!file.thumb) return
const div = document.createElement('div')
@ -927,12 +958,12 @@ page.displayPreview = id => {
}
page.selectAll = element => {
for (let i = 0; i < page.checkboxes[page.currentView].length; i++) {
const id = page.getItemID(page.checkboxes[page.currentView][i])
for (let i = 0; i < page.checkboxes.length; i++) {
const id = page.getItemID(page.checkboxes[i])
if (isNaN(id)) continue
if (page.checkboxes[page.currentView][i].checked !== element.checked) {
page.checkboxes[page.currentView][i].checked = element.checked
if (page.checkboxes[page.currentView][i].checked)
if (page.checkboxes[i].checked !== element.checked) {
page.checkboxes[i].checked = element.checked
if (page.checkboxes[i].checked)
page.selected[page.currentView].push(id)
else
page.selected[page.currentView].splice(page.selected[page.currentView].indexOf(id), 1)
@ -955,12 +986,12 @@ page.selectInBetween = (element, lastElement) => {
if (distance < 2)
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) ||
(thisIndex < lastIndex && i > thisIndex && i < lastIndex)) {
// Check or uncheck depending on the state of the initial checkbox
const checked = page.checkboxes[page.currentView][i].checked = lastElement.checked
const id = page.getItemID(page.checkboxes[page.currentView][i])
const checked = page.checkboxes[i].checked = lastElement.checked
const id = page.getItemID(page.checkboxes[i])
if (!page.selected[page.currentView].includes(id) && checked)
page.selected[page.currentView].push(id)
else if (page.selected[page.currentView].includes(id) && !checked)
@ -972,13 +1003,12 @@ page.select = (element, event) => {
const id = page.getItemID(element)
if (isNaN(id)) return
const lastSelected = page.lastSelected[page.currentView]
if (event.shiftKey && lastSelected) {
page.selectInBetween(element, lastSelected)
if (event.shiftKey && page.lastSelected) {
page.selectInBetween(element, page.lastSelected)
// Check or uncheck depending on the state of the initial checkbox
element.checked = lastSelected.checked
element.checked = page.lastSelected.checked
} else {
page.lastSelected[page.currentView] = element
page.lastSelected = element
}
if (!page.selected[page.currentView].includes(id) && element.checked)
@ -995,7 +1025,7 @@ page.select = (element, event) => {
page.clearSelection = () => {
const selected = page.selected[page.currentView]
const type = page.currentView === 'users' ? 'users' : 'uploads'
const type = page.getByView(page.currentView, 'type')
const count = selected.length
if (!count)
return swal('An error occurred!', `You have not selected any ${type}.`, 'error')
@ -1008,7 +1038,7 @@ page.clearSelection = () => {
}).then(proceed => {
if (!proceed) return
const checkboxes = page.checkboxes[page.currentView]
const checkboxes = page.checkboxes
for (let i = 0; i < checkboxes.length; i++)
if (checkboxes[i].checked)
checkboxes[i].checked = false
@ -1134,7 +1164,7 @@ page.filterUploads = element => {
}
page.viewUserUploads = (id, element) => {
const user = page.cache.users[id]
const user = page.cache[id]
if (!user) return
element.classList.add('is-loading')
// 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.postBulkDeleteUploads({
all: page.currentView === 'uploadsAll',
@ -1462,9 +1506,24 @@ page.addUploadsToAlbum = (ids, callback) => {
}
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')
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.data.success === false)
@ -1475,9 +1534,115 @@ page.getAlbums = (params = {}) => {
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>
<form class="prevent-default">
<div class="field">
@ -1504,14 +1669,22 @@ page.getAlbums = (params = {}) => {
</div>
</form>
<hr>
<h2 class="subtitle">List of albums</h2>
`
page.dom.innerHTML = `
${!params.all ? createNewAlbum : ''}
${pagination}
${extraControls}
${controls}
<div class="table-container">
<table class="table is-narrow is-fullwidth is-hoverable">
<thead>
<tr>
<th><input id="selectAll" class="checkbox" type="checkbox" title="Select all" data-action="select-all"></th>
<th>ID</th>
<th>Name</th>
<th>Files</th>
${params.all ? '<th>User</th>' : ''}
<th>Uploads</th>
<th>Created at</th>
<th>Public link</th>
<th></th>
@ -1521,38 +1694,54 @@ page.getAlbums = (params = {}) => {
</tbody>
</table>
</div>
${controls}
${bottomExtraControls}
${bottomPagination}
`
const homeDomain = response.data.homeDomain
const table = document.querySelector('#table')
for (let i = 0; i < response.data.albums.length; i++) {
const album = response.data.albums[i]
for (let i = 0; i < albums.length; i++) {
const album = albums[i]
const albumUrl = `${homeDomain}/a/${album.identifier}`
const selected = page.selected[page.currentView].includes(album.id)
if (!selected) unselected = true
// Prettify
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,
download: album.download,
public: album.public,
description: album.description
description: album.description,
enabled
}
const tr = document.createElement('tr')
tr.dataset.id = album.id
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.name}</th>
<th>${album.files}</th>
<th${enabled ? '' : ' class="has-text-grey"'}>${album.name}</td>
${params.all ? `<th>${album.userid ? (users[album.userid] || '') : ''}</th>` : ''}
<th>${album.uploads}</th>
<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}">
<a class="button is-small is-primary is-outlined" title="Edit album" data-action="edit-album">
<span class="icon is-small">
<i class="icon-pencil"></i>
</span>
</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'}>
<span class="icon is-small">
<i class="icon-clipboard"></i>
@ -1563,7 +1752,7 @@ page.getAlbums = (params = {}) => {
<i class="icon-download"></i>
</span>
</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">
<i class="icon-trash"></i>
</span>
@ -1572,9 +1761,21 @@ page.getAlbums = (params = {}) => {
`
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.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 => {
page.updateTrigger(params.trigger)
page.onAxiosError(error)
@ -1582,7 +1783,7 @@ page.getAlbums = (params = {}) => {
}
page.editAlbum = id => {
const album = page.cache.albums[id]
const album = page.cache[id]
if (!album) return
const div = document.createElement('div')
@ -1599,6 +1800,16 @@ page.editAlbum = id => {
</div>
<p class="help">Max length is ${page.albumDescMaxLength} characters.</p>
</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="control">
<label class="checkbox">
@ -1638,14 +1849,19 @@ page.editAlbum = id => {
}).then(value => {
if (!value) return
axios.post('api/albums/edit', {
const post = {
id,
name: document.querySelector('#swalName').value.trim(),
description: document.querySelector('#swalDescription').value.trim(),
download: document.querySelector('#swalDownload').checked,
public: document.querySelector('#swalPublic').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.data.success === false)
@ -1656,35 +1872,39 @@ page.editAlbum = id => {
}
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)
swal('Success!', `Your album was renamed to: ${response.data.name}.`, 'success')
swal('Success!', `The album was renamed to: ${response.data.name}.`, 'success')
else
swal('Success!', 'Your album was edited!', 'success', {
swal('Success!', 'The album was edited.', 'success', {
buttons: false,
timer: 1500
})
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)
})
}
page.deleteAlbum = id => {
page.disableAlbum = id => {
swal({
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',
dangerMode: true,
buttons: {
cancel: true,
confirm: {
text: 'Yes, delete it!',
text: 'Yes, disable it!',
closeModal: false
},
purge: {
text: 'Umm, delete the uploads too please?',
text: 'Umm, delete the uploads too, please?',
value: 'purge',
className: 'swal-button--danger',
closeModal: false
@ -1693,7 +1913,7 @@ page.deleteAlbum = id => {
}).then(proceed => {
if (!proceed) return
axios.post('api/albums/delete', {
axios.post('api/albums/disable', {
id,
purge: proceed === 'purge'
}).then(response => {
@ -1710,12 +1930,17 @@ page.deleteAlbum = id => {
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,
timer: 1500
})
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)
})
}
@ -1742,7 +1967,9 @@ page.submitAlbum = element => {
timer: 1500
})
page.getAlbumsSidebar()
page.getAlbums()
page.getAlbums({
pageNum: -1
})
}).catch(error => {
page.updateTrigger(element)
page.onAxiosError(error)
@ -1750,7 +1977,7 @@ page.submitAlbum = element => {
}
page.getAlbumsSidebar = () => {
axios.get('api/albums/sidebar').then(response => {
axios.get('api/albums', { headers: { sidebar: '1' } }).then(response => {
if (!response) return
if (response.data.success === false)
@ -1760,6 +1987,8 @@ page.getAlbumsSidebar = () => {
return swal('An error occurred!', response.data.description, 'error')
}
const albums = response.data.albums
const count = response.data.count
const albumsContainer = document.querySelector('#albumsContainer')
// Clear albums sidebar if necessary
@ -1770,16 +1999,16 @@ page.getAlbumsSidebar = () => {
albumsContainer.innerHTML = ''
}
if (response.data.albums === undefined)
if (albums === undefined)
return
for (let i = 0; i < response.data.albums.length; i++) {
const album = response.data.albums[i]
for (let i = 0; i < albums.length; i++) {
const album = albums[i]
const li = document.createElement('li')
const a = document.createElement('a')
a.id = album.id
a.innerHTML = album.name
a.className = 'is-relative'
a.innerHTML = album.name
a.addEventListener('click', event => {
page.getUploads({
@ -1792,6 +2021,23 @@ page.getAlbumsSidebar = () => {
li.appendChild(a)
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)
}
@ -1939,7 +2185,7 @@ page.getUsers = (params = {}) => {
page.updateTrigger(params.trigger, 'loading')
if (params.pageNum === undefined)
if (typeof params.pageNum !== 'number')
params.pageNum = 0
const url = `api/users/${params.pageNum}`
@ -1953,7 +2199,8 @@ page.getUsers = (params = {}) => {
}
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) {
params.pageNum = pages - 1
return page.getUsers(params)
@ -1963,7 +2210,7 @@ page.getUsers = (params = {}) => {
}
page.currentView = 'users'
page.cache.users = {}
page.cache = {}
if (params.pageNum < 0) params.pageNum = Math.max(0, pages + params.pageNum)
const pagination = page.paginate(response.data.count, 25, params.pageNum)
@ -1973,7 +2220,7 @@ page.getUsers = (params = {}) => {
<form class="prevent-default">
<div class="field has-addons">
<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 class="control">
<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>
</span>
</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">
<i class="icon-hammer"></i>
</span>
@ -2089,9 +2336,9 @@ page.getUsers = (params = {}) => {
const table = document.querySelector('#table')
for (let i = 0; i < response.data.users.length; i++) {
const user = response.data.users[i]
const selected = page.selected.users.includes(user.id)
for (let i = 0; i < users.length; i++) {
const user = users[i]
const selected = page.selected[page.currentView].includes(user.id)
if (!selected) unselected = true
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
const enabled = user.enabled !== false && user.enabled !== 0
page.cache.users[user.id] = {
page.cache[user.id] = {
username: user.username,
groups: user.groups,
enabled,
@ -2121,7 +2368,7 @@ page.getUsers = (params = {}) => {
tr.dataset.id = user.id
tr.innerHTML = `
<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>
<td>${page.getPrettyBytes(user.usage)}</td>
<td>${displayGroup}</td>
@ -2138,7 +2385,7 @@ page.getUsers = (params = {}) => {
<i class="icon-docs"></i>
</span>
</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">
<i class="icon-hammer"></i>
</span>
@ -2152,7 +2399,7 @@ page.getUsers = (params = {}) => {
`
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')
@ -2164,7 +2411,7 @@ page.getUsers = (params = {}) => {
page.fadeAndScroll()
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 => {
page.updateTrigger(params.trigger)
page.onAxiosError(error)
@ -2252,7 +2499,7 @@ page.createUser = () => {
}
page.editUser = id => {
const user = page.cache.users[id]
const user = page.cache[id]
if (!user) return
const groupOptions = Object.keys(page.permissions).map((g, i, a) => {
@ -2366,12 +2613,12 @@ page.editUser = id => {
}
page.disableUser = id => {
const user = page.cache.users[id]
const user = page.cache[id]
if (!user || !user.enabled) return
const content = document.createElement('div')
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>
`
@ -2399,7 +2646,7 @@ page.disableUser = id => {
else
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,
timer: 1500
})
@ -2409,12 +2656,12 @@ page.disableUser = id => {
}
page.deleteUser = id => {
const user = page.cache.users[id]
const user = page.cache[id]
if (!user) return
const content = document.createElement('div')
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>
`
@ -2458,7 +2705,7 @@ page.deleteUser = id => {
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,
timer: 1500
})
@ -2647,7 +2894,7 @@ window.onload = () => {
if (!('ontouchstart' in document.documentElement))
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++) {
const ls = localStorage[lsKeys.selected[selectedKeys[i]]]
if (ls) page.selected[selectedKeys[i]] = JSON.parse(ls)

View File

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

View File

@ -54,7 +54,7 @@
<h2 class='subtitle is-brighter'>What information do we know about you?</h2>
<article class="message">
<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.
</div>
</article>
@ -124,22 +124,34 @@
<td>Personalization</td>
<td>LocalStorage</td>
</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>
<th>selectedUploads</th>
<td>{{ globals.root_domain }}</td>
<td>Necessary</td>
<td>Personalization</td>
<td>LocalStorage</td>
</tr>
<tr>
<th>selectedUploadsAll</th>
<td>{{ globals.root_domain }}</td>
<td>Necessary</td>
<td>Personalization</td>
<td>LocalStorage</td>
</tr>
<tr>
<th>selectedUsers</th>
<td>{{ globals.root_domain }}</td>
<td>Necessary</td>
<td>Personalization</td>
<td>LocalStorage</td>
</tr>
<tr>
@ -175,18 +187,21 @@
<tr>
<th>viewTypeUploads</th>
<td>{{ globals.root_domain }}</td>
<td>Necessary</td>
<td>Personalization</td>
<td>LocalStorage</td>
</tr>
<tr>
<th>viewTypeUploadsAll</th>
<td>{{ globals.root_domain }}</td>
<td>Necessary</td>
<td>Personalization</td>
<td>LocalStorage</td>
</tr>
</tbody>
</table>
</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>
</article>

View File

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

View File

@ -275,7 +275,7 @@
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>
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>
</article>