diff --git a/example/app.json b/example/app.json index f0a0c578..9afa7709 100644 --- a/example/app.json +++ b/example/app.json @@ -128,6 +128,21 @@ "intervalMinutes": 15, "refresh": true } + }, + { + "id": "material_colors", + "displayName": "Material Colors Widget", + "description": "Compare client-side and server-side rendering with Android dynamic colors", + "targetCellWidth": 2, + "targetCellHeight": 2, + "resizeMode": "horizontal|vertical", + "widgetCategory": "home_screen", + "initialStatePath": "./widgets/android/android-material-colors-initial.tsx", + "serverUpdate": { + "url": "http://10.0.2.2:3333", + "intervalMinutes": 15, + "refresh": true + } } ] }, diff --git a/example/app/android-widgets/material-colors.tsx b/example/app/android-widgets/material-colors.tsx new file mode 100644 index 00000000..d5f2d285 --- /dev/null +++ b/example/app/android-widgets/material-colors.tsx @@ -0,0 +1,5 @@ +import AndroidMaterialColorsScreen from '~/screens/android/AndroidMaterialColorsScreen' + +export default function AndroidMaterialColorsRoute() { + return +} diff --git a/example/screens/android/AndroidMaterialColorsScreen.tsx b/example/screens/android/AndroidMaterialColorsScreen.tsx new file mode 100644 index 00000000..dd254487 --- /dev/null +++ b/example/screens/android/AndroidMaterialColorsScreen.tsx @@ -0,0 +1,278 @@ +import { useRouter } from 'expo-router' +import React, { useState } from 'react' +import { Alert, Platform, ScrollView, StyleSheet, Text, View } from 'react-native' +import { + reloadAndroidWidgets, + requestPinAndroidWidget, + setWidgetServerCredentials, + updateAndroidWidget, + VoltraWidgetPreview, +} from 'voltra/android/client' + +import { Button } from '~/components/Button' +import { Card } from '~/components/Card' +import { + AndroidMaterialColorsWidget, + type AndroidMaterialColorsRenderSource, +} from '~/widgets/android/AndroidMaterialColorsWidget' + +const WIDGET_ID = 'material_colors' +const DEMO_TOKEN = 'demo-token' + +const formatRenderTime = () => + new Date().toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }) + +export default function AndroidMaterialColorsScreen() { + const router = useRouter() + const [isPinning, setIsPinning] = useState(false) + const [isRenderingClient, setIsRenderingClient] = useState(false) + const [isRenderingServer, setIsRenderingServer] = useState(false) + const [previewSource, setPreviewSource] = useState('initial') + const [previewTimestamp, setPreviewTimestamp] = useState('waiting for render') + + const handlePinWidget = async () => { + if (Platform.OS !== 'android') { + Alert.alert('Not Available', 'This widget demo is only available on Android devices.') + return + } + + setIsPinning(true) + try { + const success = await requestPinAndroidWidget(WIDGET_ID, { + previewWidth: 220, + previewHeight: 220, + }) + + if (success) { + Alert.alert('Pin requested', 'Add the widget on your home screen, then use the render buttons below.') + } else { + Alert.alert('Not supported', 'Widget pinning is not available on this device.') + } + } catch (error: any) { + const message = error?.message || String(error) + Alert.alert('Error', `Failed to pin widget: ${message}`) + } finally { + setIsPinning(false) + } + } + + const handleRenderOnClient = async () => { + if (Platform.OS !== 'android') { + Alert.alert('Not Available', 'Client-side widget rendering is only available on Android devices.') + return + } + + setIsRenderingClient(true) + try { + const renderedAt = formatRenderTime() + + await updateAndroidWidget(WIDGET_ID, [ + { + size: { width: 200, height: 200 }, + content: , + }, + { + size: { width: 300, height: 200 }, + content: , + }, + ]) + + setPreviewSource('client') + setPreviewTimestamp(renderedAt) + Alert.alert( + 'Client render complete', + 'The widget JSON was rendered inside the app and pushed straight to Android.' + ) + } catch (error: any) { + const message = error?.message || String(error) + Alert.alert('Error', `Failed to render on client: ${message}`) + } finally { + setIsRenderingClient(false) + } + } + + const handleRenderOnServer = async () => { + if (Platform.OS !== 'android') { + Alert.alert('Not Available', 'Server-side widget rendering is only available on Android devices.') + return + } + + setIsRenderingServer(true) + try { + await setWidgetServerCredentials({ + token: DEMO_TOKEN, + headers: { + 'X-Widget-Source': 'voltra-example', + }, + }) + + await reloadAndroidWidgets([WIDGET_ID]) + + setPreviewSource('server') + setPreviewTimestamp('server timestamp') + Alert.alert( + 'Server render requested', + 'The widget will fetch fresh JSON from the example server. Make sure `npm run widget:server --workspace voltra-example` is running on your host machine.' + ) + } catch (error: any) { + const message = error?.message || String(error) + Alert.alert('Error', `Failed to render on server: ${message}`) + } finally { + setIsRenderingServer(false) + } + } + + return ( + + + Material Colors Widget + + Test the same Android widget through both render paths. It uses Android semantic color tokens, so both + client-side and server-side rendering resolve native Material You colors directly inside Glance. + + + + 1. Pin the Widget + + Add the widget to your home screen once, then switch between client-side and server-side renders. + + +