omnivore/packages/web/components/templates/article/ReaderSettingsControl.tsx

785 lines
20 KiB
TypeScript
Raw Permalink Normal View History

2023-03-06 08:21:20 +00:00
import { HStack, VStack, SpanBox } from '../../elements/LayoutPrimitives'
import { Button } from '../../elements/Button'
import { StyledText } from '../../elements/StyledText'
2023-02-28 10:01:57 +00:00
import { styled, theme, ThemeId } from '../../tokens/stitches.config'
import {
ArrowsHorizontal,
ArrowsInLineHorizontal,
CaretLeft,
CaretRight,
Check,
2024-06-10 09:34:38 +00:00
} from '@phosphor-icons/react'
import { TickedRangeSlider } from '../../elements/TickedRangeSlider'
import { showSuccessToast } from '../../../lib/toastHelpers'
2023-03-06 08:21:20 +00:00
import { ReaderSettings } from '../../../lib/hooks/useReaderSettings'
2023-06-20 05:36:36 +00:00
import { useCallback, useState } from 'react'
2024-02-16 04:25:45 +00:00
import { updateTheme } from '../../../lib/themeUpdater'
2023-02-28 10:01:57 +00:00
import { LineHeightIncreaseIcon } from '../../elements/images/LineHeightIncreaseIconProps'
import { LineHeightDecreaseIcon } from '../../elements/images/LineHeightDecreaseIcon'
import * as Switch from '@radix-ui/react-switch'
2024-02-14 09:12:28 +00:00
import { useCurrentTheme } from '../../../lib/hooks/useCurrentTheme'
import { useDarkModeListener } from '../../../lib/hooks/useDarkModeListener'
2022-04-12 19:58:04 +00:00
type ReaderSettingsProps = {
2023-02-28 10:01:57 +00:00
readerSettings: ReaderSettings
}
const HorizontalDivider = styled(SpanBox, {
width: '100%',
height: '1px',
background: `${theme.colors.grayLine.toString()}`,
})
2023-02-28 10:01:57 +00:00
const FONT_FAMILIES = [
'Inter',
'System Default',
'Merriweather',
'Lora',
'Open Sans',
'Roboto',
'Newsreader',
2023-11-22 04:03:52 +00:00
'Lexend',
'Montserrat',
2023-02-28 10:01:57 +00:00
'Crimson Text',
'OpenDyslexic',
'Source Serif Pro',
'LXGWWenKai',
'AtkinsonHyperlegible',
2023-10-03 12:46:30 +00:00
'IBMPlexSans',
2023-12-11 03:08:55 +00:00
'Fraunces',
'Literata',
2024-05-02 06:03:22 +00:00
'SuperNotesPro',
2023-02-28 10:01:57 +00:00
]
type SettingsProps = {
readerSettings: ReaderSettings
setShowAdvanced: (show: boolean) => void
}
2023-02-28 10:01:57 +00:00
export function ReaderSettingsControl(props: ReaderSettingsProps): JSX.Element {
const [showAdvanced, setShowAdvanced] = useState(false)
return (
<>
{showAdvanced ? (
<AdvancedSettings
readerSettings={props.readerSettings}
setShowAdvanced={setShowAdvanced}
/>
) : (
<BasicSettings
readerSettings={props.readerSettings}
setShowAdvanced={setShowAdvanced}
/>
)}
</>
)
}
function AdvancedSettings(props: SettingsProps): JSX.Element {
2023-06-20 05:36:36 +00:00
const { readerSettings } = props
return (
<VStack
css={{ width: '100%', minHeight: '320px', p: '10px' }}
alignment="start"
distribution="start"
>
<Button
style="plainIcon"
css={{
m: '10px',
mb: '20px',
display: 'flex',
fontSize: '12px',
fontWeight: '400',
fontFamily: '$display',
alignItems: 'center',
borderRadius: '6px',
gap: '5px',
'&:hover': {
textDecoration: 'underline',
},
}}
onClick={() => {
props.setShowAdvanced(false)
}}
>
<CaretLeft size={12} color="#969696" weight="bold" />
Back
</Button>
<HStack
css={{
width: '100%',
pr: '30px',
alignItems: 'center',
'&:hover': {
opacity: 0.8,
},
'&[data-state="on"]': {
bg: '$thBackground',
},
}}
alignment="start"
distribution="between"
>
<Label htmlFor="justify-text" css={{ width: '100%' }}>
<StyledText style="displaySettingsLabel" css={{ pl: '20px' }}>
Justify Text
</StyledText>
</Label>
<SwitchRoot
id="justify-text"
2023-06-20 05:36:36 +00:00
checked={readerSettings.justifyText ?? false}
onCheckedChange={(checked) => {
2023-06-20 05:36:36 +00:00
readerSettings.setJustifyText(checked)
}}
>
<SwitchThumb />
</SwitchRoot>
</HStack>
<HStack
css={{
width: '100%',
pr: '30px',
alignItems: 'center',
'&:hover': {
opacity: 0.8,
},
'&[data-state="on"]': {
bg: '$thBackground',
},
}}
alignment="start"
distribution="between"
>
<Label htmlFor="high-contrast-text" css={{ width: '100%' }}>
<StyledText style="displaySettingsLabel" css={{ pl: '20px' }}>
High Contrast Text
</StyledText>
</Label>
<SwitchRoot
id="high-contrast-text"
2023-06-20 05:36:36 +00:00
checked={readerSettings.highContrastText ?? false}
onCheckedChange={(checked) => {
2023-06-20 05:36:36 +00:00
readerSettings.setHighContrastText(checked)
}}
>
<SwitchThumb />
</SwitchRoot>
</HStack>
2024-02-07 07:22:42 +00:00
<HStack
css={{
width: '100%',
pr: '30px',
alignItems: 'center',
'&:hover': {
opacity: 0.8,
},
'&[data-state="on"]': {
bg: '$thBackground',
},
}}
alignment="start"
distribution="between"
>
<Label htmlFor="auto-highlight-mode" css={{ width: '100%' }}>
<StyledText style="displaySettingsLabel" css={{ pl: '20px' }}>
Right-to-left text
</StyledText>
</Label>
<SwitchRoot
id="rtl-text"
checked={readerSettings.textDirection == 'RTL'}
onCheckedChange={(checked) => {
readerSettings.setTextDirection(checked ? 'RTL' : 'LTR')
}}
>
<SwitchThumb />
</SwitchRoot>
</HStack>
<HStack
css={{
width: '100%',
pr: '30px',
alignItems: 'center',
2024-02-07 07:22:42 +00:00
'&:hover': {
opacity: 0.8,
},
'&[data-state="on"]': {
bg: '$thBackground',
},
}}
alignment="start"
distribution="between"
>
<Label htmlFor="auto-highlight-mode" css={{ width: '100%' }}>
<StyledText style="displaySettingsLabel" css={{ pl: '20px' }}>
Auto highlight mode
</StyledText>
</Label>
<SwitchRoot
id="high-contrast-text"
checked={readerSettings.highlightOnRelease ?? false}
onCheckedChange={(checked) => {
readerSettings.setHighlightOnRelease(checked)
}}
>
<SwitchThumb />
</SwitchRoot>
</HStack>
</VStack>
)
}
const SwitchRoot = styled(Switch.Root, {
all: 'unset',
width: 42,
height: 25,
backgroundColor: '$thBorderColor',
borderRadius: '9999px',
position: 'relative',
WebkitTapHighlightColor: 'rgba(0, 0, 0, 0)',
'&:focus': { boxShadow: `0 0 0 2px $thBorderColor` },
'&[data-state="checked"]': { backgroundColor: '$thBorderColor' },
})
const SwitchThumb = styled(Switch.Thumb, {
display: 'block',
width: 21,
height: 21,
backgroundColor: '$thTextContrast2',
borderRadius: '9999px',
transition: 'transform 100ms',
transform: 'translateX(2px)',
willChange: 'transform',
'&[data-state="checked"]': { transform: 'translateX(19px)' },
})
const Label = styled('label', {
color: 'white',
fontSize: 15,
lineHeight: 1,
})
function BasicSettings(props: SettingsProps): JSX.Element {
2023-06-20 05:36:36 +00:00
const { readerSettings } = props
return (
<VStack css={{ width: '100%' }}>
2023-06-20 05:36:36 +00:00
<FontControls readerSettings={readerSettings} />
2023-02-28 10:01:57 +00:00
<HorizontalDivider />
2023-06-20 05:36:36 +00:00
<LayoutControls readerSettings={readerSettings} />
2023-02-28 10:01:57 +00:00
<HorizontalDivider />
2024-06-06 03:39:22 +00:00
<SpanBox css={{ px: '10px', width: '100%' }}>
<ThemeSelector />
</SpanBox>
2023-02-28 10:01:57 +00:00
<HorizontalDivider />
<HStack
distribution="start"
alignment="center"
css={{ width: '100%', p: '10px' }}
>
2023-02-28 10:01:57 +00:00
<Button
style="plainIcon"
css={{
pl: '10px',
2023-02-28 10:01:57 +00:00
display: 'flex',
fontSize: '12px',
fontWeight: '600',
2023-03-03 05:29:36 +00:00
fontFamily: '$display',
2023-02-28 10:01:57 +00:00
'&:hover': {
textDecoration: 'underline',
},
}}
onClick={() => {
2023-06-20 05:36:36 +00:00
readerSettings.setFontFamily('Inter')
readerSettings.setMarginWidth(290)
readerSettings.setLineHeight(150)
readerSettings.actionHandler('resetReaderSettings')
showSuccessToast('Reader Preferences Reset', {
2023-02-28 10:01:57 +00:00
position: 'bottom-right',
})
2022-06-15 15:43:10 +00:00
}}
2023-02-28 10:01:57 +00:00
>
Reset
</Button>
<Button
style="plainIcon"
css={{
p: '5px',
display: 'flex',
fontSize: '12px',
fontWeight: '400',
fontFamily: '$display',
marginLeft: 'auto',
alignItems: 'center',
borderRadius: '6px',
gap: '5px',
'&:hover': {
textDecoration: 'underline',
},
}}
onClick={() => {
props.setShowAdvanced(true)
}}
>
Advanced Settings
<CaretRight size={12} color="#969696" weight="bold" />
</Button>
2023-02-28 10:01:57 +00:00
</HStack>
</VStack>
)
}
type FontControlsProps = {
readerSettings: ReaderSettings
}
function FontControls(props: FontControlsProps): JSX.Element {
2023-06-20 05:36:36 +00:00
const { readerSettings } = props
2023-02-28 10:01:57 +00:00
const FontSelect = styled('select', {
pl: '5px',
height: '30px',
minWidth: '100px',
display: 'flex',
alignItems: 'center',
fontSize: '12px',
background: '$thBackground',
border: '1px solid $thBorderColor',
2023-06-20 05:36:36 +00:00
fontFamily: readerSettings.fontFamily,
2023-02-28 10:01:57 +00:00
textTransform: 'capitalize',
borderRadius: '4px',
})
const handleFontSizeChange = useCallback(
(value: number) => {
2023-06-20 05:36:36 +00:00
readerSettings.actionHandler('setFontSize', value)
2023-02-28 10:01:57 +00:00
},
2023-06-20 05:36:36 +00:00
[readerSettings]
2023-02-28 10:01:57 +00:00
)
return (
<VStack css={{ width: '100%', pb: '20px' }}>
<HStack
distribution="start"
alignment="start"
css={{ width: '100%', p: '20px', pb: '10px' }}
>
<StyledText style="displaySettingsLabel">Font</StyledText>
<FontSelect
css={{ marginLeft: 'auto' }}
tabIndex={-1}
2023-06-20 05:36:36 +00:00
defaultValue={readerSettings.fontFamily}
2023-02-28 10:01:57 +00:00
onChange={(e: React.FormEvent<HTMLSelectElement>) => {
const font = e.currentTarget.value
if (FONT_FAMILIES.indexOf(font) < 0) {
return
}
2023-06-20 05:36:36 +00:00
readerSettings.setFontFamily(font)
2023-02-28 10:01:57 +00:00
}}
>
{FONT_FAMILIES.map((family) => (
<option key={`font-${family}`} value={family}>
{family}
</option>
))}
</FontSelect>
</HStack>
<HStack
css={{ px: '10px', width: '100%' }}
distribution="start"
alignment="center"
>
<Button
style="plainIcon"
css={{ py: '0px', width: '60px' }}
onClick={() => {
2023-06-20 05:36:36 +00:00
readerSettings.actionHandler('decrementFontSize')
2023-02-28 10:01:57 +00:00
}}
>
<SpanBox
css={{
fontFamily: '$display',
fontStyle: 'normal',
fontWeight: '400',
fontSize: '20px',
lineHeight: '20px',
textAlign: 'center',
color: '#969696',
width: '50px',
pr: '5px',
pb: '5px',
}}
>
a
</SpanBox>
</Button>
2023-02-28 10:01:57 +00:00
<TickedRangeSlider
min={10}
2023-08-25 09:29:09 +00:00
max={48}
2023-02-28 10:01:57 +00:00
step={2}
value={props.readerSettings.fontSize}
onChange={handleFontSizeChange}
2022-06-15 15:43:10 +00:00
/>
<Button
style="plainIcon"
css={{ py: '0px', width: '60px' }}
onClick={() => {
props.readerSettings.actionHandler('incrementFontSize')
2023-02-28 10:01:57 +00:00
}}
>
<SpanBox
css={{
fontFamily: '$display',
fontStyle: 'normal',
fontWeight: '400',
fontSize: '20px',
lineHeight: '20px',
textAlign: 'center',
color: '#969696',
width: '60px',
pl: '16px',
}}
>
A
</SpanBox>
</Button>
2023-02-28 10:01:57 +00:00
</HStack>
</VStack>
)
}
type LayoutControlsProps = {
readerSettings: ReaderSettings
}
function LayoutControls(props: LayoutControlsProps): JSX.Element {
2023-06-20 05:36:36 +00:00
const { readerSettings } = props
2023-02-28 10:01:57 +00:00
const handleMarginWidthChange = useCallback(
(value: number) => {
2023-06-20 05:36:36 +00:00
readerSettings.setMarginWidth(value)
2023-02-28 10:01:57 +00:00
},
2023-06-20 05:36:36 +00:00
[readerSettings]
2023-02-28 10:01:57 +00:00
)
return (
<>
<VStack
css={{
m: '0px',
px: '0px',
pb: '10px',
width: '100%',
height: '100%',
}}
>
<StyledText style="displaySettingsLabel" css={{ pl: '20px' }}>
Margin
</StyledText>
<HStack
distribution="between"
css={{
width: '100%',
alignItems: 'center',
alignSelf: 'center',
}}
>
<Button
style="plainIcon"
css={{ py: '0px', width: '60px' }}
onClick={() => {
const newMarginWith = Math.max(
2023-06-20 05:36:36 +00:00
readerSettings.marginWidth - 45,
2023-02-28 10:01:57 +00:00
200
)
2023-06-20 05:36:36 +00:00
readerSettings.setMarginWidth(newMarginWith)
2022-06-22 02:00:24 +00:00
}}
>
2023-02-28 10:01:57 +00:00
<ArrowsHorizontal size={24} color="#969696" />
</Button>
<TickedRangeSlider
min={200}
max={560}
step={45}
2023-06-20 05:36:36 +00:00
value={readerSettings.marginWidth}
2023-02-28 10:01:57 +00:00
onChange={handleMarginWidthChange}
2022-06-22 02:00:24 +00:00
/>
2023-02-28 10:01:57 +00:00
<Button
style="plainIcon"
css={{ py: '0px', width: '60px' }}
onClick={() => {
const newMarginWith = Math.min(
2023-06-20 05:36:36 +00:00
readerSettings.marginWidth + 45,
2023-02-28 10:01:57 +00:00
560
)
2023-06-20 05:36:36 +00:00
readerSettings.setMarginWidth(newMarginWith)
2022-06-22 02:00:24 +00:00
}}
>
2023-02-28 10:01:57 +00:00
<ArrowsInLineHorizontal size={24} color="#969696" />
</Button>
</HStack>
</VStack>
2022-06-15 15:43:10 +00:00
2023-02-28 10:01:57 +00:00
<VStack
css={{
m: '0px',
px: '0px',
pb: '20px',
width: '100%',
height: '100%',
position: 'relative',
}}
>
<StyledText style="displaySettingsLabel" css={{ pl: '20px' }}>
Line Height
</StyledText>
<HStack
distribution="between"
css={{
width: '100%',
alignItems: 'center',
alignSelf: 'center',
}}
>
<Button
style="plainIcon"
css={{ py: '0px', width: '60px' }}
onClick={() => {
const newLineHeight = Math.max(
props.readerSettings.lineHeight - 25,
100
)
props.readerSettings.setLineHeight(newLineHeight)
2022-06-22 02:00:24 +00:00
}}
>
2023-02-28 10:01:57 +00:00
<LineHeightDecreaseIcon
strokeColor={theme.colors.readerFont.toString()}
/>
</Button>
<TickedRangeSlider
min={100}
max={300}
step={25}
value={props.readerSettings.lineHeight}
onChange={(value) => {
props.readerSettings.setLineHeight(value)
}}
/>
2022-06-22 02:00:24 +00:00
<Button
style="plainIcon"
2023-02-28 10:01:57 +00:00
css={{ py: '0px', width: '60px' }}
2022-06-15 15:43:10 +00:00
onClick={() => {
2023-02-28 10:01:57 +00:00
const newLineHeight = Math.min(
props.readerSettings.lineHeight + 25,
300
)
props.readerSettings.setLineHeight(newLineHeight)
2022-06-15 15:43:10 +00:00
}}
>
2023-02-28 10:01:57 +00:00
<LineHeightIncreaseIcon
strokeColor={theme.colors.readerFont.toString()}
/>
2022-06-15 15:43:10 +00:00
</Button>
2023-02-28 10:01:57 +00:00
</HStack>
</VStack>
</>
)
}
2024-06-06 03:39:22 +00:00
export function ThemeSelector(): JSX.Element {
2024-02-16 04:25:45 +00:00
useDarkModeListener()
2024-02-14 09:12:28 +00:00
const { currentTheme, setCurrentTheme, resetSystemTheme } = useCurrentTheme()
2023-02-28 10:01:57 +00:00
return (
<VStack
css={{
2024-06-06 03:39:22 +00:00
px: '10px',
2023-02-28 10:01:57 +00:00
m: '0px',
pb: '10px',
width: '100%',
height: '100%',
}}
>
<HStack
distribution="start"
css={{
width: '100%',
}}
>
<StyledText style="displaySettingsLabel">Themes</StyledText>
<HStack
alignment="center"
distribution="center"
css={{ ml: 'auto', gap: '5px', mt: '10px', cursor: 'pointer' }}
2024-02-14 09:12:28 +00:00
onClick={() => {
console.log('clicked use system')
updateTheme(ThemeId.System)
}}
>
<Label
htmlFor="auto-checkbox"
css={{
fontFamily: '$display',
fontWeight: '500',
fontSize: '12px',
lineHeight: '20px',
color: '$thTextSubtle2',
}}
>
Auto
</Label>
2024-02-14 09:12:28 +00:00
<input
type="checkbox"
id="auto-checkbox"
checked={currentTheme == ThemeId.System}
onChange={(event) => {
if (event.target.checked) {
setCurrentTheme(ThemeId.System)
} else {
resetSystemTheme()
}
event.stopPropagation()
}}
></input>
</HStack>
</HStack>
2023-02-28 10:01:57 +00:00
<HStack
distribution="start"
css={{
gap: '16px',
pl: '5px',
}}
>
<Button
style="themeSwitch"
css={{
display: 'flex',
alignItems: 'center',
alignContent: 'center',
justifyContent: 'center',
width: '30px',
height: '30px',
background: '#F5F5F5',
borderRadius: '50%',
border: 'unset',
'&:hover': {
transform: 'scale(1.1)',
border: '2px solid #6A6968',
},
'&[data-state="selected"]': {
border: '2px solid #6A6968',
},
}}
2023-03-09 08:57:17 +00:00
data-state={currentTheme == ThemeId.Light ? 'selected' : 'unselected'}
2023-02-28 10:01:57 +00:00
onClick={() => {
2024-02-14 09:12:28 +00:00
setCurrentTheme(ThemeId.Light)
2023-02-28 10:01:57 +00:00
}}
>
{currentTheme == ThemeId.Light && (
<Check color="#6A6968" size={15} weight="bold" />
)}
2023-02-28 10:01:57 +00:00
</Button>
<Button
style="themeSwitch"
css={{
display: 'flex',
alignItems: 'center',
alignContent: 'center',
justifyContent: 'center',
width: '30px',
height: '30px',
2023-03-10 03:58:07 +00:00
background: '#2A2A2A',
2023-02-28 10:01:57 +00:00
borderRadius: '50%',
border: 'unset',
'&:hover': {
transform: 'scale(1.1)',
border: '2px solid #6A6968',
},
'&[data-state="selected"]': {
border: '2px solid #6A6968',
},
}}
data-state={currentTheme == ThemeId.Dark ? 'selected' : 'unselected'}
2023-02-28 10:01:57 +00:00
onClick={() => {
2024-02-14 09:12:28 +00:00
setCurrentTheme(ThemeId.Dark)
2023-02-28 10:01:57 +00:00
}}
>
{currentTheme == ThemeId.Dark && <Check color="#F9D354" size={20} />}
</Button>
<Button
style="themeSwitch"
css={{
display: 'flex',
alignItems: 'center',
alignContent: 'center',
justifyContent: 'center',
width: '30px',
height: '30px',
background: '#FBF0D9',
borderRadius: '50%',
border: 'unset',
'&:hover': {
transform: 'scale(1.1)',
border: '2px solid #6A6968',
},
'&[data-state="selected"]': {
border: '2px solid #6A6968',
},
}}
data-state={currentTheme == ThemeId.Sepia ? 'selected' : 'unselected'}
onClick={() => {
2024-02-14 09:12:28 +00:00
setCurrentTheme(ThemeId.Sepia)
}}
>
2023-03-09 08:57:17 +00:00
{currentTheme == ThemeId.Sepia && <Check color="#6A6968" size={20} />}
</Button>
<Button
style="themeSwitch"
css={{
display: 'flex',
alignItems: 'center',
alignContent: 'center',
justifyContent: 'center',
width: '30px',
height: '30px',
2023-03-10 03:58:07 +00:00
background: '#6A6968',
borderRadius: '50%',
border: 'unset',
'&:hover': {
transform: 'scale(1.1)',
border: '2px solid #6A6968',
},
'&[data-state="selected"]': {
border: '2px solid #6A6968',
},
}}
data-state={
currentTheme == ThemeId.Apollo ? 'selected' : 'unselected'
}
onClick={() => {
2024-02-14 09:12:28 +00:00
setCurrentTheme(ThemeId.Apollo)
}}
>
{currentTheme == ThemeId.Apollo && (
2023-03-10 03:58:07 +00:00
<Check color="#F9D354" size={20} />
)}
2023-02-28 10:01:57 +00:00
</Button>
</HStack>
</VStack>
)
}