Bluesky app fork with some witchin' additions 馃挮
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}