diff --git a/src/components/navbar/StartStopJoinButton.tsx b/src/components/navbar/StartStopJoinButton.tsx index aa1306e5a..6945490d2 100644 --- a/src/components/navbar/StartStopJoinButton.tsx +++ b/src/components/navbar/StartStopJoinButton.tsx @@ -1,47 +1,49 @@ -import React, { RefObject, useState } from 'react'; -import Button from '../button'; -import cx from 'classnames'; -import useComponentVisible from '../../hooks/useComponentVisible'; +import React, { useState } from 'react'; + import { Device } from '../../types'; import { useTranslation } from 'react-i18next'; import { toHHMMSS } from '../../utils'; import { BridgeApi } from '../../actions/BridgeApi'; import { GlobalState } from '../../store'; +import Dropdown from 'react-bootstrap/Dropdown'; +import Button from 'react-bootstrap/Button'; +import ButtonGroup from 'react-bootstrap/ButtonGroup'; + export type StartStopJoinButtonProps = Pick & Pick; export function StartStopJoinButton({ devices, setPermitJoin, bridgeInfo }: StartStopJoinButtonProps) { const { t } = useTranslation(['navbar']); - const { ref, isComponentVisible, setIsComponentVisible } = useComponentVisible(false); const [selectedRouter, setSelectedRouter] = useState({} as Device); const { permit_join: permitJoin, permit_join_timeout: permitJoinTimeout } = bridgeInfo; - const selectAndHide = (device: Device) => { - setSelectedRouter(device); - setIsComponentVisible(false); + const select = (device?: Device) => { + setSelectedRouter(device ? device : {} as Device); }; const sortByName = (a: Device, b: Device) => a.friendly_name.localeCompare(b.friendly_name); - const prioritizeCoordinator = (a: Device, b: Device) => - a.type === 'Coordinator' ? -1 : b.type === 'Coordinator' ? 1 : 0; const routers = Object.values(devices) - .filter((d) => d.type === 'Router' || d.type === 'Coordinator') + .filter((d) => d.type === 'Router') .sort(sortByName) - .sort(prioritizeCoordinator) .map((device) => ( -
  • - item={device} className="dropdown-item" onClick={selectAndHide}> - {device.friendly_name} - -
  • + { select(device) }}> + {device.friendly_name} + )); + const coordinator = Object.values(devices) + .filter((d) => d.type === 'Coordinator') + .map((device) => ( + { select(device) }}> + {t('zigbee:coordinator')} + + )); const onBtnClick = () => { setPermitJoin(!permitJoin, selectedRouter); }; const permitJoinTimer = ( <> - {permitJoinTimeout ? ( -
    + {permitJoin ? ( +
    {toHHMMSS(permitJoinTimeout)}
    ) : null} @@ -49,38 +51,30 @@ export function StartStopJoinButton({ devices, setPermitJoin, bridgeInfo }: Star ); const buttonLabel = ( <> - {permitJoin ? t('disable_join') : t('permit_join')} ({selectedRouter?.friendly_name ?? t('all')}) + {permitJoin ? t('disable_join') : t('permit_join')} {permitJoinTimer} ); return ( -
    - + {routers.length ? ( <> - - type="button" - className="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" - onClick={setIsComponentVisible} - item={!isComponentVisible} - > - {t('toggle_dropdown')} - -
      } - className={cx('dropdown-menu', { show: isComponentVisible })} - > -
    • - -
    • + + {t('toggle_dropdown')} + + + { select(undefined) }} active={selectedRouter?.friendly_name ? false : true}> + {t('all')} + + {coordinator} {routers} -
    + ) : null} -
    + ); } diff --git a/src/components/navbar/index.tsx b/src/components/navbar/index.tsx index 4a6f1c465..6412a45b3 100644 --- a/src/components/navbar/index.tsx +++ b/src/components/navbar/index.tsx @@ -11,10 +11,11 @@ import { BridgeApi } from '../../actions/BridgeApi'; import { ThemeSwitcher } from '../theme-switcher'; import { WithTranslation, withTranslation } from 'react-i18next'; import LocalePicker from '../../i18n/LocalePicker'; -import { isIframe } from '../../utils'; import { StartStopJoinButton } from './StartStopJoinButton'; import { SettingsDropdown } from './SettingsDropdown'; +import NavbarBrand from 'react-bootstrap/esm/NavbarBrand'; + const urls = [ { href: '/', @@ -54,19 +55,23 @@ const urls = [ type PropsFromStore = Pick; const NavBar: FunctionComponent & BridgeApi> = (props) => { - const { devices, setPermitJoin, bridgeInfo, restartBridge, setTheme, t } = props; + const { devices, setPermitJoin, bridgeInfo, restartBridge, t } = props; const ref = useRef(); const [navbarIsVisible, setNavbarIsVisible] = useState(false); useOnClickOutside(ref, () => { setNavbarIsVisible(false); }); return ( - ); diff --git a/src/components/theme-switcher.tsx b/src/components/theme-switcher.tsx index 6b75e0876..661542d70 100644 --- a/src/components/theme-switcher.tsx +++ b/src/components/theme-switcher.tsx @@ -1,29 +1,79 @@ import * as React from 'react'; +import { useTranslation } from 'react-i18next'; -import { useThemeSwitcher } from 'react-css-theme-switcher'; -import Button from './button'; +import Dropdown from 'react-bootstrap/Dropdown'; +import Button from 'react-bootstrap/Button'; +import ButtonGroup from 'react-bootstrap/ButtonGroup'; -export type Theme = 'light' | 'dark'; +export const ThemeSwitcher = (): JSX.Element => { + const { t } = useTranslation(['navbar']); + const getStoredTheme = () => localStorage.getItem('z2m-theme') + const setStoredTheme = theme => localStorage.setItem('z2m-theme', theme) -type ThemeSwitcherProps = { - saveCurrentTheme(theme: Theme): void; -}; -export const ThemeSwitcher = (props: ThemeSwitcherProps): JSX.Element => { - const { saveCurrentTheme } = props; - const { switcher, themes, status, currentTheme } = useThemeSwitcher(); - const isDarkMode = currentTheme === 'dark'; + 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) + } + } - if (status === 'loading') { - return
    Loading styles...
    ; + setTheme(getPreferredTheme()) + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + const storedTheme = getStoredTheme() + if (storedTheme !== 'light' && storedTheme !== 'dark') { + setTheme(getPreferredTheme()) + } + }) + + const updateTheme = (theme: string) => { + setStoredTheme(theme) + setTheme(getPreferredTheme()) + const icon = document.getElementById('theme-dropdown-icon') + icon!.className = getCurrentThemeClass() + } + + const getCurrentThemeClass = () => { + const theme = getStoredTheme() + let icon = 'fa-solid' + switch (theme) { + case 'light': { icon += ' fa-sun'; break } + case 'dark': { icon += ' fa-moon'; break } + default: { icon += ' fa-circle-half-stroke'; break } + } + return icon } - const toggleDarkMode = (light: boolean) => { - const theme = light ? themes.light : themes.dark; - saveCurrentTheme(theme as Theme); - switcher({ theme }); - }; + return ( - item={isDarkMode} className="btn btn-info" onClick={toggleDarkMode}> - {isDarkMode ? '🌑' : `🌞`} - + + + + {t('toggle_dropdown')} + + + { updateTheme('light') }}> + + {t('themes:light')} + + { updateTheme('dark') }}> + + {t('themes:dark')} + + { updateTheme('auto') }}> + + {t('themes:auto')} + + + ); }; diff --git a/src/i18n/LocalePicker.tsx b/src/i18n/LocalePicker.tsx index 8f4d9c7e2..ab70b578e 100644 --- a/src/i18n/LocalePicker.tsx +++ b/src/i18n/LocalePicker.tsx @@ -1,7 +1,5 @@ -import React, { RefObject } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; -import useComponentVisible from '../hooks/useComponentVisible'; -import cx from 'classnames'; import { Resource } from 'i18next'; import ca from './flags/ca.png'; @@ -30,6 +28,11 @@ import missing from './flags/missing-locale.png'; import localeNames from './locales/localeNames.json'; +import Dropdown from 'react-bootstrap/Dropdown'; +import Image from 'react-bootstrap/Image'; +import Button from 'react-bootstrap/Button'; +import ButtonGroup from 'react-bootstrap/ButtonGroup'; + const localesMap = { ca, en, @@ -57,52 +60,41 @@ const localesMap = { export default function LocalePicker(): JSX.Element { const { i18n } = useTranslation('localeNames'); - const { ref, isComponentVisible, setIsComponentVisible } = useComponentVisible(false); + const { t } = useTranslation(['navbar']); - const selectAndHide = (lang: string) => { - i18n.changeLanguage(lang).then(); - setIsComponentVisible(false); + const select = (lang: string) => { + i18n.changeLanguage(lang); }; const locales = Object.keys(i18n.options.resources as Resource).map((language) => ( - { - selectAndHide(language); - e.preventDefault(); - }} - > + { select(language) }}> {localeNames[language]} - {localeNames[language]} - + {localeNames[language]} + )); const currentLanguage = localesMap[i18n.language] ? i18n.language : i18n.language.split('-')[0]; return ( -
  • - { - setIsComponentVisible(!isComponentVisible); - e.preventDefault(); - }} - > - {localeNames[currentLanguage]} - -
    } - className={cx('dropdown-menu dropdown-menu-end', { show: isComponentVisible })} - > + + + + {t('toggle_dropdown')} + + {locales} -
    -
  • + + ); } diff --git a/src/i18n/flags/it.png b/src/i18n/flags/it.png index 1f50482aa..2ab69d526 100644 Binary files a/src/i18n/flags/it.png and b/src/i18n/flags/it.png differ diff --git a/src/i18n/flags/no.png b/src/i18n/flags/no.png index 0514ea081..17054c0f7 100644 Binary files a/src/i18n/flags/no.png and b/src/i18n/flags/no.png differ diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 4f0b0445e..812de7588 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -2563,6 +2563,11 @@ "toggle_dropdown": "Toggle dropdown", "touchlink": "Touchlink" }, + "themes": { + "light": "Light", + "dark": "Dark", + "auto": "Auto" + }, "ota": { "check": "Check for new updates", "check_all": "Check all", @@ -2684,6 +2689,7 @@ "command": "Command", "payload": "Payload", "save_description": "Save description", + "coordinator": "Coordinator" "update_description": "Update description" }, "scene": { diff --git a/src/images/logo.png b/src/images/logo.png new file mode 100644 index 000000000..74a5f21fa Binary files /dev/null and b/src/images/logo.png differ diff --git a/src/styles/styles.global.scss b/src/styles/styles.global.scss index 76e3b5c40..5d2ba16d7 100644 --- a/src/styles/styles.global.scss +++ b/src/styles/styles.global.scss @@ -68,3 +68,6 @@ body { display: none; } } + +// Import all of Bootstrap's CSS +@import "bootstrap/scss/bootstrap";