Feature/static image manager (#95)

* feat(static-image): Add static image manager with web interface

- Create StaticImageManager class with image scaling and transparency support
- Add configuration options for display duration, zoom scale, and background color
- Integrate with display controller and web interface
- Add image upload functionality to web interface
- Support for various image formats with proper scaling
- Efficient image processing with aspect ratio preservation
- Ready for future scrolling feature implementation

* fix(static-image): Move display duration to main display_durations block

- Remove display_duration from static_image config section
- Update StaticImageManager to read duration from display.display_durations.static_image
- Remove display duration field from web interface form
- Update web interface JavaScript to not include display_duration in payload
- Follows same pattern as all other managers in the project

* feat(static-image): Add fit to display option

- Add fit_to_display checkbox to automatically scale images to fit display
- When enabled, images are scaled to fit display dimensions while preserving aspect ratio
- When disabled, manual zoom_scale control is available
- Update web interface with smart form controls (zoom scale disabled when fit to display is on)
- Prevents stretching or cropping - images are always properly fitted
- Default to fit_to_display=true for better user experience

* refactor(static-image): Remove zoom_scale and simplify to fit_to_display only

- Remove zoom_scale option entirely as it was confusing and redundant
- Simplify image processing to always fit to display dimensions
- Remove zoom_scale field from web interface
- Clean up JavaScript to remove zoom scale logic
- Images are now always properly fitted without stretching or cropping
- Much simpler and more intuitive user experience
This commit is contained in:
Chuck
2025-10-05 10:46:36 -04:00
committed by GitHub
parent a115a1ed6b
commit f3d02e07ea
11 changed files with 600 additions and 15 deletions

View File

@@ -875,6 +875,9 @@
<button class="tab-btn" onclick="showTab('text')">
<i class="fas fa-font"></i> Text
</button>
<button class="tab-btn" onclick="showTab('static_image')">
<i class="fas fa-image"></i> Static Image
</button>
<button class="tab-btn" onclick="showTab('features')">
<i class="fas fa-star"></i> Features
</button>
@@ -1637,6 +1640,37 @@
</div>
</div>
<!-- Static Image Tab -->
<div id="static_image" class="tab-content">
<div class="config-section">
<div style="display:flex; justify-content: space-between; align-items:center;">
<h3>Static Image Display</h3>
<div style="display:flex; gap:8px;">
<button type="button" class="btn btn-info" onclick="startOnDemand('static_image')"><i class="fas fa-bolt"></i> On-Demand</button>
</div>
</div>
<form id="static_image-form">
<div class="form-group"><label><input type="checkbox" id="static_image_enabled" {% if safe_config_get(main_config, 'static_image', 'enabled', default=False) %}checked{% endif %}> Enable</label></div>
<div class="form-group">
<label for="static_image_path">Image Path</label>
<div style="display:flex; gap:8px;">
<input type="text" id="static_image_path" class="form-control" value="{{ safe_config_get(main_config, 'static_image', 'image_path', default='assets/static_images/default.png') }}">
<button type="button" class="btn btn-secondary" onclick="document.getElementById('image_upload').click()"><i class="fas fa-upload"></i> Upload</button>
</div>
<input type="file" id="image_upload" accept="image/*" style="display:none" onchange="handleImageUpload(this)">
</div>
<div class="form-row">
<div class="form-group"><label><input type="checkbox" id="static_image_fit_to_display" {% if safe_config_get(main_config, 'static_image', 'fit_to_display', default=True) %}checked{% endif %}> Fit to Display</label></div>
</div>
<div class="form-row">
<div class="form-group"><label><input type="checkbox" id="static_image_preserve_aspect_ratio" {% if safe_config_get(main_config, 'static_image', 'preserve_aspect_ratio', default=True) %}checked{% endif %}> Preserve Aspect Ratio</label></div>
<div class="form-group"><label for="static_image_background_color">Background Color</label><input type="color" id="static_image_background_color" class="form-control" data-rgb='{{ safe_config_get(main_config, 'static_image', 'background_color', default=[0, 0, 0]) | tojson }}'></div>
</div>
<button type="submit" class="btn btn-success">Save Static Image Settings</button>
</form>
</div>
</div>
<!-- Features Tab -->
<div id="features" class="tab-content">
<div class="config-section">
@@ -3114,6 +3148,55 @@
});
})();
// Static Image form submit
(function augmentStaticImageForm(){
const form = document.getElementById('static_image-form');
const initColor = (input) => {
try { const rgb = JSON.parse(input.dataset.rgb || '[255,255,255]'); input.value = rgbToHex(rgb); } catch {}
};
initColor(document.getElementById('static_image_background_color'));
form.addEventListener('submit', async function(e){
e.preventDefault();
const payload = {
static_image: {
enabled: document.getElementById('static_image_enabled').checked,
image_path: document.getElementById('static_image_path').value,
fit_to_display: document.getElementById('static_image_fit_to_display').checked,
preserve_aspect_ratio: document.getElementById('static_image_preserve_aspect_ratio').checked,
background_color: hexToRgbArray(document.getElementById('static_image_background_color').value)
}
};
await saveConfigJson(payload);
});
})();
// Image upload handler
function handleImageUpload(input) {
const file = input.files[0];
if (file) {
const formData = new FormData();
formData.append('image', file);
fetch('/upload_image', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('static_image_path').value = data.path;
showNotification('Image uploaded successfully!', 'success');
} else {
showNotification('Failed to upload image: ' + data.error, 'error');
}
})
.catch(error => {
showNotification('Error uploading image: ' + error, 'error');
});
}
}
// YouTube form submit
(function augmentYouTubeForm(){
const form = document.getElementById('youtube-form');