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:
Devon Doyle
2025-09-07 00:33:37 -04:00
committed by GitHub
parent 3300b90823
commit b43d0dde20
9 changed files with 397 additions and 38 deletions

View File

@@ -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>