forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useCallback} from 'react'
2import Animated, {
3 FadeInUp,
4 FadeOutUp,
5 LayoutAnimationConfig,
6 LinearTransition,
7} from 'react-native-reanimated'
8import {msg, Trans} from '@lingui/macro'
9import {useLingui} from '@lingui/react'
10
11import {
12 type CommonNavigatorParams,
13 type NativeStackScreenProps,
14} from '#/lib/routes/types'
15import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
16import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem'
17import {type Alf, atoms as a, native, useAlf, useTheme} from '#/alf'
18import * as SegmentedControl from '#/components/forms/SegmentedControl'
19import {type Props as SVGIconProps} from '#/components/icons/common'
20import {Moon_Stroke2_Corner0_Rounded as MoonIcon} from '#/components/icons/Moon'
21import {Phone_Stroke2_Corner0_Rounded as PhoneIcon} from '#/components/icons/Phone'
22import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/TextSize'
23import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase'
24import * as Layout from '#/components/Layout'
25import {Text} from '#/components/Typography'
26import {IS_NATIVE} from '#/env'
27import {IS_INTERNAL} from '#/env'
28import * as SettingsList from './components/SettingsList'
29
30type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'>
31export function AppearanceSettingsScreen({}: Props) {
32 const {_} = useLingui()
33 const {fonts} = useAlf()
34
35 const {colorMode, darkTheme} = useThemePrefs()
36 const {setColorMode, setDarkTheme} = useSetThemePrefs()
37
38 const onChangeAppearance = useCallback(
39 (value: 'light' | 'system' | 'dark') => {
40 setColorMode(value)
41 },
42 [setColorMode],
43 )
44
45 const onChangeDarkTheme = useCallback(
46 (value: 'dim' | 'dark') => {
47 setDarkTheme(value)
48 },
49 [setDarkTheme],
50 )
51
52 const onChangeFontFamily = useCallback(
53 (value: 'system' | 'theme') => {
54 fonts.setFontFamily(value)
55 },
56 [fonts],
57 )
58
59 const onChangeFontScale = useCallback(
60 (value: Alf['fonts']['scale']) => {
61 fonts.setFontScale(value)
62 },
63 [fonts],
64 )
65
66 return (
67 <LayoutAnimationConfig skipExiting skipEntering>
68 <Layout.Screen testID="preferencesThreadsScreen">
69 <Layout.Header.Outer>
70 <Layout.Header.BackButton />
71 <Layout.Header.Content>
72 <Layout.Header.TitleText>
73 <Trans>Appearance</Trans>
74 </Layout.Header.TitleText>
75 </Layout.Header.Content>
76 <Layout.Header.Slot />
77 </Layout.Header.Outer>
78 <Layout.Content>
79 <SettingsList.Container>
80 <AppearanceToggleButtonGroup
81 title={_(msg`Color mode`)}
82 icon={PhoneIcon}
83 items={[
84 {
85 label: _(msg`System`),
86 name: 'system',
87 },
88 {
89 label: _(msg`Light`),
90 name: 'light',
91 },
92 {
93 label: _(msg`Dark`),
94 name: 'dark',
95 },
96 ]}
97 value={colorMode}
98 onChange={onChangeAppearance}
99 />
100
101 {colorMode !== 'light' && (
102 <Animated.View
103 entering={native(FadeInUp)}
104 exiting={native(FadeOutUp)}>
105 <AppearanceToggleButtonGroup
106 title={_(msg`Dark theme`)}
107 icon={MoonIcon}
108 items={[
109 {
110 label: _(msg`Dim`),
111 name: 'dim',
112 },
113 {
114 label: _(msg`Dark`),
115 name: 'dark',
116 },
117 ]}
118 value={darkTheme ?? 'dim'}
119 onChange={onChangeDarkTheme}
120 />
121 </Animated.View>
122 )}
123
124 <Animated.View layout={native(LinearTransition)}>
125 <SettingsList.Divider />
126
127 <AppearanceToggleButtonGroup
128 title={_(msg`Font`)}
129 description={_(
130 msg`For the best experience, we recommend using the theme font.`,
131 )}
132 icon={Aa}
133 items={[
134 {
135 label: _(msg`System`),
136 name: 'system',
137 },
138 {
139 label: _(msg`Theme`),
140 name: 'theme',
141 },
142 ]}
143 value={fonts.family}
144 onChange={onChangeFontFamily}
145 />
146
147 <AppearanceToggleButtonGroup
148 title={_(msg`Font size`)}
149 icon={TextSize}
150 items={[
151 {
152 label: _(msg`Smaller`),
153 name: '-1',
154 },
155 {
156 label: _(msg`Default`),
157 name: '0',
158 },
159 {
160 label: _(msg`Larger`),
161 name: '1',
162 },
163 ]}
164 value={fonts.scale}
165 onChange={onChangeFontScale}
166 />
167
168 {IS_NATIVE && IS_INTERNAL && (
169 <>
170 <SettingsList.Divider />
171 <AppIconSettingsListItem />
172 </>
173 )}
174 </Animated.View>
175 </SettingsList.Container>
176 </Layout.Content>
177 </Layout.Screen>
178 </LayoutAnimationConfig>
179 )
180}
181
182export function AppearanceToggleButtonGroup<T extends string>({
183 title,
184 description,
185 icon: Icon,
186 items,
187 value,
188 onChange,
189}: {
190 title: string
191 description?: string
192 icon: React.ComponentType<SVGIconProps>
193 items: {
194 label: string
195 name: T
196 }[]
197 value: T
198 onChange: (value: T) => void
199}) {
200 const t = useTheme()
201 return (
202 <>
203 <SettingsList.Group contentContainerStyle={[a.gap_sm]} iconInset={false}>
204 <SettingsList.ItemIcon icon={Icon} />
205 <SettingsList.ItemText>{title}</SettingsList.ItemText>
206 {description && (
207 <Text
208 style={[
209 a.text_sm,
210 a.leading_snug,
211 t.atoms.text_contrast_medium,
212 a.w_full,
213 ]}>
214 {description}
215 </Text>
216 )}
217 <SegmentedControl.Root
218 type="radio"
219 label={title}
220 value={value}
221 onChange={onChange}>
222 {items.map(item => (
223 <SegmentedControl.Item
224 key={item.name}
225 label={item.label}
226 value={item.name}>
227 <SegmentedControl.ItemText>
228 {item.label}
229 </SegmentedControl.ItemText>
230 </SegmentedControl.Item>
231 ))}
232 </SegmentedControl.Root>
233 </SettingsList.Group>
234 </>
235 )
236}