diff --git a/public/config.json b/public/config.json index 9dd1d5a8d..d28bd65eb 100644 --- a/public/config.json +++ b/public/config.json @@ -1,6 +1,7 @@ { "defaultLocale": "en", - "defaultTheme": "dark", + "defaultMode": "dark", + "defaultTheme": "mainsail", "hostname": null, "port": null, "path": null, diff --git a/public/img/themes/sidebarLogo-klipper.svg b/public/img/themes/sidebarLogo-klipper.svg new file mode 100644 index 000000000..64cdbb9d1 --- /dev/null +++ b/public/img/themes/sidebarLogo-klipper.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index 1200ebd1d..40b2bc9aa 100644 --- a/src/App.vue +++ b/src/App.vue @@ -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 } @@ -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 @@ -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 { @@ -226,8 +222,8 @@ 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 @@ -235,73 +231,105 @@ export default class App extends Mixins(BaseMixin, ThemeMixin) { doc.className = dark ? 'theme--dark' : 'theme--light' } - drawFavicon(val: number): void { + async drawFavicon(val: number): Promise { 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(` - - - - - - - - `) - - 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(` + + + + + + + + `) + + favicon16.href = favicon + favicon32.href = favicon } @Watch('customFavicons') diff --git a/src/components/TheTopbar.vue b/src/components/TheTopbar.vue index ac4cdfb40..354a99fd8 100644 --- a/src/components/TheTopbar.vue +++ b/src/components/TheTopbar.vue @@ -3,7 +3,7 @@ - + Logo @@ -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 @@ -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 @@ -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 { diff --git a/src/components/mixins/theme.ts b/src/components/mixins/theme.ts index fee4bfb5b..b3f612a8a 100644 --- a/src/components/mixins/theme.ts +++ b/src/components/mixins/theme.ts @@ -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) } @@ -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` + } } diff --git a/src/components/settings/SettingsUiSettingsTab.vue b/src/components/settings/SettingsUiSettingsTab.vue index cef4286d9..dcba86ff4 100644 --- a/src/components/settings/SettingsUiSettingsTab.vue +++ b/src/components/settings/SettingsUiSettingsTab.vue @@ -2,6 +2,12 @@
+ + + + @@ -276,34 +282,42 @@ diff --git a/src/locales/en.json b/src/locales/en.json index 50ba5a85f..69b336d68 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1221,6 +1221,8 @@ "Logo": "Logo", "ManualProbeDialog": "Manual Probe Helper Dialog", "ManualProbeDialogDescription": "Display helper dialog for PROBE_CALIBRATE or Z_ENDSTOP_CALIBRATE.", + "Mode": "Mode", + "ModeDescription": "Change the overall look and feel of the application.", "NavigationStyle": "Navigation style", "NavigationStyleDescription": "Change navigation appearance", "NavigationStyleIconsAndText": "Icons + Text", @@ -1238,7 +1240,7 @@ "TempchartHeightDescription": "Modify the height of the temperature chart on the Dashboard.", "Theme": "Theme", "ThemeDark": "Dark", - "ThemeDescription": "Change the overall look and feel of the application", + "ThemeDescription": "Customizes the branding of the interface.", "ThemeLight": "Light", "UiSettings": "UI-Settings" }, diff --git a/src/main.ts b/src/main.ts index 13127500a..7cff4da1c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -35,7 +35,7 @@ import { DatasetComponent, GridComponent, LegendComponent, TooltipComponent } fr import 'vue-resize/dist/vue-resize.css' // @ts-ignore import VueResize from 'vue-resize' -import { defaultTheme } from './store/variables' +import { defaultMode } from './store/variables' Vue.config.productionTip = false @@ -80,9 +80,9 @@ const initLoad = async () => { await setAndLoadLocale(file.defaultLocale as string) } - // Handle theme outside of store init and before vue mount for consistency in dialog - const theme = file.defaultTheme ?? defaultTheme - vuetify.framework.theme.dark = theme !== 'light' + // Handle mode outside store init and before vue mount for consistency in dialog + const mode = file.defaultMode ?? defaultMode + vuetify.framework.theme.dark = mode !== 'light' } catch (e) { window.console.error('Failed to load config.json') window.console.error(e) diff --git a/src/store/gui/getters.ts b/src/store/gui/getters.ts index bdd6c980a..5975dab59 100644 --- a/src/store/gui/getters.ts +++ b/src/store/gui/getters.ts @@ -1,10 +1,24 @@ import { GetterTree } from 'vuex' import { GuiState } from '@/store/gui/types' import { GuiMacrosStateMacrogroup } from '@/store/gui/macros/types' -import { allDashboardPanels } from '@/store/variables' +import { allDashboardPanels, defaultTheme, themes } from '@/store/variables' +import { Theme } from '@/store/types' // eslint-disable-next-line export const getters: GetterTree = { + theme: (state): string => { + const theme = state.uiSettings.theme + + // return defaultTheme, if theme doesnt exists + if (themes.findIndex((tmp: Theme) => tmp.name === theme) === -1) return defaultTheme + + return theme + }, + + getTheme: (state, getters): Theme => { + return themes.find((theme: Theme) => theme.name === getters.theme) ?? themes[0] + }, + getDatasetValue: (state) => (payload: { name: string; type: string }) => { if ( payload.name in state.view.tempchart.datasetSettings && diff --git a/src/store/gui/index.ts b/src/store/gui/index.ts index 5241a333a..6f94a5344 100644 --- a/src/store/gui/index.ts +++ b/src/store/gui/index.ts @@ -3,7 +3,13 @@ import { Module } from 'vuex' import { actions } from '@/store/gui/actions' import { mutations } from '@/store/gui/mutations' import { getters } from '@/store/gui/getters' -import { defaultTheme, defaultLogoColor, defaultPrimaryColor, defaultBigThumbnailBackground } from '@/store/variables' +import { + defaultTheme, + defaultLogoColor, + defaultPrimaryColor, + defaultBigThumbnailBackground, + defaultMode, +} from '@/store/variables' // load modules import { console } from '@/store/gui/console' @@ -150,6 +156,7 @@ export const getDefaultState = (): GuiState => { entries: [], }, uiSettings: { + mode: defaultMode, theme: defaultTheme, logo: defaultLogoColor, primary: defaultPrimaryColor, diff --git a/src/store/gui/types.ts b/src/store/gui/types.ts index d54ab0203..a1bef68e2 100644 --- a/src/store/gui/types.ts +++ b/src/store/gui/types.ts @@ -98,7 +98,8 @@ export interface GuiState { presets?: GuiPresetsState remoteprinters?: GuiRemoteprintersState uiSettings: { - theme: 'dark' | 'light' + mode: 'dark' | 'light' + theme: string logo: string primary: string displayCancelPrint: boolean diff --git a/src/store/types.ts b/src/store/types.ts index 8efa504a2..f0aeea56b 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -38,3 +38,22 @@ export interface ConfigJsonInstance { port?: number path?: string } + +export interface Theme { + name: string + displayName: string + colorLogo: string + colorPrimary?: string + logo?: { + show: boolean + light: boolean + } + sidebarBackground?: { + show: boolean + light: boolean + } + mainBackground?: { + show: boolean + light: boolean + } +} diff --git a/src/store/variables.ts b/src/store/variables.ts index 4db839504..3f4a91c4a 100644 --- a/src/store/variables.ts +++ b/src/store/variables.ts @@ -1,4 +1,7 @@ -export const defaultTheme = 'dark' +import { Theme } from '@/store/types' + +export const defaultMode = 'dark' +export const defaultTheme = 'mainsail' export const defaultLogoColor = '#D41216' export const defaultPrimaryColor = '#2196f3' export const defaultBigThumbnailBackground = '#1e1e1e' @@ -140,3 +143,16 @@ export const genericLogfiles = ['klippy', 'moonraker', 'crowsnest', 'mmu', 'sona * List of all rollover logfiles */ export const rolloverLogfiles = ['klipper', 'moonraker'] + +/* + * List of all Themes + */ +export const themes: Theme[] = [ + { name: 'mainsail', displayName: 'Mainsail', colorLogo: defaultLogoColor }, + { + name: 'klipper', + displayName: 'Klipper', + colorLogo: '#b12f35', + logo: { show: true, light: false }, + }, +]