mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-23 01:21:40 +00:00
Add custom theming (#2081)
Adds support for user-created custom themes. Custom theme interface is tucked into the global settings in a non-invasive manner to avoid major design changes. Builds on the theme structure established by the dark theme update. <img width="1486" height="953" alt="image" src="https://github.com/user-attachments/assets/716bcfc7-af74-41dc-b14a-cfc2f2d2caa9" /> <img width="1486" height="956" alt="image" src="https://github.com/user-attachments/assets/a00f9620-0b1d-4f67-b010-e94dda5dc212" /> Here's a few examples of what teams could do, using a few color schemes from local teams. Imagine the possibilities! <img width="1485" height="951" alt="image" src="https://github.com/user-attachments/assets/c3da37b8-f6be-4152-81e0-533297f517fc" /> <img width="1483" height="951" alt="image" src="https://github.com/user-attachments/assets/0d453f7a-cf6f-4c27-97db-603b54c1f73e" /> <img width="1485" height="952" alt="image" src="https://github.com/user-attachments/assets/bf8c7770-e60d-4875-9580-ed7e54e089f4" /> <img width="1484" height="952" alt="image" src="https://github.com/user-attachments/assets/326d89e6-dd6e-4e05-a9fa-c9fc6f880847" /> <img width="1482" height="951" alt="image" src="https://github.com/user-attachments/assets/eb5a2a5d-c103-482c-a62a-5ccd5ba21cc5" /> <img width="1482" height="950" alt="image" src="https://github.com/user-attachments/assets/4831ca56-f322-4345-97af-8963ae8539b1" /> Looking for high contrast? Just moments away: <img width="1484" height="949" alt="image" src="https://github.com/user-attachments/assets/7ffc65c6-7000-4566-b4f0-c8247f75fb3d" />
This commit is contained in:
@@ -3,10 +3,12 @@ import { useStateStore } from "@/stores/StateStore";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { AutoReconnectingWebsocket } from "@/lib/AutoReconnectingWebsocket";
|
||||
import { inject } from "vue";
|
||||
import { inject, onBeforeMount } from "vue";
|
||||
import PhotonSidebar from "@/components/app/photon-sidebar.vue";
|
||||
import PhotonLogView from "@/components/app/photon-log-view.vue";
|
||||
import PhotonErrorSnackbar from "@/components/app/photon-error-snackbar.vue";
|
||||
import { useTheme } from "vuetify";
|
||||
import { restoreThemeConfig } from "@/lib/ThemeManager";
|
||||
|
||||
const is_demo = import.meta.env.MODE === "demo";
|
||||
if (!is_demo) {
|
||||
@@ -50,6 +52,11 @@ if (!is_demo) {
|
||||
);
|
||||
useStateStore().$patch({ websocket: websocket });
|
||||
}
|
||||
|
||||
const theme = useTheme();
|
||||
onBeforeMount(() => {
|
||||
restoreThemeConfig(theme);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
import { computed, inject, ref, onBeforeUnmount } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import loadingImage from "@/assets/images/loading-transparent.svg";
|
||||
import type { StyleValue } from "vue";
|
||||
import PvIcon from "@/components/common/pv-icon.vue";
|
||||
import type { UiCameraConfiguration } from "@/types/SettingTypes";
|
||||
import PvLoading from "@/components/common/pv-loading.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
streamType: "Raw" | "Processed";
|
||||
@@ -92,7 +92,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<div class="stream-container" :style="containerStyle">
|
||||
<img :src="loadingImage" class="stream-loading" />
|
||||
<pv-loading class="stream-loading" />
|
||||
<img
|
||||
:id="id"
|
||||
ref="mjpgStream"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useStateStore } from "@/stores/StateStore";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useDisplay, useTheme } from "vuetify";
|
||||
import { onBeforeMount } from "vue";
|
||||
import { toggleTheme } from "@/lib/ThemeManager";
|
||||
|
||||
const compact = computed<boolean>({
|
||||
get: () => {
|
||||
@@ -19,19 +19,6 @@ const { mdAndUp } = useDisplay();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const changeTheme = () => {
|
||||
const newTheme = theme.global.name.value === "LightTheme" ? "DarkTheme" : "LightTheme";
|
||||
theme.global.name.value = newTheme;
|
||||
localStorage.setItem("theme", newTheme);
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
const storedTheme = localStorage.getItem("theme");
|
||||
if (storedTheme) {
|
||||
theme.global.name.value = storedTheme;
|
||||
}
|
||||
});
|
||||
|
||||
const renderCompact = computed<boolean>(() => compact.value || !mdAndUp.value);
|
||||
</script>
|
||||
|
||||
@@ -88,7 +75,7 @@ const renderCompact = computed<boolean>(() => compact.value || !mdAndUp.value);
|
||||
<v-list-item
|
||||
link
|
||||
:prepend-icon="theme.global.name.value === 'LightTheme' ? 'mdi-white-balance-sunny' : 'mdi-weather-night'"
|
||||
@click="changeTheme"
|
||||
@click="() => toggleTheme(theme)"
|
||||
>
|
||||
<v-list-item-title>Theme</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
@@ -90,14 +90,7 @@ const fpsTooLow = computed<boolean>(() => {
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-0">
|
||||
<v-btn-toggle
|
||||
v-model="value"
|
||||
:multiple="true"
|
||||
mandatory
|
||||
class="fill"
|
||||
style="width: 100%"
|
||||
base-color="surface-variant"
|
||||
>
|
||||
<v-btn-toggle v-model="value" :multiple="true" mandatory class="fill" style="width: 100%">
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
class="fill"
|
||||
|
||||
205
photon-client/src/components/common/pv-loading.vue
Normal file
205
photon-client/src/components/common/pv-loading.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
width="200"
|
||||
height="200"
|
||||
style="shape-rendering: auto; display: block; background: rgba(0, 100, 146, 0)"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<g>
|
||||
<g transform="translate(80,50)">
|
||||
<g transform="rotate(0)">
|
||||
<circle class="loader-circle" fill-opacity="1" fill="#ffd943" r="6" cy="0" cx="0">
|
||||
<animateTransform
|
||||
repeatCount="indefinite"
|
||||
dur="0.9345794392523364s"
|
||||
keyTimes="0;1"
|
||||
values="1.5 1.5;1 1"
|
||||
begin="-0.8177570093457943s"
|
||||
type="scale"
|
||||
attributeName="transform"
|
||||
></animateTransform>
|
||||
<animate
|
||||
begin="-0.8177570093457943s"
|
||||
values="1;0"
|
||||
repeatCount="indefinite"
|
||||
dur="0.9345794392523364s"
|
||||
keyTimes="0;1"
|
||||
attributeName="fill-opacity"
|
||||
></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(71.21320343559643,71.21320343559643)">
|
||||
<g transform="rotate(45)">
|
||||
<circle class="loader-circle" fill-opacity="0.875" fill="#ffd943" r="6" cy="0" cx="0">
|
||||
<animateTransform
|
||||
repeatCount="indefinite"
|
||||
dur="0.9345794392523364s"
|
||||
keyTimes="0;1"
|
||||
values="1.5 1.5;1 1"
|
||||
begin="-0.7009345794392523s"
|
||||
type="scale"
|
||||
attributeName="transform"
|
||||
></animateTransform>
|
||||
<animate
|
||||
begin="-0.7009345794392523s"
|
||||
values="1;0"
|
||||
repeatCount="indefinite"
|
||||
dur="0.9345794392523364s"
|
||||
keyTimes="0;1"
|
||||
attributeName="fill-opacity"
|
||||
></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(50,80)">
|
||||
<g transform="rotate(90)">
|
||||
<circle class="loader-circle" fill-opacity="0.75" fill="#ffd943" r="6" cy="0" cx="0">
|
||||
<animateTransform
|
||||
repeatCount="indefinite"
|
||||
dur="0.9345794392523364s"
|
||||
keyTimes="0;1"
|
||||
values="1.5 1.5;1 1"
|
||||
begin="-0.5841121495327103s"
|
||||
type="scale"
|
||||
attributeName="transform"
|
||||
></animateTransform>
|
||||
<animate
|
||||
begin="-0.5841121495327103s"
|
||||
values="1;0"
|
||||
repeatCount="indefinite"
|
||||
dur="0.9345794392523364s"
|
||||
keyTimes="0;1"
|
||||
attributeName="fill-opacity"
|
||||
></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(28.786796564403577,71.21320343559643)">
|
||||
<g transform="rotate(135)">
|
||||
<circle class="loader-circle" fill-opacity="0.625" fill="#ffd943" r="6" cy="0" cx="0">
|
||||
<animateTransform
|
||||
repeatCount="indefinite"
|
||||
dur="0.9345794392523364s"
|
||||
keyTimes="0;1"
|
||||
values="1.5 1.5;1 1"
|
||||
begin="-0.4672897196261682s"
|
||||
type="scale"
|
||||
attributeName="transform"
|
||||
></animateTransform>
|
||||
<animate
|
||||
begin="-0.4672897196261682s"
|
||||
values="1;0"
|
||||
repeatCount="indefinite"
|
||||
dur="0.9345794392523364s"
|
||||
keyTimes="0;1"
|
||||
attributeName="fill-opacity"
|
||||
></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(20,50.00000000000001)">
|
||||
<g transform="rotate(180)">
|
||||
<circle class="loader-circle" fill-opacity="0.5" fill="#ffd943" r="6" cy="0" cx="0">
|
||||
<animateTransform
|
||||
repeatCount="indefinite"
|
||||
dur="0.9345794392523364s"
|
||||
keyTimes="0;1"
|
||||
values="1.5 1.5;1 1"
|
||||
begin="-0.35046728971962615s"
|
||||
type="scale"
|
||||
attributeName="transform"
|
||||
></animateTransform>
|
||||
<animate
|
||||
begin="-0.35046728971962615s"
|
||||
values="1;0"
|
||||
repeatCount="indefinite"
|
||||
dur="0.9345794392523364s"
|
||||
keyTimes="0;1"
|
||||
attributeName="fill-opacity"
|
||||
></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(28.78679656440357,28.786796564403577)">
|
||||
<g transform="rotate(225)">
|
||||
<circle class="loader-circle" fill-opacity="0.375" fill="#ffd943" r="6" cy="0" cx="0">
|
||||
<animateTransform
|
||||
repeatCount="indefinite"
|
||||
dur="0.9345794392523364s"
|
||||
keyTimes="0;1"
|
||||
values="1.5 1.5;1 1"
|
||||
begin="-0.2336448598130841s"
|
||||
type="scale"
|
||||
attributeName="transform"
|
||||
></animateTransform>
|
||||
<animate
|
||||
begin="-0.2336448598130841s"
|
||||
values="1;0"
|
||||
repeatCount="indefinite"
|
||||
dur="0.9345794392523364s"
|
||||
keyTimes="0;1"
|
||||
attributeName="fill-opacity"
|
||||
></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(49.99999999999999,20)">
|
||||
<g transform="rotate(270)">
|
||||
<circle class="loader-circle" fill-opacity="0.25" fill="#ffd943" r="6" cy="0" cx="0">
|
||||
<animateTransform
|
||||
repeatCount="indefinite"
|
||||
dur="0.9345794392523364s"
|
||||
keyTimes="0;1"
|
||||
values="1.5 1.5;1 1"
|
||||
begin="-0.11682242990654206s"
|
||||
type="scale"
|
||||
attributeName="transform"
|
||||
></animateTransform>
|
||||
<animate
|
||||
begin="-0.11682242990654206s"
|
||||
values="1;0"
|
||||
repeatCount="indefinite"
|
||||
dur="0.9345794392523364s"
|
||||
keyTimes="0;1"
|
||||
attributeName="fill-opacity"
|
||||
></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(71.21320343559643,28.78679656440357)">
|
||||
<g transform="rotate(315)">
|
||||
<circle class="loader-circle" fill-opacity="0.125" fill="#ffd943" r="6" cy="0" cx="0">
|
||||
<animateTransform
|
||||
repeatCount="indefinite"
|
||||
dur="0.9345794392523364s"
|
||||
keyTimes="0;1"
|
||||
values="1.5 1.5;1 1"
|
||||
begin="0s"
|
||||
type="scale"
|
||||
attributeName="transform"
|
||||
></animateTransform>
|
||||
<animate
|
||||
begin="0s"
|
||||
values="1;0"
|
||||
repeatCount="indefinite"
|
||||
dur="0.9345794392523364s"
|
||||
keyTimes="0;1"
|
||||
attributeName="fill-opacity"
|
||||
></animate>
|
||||
</circle>
|
||||
</g>
|
||||
</g>
|
||||
<g></g>
|
||||
</g>
|
||||
<!-- [ldio] generated by https://loading.io -->
|
||||
</svg>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.loader-circle {
|
||||
fill: rgb(var(--v-theme-buttonActive));
|
||||
}
|
||||
</style>
|
||||
@@ -28,7 +28,7 @@ const processingMode = computed<number>({
|
||||
<v-row class="pa-3 pb-0 align-center">
|
||||
<v-col class="pa-4">
|
||||
<p style="color: white">Processing Mode</p>
|
||||
<v-btn-toggle v-model="processingMode" mandatory base-color="surface-variant" class="fill w-100">
|
||||
<v-btn-toggle v-model="processingMode" mandatory class="fill w-100">
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
:disabled="!useCameraSettingsStore().hasConnected"
|
||||
@@ -59,7 +59,7 @@ const processingMode = computed<number>({
|
||||
<v-row class="pa-3 pt-0 align-center">
|
||||
<v-col class="pa-4 pt-0">
|
||||
<p style="color: white">Stream Display</p>
|
||||
<v-btn-toggle v-model="value" :multiple="true" mandatory base-color="surface-variant" class="fill w-100">
|
||||
<v-btn-toggle v-model="value" :multiple="true" mandatory class="fill w-100">
|
||||
<v-btn
|
||||
color="buttonPassive"
|
||||
class="fill w-50"
|
||||
|
||||
@@ -8,6 +8,7 @@ import PvSelect from "@/components/common/pv-select.vue";
|
||||
import { type ConfigurableNetworkSettings, NetworkConnectionType } from "@/types/SettingTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useTheme } from "vuetify";
|
||||
import { getThemeColor, setThemeColor, resetTheme } from "@/lib/ThemeManager";
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -19,6 +20,19 @@ const resetTempSettingsStruct = () => {
|
||||
|
||||
const settingsValid = ref(true);
|
||||
|
||||
const showThemeConfig = ref(false);
|
||||
const backgroundColor = ref("");
|
||||
const primaryColor = ref("");
|
||||
const secondaryColor = ref("");
|
||||
const surfaceColor = ref("");
|
||||
|
||||
const loadCurrentColors = () => {
|
||||
backgroundColor.value = getThemeColor(theme, "background");
|
||||
primaryColor.value = getThemeColor(theme, "primary");
|
||||
secondaryColor.value = getThemeColor(theme, "secondary");
|
||||
surfaceColor.value = getThemeColor(theme, "surface");
|
||||
};
|
||||
|
||||
const isValidNetworkTablesIP = (v: string | undefined): boolean => {
|
||||
// Check if it is a valid team number between 1-99999 (5 digits)
|
||||
const teamNumberRegex = /^[1-9][0-9]{0,4}$/;
|
||||
@@ -139,10 +153,23 @@ watchEffect(() => {
|
||||
|
||||
<template>
|
||||
<v-card class="mb-3 rounded-12" color="surface">
|
||||
<v-card-title>Global Settings</v-card-title>
|
||||
<v-card-title style="display: flex; justify-content: space-between">
|
||||
<span>Global Settings</span>
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="
|
||||
() => {
|
||||
loadCurrentColors();
|
||||
showThemeConfig = true;
|
||||
}
|
||||
"
|
||||
>
|
||||
<v-icon size="x-large">mdi-palette-outline</v-icon>
|
||||
Theme
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<div class="pa-5 pt-0">
|
||||
<v-divider class="pb-2" />
|
||||
<v-card-title class="pl-0 pt-3 pb-10px">Networking</v-card-title>
|
||||
<v-card-title class="pl-0 pt-0 pb-10px">Networking</v-card-title>
|
||||
<v-form ref="form" v-model="settingsValid">
|
||||
<pv-input
|
||||
v-model="tempSettingsStruct.ntServerAddress"
|
||||
@@ -203,7 +230,6 @@ watchEffect(() => {
|
||||
useSettingsStore().network.networkingDisabled
|
||||
"
|
||||
/>
|
||||
<v-divider class="mt-10px pb-2" />
|
||||
<v-card-title class="pl-0 pt-3 pb-10px">Advanced Networking</v-card-title>
|
||||
<pv-switch
|
||||
v-show="!useSettingsStore().network.networkingDisabled"
|
||||
@@ -254,7 +280,6 @@ watchEffect(() => {
|
||||
icon="mdi-information-outline"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
/>
|
||||
<v-divider class="mt-10px pb-2" />
|
||||
<v-card-title class="pl-0 pt-3 pb-10px">Miscellaneous</v-card-title>
|
||||
<pv-switch
|
||||
v-model="tempSettingsStruct.shouldPublishProto"
|
||||
@@ -270,10 +295,10 @@ watchEffect(() => {
|
||||
icon="mdi-information-outline"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
|
||||
/>
|
||||
<v-divider class="mt-10px pb-5" />
|
||||
</v-form>
|
||||
<v-btn
|
||||
color="primary"
|
||||
class="mt-3"
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
style="color: black; width: 100%"
|
||||
:disabled="!settingsValid || !settingsHaveChanged()"
|
||||
@@ -282,6 +307,84 @@ watchEffect(() => {
|
||||
Save
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-dialog v-model="showThemeConfig" width="800" dark>
|
||||
<v-card color="surface" flat>
|
||||
<v-card-title class="text-center">Theme Configuration</v-card-title>
|
||||
<v-card-text class="pt-0 pb-10px">
|
||||
<v-row>
|
||||
<v-col class="text-center">
|
||||
Background
|
||||
<v-color-picker
|
||||
class="ma-auto pt-3"
|
||||
elevation="0"
|
||||
mode="hex"
|
||||
:modes="['hex']"
|
||||
v-model:model-value="backgroundColor"
|
||||
v-on:update:model-value="(hex) => setThemeColor(theme, 'background', hex)"
|
||||
></v-color-picker>
|
||||
</v-col>
|
||||
<v-col class="text-center">
|
||||
Surface
|
||||
<v-color-picker
|
||||
class="ma-auto pt-3"
|
||||
elevation="0"
|
||||
mode="hex"
|
||||
:modes="['hex']"
|
||||
v-model:model-value="surfaceColor"
|
||||
v-on:update:model-value="(hex) => setThemeColor(theme, 'surface', hex)"
|
||||
></v-color-picker>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col class="text-center">
|
||||
Primary
|
||||
<v-color-picker
|
||||
class="ma-auto pt-3"
|
||||
elevation="0"
|
||||
mode="hex"
|
||||
:modes="['hex']"
|
||||
v-model:model-value="primaryColor"
|
||||
v-on:update:model-value="(hex) => setThemeColor(theme, 'primary', hex)"
|
||||
></v-color-picker>
|
||||
</v-col>
|
||||
<v-col class="text-center">
|
||||
Secondary
|
||||
<v-color-picker
|
||||
class="ma-auto pt-3"
|
||||
elevation="0"
|
||||
mode="hex"
|
||||
:modes="['hex']"
|
||||
v-model:model-value="secondaryColor"
|
||||
v-on:update:model-value="(hex) => setThemeColor(theme, 'secondary', hex)"
|
||||
></v-color-picker>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pa-5 pt-0">
|
||||
<v-btn
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
color="buttonPassive"
|
||||
class="text-black"
|
||||
@click="showThemeConfig = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
|
||||
color="buttonActive"
|
||||
class="text-black"
|
||||
@click="
|
||||
() => {
|
||||
resetTheme(theme);
|
||||
loadCurrentColors();
|
||||
}
|
||||
"
|
||||
>
|
||||
Reset Default
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
|
||||
65
photon-client/src/lib/ThemeManager.ts
Normal file
65
photon-client/src/lib/ThemeManager.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { type ThemeInstance } from "vuetify";
|
||||
import { LightTheme, DarkTheme } from "@/plugins/vuetify";
|
||||
|
||||
export const resetTheme = (theme: ThemeInstance) => {
|
||||
const themeType = theme.global.name.value === "LightTheme" ? "light" : "dark";
|
||||
localStorage.removeItem(`${themeType}-background`);
|
||||
localStorage.removeItem(`${themeType}-primary`);
|
||||
localStorage.removeItem(`${themeType}-secondary`);
|
||||
localStorage.removeItem(`${themeType}-surface`);
|
||||
|
||||
restoreThemeConfig(theme);
|
||||
};
|
||||
|
||||
export const getThemeColor = (theme: ThemeInstance, color: string): string => {
|
||||
const themeType = theme.global.name.value === "LightTheme" ? "light" : "dark";
|
||||
const defaultTheme = theme.global.name.value === "LightTheme" ? LightTheme : DarkTheme;
|
||||
return localStorage.getItem(`${themeType}-${color}`) ?? defaultTheme.colors![color]!;
|
||||
};
|
||||
|
||||
export const setThemeColor = (theme: ThemeInstance, color: string, value: string | null) => {
|
||||
const themeType = theme.global.name.value === "LightTheme" ? "light" : "dark";
|
||||
if (value) localStorage.setItem(`${themeType}-${color}`, value);
|
||||
else localStorage.removeItem(`${themeType}-${color}`);
|
||||
|
||||
restoreThemeConfig(theme);
|
||||
};
|
||||
|
||||
export const toggleTheme = (theme: ThemeInstance) => {
|
||||
const currentTheme = localStorage.getItem("theme");
|
||||
localStorage.setItem("theme", currentTheme === "LightTheme" ? "DarkTheme" : "LightTheme");
|
||||
|
||||
restoreThemeConfig(theme);
|
||||
};
|
||||
|
||||
export const restoreThemeConfig = (theme: ThemeInstance) => {
|
||||
// Restore theme preference
|
||||
const storedTheme = localStorage.getItem("theme");
|
||||
if (storedTheme) theme.global.name.value = storedTheme;
|
||||
|
||||
// Restore custom theme colors
|
||||
const themeType = theme.global.name.value === "LightTheme" ? "light" : "dark";
|
||||
const defaultTheme = theme.global.name.value === "LightTheme" ? LightTheme : DarkTheme;
|
||||
|
||||
const customBackground = localStorage.getItem(`${themeType}-background`);
|
||||
const customPrimary = localStorage.getItem(`${themeType}-primary`);
|
||||
const customSecondary = localStorage.getItem(`${themeType}-secondary`);
|
||||
const customSurface = localStorage.getItem(`${themeType}-surface`);
|
||||
|
||||
theme.themes.value[theme.global.name.value].colors.background = customBackground ?? defaultTheme.colors!.background!;
|
||||
theme.themes.value[theme.global.name.value].colors.sidebar = theme.themes.value[theme.global.name.value].dark
|
||||
? (customBackground ?? defaultTheme.colors!.sidebar!)
|
||||
: (customSurface ?? defaultTheme.colors!.sidebar!);
|
||||
|
||||
theme.themes.value[theme.global.name.value].colors.primary = customPrimary ?? defaultTheme.colors!.primary!;
|
||||
theme.themes.value[theme.global.name.value].colors.buttonActive = customPrimary ?? defaultTheme.colors!.buttonActive!;
|
||||
|
||||
theme.themes.value[theme.global.name.value].colors.secondary = customSecondary ?? defaultTheme.colors!.secondary!;
|
||||
theme.themes.value[theme.global.name.value].colors.buttonPassive =
|
||||
customSecondary ?? defaultTheme.colors!.buttonPassive!;
|
||||
|
||||
theme.themes.value[theme.global.name.value].colors.accent = customSecondary ?? defaultTheme.colors!.accent!;
|
||||
theme.themes.value[theme.global.name.value].colors.toggle = customSecondary ?? defaultTheme.colors!.toggle!;
|
||||
|
||||
theme.themes.value[theme.global.name.value].colors.surface = customSurface ?? defaultTheme.colors!.surface!;
|
||||
};
|
||||
@@ -12,7 +12,7 @@ const CommonColors = {
|
||||
lightGray: "#232C37"
|
||||
};
|
||||
|
||||
const DarkTheme: ThemeDefinition = {
|
||||
export const DarkTheme: ThemeDefinition = {
|
||||
dark: true,
|
||||
colors: {
|
||||
background: CommonColors.darkGray,
|
||||
@@ -39,7 +39,7 @@ const DarkTheme: ThemeDefinition = {
|
||||
}
|
||||
};
|
||||
|
||||
const LightTheme: ThemeDefinition = {
|
||||
export const LightTheme: ThemeDefinition = {
|
||||
dark: false,
|
||||
colors: {
|
||||
background: CommonColors.lightGray,
|
||||
@@ -56,8 +56,7 @@ const LightTheme: ThemeDefinition = {
|
||||
buttonActive: CommonColors.photonYellow,
|
||||
buttonPassive: CommonColors.lightBlue,
|
||||
|
||||
"surface-variant": "#358AB0",
|
||||
"surface-light": CommonColors.photonYellow,
|
||||
"surface-variant": "#8f8f8fff",
|
||||
|
||||
error: "#b80000",
|
||||
info: "#2196F3",
|
||||
|
||||
Reference in New Issue
Block a user