Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion src/blueprints/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
from plugins.plugin_registry import get_plugin_instance
from utils.app_utils import resolve_path, handle_request_files, parse_form
from refresh_task import ManualRefresh, PlaylistRefresh
import base64
import json
import os
import logging
import os
from io import BytesIO

logger = logging.getLogger(__name__)
plugin_bp = Blueprint("plugin", __name__)
Expand Down Expand Up @@ -256,3 +258,38 @@ def update_now():
return jsonify({"error": f"An error occurred: {str(e)}"}), 500

return jsonify({"success": True, "message": "Display updated"}), 200


@plugin_bp.route('/preview', methods=['POST'])
Comment thread
florianbachmann marked this conversation as resolved.
def preview():
"""Generate a preview image without updating the display"""
device_config = current_app.config['DEVICE_CONFIG']

try:
plugin_settings = parse_form(request.form)
plugin_settings.update(handle_request_files(request.files))
plugin_id = plugin_settings.pop("plugin_id", None)
if not plugin_id:
return jsonify({"error": "plugin_id is required"}), 400

plugin_config = device_config.get_plugin(plugin_id)
if not plugin_config:
return jsonify({"error": f"Plugin '{plugin_id}' not found"}), 404

Comment thread
florianbachmann marked this conversation as resolved.
plugin = get_plugin_instance(plugin_config)
image = plugin.generate_image(plugin_settings, device_config)
if image is None:
return jsonify({"error": "An error occurred: NoneType — Chromium may not be installed"}), 500

buffer = BytesIO()
image.save(buffer, format='PNG')
image_b64 = base64.b64encode(buffer.getvalue()).decode()

Comment thread
florianbachmann marked this conversation as resolved.
return jsonify({
"success": True,
"image": f"data:image/png;base64,{image_b64}"
}), 200

except Exception as e:
logger.exception(f"Error in preview: {str(e)}")
return jsonify({"error": f"An error occurred: {str(e)}"}), 500
10 changes: 8 additions & 2 deletions src/plugins/clock/settings.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<div id="preview-widget"></div>

<div class="form-group">
<label for="clock-face" class="form-label">Clock Face:</label>

Expand Down Expand Up @@ -46,13 +48,15 @@
document.getElementById('selected-clock-face').value = selectedFaceName;
document.querySelector("[name=primaryColor]").value = element.dataset.primaryColor;
document.querySelector("[name=secondaryColor]").value = element.dataset.secondaryColor;

PreviewManager.refresh();
}

// Default selection for the first clock face or based on plugin_settings
document.addEventListener('DOMContentLoaded', () => {
const clockFaceContainer = document.getElementById('clock-face-selection');
const selectedClockFaceInput = document.getElementById('selected-clock-face');

// Check if pluginSettings has a selectedClockFace value
const selectedClockFaceFromSettings = pluginSettings?.selectedClockFace;

Expand All @@ -75,5 +79,7 @@
if (pluginSettings.secondaryColor) {
document.querySelector("[name=secondaryColor]").value = pluginSettings.secondaryColor;
}

PreviewManager.init('clock');
});
</script>
</script>
4 changes: 4 additions & 0 deletions src/plugins/comic/settings.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<div id="preview-widget"></div>

<div class="form-group nowrap">
<label for="comic" class="form-label">Comic:</label>
<select id="comic" name="comic" class="form-input">
Expand Down Expand Up @@ -35,5 +37,7 @@
document.getElementById('titleCaption').checked = pluginSettings.titleCaption || false;
document.getElementById('fontSize').value = pluginSettings.fontSize || '14';
}

PreviewManager.init('comic');
});
</script>
8 changes: 7 additions & 1 deletion src/plugins/countdown/settings.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<div id="preview-widget"></div>

<div class="form-group">
<div class="form-group">
<label for="title" class="form-label">Title</label>
Expand All @@ -15,5 +17,9 @@
document.getElementById('title').value = pluginSettings.title || '';
document.getElementById('date').value = pluginSettings.date;
}

PreviewManager.init('countdown', {
required: { date: 'select a date first' }
});
});
</script>
</script>
22 changes: 16 additions & 6 deletions src/plugins/todo_list/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
gap: 1rem;
margin-bottom: 1rem;
}

.list {
display: flex;
flex-direction: column;
Expand All @@ -31,6 +31,8 @@

</style>

<div id="preview-widget"></div>

<div class="form-group">
<div class="form-group nowrap">
<label for="title" class="form-label">Title</label>
Expand All @@ -39,9 +41,9 @@
<div class="form-group nowrap">
<label for="listStyle" class="form-label">List Style:</label>
<select id="listStyle" name="listStyle" class="form-input">
<option value="disc">Disc ()</option>
<option value="square">Square ()</option>
<option value="'\25C6 '">Diamond ()</option>
<option value="disc">Disc (&#9679;)</option>
<option value="square">Square (&#9724;)</option>
<option value="'\25C6 '">Diamond (&#9670;)</option>
<option value="decimal">Decimal</option>
<option value="lower-roman">Roman Numeral</option>
<option value="lower-alpha">Alphabetical</option>
Expand Down Expand Up @@ -90,6 +92,7 @@
list.querySelector(".delete-button").onclick = () => {
list.remove();
listCount--;
PreviewManager.refresh();
};

container.appendChild(list);
Expand All @@ -116,10 +119,17 @@

// Handle Add button
addBtn.onclick = () => {
if (listCount < maxLists) createList();
if (listCount < maxLists) {
createList();
PreviewManager.refresh();
}
};

document.getElementById('fontSize').value = fontSize;
document.getElementById('listStyle').value = listStyle;

PreviewManager.init('todo_list', {
required: { 'list[]': 'add a list first' }
});
});
</script>
</script>
6 changes: 5 additions & 1 deletion src/plugins/wpotd/settings.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<div id="preview-widget"></div>

<span style="color: var(--text-primary);">If the date field is blank it will default to today's date. This is useful when adding to a playlist.</span>
<div class="form-group">
<label for="customDate" class="form-label">Date (optional)</label>
Expand Down Expand Up @@ -46,5 +48,7 @@
$date.value = "";
$toggleShrink.checked = true;
$toggleShrink.value = "true";}

PreviewManager.init('wpotd');
});
</script>
</script>
7 changes: 7 additions & 0 deletions src/plugins/year_progress/settings.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div id="preview-widget"></div>

<script>
document.addEventListener('DOMContentLoaded', () => {
PreviewManager.init('year_progress');
});
</script>
167 changes: 167 additions & 0 deletions src/static/scripts/preview_manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* Live Preview Manager for InkyPi Plugin Settings
*
* Generates a live preview image when plugin settings change,
* without pushing to the physical display.
*
* Usage in a plugin's settings.html:
*
* <!-- Add preview widget (use any unique prefix for IDs) -->
* <div id="preview-widget"></div>
*
* <script>
* document.addEventListener('DOMContentLoaded', () => {
* PreviewManager.init('your_plugin_id');
* });
* </script>
*
* The widget auto-renders into #preview-widget and listens to all
* form inputs for changes. Call PreviewManager.refresh() after
* dynamically adding form elements (e.g. new list items).
*/
const PreviewManager = (() => {
let timeout = null;
let pluginId = null;
let requiredFields = {};

function render(containerId) {
const container = document.getElementById(containerId);
if (!container) return;

container.innerHTML = `
<div class="form-group">
<label class="form-label">Preview</label>
<div style="display: flex; gap: 15px; flex-wrap: wrap;">
<div style="flex: 1; min-width: 200px; max-width: 45%;">
<p style="margin: 0 0 5px 0; font-size: 12px; color: var(--text-muted, #666);">Current</p>
<div style="border: 1px solid var(--border-color, #ccc); border-radius: 8px; overflow: hidden; background: #f5f5f5;">
<img id="preview-current"
src="/static/images/current_image.png?${Date.now()}"
alt="Current Display"
style="width: 100%; display: block;"
onerror="this.style.display='none'">
</div>
</div>
<div style="flex: 1; min-width: 200px; max-width: 45%;">
<p style="margin: 0 0 5px 0; font-size: 12px; color: var(--text-muted, #666);">Preview <span id="preview-status" style="font-style: italic;"></span></p>
<div style="border: 2px dashed var(--primary-color, #4CAF50); border-radius: 8px; overflow: hidden; background: #f5f5f5;">
<img id="preview-image"
alt="Preview with new settings"
style="width: 100%; display: block;">
</div>
</div>
</div>
</div>`;
}

function checkRequired() {
const form = document.querySelector('form');
if (!form) return null;
for (const [field, label] of Object.entries(requiredFields)) {
Comment thread
florianbachmann marked this conversation as resolved.
if (field.endsWith('[]')) {
const elements = form.querySelectorAll(`[name="${field}"]`);
const hasValue = Array.from(elements).some(el => el.value.trim());
if (!hasValue) return label;
} else {
const el = form.querySelector(`[name="${field}"]`);
if (!el || !el.value.trim()) return label;
}
}
return null;
}

function generate() {
clearTimeout(timeout);
timeout = setTimeout(async () => {
const statusEl = document.getElementById('preview-status');
const previewImg = document.getElementById('preview-image');
if (!statusEl || !previewImg) return;

const missingLabel = checkRequired();
if (missingLabel) {
previewImg.style.display = 'none';
statusEl.textContent = `(${missingLabel})`;
return;
}

statusEl.textContent = '(loading...)';

const form = document.querySelector('form');
if (!form) return;

const formData = new FormData(form);
formData.append('plugin_id', pluginId);

try {
const response = await fetch('/preview', {
method: 'POST',
body: formData
});

const result = await response.json();
if (result.success) {
previewImg.src = result.image;
previewImg.style.display = 'block';
statusEl.textContent = '';
} else {
previewImg.style.display = 'none';
const err = result.error || '';
if (err.includes('Chromium') || err.includes('NoneType')) {
statusEl.textContent = '(needs Chromium — works on Pi)';
} else if (err.includes('required')) {
// Missing required field — show friendly hint
const field = err.match(/(\w+) is required/i);
statusEl.textContent = field ? `(no ${field[1].toLowerCase()} set yet)` : '(fill in required fields)';
} else {
// Show the actual error (strip "An error occurred: " prefix)
const msg = err.replace(/^An error occurred:\s*/i, '');
statusEl.textContent = msg ? `(${msg})` : '(preview unavailable)';
Comment thread
florianbachmann marked this conversation as resolved.
}
console.error('Preview error:', err);
}
} catch (err) {
previewImg.style.display = 'none';
statusEl.textContent = '(preview unavailable)';
console.error('Preview error:', err);
}
}, 500);
}

function listenToForm() {
document.querySelectorAll('form select, form input, form textarea').forEach(el => {
if (el.dataset.previewBound) return;
el.addEventListener('change', generate);
el.addEventListener('input', generate);
Comment thread
florianbachmann marked this conversation as resolved.
el.dataset.previewBound = '1';
});
}

return {
/**
* Initialize preview for a plugin.
* @param {string} id - The plugin_id (e.g. 'clock', 'countdown')
* @param {Object} [options] - Configuration options
* @param {Object} [options.required] - Map of field names to hint messages for client-side validation
* @param {string} [options.containerId='preview-widget'] - ID of the container element
*/
init(id, options = {}) {
pluginId = id;
requiredFields = options.required || {};
render(options.containerId || 'preview-widget');
Comment thread
florianbachmann marked this conversation as resolved.
listenToForm();
generate();
},

/** Trigger a preview refresh (call after dynamically adding form elements) */
refresh() {
listenToForm();
generate();
},

/** Refresh the "current" image (call after a successful Update Now) */
refreshCurrent() {
const img = document.getElementById('preview-current');
if (img) img.src = '/static/images/current_image.png?' + Date.now();
}
};
})();
1 change: 1 addition & 0 deletions src/templates/plugin.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<script src="{{ url_for('static', filename='scripts/dark_mode.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/response_modal.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/refresh_settings_manager.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/preview_manager.js') }}"></script>
<!-- Select2 CSS -->
<link href="{{ url_for('static', filename='styles/select2.min.css') }}" rel="stylesheet" />
<!-- jQuery -->
Expand Down