forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useCallback} from 'react'
2import {Pressable, View} from 'react-native'
3import Animated, {
4 FadeInUp,
5 FadeOutUp,
6 LayoutAnimationConfig,
7 LinearTransition,
8} from 'react-native-reanimated'
9import {msg} from '@lingui/core/macro'
10import {useLingui} from '@lingui/react'
11import {Trans} from '@lingui/react/macro'
12
13import {
14 type CommonNavigatorParams,
15 type NativeStackScreenProps,
16} from '#/lib/routes/types'
17import {
18 useEnableSquareAvatars,
19 useSetEnableSquareAvatars,
20} from '#/state/preferences/enable-square-avatars'
21import {
22 useEnableSquareButtons,
23 useSetEnableSquareButtons,
24} from '#/state/preferences/enable-square-buttons'
25import {useKawaiiMode, useSetKawaiiMode} from '#/state/preferences/kawaii'
26import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
27import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem'
28import {type Alf, atoms as a, native, useAlf, useTheme} from '#/alf'
29import {
30 BLACKSKY_PALETTE,
31 BLUESKY_PALETTE,
32 CATPPUCIN_PALETTE,
33 DEER_PALETTE,
34 DEFAULT_PALETTE,
35 EVERGARDEN_PALETTE,
36 KITTY_PALETTE,
37 REDDWARF_PALETTE,
38 ZEPPELIN_PALETTE,
39} from '#/alf/themes'
40import * as SegmentedControl from '#/components/forms/SegmentedControl'
41import {Slider} from '#/components/forms/Slider'
42import * as Toggle from '#/components/forms/Toggle'
43import {Circle_And_Square_Stroke1_Corner0_Rounded_Filled as SquareIcon} from '#/components/icons/CircleAndSquare'
44import {ColorPalette_Stroke2_Corner0_Rounded as ColorPaletteIcon} from '#/components/icons/ColorPalette'
45import {type Props as SVGIconProps} from '#/components/icons/common'
46import {
47 Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled,
48 Heart2_Stroke2_Corner0_Rounded as HeartIconOutline,
49} from '#/components/icons/Heart2'
50import {Moon_Stroke2_Corner0_Rounded as MoonIcon} from '#/components/icons/Moon'
51import {Phone_Stroke2_Corner0_Rounded as PhoneIcon} from '#/components/icons/Phone'
52import {Sparkle_Stroke2_Corner0_Rounded as SparkleIcon} from '#/components/icons/Sparkle'
53import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/TextSize'
54import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase'
55import * as Layout from '#/components/Layout'
56import {Text} from '#/components/Typography'
57import {IS_INTERNAL, IS_NATIVE} from '#/env'
58import * as SettingsList from './components/SettingsList'
59
60type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'>
61
62type ColorSchemeName =
63 | 'witchsky'
64 | 'bluesky'
65 | 'blacksky'
66 | 'deer'
67 | 'zeppelin'
68 | 'kitty'
69 | 'reddwarf'
70 | 'catppuccin'
71 | 'evergarden'
72
73type ColorSchemeOption = {
74 name: ColorSchemeName
75 label: string
76 primary: string
77}
78
79export function AppearanceSettingsScreen({}: Props) {
80 const {_} = useLingui()
81 const {fonts} = useAlf()
82 const t = useTheme()
83
84 const {colorMode, colorScheme, darkTheme, hue} = useThemePrefs()
85 const {setColorMode, setColorScheme, setDarkTheme, setHue} =
86 useSetThemePrefs()
87
88 const kawaiiMode = useKawaiiMode()
89 const setKawaiiMode = useSetKawaiiMode()
90
91 const enableSquareAvatars = useEnableSquareAvatars()
92 const setEnableSquareAvatars = useSetEnableSquareAvatars()
93
94 const enableSquareButtons = useEnableSquareButtons()
95 const setEnableSquareButtons = useSetEnableSquareButtons()
96
97 const onChangeAppearance = useCallback(
98 (value: 'light' | 'system' | 'dark') => {
99 setColorMode(value)
100 },
101 [setColorMode],
102 )
103
104 const onChangeScheme = useCallback(
105 (value: ColorSchemeName) => {
106 setColorScheme(value)
107 },
108 [setColorScheme],
109 )
110
111 const onChangeDarkTheme = useCallback(
112 (value: 'dim' | 'dark') => {
113 setDarkTheme(value)
114 },
115 [setDarkTheme],
116 )
117
118 const onChangeFontFamily = useCallback(
119 (value: 'system' | 'theme') => {
120 fonts.setFontFamily(value)
121 },
122 [fonts],
123 )
124
125 const onChangeFontScale = useCallback(
126 (value: Alf['fonts']['scale']) => {
127 fonts.setFontScale(value)
128 },
129 [fonts],
130 )
131
132 const colorSchemes: ColorSchemeOption[] = [
133 {
134 name: 'witchsky',
135 label: _(msg`Witchsky`),
136 primary: DEFAULT_PALETTE.primary_500,
137 },
138 {
139 name: 'bluesky',
140 label: _(msg`Bluesky`),
141 primary: BLUESKY_PALETTE.primary_500,
142 },
143 {
144 name: 'blacksky',
145 label: _(msg`Blacksky`),
146 primary: BLACKSKY_PALETTE.primary_500,
147 },
148 {
149 name: 'deer',
150 label: _(msg`Deer`),
151 primary: DEER_PALETTE.primary_500,
152 },
153 {
154 name: 'zeppelin',
155 label: _(msg`Zeppelin`),
156 primary: ZEPPELIN_PALETTE.primary_500,
157 },
158 {
159 name: 'kitty',
160 label: _(msg`Kitty`),
161 primary: KITTY_PALETTE.primary_500,
162 },
163 {
164 name: 'reddwarf',
165 label: _(msg`Red Dwarf`),
166 primary: REDDWARF_PALETTE.primary_500,
167 },
168 {
169 name: 'catppuccin',
170 label: _(msg`Catppuccin`),
171 primary: CATPPUCIN_PALETTE.primary_500,
172 },
173 {
174 name: 'evergarden',
175 label: _(msg`Evergarden`),
176 primary: EVERGARDEN_PALETTE.primary_500,
177 },
178 ]
179
180 return (
181 <LayoutAnimationConfig skipExiting skipEntering>
182 <Layout.Screen testID="preferencesThreadsScreen">
183 <Layout.Header.Outer>
184 <Layout.Header.BackButton />
185 <Layout.Header.Content>
186 <Layout.Header.TitleText>
187 <Trans>Appearance</Trans>
188 </Layout.Header.TitleText>
189 </Layout.Header.Content>
190 <Layout.Header.Slot />
191 </Layout.Header.Outer>
192 <Layout.Content>
193 <SettingsList.Container>
194 <AppearanceToggleButtonGroup
195 title={_(msg`Color mode`)}
196 icon={PhoneIcon}
197 items={[
198 {
199 label: _(msg`System`),
200 name: 'system',
201 },
202 {
203 label: _(msg`Light`),
204 name: 'light',
205 },
206 {
207 label: _(msg`Dark`),
208 name: 'dark',
209 },
210 ]}
211 value={colorMode}
212 onChange={onChangeAppearance}
213 />
214
215 {colorMode !== 'light' && (
216 <Animated.View
217 entering={native(FadeInUp)}
218 exiting={native(FadeOutUp)}>
219 <AppearanceToggleButtonGroup
220 title={_(msg`Dark theme`)}
221 icon={MoonIcon}
222 items={[
223 {
224 label: _(msg`Dim`),
225 name: 'dim',
226 },
227 {
228 label: _(msg`Dark`),
229 name: 'dark',
230 },
231 ]}
232 value={darkTheme ?? 'dim'}
233 onChange={onChangeDarkTheme}
234 />
235 </Animated.View>
236 )}
237
238 <SettingsList.Group>
239 <SettingsList.ItemIcon icon={ColorPaletteIcon} />
240 <SettingsList.ItemText>
241 <Trans>Color Theme</Trans>
242 </SettingsList.ItemText>
243 <View style={[a.w_full, a.gap_md]}>
244 <Text style={[a.flex_1, t.atoms.text_contrast_medium]}>
245 <Trans>Choose which color scheme to use:</Trans>
246 </Text>
247 <ColorSchemeGrid
248 schemes={colorSchemes}
249 selectedScheme={colorScheme}
250 onSchemeChange={onChangeScheme}
251 />
252 <Text style={[a.flex_1, t.atoms.text_contrast_medium]}>
253 <Trans>Hue shift the colors:</Trans>
254 </Text>
255 <Slider
256 value={hue}
257 onValueChange={setHue}
258 minimumValue={0}
259 maximumValue={360}
260 step={1}
261 debounceFull={true}
262 />
263 </View>
264 </SettingsList.Group>
265
266 <Animated.View layout={native(LinearTransition)}>
267 <SettingsList.Divider />
268
269 <AppearanceToggleButtonGroup
270 title={_(msg`Font`)}
271 description={_(
272 msg`For the best experience, we recommend using the theme font.`,
273 )}
274 icon={Aa}
275 items={[
276 {
277 label: _(msg`System`),
278 name: 'system',
279 },
280 {
281 label: _(msg`Theme`),
282 name: 'theme',
283 },
284 ]}
285 value={fonts.family}
286 onChange={onChangeFontFamily}
287 />
288
289 <AppearanceToggleButtonGroup
290 title={_(msg`Font size`)}
291 icon={TextSize}
292 items={[
293 {
294 label: _(msg`Smaller`),
295 name: '-1',
296 },
297 {
298 label: _(msg`Default`),
299 name: '0',
300 },
301 {
302 label: _(msg`Larger`),
303 name: '1',
304 },
305 ]}
306 value={fonts.scale}
307 onChange={onChangeFontScale}
308 />
309
310 <SettingsList.Divider />
311
312 <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
313 <SettingsList.ItemIcon icon={SparkleIcon} />
314 <SettingsList.ItemText>
315 <Trans>Logo</Trans>
316 </SettingsList.ItemText>
317 <Toggle.Item
318 name="kawaii_mode"
319 label={_(msg`Enable kawaii logo`)}
320 value={kawaiiMode}
321 onChange={value => setKawaiiMode(value)}
322 style={[a.w_full]}>
323 <Toggle.LabelText style={[a.flex_1]}>
324 <Trans>Enable kawaii logo</Trans>
325 </Toggle.LabelText>
326 <Toggle.Platform />
327 </Toggle.Item>
328 </SettingsList.Group>
329
330 <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
331 <SettingsList.ItemIcon icon={SquareIcon} />
332 <SettingsList.ItemText>
333 <Trans>Shapes</Trans>
334 </SettingsList.ItemText>
335 <Toggle.Item
336 name="enable_square_avatars"
337 label={_(msg`Enable square avatars`)}
338 value={enableSquareAvatars}
339 onChange={value => setEnableSquareAvatars(value)}
340 style={[a.w_full]}>
341 <Toggle.LabelText style={[a.flex_1]}>
342 <Trans>Enable square avatars</Trans>
343 </Toggle.LabelText>
344 <Toggle.Platform />
345 </Toggle.Item>
346
347 <Toggle.Item
348 name="enable_square_buttons"
349 label={_(msg`Enable square buttons`)}
350 value={enableSquareButtons}
351 onChange={value => setEnableSquareButtons(value)}
352 style={[a.w_full]}>
353 <Toggle.LabelText style={[a.flex_1]}>
354 <Trans>Enable square buttons</Trans>
355 </Toggle.LabelText>
356 <Toggle.Platform />
357 </Toggle.Item>
358 </SettingsList.Group>
359 {IS_NATIVE && IS_INTERNAL && (
360 <>
361 <SettingsList.Divider />
362 <AppIconSettingsListItem />
363 </>
364 )}
365 </Animated.View>
366 </SettingsList.Container>
367 </Layout.Content>
368 </Layout.Screen>
369 </LayoutAnimationConfig>
370 )
371}
372
373function ColorSchemeGrid({
374 schemes,
375 selectedScheme,
376 onSchemeChange,
377}: {
378 schemes: ColorSchemeOption[]
379 selectedScheme: ColorSchemeName
380 onSchemeChange: (scheme: ColorSchemeName) => void
381}) {
382 const t = useTheme()
383 return (
384 <View style={[a.flex_row, a.flex_wrap, a.gap_sm]}>
385 {schemes.map(({name, label, primary}) => {
386 const isSelected = selectedScheme === name
387 const HeartIcon = isSelected ? HeartIconFilled : HeartIconOutline
388 return (
389 <Pressable
390 accessibilityRole="button"
391 key={name}
392 onPress={() => onSchemeChange(name)}
393 style={[
394 a.flex_1,
395 a.rounded_md,
396 a.overflow_hidden,
397 {minWidth: '30%'},
398 a.border,
399 {
400 borderColor: isSelected
401 ? primary
402 : t.atoms.border_contrast_low.borderColor,
403 borderWidth: isSelected ? 2 : 1,
404 },
405 ]}>
406 <View
407 style={[
408 a.p_sm,
409 a.gap_xs,
410 {backgroundColor: t.atoms.bg.backgroundColor},
411 ]}>
412 <View
413 style={[
414 a.w_full,
415 a.rounded_xs,
416 {backgroundColor: primary, height: 24},
417 ]}
418 />
419 <View
420 style={[
421 a.flex_row,
422 a.align_center,
423 a.justify_center,
424 a.gap_xs,
425 ]}>
426 <Text style={[a.text_sm, a.font_bold, t.atoms.text]}>
427 {label}
428 </Text>
429 <HeartIcon size="xs" style={[{color: primary}]} />
430 </View>
431 </View>
432 </Pressable>
433 )
434 })}
435 </View>
436 )
437}
438
439export function AppearanceToggleButtonGroup<T extends string>({
440 title,
441 description,
442 icon: Icon,
443 items,
444 value,
445 onChange,
446}: {
447 title: string
448 description?: string
449 icon: React.ComponentType<SVGIconProps>
450 items: {
451 label: string
452 name: T
453 }[]
454 value: T
455 onChange: (value: T) => void
456}) {
457 const t = useTheme()
458 return (
459 <>
460 <SettingsList.Group contentContainerStyle={[a.gap_sm]} iconInset={false}>
461 <SettingsList.ItemIcon icon={Icon} />
462 <SettingsList.ItemText>{title}</SettingsList.ItemText>
463 {description && (
464 <Text
465 style={[
466 a.text_sm,
467 a.leading_snug,
468 t.atoms.text_contrast_medium,
469 a.w_full,
470 ]}>
471 {description}
472 </Text>
473 )}
474 <SegmentedControl.Root
475 type="radio"
476 label={title}
477 value={value}
478 onChange={onChange}>
479 {items.map(item => (
480 <SegmentedControl.Item
481 key={item.name}
482 label={item.label}
483 value={item.name}>
484 <SegmentedControl.ItemText>
485 {item.label}
486 </SegmentedControl.ItemText>
487 </SegmentedControl.Item>
488 ))}
489 </SegmentedControl.Root>
490 </SettingsList.Group>
491 </>
492 )
493}