Custom Templates Guide ====================== Mycelian allows you to create custom HTML templates that integrate with your streaming services through WebSocket communication. This guide covers template creation, data integration, and advanced features. Template System Overview ------------------------- Template Architecture ~~~~~~~~~~~~~~~~~~~~~ * **HTML Templates:** Stored in ``templates/`` directory * **Template Configs:** JSON configuration files in ``templates/template_configs/`` * **Assets:** Static files (images, sounds, fonts) in ``assets/`` directory * **WebSocket Communication:** Real-time data via Socket.IO on ``http://localhost:5000`` * **Auto-Registration:** Templates automatically become available as routes File Structure ~~~~~~~~~~~~~~ .. code-block:: templates/ ├── my_custom_template.html # Your HTML template ├── template_configs/ │ └── my_custom_template.json # Configuration file └── assets/ ├── my_custom_template/ # Template-specific assets │ ├── images/ │ ├── sounds/ │ └── styles/ └── default_assets/ # Shared assets ├── fonts/ ├── images/ └── sounds/ Creating Your First Template ----------------------------- Basic HTML Template ~~~~~~~~~~~~~~~~~~~ Create ``templates/my_alerts.html``: .. code-block:: html My Custom Alerts
Creating JSON Configuration Files ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Template configuration files define the customizable settings for your templates. They are stored in ``templates/template_configs/`` and follow a specific structure. **Basic Structure:** .. code-block:: json { "template_name": "my_template", "elements": [ { "type": "separator", "label": "Section Title" }, { "type": "text", "id": "setting_id", "label": "Setting Display Name", "value": "default_value", "description": "Description of what this setting does" } ] } **Available Element Types:** **Text Input:** .. code-block:: json { "type": "text", "id": "custom_text", "label": "Custom Text", "value": "Default text here", "description": "Enter custom text to display" } **Number Input:** .. code-block:: json { "type": "number", "id": "animation_duration", "label": "Animation Duration", "value": 5, "min": 1, "max": 30, "step": 0.5, "description": "Duration in seconds" } **Slider Input:** .. code-block:: json { "type": "slider", "id": "opacity_level", "label": "Opacity Level", "value": 80, "min": 0, "max": 100, "step": 5, "description": "Opacity percentage" } **Checkbox (Boolean):** .. code-block:: json { "type": "checkbox", "id": "enable_feature", "label": "Enable Feature", "value": true, "description": "Check to enable this feature" } **Toggle (Alternative Boolean):** .. code-block:: json { "type": "toggle", "id": "compact_mode", "label": "Compact Mode", "value": false, "description": "Use compact layout" } **Color Selector:** .. code-block:: json { "type": "select", "id": "background_color", "label": "Background Color", "value": "#7c3aed", "description": "Choose background color", "display": "color_grid", "transparent": true, "options": [ "#e11d48", "#dc2626", "#ea580c", "#d97706", "#ca8a04", "#65a30d", "#16a34a", "#059669", "#0891b2", "#0284c7", "#2563eb", "#4f46e5", "#7c3aed", "#9333ea", "#c026d3", "#db2777", "#ffffff", "#f8fafc", "#f1f5f9", "#e2e8f0", "#cbd5e1", "#94a3b8", "#64748b", "#475569", "#334155", "#1e293b", "#0f172a", "#1a1a1a", "#000000", "transparent" ] } **Dropdown Select:** .. code-block:: json { "type": "select", "id": "font_weight", "label": "Font Weight", "value": "bold", "description": "Choose font weight", "options": [ "normal", "bold", "bolder", "lighter", "100", "200", "300", "400", "500", "600", "700", "800", "900" ] } **Section Separator:** .. code-block:: json { "type": "separator", "label": "Visual Settings" } **Complete Example Configuration:** .. code-block:: json { "template_name": "my_alerts", "elements": [ { "type": "separator", "label": "General Settings" }, { "type": "checkbox", "id": "enabled", "label": "Enable Template", "value": true, "description": "Enable or disable this template entirely" }, { "type": "text", "id": "alert_title", "label": "Alert Title", "value": "New Alert!", "description": "Text shown as the alert title" }, { "type": "separator", "label": "Visual Settings" }, { "type": "select", "id": "title_color", "label": "Title Color", "value": "#ffffff", "description": "Color of the alert title text", "display": "color_grid", "transparent": false, "options": [ "#ffffff", "#000000", "#e11d48", "#dc2626", "#ea580c", "#d97706", "#ca8a04", "#65a30d", "#16a34a", "#059669", "#0891b2", "#0284c7", "#2563eb", "#4f46e5", "#7c3aed", "#9333ea", "#c026d3", "#db2777" ] }, { "type": "number", "id": "font_size", "label": "Font Size", "value": 24, "min": 12, "max": 48, "step": 2, "description": "Font size in pixels" }, { "type": "slider", "id": "opacity", "label": "Background Opacity", "value": 80, "min": 0, "max": 100, "step": 5, "description": "Background opacity percentage" }, { "type": "separator", "label": "Animation Settings" }, { "type": "number", "id": "alert_duration", "label": "Alert Duration", "value": 5.0, "min": 1.0, "max": 30.0, "step": 0.5, "description": "How long alerts are displayed in seconds" }, { "type": "select", "id": "animation_easing", "label": "Animation Easing", "value": "ease-out", "description": "Easing function for animations", "options": [ "linear", "ease", "ease-in", "ease-out", "ease-in-out" ] }, { "type": "toggle", "id": "enable_sound", "label": "Enable Sound Effects", "value": true, "description": "Play sound effects with alerts" } ] } **Element Properties Reference:** **Common Properties (all elements):** - ``type``: Element type (required) - ``id``: Unique identifier for accessing the value (required) - ``label``: Display name in the UI (required) - ``description``: Help text explaining the setting (optional) **Value Properties:** - ``value``: Default value (required for most types) **Number/Slider Properties:** - ``min``: Minimum allowed value - ``max``: Maximum allowed value - ``step``: Increment step size **Select Properties:** - ``options``: Array of available choices (required) - ``display``: Display style (``"color_grid"`` for color pickers) - ``transparent``: Whether to include transparent option for colors **Best Practices:** 1. **Use descriptive IDs:** Make IDs clear and consistent (e.g., ``title_color``, ``animation_duration``) 2. **Group related settings:** Use separators to organize settings logically 3. **Provide helpful descriptions:** Explain what each setting does and its effects 4. **Set sensible defaults:** Choose default values that work well out of the box 5. **Use appropriate ranges:** Set reasonable min/max values for numbers and sliders 6. **Color grid organization:** Order colors logically (light to dark, or by hue) 7. **Consistent naming:** Use consistent naming patterns across your configuration **Template Configuration File Example:** Save as ``templates/template_configs/my_alerts.json``: WebSocket Integration --------------------- Connection Setup ~~~~~~~~~~~~~~~~ All templates must connect to the WebSocket server: .. code-block:: javascript // Connect to Socket.IO server const socket = io(); // Handle connection events socket.on('connect', function() { console.log('Connected to Mycelian'); }); socket.on('disconnect', function() { console.log('Disconnected from Mycelian'); }); Alert System Integration ~~~~~~~~~~~~~~~~~~~~~~~~ **Receiving Alerts:** .. code-block:: javascript // Listen for new alerts socket.on('next_alert', function(alertData) { console.log('New alert:', alertData); // Alert data structure: // { // alert_type: "follow|sub|bits|donation|raid|points", // username: "viewer_name", // message: "alert message", // duration: 5, // volume: 100, // tier: 1, // For subs (1, 2, 3) // amt_cheered: 100, // For bits // donation_amount: 10.00, // For donations // raider_count: 25, // For raids // point_cost: 500, // For channel points // gif_dir: "assets/alerts/", // GIF directory // gif_name: "follow.gif", // GIF filename // single_audio_dir: "assets/sounds/", // single_audio_name: "alert.mp3", // user_message: "Thanks for the follow!" // User's message // } displayAlert(alertData); }); // Notify when alert is complete function alertComplete() { socket.emit('alert_complete'); } **Pause/Resume Status:** .. code-block:: javascript // Listen for pause status changes socket.on('pause_status_update', function(data) { if (data.paused) { console.log('Alerts are paused'); // Hide alert interface or show paused state } else { console.log('Alerts are resumed'); // Show alert interface } }); // Request current pause status socket.emit('get_pause_status'); Service Data Integration ------------------------ Twitch Integration ~~~~~~~~~~~~~~~~~~ **Getting Streamer Information:** .. code-block:: javascript // Request streamer info socket.emit('get-streamer-info'); socket.on('streamer-info', function(data) { console.log('Streamer data:', data); // Data structure: // { // user_id: "123456789", // streamer_name: "MyStreamer", // current_category: "Just Chatting" // } }); **Twitch API Requests:** .. code-block:: javascript // Make Twitch API requests through proxy socket.emit('twitch-api-request', { endpoint: 'https://api.twitch.tv/helix/chat/emotes', method: 'GET', requestId: 'emotes_request_1' }); socket.on('twitch-api-response', function(data) { if (data.requestId === 'emotes_request_1' && data.success) { console.log('Emotes data:', data.data); } }); **Chat Integration:** .. code-block:: javascript // Listen for new chat messages socket.on('new-message', function(messageData) { console.log('New chat message:', messageData); // Message data structure: // { // username: "viewer123", // message: "Hello stream!", // badges: ["subscriber", "vip"], // emotes: { /* emote data */ }, // timestamp: "2024-01-01T12:00:00Z" // } }); // Listen for chat clear events socket.on('clear-chat', function() { // Clear all displayed messages }); socket.on('remove-messages', function(messageIds) { // Remove specific messages by ID }); Spotify Integration ~~~~~~~~~~~~~~~~~~~ **Getting Now Playing Data:** .. code-block:: javascript // Request current Spotify data socket.emit('get_spotify_data'); socket.on('spotify_data_update', function(spotifyData) { console.log('Now playing:', spotifyData); // Spotify data structure: // { // track_name: "Song Title", // artist_name: "Artist Name", // album_name: "Album Name", // album_art_url: "https://...", // is_playing: true, // progress_ms: 120000, // duration_ms: 240000, // external_url: "https://open.spotify.com/..." // } updateNowPlaying(spotifyData); }); PlayStation Network Integration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Getting PSN Data:** .. code-block:: javascript // Request PSN data socket.emit('get_psn_data'); socket.on('psn_data_update', function(psnData) { console.log('PSN data:', psnData); // PSN data structure: // { // current_game_name: "Game Title", // current_game_art_url: "https://...", // current_game_trophies: [ // { // trophy_name: "Achievement Name", // trophy_type: "bronze|silver|gold|platinum", // earned_date: "2024-01-01T12:00:00Z" // } // ], // trophy_counts: { // platinum: 5, // gold: 25, // silver: 100, // bronze: 500 // } // } }); Activity Feed Integration ~~~~~~~~~~~~~~~~~~~~~~~~~ **Getting Historical Alerts:** .. code-block:: javascript // Request paginated stored alerts socket.emit('get_stored_alerts_paginated', { page: 1, limit: 25 }); socket.on('stored_alerts_paginated', function(data) { console.log('Historical alerts:', data); // Response structure: // { // alerts: [ /* array of alert objects */ ], // page: 1, // total_pages: 10, // total_count: 250, // has_prev: false, // has_next: true // } }); **Activity Feed Events:** .. code-block:: javascript // Listen for new activity feed alerts socket.on('activity_feed_alert', function(alertData) { // Display alert in activity feed format addToActivityFeed(alertData); }); Accessing Template Configuration ---------------------------------- Loading Configuration via API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Templates can dynamically load their JSON configuration files through the web API. This allows real-time updates without requiring template reloads: .. code-block:: javascript // Load all template configurations async function loadTemplateConfig() { try { const response = await fetch('/api/all-template-configs'); if (response.ok) { const allConfigs = await response.json(); console.log('Loaded all template configs:', Object.keys(allConfigs)); // Get your specific template config const myTemplateConfig = allConfigs.my_template_name; if (myTemplateConfig && myTemplateConfig.elements) { console.log('Found template config:', myTemplateConfig); // Parse configuration elements into usable values const config = {}; myTemplateConfig.elements.forEach(element => { if (element.type !== 'section') { config[element.id] = element.value; } }); console.log('Parsed config values:', config); applyTemplateConfig(config); } else { console.warn('No template config found'); } } else { console.error('Failed to load template configs:', response.status); } } catch (error) { console.error('Error loading template config:', error); } } Applying Configuration Settings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Once loaded, configuration values can be applied to your template elements: .. code-block:: javascript function applyTemplateConfig(config) { console.log('Applying template config:', config); // Apply styling configurations const container = document.querySelector('.my-container'); // Position and size settings if (config.ContainerLeft !== undefined) { container.style.left = config.ContainerLeft + 'px'; } if (config.ContainerTop !== undefined) { container.style.top = config.ContainerTop + 'px'; } if (config.ContainerWidth !== undefined) { container.style.width = config.ContainerWidth + 'px'; } if (config.ContainerHeight !== undefined) { container.style.height = config.ContainerHeight + 'px'; } // Font and color settings const titleElement = document.getElementById('title'); if (titleElement) { if (config.TitleFontFamily) { titleElement.style.fontFamily = config.TitleFontFamily; } if (config.TitleFontSize !== undefined) { titleElement.style.fontSize = config.TitleFontSize + 'px'; } if (config.TitleColor) { titleElement.style.color = config.TitleColor; } } // Background settings with conditional application if (config.EnableBackground) { container.style.backgroundColor = config.BackgroundColor || 'rgba(0,0,0,0.5)'; container.style.padding = (config.BackgroundPadding || 10) + 'px'; container.style.borderRadius = (config.BackgroundBorderRadius || 5) + 'px'; if (config.BackgroundOpacity !== undefined) { container.style.opacity = config.BackgroundOpacity; } } else { container.style.backgroundColor = 'transparent'; container.style.padding = '0'; container.style.borderRadius = '0'; } // Apply behavioral settings to global variables if (config.AnimationDuration !== undefined) { window.animationDuration = config.AnimationDuration; } if (config.EnableSound !== undefined) { window.soundEnabled = config.EnableSound; } // Update audio source if configured if (config.AudioFile !== undefined) { const audio = document.getElementById('myAudio'); if (audio && audio.firstElementChild) { audio.firstElementChild.src = config.AudioFile; audio.load(); // Reload audio with new source } } } Real-time Configuration Updates ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Set up periodic reloading to pick up configuration changes: .. code-block:: javascript // Load config on startup setTimeout(function() { loadTemplateConfig(); }, 1000); // Periodically reload config (every 30 seconds) setInterval(loadTemplateConfig, 30000); Configuration-Driven Initialization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Use configuration values during template initialization: .. code-block:: javascript // Global configuration object var templateConfig = {}; var configLoaded = false; // Initialize template after config is loaded async function initializeTemplate() { // Load configuration first await loadTemplateConfig(); configLoaded = true; // Set up template with config values setupEventListeners(); initializeDisplay(); startPeriodicUpdates(); } function setupEventListeners() { const socket = io(); socket.on('next_alert', function(alertData) { if (!configLoaded) { console.warn('Config not loaded yet, skipping alert'); return; } // Use config values in alert processing const duration = templateConfig.AlertDuration || 5000; const volume = templateConfig.AudioVolume || 1.0; processAlert(alertData, duration, volume); }); } Configuration Element Types ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Handle different configuration element types properly: .. code-block:: javascript function parseConfigElement(element) { switch (element.type) { case 'text': return element.value || ''; case 'number': return parseFloat(element.value) || 0; case 'boolean': return Boolean(element.value); case 'color': return element.value || '#000000'; case 'select': return element.value || element.options[0]; case 'file': return element.value || ''; case 'font': return element.value || 'Arial'; case 'section': // Skip section headers return null; default: return element.value; } } // Enhanced config parsing function parseTemplateConfig(configData) { const config = {}; if (configData && configData.elements) { configData.elements.forEach(element => { const value = parseConfigElement(element); if (value !== null) { config[element.id] = value; } }); } return config; } Error Handling for Configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Implement robust error handling for configuration loading: .. code-block:: javascript async function loadTemplateConfigSafely() { try { const response = await fetch('/api/all-template-configs'); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const allConfigs = await response.json(); const myConfig = allConfigs.my_template_name; if (!myConfig) { console.warn('Template config not found, using defaults'); return applyDefaultConfig(); } const parsedConfig = parseTemplateConfig(myConfig); applyTemplateConfig(parsedConfig); return true; } catch (error) { console.error('Failed to load template config:', error); // Fallback to default configuration applyDefaultConfig(); return false; } } function applyDefaultConfig() { const defaultConfig = { ContainerWidth: 400, ContainerHeight: 200, TitleColor: '#ffffff', TitleFontSize: 24, AnimationDuration: 5, EnableSound: true }; applyTemplateConfig(defaultConfig); console.log('Applied default configuration'); } Template Controls Integration ----------------------------- Dynamic Controls ~~~~~~~~~~~~~~~~ Add interactive controls to your templates by defining them in your configuration: **Configuration (template_configs/interactive_counter.json):** .. code-block:: json { "template_name": "interactive_counter", "dynamic_controls": { "elements": [ { "type": "counter_control", "id": "main_counter", "label": "Main Counter", "action_increment": "counter_increment", "action_decrement": "counter_decrement", "action_reset": "counter_reset" }, { "type": "button", "id": "special_action", "label": "Special Action", "button_text": "Trigger Effect", "action": "special_effect" }, { "type": "text_input", "id": "message_input", "label": "Send Message", "action": "send_message", "placeholder": "Enter message..." } ] } } **Template JavaScript:** .. code-block:: javascript // Listen for control events socket.on('interactive_counter_counter_increment', function() { incrementCounter(); }); socket.on('interactive_counter_counter_decrement', function() { decrementCounter(); }); socket.on('interactive_counter_counter_reset', function() { resetCounter(); }); socket.on('interactive_counter_special_effect', function() { triggerSpecialEffect(); }); socket.on('interactive_counter_send_message', function(data) { displayMessage(data.text); }); Database Integration ~~~~~~~~~~~~~~~~~~~~ **Setting Data:** .. code-block:: javascript // Save data to database socket.emit('set_data', { path: 'custom_templates/my_template/settings', data: { counter_value: 42, last_update: new Date().toISOString(), user_preferences: { theme: 'dark', animations: true } } }); **Getting Data:** .. code-block:: javascript // Retrieve data from database socket.emit('get_data', { path: 'custom_templates/my_template/settings' }); socket.on('get_data', function(response) { if (response.error) { console.error('Database error:', response.error); } else { console.log('Retrieved data:', response); // Use the data to initialize your template } }); **Updating Data:** .. code-block:: javascript // Update existing data socket.emit('update_data', { path: 'custom_templates/my_template/settings', data: { counter_value: 43 // Only update specific fields } }); Connector Automation Integration -------------------------------- Templates can be fully integrated with the Connector automation system to enable automated control through trigger-action workflows. This allows your templates to respond to stream events automatically. Defining Connector Actions ~~~~~~~~~~~~~~~~~~~~~~~~~~ To make your template compatible with the Connector system, add a ``connector_actions`` section to your template configuration file. This defines what actions connectors can perform on your template. **Configuration (template_configs/my_template.json):** .. code-block:: json { "template_name": "my_template", "elements": [ // Regular configuration elements... ], "connector_actions": { "increment_counter": { "action_name": "Increment Counter", "elements": [ { "type": "number", "id": "increment_amount", "label": "Increment Amount", "value": 1, "min": 1, "max": 100, "description": "How much to increment by" } ] }, "set_text": { "action_name": "Set Text", "elements": [ { "type": "text", "id": "new_text", "label": "Text Content", "value": "Default text", "placeholder": "Enter text here...", "description": "Text to display in the template" } ] }, "trigger_animation": { "action_name": "Trigger Animation", "elements": [ { "type": "select", "id": "animation_type", "label": "Animation Type", "value": "fade", "options": ["fade", "slide", "bounce", "spin"], "description": "Type of animation to trigger" }, { "type": "number", "id": "animation_duration", "label": "Duration (seconds)", "value": 2.0, "min": 0.1, "max": 10.0, "step": 0.1, "description": "How long the animation should last" } ] } } } **Action Element Types:** The ``elements`` array in each connector action supports all the same element types as regular template configuration: * ``text`` - Text input fields * ``number`` - Numeric input with validation * ``select`` - Dropdown selection with options * ``checkbox`` - Boolean true/false values * ``slider`` - Numeric slider with range * ``separator`` - Section headers for organization Listening for Connector Events ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In your template JavaScript, listen for connector-triggered events using the format: ``{template_name}_{action_id}`` .. code-block:: javascript const socket = io(); // Listen for connector-triggered actions socket.on('my_template_increment_counter', function(data) { console.log('Connector triggered increment:', data); // data includes: // - increment_amount: Value from connector action configuration // - trigger_id: ID of the trigger that fired this action // - event_type: Type of original event (e.g., "twitch_bits") // - timestamp: When the trigger fired // - event data from the original trigger incrementCounter(data.increment_amount || 1); }); socket.on('my_template_set_text', function(data) { console.log('Connector triggered text update:', data); // data.new_text contains the configured text (with placeholders replaced) updateDisplayText(data.new_text); }); socket.on('my_template_trigger_animation', function(data) { console.log('Connector triggered animation:', data); // Use the configured animation parameters triggerAnimation(data.animation_type, data.animation_duration); }); Placeholder Support in Connector Actions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Connector actions support placeholder replacement, allowing dynamic content based on the triggering event: **Example Configuration with Placeholders:** .. code-block:: json { "connector_actions": { "show_donation_message": { "action_name": "Show Donation Message", "elements": [ { "type": "text", "id": "message_template", "label": "Message Template", "value": "Thanks {{username}} for the ${{amount}} donation!", "description": "Use {{username}}, {{amount}}, {{message}} placeholders" }, { "type": "number", "id": "display_duration", "label": "Display Duration (seconds)", "value": 5, "min": 1, "max": 30 } ] } } } **Template JavaScript:** .. code-block:: javascript socket.on('my_template_show_donation_message', function(data) { // data.message_template will have placeholders replaced // e.g., "Thanks StreamViewer for the $10.00 donation!" showMessage(data.message_template, data.display_duration * 1000); }); function showMessage(text, duration) { const messageEl = document.getElementById('message'); messageEl.textContent = text; messageEl.style.display = 'block'; setTimeout(() => { messageEl.style.display = 'none'; }, duration); } **Available Placeholders:** Common placeholders available in connector actions: * ``{{username}}`` - User who triggered the event * ``{{amount}}`` - Numeric amount (bits, donation, viewer count) * ``{{message}}`` - Message content (chat, donation message) * ``{{timestamp}}`` - When the event occurred * ``{{event_type}}`` - Type of triggering event * ``{{tier}}`` - Subscription tier (for sub events) * ``{{months}}`` - Subscription months (for sub events) * ``{{command}}`` - Chat command (for command events) * ``{{reward_title}}`` - Channel point reward name Advanced Connector Integration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Dynamic Control Data:** Your template can provide dynamic data back to the connector system for use in conditions: .. code-block:: javascript // Emit template state for connector conditions function updateConnectorState() { socket.emit('template_state_update', { template_name: 'my_template', state: { counter_value: currentCounter, last_action: lastActionType, is_active: isTemplateActive, current_mode: displayMode } }); } // Call this whenever your template state changes incrementCounter = function(amount) { currentCounter += amount; updateDisplay(); updateConnectorState(); // Notify connector system }; **Complex Action Handlers:** Handle complex connector actions with multiple configuration parameters: .. code-block:: javascript socket.on('my_template_complex_action', function(data) { console.log('Complex action triggered:', data); // Access all configured parameters const config = { primaryColor: data.primary_color || '#ffffff', secondaryColor: data.secondary_color || '#000000', animationSpeed: data.animation_speed || 1.0, enableSound: data.enable_sound || false, customText: data.custom_text || 'Default text', repeatCount: data.repeat_count || 1 }; // Execute complex template behavior executeComplexAnimation(config); }); Manual Connector Triggers ~~~~~~~~~~~~~~~~~~~~~~~~~~ Templates can trigger manual connector events for testing or special interactions: .. code-block:: javascript // Trigger manual connectors from template function triggerManualConnector(customData) { socket.emit('trigger_manual_connector', { event_data: { username: 'TemplateUser', source: 'template_manual', action_type: 'button_click', template_name: 'my_template', ...customData } }); } // Example: Button in template triggers manual connectors document.getElementById('testButton').addEventListener('click', function() { triggerManualConnector({ button_id: 'test_button', click_count: Math.floor(Math.random() * 100) }); }); Template Action Categories ~~~~~~~~~~~~~~~~~~~~~~~~~~ Organize your connector actions into logical categories: .. code-block:: json { "connector_actions": { // Counter Actions "counter_increment": { "action_name": "Increment Counter", "category": "Counter", "elements": [...] }, "counter_decrement": { "action_name": "Decrement Counter", "category": "Counter", "elements": [...] }, "counter_reset": { "action_name": "Reset Counter", "category": "Counter", "elements": [] }, // Visual Actions "change_theme": { "action_name": "Change Theme", "category": "Visual", "elements": [...] }, "trigger_effect": { "action_name": "Trigger Effect", "category": "Visual", "elements": [...] }, // Text Actions "update_title": { "action_name": "Update Title", "category": "Text", "elements": [...] } } } Best Practices for Connector Integration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1. **Descriptive Action Names:** Use clear, descriptive names for connector actions that explain what they do. 2. **Logical Grouping:** Organize related actions and use categories to group them in the UI. 3. **Sensible Defaults:** Provide good default values for action parameters that work out of the box. 4. **Parameter Validation:** Set appropriate min/max values and validate input ranges. 5. **Comprehensive Placeholders:** Support relevant placeholders for the events that might trigger your actions. 6. **Error Handling:** Gracefully handle missing or invalid connector data. 7. **State Reporting:** Update connector state when your template changes to enable condition-based automation. 8. **Testing Support:** Provide ways to manually trigger actions for testing connector workflows. Example: Complete Connector-Enabled Template ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Template Configuration (template_configs/smart_counter.json):** .. code-block:: json { "template_name": "smart_counter", "elements": [ { "type": "separator", "label": "Display Settings" }, { "type": "select", "id": "counter_color", "label": "Counter Color", "value": "#ffffff", "display": "color_grid", "options": ["#ffffff", "#ff0000", "#00ff00", "#0000ff"] }, { "type": "number", "id": "font_size", "label": "Font Size", "value": 48, "min": 12, "max": 128 } ], "connector_actions": { "increment": { "action_name": "Increment Counter", "elements": [ { "type": "number", "id": "amount", "label": "Increment Amount", "value": 1, "min": 1, "max": 1000 } ] }, "set_value": { "action_name": "Set Counter Value", "elements": [ { "type": "number", "id": "new_value", "label": "New Value", "value": 0, "min": 0, "max": 999999 } ] }, "show_message": { "action_name": "Show Message", "elements": [ { "type": "text", "id": "message_text", "label": "Message", "value": "Thanks {{username}}!", "placeholder": "Enter message with {{placeholders}}" }, { "type": "number", "id": "duration", "label": "Duration (seconds)", "value": 3, "min": 1, "max": 30 } ] } } } **Template HTML/JavaScript (templates/smart_counter.html):** .. code-block:: html Smart Counter
0
Advanced Features ----------------- Audio Integration ~~~~~~~~~~~~~~~~~ **Getting Available Audio Files:** .. code-block:: javascript // Get audio files from a specific folder socket.emit('get_audio_files', { folder: 'alerts/follows', request_id: 'audio_request_1' }); socket.on('audio_files_response', function(data) { if (data.request_id === 'audio_request_1' && data.success) { console.log('Available audio files:', data.files); // data.files contains array of filenames } }); **Playing Audio:** .. code-block:: javascript function playAlertSound(audioPath, volume = 1.0) { const audio = new Audio(`/assets/${audioPath}`); audio.volume = volume; audio.play().catch(e => console.error('Audio play error:', e)); } Asset Management ~~~~~~~~~~~~~~~~ **Loading Images:** .. code-block:: javascript function loadAlertGif(gifPath) { const img = document.createElement('img'); img.src = `/assets/${gifPath}`; img.onload = function() { // GIF loaded successfully document.body.appendChild(img); }; img.onerror = function() { console.error('Failed to load GIF:', gifPath); }; } **Asset URLs:** * **Template-specific assets:** ``/assets/template_name/file.ext`` * **Shared assets:** ``/assets/default_assets/file.ext`` * **Fonts:** ``/assets/default_assets/fonts/font.ttf`` * **Sounds:** ``/assets/sounds/sound.mp3`` Template Examples ----------------- Donation Goal Tracker ~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: html Donation Goal

Donation Goal

$0 / $500

Real-time Follower Count ~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: html Follower Count
0 Followers
Best Practices --------------- Template Development ~~~~~~~~~~~~~~~~~~~~ 1. **Always include Socket.IO:** ```` 2. **Handle connection events:** Listen for connect/disconnect events 3. **Use transparent backgrounds:** For overlay compatibility 4. **Implement error handling:** Gracefully handle missing data 5. **Test with different data:** Ensure your template works with various alert types 6. **Optimize performance:** Avoid heavy animations that could impact stream performance Configuration Management ~~~~~~~~~~~~~~~~~~~~~~~~~ 1. **Use descriptive labels:** Make configuration options clear for users 2. **Provide sensible defaults:** Templates should work out of the box 3. **Include descriptions:** Help users understand what each setting does 4. **Group related settings:** Use logical organization in your config 5. **Validate input ranges:** Set appropriate min/max values for numbers Asset Organization ~~~~~~~~~~~~~~~~~~ 1. **Create template-specific folders:** Keep assets organized by template 2. **Use appropriate file formats:** Optimize images and audio for web use 3. **Provide fallbacks:** Handle missing assets gracefully 4. **Consider file sizes:** Large assets can impact loading performance 5. **Use relative paths:** Always reference assets via ``/assets/`` URLs Error Handling ~~~~~~~~~~~~~~ .. code-block:: javascript // Always handle WebSocket errors socket.on('connect_error', function(error) { console.error('Connection error:', error); // Show offline state or retry logic }); // Handle missing data gracefully socket.on('next_alert', function(data) { try { if (!data || !data.alert_type) { console.warn('Invalid alert data received'); return; } // Process alert... } catch (error) { console.error('Error processing alert:', error); } }); Debugging Templates ~~~~~~~~~~~~~~~~~~~ 1. **Use browser console:** Check for JavaScript errors 2. **Test WebSocket connection:** Verify events are being received 3. **Validate JSON configs:** Ensure configuration files are valid 4. **Check asset paths:** Verify all assets load correctly 5. **Monitor network requests:** Use browser dev tools to debug loading issues Template Testing ~~~~~~~~~~~~~~~~~ 1. **Test with real data:** Use actual alerts to verify functionality 2. **Test edge cases:** Handle missing or unusual data 3. **Test different screen sizes:** Ensure compatibility with various resolutions 4. **Test performance:** Monitor CPU/memory usage during heavy activity 5. **Test in OBS:** Verify the template works correctly as a browser source