mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-22 01:36:29 -05:00
feat: add settings dialog
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
39823275a0
commit
4e18ce38f3
45 changed files with 656 additions and 376 deletions
|
@ -9,6 +9,7 @@ import { languages } from '../fixtures/languages'
|
||||||
describe('Languages', () => {
|
describe('Languages', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visitHome()
|
cy.visitHome()
|
||||||
|
cy.getByCypressId('settingsButton').click()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('all languages are available', () => {
|
it('all languages are available', () => {
|
||||||
|
|
|
@ -307,16 +307,8 @@
|
||||||
"view": "View",
|
"view": "View",
|
||||||
"both": "Both"
|
"both": "Both"
|
||||||
},
|
},
|
||||||
"darkMode": {
|
|
||||||
"switchToDark": "Switch to Dark Mode",
|
|
||||||
"switchToLight": "Switch to Light Mode"
|
|
||||||
},
|
|
||||||
"appBar": {
|
"appBar": {
|
||||||
"new": "New",
|
"new": "New"
|
||||||
"syncScroll": {
|
|
||||||
"disable": "Disable sync scroll",
|
|
||||||
"enable": "Enable sync scroll"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"editorToolbar": {
|
"editorToolbar": {
|
||||||
"bold": "Bold",
|
"bold": "Bold",
|
||||||
|
@ -516,6 +508,8 @@
|
||||||
"common": {
|
"common": {
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
|
"on": "On",
|
||||||
|
"off": "Off",
|
||||||
"import": "Import",
|
"import": "Import",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
|
@ -591,5 +585,46 @@
|
||||||
"title": "Note '{{noteTitle}}' deleted",
|
"title": "Note '{{noteTitle}}' deleted",
|
||||||
"text": "You were redirected to the history page, because the note you just edited was deleted."
|
"text": "You were redirected to the history page, because the note you just edited was deleted."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Settings",
|
||||||
|
"editor": {
|
||||||
|
"label": "Editor",
|
||||||
|
"ligatures": {
|
||||||
|
"label": "Ligatures",
|
||||||
|
"help": "(De)Activates support for ligatures in the editor."
|
||||||
|
},
|
||||||
|
"smartPaste": {
|
||||||
|
"label": "Smart Paste",
|
||||||
|
"help": "Smart paste detects common formats (like tables) when pasting content into the editor and formats them as markdown."
|
||||||
|
},
|
||||||
|
"syncScroll": {
|
||||||
|
"label": "Sync Scrolling",
|
||||||
|
"help": "Synchronizes the scroll state of editor and rendering view."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"global": {
|
||||||
|
"label": "Global",
|
||||||
|
"darkMode": {
|
||||||
|
"label": "Dark mode",
|
||||||
|
"help": "Enforces the (de)activation of the dark mode for the app or lets the browser decide.",
|
||||||
|
"dark": {
|
||||||
|
"tooltip": "Switch to dark mode",
|
||||||
|
"label": "Dark"
|
||||||
|
},
|
||||||
|
"light": {
|
||||||
|
"tooltip": "Switch to light mode",
|
||||||
|
"label": "Light"
|
||||||
|
},
|
||||||
|
"browser": {
|
||||||
|
"tooltip": "Let browser decide",
|
||||||
|
"label": "Auto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"label": "Language",
|
||||||
|
"help": "The primary user interface language"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,11 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { setDarkMode } from '../../../redux/dark-mode/methods'
|
import { setDarkModePreference } from '../../../redux/dark-mode/methods'
|
||||||
import { Logger } from '../../../utils/logger'
|
import { Logger } from '../../../utils/logger'
|
||||||
import type { DarkModeConfig } from '../../../redux/dark-mode/types'
|
|
||||||
import { isClientSideRendering } from '../../../utils/is-client-side-rendering'
|
import { isClientSideRendering } from '../../../utils/is-client-side-rendering'
|
||||||
|
import { DarkModePreference } from '../../../redux/dark-mode/types'
|
||||||
|
import { DARK_MODE_LOCAL_STORAGE_KEY } from '../../../hooks/common/use-apply-dark-mode'
|
||||||
|
|
||||||
const logger = new Logger('Dark mode initializer')
|
const logger = new Logger('Dark mode initializer')
|
||||||
|
|
||||||
|
@ -16,16 +17,9 @@ const logger = new Logger('Dark mode initializer')
|
||||||
*
|
*
|
||||||
* @return A promise that resolves as soon as the dark mode has been loaded.
|
* @return A promise that resolves as soon as the dark mode has been loaded.
|
||||||
*/
|
*/
|
||||||
export const loadDarkMode = async (): Promise<void> => {
|
export const loadDarkMode = (): Promise<void> => {
|
||||||
return new Promise<void>((resolve) => {
|
setDarkModePreference(fetchDarkModeFromLocalStorage())
|
||||||
setDarkMode(
|
return Promise.resolve()
|
||||||
fetchDarkModeFromLocalStorage() ??
|
|
||||||
determineDarkModeBrowserSettings() ?? {
|
|
||||||
darkMode: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,38 +28,21 @@ export const loadDarkMode = async (): Promise<void> => {
|
||||||
* @return {@link true} if the local storage has saved that the user prefers dark mode.
|
* @return {@link true} if the local storage has saved that the user prefers dark mode.
|
||||||
* {@link false} if the user doesn't prefer dark mode or if the value couldn't be read from local storage.
|
* {@link false} if the user doesn't prefer dark mode or if the value couldn't be read from local storage.
|
||||||
*/
|
*/
|
||||||
const fetchDarkModeFromLocalStorage = (): boolean => {
|
const fetchDarkModeFromLocalStorage = (): DarkModePreference => {
|
||||||
if (!isClientSideRendering()) {
|
if (!isClientSideRendering()) {
|
||||||
return false
|
return DarkModePreference.AUTO
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return window.localStorage.getItem('nightMode') === 'true'
|
const colorScheme = window.localStorage.getItem(DARK_MODE_LOCAL_STORAGE_KEY)
|
||||||
|
if (colorScheme === 'dark') {
|
||||||
|
return DarkModePreference.DARK
|
||||||
|
} else if (colorScheme === 'light') {
|
||||||
|
return DarkModePreference.LIGHT
|
||||||
|
} else {
|
||||||
|
return DarkModePreference.AUTO
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Loading from local storage failed', error)
|
logger.error('Loading from local storage failed', error)
|
||||||
return false
|
return DarkModePreference.AUTO
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tries to read the preferred dark mode setting from the browser settings.
|
|
||||||
*
|
|
||||||
* @return {@link true} if the browser has reported that the user prefers dark mode.
|
|
||||||
* {@link false} if the browser doesn't prefer dark mode.
|
|
||||||
* {@link undefined} if the browser doesn't support the `prefers-color-scheme` media query.
|
|
||||||
*/
|
|
||||||
const determineDarkModeBrowserSettings = (): DarkModeConfig | undefined => {
|
|
||||||
if (!isClientSideRendering()) {
|
|
||||||
return {
|
|
||||||
darkMode: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const mediaQueryResult = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
||||||
return {
|
|
||||||
darkMode: mediaQueryResult
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Can not determine setting from browser', error)
|
|
||||||
return undefined
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,10 @@ import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon'
|
||||||
import type { IconName } from '../fork-awesome/types'
|
import type { IconName } from '../fork-awesome/types'
|
||||||
import { ShowIf } from '../show-if/show-if'
|
import { ShowIf } from '../show-if/show-if'
|
||||||
import styles from './icon-button.module.scss'
|
import styles from './icon-button.module.scss'
|
||||||
|
import type { PropsWithDataTestId } from '../../../utils/test-id'
|
||||||
import { testId } from '../../../utils/test-id'
|
import { testId } from '../../../utils/test-id'
|
||||||
|
|
||||||
export interface IconButtonProps extends ButtonProps {
|
export interface IconButtonProps extends ButtonProps, PropsWithDataTestId {
|
||||||
icon: IconName
|
icon: IconName
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
border?: boolean
|
border?: boolean
|
||||||
|
|
|
@ -9,15 +9,14 @@ import { Nav, Navbar } from 'react-bootstrap'
|
||||||
import { ShowIf } from '../../common/show-if/show-if'
|
import { ShowIf } from '../../common/show-if/show-if'
|
||||||
import { SignInButton } from '../../landing-layout/navigation/sign-in-button'
|
import { SignInButton } from '../../landing-layout/navigation/sign-in-button'
|
||||||
import { UserDropdown } from '../../landing-layout/navigation/user-dropdown'
|
import { UserDropdown } from '../../landing-layout/navigation/user-dropdown'
|
||||||
import { DarkModeButton } from './dark-mode-button'
|
|
||||||
import { HelpButton } from './help-button/help-button'
|
import { HelpButton } from './help-button/help-button'
|
||||||
import { NavbarBranding } from './navbar-branding'
|
import { NavbarBranding } from './navbar-branding'
|
||||||
import { SyncScrollButtons } from './sync-scroll-buttons/sync-scroll-buttons'
|
|
||||||
import { SlideModeButton } from './slide-mode-button'
|
import { SlideModeButton } from './slide-mode-button'
|
||||||
import { ReadOnlyModeButton } from './read-only-mode-button'
|
import { ReadOnlyModeButton } from './read-only-mode-button'
|
||||||
import { NewNoteButton } from './new-note-button'
|
import { NewNoteButton } from './new-note-button'
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
import { NoteType } from '../../../redux/note-details/types/note-details'
|
import { NoteType } from '../../../redux/note-details/types/note-details'
|
||||||
|
import { SettingsButton } from '../../layout/settings-dialog/settings-button'
|
||||||
|
|
||||||
export enum AppBarMode {
|
export enum AppBarMode {
|
||||||
BASIC,
|
BASIC,
|
||||||
|
@ -41,10 +40,6 @@ export const AppBar: React.FC<AppBarProps> = ({ mode }) => {
|
||||||
<Navbar expand={true} className={'bg-light px-3'}>
|
<Navbar expand={true} className={'bg-light px-3'}>
|
||||||
<Nav className='me-auto d-flex align-items-center'>
|
<Nav className='me-auto d-flex align-items-center'>
|
||||||
<NavbarBranding />
|
<NavbarBranding />
|
||||||
<ShowIf condition={mode === AppBarMode.EDITOR}>
|
|
||||||
<SyncScrollButtons />
|
|
||||||
</ShowIf>
|
|
||||||
<DarkModeButton />
|
|
||||||
<ShowIf condition={mode === AppBarMode.EDITOR}>
|
<ShowIf condition={mode === AppBarMode.EDITOR}>
|
||||||
<ShowIf condition={noteFrontmatter.type === NoteType.SLIDE}>
|
<ShowIf condition={noteFrontmatter.type === NoteType.SLIDE}>
|
||||||
<SlideModeButton />
|
<SlideModeButton />
|
||||||
|
@ -56,6 +51,7 @@ export const AppBar: React.FC<AppBarProps> = ({ mode }) => {
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</Nav>
|
</Nav>
|
||||||
<Nav className='d-flex align-items-center text-secondary justify-content-end'>
|
<Nav className='d-flex align-items-center text-secondary justify-content-end'>
|
||||||
|
<SettingsButton className={'p-1 mx-2'} variant={'outline-dark'} />
|
||||||
<NewNoteButton />
|
<NewNoteButton />
|
||||||
<ShowIf condition={!userExists}>
|
<ShowIf condition={!userExists}>
|
||||||
<SignInButton size={'sm'} />
|
<SignInButton size={'sm'} />
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useCallback } from 'react'
|
|
||||||
import { Button, ButtonGroup } from 'react-bootstrap'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { useIsDarkModeActivated } from '../../../hooks/common/use-is-dark-mode-activated'
|
|
||||||
import { setDarkMode } from '../../../redux/dark-mode/methods'
|
|
||||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders a button group to activate / deactivate the dark mode.
|
|
||||||
*/
|
|
||||||
const DarkModeButton: React.FC = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const darkModeEnabled = useIsDarkModeActivated()
|
|
||||||
|
|
||||||
const enable = useCallback(() => setDarkMode(true), [])
|
|
||||||
const disable = useCallback(() => setDarkMode(false), [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ButtonGroup className='ms-2'>
|
|
||||||
<Button
|
|
||||||
onClick={enable}
|
|
||||||
variant={darkModeEnabled ? 'secondary' : 'outline-secondary'}
|
|
||||||
title={t('editor.darkMode.switchToDark') ?? undefined}>
|
|
||||||
<ForkAwesomeIcon icon='moon' />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={disable}
|
|
||||||
variant={darkModeEnabled ? 'outline-secondary' : 'secondary'}
|
|
||||||
title={t('editor.darkMode.switchToLight') ?? undefined}>
|
|
||||||
<ForkAwesomeIcon icon='sun-o' />
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { DarkModeButton }
|
|
|
@ -7,7 +7,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Navbar } from 'react-bootstrap'
|
import { Navbar } from 'react-bootstrap'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useIsDarkModeActivated } from '../../../hooks/common/use-is-dark-mode-activated'
|
import { useDarkModeState } from '../../../hooks/common/use-dark-mode-state'
|
||||||
import { Branding } from '../../common/branding/branding'
|
import { Branding } from '../../common/branding/branding'
|
||||||
import {
|
import {
|
||||||
HedgeDocLogoSize,
|
HedgeDocLogoSize,
|
||||||
|
@ -19,7 +19,7 @@ import {
|
||||||
* Renders the branding for the {@link AppBar}
|
* Renders the branding for the {@link AppBar}
|
||||||
*/
|
*/
|
||||||
export const NavbarBranding: React.FC = () => {
|
export const NavbarBranding: React.FC = () => {
|
||||||
const darkModeActivated = useIsDarkModeActivated()
|
const darkModeActivated = useDarkModeState()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Navbar.Brand>
|
<Navbar.Brand>
|
||||||
|
|
|
@ -1,84 +0,0 @@
|
||||||
<!--
|
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
|
|
||||||
SPDX-License-Identifier: CC0-1.0
|
|
||||||
-->
|
|
||||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="512"
|
|
||||||
height="512"
|
|
||||||
viewBox="0 0 135.46666 135.46666"
|
|
||||||
version="1.1"
|
|
||||||
id="svg8"
|
|
||||||
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
|
|
||||||
sodipodi:docname="buttonIcon.svg">
|
|
||||||
<defs id="defs2"/>
|
|
||||||
<sodipodi:namedview
|
|
||||||
fit-margin-bottom="0"
|
|
||||||
fit-margin-right="0"
|
|
||||||
fit-margin-left="0"
|
|
||||||
fit-margin-top="0"
|
|
||||||
id="base"
|
|
||||||
pagecolor="#545b62"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:pageopacity="1"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:zoom="1.4"
|
|
||||||
inkscape:cx="151.94971"
|
|
||||||
inkscape:cy="220.06486"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
inkscape:current-layer="layer1"
|
|
||||||
inkscape:document-rotation="0"
|
|
||||||
showgrid="false"
|
|
||||||
units="px"
|
|
||||||
inkscape:window-width="3434"
|
|
||||||
inkscape:window-height="1321"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="84"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:pagecheckerboard="false"/>
|
|
||||||
<metadata id="metadata5">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<g transform="translate(253.17277,890.86874)"
|
|
||||||
inkscape:label="Ebene 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1">
|
|
||||||
<path id="path864"
|
|
||||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000"
|
|
||||||
d="m -185.5862,-882.45881 c -1.05348,0.0354 -2.05943,0.44876 -2.83393,1.16582 l -17.37672,16.08016 c -1.77986,1.64837 -1.89553,4.45148 -0.25787,6.24199 1.63744,1.79079 4.43758,1.90901 6.21875,0.262 l 14.39653,-13.32218 14.39654,13.32218 c 1.78118,1.64702 4.5813,1.5288 6.21874,-0.262 1.63691,-1.79129 1.52077,-4.59444 -0.25993,-6.24199 l -17.37465,-16.08016 c -0.84861,-0.78564 -1.97318,-1.20418 -3.12746,-1.16582 z"/>
|
|
||||||
<path id="path851"
|
|
||||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000"
|
|
||||||
d="m -202.99806,-788.72004 c -1.12724,0.0475 -2.23794,0.52467 -3.05666,1.42007 -1.63766,1.79051 -1.52199,4.59363 0.25787,6.24199 l 17.37672,16.08016 c 0.7745,0.71706 1.78045,1.13042 2.83393,1.16582 1.15428,0.0384 2.27885,-0.38018 3.12746,-1.16582 l 17.37465,-16.08016 c 1.7807,-1.64754 1.89684,-4.4507 0.25993,-6.24199 -1.63744,-1.79081 -4.43756,-1.90902 -6.21874,-0.262 l -14.39654,13.32218 -14.39653,-13.32218 c -0.89058,-0.8235 -2.03485,-1.20561 -3.16209,-1.15807 z"/>
|
|
||||||
<circle
|
|
||||||
r="16" cy="-823.13544"
|
|
||||||
cx="-185.43944"
|
|
||||||
id="path845"
|
|
||||||
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:5.565;stroke-linecap:round;stroke-linejoin:round"/>
|
|
||||||
<g
|
|
||||||
style="display:inline;opacity:1;stroke:none;stroke-opacity:1"
|
|
||||||
id="g855">
|
|
||||||
<path
|
|
||||||
id="path858"
|
|
||||||
d="m -128.54012,-883.40784 -121.89532,111.71083 8.0967,8.83405 121.89531,-111.71083 z"
|
|
||||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.565;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1"/>
|
|
||||||
<path
|
|
||||||
id="path860"
|
|
||||||
d="m -128.61914,-886.18945 a 2.7827783,2.7827783 0 0 0 -1.80078,0.73047 l -121.89453,111.71093 a 2.7827783,2.7827783 0 0 0 -0.17188,3.93164 l 8.09571,8.83399 a 2.7827783,2.7827783 0 0 0 3.93164,0.16992 l 121.89648,-111.70898 a 2.7827783,2.7827783 0 0 0 0.16992,-3.93164 l -8.0957,-8.83399 a 2.7827783,2.7827783 0 0 0 -2.13086,-0.90234 z m -0.0937,6.71289 4.33789,4.73047 -117.79297,107.95117 -4.33594,-4.73047 z"
|
|
||||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#545b62;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.565;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 9.3 KiB |
|
@ -1,14 +0,0 @@
|
||||||
<!--
|
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
|
|
||||||
SPDX-License-Identifier: CC0-1.0
|
|
||||||
-->
|
|
||||||
<svg version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
|
||||||
<g transform="matrix(.11811 0 0 .11811 29.639 105.22)" fill="currentColor">
|
|
||||||
<path d="m-185.61-890.87c-1.2028 0.0405-2.3513 0.51236-3.2355 1.331l-19.839 18.359c-2.0321 1.882-2.1642 5.0823-0.29441 7.1266 1.8695 2.0446 5.0664 2.1796 7.1 0.29914l16.437-15.21 16.437 15.21c2.0336 1.8804 5.2306 1.7454 7.1-0.29914 1.8689-2.0451 1.7363-5.2456-0.29677-7.1266l-19.837-18.359c-0.96887-0.89698-2.2528-1.3748-3.5707-1.331z"/>
|
|
||||||
<path d="m-205.49-783.84c-1.287 0.0542-2.5551 0.59901-3.4898 1.6213-1.8698 2.0443-1.7377 5.2446 0.29441 7.1266l19.839 18.359c0.88425 0.81868 2.0328 1.2906 3.2355 1.331 1.3179 0.0439 2.6018-0.43406 3.5707-1.331l19.837-18.359c2.0331-1.881 2.1657-5.0814 0.29677-7.1266-1.8695-2.0446-5.0664-2.1796-7.1-0.29913l-16.437 15.21-16.437-15.21c-1.0168-0.94021-2.3232-1.3765-3.6102-1.3222z"/>
|
|
||||||
<path d="m-185.49-841.4a18.267 18.267 0 0 0-18.216 18.268 18.267 18.267 0 0 0 0.23777 2.9358l22.524-20.642a18.267 18.267 0 0 0-4.494-0.56168 18.267 18.267 0 0 0-0.0519 0zm18.082 15.333-22.523 20.641a18.267 18.267 0 0 0 4.4934 0.56168 18.267 18.267 0 0 0 18.268-18.268 18.267 18.267 0 0 0-0.23777-2.9347z"
|
|
||||||
fill-rule="evenodd"/>
|
|
||||||
<path d="m-124.79-883.92-126.15 116.29 4.9507 5.4008 126.15-116.29z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.4 KiB |
|
@ -1,12 +0,0 @@
|
||||||
<!--
|
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
|
|
||||||
SPDX-License-Identifier: CC0-1.0
|
|
||||||
-->
|
|
||||||
<svg version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
|
||||||
<g transform="scale(.11811)" fill="currentColor">
|
|
||||||
<path d="m67.566 0.0027667c-1.2028 0.040417-2.3513 0.51236-3.2356 1.331l-19.839 18.359c-2.0321 1.882-2.1642 5.0823-0.29442 7.1266 1.8695 2.0446 5.0665 2.1796 7.1 0.29913l16.437-15.21 16.437 15.21c2.0336 1.8804 5.2306 1.7455 7.1-0.29913 1.8689-2.0451 1.7363-5.2456-0.29677-7.1266l-19.837-18.359c-0.96887-0.89698-2.2528-1.3748-3.5707-1.331z"/>
|
|
||||||
<path d="m47.687 107.03c-1.287 0.0542-2.5551 0.59903-3.4898 1.6213-1.8698 2.0443-1.7377 5.2446 0.29442 7.1266l19.839 18.359c0.88426 0.81868 2.0328 1.2906 3.2356 1.331 1.3179 0.0438 2.6018-0.43406 3.5707-1.331l19.837-18.359c2.0331-1.881 2.1657-5.0814 0.29677-7.1266-1.8695-2.0446-5.0664-2.1796-7.1-0.29913l-16.437 15.21-16.437-15.21c-1.0168-0.94021-2.3232-1.3765-3.6102-1.3222z"/>
|
|
||||||
<circle cx="67.734" cy="67.733" r="18.267" fill-rule="evenodd"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.1 KiB |
|
@ -1,24 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
.sync-scroll-buttons {
|
|
||||||
svg {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.btn) {
|
|
||||||
svg g {
|
|
||||||
fill: var(--bs-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(&.active), &:hover {
|
|
||||||
svg g {
|
|
||||||
fill: var(--bs-light);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useCallback } from 'react'
|
|
||||||
import { Button, ButtonGroup } from 'react-bootstrap'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { setEditorSyncScroll } from '../../../../redux/editor/methods'
|
|
||||||
import DisabledScroll from './disabledScroll.svg'
|
|
||||||
import EnabledScroll from './enabledScroll.svg'
|
|
||||||
import './sync-scroll-buttons.module.scss'
|
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders a button group with two states for the sync scroll buttons.
|
|
||||||
* This makes it possible to activate or deactivate sync scrolling.
|
|
||||||
*/
|
|
||||||
export const SyncScrollButtons: React.FC = () => {
|
|
||||||
const syncScrollEnabled = useApplicationState((state) => state.editorConfig.syncScroll)
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const enable = useCallback(() => setEditorSyncScroll(true), [])
|
|
||||||
const disable = useCallback(() => setEditorSyncScroll(false), [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ButtonGroup className='ms-2 ms-2 sync-scroll-buttons'>
|
|
||||||
<Button
|
|
||||||
onClick={enable}
|
|
||||||
variant={syncScrollEnabled ? 'secondary' : 'outline-secondary'}
|
|
||||||
title={t('editor.appBar.syncScroll.enable') ?? undefined}>
|
|
||||||
<EnabledScroll />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={disable}
|
|
||||||
variant={syncScrollEnabled ? 'outline-secondary' : 'secondary'}
|
|
||||||
title={t('editor.appBar.syncScroll.disable') ?? undefined}>
|
|
||||||
<DisabledScroll />
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -8,7 +8,7 @@ import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer'
|
||||||
import { useAsync } from 'react-use'
|
import { useAsync } from 'react-use'
|
||||||
import { getRevision } from '../../../../api/revisions'
|
import { getRevision } from '../../../../api/revisions'
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||||
import { useIsDarkModeActivated } from '../../../../hooks/common/use-is-dark-mode-activated'
|
import { useDarkModeState } from '../../../../hooks/common/use-dark-mode-state'
|
||||||
import { AsyncLoadingBoundary } from '../../../common/async-loading-boundary'
|
import { AsyncLoadingBoundary } from '../../../common/async-loading-boundary'
|
||||||
import { applyPatch, parsePatch } from 'diff'
|
import { applyPatch, parsePatch } from 'diff'
|
||||||
import { invertUnifiedPatch } from './invert-unified-patch'
|
import { invertUnifiedPatch } from './invert-unified-patch'
|
||||||
|
@ -26,7 +26,7 @@ export interface RevisionViewerProps {
|
||||||
*/
|
*/
|
||||||
export const RevisionViewer: React.FC<RevisionViewerProps> = ({ selectedRevisionId }) => {
|
export const RevisionViewer: React.FC<RevisionViewerProps> = ({ selectedRevisionId }) => {
|
||||||
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
const noteIdentifier = useApplicationState((state) => state.noteDetails.primaryAddress)
|
||||||
const darkModeEnabled = useIsDarkModeActivated()
|
const darkModeEnabled = useDarkModeState()
|
||||||
|
|
||||||
const { value, error, loading } = useAsync(async () => {
|
const { value, error, loading } = useAsync(async () => {
|
||||||
if (selectedRevisionId === undefined) {
|
if (selectedRevisionId === undefined) {
|
||||||
|
|
|
@ -40,7 +40,7 @@ import { lintGutter } from '@codemirror/lint'
|
||||||
import { useLinter } from './linter/linter'
|
import { useLinter } from './linter/linter'
|
||||||
import { useOnNoteDeleted } from './hooks/yjs/use-on-note-deleted'
|
import { useOnNoteDeleted } from './hooks/yjs/use-on-note-deleted'
|
||||||
import { findLanguageByCodeBlockName } from '../../markdown-renderer/extensions/base/code-block-markdown-extension/find-language-by-code-block-name'
|
import { findLanguageByCodeBlockName } from '../../markdown-renderer/extensions/base/code-block-markdown-extension/find-language-by-code-block-name'
|
||||||
import { useIsDarkModeActivated } from '../../../hooks/common/use-is-dark-mode-activated'
|
import { useDarkModeState } from '../../../hooks/common/use-dark-mode-state'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the text editor pane of the editor.
|
* Renders the text editor pane of the editor.
|
||||||
|
@ -128,7 +128,7 @@ export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMak
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const darkModeActivated = useIsDarkModeActivated()
|
const darkModeActivated = useDarkModeState()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -11,7 +11,7 @@ import type { Extension } from '@codemirror/state'
|
||||||
import type { EditorView } from '@codemirror/view'
|
import type { EditorView } from '@codemirror/view'
|
||||||
import { optionalAppExtensions } from '../../../../extensions/extra-integrations/optional-app-extensions'
|
import { optionalAppExtensions } from '../../../../extensions/extra-integrations/optional-app-extensions'
|
||||||
import { FrontmatterLinter } from './frontmatter-linter'
|
import { FrontmatterLinter } from './frontmatter-linter'
|
||||||
import { useIsDarkModeActivated } from '../../../../hooks/common/use-is-dark-mode-activated'
|
import { useDarkModeState } from '../../../../hooks/common/use-dark-mode-state'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Linter interface.
|
* The Linter interface.
|
||||||
|
@ -37,7 +37,7 @@ const createLinterExtension = () =>
|
||||||
* @return The build codemirror linter extension
|
* @return The build codemirror linter extension
|
||||||
*/
|
*/
|
||||||
export const useLinter = (): Extension => {
|
export const useLinter = (): Extension => {
|
||||||
const darkModeActivated = useIsDarkModeActivated()
|
const darkModeActivated = useDarkModeState()
|
||||||
|
|
||||||
return useMemo(() => (darkModeActivated ? createLinterExtension() : createLinterExtension()), [darkModeActivated])
|
return useMemo(() => (darkModeActivated ? createLinterExtension() : createLinterExtension()), [darkModeActivated])
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
import { Picker } from 'emoji-picker-element'
|
import { Picker } from 'emoji-picker-element'
|
||||||
import type { CustomEmoji, EmojiClickEvent, EmojiClickEventDetail } from 'emoji-picker-element/shared'
|
import type { CustomEmoji, EmojiClickEvent, EmojiClickEventDetail } from 'emoji-picker-element/shared'
|
||||||
import React, { useEffect, useRef } from 'react'
|
import React, { useEffect, useRef } from 'react'
|
||||||
import { useIsDarkModeActivated } from '../../../../../hooks/common/use-is-dark-mode-activated'
|
import { useDarkModeState } from '../../../../../hooks/common/use-dark-mode-state'
|
||||||
import styles from './emoji-picker.module.scss'
|
import styles from './emoji-picker.module.scss'
|
||||||
import forkawesomeIcon from './forkawesome.png'
|
import forkawesomeIcon from './forkawesome.png'
|
||||||
import { ForkAwesomeIcons } from '../../../../common/fork-awesome/fork-awesome-icons'
|
import { ForkAwesomeIcons } from '../../../../common/fork-awesome/fork-awesome-icons'
|
||||||
|
@ -49,7 +49,7 @@ export interface EmojiPickerProps extends PopoverProps {
|
||||||
*/
|
*/
|
||||||
export const EmojiPickerPopover = React.forwardRef<HTMLDivElement, EmojiPickerProps>(
|
export const EmojiPickerPopover = React.forwardRef<HTMLDivElement, EmojiPickerProps>(
|
||||||
({ onEmojiSelected, ...props }, ref) => {
|
({ onEmojiSelected, ...props }, ref) => {
|
||||||
const darkModeEnabled = useIsDarkModeActivated()
|
const darkModeEnabled = useDarkModeState()
|
||||||
const pickerContainerRef = useRef<HTMLDivElement>(null)
|
const pickerContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const pickerRef = useRef<Picker>()
|
const pickerRef = useRef<Picker>()
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useIsDarkModeActivated } from '../../../../hooks/common/use-is-dark-mode-activated'
|
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { CommunicationMessageType } from '../../../render-page/window-post-message-communicator/rendering-message'
|
import { CommunicationMessageType } from '../../../render-page/window-post-message-communicator/rendering-message'
|
||||||
import { useSendToRenderer } from '../../../render-page/window-post-message-communicator/hooks/use-send-to-renderer'
|
import { useSendToRenderer } from '../../../render-page/window-post-message-communicator/hooks/use-send-to-renderer'
|
||||||
|
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends the current dark mode setting to the renderer.
|
* Sends the current dark mode setting to the renderer.
|
||||||
|
@ -16,15 +16,15 @@ import { useSendToRenderer } from '../../../render-page/window-post-message-comm
|
||||||
* @param rendererReady Defines if the target renderer is ready
|
* @param rendererReady Defines if the target renderer is ready
|
||||||
*/
|
*/
|
||||||
export const useSendDarkModeStatusToRenderer = (forcedDarkMode: boolean | undefined, rendererReady: boolean): void => {
|
export const useSendDarkModeStatusToRenderer = (forcedDarkMode: boolean | undefined, rendererReady: boolean): void => {
|
||||||
const savedDarkMode = useIsDarkModeActivated()
|
const darkModePreference = useApplicationState((state) => state.darkMode.darkModePreference)
|
||||||
|
|
||||||
useSendToRenderer(
|
useSendToRenderer(
|
||||||
useMemo(
|
useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
type: CommunicationMessageType.SET_DARKMODE,
|
type: CommunicationMessageType.SET_DARKMODE,
|
||||||
activated: forcedDarkMode ?? savedDarkMode
|
preference: darkModePreference
|
||||||
}),
|
}),
|
||||||
[forcedDarkMode, savedDarkMode]
|
[darkModePreference]
|
||||||
),
|
),
|
||||||
rendererReady
|
rendererReady
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { LanguagePicker } from './language-picker'
|
|
||||||
import { PoweredByLinks } from './powered-by-links'
|
import { PoweredByLinks } from './powered-by-links'
|
||||||
import { SocialLink } from './social-links'
|
import { SocialLink } from './social-links'
|
||||||
|
|
||||||
|
@ -15,7 +14,6 @@ import { SocialLink } from './social-links'
|
||||||
export const Footer: React.FC = () => {
|
export const Footer: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<footer className='text-light small'>
|
<footer className='text-light small'>
|
||||||
<LanguagePicker />
|
|
||||||
<PoweredByLinks />
|
<PoweredByLinks />
|
||||||
<SocialLink />
|
<SocialLink />
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { NewUserNoteButton } from '../new-user-note-button'
|
||||||
import { SignInButton } from '../sign-in-button'
|
import { SignInButton } from '../sign-in-button'
|
||||||
import { UserDropdown } from '../user-dropdown'
|
import { UserDropdown } from '../user-dropdown'
|
||||||
import { cypressId } from '../../../../utils/cypress-attribute'
|
import { cypressId } from '../../../../utils/cypress-attribute'
|
||||||
|
import { SettingsButton } from '../../../layout/settings-dialog/settings-button'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a header bar for the intro and history page.
|
* Renders a header bar for the intro and history page.
|
||||||
|
@ -33,6 +34,7 @@ const HeaderBar: React.FC = () => {
|
||||||
</HeaderNavLink>
|
</HeaderNavLink>
|
||||||
</div>
|
</div>
|
||||||
<div className='d-inline-flex'>
|
<div className='d-inline-flex'>
|
||||||
|
<SettingsButton className={'p-1 mx-2'} variant={'outline-light'} />
|
||||||
{!userExists ? (
|
{!userExists ? (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<span className={'mx-1 d-flex'}>
|
<span className={'mx-1 d-flex'}>
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { ListGroup } from 'react-bootstrap'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { SettingLine } from '../utils/setting-line'
|
||||||
|
import { LigatureSettingButtonGroup } from './ligature-setting-button-group'
|
||||||
|
import { SmartPasteSettingButtonGroup } from './smart-paste-setting-button-group'
|
||||||
|
import { SyncScrollSettingButtonGroup } from './sync-scroll-setting-button-group'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the editor specific settings.
|
||||||
|
*/
|
||||||
|
export const EditorSettingsTabContent: React.FC = () => {
|
||||||
|
useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListGroup>
|
||||||
|
<SettingLine i18nKey={'editor.ligatures'}>
|
||||||
|
<LigatureSettingButtonGroup />
|
||||||
|
</SettingLine>
|
||||||
|
<SettingLine i18nKey={'editor.smartPaste'}>
|
||||||
|
<SmartPasteSettingButtonGroup />
|
||||||
|
</SettingLine>
|
||||||
|
<SettingLine i18nKey={'editor.syncScroll'}>
|
||||||
|
<SyncScrollSettingButtonGroup />
|
||||||
|
</SettingLine>
|
||||||
|
</ListGroup>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { OnOffButtonGroup } from '../utils/on-off-button-group'
|
||||||
|
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||||
|
import { setEditorLigatures } from '../../../../redux/editor/methods'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to change if ligatures should be used or not in the editor.
|
||||||
|
*/
|
||||||
|
export const LigatureSettingButtonGroup: React.FC = () => {
|
||||||
|
const enabled = useApplicationState((state) => state.editorConfig.ligatures)
|
||||||
|
return <OnOffButtonGroup value={enabled} onSelect={setEditorLigatures} />
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { OnOffButtonGroup } from '../utils/on-off-button-group'
|
||||||
|
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||||
|
import { setEditorSmartPaste } from '../../../../redux/editor/methods'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to change if smart paste should be used in the editor.
|
||||||
|
*/
|
||||||
|
export const SmartPasteSettingButtonGroup: React.FC = () => {
|
||||||
|
const enabled = useApplicationState((state) => state.editorConfig.smartPaste)
|
||||||
|
return <OnOffButtonGroup value={enabled} onSelect={setEditorSmartPaste} />
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { OnOffButtonGroup } from '../utils/on-off-button-group'
|
||||||
|
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||||
|
import { setEditorSyncScroll } from '../../../../redux/editor/methods'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to change if editor and rendering should scroll in sync.
|
||||||
|
*/
|
||||||
|
export const SyncScrollSettingButtonGroup: React.FC = () => {
|
||||||
|
const enabled = useApplicationState((state) => state.editorConfig.syncScroll)
|
||||||
|
return <OnOffButtonGroup value={enabled} onSelect={setEditorSyncScroll} />
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useCallback } from 'react'
|
||||||
|
import { ToggleButtonGroup } from 'react-bootstrap'
|
||||||
|
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||||
|
import { SettingsToggleButton } from '../utils/settings-toggle-button'
|
||||||
|
import { setDarkModePreference } from '../../../../redux/dark-mode/methods'
|
||||||
|
import { DarkModePreference } from '../../../../redux/dark-mode/types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to change if the app should enforce dark mode, light mode or let the browser decide.
|
||||||
|
*/
|
||||||
|
const DarkModeSettingButtonGroup: React.FC = () => {
|
||||||
|
const darkModePreference = useApplicationState((state) => state.darkMode.darkModePreference)
|
||||||
|
|
||||||
|
const onSelect = useCallback((value: DarkModePreference) => setDarkModePreference(value), [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToggleButtonGroup type='radio' name='dark-mode'>
|
||||||
|
<SettingsToggleButton
|
||||||
|
onSelect={onSelect}
|
||||||
|
value={DarkModePreference.DARK}
|
||||||
|
selected={darkModePreference === DarkModePreference.DARK}
|
||||||
|
i18nKeyLabel={'settings.global.darkMode.dark.label'}
|
||||||
|
i18nKeyTooltip={'settings.global.darkMode.dark.tooltip'}
|
||||||
|
/>
|
||||||
|
<SettingsToggleButton
|
||||||
|
onSelect={onSelect}
|
||||||
|
value={DarkModePreference.LIGHT}
|
||||||
|
selected={darkModePreference === DarkModePreference.LIGHT}
|
||||||
|
i18nKeyLabel={'settings.global.darkMode.light.label'}
|
||||||
|
i18nKeyTooltip={'settings.global.darkMode.light.tooltip'}
|
||||||
|
/>
|
||||||
|
<SettingsToggleButton
|
||||||
|
onSelect={onSelect}
|
||||||
|
value={DarkModePreference.AUTO}
|
||||||
|
selected={darkModePreference === DarkModePreference.AUTO}
|
||||||
|
i18nKeyLabel={'settings.global.darkMode.browser.label'}
|
||||||
|
i18nKeyTooltip={'settings.global.darkMode.browser.tooltip'}
|
||||||
|
/>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DarkModeSettingButtonGroup }
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { DarkModeSettingButtonGroup } from './dark-mode-setting-button-group'
|
||||||
|
import { ListGroup } from 'react-bootstrap'
|
||||||
|
import { LanguagePicker } from './language-picker'
|
||||||
|
import { SettingLine } from '../utils/setting-line'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains global settings that influence every page of the app.
|
||||||
|
*/
|
||||||
|
export const GlobalSettingsTabContent: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<ListGroup>
|
||||||
|
<SettingLine i18nKey={'global.darkMode'}>
|
||||||
|
<DarkModeSettingButtonGroup />
|
||||||
|
</SettingLine>
|
||||||
|
<SettingLine i18nKey={'global.language'}>
|
||||||
|
<LanguagePicker />
|
||||||
|
</SettingLine>
|
||||||
|
</ListGroup>
|
||||||
|
)
|
||||||
|
}
|
|
@ -8,8 +8,8 @@ import { Settings } from 'luxon'
|
||||||
import React, { useCallback, useMemo } from 'react'
|
import React, { useCallback, useMemo } from 'react'
|
||||||
import { Form } from 'react-bootstrap'
|
import { Form } from 'react-bootstrap'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Logger } from '../../../utils/logger'
|
import { Logger } from '../../../../utils/logger'
|
||||||
import { cypressId } from '../../../utils/cypress-attribute'
|
import { cypressId } from '../../../../utils/cypress-attribute'
|
||||||
|
|
||||||
const log = new Logger('LanguagePicker')
|
const log = new Logger('LanguagePicker')
|
||||||
const languages = {
|
const languages = {
|
||||||
|
@ -88,8 +88,7 @@ export const LanguagePicker: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<Form.Select
|
<Form.Select
|
||||||
as='select'
|
as='select'
|
||||||
size='sm'
|
className='w-auto'
|
||||||
className='mb-2 mx-auto w-auto'
|
|
||||||
value={languageCode}
|
value={languageCode}
|
||||||
onChange={onChangeLang}
|
onChange={onChangeLang}
|
||||||
{...cypressId('language-picker')}>
|
{...cypressId('language-picker')}>
|
26
src/components/layout/settings-dialog/settings-button.tsx
Normal file
26
src/components/layout/settings-dialog/settings-button.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Fragment } from 'react'
|
||||||
|
import { useBooleanState } from '../../../hooks/common/use-boolean-state'
|
||||||
|
import { IconButton } from '../../common/icon-button/icon-button'
|
||||||
|
import { SettingsModal } from './settings-modal'
|
||||||
|
import type { ButtonProps } from 'react-bootstrap'
|
||||||
|
import { cypressId } from '../../../utils/cypress-attribute'
|
||||||
|
|
||||||
|
export type SettingsButtonProps = Omit<ButtonProps, 'onClick'>
|
||||||
|
/**
|
||||||
|
* Renders a button that opens a settings modal.
|
||||||
|
*/
|
||||||
|
export const SettingsButton: React.FC<SettingsButtonProps> = (props) => {
|
||||||
|
const [show, showModal, hideModal] = useBooleanState(false)
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<IconButton {...props} {...cypressId('settingsButton')} onClick={showModal} icon={'cog'} />
|
||||||
|
<SettingsModal show={show} onHide={hideModal} />
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
42
src/components/layout/settings-dialog/settings-modal.tsx
Normal file
42
src/components/layout/settings-dialog/settings-modal.tsx
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import type { CommonModalProps } from '../../common/modals/common-modal'
|
||||||
|
import { CommonModal } from '../../common/modals/common-modal'
|
||||||
|
import { Modal, Tab, Tabs } from 'react-bootstrap'
|
||||||
|
import { t } from 'i18next'
|
||||||
|
import { GlobalSettingsTabContent } from './global/global-settings-tab-content'
|
||||||
|
import { EditorSettingsTabContent } from './editor/editor-settings-tab-content'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows global and scope specific settings
|
||||||
|
*
|
||||||
|
* @param show if the modal should be visible
|
||||||
|
* @param onHide callback that is executed if the modal should be closed
|
||||||
|
*/
|
||||||
|
export const SettingsModal: React.FC<CommonModalProps> = ({ show, onHide }) => {
|
||||||
|
return (
|
||||||
|
<CommonModal
|
||||||
|
show={show}
|
||||||
|
modalSize={'lg'}
|
||||||
|
onHide={onHide}
|
||||||
|
titleIcon={'cog'}
|
||||||
|
title={'settings.title'}
|
||||||
|
showCloseButton={true}>
|
||||||
|
<Modal.Body>
|
||||||
|
<Tabs navbar={false} variant={'pills'} defaultActiveKey={'global'}>
|
||||||
|
<Tab title={t('settings.global.label')} eventKey={'global'}>
|
||||||
|
<GlobalSettingsTabContent />
|
||||||
|
</Tab>
|
||||||
|
<Tab title={t('settings.editor.label')} eventKey={'editor'}>
|
||||||
|
<EditorSettingsTabContent />
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</Modal.Body>
|
||||||
|
</CommonModal>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Settings On-Off Button Group can switch value 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
data-testid="onOffButtonGroupOn"
|
||||||
|
name="dark-mode"
|
||||||
|
title="common.on"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.on
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
data-testid="onOffButtonGroupOff"
|
||||||
|
name="dark-mode"
|
||||||
|
title="common.off"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.off
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Settings On-Off Button Group can switch value 2`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
data-testid="onOffButtonGroupOn"
|
||||||
|
name="dark-mode"
|
||||||
|
title="common.on"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.on
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
data-testid="onOffButtonGroupOff"
|
||||||
|
name="dark-mode"
|
||||||
|
title="common.off"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.off
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { act, render, screen } from '@testing-library/react'
|
||||||
|
import { OnOffButtonGroup } from './on-off-button-group'
|
||||||
|
import { mockI18n } from '../../../markdown-renderer/test-utils/mock-i18n'
|
||||||
|
|
||||||
|
describe('Settings On-Off Button Group', () => {
|
||||||
|
beforeAll(mockI18n)
|
||||||
|
|
||||||
|
it('can switch value', async () => {
|
||||||
|
let value = false
|
||||||
|
const onSelect = (newValue: boolean) => (value = newValue)
|
||||||
|
|
||||||
|
const view = render(<OnOffButtonGroup value={value} onSelect={onSelect} />)
|
||||||
|
expect(view.container).toMatchSnapshot()
|
||||||
|
const onButton = await screen.findByTestId('onOffButtonGroupOn')
|
||||||
|
act(() => {
|
||||||
|
onButton.click()
|
||||||
|
})
|
||||||
|
expect(value).toBeTruthy()
|
||||||
|
|
||||||
|
view.rerender(<OnOffButtonGroup value={value} onSelect={onSelect} />)
|
||||||
|
expect(view.container).toMatchSnapshot()
|
||||||
|
const offButton = await screen.findByTestId('onOffButtonGroupOff')
|
||||||
|
act(() => {
|
||||||
|
offButton.click()
|
||||||
|
})
|
||||||
|
expect(value).toBeFalsy()
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useCallback, useMemo } from 'react'
|
||||||
|
import { ToggleButtonGroup } from 'react-bootstrap'
|
||||||
|
import { SettingsToggleButton } from './settings-toggle-button'
|
||||||
|
import { testId } from '../../../../utils/test-id'
|
||||||
|
|
||||||
|
enum OnOffState {
|
||||||
|
ON,
|
||||||
|
OFF
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OnOffButtonGroupProps {
|
||||||
|
value: boolean
|
||||||
|
onSelect: (value: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a button group that is used to toggle a setting on or off.
|
||||||
|
*
|
||||||
|
* @param onSelect callback that is executed if the on/off value has changed
|
||||||
|
* @param value the current on/off value that should be visible
|
||||||
|
*/
|
||||||
|
export const OnOffButtonGroup: React.FC<OnOffButtonGroupProps> = ({ onSelect, value }) => {
|
||||||
|
const buttonGroupValue = useMemo(() => (value ? OnOffState.ON : OnOffState.OFF), [value])
|
||||||
|
const onButtonSelect = useCallback(
|
||||||
|
(value: number) => {
|
||||||
|
onSelect(value === OnOffState.ON)
|
||||||
|
},
|
||||||
|
[onSelect]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToggleButtonGroup type='radio' name='dark-mode' value={buttonGroupValue}>
|
||||||
|
<SettingsToggleButton
|
||||||
|
onSelect={onButtonSelect}
|
||||||
|
selected={buttonGroupValue === OnOffState.ON}
|
||||||
|
value={OnOffState.ON}
|
||||||
|
i18nKeyTooltip={'common.on'}
|
||||||
|
i18nKeyLabel={'common.on'}
|
||||||
|
{...testId('onOffButtonGroupOn')}
|
||||||
|
/>
|
||||||
|
<SettingsToggleButton
|
||||||
|
onSelect={onButtonSelect}
|
||||||
|
selected={buttonGroupValue === OnOffState.OFF}
|
||||||
|
value={OnOffState.OFF}
|
||||||
|
i18nKeyTooltip={'common.off'}
|
||||||
|
i18nKeyLabel={'common.off'}
|
||||||
|
{...testId('onOffButtonGroupOff')}
|
||||||
|
/>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
)
|
||||||
|
}
|
36
src/components/layout/settings-dialog/utils/setting-line.tsx
Normal file
36
src/components/layout/settings-dialog/utils/setting-line.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { PropsWithChildren } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import { Col, ListGroup, Row } from 'react-bootstrap'
|
||||||
|
import { Trans } from 'react-i18next'
|
||||||
|
|
||||||
|
export interface SettingLineProps {
|
||||||
|
i18nKey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders one setting with label and help text
|
||||||
|
*
|
||||||
|
* @param i18nKey The i18n key that is used as namespace for label and help
|
||||||
|
* @param children The setting control that should be placed in this line
|
||||||
|
*/
|
||||||
|
export const SettingLine: React.FC<PropsWithChildren<SettingLineProps>> = ({ i18nKey, children }) => {
|
||||||
|
return (
|
||||||
|
<ListGroup.Item>
|
||||||
|
<Row>
|
||||||
|
<Col md={3}>
|
||||||
|
<Trans i18nKey={`settings.${i18nKey}.label`} />
|
||||||
|
</Col>
|
||||||
|
<Col md={4}>{children}</Col>
|
||||||
|
<Col md={5}>
|
||||||
|
<Trans i18nKey={`settings.${i18nKey}.help`} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</ListGroup.Item>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useCallback, useMemo } from 'react'
|
||||||
|
import type { ButtonProps } from 'react-bootstrap'
|
||||||
|
import { Button } from 'react-bootstrap'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import type { PropsWithDataTestId } from '../../../../utils/test-id'
|
||||||
|
|
||||||
|
type DarkModeToggleButtonProps = Omit<ButtonProps, 'onSelect'> &
|
||||||
|
PropsWithDataTestId & {
|
||||||
|
onSelect: (value: number) => void
|
||||||
|
selected: boolean
|
||||||
|
value: number
|
||||||
|
i18nKeyLabel: string
|
||||||
|
i18nKeyTooltip: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A button that is used in a toggle group to change settings.
|
||||||
|
*
|
||||||
|
* @param settingI18nKey The partial i18n key in the "settings" namespace for the setting
|
||||||
|
* @param selected Defines if the button should be rendered as selected
|
||||||
|
* @param onSelect Callback that is executed when the button is selected
|
||||||
|
* @param value The value of the button that is sent back through the onSelect callback
|
||||||
|
* @param props Other button props
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export const SettingsToggleButton = ({
|
||||||
|
i18nKeyLabel,
|
||||||
|
i18nKeyTooltip,
|
||||||
|
selected,
|
||||||
|
onSelect,
|
||||||
|
value,
|
||||||
|
...props
|
||||||
|
}: DarkModeToggleButtonProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const title = useMemo(() => t(i18nKeyTooltip), [i18nKeyTooltip, t])
|
||||||
|
|
||||||
|
const onChange = useCallback(() => {
|
||||||
|
if (!selected) {
|
||||||
|
onSelect(value)
|
||||||
|
}
|
||||||
|
}, [onSelect, selected, value])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button {...props} variant={selected ? 'secondary' : 'outline-secondary'} title={title} onClick={onChange}>
|
||||||
|
<Trans i18nKey={i18nKeyLabel} />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import type { ScrollState } from '../editor-page/synced-scroll/scroll-props'
|
import type { ScrollState } from '../editor-page/synced-scroll/scroll-props'
|
||||||
import type { BaseConfiguration } from './window-post-message-communicator/rendering-message'
|
import type { BaseConfiguration } from './window-post-message-communicator/rendering-message'
|
||||||
import { CommunicationMessageType, RendererType } from './window-post-message-communicator/rendering-message'
|
import { CommunicationMessageType, RendererType } from './window-post-message-communicator/rendering-message'
|
||||||
import { setDarkMode } from '../../redux/dark-mode/methods'
|
import { setDarkModePreference } from '../../redux/dark-mode/methods'
|
||||||
import { MarkdownDocument } from './markdown-document'
|
import { MarkdownDocument } from './markdown-document'
|
||||||
import { countWords } from './word-counter'
|
import { countWords } from './word-counter'
|
||||||
import { useRendererToEditorCommunicator } from '../editor-page/render-context/renderer-to-editor-communicator-context-provider'
|
import { useRendererToEditorCommunicator } from '../editor-page/render-context/renderer-to-editor-communicator-context-provider'
|
||||||
|
@ -55,7 +55,7 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
||||||
|
|
||||||
useRendererReceiveHandler(
|
useRendererReceiveHandler(
|
||||||
CommunicationMessageType.SET_DARKMODE,
|
CommunicationMessageType.SET_DARKMODE,
|
||||||
useCallback((values) => setDarkMode(values.activated), [])
|
useCallback((values) => setDarkModePreference(values.preference), [])
|
||||||
)
|
)
|
||||||
|
|
||||||
useRendererReceiveHandler(
|
useRendererReceiveHandler(
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
import type { ScrollState } from '../../editor-page/synced-scroll/scroll-props'
|
import type { ScrollState } from '../../editor-page/synced-scroll/scroll-props'
|
||||||
import type { SlideOptions } from '../../../redux/note-details/types/slide-show-options'
|
import type { SlideOptions } from '../../../redux/note-details/types/slide-show-options'
|
||||||
|
import type { DarkModePreference } from '../../../redux/dark-mode/types'
|
||||||
|
|
||||||
export enum CommunicationMessageType {
|
export enum CommunicationMessageType {
|
||||||
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
|
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
|
||||||
|
@ -29,7 +30,7 @@ export interface NoPayloadMessage<TYPE extends CommunicationMessageType> {
|
||||||
|
|
||||||
export interface SetDarkModeMessage {
|
export interface SetDarkModeMessage {
|
||||||
type: CommunicationMessageType.SET_DARKMODE
|
type: CommunicationMessageType.SET_DARKMODE
|
||||||
activated: boolean
|
preference: DarkModePreference
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtensionEvent {
|
export interface ExtensionEvent {
|
||||||
|
|
|
@ -9,12 +9,18 @@ import { FlowChart } from './flowchart'
|
||||||
import type * as flowchartJsModule from 'flowchart.js'
|
import type * as flowchartJsModule from 'flowchart.js'
|
||||||
import { mockI18n } from '../../../components/markdown-renderer/test-utils/mock-i18n'
|
import { mockI18n } from '../../../components/markdown-renderer/test-utils/mock-i18n'
|
||||||
import { StoreProvider } from '../../../redux/store-provider'
|
import { StoreProvider } from '../../../redux/store-provider'
|
||||||
|
import * as useMediaQuery from '@restart/hooks/useMediaQuery'
|
||||||
|
|
||||||
|
jest.mock('@restart/hooks/useMediaQuery')
|
||||||
|
|
||||||
describe('Flowchart', () => {
|
describe('Flowchart', () => {
|
||||||
const successText = 'Flowchart rendering succeeded!'
|
const successText = 'Flowchart rendering succeeded!'
|
||||||
const expectedValidFlowchartCode = 'test code'
|
const expectedValidFlowchartCode = 'test code'
|
||||||
|
|
||||||
beforeAll(() => mockI18n())
|
beforeAll(async () => {
|
||||||
|
jest.spyOn(useMediaQuery, 'default').mockImplementation(() => false)
|
||||||
|
await mockI18n()
|
||||||
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.resetModules()
|
jest.resetModules()
|
||||||
|
|
|
@ -13,7 +13,7 @@ import type { CodeProps } from '../../../components/markdown-renderer/replace-co
|
||||||
import { AsyncLoadingBoundary } from '../../../components/common/async-loading-boundary'
|
import { AsyncLoadingBoundary } from '../../../components/common/async-loading-boundary'
|
||||||
import { ShowIf } from '../../../components/common/show-if/show-if'
|
import { ShowIf } from '../../../components/common/show-if/show-if'
|
||||||
import { testId } from '../../../utils/test-id'
|
import { testId } from '../../../utils/test-id'
|
||||||
import { useIsDarkModeActivated } from '../../../hooks/common/use-is-dark-mode-activated'
|
import { useDarkModeState } from '../../../hooks/common/use-dark-mode-state'
|
||||||
import { Logger } from '../../../utils/logger'
|
import { Logger } from '../../../utils/logger'
|
||||||
|
|
||||||
const log = new Logger('FlowChart')
|
const log = new Logger('FlowChart')
|
||||||
|
@ -27,7 +27,7 @@ const log = new Logger('FlowChart')
|
||||||
export const FlowChart: React.FC<CodeProps> = ({ code }) => {
|
export const FlowChart: React.FC<CodeProps> = ({ code }) => {
|
||||||
const diagramRef = useRef<HTMLDivElement>(null)
|
const diagramRef = useRef<HTMLDivElement>(null)
|
||||||
const [syntaxError, setSyntaxError] = useState(false)
|
const [syntaxError, setSyntaxError] = useState(false)
|
||||||
const darkModeActivated = useIsDarkModeActivated()
|
const darkModeActivated = useDarkModeState()
|
||||||
|
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
|
||||||
|
|
|
@ -5,22 +5,48 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useIsDarkModeActivated } from './use-is-dark-mode-activated'
|
import { useApplicationState } from './use-application-state'
|
||||||
|
import { isClientSideRendering } from '../../utils/is-client-side-rendering'
|
||||||
|
import { Logger } from '../../utils/logger'
|
||||||
|
import useMediaQuery from '@restart/hooks/useMediaQuery'
|
||||||
|
import { DarkModePreference } from '../../redux/dark-mode/types'
|
||||||
|
|
||||||
|
const logger = new Logger('useApplyDarkMode')
|
||||||
|
|
||||||
|
export const DARK_MODE_LOCAL_STORAGE_KEY = 'forcedDarkMode'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies the `dark` css class to the body tag according to the dark mode state.
|
* Applies the `dark` css class to the body tag according to the dark mode state.
|
||||||
*/
|
*/
|
||||||
export const useApplyDarkMode = (): void => {
|
export const useApplyDarkMode = (): void => {
|
||||||
const darkModeActivated = useIsDarkModeActivated()
|
const preference = useApplicationState((state) => state.darkMode.darkModePreference)
|
||||||
|
const isBrowserPreferringDark = useMediaQuery('(prefers-color-scheme: dark)')
|
||||||
|
|
||||||
|
useEffect(() => saveToLocalStorage(preference), [preference])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (darkModeActivated) {
|
if (preference === DarkModePreference.DARK || (preference === DarkModePreference.AUTO && isBrowserPreferringDark)) {
|
||||||
window.document.body.classList.add('dark')
|
window.document.body.classList.add('dark')
|
||||||
} else {
|
} else {
|
||||||
window.document.body.classList.remove('dark')
|
window.document.body.classList.remove('dark')
|
||||||
}
|
}
|
||||||
return () => {
|
}, [isBrowserPreferringDark, preference])
|
||||||
window.document.body.classList.remove('dark')
|
|
||||||
}
|
useEffect(() => () => window.document.body.classList.remove('dark'), [])
|
||||||
}, [darkModeActivated])
|
}
|
||||||
|
|
||||||
|
export const saveToLocalStorage = (preference: DarkModePreference): void => {
|
||||||
|
if (!isClientSideRendering()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (preference === DarkModePreference.DARK) {
|
||||||
|
window.localStorage.setItem(DARK_MODE_LOCAL_STORAGE_KEY, 'dark')
|
||||||
|
} else if (preference === DarkModePreference.LIGHT) {
|
||||||
|
window.localStorage.setItem(DARK_MODE_LOCAL_STORAGE_KEY, 'light')
|
||||||
|
} else if (preference === DarkModePreference.AUTO) {
|
||||||
|
window.localStorage.removeItem(DARK_MODE_LOCAL_STORAGE_KEY)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Saving to local storage failed', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
21
src/hooks/common/use-dark-mode-state.ts
Normal file
21
src/hooks/common/use-dark-mode-state.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useApplicationState } from './use-application-state'
|
||||||
|
import useMediaQuery from '@restart/hooks/useMediaQuery'
|
||||||
|
import { DarkModePreference } from '../../redux/dark-mode/types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses the user settings and the browser preference to determine if dark mode should be used.
|
||||||
|
*
|
||||||
|
* @return The current state of the dark mode.
|
||||||
|
*/
|
||||||
|
export const useDarkModeState = (): boolean => {
|
||||||
|
const preference = useApplicationState((state) => state.darkMode.darkModePreference)
|
||||||
|
const isBrowserPreferringDark = useMediaQuery('(prefers-color-scheme: dark)')
|
||||||
|
|
||||||
|
return preference === DarkModePreference.DARK || (preference === DarkModePreference.AUTO && isBrowserPreferringDark)
|
||||||
|
}
|
|
@ -1,16 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useApplicationState } from './use-application-state'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts the current state of the dark mode from the global application state.
|
|
||||||
*
|
|
||||||
* @return The current state of the dark mode.
|
|
||||||
*/
|
|
||||||
export const useIsDarkModeActivated = (): boolean => {
|
|
||||||
return useApplicationState((state) => state.darkMode.darkMode)
|
|
||||||
}
|
|
|
@ -1,30 +1,16 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { store } from '..'
|
import { store } from '..'
|
||||||
import type { DarkModeConfig, SetDarkModeConfigAction } from './types'
|
import type { DarkModeConfigAction, DarkModePreference } from './types'
|
||||||
import { DarkModeConfigActionType } from './types'
|
import { DarkModeConfigActionType } from './types'
|
||||||
import { Logger } from '../../utils/logger'
|
|
||||||
|
|
||||||
const log = new Logger('Redux > DarkMode')
|
export const setDarkModePreference = (darkModePreference: DarkModePreference): void => {
|
||||||
|
|
||||||
export const setDarkMode = (darkMode: boolean): void => {
|
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: DarkModeConfigActionType.SET_DARK_MODE,
|
type: DarkModeConfigActionType.SET_DARK_MODE,
|
||||||
darkMode: darkMode
|
darkModePreference
|
||||||
} as SetDarkModeConfigAction)
|
} as DarkModeConfigAction)
|
||||||
}
|
|
||||||
|
|
||||||
export const saveToLocalStorage = (darkModeConfig: DarkModeConfig): void => {
|
|
||||||
if (!window) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
window.localStorage.setItem('nightMode', String(darkModeConfig.darkMode))
|
|
||||||
} catch (error) {
|
|
||||||
log.error('Saving to local storage failed', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,26 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Reducer } from 'redux'
|
import type { Reducer } from 'redux'
|
||||||
import { saveToLocalStorage } from './methods'
|
import type { DarkModeConfig, DarkModeConfigAction } from './types'
|
||||||
import type { DarkModeConfig, DarkModeConfigActions } from './types'
|
import { DarkModeConfigActionType, DarkModePreference } from './types'
|
||||||
import { DarkModeConfigActionType } from './types'
|
|
||||||
|
|
||||||
const initalState: DarkModeConfig = {
|
const initialState: DarkModeConfig = {
|
||||||
darkMode: false
|
darkModePreference: DarkModePreference.AUTO
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DarkModeConfigReducer: Reducer<DarkModeConfig, DarkModeConfigActions> = (
|
export const DarkModeConfigReducer: Reducer<DarkModeConfig, DarkModeConfigAction> = (
|
||||||
state: DarkModeConfig = initalState,
|
state: DarkModeConfig = initialState,
|
||||||
action: DarkModeConfigActions
|
action: DarkModeConfigAction
|
||||||
) => {
|
) => {
|
||||||
let darkModeConfigState: DarkModeConfig
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case DarkModeConfigActionType.SET_DARK_MODE:
|
case DarkModeConfigActionType.SET_DARK_MODE:
|
||||||
darkModeConfigState = {
|
return {
|
||||||
...state,
|
darkModePreference: action.darkModePreference
|
||||||
darkMode: action.darkMode
|
|
||||||
}
|
}
|
||||||
saveToLocalStorage(darkModeConfigState)
|
|
||||||
return darkModeConfigState
|
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -10,13 +10,14 @@ export enum DarkModeConfigActionType {
|
||||||
SET_DARK_MODE = 'dark-mode/set'
|
SET_DARK_MODE = 'dark-mode/set'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum DarkModePreference {
|
||||||
|
DARK,
|
||||||
|
LIGHT,
|
||||||
|
AUTO
|
||||||
|
}
|
||||||
|
|
||||||
export interface DarkModeConfig {
|
export interface DarkModeConfig {
|
||||||
darkMode: boolean
|
darkModePreference: DarkModePreference
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DarkModeConfigActions = SetDarkModeConfigAction
|
export type DarkModeConfigAction = Action<DarkModeConfigActionType.SET_DARK_MODE> & DarkModeConfig
|
||||||
|
|
||||||
export interface SetDarkModeConfigAction extends Action<DarkModeConfigActionType> {
|
|
||||||
type: DarkModeConfigActionType.SET_DARK_MODE
|
|
||||||
darkMode: boolean
|
|
||||||
}
|
|
||||||
|
|
|
@ -18,18 +18,14 @@ export interface PropsWithDataCypressId {
|
||||||
* @param identifier The identifier that is used to find the element
|
* @param identifier The identifier that is used to find the element
|
||||||
* @return An object if in test mode, {@link undefined} otherwise.
|
* @return An object if in test mode, {@link undefined} otherwise.
|
||||||
*/
|
*/
|
||||||
export const cypressId = (
|
export const cypressId = (identifier: string | undefined | PropsWithDataCypressId): PropsWithDataCypressId => {
|
||||||
identifier: string | undefined | PropsWithDataCypressId
|
|
||||||
): Record<'data-cypress-id', string> | undefined => {
|
|
||||||
if (!isTestMode || !identifier) {
|
if (!isTestMode || !identifier) {
|
||||||
return
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const attributeContent = typeof identifier === 'string' ? identifier : identifier['data-cypress-id']
|
const attributeContent = typeof identifier === 'string' ? identifier : identifier['data-cypress-id']
|
||||||
|
|
||||||
if (attributeContent !== undefined) {
|
return attributeContent !== undefined ? { 'data-cypress-id': attributeContent } : {}
|
||||||
return { 'data-cypress-id': attributeContent }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,6 +4,10 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export interface PropsWithDataTestId {
|
||||||
|
'data-testid'?: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an object with the "data-testid" attribute that is used to find
|
* Returns an object with the "data-testid" attribute that is used to find
|
||||||
* elements in unit tests.
|
* elements in unit tests.
|
||||||
|
@ -12,8 +16,6 @@
|
||||||
* @param identifier The identifier that is used to find the element
|
* @param identifier The identifier that is used to find the element
|
||||||
* @return An object if in test mode, undefined otherwise.
|
* @return An object if in test mode, undefined otherwise.
|
||||||
*/
|
*/
|
||||||
export const testId = (identifier: string): Record<'data-testid', string> | undefined => {
|
export const testId = (identifier: string): PropsWithDataTestId => {
|
||||||
if (process.env.NODE_ENV === 'test') {
|
return process.env.NODE_ENV === 'test' ? { 'data-testid': identifier } : {}
|
||||||
return { 'data-testid': identifier }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue