Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/orange-ways-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@use-voltra/android': minor
'voltra': minor
'@use-voltra/ios': minor
---

Add SVG support to image preloading on iOS and Android.
165 changes: 164 additions & 1 deletion example/screens/android/AndroidImagePreloadingScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { useRouter } from 'expo-router'
import React, { useState } from 'react'
import { Alert, StyleSheet, Text, View } from 'react-native'
import { VoltraAndroid } from '@use-voltra/android'
import { clearPreloadedImages, preloadImages, reloadWidgets, updateAndroidWidget } from '@use-voltra/android-client'
import {
clearPreloadedImages,
preloadImages,
reloadWidgets,
updateAndroidWidget,
VoltraWidgetPreview,
} from '@use-voltra/android-client'

import { Button } from '~/components/Button'
import { ScreenLayout } from '~/components/ScreenLayout'
Expand All @@ -12,12 +18,70 @@ function generateRandomUrl(): string {
return `https://picsum.photos/id/${Math.floor(Math.random() * 200)}/300/200`
}

const ANDROID_SVG_OPTIONS = {
green: {
key: 'android-widget-svg-test-green',
color: '#34C759',
title: 'Show Green SVG in Widget',
},
purple: {
key: 'android-widget-svg-test-purple',
color: '#7C3AED',
title: 'Show Purple SVG in Widget',
},
} as const

type AndroidSvgOption = (typeof ANDROID_SVG_OPTIONS)[keyof typeof ANDROID_SVG_OPTIONS]

function createAndroidTestSvg(color: string): string {
return `
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" rx="12" fill="${color}"/>
<path d="M15 25.5l6.2 6.2L34 17" stroke="white" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`
}

function AndroidSvgWidgetContent({ assetKey, color }: { assetKey: string; color: string }) {
return (
<VoltraAndroid.Box
style={{
padding: 14,
backgroundColor: '#1E293B',
borderRadius: 16,
flex: 1,
}}
>
<VoltraAndroid.Column horizontalAlignment="center-horizontally" verticalAlignment="center-vertically">
<VoltraAndroid.Image source={{ assetName: assetKey }} style={{ width: 42, height: 42 }} resizeMode="contain" />

<VoltraAndroid.Spacer style={{ height: 10 }} />

<VoltraAndroid.Text maxLines={1} style={{ fontSize: 16, fontWeight: 'bold', color: '#FFFFFF' }}>
SVG preload
</VoltraAndroid.Text>
<VoltraAndroid.Text maxLines={1} style={{ fontSize: 12, color: '#CBD5E1' }}>
{color}
</VoltraAndroid.Text>

<VoltraAndroid.Spacer style={{ height: 14 }} />

<VoltraAndroid.Text maxLines={2} style={{ fontSize: 11, color: '#94A3B8', textAlign: 'center' }}>
Preloaded SVG asset
</VoltraAndroid.Text>
</VoltraAndroid.Column>
</VoltraAndroid.Box>
)
}

