Skip to content

Commit

Permalink
feat: add support for build-in themes and add a Klipper theme (#1859)
Browse files Browse the repository at this point in the history
  • Loading branch information
meteyou authored Jul 6, 2024
1 parent bee05b3 commit 6fd3251
Show file tree
Hide file tree
Showing 13 changed files with 280 additions and 96 deletions.
3 changes: 2 additions & 1 deletion public/config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"defaultLocale": "en",
"defaultTheme": "dark",
"defaultMode": "dark",
"defaultTheme": "mainsail",
"hostname": null,
"port": null,
"path": null,
Expand Down
15 changes: 15 additions & 0 deletions public/img/themes/sidebarLogo-klipper.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
172 changes: 100 additions & 72 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,6 @@ export default class App extends Mixins(BaseMixin, ThemeMixin) {
return this.$store.getters['getTitle']
}
get mainBackground(): string {
return this.$store.getters['files/getMainBackground']
}
get naviDrawer(): boolean {
return this.$store.state.naviDrawer
}
Expand All @@ -98,8 +94,8 @@ export default class App extends Mixins(BaseMixin, ThemeMixin) {
paddingLeft: '0',
}
if (this.mainBackground !== null) {
style.backgroundImage = 'url(' + this.mainBackground + ')'
if (this.mainBgImage !== null) {
style.backgroundImage = 'url(' + this.mainBgImage + ')'
}
// overwrite padding left for the sidebar
Expand Down Expand Up @@ -127,8 +123,8 @@ export default class App extends Mixins(BaseMixin, ThemeMixin) {
return this.$store.state.printer.print_stats?.filename ?? ''
}
get theme(): string {
return this.$store.state.gui.uiSettings.theme
get mode(): string {
return this.$store.state.gui.uiSettings.mode
}
get logoColor(): string {
Expand Down Expand Up @@ -226,82 +222,114 @@ export default class App extends Mixins(BaseMixin, ThemeMixin) {
})
}
@Watch('theme')
themeChanged(newVal: string): void {
@Watch('mode')
modeChanged(newVal: string): void {
const dark = newVal !== 'light'
this.$vuetify.theme.dark = dark
const doc = document.documentElement
doc.className = dark ? 'theme--dark' : 'theme--light'
}
drawFavicon(val: number): void {
async drawFavicon(val: number): Promise<void> {
const favicon16: HTMLLinkElement | null = document.querySelector("link[rel*='icon'][sizes='16x16']")
const favicon32: HTMLLinkElement | null = document.querySelector("link[rel*='icon'][sizes='32x32']")
if (favicon16 && favicon32) {
if (this.progressAsFavicon && this.printerIsPrinting) {
let faviconSize = 64
let canvas = document.createElement('canvas')
canvas.width = faviconSize
canvas.height = faviconSize
const context = canvas.getContext('2d')
const centerX = canvas.width / 2
const centerY = canvas.height / 2
const radius = 32
// draw the grey circle
if (context) {
context.beginPath()
context.moveTo(centerX, centerY)
context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false)
context.closePath()
context.fillStyle = '#ddd'
context.fill()
context.strokeStyle = 'rgba(200, 208, 218, 0.66)'
context.stroke()
// draw the green circle based on percentage
let startAngle = 1.5 * Math.PI
let endAngle = 0
let unitValue = (Math.PI - 0.5 * Math.PI) / 25
if (val >= 0 && val <= 25) endAngle = startAngle + val * unitValue
else if (val > 25 && val <= 50) endAngle = startAngle + val * unitValue
else if (val > 50 && val <= 75) endAngle = startAngle + val * unitValue
else if (val > 75 && val <= 100) endAngle = startAngle + val * unitValue
context.beginPath()
context.moveTo(centerX, centerY)
context.arc(centerX, centerY, radius, startAngle, endAngle, false)
context.closePath()
context.fillStyle = this.logoColor
context.fill()
favicon16.href = canvas.toDataURL('image/png')
favicon32.href = canvas.toDataURL('image/png')
}
} else if (this.customFavicons) {
const [favicon16Path, favicon32Path] = this.customFavicons
favicon16.href = favicon16Path
favicon32.href = favicon32Path
} else {
const favicon =
'data:image/svg+xml;base64,' +
window.btoa(`
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 599.38 523.11" xml:space="preserve">
<g>
<path style="fill:${this.logoColor};" d="M382.29,142.98L132.98,522.82L0,522.68L344.3,0l0,0C352.18,49.06,365.2,97.68,382.29,142.98"/>
<path style="fill:${this.logoColor};" d="M413.28,213.54L208.5,522.92l132.94,0.19l135.03-206.33l0,0C452.69,284.29,431.53,249.77,413.28,213.54 L413.28,213.54"/>
<path style="fill:${this.logoColor};" d="M599.38,447.69l-49.25,75.42L417,522.82l101.6-153.67l0,0C543.48,397.35,570.49,423.61,599.38,447.69 L599.38,447.69z"/>
</g>
</svg>
`)
favicon16.href = favicon
favicon32.href = favicon
// if no favicon is found, stop
if (!favicon16 || !favicon32) return
// if progressAsFavicon is enabled and the printer is printing, draw the progress as favicon
if (this.progressAsFavicon && this.printerIsPrinting) {
let faviconSize = 64
let canvas = document.createElement('canvas')
canvas.width = faviconSize
canvas.height = faviconSize
const context = canvas.getContext('2d')
const centerX = canvas.width / 2
const centerY = canvas.height / 2
const radius = 32
if (!context) return
// draw the grey circle
context.beginPath()
context.moveTo(centerX, centerY)
context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false)
context.closePath()
context.fillStyle = '#ddd'
context.fill()
context.strokeStyle = 'rgba(200, 208, 218, 0.66)'
context.stroke()
// draw the green circle based on percentage
let startAngle = 1.5 * Math.PI
let endAngle = 0
let unitValue = (Math.PI - 0.5 * Math.PI) / 25
if (val >= 0 && val <= 25) endAngle = startAngle + val * unitValue
else if (val > 25 && val <= 50) endAngle = startAngle + val * unitValue
else if (val > 50 && val <= 75) endAngle = startAngle + val * unitValue
else if (val > 75 && val <= 100) endAngle = startAngle + val * unitValue
context.beginPath()
context.moveTo(centerX, centerY)
context.arc(centerX, centerY, radius, startAngle, endAngle, false)
context.closePath()
context.fillStyle = this.logoColor
context.fill()
favicon16.href = canvas.toDataURL('image/png')
favicon32.href = canvas.toDataURL('image/png')
return
}
// if custom favicons are set, use them
if (this.customFavicons) {
const [favicon16Path, favicon32Path] = this.customFavicons
favicon16.href = favicon16Path
favicon32.href = favicon32Path
return
}
// if a theme sidebar logo is set, use it
if ((this.theme?.logo?.show ?? false) && this.sidebarLogo.endsWith('.svg')) {
const response = await fetch(this.sidebarLogo)
if (!response.ok) return
const text = await response.text()
const modifiedSvg = text.replace(/fill="var\(--color-logo, #[0-9a-fA-F]{6}\)"/g, `fill="${this.logoColor}"`)
const blob = new Blob([modifiedSvg], { type: 'image/svg+xml' })
const reader = new FileReader()
reader.onloadend = () => {
const base64data = reader.result as string
favicon16.href = base64data
favicon32.href = base64data
}
reader.readAsDataURL(blob)
return
}
// if no custom favicon is set, use the default one
const favicon =
'data:image/svg+xml;base64,' +
window.btoa(`
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 599.38 523.11" xml:space="preserve">
<g>
<path style="fill:${this.logoColor};" d="M382.29,142.98L132.98,522.82L0,522.68L344.3,0l0,0C352.18,49.06,365.2,97.68,382.29,142.98"/>
<path style="fill:${this.logoColor};" d="M413.28,213.54L208.5,522.92l132.94,0.19l135.03-206.33l0,0C452.69,284.29,431.53,249.77,413.28,213.54 L413.28,213.54"/>
<path style="fill:${this.logoColor};" d="M599.38,447.69l-49.25,75.42L417,522.82l101.6-153.67l0,0C543.48,397.35,570.49,423.61,599.38,447.69 L599.38,447.69z"/>
</g>
</svg>
`)
favicon16.href = favicon
favicon32.href = favicon
}
@Watch('customFavicons')
Expand Down
11 changes: 4 additions & 7 deletions src/components/TheTopbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<v-app-bar app elevate-on-scroll :height="topbarHeight" class="topbar pa-0" clipped-left>
<v-app-bar-nav-icon tile @click.stop="naviDrawer = !naviDrawer" />
<router-link to="/">
<inline-svg v-if="sidebarLogo && isSvgLogo" :src="'http:' + sidebarLogo" :class="logoClasses" />
<inline-svg v-if="sidebarLogo && isSvgLogo" :src="sidebarLogo" :class="logoClasses" />
<img v-else-if="sidebarLogo" :src="sidebarLogo" :class="logoClasses" alt="Logo" />
<mainsail-logo v-else :color="logoColor" :class="logoClasses" router to="/" :ripple="false" />
</router-link>
Expand Down Expand Up @@ -90,6 +90,7 @@ import { topbarHeight } from '@/store/variables'
import { mdiAlertOctagonOutline, mdiContentSave, mdiFileUpload, mdiClose, mdiCloseThick } from '@mdi/js'
import EmergencyStopDialog from '@/components/dialogs/EmergencyStopDialog.vue'
import InlineSvg from 'vue-inline-svg'
import ThemeMixin from '@/components/mixins/theme'
type uploadSnackbar = {
status: boolean
Expand All @@ -112,7 +113,7 @@ type uploadSnackbar = {
TheNotificationMenu,
},
})
export default class TheTopbar extends Mixins(BaseMixin) {
export default class TheTopbar extends Mixins(BaseMixin, ThemeMixin) {
mdiAlertOctagonOutline = mdiAlertOctagonOutline
mdiContentSave = mdiContentSave
mdiFileUpload = mdiFileUpload
Expand Down Expand Up @@ -188,12 +189,8 @@ export default class TheTopbar extends Mixins(BaseMixin) {
return this.$store.state.gui.uiSettings.boolHideUploadAndPrintButton ?? false
}
get sidebarLogo(): string {
return this.$store.getters['files/getSidebarLogo']
}
get isSvgLogo() {
return this.sidebarLogo.includes('.svg?timestamp=')
return this.sidebarLogo.includes('.svg?timestamp=') || this.sidebarLogo.endsWith('.svg')
}
get logoColor(): string {
Expand Down
46 changes: 46 additions & 0 deletions src/components/mixins/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ export default class ThemeMixin extends Vue {
return this.fgColor(alpha, !this.$vuetify.theme.dark)
}

get themeName() {
return this.$store.getters['gui/theme']
}

get theme() {
return this.$store.getters['gui/getTheme']
}

get themeMode() {
return this.$store.state.gui.uiSettings.mode ?? 'dark'
}

get fgColorHi() {
return this.fgColor(0.8)
}
Expand Down Expand Up @@ -42,6 +54,40 @@ export default class ThemeMixin extends Vue {
}

get sidebarBgImage() {
if (this.theme.sidebarBackground?.show) {
if (this.theme.sidebarBackground?.light && this.themeMode === 'light')
return `/img/themes/sidebarBackground-${this.themeName}-light.png`

return `/img/themes/sidebarBackground-${this.themeName}.png`
}

return this.$vuetify.theme.dark ? '/img/sidebar-background.svg' : '/img/sidebar-background-light.svg'
}

get sidebarLogo(): string {
const url = this.$store.getters['files/getSidebarLogo']
if (url !== '' || this.themeName === 'mainsail') return url

// if no theme is set, return empty string to load the default logo
if (!(this.theme.logo?.show ?? false)) return ''

// return light logo if theme is light and sidebarLogo is set to both
if (this.theme.logo?.light && this.themeMode === 'light')
return `/img/themes/sidebarLogo-${this.themeName}-light.svg`

// return dark/generic theme logo
return `/img/themes/sidebarLogo-${this.themeName}.svg`
}

get mainBgImage() {
const url = this.$store.getters['files/getMainBackground']
if (url || this.themeName === 'mainsail') return url

if (!this.theme.mainBackground?.show) return null

if (this.theme.mainBackground?.light && this.themeMode === 'light')
return `/img/themes/mainBackground-${this.themeName}-light.png`

return `/img/themes/mainBackground-${this.themeName}.png`
}
}
Loading

0 comments on commit 6fd3251

Please sign in to comment.