diff --git a/js/app/components/settings/branding-admin.tsx b/js/app/components/settings/branding-admin.tsx index 825619e3..6fce1b9f 100644 --- a/js/app/components/settings/branding-admin.tsx +++ b/js/app/components/settings/branding-admin.tsx @@ -10,6 +10,7 @@ import { import { useBrandingAsset, useFetchBranding, + useSidebarBackgroundImage, } from "@streamplace/components/src/streamplace-store/branding"; import { usePDSAgent } from "@streamplace/components/src/streamplace-store/xrpc"; import { useEffect, useState } from "react"; @@ -38,13 +39,17 @@ export function BrandingAdmin() { const currentDefaultStreamer = useBrandingAsset("defaultStreamer"); const currentLogo = useBrandingAsset("mainLogo"); const currentFavicon = useBrandingAsset("favicon"); + const currentSidebarBg = useSidebarBackgroundImage(); // load current branding on mount useEffect(() => { fetchBranding(); - setBroadcasterDID(currentBroadcasterDID || ""); }, []); + useEffect(() => { + setBroadcasterDID(currentBroadcasterDID || ""); + }, [currentBroadcasterDID]); + const uploadText = async (key: string, value: string) => { if (!agent) { toast.show("Not authenticated", "Please log in first", { @@ -118,11 +123,36 @@ export function BrandingAdmin() { const uint8Array = new Uint8Array(arrayBuffer); const base64Data = btoa(String.fromCharCode(...uint8Array)); + // detect image dimensions if it's an image + let width: number | undefined; + let height: number | undefined; + + if (file.type.startsWith("image/") && Platform.OS === "web") { + const img = new window.Image(); + const imageUrl = URL.createObjectURL(file); + + await new Promise((resolve, reject) => { + img.onload = () => { + width = img.naturalWidth; + height = img.naturalHeight; + URL.revokeObjectURL(imageUrl); + resolve(); + }; + img.onerror = () => { + URL.revokeObjectURL(imageUrl); + reject(new Error("Failed to load image")); + }; + img.src = imageUrl; + }); + } + await agent.place.stream.branding.updateBlob({ key, broadcaster: broadcasterDID || undefined, data: base64Data, mimeType: file.type, + width, + height, }); toast.show("Success", `${key} uploaded successfully`, { @@ -223,7 +253,7 @@ export function BrandingAdmin() { )} {/* Broadcaster DID */} - + Broadcaster DID @@ -238,14 +268,14 @@ export function BrandingAdmin() { {/* Site Title */} - + Site Title Current: {currentTitle?.data || "Streamplace"} - + {/* Site Description */} - + Site Description Current: {currentDescription?.data || "Live streaming platform"} - + {/* Primary Color */} - + Primary Color Current: {currentPrimaryColor?.data || "#6366f1"} - + {/* Accent Color */} - + Accent Color Current: {currentAccentColor?.data || "#8b5cf6"} - + {/* Default Streamer */} - + Default Streamer Current: {currentDefaultStreamer?.data || "None"} - + {/* Main Logo */} - + Main Logo - SVG, PNG, or JPEG (max 500KB) - Web only + SVG, PNG, or JPEG (max 500KB) {currentLogo?.data && ( )} - + + + + + - Note: You must be an authorized admin DID to make changes. {Platform.OS !== "web" && - " Image uploads are only available on web."} + "Image uploads are only available on web."} diff --git a/js/app/components/sidebar/sidebar.tsx b/js/app/components/sidebar/sidebar.tsx index 3e40d5a1..71c81bb4 100644 --- a/js/app/components/sidebar/sidebar.tsx +++ b/js/app/components/sidebar/sidebar.tsx @@ -6,7 +6,13 @@ import { ParamListBase, useNavigation, } from "@react-navigation/native"; -import { Text, useMainLogo, useSiteTitle, zero } from "@streamplace/components"; +import { + Text, + useMainLogo, + useSidebarBackgroundImage, + useSiteTitle, + zero, +} from "@streamplace/components"; import { useAQLinkHref } from "components/aqlink"; import React from "react"; import { Image, Platform, Pressable, View } from "react-native"; @@ -48,6 +54,7 @@ export default function Sidebar({ const navigation = useNavigation(); const siteTitle = useSiteTitle(); const mainLogo = useMainLogo(); + const sidebarBackgroundImageAsset = useSidebarBackgroundImage(); const animatedSidebarStyle = useAnimatedStyle(() => { return { @@ -73,9 +80,31 @@ export default function Sidebar({ animatedSidebarStyle, zero.p[2], zero.gap.all[2], + zero.flex.values[1], zero.layout.flex.column, + { position: "relative" }, ]} > + {sidebarBackgroundImageAsset?.data && ( + + )} on web! href={route ? href : undefined} diff --git a/js/app/src/screens/mobile-stream.tsx b/js/app/src/screens/mobile-stream.tsx index b9fd4f53..e41a066b 100644 --- a/js/app/src/screens/mobile-stream.tsx +++ b/js/app/src/screens/mobile-stream.tsx @@ -64,7 +64,6 @@ export default function MobileStream({ route }) { const defaultStreamer = useDefaultStreamer(); - if (!user) user = defaultStreamer; let extraProps: Partial = {}; if (isWeb) { diff --git a/js/components/src/streamplace-store/branding.tsx b/js/components/src/streamplace-store/branding.tsx index 6e081105..5e477819 100644 --- a/js/components/src/streamplace-store/branding.tsx +++ b/js/components/src/streamplace-store/branding.tsx @@ -11,6 +11,8 @@ export interface BrandingAsset { mimeType: string; url?: string; // URL for images data?: string; // inline data for text, or base64 for images + width?: number; // image width in pixels + height?: number; // image height in pixels } // helper to convert blob to base64 @@ -183,6 +185,11 @@ export function useDefaultStreamer(): string | undefined { return asset?.data || undefined; } +// convenience hook for sidebar background image +export function useSidebarBackgroundImage(): BrandingAsset | undefined { + return useBrandingAsset("sidebarBackgroundImage"); +} + // hook to auto-fetch branding when broadcaster changes export function useBrandingAutoFetch() { const fetchBranding = useFetchBranding(); diff --git a/js/docs/src/content/docs/lex-reference/branding/place-stream-branding-deleteblob.md b/js/docs/src/content/docs/lex-reference/branding/place-stream-branding-deleteblob.md new file mode 100644 index 00000000..98ab727a --- /dev/null +++ b/js/docs/src/content/docs/lex-reference/branding/place-stream-branding-deleteblob.md @@ -0,0 +1,103 @@ +--- +title: place.stream.branding.deleteBlob +description: Reference for the place.stream.branding.deleteBlob lexicon +--- + +**Lexicon Version:** 1 + +## Definitions + + + +### `main` + +**Type:** `procedure` + +Delete a branding asset blob. Requires admin authorization. + +**Parameters:** _(None defined)_ + +**Input:** + +- **Encoding:** `application/json` +- **Schema:** + +**Schema Type:** `object` + +| Name | Type | Req'd | Description | Constraints | +| ------------- | -------- | ----- | ------------------------------------------------------------------------------- | ------------- | +| `key` | `string` | ✅ | Branding asset key (mainLogo, favicon, siteTitle, etc.) | | +| `broadcaster` | `string` | ❌ | DID of the broadcaster. If not provided, uses the server's default broadcaster. | Format: `did` | + +**Output:** + +- **Encoding:** `application/json` +- **Schema:** + +**Schema Type:** `object` + +| Name | Type | Req'd | Description | Constraints | +| --------- | --------- | ----- | ----------- | ----------- | +| `success` | `boolean` | ✅ | | | + +**Possible Errors:** + +- `Unauthorized`: The authenticated DID is not authorized to modify branding +- `BrandingNotFound`: The requested branding asset does not exist + +--- + +## Lexicon Source + +```json +{ + "lexicon": 1, + "id": "place.stream.branding.deleteBlob", + "defs": { + "main": { + "type": "procedure", + "description": "Delete a branding asset blob. Requires admin authorization.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["key"], + "properties": { + "key": { + "type": "string", + "description": "Branding asset key (mainLogo, favicon, siteTitle, etc.)" + }, + "broadcaster": { + "type": "string", + "format": "did", + "description": "DID of the broadcaster. If not provided, uses the server's default broadcaster." + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["success"], + "properties": { + "success": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "Unauthorized", + "description": "The authenticated DID is not authorized to modify branding" + }, + { + "name": "BrandingNotFound", + "description": "The requested branding asset does not exist" + } + ] + } + } +} +``` diff --git a/js/docs/src/content/docs/lex-reference/branding/place-stream-branding-getbranding.md b/js/docs/src/content/docs/lex-reference/branding/place-stream-branding-getbranding.md index 2007f951..365af05d 100644 --- a/js/docs/src/content/docs/lex-reference/branding/place-stream-branding-getbranding.md +++ b/js/docs/src/content/docs/lex-reference/branding/place-stream-branding-getbranding.md @@ -42,12 +42,14 @@ Get all branding configuration for the broadcaster. **Properties:** -| Name | Type | Req'd | Description | Constraints | -| ---------- | -------- | ----- | ---------------------------------------- | ----------- | -| `key` | `string` | ✅ | Asset key identifier | | -| `mimeType` | `string` | ✅ | MIME type of the asset | | -| `url` | `string` | ❌ | URL to fetch the asset blob (for images) | | -| `data` | `string` | ❌ | Inline data for text assets | | +| Name | Type | Req'd | Description | Constraints | +| ---------- | --------- | ----- | -------------------------------------------------- | ----------- | +| `key` | `string` | ✅ | Asset key identifier | | +| `mimeType` | `string` | ✅ | MIME type of the asset | | +| `url` | `string` | ❌ | URL to fetch the asset blob (for images) | | +| `data` | `string` | ❌ | Inline data for text assets | | +| `width` | `integer` | ❌ | Image width in pixels (optional, for images only) | | +| `height` | `integer` | ❌ | Image height in pixels (optional, for images only) | | --- @@ -110,6 +112,14 @@ Get all branding configuration for the broadcaster. "data": { "type": "string", "description": "Inline data for text assets" + }, + "width": { + "type": "integer", + "description": "Image width in pixels (optional, for images only)" + }, + "height": { + "type": "integer", + "description": "Image height in pixels (optional, for images only)" } } } diff --git a/js/docs/src/content/docs/lex-reference/branding/place-stream-branding-updateblob.md b/js/docs/src/content/docs/lex-reference/branding/place-stream-branding-updateblob.md new file mode 100644 index 00000000..92b651b8 --- /dev/null +++ b/js/docs/src/content/docs/lex-reference/branding/place-stream-branding-updateblob.md @@ -0,0 +1,123 @@ +--- +title: place.stream.branding.updateBlob +description: Reference for the place.stream.branding.updateBlob lexicon +--- + +**Lexicon Version:** 1 + +## Definitions + + + +### `main` + +**Type:** `procedure` + +Update or create a branding asset blob. Requires admin authorization. + +**Parameters:** _(None defined)_ + +**Input:** + +- **Encoding:** `application/json` +- **Schema:** + +**Schema Type:** `object` + +| Name | Type | Req'd | Description | Constraints | +| ------------- | --------- | ----- | ------------------------------------------------------------------------------- | ------------- | +| `key` | `string` | ✅ | Branding asset key (mainLogo, favicon, siteTitle, etc.) | | +| `broadcaster` | `string` | ❌ | DID of the broadcaster. If not provided, uses the server's default broadcaster. | Format: `did` | +| `data` | `string` | ✅ | Base64-encoded blob data | | +| `mimeType` | `string` | ✅ | MIME type of the blob (e.g., image/png, text/plain) | | +| `width` | `integer` | ❌ | Image width in pixels (optional, for images only) | | +| `height` | `integer` | ❌ | Image height in pixels (optional, for images only) | | + +**Output:** + +- **Encoding:** `application/json` +- **Schema:** + +**Schema Type:** `object` + +| Name | Type | Req'd | Description | Constraints | +| --------- | --------- | ----- | ----------- | ----------- | +| `success` | `boolean` | ✅ | | | + +**Possible Errors:** + +- `Unauthorized`: The authenticated DID is not authorized to modify branding +- `BlobTooLarge`: The blob exceeds the maximum size limit + +--- + +## Lexicon Source + +```json +{ + "lexicon": 1, + "id": "place.stream.branding.updateBlob", + "defs": { + "main": { + "type": "procedure", + "description": "Update or create a branding asset blob. Requires admin authorization.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["key", "data", "mimeType"], + "properties": { + "key": { + "type": "string", + "description": "Branding asset key (mainLogo, favicon, siteTitle, etc.)" + }, + "broadcaster": { + "type": "string", + "format": "did", + "description": "DID of the broadcaster. If not provided, uses the server's default broadcaster." + }, + "data": { + "type": "string", + "description": "Base64-encoded blob data" + }, + "mimeType": { + "type": "string", + "description": "MIME type of the blob (e.g., image/png, text/plain)" + }, + "width": { + "type": "integer", + "description": "Image width in pixels (optional, for images only)" + }, + "height": { + "type": "integer", + "description": "Image height in pixels (optional, for images only)" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["success"], + "properties": { + "success": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "Unauthorized", + "description": "The authenticated DID is not authorized to modify branding" + }, + { + "name": "BlobTooLarge", + "description": "The blob exceeds the maximum size limit" + } + ] + } + } +} +``` diff --git a/js/docs/src/content/docs/lex-reference/openapi.json b/js/docs/src/content/docs/lex-reference/openapi.json index e76582e0..ad8ad277 100644 --- a/js/docs/src/content/docs/lex-reference/openapi.json +++ b/js/docs/src/content/docs/lex-reference/openapi.json @@ -891,6 +891,80 @@ } } }, + "/xrpc/place.stream.branding.deleteBlob": { + "post": { + "summary": "Delete a branding asset blob. Requires admin authorization.", + "operationId": "place.stream.branding.deleteBlob", + "tags": ["place.stream.branding"], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"] + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["error", "message"], + "properties": { + "error": { + "type": "string", + "oneOf": [ + { + "const": "Unauthorized" + }, + { + "const": "BrandingNotFound" + } + ] + }, + "message": { + "type": "string" + } + } + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Branding asset key (mainLogo, favicon, siteTitle, etc.)" + }, + "broadcaster": { + "type": "string", + "description": "DID of the broadcaster. If not provided, uses the server's default broadcaster.", + "format": "did" + } + }, + "required": ["key"] + } + } + } + } + } + }, "/xrpc/place.stream.branding.getBlob": { "get": { "summary": "Get a specific branding asset blob by key.", @@ -997,6 +1071,96 @@ ] } }, + "/xrpc/place.stream.branding.updateBlob": { + "post": { + "summary": "Update or create a branding asset blob. Requires admin authorization.", + "operationId": "place.stream.branding.updateBlob", + "tags": ["place.stream.branding"], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"] + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["error", "message"], + "properties": { + "error": { + "type": "string", + "oneOf": [ + { + "const": "Unauthorized" + }, + { + "const": "BlobTooLarge" + } + ] + }, + "message": { + "type": "string" + } + } + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Branding asset key (mainLogo, favicon, siteTitle, etc.)" + }, + "broadcaster": { + "type": "string", + "description": "DID of the broadcaster. If not provided, uses the server's default broadcaster.", + "format": "did" + }, + "data": { + "type": "string", + "description": "Base64-encoded blob data" + }, + "mimeType": { + "type": "string", + "description": "MIME type of the blob (e.g., image/png, text/plain)" + }, + "width": { + "type": "integer", + "description": "Image width in pixels (optional, for images only)" + }, + "height": { + "type": "integer", + "description": "Image height in pixels (optional, for images only)" + } + }, + "required": ["key", "data", "mimeType"] + } + } + } + } + } + }, "/xrpc/com.atproto.sync.getRecord": { "get": { "summary": "Get data blocks needed to prove the existence or non-existence of record in the current version of repo. Does not require auth.", @@ -2432,6 +2596,14 @@ "data": { "type": "string", "description": "Inline data for text assets" + }, + "width": { + "type": "integer", + "description": "Image width in pixels (optional, for images only)" + }, + "height": { + "type": "integer", + "description": "Image height in pixels (optional, for images only)" } }, "required": ["key", "mimeType"] diff --git a/lexicons/place/stream/branding/getBranding.json b/lexicons/place/stream/branding/getBranding.json index 07e0a8f0..983cbde7 100644 --- a/lexicons/place/stream/branding/getBranding.json +++ b/lexicons/place/stream/branding/getBranding.json @@ -54,6 +54,14 @@ "data": { "type": "string", "description": "Inline data for text assets" + }, + "width": { + "type": "integer", + "description": "Image width in pixels (optional, for images only)" + }, + "height": { + "type": "integer", + "description": "Image height in pixels (optional, for images only)" } } } diff --git a/lexicons/place/stream/branding/updateBlob.json b/lexicons/place/stream/branding/updateBlob.json index f08a1c89..ed5fb96b 100644 --- a/lexicons/place/stream/branding/updateBlob.json +++ b/lexicons/place/stream/branding/updateBlob.json @@ -27,6 +27,14 @@ "mimeType": { "type": "string", "description": "MIME type of the blob (e.g., image/png, text/plain)" + }, + "width": { + "type": "integer", + "description": "Image width in pixels (optional, for images only)" + }, + "height": { + "type": "integer", + "description": "Image height in pixels (optional, for images only)" } } } diff --git a/pkg/spxrpc/place_stream_branding.go b/pkg/spxrpc/place_stream_branding.go index 1161a222..04c86e77 100644 --- a/pkg/spxrpc/place_stream_branding.go +++ b/pkg/spxrpc/place_stream_branding.go @@ -45,16 +45,18 @@ func (s *Server) getBroadcasterID(ctx context.Context, broadcasterDID string) st return s.cli.BroadcasterHost } -func (s *Server) getBrandingBlobCached(ctx context.Context, broadcasterID, key string) ([]byte, string, error) { +func (s *Server) getBrandingBlobCached(ctx context.Context, broadcasterID, key string) ([]byte, string, *int, *int, error) { cacheKey := fmt.Sprintf("%s:%s", broadcasterID, key) // check cache first if cached, found := s.BrandingCache.Get(cacheKey); found { blob := cached.(struct { - data []byte - mime string + data []byte + mime string + width *int + height *int }) - return blob.data, blob.mime, nil + return blob.data, blob.mime, blob.width, blob.height, nil } // cache miss - fetch from db @@ -63,23 +65,31 @@ func (s *Server) getBrandingBlobCached(ctx context.Context, broadcasterID, key s // not in db, use default if def, ok := defaultBrandingAssets[key]; ok { // cache the default too - s.BrandingCache.Set(cacheKey, def, cache.DefaultExpiration) - return def.data, def.mime, nil + cacheData := struct { + data []byte + mime string + width *int + height *int + }{data: def.data, mime: def.mime, width: nil, height: nil} + s.BrandingCache.Set(cacheKey, cacheData, cache.DefaultExpiration) + return def.data, def.mime, nil, nil, nil } - return nil, "", fmt.Errorf("unknown branding key: %s", key) + return nil, "", nil, nil, fmt.Errorf("unknown branding key: %s", key) } if err != nil { - return nil, "", fmt.Errorf("error fetching branding blob: %w", err) + return nil, "", nil, nil, fmt.Errorf("error fetching branding blob: %w", err) } // store in cache cacheData := struct { - data []byte - mime string - }{data: blob.Data, mime: blob.MimeType} + data []byte + mime string + width *int + height *int + }{data: blob.Data, mime: blob.MimeType, width: blob.Width, height: blob.Height} s.BrandingCache.Set(cacheKey, cacheData, cache.DefaultExpiration) - return blob.Data, blob.MimeType, nil + return blob.Data, blob.MimeType, blob.Width, blob.Height, nil } func (s *Server) handlePlaceStreamBrandingGetBlob(ctx context.Context, broadcasterDID string, key string) (io.Reader, error) { @@ -89,7 +99,7 @@ func (s *Server) handlePlaceStreamBrandingGetBlob(ctx context.Context, broadcast // HandlePlaceStreamBrandingGetBlobDirect is the exported version for direct calls func (s *Server) HandlePlaceStreamBrandingGetBlobDirect(ctx context.Context, broadcasterDID string, key string) (io.Reader, error) { broadcasterID := s.getBroadcasterID(ctx, broadcasterDID) - data, _, err := s.getBrandingBlobCached(ctx, broadcasterID, key) + data, _, _, _, err := s.getBrandingBlobCached(ctx, broadcasterID, key) if err != nil { return nil, err } @@ -122,7 +132,7 @@ func (s *Server) HandlePlaceStreamBrandingGetBrandingDirect(ctx context.Context, // build output assets := make([]*placestreamtypes.BrandingGetBranding_BrandingAsset, 0, len(allKeys)) for key := range allKeys { - data, mimeType, err := s.getBrandingBlobCached(ctx, broadcasterID, key) + data, mimeType, width, height, err := s.getBrandingBlobCached(ctx, broadcasterID, key) if err != nil { continue // skip if error } @@ -132,6 +142,16 @@ func (s *Server) HandlePlaceStreamBrandingGetBrandingDirect(ctx context.Context, MimeType: mimeType, } + // add dimensions if available + if width != nil { + w := int64(*width) + asset.Width = &w + } + if height != nil { + h := int64(*height) + asset.Height = &h + } + // for text assets, include data inline; for images, provide URL if mimeType == "text/plain" { str := string(data) @@ -190,12 +210,23 @@ func (s *Server) handlePlaceStreamBrandingUpdateBlob(ctx context.Context, input } else if input.Key == "siteTitle" || input.Key == "siteDescription" || input.Key == "primaryColor" || input.Key == "accentColor" || input.Key == "defaultStreamKey" || input.Key == "defaultStreamer" { maxSize = 1024 // 1KB for text values } + // sidebarBackgroundImage uses default 500KB limit if len(data) > maxSize { return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("blob too large (max %d bytes)", maxSize)) } // store in database - err = s.statefulDB.PutBrandingBlob(broadcasterID, input.Key, input.MimeType, data) + var width, height *int + if input.Width != nil { + w := int(*input.Width) + width = &w + } + if input.Height != nil { + h := int(*input.Height) + height = &h + } + + err = s.statefulDB.PutBrandingBlob(broadcasterID, input.Key, input.MimeType, data, width, height) if err != nil { log.Error(ctx, "failed to store branding blob", "err", err) return nil, echo.NewHTTPError(http.StatusInternalServerError, "unable to store branding blob") diff --git a/pkg/statedb/branding.go b/pkg/statedb/branding.go index 8751285f..b24303e5 100644 --- a/pkg/statedb/branding.go +++ b/pkg/statedb/branding.go @@ -13,6 +13,8 @@ type BrandingBlob struct { Key string `gorm:"index:idx_broadcaster_key,priority:2,unique"` // "mainLogo", "favicon", "siteTitle", etc. MimeType string // "image/svg+xml", "image/png", "text/plain" Data []byte `gorm:"type:bytea"` // actual blob data + Width *int // image width in pixels (nullable) + Height *int // image height in pixels (nullable) } // GetBrandingBlob fetches a single branding asset @@ -26,7 +28,7 @@ func (state *StatefulDB) GetBrandingBlob(broadcasterID, key string) (*BrandingBl } // PutBrandingBlob stores or updates a branding asset -func (state *StatefulDB) PutBrandingBlob(broadcasterID, key, mimeType string, data []byte) error { +func (state *StatefulDB) PutBrandingBlob(broadcasterID, key, mimeType string, data []byte, width, height *int) error { // try to find existing blob (including soft-deleted ones) var existing BrandingBlob err := state.DB.Unscoped().Where("broadcaster_id = ? AND key = ?", broadcasterID, key).First(&existing).Error @@ -38,6 +40,8 @@ func (state *StatefulDB) PutBrandingBlob(broadcasterID, key, mimeType string, da Key: key, MimeType: mimeType, Data: data, + Width: width, + Height: height, } if err := state.DB.Create(&blob).Error; err != nil { return fmt.Errorf("error creating branding blob: %w", err) @@ -50,6 +54,8 @@ func (state *StatefulDB) PutBrandingBlob(broadcasterID, key, mimeType string, da // update existing blob (restore if soft-deleted) existing.MimeType = mimeType existing.Data = data + existing.Width = width + existing.Height = height existing.DeletedAt = gorm.DeletedAt{} // clear soft delete if err := state.DB.Unscoped().Save(&existing).Error; err != nil { return fmt.Errorf("error updating branding blob: %w", err) diff --git a/pkg/streamplace/brandinggetBranding.go b/pkg/streamplace/brandinggetBranding.go index 0fca4d3d..44518aca 100644 --- a/pkg/streamplace/brandinggetBranding.go +++ b/pkg/streamplace/brandinggetBranding.go @@ -14,12 +14,16 @@ import ( type BrandingGetBranding_BrandingAsset struct { // data: Inline data for text assets Data *string `json:"data,omitempty" cborgen:"data,omitempty"` + // height: Image height in pixels (optional, for images only) + Height *int64 `json:"height,omitempty" cborgen:"height,omitempty"` // key: Asset key identifier Key string `json:"key" cborgen:"key"` // mimeType: MIME type of the asset MimeType string `json:"mimeType" cborgen:"mimeType"` // url: URL to fetch the asset blob (for images) Url *string `json:"url,omitempty" cborgen:"url,omitempty"` + // width: Image width in pixels (optional, for images only) + Width *int64 `json:"width,omitempty" cborgen:"width,omitempty"` } // BrandingGetBranding_Output is the output of a place.stream.branding.getBranding call. diff --git a/pkg/streamplace/brandingupdateBlob.go b/pkg/streamplace/brandingupdateBlob.go index db8d1d7c..3041d24c 100644 --- a/pkg/streamplace/brandingupdateBlob.go +++ b/pkg/streamplace/brandingupdateBlob.go @@ -16,10 +16,14 @@ type BrandingUpdateBlob_Input struct { Broadcaster *string `json:"broadcaster,omitempty" cborgen:"broadcaster,omitempty"` // data: Base64-encoded blob data Data string `json:"data" cborgen:"data"` + // height: Image height in pixels (optional, for images only) + Height *int64 `json:"height,omitempty" cborgen:"height,omitempty"` // key: Branding asset key (mainLogo, favicon, siteTitle, etc.) Key string `json:"key" cborgen:"key"` // mimeType: MIME type of the blob (e.g., image/png, text/plain) MimeType string `json:"mimeType" cborgen:"mimeType"` + // width: Image width in pixels (optional, for images only) + Width *int64 `json:"width,omitempty" cborgen:"width,omitempty"` } // BrandingUpdateBlob_Output is the output of a place.stream.branding.updateBlob call.