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