From 5125332168c1ed563703a0b58af0c9f8cc6151cd Mon Sep 17 00:00:00 2001 From: Stefan Dej Date: Sun, 28 Apr 2024 11:11:37 +0200 Subject: [PATCH 1/8] feat: add support for build-in themes and add a Klipper theme Signed-off-by: Stefan Dej --- public/config.json | 3 +- public/img/themes/sidebarLogo-klipper.svg | 15 +++++ src/App.vue | 8 +-- src/components/TheTopbar.vue | 22 ++++++- src/components/mixins/theme.ts | 8 +++ .../settings/SettingsUiSettingsTab.vue | 60 ++++++++++++++++--- src/locales/en.json | 8 ++- src/main.ts | 8 +-- src/store/gui/getters.ts | 12 +++- src/store/gui/index.ts | 9 ++- src/store/gui/types.ts | 3 +- src/store/types.ts | 10 ++++ src/store/variables.ts | 13 +++- 13 files changed, 154 insertions(+), 25 deletions(-) create mode 100644 public/img/themes/sidebarLogo-klipper.svg 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..a5015cfdb 100644 --- a/src/App.vue +++ b/src/App.vue @@ -127,8 +127,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 +226,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 diff --git a/src/components/TheTopbar.vue b/src/components/TheTopbar.vue index 8fa913759..83ae7b337 100644 --- a/src/components/TheTopbar.vue +++ b/src/components/TheTopbar.vue @@ -76,7 +76,7 @@ diff --git a/src/locales/en.json b/src/locales/en.json index 963cb1362..f7fe3f9b5 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1152,6 +1152,7 @@ "BoolBigThumbnailDescription": "Display a large thumbnail in the status panel during a print.", "BoolHideUploadAndPrintButton": "Hide Upload and Print Button", "BoolHideUploadAndPrintButtonDescription": "Show or hide the \"Upload and Print\" button in the top bar.", + "CommunityTheme": "Community - {name}", "ConfirmOnCoolDown": "Require confirm on CoolDown", "ConfirmOnCoolDownDescription": "Show a confirmation dialog on CoolDown", "ConfirmOnEmergencyStop": "Require confirm on Emergency Stop", @@ -1181,6 +1182,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", @@ -1196,9 +1199,10 @@ "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": "Change the overall look and feel of the application.", "ThemeLight": "Light", - "UiSettings": "UI-Settings" + "UiSettings": "UI-Settings", + "VendorTheme": "Vendor - {name}" }, "Update": "update", "WebcamsTab": { diff --git a/src/main.ts b/src/main.ts index 269588c33..93ebf69b0 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 @@ -77,9 +77,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..9c28f1ad8 100644 --- a/src/store/gui/getters.ts +++ b/src/store/gui/getters.ts @@ -1,10 +1,20 @@ 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) => { + const theme = state.uiSettings.theme + + // return defaultTheme, if theme doesnt exists + if (themes.findIndex((tmp: Theme) => tmp.name === theme) === -1) return defaultTheme + + return theme + }, + 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 11c6cae55..db1338865 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' @@ -149,6 +155,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 1cd61ea6e..5b71c4814 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..cdfa37f14 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -38,3 +38,13 @@ export interface ConfigJsonInstance { port?: number path?: string } + +export interface Theme { + type: 'community' | 'vendor' + name: string + displayName: string + colorLogo?: string + colorPrimary?: string + sidebarLogo?: false | true | 'both' // both means that it also has a light version + sidebarBackground?: false | true | 'both' // both means that it also has a light version +} diff --git a/src/store/variables.ts b/src/store/variables.ts index f3676d781..d1c454a1b 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' @@ -139,3 +142,11 @@ 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[] = [ + { type: 'community', name: 'mainsail', displayName: 'Mainsail', colorLogo: defaultLogoColor }, + { type: 'community', name: 'klipper', displayName: 'Klipper', colorLogo: '#b12f35', sidebarLogo: true }, +] From 472957d2da42e1cd72fdb25b5bd44e66225c0a53 Mon Sep 17 00:00:00 2001 From: Stefan Dej Date: Sun, 28 Apr 2024 16:04:07 +0200 Subject: [PATCH 2/8] fix: remove community/vendor type in themes Signed-off-by: Stefan Dej --- src/components/settings/SettingsUiSettingsTab.vue | 10 +--------- src/locales/en.json | 4 +--- src/store/types.ts | 1 - src/store/variables.ts | 4 ++-- 4 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/components/settings/SettingsUiSettingsTab.vue b/src/components/settings/SettingsUiSettingsTab.vue index 91125c50c..143de9e91 100644 --- a/src/components/settings/SettingsUiSettingsTab.vue +++ b/src/components/settings/SettingsUiSettingsTab.vue @@ -323,16 +323,8 @@ export default class SettingsUiSettingsTab extends Mixins(BaseMixin, ThemeMixin) get themes() { return themes.map((theme) => { - let text = theme.displayName - - if (theme.type === 'community' && theme.name !== 'mainsail') - text = this.$t('Settings.UiSettingsTab.CommunityTheme', { name: theme.displayName }).toString() - - if (theme.type === 'vendor') - text = this.$t('Settings.UiSettingsTab.VendorTheme', { name: theme.displayName }).toString() - return { - text, + text: theme.displayName, value: theme.name, } }) diff --git a/src/locales/en.json b/src/locales/en.json index f7fe3f9b5..8c9ef596d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1152,7 +1152,6 @@ "BoolBigThumbnailDescription": "Display a large thumbnail in the status panel during a print.", "BoolHideUploadAndPrintButton": "Hide Upload and Print Button", "BoolHideUploadAndPrintButtonDescription": "Show or hide the \"Upload and Print\" button in the top bar.", - "CommunityTheme": "Community - {name}", "ConfirmOnCoolDown": "Require confirm on CoolDown", "ConfirmOnCoolDownDescription": "Show a confirmation dialog on CoolDown", "ConfirmOnEmergencyStop": "Require confirm on Emergency Stop", @@ -1201,8 +1200,7 @@ "ThemeDark": "Dark", "ThemeDescription": "Change the overall look and feel of the application.", "ThemeLight": "Light", - "UiSettings": "UI-Settings", - "VendorTheme": "Vendor - {name}" + "UiSettings": "UI-Settings" }, "Update": "update", "WebcamsTab": { diff --git a/src/store/types.ts b/src/store/types.ts index cdfa37f14..f464b8cb6 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -40,7 +40,6 @@ export interface ConfigJsonInstance { } export interface Theme { - type: 'community' | 'vendor' name: string displayName: string colorLogo?: string diff --git a/src/store/variables.ts b/src/store/variables.ts index d1c454a1b..11ee6d077 100644 --- a/src/store/variables.ts +++ b/src/store/variables.ts @@ -147,6 +147,6 @@ export const rolloverLogfiles = ['klipper', 'moonraker'] * List of all Themes */ export const themes: Theme[] = [ - { type: 'community', name: 'mainsail', displayName: 'Mainsail', colorLogo: defaultLogoColor }, - { type: 'community', name: 'klipper', displayName: 'Klipper', colorLogo: '#b12f35', sidebarLogo: true }, + { name: 'mainsail', displayName: 'Mainsail', colorLogo: defaultLogoColor }, + { name: 'klipper', displayName: 'Klipper', colorLogo: '#b12f35', sidebarLogo: true }, ] From a824fb4c35135b7fc2f565ee9b0b4a4a64eefd90 Mon Sep 17 00:00:00 2001 From: Stefan Dej Date: Sun, 30 Jun 2024 16:52:57 +0200 Subject: [PATCH 3/8] fix: fix svg detection in TheTopbar Signed-off-by: Stefan Dej --- src/components/TheTopbar.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/TheTopbar.vue b/src/components/TheTopbar.vue index 83ae7b337..fd5261542 100644 --- a/src/components/TheTopbar.vue +++ b/src/components/TheTopbar.vue @@ -3,7 +3,7 @@ - + Logo @@ -213,7 +213,7 @@ export default class TheTopbar extends Mixins(BaseMixin, ThemeMixin) { } get isSvgLogo() { - return this.sidebarLogo.includes('.svg?timestamp=') + return this.sidebarLogo.includes('.svg?timestamp=') || this.sidebarLogo.endsWith('.svg') } get logoColor(): string { From fc4aa5723a8df02d5224308a602eb6304f4f5a16 Mon Sep 17 00:00:00 2001 From: Stefan Dej Date: Sun, 30 Jun 2024 19:33:40 +0200 Subject: [PATCH 4/8] feat: display sidebarLogo als favicon from theme also added a function to change the logo color in the sidebar Signed-off-by: Stefan Dej --- src/App.vue | 156 +++++++++++------- src/components/TheTopbar.vue | 19 --- src/components/mixins/theme.ts | 21 ++- .../settings/SettingsUiSettingsTab.vue | 2 +- src/store/gui/getters.ts | 6 +- 5 files changed, 120 insertions(+), 84 deletions(-) diff --git a/src/App.vue b/src/App.vue index a5015cfdb..56c4e6aaa 100644 --- a/src/App.vue +++ b/src/App.vue @@ -235,73 +235,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.sidebarLogo ?? false) !== 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 fd5261542..936f195f3 100644 --- a/src/components/TheTopbar.vue +++ b/src/components/TheTopbar.vue @@ -193,25 +193,6 @@ export default class TheTopbar extends Mixins(BaseMixin, ThemeMixin) { return this.$store.state.gui.uiSettings.boolHideUploadAndPrintButton ?? false } - get themeObject() { - return themes.find((t) => t.name === this.theme) ?? null - } - - get sidebarLogo(): string { - let url = this.$store.getters['files/getSidebarLogo'] - if (url !== '' || this.theme === 'mainsail') return url - - // if no theme is set, return empty string to load the default logo - if (this.themeObject === null || (this.themeObject.sidebarLogo ?? false) === false) return '' - - // return light logo if theme is light and sidebarLogo is set to both - if (this.themeObject.sidebarLogo === 'both' && this.themeMode === 'light') - return `/img/themes/sidebarLogo-${this.theme}-light.svg` - - // return dark/generic theme logo - return `/img/themes/sidebarLogo-${this.theme}.svg` - } - get isSvgLogo() { return this.sidebarLogo.includes('.svg?timestamp=') || this.sidebarLogo.endsWith('.svg') } diff --git a/src/components/mixins/theme.ts b/src/components/mixins/theme.ts index 511c1c879..ea767ee97 100644 --- a/src/components/mixins/theme.ts +++ b/src/components/mixins/theme.ts @@ -12,10 +12,14 @@ export default class ThemeMixin extends Vue { return this.fgColor(alpha, !this.$vuetify.theme.dark) } - get theme() { + 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' } @@ -52,4 +56,19 @@ export default class ThemeMixin extends Vue { get sidebarBgImage() { 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.sidebarLogo ?? false) === false) return '' + + // return light logo if theme is light and sidebarLogo is set to both + if (this.theme.sidebarLogo === 'both' && this.themeMode === 'light') + return `/img/themes/sidebarLogo-${this.themeName}-light.svg` + + // return dark/generic theme logo + return `/img/themes/sidebarLogo-${this.themeName}.svg` + } } diff --git a/src/components/settings/SettingsUiSettingsTab.vue b/src/components/settings/SettingsUiSettingsTab.vue index 617afa1bf..dcba86ff4 100644 --- a/src/components/settings/SettingsUiSettingsTab.vue +++ b/src/components/settings/SettingsUiSettingsTab.vue @@ -348,7 +348,7 @@ export default class SettingsUiSettingsTab extends Mixins(BaseMixin, ThemeMixin) } get defaultLogoColor() { - return themes.find((theme) => theme.name === this.theme)?.colorLogo ?? defaultLogoColor + return themes.find((theme) => theme.name === this.themeName)?.colorLogo ?? defaultLogoColor } get primaryColor() { diff --git a/src/store/gui/getters.ts b/src/store/gui/getters.ts index 9c28f1ad8..5975dab59 100644 --- a/src/store/gui/getters.ts +++ b/src/store/gui/getters.ts @@ -6,7 +6,7 @@ import { Theme } from '@/store/types' // eslint-disable-next-line export const getters: GetterTree = { - theme: (state) => { + theme: (state): string => { const theme = state.uiSettings.theme // return defaultTheme, if theme doesnt exists @@ -15,6 +15,10 @@ export const getters: GetterTree = { 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 && From 5643ca3c8a350166b0c147aeb7a5868db98c1caf Mon Sep 17 00:00:00 2001 From: Stefan Dej Date: Sun, 30 Jun 2024 19:36:14 +0200 Subject: [PATCH 5/8] refactor: remove unused import Signed-off-by: Stefan Dej --- src/components/TheTopbar.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TheTopbar.vue b/src/components/TheTopbar.vue index 936f195f3..26182ccbd 100644 --- a/src/components/TheTopbar.vue +++ b/src/components/TheTopbar.vue @@ -76,7 +76,7 @@