mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
* fix: remove plugin-specific calendar duration from config template
Plugin display durations should be added dynamically when plugins are
installed, not hardcoded in the template.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(web): ensure unchecked checkboxes save as false in main config forms
HTML checkboxes omit their key entirely when unchecked, so the backend
never received updates to set boolean values to false. This affected:
- vegas_scroll_enabled: Now uses _coerce_to_bool helper
- use_short_date_format: Now uses _coerce_to_bool helper
- Plugin system checkboxes (auto_discover, auto_load_enabled, development_mode):
Now uses _coerce_to_bool helper
- Hardware checkboxes (disable_hardware_pulsing, inverse_colors, show_refresh_rate):
Now uses _coerce_to_bool helper
- web_display_autostart: Now uses _coerce_to_bool helper
Added _coerce_to_bool() helper function that properly converts form string
values ("true", "on", "1", "yes") to actual Python booleans, ensuring
consistent JSON types in config and correct downstream boolean checks.
Also added value="true" to all main config checkboxes for consistent boolean
parsing (sends "true" instead of "on" when checked).
This is the same issue fixed in commit 10d70d91 for plugin configs, but
for the main configuration forms (display settings, general settings).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
592 lines
30 KiB
HTML
592 lines
30 KiB
HTML
<div class="bg-white rounded-lg shadow p-6">
|
|
<div class="border-b border-gray-200 pb-4 mb-6">
|
|
<h2 class="text-lg font-semibold text-gray-900">Display Settings</h2>
|
|
<p class="mt-1 text-sm text-gray-600">Configure LED matrix hardware settings and display options.</p>
|
|
</div>
|
|
|
|
<form hx-post="/api/v3/config/main"
|
|
hx-ext="json-enc"
|
|
hx-headers='{"Content-Type": "application/json"}'
|
|
hx-swap="none"
|
|
hx-on:htmx:after-request="showNotification(event.detail.xhr.responseJSON?.message || 'Display settings saved', event.detail.xhr.responseJSON?.status || 'success')"
|
|
class="space-y-6"
|
|
novalidate
|
|
onsubmit="fixInvalidNumberInputs(this); return true;">
|
|
|
|
<!-- Hardware Settings -->
|
|
<div class="bg-gray-50 rounded-lg p-4">
|
|
<h3 class="text-md font-medium text-gray-900 mb-4">Hardware Configuration</h3>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-4 gap-4 mb-4">
|
|
<div class="form-group">
|
|
<label for="rows" class="block text-sm font-medium text-gray-700">Rows</label>
|
|
<input type="number"
|
|
id="rows"
|
|
name="rows"
|
|
value="{{ main_config.display.hardware.rows or 32 }}"
|
|
min="1"
|
|
max="64"
|
|
class="form-control">
|
|
<p class="mt-1 text-sm text-gray-600">Number of LED rows</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="cols" class="block text-sm font-medium text-gray-700">Columns</label>
|
|
<input type="number"
|
|
id="cols"
|
|
name="cols"
|
|
value="{{ main_config.display.hardware.cols or 64 }}"
|
|
min="1"
|
|
max="128"
|
|
class="form-control">
|
|
<p class="mt-1 text-sm text-gray-600">Number of LED columns</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="chain_length" class="block text-sm font-medium text-gray-700">Chain Length</label>
|
|
<input type="number"
|
|
id="chain_length"
|
|
name="chain_length"
|
|
value="{{ main_config.display.hardware.chain_length or 2 }}"
|
|
min="1"
|
|
max="8"
|
|
class="form-control">
|
|
<p class="mt-1 text-sm text-gray-600">Number of LED panels chained together</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="parallel" class="block text-sm font-medium text-gray-700">Parallel</label>
|
|
<input type="number"
|
|
id="parallel"
|
|
name="parallel"
|
|
value="{{ main_config.display.hardware.parallel or 1 }}"
|
|
min="1"
|
|
max="4"
|
|
class="form-control">
|
|
<p class="mt-1 text-sm text-gray-600">Number of parallel chains</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="form-group">
|
|
<label for="brightness" class="block text-sm font-medium text-gray-700">Brightness</label>
|
|
<div class="flex items-center space-x-2">
|
|
<input type="range"
|
|
id="brightness"
|
|
name="brightness"
|
|
value="{{ main_config.display.hardware.brightness or 95 }}"
|
|
min="1"
|
|
max="100"
|
|
class="flex-1">
|
|
<span id="brightness-value" class="text-sm font-medium w-12">{{ main_config.display.hardware.brightness or 95 }}</span>
|
|
</div>
|
|
<p class="mt-1 text-sm text-gray-600">LED brightness: <span id="brightness-display">{{ main_config.display.hardware.brightness or 95 }}</span>%</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="hardware_mapping" class="block text-sm font-medium text-gray-700">Hardware Mapping</label>
|
|
<select id="hardware_mapping" name="hardware_mapping" class="form-control">
|
|
<option value="adafruit-hat-pwm" {% if main_config.display.hardware.hardware_mapping == "adafruit-hat-pwm" %}selected{% endif %}>Adafruit HAT PWM</option>
|
|
<option value="adafruit-hat" {% if main_config.display.hardware.hardware_mapping == "adafruit-hat" %}selected{% endif %}>Adafruit HAT</option>
|
|
<option value="regular" {% if main_config.display.hardware.hardware_mapping == "regular" %}selected{% endif %}>Regular</option>
|
|
<option value="regular-pi1" {% if main_config.display.hardware.hardware_mapping == "regular-pi1" %}selected{% endif %}>Regular Pi1</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="form-group">
|
|
<label for="gpio_slowdown" class="block text-sm font-medium text-gray-700">GPIO Slowdown</label>
|
|
<input type="number"
|
|
id="gpio_slowdown"
|
|
name="gpio_slowdown"
|
|
value="{{ main_config.display.runtime.gpio_slowdown or 3 }}"
|
|
min="0"
|
|
max="5"
|
|
class="form-control">
|
|
<p class="mt-1 text-sm text-gray-600">GPIO slowdown factor (0-5)</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="scan_mode" class="block text-sm font-medium text-gray-700">Scan Mode</label>
|
|
<input type="number"
|
|
id="scan_mode"
|
|
name="scan_mode"
|
|
value="{{ main_config.display.hardware.scan_mode or 0 }}"
|
|
min="0"
|
|
max="1"
|
|
class="form-control">
|
|
<p class="mt-1 text-sm text-gray-600">Scan mode for LED matrix (0-1)</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="form-group">
|
|
<label for="pwm_bits" class="block text-sm font-medium text-gray-700">PWM Bits</label>
|
|
<input type="number"
|
|
id="pwm_bits"
|
|
name="pwm_bits"
|
|
value="{{ main_config.display.hardware.pwm_bits or 9 }}"
|
|
min="1"
|
|
max="11"
|
|
class="form-control">
|
|
<p class="mt-1 text-sm text-gray-600">PWM bits for brightness control (1-11)</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="pwm_dither_bits" class="block text-sm font-medium text-gray-700">PWM Dither Bits</label>
|
|
<input type="number"
|
|
id="pwm_dither_bits"
|
|
name="pwm_dither_bits"
|
|
value="{{ main_config.display.hardware.pwm_dither_bits or 1 }}"
|
|
min="0"
|
|
max="4"
|
|
class="form-control">
|
|
<p class="mt-1 text-sm text-gray-600">PWM dither bits (0-4)</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="form-group">
|
|
<label for="pwm_lsb_nanoseconds" class="block text-sm font-medium text-gray-700">PWM LSB Nanoseconds</label>
|
|
<input type="number"
|
|
id="pwm_lsb_nanoseconds"
|
|
name="pwm_lsb_nanoseconds"
|
|
value="{{ main_config.display.hardware.pwm_lsb_nanoseconds or 130 }}"
|
|
min="50"
|
|
max="500"
|
|
class="form-control">
|
|
<p class="mt-1 text-sm text-gray-600">PWM LSB nanoseconds (50-500)</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="limit_refresh_rate_hz" class="block text-sm font-medium text-gray-700">Limit Refresh Rate (Hz)</label>
|
|
<input type="number"
|
|
id="limit_refresh_rate_hz"
|
|
name="limit_refresh_rate_hz"
|
|
value="{{ main_config.display.hardware.limit_refresh_rate_hz or 120 }}"
|
|
min="1"
|
|
max="1000"
|
|
class="form-control">
|
|
<p class="mt-1 text-sm text-gray-600">Limit refresh rate in Hz (1-1000)</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Display Options -->
|
|
<div class="bg-gray-50 rounded-lg p-4">
|
|
<h3 class="text-md font-medium text-gray-900 mb-4">Display Options</h3>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="form-group">
|
|
<label class="flex items-center">
|
|
<input type="checkbox"
|
|
name="disable_hardware_pulsing"
|
|
value="true"
|
|
{% if main_config.display.hardware.disable_hardware_pulsing %}checked{% endif %}
|
|
class="form-control h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
|
<span class="ml-2 text-sm font-medium text-gray-900">Disable Hardware Pulsing</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="flex items-center">
|
|
<input type="checkbox"
|
|
name="inverse_colors"
|
|
value="true"
|
|
{% if main_config.display.hardware.inverse_colors %}checked{% endif %}
|
|
class="form-control h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
|
<span class="ml-2 text-sm font-medium text-gray-900">Inverse Colors</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="flex items-center">
|
|
<input type="checkbox"
|
|
name="show_refresh_rate"
|
|
value="true"
|
|
{% if main_config.display.hardware.show_refresh_rate %}checked{% endif %}
|
|
class="form-control h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
|
<span class="ml-2 text-sm font-medium text-gray-900">Show Refresh Rate</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="flex items-center">
|
|
<input type="checkbox"
|
|
name="use_short_date_format"
|
|
value="true"
|
|
{% if main_config.display.use_short_date_format %}checked{% endif %}
|
|
class="form-control h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
|
<span class="ml-2 text-sm font-medium text-gray-900">Use Short Date Format</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Dynamic Duration Settings -->
|
|
<div class="mt-6 pt-4 border-t border-gray-300">
|
|
<h4 class="text-sm font-medium text-gray-900 mb-3">Dynamic Duration</h4>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="form-group">
|
|
<label for="max_dynamic_duration_seconds" class="block text-sm font-medium text-gray-700">Max Dynamic Duration (seconds)</label>
|
|
<input type="number"
|
|
id="max_dynamic_duration_seconds"
|
|
name="max_dynamic_duration_seconds"
|
|
value="{{ main_config.display.get('dynamic_duration', {}).get('max_duration_seconds', 180) }}"
|
|
min="30"
|
|
max="1800"
|
|
class="form-control">
|
|
<p class="mt-1 text-sm text-gray-600">Maximum time plugins can extend display duration (30-1800 seconds)</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Vegas Scroll Mode Settings -->
|
|
<div class="bg-gray-50 rounded-lg p-4 mt-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div>
|
|
<h3 id="vegas_scroll_label" class="text-md font-medium text-gray-900">
|
|
<i class="fas fa-scroll mr-2"></i>Vegas Scroll Mode
|
|
</h3>
|
|
<p class="mt-1 text-sm text-gray-600">Combine all plugin content into one continuous scrolling ticker display.</p>
|
|
</div>
|
|
<label class="flex items-center cursor-pointer">
|
|
<input type="checkbox"
|
|
id="vegas_scroll_enabled"
|
|
name="vegas_scroll_enabled"
|
|
value="true"
|
|
aria-label="Enable Vegas Scroll Mode"
|
|
{% if main_config.display.get('vegas_scroll', {}).get('enabled', false) %}checked{% endif %}
|
|
class="h-5 w-5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
|
<span class="ml-2 text-sm font-medium text-gray-700">Enable</span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Vegas Settings (shown when enabled) -->
|
|
<div id="vegas_scroll_settings" class="space-y-4" style="{% if not main_config.display.get('vegas_scroll', {}).get('enabled', false) %}display: none;{% endif %}">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="form-group">
|
|
<label for="vegas_scroll_speed" class="block text-sm font-medium text-gray-700">Scroll Speed (pixels/second)</label>
|
|
<div class="flex items-center space-x-2">
|
|
<input type="range"
|
|
id="vegas_scroll_speed"
|
|
name="vegas_scroll_speed"
|
|
value="{{ main_config.display.get('vegas_scroll', {}).get('scroll_speed', 50) }}"
|
|
min="10"
|
|
max="200"
|
|
step="5"
|
|
class="flex-1">
|
|
<span id="vegas_scroll_speed_value" class="text-sm font-medium w-12">{{ main_config.display.get('vegas_scroll', {}).get('scroll_speed', 50) }}</span>
|
|
</div>
|
|
<p class="mt-1 text-sm text-gray-600">Speed of the scrolling ticker (10-200 px/s)</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="vegas_separator_width" class="block text-sm font-medium text-gray-700">Separator Width (pixels)</label>
|
|
<input type="number"
|
|
id="vegas_separator_width"
|
|
name="vegas_separator_width"
|
|
value="{{ main_config.display.get('vegas_scroll', {}).get('separator_width', 32) }}"
|
|
min="0"
|
|
max="128"
|
|
class="form-control">
|
|
<p class="mt-1 text-sm text-gray-600">Gap between plugin content blocks (0-128 px)</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="form-group">
|
|
<label for="vegas_target_fps" class="block text-sm font-medium text-gray-700">Target FPS</label>
|
|
<select id="vegas_target_fps" name="vegas_target_fps" class="form-control">
|
|
<option value="60" {% if main_config.display.get('vegas_scroll', {}).get('target_fps', 125) == 60 %}selected{% endif %}>60 FPS (Lower CPU)</option>
|
|
<option value="90" {% if main_config.display.get('vegas_scroll', {}).get('target_fps', 125) == 90 %}selected{% endif %}>90 FPS (Balanced)</option>
|
|
<option value="125" {% if main_config.display.get('vegas_scroll', {}).get('target_fps', 125) == 125 %}selected{% endif %}>125 FPS (Smoothest)</option>
|
|
</select>
|
|
<p class="mt-1 text-sm text-gray-600">Higher FPS = smoother scroll, more CPU usage</p>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="vegas_buffer_ahead" class="block text-sm font-medium text-gray-700">Buffer Ahead</label>
|
|
<select id="vegas_buffer_ahead" name="vegas_buffer_ahead" class="form-control">
|
|
<option value="1" {% if main_config.display.get('vegas_scroll', {}).get('buffer_ahead', 2) == 1 %}selected{% endif %}>1 Plugin (Less memory)</option>
|
|
<option value="2" {% if main_config.display.get('vegas_scroll', {}).get('buffer_ahead', 2) == 2 %}selected{% endif %}>2 Plugins (Recommended)</option>
|
|
<option value="3" {% if main_config.display.get('vegas_scroll', {}).get('buffer_ahead', 2) == 3 %}selected{% endif %}>3 Plugins (More buffer)</option>
|
|
</select>
|
|
<p class="mt-1 text-sm text-gray-600">How many plugins to pre-load ahead</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Plugin Order Section -->
|
|
<div class="mt-4 pt-4 border-t border-gray-200">
|
|
<h4 class="text-sm font-medium text-gray-900 mb-3">Plugin Order</h4>
|
|
<p class="text-sm text-gray-600 mb-3">Drag to reorder plugins. Uncheck to exclude from Vegas scroll.</p>
|
|
<div id="vegas_plugin_order" class="space-y-2 bg-white rounded-lg p-3 border border-gray-200">
|
|
<!-- Plugin order list will be populated by JavaScript -->
|
|
<p class="text-sm text-gray-500 italic">Loading plugins...</p>
|
|
</div>
|
|
<input type="hidden" id="vegas_plugin_order_value" name="vegas_plugin_order" value="{{ main_config.display.get('vegas_scroll', {}).get('plugin_order', [])|tojson }}">
|
|
<input type="hidden" id="vegas_excluded_plugins_value" name="vegas_excluded_plugins" value="{{ main_config.display.get('vegas_scroll', {}).get('excluded_plugins', [])|tojson }}">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Submit Button -->
|
|
<div class="flex justify-end">
|
|
<button type="submit"
|
|
class="btn bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md">
|
|
<i class="fas fa-save mr-2"></i>
|
|
Save Display Settings
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<script>
|
|
// Update brightness display
|
|
document.getElementById('brightness').addEventListener('input', function() {
|
|
document.getElementById('brightness-value').textContent = this.value;
|
|
document.getElementById('brightness-display').textContent = this.value;
|
|
});
|
|
|
|
// Fix invalid number inputs function (if not already defined globally)
|
|
if (typeof window.fixInvalidNumberInputs !== 'function') {
|
|
window.fixInvalidNumberInputs = function(form) {
|
|
if (!form) return;
|
|
const allInputs = form.querySelectorAll('input[type="number"]');
|
|
allInputs.forEach(input => {
|
|
const min = parseFloat(input.getAttribute('min'));
|
|
const max = parseFloat(input.getAttribute('max'));
|
|
const value = parseFloat(input.value);
|
|
|
|
if (!isNaN(value)) {
|
|
if (!isNaN(min) && value < min) {
|
|
input.value = min;
|
|
} else if (!isNaN(max) && value > max) {
|
|
input.value = max;
|
|
}
|
|
}
|
|
});
|
|
};
|
|
}
|
|
|
|
// Vegas Scroll Mode Settings
|
|
(function() {
|
|
// Escape HTML to prevent XSS
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = String(text || '');
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Escape for use in HTML attributes
|
|
function escapeAttr(text) {
|
|
return escapeHtml(text).replace(/"/g, '"').replace(/'/g, ''');
|
|
}
|
|
|
|
// Toggle settings visibility
|
|
const vegasEnabledCheckbox = document.getElementById('vegas_scroll_enabled');
|
|
const vegasSettings = document.getElementById('vegas_scroll_settings');
|
|
|
|
if (vegasEnabledCheckbox && vegasSettings) {
|
|
vegasEnabledCheckbox.addEventListener('change', function() {
|
|
vegasSettings.style.display = this.checked ? 'block' : 'none';
|
|
});
|
|
}
|
|
|
|
// Update scroll speed display
|
|
const scrollSpeedSlider = document.getElementById('vegas_scroll_speed');
|
|
const scrollSpeedValue = document.getElementById('vegas_scroll_speed_value');
|
|
|
|
if (scrollSpeedSlider && scrollSpeedValue) {
|
|
scrollSpeedSlider.addEventListener('input', function() {
|
|
scrollSpeedValue.textContent = this.value;
|
|
});
|
|
}
|
|
|
|
// Initialize plugin order list
|
|
function initPluginOrderList() {
|
|
const container = document.getElementById('vegas_plugin_order');
|
|
if (!container) return;
|
|
|
|
// Fetch available plugins
|
|
fetch('/api/v3/plugins/installed')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// Handle both {data: {plugins: []}} and {plugins: []} response formats
|
|
const allPlugins = data.data?.plugins || data.plugins || [];
|
|
if (!allPlugins || allPlugins.length === 0) {
|
|
container.innerHTML = '<p class="text-sm text-gray-500 italic">No plugins available</p>';
|
|
return;
|
|
}
|
|
|
|
// Get current order and exclusions
|
|
const orderInput = document.getElementById('vegas_plugin_order_value');
|
|
const excludedInput = document.getElementById('vegas_excluded_plugins_value');
|
|
let currentOrder = [];
|
|
let excluded = [];
|
|
|
|
try {
|
|
currentOrder = JSON.parse(orderInput.value || '[]');
|
|
excluded = JSON.parse(excludedInput.value || '[]');
|
|
} catch (e) {
|
|
console.error('Error parsing vegas config:', e);
|
|
}
|
|
|
|
// Build ordered plugin list (only enabled plugins)
|
|
const plugins = allPlugins.filter(p => p.enabled);
|
|
const orderedPlugins = [];
|
|
|
|
// First add plugins in current order
|
|
currentOrder.forEach(id => {
|
|
const plugin = plugins.find(p => p.id === id);
|
|
if (plugin) orderedPlugins.push(plugin);
|
|
});
|
|
|
|
// Then add remaining plugins
|
|
plugins.forEach(plugin => {
|
|
if (!orderedPlugins.find(p => p.id === plugin.id)) {
|
|
orderedPlugins.push(plugin);
|
|
}
|
|
});
|
|
|
|
// Build HTML with display mode indicators
|
|
let html = '';
|
|
orderedPlugins.forEach((plugin, index) => {
|
|
const isExcluded = excluded.includes(plugin.id);
|
|
// Determine display mode (from plugin config or default)
|
|
const vegasMode = plugin.vegas_mode || plugin.vegas_content_type || 'fixed';
|
|
const modeLabels = {
|
|
'scroll': { label: 'Scroll', icon: 'fa-scroll', color: 'text-blue-600' },
|
|
'fixed': { label: 'Fixed', icon: 'fa-square', color: 'text-green-600' },
|
|
'static': { label: 'Static', icon: 'fa-pause', color: 'text-orange-600' }
|
|
};
|
|
const modeInfo = modeLabels[vegasMode] || modeLabels['fixed'];
|
|
// Escape plugin metadata to prevent XSS
|
|
const safePluginId = escapeAttr(plugin.id);
|
|
const safePluginName = escapeHtml(plugin.name || plugin.id);
|
|
html += `
|
|
<div class="flex items-center p-2 bg-gray-50 rounded border border-gray-200 cursor-move vegas-plugin-item"
|
|
data-plugin-id="${safePluginId}" draggable="true">
|
|
<i class="fas fa-grip-vertical text-gray-400 mr-3"></i>
|
|
<label class="flex items-center flex-1">
|
|
<input type="checkbox"
|
|
class="vegas-plugin-include h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-2"
|
|
${!isExcluded ? 'checked' : ''}>
|
|
<span class="text-sm font-medium text-gray-700">${safePluginName}</span>
|
|
</label>
|
|
<span class="text-xs ${modeInfo.color} ml-2" title="Vegas display mode: ${modeInfo.label}">
|
|
<i class="fas ${modeInfo.icon} mr-1"></i>${modeInfo.label}
|
|
</span>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
container.innerHTML = html || '<p class="text-sm text-gray-500 italic">No enabled plugins</p>';
|
|
|
|
// Setup drag and drop
|
|
setupDragAndDrop(container);
|
|
|
|
// Setup checkbox handlers
|
|
container.querySelectorAll('.vegas-plugin-include').forEach(checkbox => {
|
|
checkbox.addEventListener('change', updatePluginConfig);
|
|
});
|
|
|
|
// Initialize hidden inputs with current state
|
|
updatePluginConfig();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching plugins:', error);
|
|
container.innerHTML = '<p class="text-sm text-red-500">Error loading plugins</p>';
|
|
});
|
|
}
|
|
|
|
function setupDragAndDrop(container) {
|
|
let draggedItem = null;
|
|
|
|
container.querySelectorAll('.vegas-plugin-item').forEach(item => {
|
|
item.addEventListener('dragstart', function(e) {
|
|
draggedItem = this;
|
|
this.style.opacity = '0.5';
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
});
|
|
|
|
item.addEventListener('dragend', function() {
|
|
this.style.opacity = '1';
|
|
draggedItem = null;
|
|
updatePluginConfig();
|
|
});
|
|
|
|
item.addEventListener('dragover', function(e) {
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = 'move';
|
|
|
|
const rect = this.getBoundingClientRect();
|
|
const midY = rect.top + rect.height / 2;
|
|
|
|
if (e.clientY < midY) {
|
|
this.style.borderTop = '2px solid #3b82f6';
|
|
this.style.borderBottom = '';
|
|
} else {
|
|
this.style.borderBottom = '2px solid #3b82f6';
|
|
this.style.borderTop = '';
|
|
}
|
|
});
|
|
|
|
item.addEventListener('dragleave', function() {
|
|
this.style.borderTop = '';
|
|
this.style.borderBottom = '';
|
|
});
|
|
|
|
item.addEventListener('drop', function(e) {
|
|
e.preventDefault();
|
|
this.style.borderTop = '';
|
|
this.style.borderBottom = '';
|
|
|
|
if (draggedItem && draggedItem !== this) {
|
|
const rect = this.getBoundingClientRect();
|
|
const midY = rect.top + rect.height / 2;
|
|
|
|
if (e.clientY < midY) {
|
|
container.insertBefore(draggedItem, this);
|
|
} else {
|
|
container.insertBefore(draggedItem, this.nextSibling);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function updatePluginConfig() {
|
|
const container = document.getElementById('vegas_plugin_order');
|
|
const orderInput = document.getElementById('vegas_plugin_order_value');
|
|
const excludedInput = document.getElementById('vegas_excluded_plugins_value');
|
|
|
|
if (!container || !orderInput || !excludedInput) return;
|
|
|
|
const order = [];
|
|
const excluded = [];
|
|
|
|
container.querySelectorAll('.vegas-plugin-item').forEach(item => {
|
|
const pluginId = item.dataset.pluginId;
|
|
const checkbox = item.querySelector('.vegas-plugin-include');
|
|
|
|
order.push(pluginId);
|
|
if (checkbox && !checkbox.checked) {
|
|
excluded.push(pluginId);
|
|
}
|
|
});
|
|
|
|
orderInput.value = JSON.stringify(order);
|
|
excludedInput.value = JSON.stringify(excluded);
|
|
}
|
|
|
|
// Initialize on DOM ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initPluginOrderList);
|
|
} else {
|
|
initPluginOrderList();
|
|
}
|
|
})();
|
|
</script>
|