forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useCallback} from 'react'
2import {View} from 'react-native'
3import Animated, {
4 FadeInUp,
5 FadeOutUp,
6 LayoutAnimationConfig,
7 LinearTransition,
8} from 'react-native-reanimated'
9import {msg, Trans} from '@lingui/macro'
10import {useLingui} from '@lingui/react'
11
12import {
13 type CommonNavigatorParams,
14 type NativeStackScreenProps,
15} from '#/lib/routes/types'
16import {
17 useEnableSquareAvatars,
18 useSetEnableSquareAvatars,
19} from '#/state/preferences/enable-square-avatars'
20import {
21 useEnableSquareButtons,
22 useSetEnableSquareButtons,
23} from '#/state/preferences/enable-square-buttons'
24import {useKawaiiMode, useSetKawaiiMode} from '#/state/preferences/kawaii'
25import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
26import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem'
27import {type Alf, atoms as a, native, useAlf, useTheme} from '#/alf'
28import * as SegmentedControl from '#/components/forms/SegmentedControl'
29import {Slider} from '#/components/forms/Slider'
30import * as Toggle from '#/components/forms/Toggle'
31import {Circle_And_Square_Stroke1_Corner0_Rounded_Filled as SquareIcon} from '#/components/icons/CircleAndSquare'
32import {ColorPalette_Stroke2_Corner0_Rounded as ColorPaletteIcon} from '#/components/icons/ColorPalette'
33import {type Props as SVGIconProps} from '#/components/icons/common'
34import {Moon_Stroke2_Corner0_Rounded as MoonIcon} from '#/components/icons/Moon'
35import {Phone_Stroke2_Corner0_Rounded as PhoneIcon} from '#/components/icons/Phone'
36import {Sparkle_Stroke2_Corner0_Rounded as SparkleIcon} from '#/components/icons/Sparkle'
37import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/TextSize'
38import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase'
39import * as Layout from '#/components/Layout'
40import {Text} from '#/components/Typography'
41import {IS_NATIVE} from '#/env'
42import {IS_INTERNAL} from '#/env'
43import * as SettingsList from './components/SettingsList'
44
45type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'>
46export function AppearanceSettingsScreen({}: Props) {
47 const {_} = useLingui()
48 const {fonts} = useAlf()
49 const t = useTheme()
50
51 const {colorMode, colorScheme, darkTheme, hue} = useThemePrefs()
52 const {setColorMode, setColorScheme, setDarkTheme, setHue} =
53 useSetThemePrefs()
54
55 const kawaiiMode = useKawaiiMode()
56 const setKawaiiMode = useSetKawaiiMode()
57
58 const enableSquareAvatars = useEnableSquareAvatars()
59 const setEnableSquareAvatars = useSetEnableSquareAvatars()
60
61 const enableSquareButtons = useEnableSquareButtons()
62 const setEnableSquareButtons = useSetEnableSquareButtons()
63
64 const onChangeAppearance = useCallback(
65 (value: 'light' | 'system' | 'dark') => {
66 setColorMode(value)
67 },
68 [setColorMode],
69 )
70
71 const onChangeScheme = useCallback(
72 (
73 value:
74 | 'witchsky'
75 | 'bluesky'
76 | 'blacksky'
77 | 'deer'
78 | 'zeppelin'
79 | 'kitty'
80 | 'reddwarf',
81 ) => {
82 setColorScheme(value)
83 },
84 [setColorScheme],
85 )
86
87 const onChangeDarkTheme = useCallback(
88 (value: 'dim' | 'dark') => {
89 setDarkTheme(value)
90 },
91 [setDarkTheme],
92 )
93
94 const onChangeFontFamily = useCallback(
95 (value: 'system' | 'theme') => {
96 fonts.setFontFamily(value)
97 },
98 [fonts],
99 )
100
101 const onChangeFontScale = useCallback(
102 (value: Alf['fonts']['scale']) => {
103 fonts.setFontScale(value)
104 },
105 [fonts],
106 )
107
108 const colorSchemes = [
109 {name: 'witchsky', label: _(msg`Witchsky`)},
110 {name: 'bluesky', label: _(msg`Bluesky`)},
111 {name: 'blacksky', label: _(msg`Blacksky`)},
112 {name: 'deer', label: _(msg`Deer`)},
113 {name: 'zeppelin', label: _(msg`Zeppelin`)},
114 {name: 'kitty', label: _(msg`Kitty`)},
115 {name: 'reddwarf', label: _(msg`Red Dwarf`)},
116 ]
117
118 return (
119 <LayoutAnimationConfig skipExiting skipEntering>
120 <Layout.Screen testID="preferencesThreadsScreen">
121 <Layout.Header.Outer>
122 <Layout.Header.BackButton />
123 <Layout.Header.Content>
124 <Layout.Header.TitleText>
125 <Trans>Appearance</Trans>
126 </Layout.Header.TitleText>
127 </Layout.Header.Content>
128 <Layout.Header.Slot />
129 </Layout.Header.Outer>
130 <Layout.Content>
131 <SettingsList.Container>
132 <AppearanceToggleButtonGroup
133 title={_(msg`Color mode`)}
134 icon={PhoneIcon}
135 items={[
136 {
137 label: _(msg`System`),
138 name: 'system',
139 },
140 {
141 label: _(msg`Light`),
142 name: 'light',
143 },
144 {
145 label: _(msg`Dark`),
146 name: 'dark',
147 },
148 ]}
149 value={colorMode}
150 onChange={onChangeAppearance}
151 />
152
153 {colorMode !== 'light' && (
154 <Animated.View
155 entering={native(FadeInUp)}
156 exiting={native(FadeOutUp)}>
157 <AppearanceToggleButtonGroup
158 title={_(msg`Dark theme`)}
159 icon={MoonIcon}
160 items={[
161 {
162 label: _(msg`Dim`),
163 name: 'dim',
164 },
165 {
166 label: _(msg`Dark`),
167 name: 'dark',
168 },
169 ]}
170 value={darkTheme ?? 'dim'}
171 onChange={onChangeDarkTheme}
172 />
173 </Animated.View>
174 )}
175
176 <SettingsList.Group>
177 <SettingsList.ItemIcon icon={ColorPaletteIcon} />
178 <SettingsList.ItemText>
179 <Trans>Color Theme</Trans>
180 </SettingsList.ItemText>
181 <View style={[a.w_full, a.gap_md]}>
182 <Text style={[a.flex_1, t.atoms.text_contrast_medium]}>
183 <Trans>Choose which color scheme to use:</Trans>
184 </Text>
185 <Toggle.Group
186 label={_(msg`Color Theme`)}
187 type="radio"
188 values={[colorScheme]}
189 onChange={([value]) =>
190 onChangeScheme(value as typeof colorScheme)
191 }>
192 <View style={[a.gap_sm, a.flex_1]}>
193 {colorSchemes.map(({name, label}) => (
194 <Toggle.Item key={name} name={name} label={label}>
195 <Toggle.Radio />
196 <Toggle.LabelText>
197 <Trans>{label}</Trans>
198 </Toggle.LabelText>
199 </Toggle.Item>
200 ))}
201 </View>
202 </Toggle.Group>
203 <Text style={[a.flex_1, t.atoms.text_contrast_medium]}>
204 <Trans>Hue shift the colors:</Trans>
205 </Text>
206 <Slider
207 value={hue}
208 onValueChange={setHue}
209 minimumValue={0}
210 maximumValue={360}
211 step={1}
212 debounceFull={true}
213 />
214 </View>
215 </SettingsList.Group>
216
217 <Animated.View layout={native(LinearTransition)}>
218 <SettingsList.Divider />
219
220 <AppearanceToggleButtonGroup
221 title={_(msg`Font`)}
222 description={_(
223 msg`For the best experience, we recommend using the theme font.`,
224 )}
225 icon={Aa}
226 items={[
227 {
228 label: _(msg`System`),
229 name: 'system',
230 },
231 {
232 label: _(msg`Theme`),
233 name: 'theme',
234 },
235 ]}
236 value={fonts.family}
237 onChange={onChangeFontFamily}
238 />
239
240 <AppearanceToggleButtonGroup
241 title={_(msg`Font size`)}
242 icon={TextSize}
243 items={[
244 {
245 label: _(msg`Smaller`),
246 name: '-1',
247 },
248 {
249 label: _(msg`Default`),
250 name: '0',
251 },
252 {
253 label: _(msg`Larger`),
254 name: '1',
255 },
256 ]}
257 value={fonts.scale}
258 onChange={onChangeFontScale}
259 />
260
261 <SettingsList.Divider />
262
263 <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
264 <SettingsList.ItemIcon icon={SparkleIcon} />
265 <SettingsList.ItemText>
266 <Trans>Logo</Trans>
267 </SettingsList.ItemText>
268 <Toggle.Item
269 name="kawaii_mode"
270 label={_(msg`Enable kawaii logo`)}
271 value={kawaiiMode}
272 onChange={value => setKawaiiMode(value)}
273 style={[a.w_full]}>
274 <Toggle.LabelText style={[a.flex_1]}>
275 <Trans>Enable kawaii logo</Trans>
276 </Toggle.LabelText>
277 <Toggle.Platform />
278 </Toggle.Item>
279 </SettingsList.Group>
280
281 <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
282 <SettingsList.ItemIcon icon={SquareIcon} />
283 <SettingsList.ItemText>
284 <Trans>Shapes</Trans>
285 </SettingsList.ItemText>
286 <Toggle.Item
287 name="enable_square_avatars"
288 label={_(msg`Enable square avatars`)}
289 value={enableSquareAvatars}
290 onChange={value => setEnableSquareAvatars(value)}
291 style={[a.w_full]}>
292 <Toggle.LabelText style={[a.flex_1]}>
293 <Trans>Enable square avatars</Trans>
294 </Toggle.LabelText>
295 <Toggle.Platform />
296 </Toggle.Item>
297
298 <Toggle.Item
299 name="enable_square_buttons"
300 label={_(msg`Enable square buttons`)}
301 value={enableSquareButtons}
302 onChange={value => setEnableSquareButtons(value)}
303 style={[a.w_full]}>
304 <Toggle.LabelText style={[a.flex_1]}>
305 <Trans>Enable square buttons</Trans>
306 </Toggle.LabelText>
307 <Toggle.Platform />
308 </Toggle.Item>
309 </SettingsList.Group>
310 {IS_NATIVE && IS_INTERNAL && (
311 <>
312 <SettingsList.Divider />
313 <AppIconSettingsListItem />
314 </>
315 )}
316 </Animated.View>
317 </SettingsList.Container>
318 </Layout.Content>
319 </Layout.Screen>
320 </LayoutAnimationConfig>
321 )
322}
323
324export function AppearanceToggleButtonGroup<T extends string>({
325 title,
326 description,
327 icon: Icon,
328 items,
329 value,
330 onChange,
331}: {
332 title: string
333 description?: string
334 icon: React.ComponentType<SVGIconProps>
335 items: {
336 label: string
337 name: T
338 }[]
339 value: T
340 onChange: (value: T) => void
341}) {
342 const t = useTheme()
343 return (
344 <>
345 <SettingsList.Group contentContainerStyle={[a.gap_sm]} iconInset={false}>
346 <SettingsList.ItemIcon icon={Icon} />
347 <SettingsList.ItemText>{title}</SettingsList.ItemText>
348 {description && (
349 <Text
350 style={[
351 a.text_sm,
352 a.leading_snug,
353 t.atoms.text_contrast_medium,
354 a.w_full,
355 ]}>
356 {description}
357 </Text>
358 )}
359 <SegmentedControl.Root
360 type="radio"
361 label={title}
362 value={value}
363 onChange={onChange}>
364 {items.map(item => (
365 <SegmentedControl.Item
366 key={item.name}
367 label={item.label}
368 value={item.name}>
369 <SegmentedControl.ItemText>
370 {item.label}
371 </SegmentedControl.ItemText>
372 </SegmentedControl.Item>
373 ))}
374 </SegmentedControl.Root>
375 </SettingsList.Group>
376 </>
377 )
378}