Skip to content

Commit

Permalink
feat: theme switcher
Browse files Browse the repository at this point in the history
  • Loading branch information
hecht-a committed Jan 15, 2025
1 parent abbb495 commit c3f0b5d
Show file tree
Hide file tree
Showing 8 changed files with 365 additions and 22 deletions.
62 changes: 49 additions & 13 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,25 +228,41 @@ Example:

```yaml
theme:
background-color: 100 20 10
primary-color: 40 90 40
contrast-multiplier: 1.1
background-color: 186 21 20
contrast-multiplier: 1.2
primary-color: 97 13 80
presets:
my-custom-dark-theme:
background-color: 229 19 23
contrast-multiplier: 1.2
primary-color: 222 74 74
positive-color: 96 44 68
negative-color: 359 68 71
my-custom-light-theme:
light: true
background-color: 220 23 95
contrast-multiplier: 1.0
primary-color: 220 91 54
positive-color: 109 58 40
negative-color: 347 87 44
```

### Themes
If you don't want to spend time configuring your own theme, there are [several available themes](themes.md) which you can simply copy the values for.

### Properties
| Name | Type | Required | Default |
| ---- | ---- | -------- | ------- |
| light | boolean | no | false |
| background-color | HSL | no | 240 8 9 |
| primary-color | HSL | no | 43 50 70 |
| positive-color | HSL | no | same as `primary-color` |
| negative-color | HSL | no | 0 70 70 |
| contrast-multiplier | number | no | 1 |
| text-saturation-multiplier | number | no | 1 |
| custom-css-file | string | no | |
| Name | Type | Required | Default |
| ---- |-------|----------| ------- |
| light | boolean | no | false |
| background-color | HSL | no | 240 8 9 |
| primary-color | HSL | no | 43 50 70 |
| positive-color | HSL | no | same as `primary-color` |
| negative-color | HSL | no | 0 70 70 |
| contrast-multiplier | number | no | 1 |
| text-saturation-multiplier | number | no | 1 |
| custom-css-file | string | no | |
| presets | array | no | |

