Files

291 lines
11 KiB
JavaScript
Raw Permalink Normal View History

2026-05-24 21:58:23 -07:00
(() => {
// Support Functions
const trimDataURL = (s, x = undefined) => s.slice(s.indexOf(',') + 1, x)
const redirectsToString = s => s.sort().join()
function PrepareTemplate(selector) {
const elem = $(selector)
elem.removeAttribute('hidden')
elem.removeAttribute('id')
elem.remove()
return elem
}
// No Applications Warning
let appCount = 0
function DisplayNoApps(newCount) {
_ActionCreate.removeAttribute('hidden')
appCount = newCount
appCount === 0
? _AlertEmpty.removeAttribute('hidden')
: _AlertEmpty.setAttribute('hidden', true)
}
// Displays Modal with Secret Key
function DisplaySecret(secretKey) {
CreateModal({
behaviour: {
title: 'Your Secret Key',
tooltip: 'Do not share this key with anyone!',
submitText: 'Close',
cancelText: '',
},
_secret: {
type: 'text',
title: '',
value: secretKey,
readonly: true,
}
})
}
// Use Application Template
const
_AlertEmpty = $('#alert-empty'),
_AlertLoad = $('#alert-load'),
_ActionCreate = $('#action-create'),
_Collection = $('#application-collection'),
_LinkTemplate = PrepareTemplate('.input-redirect'),
_ItemTemplate = PrepareTemplate('#application-template')
function DisplayApplication(a) {
let removingIcon = false
let unsavedChanges = false
const
item = _ItemTemplate.cloneNode(true),
elemStatus = $$(item, '#status-message'),
elemOptions = $$(item, '.application-options'),
elemRedirects = $$(item, '#container-redirects'),
previewIcon = $$(item, '#app-icon'),
previewName = $$(item, '#app-name'),
previewDesc = $$(item, '#app-desc'),
inputName = $$(item, '#input-name'),
inputDesc = $$(item, '#input-desc'),
inputIcon = $$(item, '#input-icon'),
inputIconFile = $$(item, '#input-icon-file'),
inputIconRemove = $$(item, '#input-icon-remove'),
inputRedirectNew = $$(item, '#input-redirect-new'),
inputSave = $$(item, '#input-save'),
inputDiscard = $$(item, '#input-discard'),
inputResetSecret = $$(item, '#input-reset'),
inputDelete = $$(item, '#input-delete'),
inputEdit = $$(item, '#input-edit')
function CreateRedirect(givenURL) {
const elem = _LinkTemplate.cloneNode(true)
const inputUrl = elem.firstChild
inputUrl.value = givenURL
inputUrl.addEventListener('change', UpdateForm)
elem.lastChild.addEventListener('click', () => {
elem.remove()
UpdateForm()
})
elemRedirects.appendChild(elem)
}
function ResetForm() {
removingIcon = false
previewName.textContent = a.name?.trim() || 'Untitled'
previewDesc.textContent = a.description?.trim() || 'No Description Provided.'
previewIcon.src = ImageURL('icons', a.id, a.icon, false, 128)
previewIcon.alt = `Icon for ${a.name}`
inputIconFile.value = null
inputDesc.value = a.description
inputName.value = a.name
elemRedirects.innerHTML = ''
a.redirects.forEach(CreateRedirect)
UpdateForm()
}
function UpdateForm() {
// New Redirect
elemRedirects.childElementCount >= 10
? inputRedirectNew.setAttribute('hidden', true)
: inputRedirectNew.removeAttribute('hidden')
// Remove Icon
!removingIcon && (a.icon !== null || inputIconFile.files.length > 0)
? inputIconRemove.removeAttribute('disabled')
: inputIconRemove.setAttribute('disabled', true)
// Save/Discard Changes
unsavedChanges =
Object.keys(CollectBody()).length &&
Array.from(elemRedirects.childNodes).every(e => e.firstChild.checkValidity())
if (unsavedChanges) {
inputDiscard.removeAttribute('disabled')
inputSave.removeAttribute('disabled')
} else {
inputDiscard.setAttribute('disabled', true)
inputSave.setAttribute('disabled', true)
}
}
function CollectBody() {
const body = {}
if ((inputName.value.trim() || a.name) !== a.name)
body['name'] = inputName.value
if (inputDesc.value.trim() !== (a.description || ''))
body['description'] = inputDesc.value
if (removingIcon && body.icon)
body['icon'] = ''
if (inputIconFile.files.length)
body['icon'] = trimDataURL(previewIcon.src)
const collectedRedirects = Array
.from(elemRedirects.childNodes)
.filter(e => e.firstChild.checkValidity())
.map(e => e.firstChild.value)
if (redirectsToString(collectedRedirects) !== redirectsToString(a.redirects)) {
body.redirects = collectedRedirects
}
return body
}
// Set/Remove Application Icon
inputIconRemove.addEventListener('click', () => {
previewIcon.src = ImageURL('icon', a.id, null)
inputIconFile.value = null
removingIcon = true
UpdateForm()
})
inputIconFile.addEventListener('change', ev => {
const file = ev.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = e => {
previewIcon.src = e.target.result
removingIcon = false
UpdateForm()
}
reader.readAsDataURL(file)
})
// Field Updating
inputName.addEventListener('input', () => previewName.textContent = inputName.value.trim() || a.name)
inputDesc.addEventListener('input', () => previewDesc.textContent = inputDesc.value.trim() || a.description)
for (const needsListener of [inputName, inputDesc]) {
needsListener.addEventListener('change', UpdateForm)
}
// Button Listeners
inputRedirectNew.addEventListener('click',
() => CreateRedirect('')
)
inputDiscard.addEventListener('click',
() => ResetForm()
)
inputIcon.addEventListener('click',
() => inputIconFile.click()
)
inputEdit.addEventListener('click', () => {
elemOptions.classList.toggle('options-hidden')
ResetForm()
})
inputSave.addEventListener('click', () => {
if (!unsavedChanges) return
const unlock = LockInteraction()
elemStatus.textContent = 'Saving Changes...'
SendRequest('PATCH', `/api/v1/users/@me/applications/${a.id}`, CollectBody(), 1)
.then(([_, patchedBody]) => {
elemStatus.textContent = 'Changes Saved!'
elemStatus.classList.remove('text-error')
a = patchedBody
unlock()
ResetForm()
})
.catch(e => {
elemStatus.textContent = e
elemStatus.classList.add('text-error')
unlock()
})
// we dont use finally block here because race condition :p
})
inputResetSecret.addEventListener('click', () => CreateModal({
behaviour: {
title: 'Reset Secret Key?',
tooltip: `${a.name} will stop functioning until you update the secret key in your code.`,
submitText: 'Reset',
onSubmit: () => new Promise(ok => {
SendRequest('DELETE', `/api/v1/users/@me/applications/${a.id}/reset`, undefined, 2)
.then(([_, body]) => {
DisplaySecret(body.secret_key)
ok()
})
.catch(ok)
})
}
}))
inputDelete.addEventListener('click', () => CreateModal({
behaviour: {
title: 'Delete Application?',
tooltip: `${a.name} will be deleted and all connections will be severed. This action cannot be undone.`,
submitText: 'Delete',
onSubmit: () => new Promise(ok => {
SendRequest('DELETE', `/api/v1/users/@me/applications/${a.id}`, undefined, 2)
.then(() => {
item.remove()
DisplayNoApps(appCount - 1)
ok()
})
.catch(ok)
})
}
}))
// Append to top
// Official Application?
if ((a.flags & 1 << 3) !== 0) {
$$(item, '#verified-message').removeAttribute('hidden')
}
$$(item, '#input-clientid').textContent += a.id
ResetForm()
_Collection.appendChild(item)
}
// Fetch Connections
SendRequest('GET', '/api/v1/users/@me/applications', null, 1)
.then(([_, body]) => {
// Display Applications
body
.sort((a, b) => new Date(b.created) - new Date(a.created))
.forEach(DisplayApplication)
DisplayNoApps(body.length)
// Bind Create Application
_ActionCreate.addEventListener('click', function ActionCreate() {
CreateModal({
behaviour: {
title: 'Create Application',
tooltip: 'Name it something cool like Phase Connect.',
onSubmit: ({ name }) => new Promise(ok => {
SendRequest('POST', '/api/v1/users/@me/applications', { name }, 1)
.then(([_, body]) => {
DisplaySecret(body.secret_key)
DisplayNoApps(appCount + 1)
DisplayApplication(body)
ok()
})
.catch(ok)
})
},
_divider: { type: 'divider-line' },
name: {
type: 'text',
required: true,
title: 'Name',
validator: function ValidateName(v) {
if (v.length > 24) return 'Application name cannot be longer than 32 characters'
if (v.length < 1) return 'Application name cannot be shorter than 1 character'
return true
}
},
})
})
})
.catch(e => {
// Display Error Message
$('#alert-error').removeAttribute('hidden')
$('#error-message').textContent = e
console.log(e)
})
.finally(() => {
// Hide Loading Symbol
_AlertLoad.setAttribute('hidden', true)
})
})()