diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml deleted file mode 100644 index 6df4e03..0000000 --- a/.github/workflows/validate.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Validate - -on: - push: - pull_request: - schedule: - - cron: "0 0 * * *" - -jobs: - validate: - runs-on: "ubuntu-latest" - steps: - - uses: "actions/checkout@v3" - - name: HACS validation - uses: "hacs/action@main" - with: - category: "integration" diff --git a/server/src/wled_controller/api/schemas.py b/server/src/wled_controller/api/schemas.py index 030aca6..42cfa45 100644 --- a/server/src/wled_controller/api/schemas.py +++ b/server/src/wled_controller/api/schemas.py @@ -79,7 +79,7 @@ class ProcessingSettings(BaseModel): """Processing settings for a device.""" display_index: int = Field(default=0, description="Display to capture", ge=0) - fps: int = Field(default=30, description="Target frames per second", ge=1, le=60) + fps: int = Field(default=30, description="Target frames per second", ge=10, le=90) border_width: int = Field(default=10, description="Border width in pixels", ge=1, le=100) brightness: float = Field(default=1.0, description="Global brightness (0.0-1.0)", ge=0.0, le=1.0) state_check_interval: int = Field( diff --git a/server/src/wled_controller/config.py b/server/src/wled_controller/config.py index 887382b..4cff52f 100644 --- a/server/src/wled_controller/config.py +++ b/server/src/wled_controller/config.py @@ -28,8 +28,8 @@ class ProcessingConfig(BaseSettings): """Processing configuration.""" default_fps: int = 30 - max_fps: int = 60 - min_fps: int = 1 + max_fps: int = 90 + min_fps: int = 10 border_width: int = 10 interpolation_mode: Literal["average", "median", "dominant"] = "average" diff --git a/server/src/wled_controller/static/app.js b/server/src/wled_controller/static/app.js index 461f3be..8a9984d 100644 --- a/server/src/wled_controller/static/app.js +++ b/server/src/wled_controller/static/app.js @@ -871,11 +871,12 @@ let captureSettingsInitialValues = {}; async function showCaptureSettings(deviceId) { try { - // Fetch device data, displays, and templates in parallel - const [deviceResponse, displaysResponse, templatesResponse] = await Promise.all([ + // Fetch device data, displays, templates, and settings in parallel + const [deviceResponse, displaysResponse, templatesResponse, settingsResponse] = await Promise.all([ fetch(`${API_BASE}/devices/${deviceId}`, { headers: getHeaders() }), fetch(`${API_BASE}/config/displays`, { headers: getHeaders() }), fetchWithAuth('/capture-templates'), + fetch(`${API_BASE}/devices/${deviceId}/settings`, { headers: getHeaders() }), ]); if (deviceResponse.status === 401) { @@ -889,6 +890,7 @@ async function showCaptureSettings(deviceId) { } const device = await deviceResponse.json(); + const currentSettings = settingsResponse.ok ? await settingsResponse.json() : {}; // Populate display index select const displaySelect = document.getElementById('capture-settings-display-index'); @@ -910,6 +912,11 @@ async function showCaptureSettings(deviceId) { } displaySelect.value = String(device.settings.display_index ?? 0); + // Populate FPS slider + const fpsValue = Math.max(10, Math.min(90, currentSettings.fps ?? 30)); + document.getElementById('capture-settings-fps').value = fpsValue; + document.getElementById('capture-settings-fps-value').textContent = fpsValue; + // Populate capture template select const templateSelect = document.getElementById('capture-settings-template'); templateSelect.innerHTML = ''; @@ -931,11 +938,13 @@ async function showCaptureSettings(deviceId) { } templateSelect.value = device.capture_template_id || 'tpl_mss_default'; - // Store device ID and snapshot initial values + // Store device ID, current settings snapshot, and initial values for dirty check document.getElementById('capture-settings-device-id').value = device.id; captureSettingsInitialValues = { display_index: String(device.settings.display_index ?? 0), + fps: String(currentSettings.fps ?? 30), capture_template_id: device.capture_template_id || 'tpl_mss_default', + _currentSettings: currentSettings, }; // Show modal @@ -952,6 +961,7 @@ async function showCaptureSettings(deviceId) { function isCaptureSettingsDirty() { return ( document.getElementById('capture-settings-display-index').value !== captureSettingsInitialValues.display_index || + document.getElementById('capture-settings-fps').value !== captureSettingsInitialValues.fps || document.getElementById('capture-settings-template').value !== captureSettingsInitialValues.capture_template_id ); } @@ -976,6 +986,7 @@ async function closeCaptureSettingsModal() { async function saveCaptureSettings() { const deviceId = document.getElementById('capture-settings-device-id').value; const display_index = parseInt(document.getElementById('capture-settings-display-index').value) || 0; + const fps = parseInt(document.getElementById('capture-settings-fps').value) || 30; const capture_template_id = document.getElementById('capture-settings-template').value; const error = document.getElementById('capture-settings-error'); @@ -999,11 +1010,18 @@ async function saveCaptureSettings() { return; } - // Update display index in settings + // Merge changed fields with current settings to avoid resetting other values + const mergedSettings = { + ...(captureSettingsInitialValues._currentSettings || {}), + display_index, + fps, + }; + + // Update settings const settingsResponse = await fetch(`${API_BASE}/devices/${deviceId}/settings`, { method: 'PUT', headers: getHeaders(), - body: JSON.stringify({ display_index }) + body: JSON.stringify(mergedSettings) }); if (settingsResponse.status === 401) { diff --git a/server/src/wled_controller/static/index.html b/server/src/wled_controller/static/index.html index e749d5c..563e9ad 100644 --- a/server/src/wled_controller/static/index.html +++ b/server/src/wled_controller/static/index.html @@ -261,6 +261,15 @@ Which screen to capture for this device +
+ +
+ + 30 +
+ Target frames per second (10-90) +
+
diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index db9b91e..95db868 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -152,6 +152,8 @@ "settings.url.hint": "IP address or hostname of your WLED device", "settings.display_index": "Display:", "settings.display_index.hint": "Which screen to capture for this device", + "settings.fps": "Target FPS:", + "settings.fps.hint": "Target frames per second (10-90)", "settings.capture_template": "Capture Template:", "settings.capture_template.hint": "Screen capture engine and configuration for this device", "settings.button.cancel": "Cancel", diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index a95cdce..0fac63c 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -152,6 +152,8 @@ "settings.url.hint": "IP адрес или имя хоста вашего WLED устройства", "settings.display_index": "Дисплей:", "settings.display_index.hint": "Какой экран захватывать для этого устройства", + "settings.fps": "Целевой FPS:", + "settings.fps.hint": "Целевая частота кадров (10-90)", "settings.capture_template": "Шаблон Захвата:", "settings.capture_template.hint": "Движок захвата экрана и конфигурация для этого устройства", "settings.button.cancel": "Отмена", diff --git a/server/src/wled_controller/static/style.css b/server/src/wled_controller/static/style.css index 62bd399..817b55c 100644 --- a/server/src/wled_controller/static/style.css +++ b/server/src/wled_controller/static/style.css @@ -997,6 +997,24 @@ input:-webkit-autofill:focus { font-size: 0.85rem; } +.slider-row { + display: flex; + align-items: center; + gap: 12px; +} + +.slider-row input[type="range"] { + flex: 1; +} + +.slider-value { + min-width: 28px; + text-align: center; + font-weight: 600; + font-size: 0.95rem; + color: var(--text-primary); +} + .error-message { background: rgba(244, 67, 54, 0.1); border: 1px solid var(--danger-color);