export default function AndroidImagePreloadingScreen() {
const router = useRouter()
const [url, setUrl] = useState(generateRandomUrl())
const [isProcessing, setIsProcessing] = useState(false)
const [assetKey] = useState('android-preload-test')
const [updateCount, setUpdateCount] = useState(0)
const [isSvgProcessing, setIsSvgProcessing] = useState(false)
const [selectedSvgOption, setSelectedSvgOption] = useState<AndroidSvgOption | null>(null)

const handleUpdateAndPreload = async () => {
if (!url.trim()) {
Expand Down Expand Up @@ -129,6 +193,40 @@ export default function AndroidImagePreloadingScreen() {
}
}

const handleShowSvgWidget = async (option: AndroidSvgOption) => {
setIsSvgProcessing(true)

try {
const result = await preloadImages([
{
key: option.key,
svg: createAndroidTestSvg(option.color),
width: 48,
height: 48,
},
])

if (result.failed.length > 0) {
throw new Error(result.failed[0].error)
}

await updateAndroidWidget('image_preloading', [
{
size: { width: 300, height: 200 },
content: <AndroidSvgWidgetContent assetKey={option.key} color={option.color} />,
},
])
await reloadWidgets(['image_preloading'])

setSelectedSvgOption(option)
Alert.alert('Success', 'SVG preloaded and widget updated!')
} catch (error) {
Alert.alert('Error', `Failed to update SVG widget: ${error}`)
} finally {
setIsSvgProcessing(false)
}
}

return (
<ScreenLayout
title="Android Image Preloading"
Expand Down Expand Up @@ -162,6 +260,39 @@ export default function AndroidImagePreloadingScreen() {
<Button title="Clear Images" variant="secondary" onPress={handleClearImages} />
</View>

<View style={styles.section}>
<Text style={styles.sectionTitle}>SVG Widget Test</Text>
<Text style={styles.sectionText}>
Preload an inline SVG as a PNG asset and update the Android image preloading widget to render it.
</Text>

<View style={styles.buttonRow}>
<Button
title={isSvgProcessing ? 'Updating...' : ANDROID_SVG_OPTIONS.green.title}
variant={selectedSvgOption?.key === ANDROID_SVG_OPTIONS.green.key ? 'primary' : 'secondary'}
style={selectedSvgOption?.key === ANDROID_SVG_OPTIONS.green.key ? styles.greenButtonSelected : undefined}
onPress={() => handleShowSvgWidget(ANDROID_SVG_OPTIONS.green)}
disabled={isSvgProcessing}
/>
<Button
title={ANDROID_SVG_OPTIONS.purple.title}
variant={selectedSvgOption?.key === ANDROID_SVG_OPTIONS.purple.key ? 'primary' : 'secondary'}
style={selectedSvgOption?.key === ANDROID_SVG_OPTIONS.purple.key ? styles.purpleButtonSelected : undefined}
onPress={() => handleShowSvgWidget(ANDROID_SVG_OPTIONS.purple)}
disabled={isSvgProcessing}
/>
</View>

{selectedSvgOption ? (
<View style={styles.previewContainer}>
<Text style={styles.inputLabel}>Preview</Text>
<VoltraWidgetPreview key={selectedSvgOption.key} family="mediumWide" style={styles.widgetPreview}>
<AndroidSvgWidgetContent assetKey={selectedSvgOption.key} color={selectedSvgOption.color} />
</VoltraWidgetPreview>
</View>
) : null}
</View>

<View style={styles.footer}>
<Button title="Back to Android Home" variant="ghost" onPress={() => router.back()} />
</View>
Expand Down Expand Up @@ -190,6 +321,38 @@ const styles = StyleSheet.create({
flexDirection: 'column',
gap: 12,
},
section: {
marginTop: 24,
padding: 16,
borderRadius: 16,
borderWidth: 1,
borderColor: 'rgba(148, 163, 184, 0.2)',
backgroundColor: 'rgba(15, 23, 42, 0.8)',
},
sectionTitle: {
fontSize: 20,
fontWeight: '700',
color: '#FFFFFF',
},
sectionText: {
marginTop: 8,
fontSize: 14,
lineHeight: 20,
color: '#CBD5F5',
},
greenButtonSelected: {
backgroundColor: '#34C759',
},
purpleButtonSelected: {
backgroundColor: '#7C3AED',
},
previewContainer: {
marginTop: 16,
},
widgetPreview: {
borderRadius: 16,
overflow: 'hidden',
},
footer: {
marginTop: 24,
alignItems: 'center',
Expand Down
143 changes: 142 additions & 1 deletion example/screens/testing-grounds/ImagePreloadingScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ import { useRouter } from 'expo-router'
import React, { useState } from 'react'
import { Alert, StyleSheet, Text, View } from 'react-native'
import { Voltra } from '@use-voltra/ios'
import { clearPreloadedImages, preloadImages, reloadLiveActivities, startLiveActivity } from '@use-voltra/ios-client'
import {
clearPreloadedImages,
preloadImages,
reloadLiveActivities,
reloadWidgets,
startLiveActivity,
updateWidget,
VoltraWidgetPreview,
} from '@use-voltra/ios-client'

import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
Expand All @@ -13,11 +21,62 @@ function generateRandomKey(): string {
return `asset-${Math.random().toString(36).substring(2, 15)}`
}

const SVG_OPTIONS = {
green: {
key: 'ios-widget-svg-test-green',
color: '#34C759',
title: 'Show Green SVG in Widget',
},
purple: {
key: 'ios-widget-svg-test-purple',
color: '#7C3AED',
title: 'Show Purple SVG in Widget',
},
} as const

type SvgOption = (typeof SVG_OPTIONS)[keyof typeof SVG_OPTIONS]

function createTestSvg(color: string): string {
return `
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" rx="12" fill="${color}"/>
<path d="M15 25.5l6.2 6.2L34 17" stroke="white" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`
}

function SvgWidgetContent({ assetKey, color }: { assetKey: string; color: string }) {
return (
<Voltra.LinearGradient
colors={['#101828', '#1D2939']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={{ flex: 1 }}
>
<Voltra.VStack style={{ flex: 1, padding: 16 }}>
<Voltra.HStack alignment="center" spacing={10}>
<Voltra.Image source={{ assetName: assetKey }} resizeMode="contain" style={{ width: 48, height: 48 }} />
<Voltra.VStack spacing={3}>
<Voltra.Text style={{ color: '#FFFFFF', fontSize: 16, fontWeight: '700' }}>SVG preload</Voltra.Text>
<Voltra.Text style={{ color: '#D0D5DD', fontSize: 12 }}>{color}</Voltra.Text>
</Voltra.VStack>
</Voltra.HStack>

<Voltra.Spacer />

<Voltra.Text style={{ color: '#98A2B3', fontSize: 11 }}>Rendered from a preloaded SVG asset</Voltra.Text>
</Voltra.VStack>
</Voltra.LinearGradient>
)
}

export default function ImagePreloadingScreen() {
const router = useRouter()
const [url, setUrl] = useState(`https://picsum.photos/id/${Math.floor(Math.random() * 120)}/100/100`)
const [isProcessing, setIsProcessing] = useState(false)
const [currentAssetKey, setCurrentAssetKey] = useState<string | null>(null)
const [isSvgProcessing, setIsSvgProcessing] = useState(false)
const [selectedSvgOption, setSelectedSvgOption] = useState<SvgOption | null>(null)

const handleShowAndDownload = async () => {
if (!url.trim()) {
Expand Down Expand Up @@ -93,6 +152,42 @@ export default function ImagePreloadingScreen() {
}
}

const handleShowSvgWidget = async (option: SvgOption) => {
setIsSvgProcessing(true)

try {
const result = await preloadImages([
{
key: option.key,
svg: createTestSvg(option.color),
width: 48,
height: 48,
},
])

if (result.failed.length > 0) {
Alert.alert('SVG preload failed', result.failed.map((failure) => failure.error).join('\n'))
return
}

const variants = {
systemSmall: <SvgWidgetContent assetKey={option.key} color={option.color} />,
systemMedium: <SvgWidgetContent assetKey={option.key} color={option.color} />,
systemLarge: <SvgWidgetContent assetKey={option.key} color={option.color} />,
}

await updateWidget('weather', variants)
await reloadWidgets(['weather'])

setSelectedSvgOption(option)
Alert.alert('Success', 'SVG preloaded and the Weather widget was updated.')
} catch (error) {
Alert.alert('Error', `Failed to update SVG widget: ${error}`)
} finally {
setIsSvgProcessing(false)
}
}

return (
<ScreenLayout
title="Image Preloading"
Expand Down Expand Up @@ -125,6 +220,39 @@ export default function ImagePreloadingScreen() {
</View>
</Card>

<Card>
<Card.Title>SVG Widget Test</Card.Title>
<Card.Text>
Preload an inline SVG as a PNG asset and update the iOS Weather widget to render it with Voltra.Image.
</Card.Text>

<View style={styles.buttonRow}>
<Button
title={isSvgProcessing ? 'Updating...' : SVG_OPTIONS.green.title}
variant={selectedSvgOption?.key === SVG_OPTIONS.green.key ? 'primary' : 'secondary'}
style={selectedSvgOption?.key === SVG_OPTIONS.green.key ? styles.greenButtonSelected : undefined}
onPress={() => handleShowSvgWidget(SVG_OPTIONS.green)}
disabled={isSvgProcessing}
/>
<Button
title={SVG_OPTIONS.purple.title}
variant={selectedSvgOption?.key === SVG_OPTIONS.purple.key ? 'primary' : 'secondary'}
style={selectedSvgOption?.key === SVG_OPTIONS.purple.key ? styles.purpleButtonSelected : undefined}
onPress={() => handleShowSvgWidget(SVG_OPTIONS.purple)}
disabled={isSvgProcessing}
/>
</View>

{selectedSvgOption ? (
<View style={styles.previewContainer}>
<Text style={styles.inputLabel}>Preview</Text>
<VoltraWidgetPreview key={selectedSvgOption.key} family="systemMedium" style={styles.widgetPreview}>
<SvgWidgetContent assetKey={selectedSvgOption.key} color={selectedSvgOption.color} />
</VoltraWidgetPreview>
</View>
) : null}
</Card>

<View style={styles.footer}>
<Button title="Back to Testing Grounds" variant="ghost" onPress={() => router.back()} />
</View>
Expand All @@ -147,6 +275,19 @@ const styles = StyleSheet.create({
flexDirection: 'column',
gap: 12,
},
greenButtonSelected: {
backgroundColor: '#34C759',
},
purpleButtonSelected: {
backgroundColor: '#7C3AED',
},
previewContainer: {
marginTop: 16,
},
widgetPreview: {
borderRadius: 16,
overflow: 'hidden',
},
footer: {
marginTop: 24,
alignItems: 'center',
Expand Down
Loading