dashie/js/app.mjs

303 lines
8.4 KiB
JavaScript

// import * as YAML from 'yaml'
import jsYaml from '/js/js-yaml.mjs'
class App {
constructor() {//{{{
window._app = this
this.theme = ''
this.themes = { default: Theme.default() }
this.sections = []
this.retrieveVersion()
this.retrieveThemes()
.then(()=>this.retrieveConfig())
.then(config=>this.parseConfig(config))
.then(()=>this.setTheme(this.theme))
.then(()=>this.renderPage())
.catch(alert)
}//}}}
async retrieveThemes() {//{{{
return fetch(`/themes.yaml`)
.then(res=>{
if(res.ok)
return res.text()
throw `Error when fetching /theme.yaml: ${res.status} ${res.statusText}`
})
.then(text=>{
let themes = jsYaml.load(text)
Object.keys(themes).forEach(themeName=>{
this.themes[themeName] = new Theme(themeName, themes[themeName])
})
})
}//}}}
retrieveVersion() {//{{{
return fetch('/VERSION', {cache: "no-store"})
.then(res=>{
if(res.ok)
return res.text()
throw `Error when fetching /${page}.yaml: ${res.status} ${res.statusText}`
})
.then(version=>{
this.version = version
document.querySelector('footer').innerHTML = `Dashie ${version}`
})
}//}}}
retrieveConfig() {//{{{
const urlParams = new URLSearchParams(window.location.search);
let page = urlParams.get('page')
if(page === null)
page = 'start'
return fetch(`/${page}.yaml`, {cache: "no-store"})
.then(res=>{
if(res.ok)
return res.text()
throw `Error when fetching /${page}.yaml: ${res.status} ${res.statusText}`
})
.then(text=>{
return jsYaml.load(text)
})
}//}}}
parseConfig(config) {//{{{
// Pages to switch between
this.pages = config.pages ? config.pages : []
// Settings for this page.
this.page = config.page
// Theme to be used for the initial page render.
// Can be changed later.
this.theme = this.themes[config.page.theme ? config.page.theme : 'default']
if(this.theme === undefined)
throw new Error(`Theme '${config.page.theme}' isn't defined in themes.yaml.`)
// Sections are created; items are created within the Section objects.
if(config.sections === undefined)
config.sections = []
this.sections = config.sections.map((d,idx)=>new Section(idx, d))
}//}}}
renderPage() {//{{{
// Page header
document.querySelector('header').style.backgroundColor = this.theme.colors.page.header
// Pages to switch between
let pageStyle = `color: ${this.theme.colors.page_select.text}; background: ${this.theme.colors.page_select.background}`
let pages = this.pages.map(page=>`<div class="page" style="${pageStyle}" onclick="window.open('/?page=${page.name}', '_self')">${page.label}</div>`)
document.getElementById('pages').innerHTML = pages.join('')
// Page label
let pageLabel = document.getElementById('page-label')
pageLabel.innerHTML = this.page.label
pageLabel.style.color = this.theme.colors.page.text
// The theme selector is sorted by name for easier usage.
let themeOptions = Object.keys(this.themes)
.sort((a, b)=>{
if(a < b) return -1
if(a > b) return 1
return 0
})
.map(themeName=>`<option value="${themeName}" ${this.theme.name == themeName ? 'selected' : ''}>${themeName}</option>`)
.join('')
// Theme selector also has prev and next buttons to
// faster switch between themes (probably trying them out).
let t = this.theme
let themes = document.getElementById('theme')
let style = `color: ${t.colors.theme_select.text}; background: ${t.colors.theme_select.background}; border: 1px solid ${t.colors.theme_select.border}`
themes.innerHTML = `
<button style="${style}" onclick="window._app.previousTheme()">&lt;</button>
<select id="theme-select" onchange="window._app.selectTheme()" style="${style}">
${themeOptions}
</select>
<button style="${style}" onclick="window._app.nextTheme()">&gt;</button>
`
let sectionHTML = this.sections.map(s=>s.html())
document.getElementById('sections').innerHTML = sectionHTML.join('')
// Footer
let footer = document.querySelector('footer')
footer.style.display = t.colors.page.footer.show ? 'block' : 'none';
footer.style.color = t.colors.page.footer.text
footer.style.backgroundColor = t.colors.page.footer.background
}//}}}
setTheme(theme) {//{{{
this.theme = theme
document.body.style.color = theme.colors.page.text
document.body.style.background = theme.colors.page.background
let sectionClasses = document.getElementById('sections').classList
sectionClasses.remove('theme-brighter')
sectionClasses.remove('theme-darker')
if(theme.hover == 'brighter')
sectionClasses.add('theme-brighter')
else if(theme.hover == 'darker')
sectionClasses.add('theme-darker')
}//}}}
selectTheme() {//{{{
let option = document.querySelector('#theme option:checked')
this.setThemeByName(option.value)
// Focus the theme select box after page render, since
// user could be trying themes out and wanting to use the
// keyboard up/down.
document.querySelector('#theme select').focus()
}//}}}
setThemeByName(name) {//{{{
this.setTheme(this.themes[name])
this.renderPage()
}//}}}
previousTheme() {//{{{
let selected = document.querySelector('#theme option:checked')
if(!selected)
return
let prevOption = selected.previousElementSibling
if(prevOption) {
this.setThemeByName(prevOption.value)
}
}//}}}
nextTheme() {//{{{
let selected = document.querySelector('#theme option:checked')
if(!selected)
return
let nextOption = selected.nextElementSibling
if(nextOption) {
this.setThemeByName(nextOption.value)
}
}//}}}
open(url) {//{{{
window.open(url, '_blank')
}//}}}
}
class Theme {
static default() {//{{{
return new Theme('default', {
page: {
text: '#fff',
header: '#333',
background: '#fff',
footer: {
show: true,
text: "#aaa",
background: "#eee"
}
},
page_select: {
text: "#ccc",
background: "#666"
},
theme_select: {
text: "#ccc",
background: "#333",
border: "#888"
},
section: {
background: '#fff',
borders: [
['#fff', '#22a511']
]
},
item: {
label: '#333',
background: '#eaeaea',
description: '#555',
url: '#666',
icon: '#000',
hover: 'darker'
}
})
}//}}}
constructor(name, data) {//{{{
this.name = name
this.hover = data.item.hover
this.colors = data
}//}}}
colorNo(i) {//{{{
// Returns a wrapping color from the list of
let idx = i % this.colors.section.borders.length
return this.colors.section.borders[idx]
}//}}}
}
class Section {
constructor(idx, data) {//{{{
this.section_index = idx
this.label = 'Section'
this.icon = 'head-question'
this.items = []
Object.keys(data).forEach(key=>
this[key] = data[key]
)
}//}}}
html() {//{{{
let theme = window._app.theme
let colorIndex = this.color ? this.color : this.section_index
let color = theme.colorNo(colorIndex)
let itemHTML = this.items.map(itemData=>
new Item(this, itemData).html()
)
return `
<div class="section" style="border: 4px solid ${color[0]}; background: ${theme.colors.section.background}; box-shadow: ${theme.colors.section.shadow}">
<div class="name" style="background: ${color[0]}; color: ${color[1]}">${this.label}</div>
<div class="items" style="background: ${theme.colors.section.background}">
${itemHTML.join('')}
</div>
</div>
`
}//}}}
}
class Item {
constructor(section, data) {//{{{
this.section = section
this.label = 'Item'
this.icon = 'head-question'
this.url = 'https://example.com'
Object.keys(data).forEach(key=>
this[key] = data[key]
)
}//}}}
html() {//{{{
let theme = window._app.theme
let colorIndex = this.section.color ? this.section.color : this.section.section_index
let sectionColor = theme.colorNo(colorIndex)[0]
let colorLabel = theme.colors.item.label
if(!colorLabel)
colorLabel = sectionColor
let colorIcon = theme.colors.item.icon
if(!colorIcon)
colorIcon = sectionColor
let description = ''
if(this.description)
description = `<div class="description" style="color: ${theme.colors.item.description}">${this.description}</div>`
return `
<div class="item" style="background: ${theme.colors.item.background}" onclick="window._app.open('${this.url}')">
<div class="icon mdi-set mdi-${this.icon}" style="color: ${colorIcon}"></div>
<div class="label" style="color: ${colorLabel}">${this.label}</div>
${description}
<div class="url" style="color: ${theme.colors.item.url}">${this.url}</div>
</div>
`
}//}}}
}
let app = new App()
// vim: foldmethod=marker