diff --git a/src_assets/common/assets/web/Navbar.vue b/src_assets/common/assets/web/Navbar.vue index 9e4e1be64f5..838c630f45a 100644 --- a/src_assets/common/assets/web/Navbar.vue +++ b/src_assets/common/assets/web/Navbar.vue @@ -1,60 +1,89 @@ <template> - <nav class="navbar navbar-expand-lg navbar-light" style="background-color: #ffc400"> - <div class="container-fluid"> - <a class="navbar-brand" href="/" title="Sunshine"> - <img src="/images/logo-sunshine-45.png" height="45" alt="Sunshine"> - </a> - <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" - aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> - <span class="navbar-toggler-icon"></span> - </button> - <div class="collapse navbar-collapse" id="navbarSupportedContent"> - <ul class="navbar-nav me-auto mb-2 mb-lg-0"> - <li class="nav-item"> - <a class="nav-link" href="/"><i class="fas fa-fw fa-home"></i> {{ $t('navbar.home') }}</a> - </li> - <li class="nav-item"> - <a class="nav-link" href="/pin"><i class="fas fa-fw fa-unlock"></i> {{ $t('navbar.pin') }}</a> - </li> - <li class="nav-item"> - <a class="nav-link" href="/apps"><i class="fas fa-fw fa-stream"></i> {{ $t('navbar.applications') }}</a> - </li> - <li class="nav-item"> - <a class="nav-link" href="/config"><i class="fas fa-fw fa-cog"></i> {{ $t('navbar.configuration') }}</a> - </li> - <li class="nav-item"> - <a class="nav-link" href="/password"><i class="fas fa-fw fa-user-shield"></i> {{ $t('navbar.password') }}</a> - </li> - <li class="nav-item"> - <a class="nav-link" href="/troubleshooting"><i class="fas fa-fw fa-info"></i> {{ $t('navbar.troubleshoot') }}</a> - </li> - </ul> - </div> - </div> - </nav> + <nav class="navbar navbar-light navbar-expand-lg navbar-background header"> + <div class="container-fluid"> + <a class="navbar-brand" href="/" title="Sunshine"> + <img src="/images/logo-sunshine-45.png" height="45" alt="Sunshine"> + </a> + <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" + aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> + <span class="navbar-toggler-icon"></span> + </button> + <div class="collapse navbar-collapse" id="navbarSupportedContent"> + <ul class="navbar-nav me-auto mb-2 mb-lg-0"> + <li class="nav-item"> + <a class="nav-link" href="/"><i class="fas fa-fw fa-home"></i> {{ $t('navbar.home') }}</a> + </li> + <li class="nav-item"> + <a class="nav-link" href="/pin"><i class="fas fa-fw fa-unlock"></i> {{ $t('navbar.pin') }}</a> + </li> + <li class="nav-item"> + <a class="nav-link" href="/apps"><i class="fas fa-fw fa-stream"></i> {{ $t('navbar.applications') }}</a> + </li> + <li class="nav-item"> + <a class="nav-link" href="/config"><i class="fas fa-fw fa-cog"></i> {{ $t('navbar.configuration') }}</a> + </li> + <li class="nav-item"> + <a class="nav-link" href="/password"><i class="fas fa-fw fa-user-shield"></i> {{ $t('navbar.password') }}</a> + </li> + <li class="nav-item"> + <a class="nav-link" href="/troubleshooting"><i class="fas fa-fw fa-info"></i> {{ $t('navbar.troubleshoot') }}</a> + </li> + <li class="nav-item"> + <ThemeToggle/> + </li> + </ul> + </div> + </div> + </nav> </template> <script> +import ThemeToggle from './ThemeToggle.vue' + export default { - created() { - console.log("Header mounted!") - }, - mounted() { - let el = document.querySelector("a[href='" + document.location.pathname + "']"); - if (el) el.classList.add("active") - let discordWidget = document.createElement('script') - discordWidget.setAttribute('src', 'https://app.lizardbyte.dev/js/discord.js') - document.head.appendChild(discordWidget) - } + components: { ThemeToggle }, + created() { + console.log("Header mounted!") + }, + mounted() { + let el = document.querySelector("a[href='" + document.location.pathname + "']"); + if (el) el.classList.add("active") + let discordWidget = document.createElement('script') + discordWidget.setAttribute('src', 'https://app.lizardbyte.dev/js/discord.js') + document.head.appendChild(discordWidget) + } } </script> <style> -.nav-link.active { - font-weight: 500; +.navbar-background { + background-color: #ffc400 +} + +.header .nav-link { + color: rgba(0, 0, 0, .65) !important; +} + +.header .nav-link.active { + color: rgb(0, 0, 0) !important; + font-weight: 500; +} + +.header .nav-link:hover { + color: rgb(0, 0, 0) !important; + font-weight: 500; +} + +.header .navbar-toggler { + color: rgba(var(--bs-dark-rgb), .65) !important; + border: var(--bs-border-width) solid rgba(var(--bs-dark-rgb), 0.15) !important; +} + +.header .navbar-toggler-icon { + --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") !important; } .form-control::placeholder { - opacity: 0.5; + opacity: 0.5; } </style> diff --git a/src_assets/common/assets/web/ThemeToggle.vue b/src_assets/common/assets/web/ThemeToggle.vue new file mode 100644 index 00000000000..7c34916adc9 --- /dev/null +++ b/src_assets/common/assets/web/ThemeToggle.vue @@ -0,0 +1,46 @@ +<script setup> +import { loadAutoTheme, setupThemeToggleListener } from './theme' +import { onMounted } from 'vue' + +onMounted(() => { + loadAutoTheme() + setupThemeToggleListener() +}) +</script> + +<template> + <div class="dropdown bd-mode-toggle"> + <a class="nav-link dropdown-toggle align-items-center" + id="bd-theme" + type="button" + aria-expanded="false" + data-bs-toggle="dropdown" + aria-label="{{ $t('navbar.toggle_theme') }} ({{ $t('navbar.theme_auto') }})"> + <span class="bi my-1 theme-icon-active"><i class="fa-solid fa-circle-half-stroke"></i></span> + <span id="bd-theme-text">{{ $t('navbar.toggle_theme') }}</span> + </a> + <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="bd-theme-text"> + <li> + <button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="light" aria-pressed="false"> + <i class="bi me-2 theme-icon fas fa-fw fa-solid fa-sun"></i> + {{ $t('navbar.theme_light') }} + </button> + </li> + <li> + <button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="dark" aria-pressed="false"> + <i class="bi me-2 theme-icon fas fa-fw fa-solid fa-moon"></i> + {{ $t('navbar.theme_dark') }} + </button> + </li> + <li> + <button type="button" class="dropdown-item d-flex align-items-center active" data-bs-theme-value="auto" aria-pressed="true"> + <i class="bi me-2 theme-icon fas fa-fw fa-solid fa-circle-half-stroke"></i> + {{ $t('navbar.theme_auto') }} + </button> + </li> + </ul> + </div> +</template> + +<style scoped> +</style> diff --git a/src_assets/common/assets/web/apps.html b/src_assets/common/assets/web/apps.html index 0fd0651aa13..9a8c65d16ae 100644 --- a/src_assets/common/assets/web/apps.html +++ b/src_assets/common/assets/web/apps.html @@ -1,5 +1,5 @@ <!DOCTYPE html> -<html lang="en"> +<html lang="en" data-bs-theme="auto"> <head> <%- header %> @@ -355,10 +355,10 @@ <h4>{{ $t('apps.env_vars_about') }}</h4> </div> </body> <script type="module"> - import { createApp } from 'vue'; + import { createApp } from 'vue' import { initApp } from './init' import Navbar from './Navbar.vue' - import {Dropdown} from 'bootstrap' + import { Dropdown } from 'bootstrap/dist/js/bootstrap' const app = createApp({ components: { diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index 722d55c7600..7df3880ceb4 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -1,5 +1,5 @@ <!DOCTYPE html> -<html lang="en"> +<html lang="en" data-bs-theme="auto"> <head> <%- header %> @@ -13,12 +13,6 @@ .buttons { padding: 1em 0; } - - .ms-item { - background-color: #ccc; - font-size: 12px; - font-weight: bold; - } </style> </head> diff --git a/src_assets/common/assets/web/configs/tabs/AudioVideo.vue b/src_assets/common/assets/web/configs/tabs/AudioVideo.vue index 851e1e03a7e..58695ba1d2a 100644 --- a/src_assets/common/assets/web/configs/tabs/AudioVideo.vue +++ b/src_assets/common/assets/web/configs/tabs/AudioVideo.vue @@ -87,3 +87,6 @@ const config = ref(props.config) </div> </template> + +<style scoped> +</style> diff --git a/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayModesSettings.vue b/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayModesSettings.vue index 74bd5d9f87b..7fb5ca3b0f9 100644 --- a/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayModesSettings.vue +++ b/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayModesSettings.vue @@ -65,3 +65,11 @@ const fpsIn = ref("") <div class="form-text">{{ $t('config.res_fps_desc') }}</div> </div> </template> + +<style scoped> +.ms-item { + background-color: var(--bs-dark-bg-subtle); + font-size: 12px; + font-weight: bold; +} +</style> diff --git a/src_assets/common/assets/web/index.html b/src_assets/common/assets/web/index.html index 9aeb4e399b5..d279f16ddc0 100644 --- a/src_assets/common/assets/web/index.html +++ b/src_assets/common/assets/web/index.html @@ -1,5 +1,5 @@ <!DOCTYPE html> -<html lang="en"> +<html lang="en" data-bs-theme="auto"> <head> <%- header %> diff --git a/src_assets/common/assets/web/init.js b/src_assets/common/assets/web/init.js index 3f30a0f034e..22b84df293e 100644 --- a/src_assets/common/assets/web/init.js +++ b/src_assets/common/assets/web/init.js @@ -1,5 +1,10 @@ import i18n from './locale' +// must import even if not implicitly using here +// https://github.com/aurelia/skeleton-navigation/issues/894 +// https://discourse.aurelia.io/t/bootstrap-import-bootstrap-breaks-dropdown-menu-in-navbar/641/9 +import 'bootstrap/dist/js/bootstrap' + export function initApp(app, config) { //Wait for locale initialization, then render i18n().then(i18n => { diff --git a/src_assets/common/assets/web/password.html b/src_assets/common/assets/web/password.html index 9a47cc565c8..639c82b7401 100644 --- a/src_assets/common/assets/web/password.html +++ b/src_assets/common/assets/web/password.html @@ -1,5 +1,5 @@ <!DOCTYPE html> -<html lang="en"> +<html lang="en" data-bs-theme="auto"> <head> <%- header %> diff --git a/src_assets/common/assets/web/pin.html b/src_assets/common/assets/web/pin.html index 359c5e527ba..f3139e123cc 100644 --- a/src_assets/common/assets/web/pin.html +++ b/src_assets/common/assets/web/pin.html @@ -1,5 +1,5 @@ <!DOCTYPE html> -<html lang="en"> +<html lang="en" data-bs-theme="auto"> <head> <%- header %> diff --git a/src_assets/common/assets/web/public/assets/css/sunshine.css b/src_assets/common/assets/web/public/assets/css/sunshine.css index de2acffee46..843600feebd 100644 --- a/src_assets/common/assets/web/public/assets/css/sunshine.css +++ b/src_assets/common/assets/web/public/assets/css/sunshine.css @@ -2,3 +2,15 @@ [v-cloak] { display: none; } + +[data-bs-theme=dark] .element { + color: var(--bs-primary-text-emphasis); + background-color: var(--bs-primary-bg-subtle); +} + +@media (prefers-color-scheme: dark) { + .element { + color: var(--bs-primary-text-emphasis); + background-color: var(--bs-primary-bg-subtle); + } +} diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json index 7c572cedd80..ac69860c305 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -337,6 +337,10 @@ "home": "Home", "password": "Change Password", "pin": "Pin", + "theme_auto": "Auto", + "theme_dark": "Dark", + "theme_light": "Light", + "toggle_theme": "Theme", "troubleshoot": "Troubleshooting" }, "password": { diff --git a/src_assets/common/assets/web/template_header.html b/src_assets/common/assets/web/template_header.html index 54109ca5255..e636a8ac9e2 100644 --- a/src_assets/common/assets/web/template_header.html +++ b/src_assets/common/assets/web/template_header.html @@ -7,4 +7,3 @@ <link href="@fortawesome/fontawesome-free/css/all.min.css" rel="stylesheet"> <link href="bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" /> <link href="/assets/css/sunshine.css" rel="stylesheet" /> -<script type="module" src="bootstrap/dist/js/bootstrap.bundle.min.js"></script> diff --git a/src_assets/common/assets/web/theme.js b/src_assets/common/assets/web/theme.js new file mode 100644 index 00000000000..a1f497802fa --- /dev/null +++ b/src_assets/common/assets/web/theme.js @@ -0,0 +1,84 @@ +const getStoredTheme = () => localStorage.getItem('theme') +const setStoredTheme = theme => localStorage.setItem('theme', theme) + +export const getPreferredTheme = () => { + const storedTheme = getStoredTheme() + if (storedTheme) { + return storedTheme + } + + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' +} + +const setTheme = theme => { + if (theme === 'auto') { + document.documentElement.setAttribute( + 'data-bs-theme', + (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') + ) + } else { + document.documentElement.setAttribute('data-bs-theme', theme) + } +} + +export const showActiveTheme = (theme, focus = false) => { + const themeSwitcher = document.querySelector('#bd-theme') + + if (!themeSwitcher) { + return + } + + const themeSwitcherText = document.querySelector('#bd-theme-text') + const activeThemeIcon = document.querySelector('.theme-icon-active i') + const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`) + const classListOfActiveBtn = btnToActive.querySelector('i').classList + + document.querySelectorAll('[data-bs-theme-value]').forEach(element => { + element.classList.remove('active') + element.setAttribute('aria-pressed', 'false') + }) + + btnToActive.classList.add('active') + btnToActive.setAttribute('aria-pressed', 'true') + activeThemeIcon.classList.remove(...activeThemeIcon.classList.values()) + activeThemeIcon.classList.add(...classListOfActiveBtn) + const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.textContent.trim()})` + themeSwitcher.setAttribute('aria-label', themeSwitcherLabel) + + if (focus) { + themeSwitcher.focus() + } +} + +export function setupThemeToggleListener() { + document.querySelectorAll('[data-bs-theme-value]') + .forEach(toggle => { + toggle.addEventListener('click', () => { + const theme = toggle.getAttribute('data-bs-theme-value') + setStoredTheme(theme) + setTheme(theme) + showActiveTheme(theme, true) + }) + }) + + showActiveTheme(getPreferredTheme(), false) +} + +export function loadAutoTheme() { + (() => { + 'use strict' + + setTheme(getPreferredTheme()) + + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + const storedTheme = getStoredTheme() + if (storedTheme !== 'light' && storedTheme !== 'dark') { + setTheme(getPreferredTheme()) + } + }) + + window.addEventListener('DOMContentLoaded', () => { + showActiveTheme(getPreferredTheme()) + }) + })() +} diff --git a/src_assets/common/assets/web/troubleshooting.html b/src_assets/common/assets/web/troubleshooting.html index 00497741368..0adc16542af 100644 --- a/src_assets/common/assets/web/troubleshooting.html +++ b/src_assets/common/assets/web/troubleshooting.html @@ -1,5 +1,5 @@ <!DOCTYPE html> -<html lang="en"> +<html lang="en" data-bs-theme="auto"> <head> <%- header %> diff --git a/src_assets/common/assets/web/welcome.html b/src_assets/common/assets/web/welcome.html index cf1e74ba8e6..18c67b2ee79 100644 --- a/src_assets/common/assets/web/welcome.html +++ b/src_assets/common/assets/web/welcome.html @@ -1,5 +1,5 @@ <!DOCTYPE html> -<html lang="en"> +<html lang="en" data-bs-theme="auto"> <head> <%- header %>