Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at a876aae44ea07494ebea9727350aa060b81f317b 657 lines 20 kB view raw
1import {useCallback, useMemo} 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 {type Schema} from '#/state/persisted' 18import { 19 useEnableSquareAvatars, 20 useSetEnableSquareAvatars, 21} from '#/state/preferences/enable-square-avatars' 22import { 23 useEnableSquareButtons, 24 useSetEnableSquareButtons, 25} from '#/state/preferences/enable-square-buttons' 26import {useKawaiiMode, useSetKawaiiMode} from '#/state/preferences/kawaii' 27import {useSetThemePrefs, useThemePrefs} from '#/state/shell' 28import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem' 29import {type Alf, atoms as a, native, useAlf, useTheme} from '#/alf' 30import { 31 BLACKSKY_PALETTE, 32 BLUESKY_PALETTE, 33 CATPPUCIN_PALETTE, 34 DEER_PALETTE, 35 DEFAULT_PALETTE, 36 EVERGARDEN_PALETTE, 37 KITTY_PALETTE, 38 REDDWARF_PALETTE, 39 ZEPPELIN_PALETTE, 40} from '#/alf/themes' 41import {getMaterial3Colors} from '#/alf/util/material3Theme' 42import {useMaterialYouPalette} from '#/alf/util/materialYou' 43import * as SegmentedControl from '#/components/forms/SegmentedControl' 44import {Slider} from '#/components/forms/Slider' 45import * as Toggle from '#/components/forms/Toggle' 46import {Circle_And_Square_Stroke1_Corner0_Rounded_Filled as SquareIcon} from '#/components/icons/CircleAndSquare' 47import {ColorPalette_Stroke2_Corner0_Rounded as ColorPaletteIcon} from '#/components/icons/ColorPalette' 48import {type Props as SVGIconProps} from '#/components/icons/common' 49import { 50 Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled, 51 Heart2_Stroke2_Corner0_Rounded as HeartIconOutline, 52} from '#/components/icons/Heart2' 53import {Moon_Stroke2_Corner0_Rounded as MoonIcon} from '#/components/icons/Moon' 54import {Phone_Stroke2_Corner0_Rounded as PhoneIcon} from '#/components/icons/Phone' 55import {Sparkle_Stroke2_Corner0_Rounded as SparkleIcon} from '#/components/icons/Sparkle' 56import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/TextSize' 57import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase' 58import * as Layout from '#/components/Layout' 59import {Text} from '#/components/Typography' 60import {IS_ANDROID, IS_INTERNAL, IS_NATIVE} from '#/env' 61import * as SettingsList from './components/SettingsList' 62 63type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'> 64 65type ColorSchemeName = 66 | 'witchsky' 67 | 'bluesky' 68 | 'blacksky' 69 | 'deer' 70 | 'zeppelin' 71 | 'kitty' 72 | 'reddwarf' 73 | 'catppuccin' 74 | 'evergarden' 75 | 'material3' 76 77type ColorSchemeOption = { 78 name: ColorSchemeName 79 label: string 80 primary: string 81} 82 83export function AppearanceSettingsScreen({}: Props) { 84 const {_} = useLingui() 85 const {fonts} = useAlf() 86 const t = useTheme() 87 88 const { 89 colorMode, 90 colorScheme, 91 darkTheme, 92 hue, 93 material3Accent, 94 material3Style, 95 } = useThemePrefs() 96 const { 97 setColorMode, 98 setColorScheme, 99 setDarkTheme, 100 setHue, 101 setMaterial3Accent, 102 setMaterial3Style, 103 } = useSetThemePrefs() 104 105 const kawaiiMode = useKawaiiMode() 106 const setKawaiiMode = useSetKawaiiMode() 107 108 const enableSquareAvatars = useEnableSquareAvatars() 109 const setEnableSquareAvatars = useSetEnableSquareAvatars() 110 111 const enableSquareButtons = useEnableSquareButtons() 112 const setEnableSquareButtons = useSetEnableSquareButtons() 113 114 const material3Palette = useMaterialYouPalette() 115 const cachedScheme = useMemo( 116 () => getMaterial3Colors(material3Palette), 117 [material3Palette], 118 ) 119 120 const onChangeAppearance = useCallback( 121 (value: 'light' | 'system' | 'dark') => { 122 setColorMode(value) 123 }, 124 [setColorMode], 125 ) 126 127 const onChangeScheme = useCallback( 128 (value: ColorSchemeName) => { 129 setColorScheme(value) 130 }, 131 [setColorScheme], 132 ) 133 134 const onChangeDarkTheme = useCallback( 135 (value: 'dim' | 'dark') => { 136 setDarkTheme(value) 137 }, 138 [setDarkTheme], 139 ) 140 141 const onChangeFontFamily = useCallback( 142 (value: 'system' | 'theme' | 'material') => { 143 fonts.setFontFamily(value) 144 }, 145 [fonts], 146 ) 147 148 const onChangeFontScale = useCallback( 149 (value: Alf['fonts']['scale']) => { 150 fonts.setFontScale(value) 151 }, 152 [fonts], 153 ) 154 155 const colorSchemes: ColorSchemeOption[] = [ 156 { 157 name: 'witchsky', 158 label: _(msg`Witchsky`), 159 primary: DEFAULT_PALETTE.primary_500, 160 }, 161 { 162 name: 'bluesky', 163 label: _(msg`Bluesky`), 164 primary: BLUESKY_PALETTE.primary_500, 165 }, 166 { 167 name: 'blacksky', 168 label: _(msg`Blacksky`), 169 primary: BLACKSKY_PALETTE.primary_500, 170 }, 171 { 172 name: 'deer', 173 label: _(msg`Deer`), 174 primary: DEER_PALETTE.primary_500, 175 }, 176 { 177 name: 'zeppelin', 178 label: _(msg`Zeppelin`), 179 primary: ZEPPELIN_PALETTE.primary_500, 180 }, 181 { 182 name: 'kitty', 183 label: _(msg`Kitty`), 184 primary: KITTY_PALETTE.primary_500, 185 }, 186 { 187 name: 'reddwarf', 188 label: _(msg`Red Dwarf`), 189 primary: REDDWARF_PALETTE.primary_500, 190 }, 191 { 192 name: 'catppuccin', 193 label: _(msg`Catppuccin`), 194 primary: CATPPUCIN_PALETTE.primary_500, 195 }, 196 { 197 name: 'evergarden', 198 label: _(msg`Evergarden`), 199 primary: EVERGARDEN_PALETTE.primary_500, 200 }, 201 { 202 name: 'material3', 203 label: _(msg`Material You`), 204 primary: cachedScheme.regular.primary_500, 205 }, 206 ] 207 208 return ( 209 <LayoutAnimationConfig skipExiting skipEntering> 210 <Layout.Screen testID="preferencesThreadsScreen"> 211 <Layout.Header.Outer> 212 <Layout.Header.BackButton /> 213 <Layout.Header.Content> 214 <Layout.Header.TitleText> 215 <Trans>Appearance</Trans> 216 </Layout.Header.TitleText> 217 </Layout.Header.Content> 218 <Layout.Header.Slot /> 219 </Layout.Header.Outer> 220 <Layout.Content> 221 <SettingsList.Container> 222 <AppearanceToggleButtonGroup 223 title={_(msg`Color mode`)} 224 icon={PhoneIcon} 225 items={[ 226 { 227 label: _(msg`System`), 228 name: 'system', 229 }, 230 { 231 label: _(msg`Light`), 232 name: 'light', 233 }, 234 { 235 label: _(msg`Dark`), 236 name: 'dark', 237 }, 238 ]} 239 value={colorMode} 240 onChange={onChangeAppearance} 241 /> 242 243 {colorMode !== 'light' && ( 244 <Animated.View 245 entering={native(FadeInUp)} 246 exiting={native(FadeOutUp)}> 247 <AppearanceToggleButtonGroup 248 title={_(msg`Dark theme`)} 249 icon={MoonIcon} 250 items={[ 251 { 252 label: _(msg`Dim`), 253 name: 'dim', 254 }, 255 { 256 label: _(msg`Dark`), 257 name: 'dark', 258 }, 259 ]} 260 value={darkTheme ?? 'dim'} 261 onChange={onChangeDarkTheme} 262 /> 263 </Animated.View> 264 )} 265 266 <SettingsList.Group> 267 <SettingsList.ItemIcon icon={ColorPaletteIcon} /> 268 <SettingsList.ItemText> 269 <Trans>Color Theme</Trans> 270 </SettingsList.ItemText> 271 <View style={[a.w_full, a.gap_md]}> 272 <Text style={[a.flex_1, t.atoms.text_contrast_medium]}> 273 <Trans>Choose which color scheme to use:</Trans> 274 </Text> 275 <ColorSchemeGrid 276 schemes={colorSchemes} 277 selectedScheme={colorScheme} 278 onSchemeChange={onChangeScheme} 279 /> 280 {colorScheme === 'material3' && !IS_ANDROID && ( 281 <> 282 <Text style={[a.flex_1, t.atoms.text_contrast_medium]}> 283 <Trans>Accent hue:</Trans> 284 </Text> 285 <Slider 286 value={hexToHue(material3Accent)} 287 onValueChange={v => { 288 setMaterial3Accent(hueToHex(v)) 289 setHue(0) 290 }} 291 minimumValue={0} 292 maximumValue={360} 293 step={1} 294 debounceFull={true} 295 /> 296 297 <Text style={[a.flex_1, t.atoms.text_contrast_medium]}> 298 <Trans>Style:</Trans> 299 </Text> 300 <View style={[a.flex_row, a.flex_wrap, a.gap_sm]}> 301 {MATERIAL3_STYLE_OPTIONS.map(({name, label}) => { 302 const isSelected = material3Style === name 303 return ( 304 <Pressable 305 accessibilityRole="button" 306 key={name} 307 onPress={() => setMaterial3Style(name)} 308 style={[ 309 a.flex_1, 310 a.rounded_sm, 311 a.align_center, 312 a.px_sm, 313 a.py_sm, 314 a.border, 315 {minWidth: '22%'}, 316 { 317 borderColor: isSelected 318 ? t.palette.primary_500 319 : t.atoms.border_contrast_low.borderColor, 320 borderWidth: 2, 321 backgroundColor: isSelected 322 ? t.palette.primary_100 323 : t.atoms.bg.backgroundColor, 324 }, 325 ]}> 326 <Text 327 style={[ 328 a.text_xs, 329 a.font_bold, 330 isSelected 331 ? {color: t.palette.primary_500} 332 : t.atoms.text, 333 ]}> 334 {label} 335 </Text> 336 </Pressable> 337 ) 338 })} 339 </View> 340 </> 341 )} 342 {colorScheme !== 'material3' && ( 343 <> 344 <Text style={[a.flex_1, t.atoms.text_contrast_medium]}> 345 <Trans>Hue shift the colors:</Trans> 346 </Text> 347 <Slider 348 value={hue} 349 onValueChange={setHue} 350 minimumValue={0} 351 maximumValue={360} 352 step={1} 353 debounceFull={true} 354 /> 355 </> 356 )} 357 </View> 358 </SettingsList.Group> 359 360 <Animated.View layout={native(LinearTransition)}> 361 <SettingsList.Divider /> 362 363 <AppearanceToggleButtonGroup 364 title={_(msg`Font`)} 365 description={_( 366 msg`For the best experience, we recommend using the theme font.`, 367 )} 368 icon={Aa} 369 items={[ 370 { 371 label: _(msg`System`), 372 name: 'system', 373 }, 374 { 375 label: _(msg`Theme`), 376 name: 'theme', 377 }, 378 ...(IS_ANDROID 379 ? [ 380 { 381 label: _(msg`Google Sans`), 382 name: 'material' as 'system' | 'theme' | 'material', 383 }, 384 ] 385 : []), 386 ]} 387 value={fonts.family} 388 onChange={onChangeFontFamily} 389 /> 390 391 <AppearanceToggleButtonGroup 392 title={_(msg`Font size`)} 393 icon={TextSize} 394 items={[ 395 { 396 label: _(msg`Smaller`), 397 name: '-1', 398 }, 399 { 400 label: _(msg`Default`), 401 name: '0', 402 }, 403 { 404 label: _(msg`Larger`), 405 name: '1', 406 }, 407 ]} 408 value={fonts.scale} 409 onChange={onChangeFontScale} 410 /> 411 412 <SettingsList.Divider /> 413 414 <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 415 <SettingsList.ItemIcon icon={SparkleIcon} /> 416 <SettingsList.ItemText> 417 <Trans>Logo</Trans> 418 </SettingsList.ItemText> 419 <Toggle.Item 420 name="kawaii_mode" 421 label={_(msg`Enable kawaii logo`)} 422 value={kawaiiMode} 423 onChange={value => setKawaiiMode(value)} 424 style={[a.w_full]}> 425 <Toggle.LabelText style={[a.flex_1]}> 426 <Trans>Enable kawaii logo</Trans> 427 </Toggle.LabelText> 428 <Toggle.Platform /> 429 </Toggle.Item> 430 </SettingsList.Group> 431 432 <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 433 <SettingsList.ItemIcon icon={SquareIcon} /> 434 <SettingsList.ItemText> 435 <Trans>Shapes</Trans> 436 </SettingsList.ItemText> 437 <Toggle.Item 438 name="enable_square_avatars" 439 label={_(msg`Enable square avatars`)} 440 value={enableSquareAvatars} 441 onChange={value => setEnableSquareAvatars(value)} 442 style={[a.w_full]}> 443 <Toggle.LabelText style={[a.flex_1]}> 444 <Trans>Enable square avatars</Trans> 445 </Toggle.LabelText> 446 <Toggle.Platform /> 447 </Toggle.Item> 448 449 <Toggle.Item 450 name="enable_square_buttons" 451 label={_(msg`Enable square buttons`)} 452 value={enableSquareButtons} 453 onChange={value => setEnableSquareButtons(value)} 454 style={[a.w_full]}> 455 <Toggle.LabelText style={[a.flex_1]}> 456 <Trans>Enable square buttons</Trans> 457 </Toggle.LabelText> 458 <Toggle.Platform /> 459 </Toggle.Item> 460 </SettingsList.Group> 461 {IS_NATIVE && IS_INTERNAL && ( 462 <> 463 <SettingsList.Divider /> 464 <AppIconSettingsListItem /> 465 </> 466 )} 467 </Animated.View> 468 </SettingsList.Container> 469 </Layout.Content> 470 </Layout.Screen> 471 </LayoutAnimationConfig> 472 ) 473} 474 475function ColorSchemeGrid({ 476 schemes, 477 selectedScheme, 478 onSchemeChange, 479}: { 480 schemes: ColorSchemeOption[] 481 selectedScheme: ColorSchemeName 482 onSchemeChange: (scheme: ColorSchemeName) => void 483}) { 484 const t = useTheme() 485 return ( 486 <View style={[a.flex_row, a.flex_wrap, a.gap_sm]}> 487 {schemes.map(({name, label, primary}) => { 488 const isSelected = selectedScheme === name 489 const HeartIcon = isSelected ? HeartIconFilled : HeartIconOutline 490 return ( 491 <Pressable 492 accessibilityRole="button" 493 key={name} 494 onPress={() => onSchemeChange(name)} 495 style={[ 496 a.flex_1, 497 a.rounded_md, 498 a.overflow_hidden, 499 {minWidth: '30%'}, 500 a.border, 501 { 502 borderColor: isSelected 503 ? primary 504 : t.atoms.border_contrast_low.borderColor, 505 borderWidth: 2, 506 }, 507 ]}> 508 <View 509 style={[ 510 a.p_sm, 511 a.gap_xs, 512 {backgroundColor: t.atoms.bg.backgroundColor}, 513 ]}> 514 <View 515 style={[ 516 a.w_full, 517 a.rounded_xs, 518 {backgroundColor: primary, height: 24}, 519 ]} 520 /> 521 <View 522 style={[ 523 a.flex_row, 524 a.align_center, 525 a.justify_center, 526 a.gap_xs, 527 ]}> 528 <Text style={[a.text_sm, a.font_bold, t.atoms.text]}> 529 {label} 530 </Text> 531 <HeartIcon size="xs" style={[{color: primary}]} /> 532 </View> 533 </View> 534 </Pressable> 535 ) 536 })} 537 </View> 538 ) 539} 540 541export function AppearanceToggleButtonGroup<T extends string>({ 542 title, 543 description, 544 icon: Icon, 545 items, 546 value, 547 onChange, 548}: { 549 title: string 550 description?: string 551 icon: React.ComponentType<SVGIconProps> 552 items: { 553 label: string 554 name: T 555 }[] 556 value: T 557 onChange: (value: T) => void 558}) { 559 const t = useTheme() 560 return ( 561 <> 562 <SettingsList.Group contentContainerStyle={[a.gap_sm]} iconInset={false}> 563 <SettingsList.ItemIcon icon={Icon} /> 564 <SettingsList.ItemText>{title}</SettingsList.ItemText> 565 {description && ( 566 <Text 567 style={[ 568 a.text_sm, 569 a.leading_snug, 570 t.atoms.text_contrast_medium, 571 a.w_full, 572 ]}> 573 {description} 574 </Text> 575 )} 576 <SegmentedControl.Root 577 type="radio" 578 label={title} 579 value={value} 580 onChange={onChange}> 581 {items.map(item => ( 582 <SegmentedControl.Item 583 key={item.name} 584 label={item.label} 585 value={item.name}> 586 <SegmentedControl.ItemText> 587 {item.label} 588 </SegmentedControl.ItemText> 589 </SegmentedControl.Item> 590 ))} 591 </SegmentedControl.Root> 592 </SettingsList.Group> 593 </> 594 ) 595} 596 597const MATERIAL3_STYLE_OPTIONS: { 598 name: Schema['material3Style'] 599 label: string 600}[] = [ 601 {name: 'TONAL_SPOT', label: 'Tonal Spot'}, 602 {name: 'VIBRANT', label: 'Vibrant'}, 603 {name: 'EXPRESSIVE', label: 'Expressive'}, 604 {name: 'SPRITZ', label: 'Spritz'}, 605 {name: 'RAINBOW', label: 'Rainbow'}, 606 {name: 'FRUIT_SALAD', label: 'Fruit Salad'}, 607 {name: 'CONTENT', label: 'Content'}, 608 {name: 'MONOCHROMATIC', label: 'Mono'}, 609] 610 611function hueToHex(hue: number): string { 612 const h = hue / 60 613 const x = 1 - Math.abs((h % 2) - 1) 614 let r = 0, 615 g = 0, 616 b = 0 617 if (h < 1) { 618 r = 1 619 g = x 620 } else if (h < 2) { 621 r = x 622 g = 1 623 } else if (h < 3) { 624 g = 1 625 b = x 626 } else if (h < 4) { 627 g = x 628 b = 1 629 } else if (h < 5) { 630 r = x 631 b = 1 632 } else { 633 r = 1 634 b = x 635 } 636 const toHex = (v: number) => 637 Math.round(v * 255) 638 .toString(16) 639 .padStart(2, '0') 640 return `#${toHex(r)}${toHex(g)}${toHex(b)}` 641} 642 643function hexToHue(hex: string): number { 644 const r = parseInt(hex.slice(1, 3), 16) / 255 645 const g = parseInt(hex.slice(3, 5), 16) / 255 646 const b = parseInt(hex.slice(5, 7), 16) / 255 647 const max = Math.max(r, g, b) 648 const min = Math.min(r, g, b) 649 const d = max - min 650 if (d === 0) return 0 651 let h = 0 652 if (max === r) h = ((g - b) / d) % 6 653 else if (max === g) h = (b - r) / d + 2 654 else h = (r - g) / d + 4 655 h = Math.round(h * 60) 656 return h < 0 ? h + 360 : h 657}