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