#### `light`
Whether the scheme is light or dark. This does not change the background color, it inverts the text colors so that they look appropriately on a light background.
Expand Down Expand Up @@ -279,6 +295,26 @@ theme:
custom-css-file: /assets/my-style.css
```

#### `presets`
Define theme presets that can be selected from a dropdown menu in the webpage. Example:
```yaml
theme:
presets:
my-custom-dark-theme: # This will be displayed in the dropdown menu to select this theme
background-color: 229 19 23
contrast-multiplier: 1.2
primary-color: 222 74 74
positive-color: 96 44 68
negative-color: 359 68 71
my-custom-light-theme: # This will be displayed in the dropdown menu to select this theme
light: true
background-color: 220 23 95
contrast-multiplier: 1.0
primary-color: 220 91 54
positive-color: 109 58 40
negative-color: 347 87 44
```

> [!TIP]
>
> Because Glance uses a lot of utility classes it might be difficult to target some elements. To make it easier to style specific widgets, each widget has a `widget-type-{name}` class, so for example if you wanted to make the links inside just the RSS widget bigger you could use the following selector:
Expand Down
13 changes: 13 additions & 0 deletions internal/glance/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ import (
"gopkg.in/yaml.v3"
)

type CssProperties struct {
BackgroundColor *hslColorField `yaml:"background-color"`
PrimaryColor *hslColorField `yaml:"primary-color"`
PositiveColor *hslColorField `yaml:"positive-color"`
NegativeColor *hslColorField `yaml:"negative-color"`
Light bool `yaml:"light"`
ContrastMultiplier float32 `yaml:"contrast-multiplier"`
TextSaturationMultiplier float32 `yaml:"text-saturation-multiplier"`
}

type config struct {
Server struct {
Host string `yaml:"host"`
Expand All @@ -31,6 +41,7 @@ type config struct {
} `yaml:"document"`

Theme struct {
// Todo : Find a way to use CssProperties struct to avoid duplicates
BackgroundColor *hslColorField `yaml:"background-color"`
PrimaryColor *hslColorField `yaml:"primary-color"`
PositiveColor *hslColorField `yaml:"positive-color"`
Expand All @@ -39,6 +50,8 @@ type config struct {
ContrastMultiplier float32 `yaml:"contrast-multiplier"`
TextSaturationMultiplier float32 `yaml:"text-saturation-multiplier"`
CustomCSSFile string `yaml:"custom-css-file"`

Presets map[string]CssProperties `yaml:"presets"`
} `yaml:"theme"`

Branding struct {
Expand Down
21 changes: 17 additions & 4 deletions internal/glance/glance.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package glance
import (
"bytes"
"context"
"encoding/json"
"fmt"
"html/template"
"log"
Expand Down Expand Up @@ -125,8 +126,9 @@ func (a *application) transformUserDefinedAssetPath(path string) string {
}

type pageTemplateData struct {
App *application
Page *page
App *application
Page *page
Presets string
}

func (a *application) handlePageRequest(w http.ResponseWriter, r *http.Request) {
Expand All @@ -137,9 +139,20 @@ func (a *application) handlePageRequest(w http.ResponseWriter, r *http.Request)
return
}

presets := a.Config.Theme.Presets
keys := make([]string, 0, len(presets))
for key := range presets {
keys = append(keys, key)
}
presetsAsJSON, jsonErr := json.Marshal(presets)
if jsonErr != nil {
log.Fatalf("Erreur lors de la conversion en JSON : %v", jsonErr)
}

pageData := pageTemplateData{
Page: page,
App: a,
App: a,
Page: page,
Presets: string(presetsAsJSON),
}

var responseBytes bytes.Buffer
Expand Down
120 changes: 120 additions & 0 deletions internal/glance/static/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@ import { setupPopovers } from './popover.js';
import { setupMasonries } from './masonry.js';
import { throttledDebounce, isElementVisible, openURLInNewTab } from './utils.js';

document.addEventListener('DOMContentLoaded', () => {
const theme = localStorage.getItem('theme');

if (!theme) {
return;
}

const html = document.querySelector('html');
const jsonTheme = JSON.parse(theme);
if (jsonTheme.themeScheme === 'light') {
html.classList.remove('dark-scheme');
html.classList.add('light-scheme');
} else if (jsonTheme.themeScheme === 'dark') {
html.classList.add('dark-scheme');
html.classList.remove('light-scheme');
}

html.classList.add(jsonTheme.theme);
document.querySelector('[name=color-scheme]').setAttribute('content', jsonTheme.themeScheme);
Array.from(document.querySelectorAll('.dropdown-button span')).forEach((button) => {
button.textContent = jsonTheme.theme;
})
})

async function fetchPageContent(pageData) {
// TODO: handle non 200 status codes/time outs
// TODO: add retries
Expand Down Expand Up @@ -638,6 +662,101 @@ function setupTruncatedElementTitles() {
}
}

/**
* @typedef {Object} HslColorField
* @property {number} Hue
* @property {number} Saturation
* @property {number} Lightness
*/

/**
* @typedef {Object} Theme
* @property {HslColorField} BackgroundColor
* @property {HslColorField} PrimaryColor
* @property {HslColorField} PositiveColor
* @property {HslColorField} NegativeColor
* @property {boolean} Light
* @property {number} ContrastMultiplier
* @property {number} TextSaturationMultiplier
*/

/**
* @typedef {Record<string, Theme>} ThemeCollection
*/
function setupThemeSwitcher() {
const presetsContainers = Array.from(document.querySelectorAll('.custom-presets'));
const userThemesKeys = Object.keys(userThemes);

presetsContainers.forEach((presetsContainer) => {
userThemesKeys.forEach(preset => {
const presetElement = document.createElement('div');
presetElement.className = 'theme-option';
presetElement.setAttribute('data-theme', preset);
presetElement.setAttribute('data-scheme', userThemes[preset].Light ? 'light' : 'dark');
presetElement.textContent = preset;
presetsContainer.appendChild(presetElement);
});
});

const dropdownButtons = Array.from(document.querySelectorAll('.dropdown-button'));
const dropdownContents = Array.from(document.querySelectorAll('.dropdown-content'));

dropdownButtons.forEach((dropdownButton) => {
dropdownButton.addEventListener('click', (e) => {
e.stopPropagation();
dropdownContents.forEach((dropdownContent) => {
dropdownContent.classList.toggle('show');
});
dropdownButton.classList.toggle('active');
});
});

document.addEventListener('click', (e) => {
if (!e.target.closest('.theme-dropdown')) {
dropdownContents.forEach((dropdownContent) => {
dropdownContent.classList.remove('show');
});
dropdownButtons.forEach((dropdownButton) => {
dropdownButton.classList.remove('active');
});
}
});

document.querySelectorAll('.theme-option').forEach(option => {
option.addEventListener('click', () => {
const selectedTheme = option.getAttribute('data-theme');
const selectedThemeScheme = option.getAttribute('data-scheme');
const previousTheme = localStorage.getItem('theme');
dropdownContents.forEach((dropdownContent) => {
dropdownContent.classList.remove('show');
});
dropdownButtons.forEach((dropdownButton) => {
const html = document.querySelector('html');
if (previousTheme) {
html.classList.remove(JSON.parse(previousTheme).theme);
}
dropdownButton.classList.remove('active');
dropdownButton.querySelector('span').textContent = option.textContent;
html.classList.add(selectedTheme);

if (selectedThemeScheme === 'light') {
html.classList.remove('dark-scheme');
html.classList.add('light-scheme');
} else if (selectedThemeScheme === 'dark') {
html.classList.add('dark-scheme');
html.classList.remove('light-scheme');
}

document.querySelector('[name=color-scheme]').setAttribute('content', selectedThemeScheme);
localStorage.setItem('theme', JSON.stringify({
theme: selectedTheme,
themeScheme: selectedThemeScheme
}));
});
});
});
}

async function setupPage() {
const pageElement = document.getElementById("page");
const pageContentElement = document.getElementById("page-content");
Expand All @@ -646,6 +765,7 @@ async function setupPage() {
pageContentElement.innerHTML = pageContent;

try {
setupThemeSwitcher();
setupPopovers();
setupClocks()
setupCarousels();
Expand Down
Loading

0 comments on commit c3f0b5d

Please sign in to comment.