forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useState} from 'react'
2import {Alert, View} from 'react-native'
3import {msg, Trans} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
5import * as DynamicAppIcon from '@mozzius/expo-dynamic-app-icon'
6import {type NativeStackScreenProps} from '@react-navigation/native-stack'
7
8import {PressableScale} from '#/lib/custom-animations/PressableScale'
9import {type CommonNavigatorParams} from '#/lib/routes/types'
10import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage'
11import {type AppIconSet} from '#/screens/Settings/AppIconSettings/types'
12import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets'
13import {atoms as a, useTheme} from '#/alf'
14import * as Toggle from '#/components/forms/Toggle'
15import * as Layout from '#/components/Layout'
16import {Text} from '#/components/Typography'
17import {IS_ANDROID, IS_INTERNAL} from '#/env'
18
19type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'>
20export function AppIconSettingsScreen({}: Props) {
21 const t = useTheme()
22 const {_} = useLingui()
23 const sets = useAppIconSets()
24 const [currentAppIcon, setCurrentAppIcon] = useState(() =>
25 getAppIconName(DynamicAppIcon.getAppIcon()),
26 )
27
28 const onSetAppIcon = (icon: DynamicAppIcon.IconName) => {
29 if (IS_ANDROID) {
30 const next =
31 sets.defaults.find(i => i.id === icon) ??
32 sets.core.find(i => i.id === icon)
33 Alert.alert(
34 next
35 ? _(msg`Change app icon to "${next.name}"`)
36 : _(msg`Change app icon`),
37 // unfortunately necessary -sfn
38 _(msg`The app will be restarted`),
39 [
40 {
41 text: _(msg`Cancel`),
42 style: 'cancel',
43 },
44 {
45 text: _(msg`OK`),
46 onPress: () => {
47 setCurrentAppIcon(setAppIcon(icon))
48 },
49 style: 'default',
50 },
51 ],
52 )
53 } else {
54 setCurrentAppIcon(setAppIcon(icon))
55 }
56 }
57
58 return (
59 <Layout.Screen>
60 <Layout.Header.Outer>
61 <Layout.Header.BackButton />
62 <Layout.Header.Content>
63 <Layout.Header.TitleText>
64 <Trans>App Icon</Trans>
65 </Layout.Header.TitleText>
66 </Layout.Header.Content>
67 <Layout.Header.Slot />
68 </Layout.Header.Outer>
69
70 <Layout.Content contentContainerStyle={[a.p_lg]}>
71 <Group
72 label={_(msg`Default icons`)}
73 value={currentAppIcon}
74 onChange={onSetAppIcon}>
75 {sets.defaults.map((icon, i) => (
76 <Row
77 key={icon.id}
78 icon={icon}
79 isEnd={i === sets.defaults.length - 1}>
80 <AppIcon icon={icon} key={icon.id} size={40} />
81 <RowText>{icon.name}</RowText>
82 </Row>
83 ))}
84 </Group>
85
86 {IS_INTERNAL && (
87 <>
88 <Text
89 style={[
90 a.text_md,
91 a.mt_xl,
92 a.mb_sm,
93 a.font_semi_bold,
94 t.atoms.text_contrast_medium,
95 ]}>
96 <Trans>Bluesky+</Trans>
97 </Text>
98 <Group
99 label={_(msg`Bluesky+ icons`)}
100 value={currentAppIcon}
101 onChange={onSetAppIcon}>
102 {sets.core.map((icon, i) => (
103 <Row
104 key={icon.id}
105 icon={icon}
106 isEnd={i === sets.core.length - 1}>
107 <AppIcon icon={icon} key={icon.id} size={40} />
108 <RowText>{icon.name}</RowText>
109 </Row>
110 ))}
111 </Group>
112 </>
113 )}
114 </Layout.Content>
115 </Layout.Screen>
116 )
117}
118
119function setAppIcon(icon: DynamicAppIcon.IconName) {
120 if (icon === 'default_light') {
121 return getAppIconName(DynamicAppIcon.setAppIcon(null))
122 } else {
123 return getAppIconName(DynamicAppIcon.setAppIcon(icon))
124 }
125}
126
127function getAppIconName(icon: string | false): DynamicAppIcon.IconName {
128 if (!icon || icon === 'DEFAULT') {
129 return 'default_light'
130 } else {
131 return icon as DynamicAppIcon.IconName
132 }
133}
134
135function Group({
136 children,
137 label,
138 value,
139 onChange,
140}: {
141 children: React.ReactNode
142 label: string
143 value: DynamicAppIcon.IconName
144 onChange: (value: DynamicAppIcon.IconName) => void
145}) {
146 return (
147 <Toggle.Group
148 type="radio"
149 label={label}
150 values={[value]}
151 maxSelections={1}
152 onChange={vals => {
153 if (vals[0]) onChange(vals[0] as DynamicAppIcon.IconName)
154 }}>
155 <View style={[a.flex_1, a.rounded_md, a.overflow_hidden]}>
156 {children}
157 </View>
158 </Toggle.Group>
159 )
160}
161
162function Row({
163 icon,
164 children,
165 isEnd,
166}: {
167 icon: AppIconSet
168 children: React.ReactNode
169 isEnd: boolean
170}) {
171 const t = useTheme()
172 const {_} = useLingui()
173
174 return (
175 <Toggle.Item label={_(msg`Set app icon to ${icon.name}`)} name={icon.id}>
176 {({hovered, pressed}) => (
177 <View
178 style={[
179 a.flex_1,
180 a.p_md,
181 a.flex_row,
182 a.gap_md,
183 a.align_center,
184 t.atoms.bg_contrast_25,
185 (hovered || pressed) && t.atoms.bg_contrast_50,
186 t.atoms.border_contrast_high,
187 !isEnd && a.border_b,
188 ]}>
189 {children}
190 <Toggle.Radio />
191 </View>
192 )}
193 </Toggle.Item>
194 )
195}
196
197function RowText({children}: {children: React.ReactNode}) {
198 const t = useTheme()
199 return (
200 <Text
201 style={[
202 a.text_md,
203 a.font_semi_bold,
204 a.flex_1,
205 t.atoms.text_contrast_medium,
206 ]}
207 emoji>
208 {children}
209 </Text>
210 )
211}
212
213function AppIcon({icon, size = 50}: {icon: AppIconSet; size: number}) {
214 const {_} = useLingui()
215 return (
216 <PressableScale
217 accessibilityLabel={icon.name}
218 accessibilityHint={_(msg`Changes app icon`)}
219 targetScale={0.95}
220 onPress={() => {
221 if (IS_ANDROID) {
222 Alert.alert(
223 _(msg`Change app icon to "${icon.name}"`),
224 _(msg`The app will be restarted`),
225 [
226 {
227 text: _(msg`Cancel`),
228 style: 'cancel',
229 },
230 {
231 text: _(msg`OK`),
232 onPress: () => {
233 DynamicAppIcon.setAppIcon(icon.id)
234 },
235 style: 'default',
236 },
237 ],
238 )
239 } else {
240 DynamicAppIcon.setAppIcon(icon.id)
241 }
242 }}>
243 <AppIconImage icon={icon} size={size} />
244 </PressableScale>
245 )
246}