Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

feat: new color theme selector (and fixes)

fix: evergarden primary color + hue shift for positive/negative colors, and pink/yellow colors!

xan.lol 719154b3 085a7fc4

verified
+180 -59
+26 -16
src/alf/index.tsx
··· 92 92 const keys = Object.keys(newPalette) as Array<keyof Palette> 93 93 94 94 keys.forEach(key => { 95 + if ( 96 + key.startsWith('positive_') || 97 + key.startsWith('negative_') || 98 + key === 'like' || 99 + key === 'pink' || 100 + key === 'yellow' 101 + ) { 102 + return 103 + } 95 104 newPalette[key] = changeHue(newPalette[key], hueShift) 96 105 }) 97 106 ··· 201 210 [setFontFamily], 202 211 ) 203 212 204 - const value = React.useMemo<Alf>( 205 - () => ({ 206 - themes: hueShifter(currentScheme, hue), 213 + const value = React.useMemo<Alf>(() => { 214 + const shiftedThemes = hueShifter(currentScheme, hue) 215 + 216 + return { 217 + themes: shiftedThemes, 207 218 themeName: themeName, 208 - theme: hueShifter(currentScheme, hue)[themeName], 219 + theme: shiftedThemes[themeName], 209 220 fonts: { 210 221 scale: fontScale, 211 222 scaleMultiplier: fontScaleMultiplier, ··· 214 225 setFontFamily: setFontFamilyAndPersist, 215 226 }, 216 227 flags: {}, 217 - }), 218 - [ 219 - currentScheme, 220 - hue, 221 - themeName, 222 - fontScale, 223 - fontScaleMultiplier, 224 - fontFamily, 225 - setFontScaleAndPersist, 226 - setFontFamilyAndPersist, 227 - ], 228 - ) 228 + } 229 + }, [ 230 + currentScheme, 231 + hue, 232 + themeName, 233 + fontScale, 234 + fontScaleMultiplier, 235 + fontFamily, 236 + setFontScaleAndPersist, 237 + setFontFamilyAndPersist, 238 + ]) 229 239 230 240 return <Context.Provider value={value}>{children}</Context.Provider> 231 241 }
+2 -2
src/alf/themes.ts
··· 1270 1270 primary_200: `hsl(72, 42%, 86%)`, 1271 1271 primary_300: `hsl(74, 54%, 84%)`, 1272 1272 primary_400: `hsl(83, 46%, 83%)`, 1273 - primary_500: `hsl(90, 46.2%, 79.6%)`, 1273 + primary_500: `hsl(150, 24%, 64%)`, 1274 1274 primary_600: `hsl(149, 44%, 72%)`, 1275 1275 primary_700: `hsl(167, 35%, 60%)`, 1276 1276 primary_800: `hsl(187, 42%, 36%)`, ··· 1336 1336 primary_200: `hsl(72, 42%, 86%)`, 1337 1337 primary_300: `hsl(74, 54%, 84%)`, 1338 1338 primary_400: `hsl(83, 46%, 83%)`, 1339 - primary_500: `hsl(90, 46.2%, 79.6%)`, 1339 + primary_500: `hsl(150, 24%, 64%)`, 1340 1340 primary_600: `hsl(149, 44%, 72%)`, 1341 1341 primary_700: `hsl(167, 35%, 60%)`, 1342 1342 primary_800: `hsl(187, 42%, 36%)`,
+152 -41
src/screens/Settings/AppearanceSettings.tsx
··· 1 1 import {useCallback} from 'react' 2 - import {View} from 'react-native' 2 + import {Pressable, View} from 'react-native' 3 3 import Animated, { 4 4 FadeInUp, 5 5 FadeOutUp, ··· 26 26 import {useSetThemePrefs, useThemePrefs} from '#/state/shell' 27 27 import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem' 28 28 import {type Alf, atoms as a, native, useAlf, useTheme} from '#/alf' 29 + import { 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' 29 40 import * as SegmentedControl from '#/components/forms/SegmentedControl' 30 41 import {Slider} from '#/components/forms/Slider' 31 42 import * as Toggle from '#/components/forms/Toggle' 32 43 import {Circle_And_Square_Stroke1_Corner0_Rounded_Filled as SquareIcon} from '#/components/icons/CircleAndSquare' 33 44 import {ColorPalette_Stroke2_Corner0_Rounded as ColorPaletteIcon} from '#/components/icons/ColorPalette' 34 45 import {type Props as SVGIconProps} from '#/components/icons/common' 46 + import { 47 + Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled, 48 + Heart2_Stroke2_Corner0_Rounded as HeartIconOutline, 49 + } from '#/components/icons/Heart2' 35 50 import {Moon_Stroke2_Corner0_Rounded as MoonIcon} from '#/components/icons/Moon' 36 51 import {Phone_Stroke2_Corner0_Rounded as PhoneIcon} from '#/components/icons/Phone' 37 52 import {Sparkle_Stroke2_Corner0_Rounded as SparkleIcon} from '#/components/icons/Sparkle' ··· 43 58 import * as SettingsList from './components/SettingsList' 44 59 45 60 type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'> 61 + 62 + type ColorSchemeName = 63 + | 'witchsky' 64 + | 'bluesky' 65 + | 'blacksky' 66 + | 'deer' 67 + | 'zeppelin' 68 + | 'kitty' 69 + | 'reddwarf' 70 + | 'catppuccin' 71 + | 'evergarden' 72 + 73 + type ColorSchemeOption = { 74 + name: ColorSchemeName 75 + label: string 76 + primary: string 77 + } 78 + 46 79 export function AppearanceSettingsScreen({}: Props) { 47 80 const {_} = useLingui() 48 81 const {fonts} = useAlf() ··· 69 102 ) 70 103 71 104 const onChangeScheme = useCallback( 72 - ( 73 - value: 74 - | 'witchsky' 75 - | 'bluesky' 76 - | 'blacksky' 77 - | 'deer' 78 - | 'zeppelin' 79 - | 'kitty' 80 - | 'reddwarf' 81 - | 'catppuccin' 82 - | 'evergarden', 83 - ) => { 105 + (value: ColorSchemeName) => { 84 106 setColorScheme(value) 85 107 }, 86 108 [setColorScheme], ··· 107 129 [fonts], 108 130 ) 109 131 110 - const colorSchemes = [ 111 - {name: 'witchsky', label: _(msg`Witchsky`)}, 112 - {name: 'bluesky', label: _(msg`Bluesky`)}, 113 - {name: 'blacksky', label: _(msg`Blacksky`)}, 114 - {name: 'deer', label: _(msg`Deer`)}, 115 - {name: 'zeppelin', label: _(msg`Zeppelin`)}, 116 - {name: 'kitty', label: _(msg`Kitty`)}, 117 - {name: 'reddwarf', label: _(msg`Red Dwarf`)}, 118 - {name: 'catppuccin', label: _(msg`Catppuccin`)}, 119 - {name: 'evergarden', label: _(msg`Evergarden`)}, 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 + }, 120 178 ] 121 179 122 180 return ( ··· 186 244 <Text style={[a.flex_1, t.atoms.text_contrast_medium]}> 187 245 <Trans>Choose which color scheme to use:</Trans> 188 246 </Text> 189 - <Toggle.Group 190 - label={_(msg`Color Theme`)} 191 - type="radio" 192 - values={[colorScheme]} 193 - onChange={([value]) => 194 - onChangeScheme(value as typeof colorScheme) 195 - }> 196 - <View style={[a.gap_sm, a.flex_1]}> 197 - {colorSchemes.map(({name, label}) => ( 198 - <Toggle.Item key={name} name={name} label={label}> 199 - <Toggle.Radio /> 200 - <Toggle.LabelText> 201 - <Trans>{label}</Trans> 202 - </Toggle.LabelText> 203 - </Toggle.Item> 204 - ))} 205 - </View> 206 - </Toggle.Group> 247 + <ColorSchemeGrid 248 + schemes={colorSchemes} 249 + selectedScheme={colorScheme} 250 + onSchemeChange={onChangeScheme} 251 + /> 207 252 <Text style={[a.flex_1, t.atoms.text_contrast_medium]}> 208 253 <Trans>Hue shift the colors:</Trans> 209 254 </Text> ··· 322 367 </Layout.Content> 323 368 </Layout.Screen> 324 369 </LayoutAnimationConfig> 370 + ) 371 + } 372 + 373 + function 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> 325 436 ) 326 437 } 327 438