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

Configure Feed

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

at cope-settings-sync 418 lines 14 kB view raw
1import {useState} from 'react' 2import {View} from 'react-native' 3import {Trans, useLingui} from '@lingui/react/macro' 4import {type NativeStackScreenProps} from '@react-navigation/native-stack' 5 6import { 7 DEFAULT_ALT_TEXT_AI_MODEL, 8 DEFAULT_ALT_TEXT_AI_PROMPT, 9} from '#/lib/constants' 10import {usePalette} from '#/lib/hooks/usePalette' 11import {type CommonNavigatorParams} from '#/lib/routes/types' 12import { 13 useHapticsDisabled, 14 useRequireAltTextEnabled, 15 useSetHapticsDisabled, 16 useSetRequireAltTextEnabled, 17} from '#/state/preferences' 18import { 19 useLargeAltBadgeEnabled, 20 useSetLargeAltBadgeEnabled, 21} from '#/state/preferences/large-alt-badge' 22import { 23 useOpenRouterApiKey, 24 useOpenRouterConfigured, 25 useOpenRouterModel, 26 useOpenRouterPrompt, 27 useSetOpenRouterApiKey, 28 useSetOpenRouterModel, 29 useSetOpenRouterPrompt, 30} from '#/state/preferences/openrouter' 31import * as SettingsList from '#/screens/Settings/components/SettingsList' 32import {atoms as a} from '#/alf' 33import {Admonition} from '#/components/Admonition' 34import {Button, ButtonText} from '#/components/Button' 35import * as Dialog from '#/components/Dialog' 36import * as Toggle from '#/components/forms/Toggle' 37import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' 38import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic' 39import {Lab_Stroke2_Corner0_Rounded as BeakerIcon} from '#/components/icons/Lab' 40import * as Layout from '#/components/Layout' 41import {InlineLinkText} from '#/components/Link' 42import {Text} from '#/components/Typography' 43import {IS_NATIVE, IS_WEB} from '#/env' 44 45type Props = NativeStackScreenProps< 46 CommonNavigatorParams, 47 'AccessibilitySettings' 48> 49export function AccessibilitySettingsScreen({}: Props) { 50 const {t: l} = useLingui() 51 52 const requireAltTextEnabled = useRequireAltTextEnabled() 53 const setRequireAltTextEnabled = useSetRequireAltTextEnabled() 54 const hapticsDisabled = useHapticsDisabled() 55 const setHapticsDisabled = useSetHapticsDisabled() 56 const largeAltBadgeEnabled = useLargeAltBadgeEnabled() 57 const setLargeAltBadgeEnabled = useSetLargeAltBadgeEnabled() 58 59 const setOpenRouterApiKeyControl = Dialog.useDialogControl() 60 const openRouterConfigured = useOpenRouterConfigured() 61 const openRouterModel = useOpenRouterModel() 62 const setOpenRouterModelControl = Dialog.useDialogControl() 63 const setOpenRouterPromptControl = Dialog.useDialogControl() 64 65 return ( 66 <Layout.Screen> 67 <Layout.Header.Outer> 68 <Layout.Header.BackButton /> 69 <Layout.Header.Content> 70 <Layout.Header.TitleText> 71 <Trans>Accessibility</Trans> 72 </Layout.Header.TitleText> 73 </Layout.Header.Content> 74 <Layout.Header.Slot /> 75 </Layout.Header.Outer> 76 <Layout.Content> 77 <SettingsList.Container> 78 <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 79 <SettingsList.ItemIcon icon={AccessibilityIcon} /> 80 <SettingsList.ItemText> 81 <Trans>Alt text</Trans> 82 </SettingsList.ItemText> 83 <Toggle.Item 84 name="require_alt_text" 85 label={l`Require alt text before posting`} 86 value={requireAltTextEnabled ?? false} 87 onChange={value => setRequireAltTextEnabled(value)} 88 style={[a.w_full]}> 89 <Toggle.LabelText style={[a.flex_1]}> 90 <Trans>Require alt text before posting</Trans> 91 </Toggle.LabelText> 92 <Toggle.Platform /> 93 </Toggle.Item> 94 <Toggle.Item 95 name="large_alt_badge" 96 label={l`Display larger alt text badges`} 97 value={!!largeAltBadgeEnabled} 98 onChange={value => setLargeAltBadgeEnabled(value)} 99 style={[a.w_full]}> 100 <Toggle.LabelText style={[a.flex_1]}> 101 <Trans>Display larger alt text badges</Trans> 102 </Toggle.LabelText> 103 <Toggle.Platform /> 104 </Toggle.Item> 105 </SettingsList.Group> 106 {IS_NATIVE && ( 107 <> 108 <SettingsList.Divider /> 109 <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 110 <SettingsList.ItemIcon icon={HapticIcon} /> 111 <SettingsList.ItemText> 112 <Trans>Haptics</Trans> 113 </SettingsList.ItemText> 114 <Toggle.Item 115 name="haptics" 116 label={l`Disable haptic feedback`} 117 value={hapticsDisabled ?? false} 118 onChange={value => setHapticsDisabled(value)} 119 style={[a.w_full]}> 120 <Toggle.LabelText style={[a.flex_1]}> 121 <Trans>Disable haptic feedback</Trans> 122 </Toggle.LabelText> 123 <Toggle.Platform /> 124 </Toggle.Item> 125 </SettingsList.Group> 126 </> 127 )} 128 129 <SettingsList.Item> 130 <SettingsList.ItemIcon icon={BeakerIcon} /> 131 <SettingsList.ItemText> 132 <Trans>OpenRouter API Key</Trans> 133 </SettingsList.ItemText> 134 <SettingsList.BadgeButton 135 label={openRouterConfigured ? l`Change` : l`Set`} 136 onPress={() => setOpenRouterApiKeyControl.open()} 137 /> 138 </SettingsList.Item> 139 140 <SettingsList.Item> 141 <Admonition type="info" style={[a.flex_1]}> 142 <Trans> 143 Set your OpenRouter API key to enable AI-powered alt text 144 generation for images in the composer. Get an API key at{' '} 145 <InlineLinkText 146 to="https://openrouter.ai" 147 label="openrouter.ai"> 148 openrouter.ai 149 </InlineLinkText> 150 </Trans> 151 </Admonition> 152 </SettingsList.Item> 153 154 {openRouterConfigured && ( 155 <SettingsList.Item> 156 <SettingsList.ItemIcon icon={BeakerIcon} /> 157 <SettingsList.ItemText> 158 <Trans>{`OpenRouter Model`}</Trans> 159 </SettingsList.ItemText> 160 <SettingsList.BadgeButton 161 label={l`Change`} 162 onPress={() => setOpenRouterModelControl.open()} 163 /> 164 </SettingsList.Item> 165 )} 166 167 {openRouterConfigured && ( 168 <SettingsList.Item> 169 <Admonition type="info" style={[a.flex_1]}> 170 <Trans> 171 Current model: {openRouterModel ?? DEFAULT_ALT_TEXT_AI_MODEL}.{' '} 172 <InlineLinkText 173 to="https://openrouter.ai/models?fmt=cards&input_modalities=image&order=most-popular" 174 label="openrouter.ai"> 175 Search models 176 </InlineLinkText> 177 </Trans> 178 </Admonition> 179 </SettingsList.Item> 180 )} 181 182 {openRouterConfigured && ( 183 <SettingsList.Item> 184 <SettingsList.ItemIcon icon={BeakerIcon} /> 185 <SettingsList.ItemText> 186 <Trans>Alt Text Prompt</Trans> 187 </SettingsList.ItemText> 188 <SettingsList.BadgeButton 189 label={l`Change`} 190 onPress={() => setOpenRouterPromptControl.open()} 191 /> 192 </SettingsList.Item> 193 )} 194 195 {openRouterConfigured && ( 196 <SettingsList.Item> 197 <Admonition type="info" style={[a.flex_1]}> 198 <Trans> 199 Customize the prompt sent to the AI model when generating alt 200 text. Leave empty to use the default prompt. 201 </Trans> 202 </Admonition> 203 </SettingsList.Item> 204 )} 205 206 <OpenRouterApiKeyDialog control={setOpenRouterApiKeyControl} /> 207 <OpenRouterModelDialog control={setOpenRouterModelControl} /> 208 <OpenRouterPromptDialog control={setOpenRouterPromptControl} /> 209 </SettingsList.Container> 210 </Layout.Content> 211 </Layout.Screen> 212 ) 213} 214 215function OpenRouterApiKeyDialog({ 216 control, 217}: { 218 control: Dialog.DialogControlProps 219}) { 220 const pal = usePalette('default') 221 const {t: l} = useLingui() 222 223 const apiKey = useOpenRouterApiKey() 224 const [value, setValue] = useState(apiKey ?? '') 225 const setApiKey = useSetOpenRouterApiKey() 226 227 return ( 228 <Dialog.Outer 229 control={control} 230 nativeOptions={{preventExpansion: true}} 231 onClose={() => setValue(apiKey ?? '')}> 232 <Dialog.Handle /> 233 <Dialog.ScrollableInner label={l`OpenRouter API Key`}> 234 <View style={[a.gap_sm, a.pb_lg]}> 235 <Text style={[a.text_2xl, a.font_bold]}> 236 <Trans>OpenRouter API Key</Trans> 237 </Text> 238 </View> 239 240 <View style={a.gap_lg}> 241 <Dialog.Input 242 label="API Key" 243 autoFocus 244 style={[styles.textInput, pal.border, pal.text]} 245 onChangeText={setValue} 246 placeholder="sk-or-..." 247 placeholderTextColor={pal.colors.textLight} 248 onSubmitEditing={() => { 249 setApiKey(value.trim() || undefined) 250 control.close() 251 }} 252 accessibilityHint={l`Enter your OpenRouter API key for AI alt text generation`} 253 defaultValue={apiKey ?? ''} 254 secureTextEntry 255 /> 256 257 <View style={IS_WEB && [a.flex_row, a.justify_end]}> 258 <Button 259 label={l`Save`} 260 size="large" 261 onPress={() => { 262 setApiKey(value.trim() || undefined) 263 control.close() 264 }} 265 variant="solid" 266 color="primary"> 267 <ButtonText> 268 <Trans>Save</Trans> 269 </ButtonText> 270 </Button> 271 </View> 272 </View> 273 274 <Dialog.Close /> 275 </Dialog.ScrollableInner> 276 </Dialog.Outer> 277 ) 278} 279 280function OpenRouterModelDialog({ 281 control, 282}: { 283 control: Dialog.DialogControlProps 284}) { 285 const pal = usePalette('default') 286 const {t: l} = useLingui() 287 288 const model = useOpenRouterModel() 289 const [value, setValue] = useState(model ?? '') 290 const setModel = useSetOpenRouterModel() 291 292 return ( 293 <Dialog.Outer 294 control={control} 295 nativeOptions={{preventExpansion: true}} 296 onClose={() => setValue(model ?? '')}> 297 <Dialog.Handle /> 298 <Dialog.ScrollableInner label={l`OpenRouter Model`}> 299 <View style={[a.gap_sm, a.pb_lg]}> 300 <Text style={[a.text_2xl, a.font_bold]}> 301 <Trans>OpenRouter Model</Trans> 302 </Text> 303 </View> 304 305 <View style={a.gap_lg}> 306 <Dialog.Input 307 label="Model" 308 autoFocus 309 style={[styles.textInput, pal.border, pal.text]} 310 onChangeText={setValue} 311 placeholder={DEFAULT_ALT_TEXT_AI_MODEL} 312 placeholderTextColor={pal.colors.textLight} 313 onSubmitEditing={() => { 314 setModel(value.trim() || undefined) 315 control.close() 316 }} 317 accessibilityHint={l`Enter the model ID to use for alt text generation`} 318 defaultValue={model ?? ''} 319 /> 320 321 <View style={IS_WEB && [a.flex_row, a.justify_end]}> 322 <Button 323 label={l`Save`} 324 size="large" 325 onPress={() => { 326 setModel(value.trim() || undefined) 327 control.close() 328 }} 329 variant="solid" 330 color="primary"> 331 <ButtonText> 332 <Trans>Save</Trans> 333 </ButtonText> 334 </Button> 335 </View> 336 </View> 337 338 <Dialog.Close /> 339 </Dialog.ScrollableInner> 340 </Dialog.Outer> 341 ) 342} 343 344function OpenRouterPromptDialog({ 345 control, 346}: { 347 control: Dialog.DialogControlProps 348}) { 349 const pal = usePalette('default') 350 const {t: l} = useLingui() 351 352 const prompt = useOpenRouterPrompt() 353 const [value, setValue] = useState(prompt ?? '') 354 const setPrompt = useSetOpenRouterPrompt() 355 356 return ( 357 <Dialog.Outer 358 control={control} 359 nativeOptions={{preventExpansion: true}} 360 onClose={() => setValue(prompt ?? '')}> 361 <Dialog.Handle /> 362 <Dialog.ScrollableInner label={l`Alt Text Prompt`}> 363 <View style={[a.gap_sm, a.pb_lg]}> 364 <Text style={[a.text_2xl, a.font_bold]}> 365 <Trans>Alt Text Prompt</Trans> 366 </Text> 367 </View> 368 369 <View style={a.gap_lg}> 370 <Dialog.Input 371 label="Prompt" 372 multiline 373 numberOfLines={6} 374 style={[ 375 styles.textInput, 376 pal.border, 377 pal.text, 378 {minHeight: 120, textAlignVertical: 'top'}, 379 ]} 380 onChangeText={setValue} 381 placeholder={DEFAULT_ALT_TEXT_AI_PROMPT} 382 placeholderTextColor={pal.colors.textLight} 383 accessibilityHint={l`Enter a custom prompt for AI alt text generation`} 384 defaultValue={prompt ?? ''} 385 /> 386 387 <View style={IS_WEB && [a.flex_row, a.justify_end]}> 388 <Button 389 label={l`Save`} 390 size="large" 391 onPress={() => { 392 setPrompt(value.trim() || undefined) 393 control.close() 394 }} 395 variant="solid" 396 color="primary"> 397 <ButtonText> 398 <Trans>Save</Trans> 399 </ButtonText> 400 </Button> 401 </View> 402 </View> 403 404 <Dialog.Close /> 405 </Dialog.ScrollableInner> 406 </Dialog.Outer> 407 ) 408} 409 410const styles = { 411 textInput: { 412 borderWidth: 1, 413 borderRadius: 6, 414 paddingHorizontal: 14, 415 paddingVertical: 10, 416 fontSize: 16, 417 }, 418}