Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for build-in themes and add a Klipper theme #1859

Merged
merged 9 commits into from
Jul 6, 2024
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 @@ -192,12 +193,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
Loading