Move frame name/mode into the page header; Calendar gets its own tab
Frame name (pencil-icon inline edit) and the Photos/Calendar mode
selector now live in the page header, shared across all four per-frame
pages instead of being buried in the Configuration form -- so renaming
a frame or flipping its mode no longer requires navigating to a
specific tab first.
Calendar settings move out of a conditionally-hidden card on the
Configuration tab into their own dedicated tab (new /frames/{id}/
calendar route), visually greyed out when the frame is in Photos mode
but still fully usable so calendar settings can be configured ahead of
switching modes. Also wires the week-start setting into the UI for the
first time (the column/backend support landed earlier but had no
control anywhere).
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""The per-frame HTML pages: Photos (/frames/{id}), Configuration, and
|
||||
Stats tabs, all inside the sidebar app shell. Data loading happens
|
||||
client-side against /api/frames/{id}/... (routers/api_frames.py); these
|
||||
routes just authorize and render the scaffold."""
|
||||
"""The per-frame HTML pages: Photos (/frames/{id}), Configuration,
|
||||
Calendar, and Stats tabs, all inside the sidebar app shell. Data loading
|
||||
happens client-side against /api/frames/{id}/... (routers/api_frames.py);
|
||||
these routes just authorize and render the scaffold."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -73,8 +73,20 @@ def frame_config_page(frame_id: int, request: Request, db: Session = Depends(get
|
||||
default_palette_rgb=DEFAULT_PALETTE_RGB,
|
||||
palette_to_hex=palette_to_hex,
|
||||
display_mode_labels=DISPLAY_MODE_LABELS,
|
||||
)
|
||||
|
||||
|
||||
WEEK_START_LABELS = {0: "Monday", 1: "Tuesday", 2: "Wednesday", 3: "Thursday",
|
||||
4: "Friday", 5: "Saturday", 6: "Sunday"}
|
||||
|
||||
|
||||
@router.get("/frames/{frame_id}/calendar", response_class=HTMLResponse)
|
||||
def frame_calendar_page(frame_id: int, request: Request, db: Session = Depends(get_db)):
|
||||
return _frame_page(
|
||||
request, db, frame_id, "frame_calendar.html", "calendar",
|
||||
calendar_views=CALENDAR_VIEW_LABELS,
|
||||
calendar_users=_calendar_users_for_frame(db, frame_id),
|
||||
week_start_labels=WEEK_START_LABELS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Calendar tab: view/week-start/photo-inlay settings, per-user opt-in,
|
||||
// and the rendered preview. Extracted from frame_config.js when the
|
||||
// Calendar card became its own tab (window.FRAME_API is set by the
|
||||
// template; checkboxes are always sent explicitly as "true"/"false").
|
||||
|
||||
document.getElementById('calendar-config-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const body = new URLSearchParams({
|
||||
calendar_view: document.getElementById('calendar_view').value,
|
||||
calendar_week_start: document.getElementById('calendar_week_start').value,
|
||||
calendar_photo_inlay: String(document.getElementById('calendar_photo_inlay').checked),
|
||||
});
|
||||
try {
|
||||
const resp = await fetch(`${window.FRAME_API}/config`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body,
|
||||
});
|
||||
if (!resp.ok) throw new Error(await apiError(resp));
|
||||
showStatus(true, 'Saved.');
|
||||
loadCalendarPreview();
|
||||
} catch (e) {
|
||||
showStatus(false, e.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Each person's own opt-in -- auto-saves on toggle, not batched into the
|
||||
// form above, since it's the toggling user's own preference (see
|
||||
// api_frames.py's /calendar-included), not a frame-wide setting.
|
||||
document.querySelectorAll('.calendar-self-toggle').forEach((el) => {
|
||||
el.addEventListener('change', async () => {
|
||||
try {
|
||||
const resp = await fetch(`${window.FRAME_API}/calendar-included`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ included: el.checked }),
|
||||
});
|
||||
if (!resp.ok) throw new Error(await apiError(resp));
|
||||
showStatus(true, el.checked ? 'Your calendar is included on this frame.' : 'Your calendar removed from this frame.');
|
||||
} catch (e) {
|
||||
el.checked = !el.checked;
|
||||
showStatus(false, e.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function loadCalendarPreview() {
|
||||
document.getElementById('calendar-preview').src = `${window.FRAME_API}/preview/calendar?_=${Date.now()}`;
|
||||
}
|
||||
document.getElementById('calendar-preview-refresh').addEventListener('click', loadCalendarPreview);
|
||||
loadCalendarPreview();
|
||||
|
||||
async function takeControl() {
|
||||
try {
|
||||
const resp = await fetch(`${window.FRAME_API}/take-control`, { method: 'POST' });
|
||||
if (!resp.ok) throw new Error(await apiError(resp));
|
||||
showStatus(true, 'You have control now.');
|
||||
loadControl();
|
||||
} catch (e) {
|
||||
showStatus(false, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadControl() {
|
||||
const banner = document.getElementById('control-banner');
|
||||
try {
|
||||
const resp = await fetch(`${window.FRAME_API}/queue`);
|
||||
if (!resp.ok) return; // unconfigured frame: control still works via 409s
|
||||
const data = await resp.json();
|
||||
if (data.control && !data.control.you) {
|
||||
banner.style.display = 'flex';
|
||||
document.getElementById('control-holder').textContent = data.control.controller
|
||||
? `${data.control.controller} currently has control of this frame.`
|
||||
: 'Nobody has control of this frame yet.';
|
||||
} else {
|
||||
banner.style.display = 'none';
|
||||
}
|
||||
} catch (e) { /* banner is best-effort */ }
|
||||
}
|
||||
|
||||
document.getElementById('take-control').addEventListener('click', takeControl);
|
||||
loadControl();
|
||||
@@ -1,13 +1,13 @@
|
||||
// Configuration tab: frame settings + firmware card + take control.
|
||||
// window.FRAME_API is set by the template. Checkboxes are always sent
|
||||
// Frame name/mode live in the page header now (frame_header.js) and
|
||||
// Calendar settings have their own tab (frame_calendar.js). window.
|
||||
// FRAME_API is set by the template. Checkboxes are always sent
|
||||
// explicitly as "true"/"false" -- the server treats absent fields as
|
||||
// "leave unchanged", so a checkbox must never be simply omitted.
|
||||
|
||||
async function saveConfig() {
|
||||
const minutes = parseInt(document.getElementById('refresh_interval_minutes').value, 10) || 60;
|
||||
const body = new URLSearchParams({
|
||||
mode: document.getElementById('frame_mode').value,
|
||||
name: document.getElementById('frame_name').value || '',
|
||||
order: document.getElementById('order').value,
|
||||
orientation: document.getElementById('orientation').value,
|
||||
refresh_interval_s: String(minutes * 60),
|
||||
@@ -37,69 +37,6 @@ document.getElementById('config-form').addEventListener('submit', async (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ---- Calendar card: mode/view toggling, its own save, self opt-in, preview ----
|
||||
|
||||
const calendarCard = document.getElementById('calendar-card');
|
||||
if (calendarCard) {
|
||||
document.getElementById('frame_mode').addEventListener('change', () => {
|
||||
calendarCard.style.display = document.getElementById('frame_mode').value === 'calendar' ? 'block' : 'none';
|
||||
});
|
||||
|
||||
const inlayRow = document.getElementById('calendar-inlay-row');
|
||||
const inlayHint = document.getElementById('calendar-inlay-hint');
|
||||
document.getElementById('calendar_view').addEventListener('change', () => {
|
||||
const isAgenda = document.getElementById('calendar_view').value === 'agenda';
|
||||
inlayRow.style.display = isAgenda ? 'flex' : 'none';
|
||||
inlayHint.style.display = isAgenda ? 'block' : 'none';
|
||||
});
|
||||
|
||||
document.getElementById('calendar-config-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const body = new URLSearchParams({
|
||||
calendar_view: document.getElementById('calendar_view').value,
|
||||
calendar_photo_inlay: String(document.getElementById('calendar_photo_inlay').checked),
|
||||
});
|
||||
try {
|
||||
const resp = await fetch(`${window.FRAME_API}/config`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body,
|
||||
});
|
||||
if (!resp.ok) throw new Error(await apiError(resp));
|
||||
showStatus(true, 'Saved.');
|
||||
loadCalendarPreview();
|
||||
} catch (e) {
|
||||
showStatus(false, e.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Each person's own opt-in -- auto-saves on toggle, not batched into
|
||||
// the form above, since it's the toggling user's own preference (see
|
||||
// api_frames.py's /calendar-included), not a frame-wide setting.
|
||||
document.querySelectorAll('.calendar-self-toggle').forEach((el) => {
|
||||
el.addEventListener('change', async () => {
|
||||
try {
|
||||
const resp = await fetch(`${window.FRAME_API}/calendar-included`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ included: el.checked }),
|
||||
});
|
||||
if (!resp.ok) throw new Error(await apiError(resp));
|
||||
showStatus(true, el.checked ? 'Your calendar is included on this frame.' : 'Your calendar removed from this frame.');
|
||||
} catch (e) {
|
||||
el.checked = !el.checked;
|
||||
showStatus(false, e.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function loadCalendarPreview() {
|
||||
document.getElementById('calendar-preview').src = `${window.FRAME_API}/preview/calendar?_=${Date.now()}`;
|
||||
}
|
||||
document.getElementById('calendar-preview-refresh').addEventListener('click', loadCalendarPreview);
|
||||
loadCalendarPreview();
|
||||
}
|
||||
|
||||
async function takeControl() {
|
||||
try {
|
||||
const resp = await fetch(`${window.FRAME_API}/take-control`, { method: 'POST' });
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Page-header controls shared by every per-frame page (Photos/
|
||||
// Configuration/Calendar/Stats): the frame-name pencil-edit and the
|
||||
// mode selector, both now living outside the tab structure since they
|
||||
// apply regardless of which tab is open. Depends on window.FRAME_API
|
||||
// (set per-page) and common.js's showStatus/apiError.
|
||||
|
||||
(function () {
|
||||
var view = document.getElementById('frame-name-view');
|
||||
var editRow = document.getElementById('frame-name-edit-row');
|
||||
var pencil = document.getElementById('frame-name-pencil');
|
||||
var input = document.getElementById('frame-name-input');
|
||||
var textEl = document.getElementById('frame-name-text');
|
||||
var saveBtn = document.getElementById('frame-name-save');
|
||||
var cancelBtn = document.getElementById('frame-name-cancel');
|
||||
if (!view || !window.FRAME_API) return;
|
||||
|
||||
function openEdit() {
|
||||
input.value = textEl.textContent.trim();
|
||||
view.style.display = 'none';
|
||||
editRow.style.display = 'inline-flex';
|
||||
input.focus();
|
||||
input.select();
|
||||
}
|
||||
function closeEdit() {
|
||||
editRow.style.display = 'none';
|
||||
view.style.display = 'inline-flex';
|
||||
}
|
||||
|
||||
pencil.addEventListener('click', openEdit);
|
||||
cancelBtn.addEventListener('click', closeEdit);
|
||||
|
||||
async function save() {
|
||||
var name = input.value.trim();
|
||||
if (!name || name === textEl.textContent.trim()) {
|
||||
closeEdit();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(`${window.FRAME_API}/config`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ name }),
|
||||
});
|
||||
if (!resp.ok) throw new Error(await apiError(resp));
|
||||
textEl.textContent = name;
|
||||
closeEdit();
|
||||
showStatus(true, 'Renamed.');
|
||||
} catch (e) {
|
||||
showStatus(false, e.message);
|
||||
}
|
||||
}
|
||||
saveBtn.addEventListener('click', save);
|
||||
input.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') save();
|
||||
if (e.key === 'Escape') closeEdit();
|
||||
});
|
||||
})();
|
||||
|
||||
(function () {
|
||||
var sel = document.getElementById('frame-mode-select');
|
||||
if (!sel || !window.FRAME_API) return;
|
||||
var previous = sel.value;
|
||||
|
||||
sel.addEventListener('change', async function () {
|
||||
var mode = sel.value;
|
||||
try {
|
||||
const resp = await fetch(`${window.FRAME_API}/config`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ mode }),
|
||||
});
|
||||
if (!resp.ok) throw new Error(await apiError(resp));
|
||||
previous = mode;
|
||||
showStatus(true, mode === 'calendar' ? 'Switched to Calendar mode.' : 'Switched to Photos mode.');
|
||||
var calTab = document.querySelector('.tabs a[href$="/calendar"]');
|
||||
if (calTab) calTab.classList.toggle('tab-disabled', mode !== 'calendar');
|
||||
} catch (e) {
|
||||
sel.value = previous;
|
||||
showStatus(false, e.message);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -468,6 +468,35 @@ code {
|
||||
}
|
||||
.tabs a:hover { color: var(--text); }
|
||||
.tabs a.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
|
||||
.tabs a.tab-disabled { opacity: 0.45; }
|
||||
.tabs a.tab-disabled:hover { opacity: 0.7; }
|
||||
|
||||
.frame-name-view { display: inline-flex; align-items: center; gap: 6px; }
|
||||
.frame-name-pencil {
|
||||
background: none;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
padding: 4px;
|
||||
margin: 0;
|
||||
opacity: 0.55;
|
||||
color: var(--text);
|
||||
transition: opacity .12s ease, background-color .12s ease;
|
||||
}
|
||||
.frame-name-pencil:hover { opacity: 1; background: var(--surface-alt); border-radius: 6px; }
|
||||
.frame-name-edit { display: inline-flex; align-items: center; gap: 6px; }
|
||||
.frame-name-edit input {
|
||||
width: auto;
|
||||
margin-top: 0;
|
||||
padding: 5px 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.frame-name-edit button { margin-top: 0; }
|
||||
|
||||
.frame-mode-select { width: auto; margin-top: 0; padding: 7px 10px; font-size: 13px; font-weight: 600; }
|
||||
|
||||
.control-banner {
|
||||
display: flex;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<select id="frame-mode-select" class="frame-mode-select" title="Frame mode" aria-label="Frame mode">
|
||||
<option value="photos" {% if frame.mode == "photos" %}selected{% endif %}>Photos</option>
|
||||
<option value="calendar" {% if frame.mode == "calendar" %}selected{% endif %}>Calendar</option>
|
||||
</select>
|
||||
@@ -0,0 +1,9 @@
|
||||
<span class="frame-name-view" id="frame-name-view">
|
||||
<span id="frame-name-text">{{ frame.name or ("Frame " ~ frame.id) }}</span>
|
||||
<button type="button" class="frame-name-pencil" id="frame-name-pencil" title="Rename frame" aria-label="Rename frame">✎</button>
|
||||
</span>
|
||||
<span class="frame-name-edit" id="frame-name-edit-row" style="display: none;">
|
||||
<input type="text" id="frame-name-input" maxlength="64" value="{{ frame.name }}">
|
||||
<button type="button" class="btn-inline" id="frame-name-save">Save</button>
|
||||
<button type="button" class="btn-inline secondary" id="frame-name-cancel">Cancel</button>
|
||||
</span>
|
||||
@@ -1,5 +1,7 @@
|
||||
<nav class="tabs">
|
||||
<a href="/frames/{{ frame.id }}" class="{% if active_tab == 'photos' %}active{% endif %}">Photos</a>
|
||||
<a href="/frames/{{ frame.id }}/config" class="{% if active_tab == 'config' %}active{% endif %}">Configuration</a>
|
||||
<a href="/frames/{{ frame.id }}/calendar"
|
||||
class="{% if active_tab == 'calendar' %}active{% endif %} {% if frame.mode != 'calendar' %}tab-disabled{% endif %}">Calendar</a>
|
||||
<a href="/frames/{{ frame.id }}/stats" class="{% if active_tab == 'stats' %}active{% endif %}">Stats</a>
|
||||
</nav>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
{% extends "app_base.html" %}
|
||||
|
||||
{% block title %}{{ frame.name or "Frame" }} · Calendar{% endblock %}
|
||||
{% block page_title %}{% include "_frame_name_edit.html" %}{% endblock %}
|
||||
{% block head_actions %}{% include "_frame_mode_select.html" %}{% endblock %}
|
||||
|
||||
{% block device_status %}{% include "_device_status_bar.html" %}{% endblock %}
|
||||
{% block tabs %}{% include "_frame_tabs.html" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="control-banner" class="control-banner" style="display: none;">
|
||||
<span id="control-holder"></span>
|
||||
<button type="button" id="take-control" class="btn-inline">Take control</button>
|
||||
</div>
|
||||
|
||||
{% if frame.mode != 'calendar' %}
|
||||
<div class="info-box">This frame is currently in <strong>Photos</strong> mode --
|
||||
settings below take effect once you switch it to <strong>Calendar</strong> mode
|
||||
using the selector at the top of the page.</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="layout">
|
||||
<div class="main-col">
|
||||
<section class="card">
|
||||
<h2 class="card-title">Calendar</h2>
|
||||
<form id="calendar-config-form">
|
||||
<label>View
|
||||
<select id="calendar_view">
|
||||
{% for value, label in calendar_views.items() %}
|
||||
<option value="{{ value }}" {% if frame.calendar_view == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>Week starts on
|
||||
<select id="calendar_week_start">
|
||||
{% for value, label in week_start_labels.items() %}
|
||||
<option value="{{ value }}" {% if frame.calendar_week_start == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<p class="sub" style="margin-top: 4px;">Only affects the Week and Month views.</p>
|
||||
<div class="checkbox-row" id="calendar-inlay-row">
|
||||
<input type="checkbox" id="calendar_photo_inlay" {% if frame.calendar_photo_inlay %}checked{% endif %}>
|
||||
<label for="calendar_photo_inlay">Show a photo alongside the calendar</label>
|
||||
</div>
|
||||
<p class="sub" id="calendar-inlay-hint" style="margin-top: 4px;">
|
||||
Uses the same album configured on the Photos tab -- nothing extra to set up.</p>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
|
||||
<h2 class="card-title" style="margin-top: 20px;">Included calendars</h2>
|
||||
<p class="sub">Each linked person decides whether their own calendar
|
||||
contributes to this frame -- being linked here doesn't include it
|
||||
automatically.</p>
|
||||
<ul class="calendar-user-list">
|
||||
{% for u in calendar_users %}
|
||||
<li>
|
||||
{% if u.user_id == user.id %}
|
||||
{% if u.has_url %}
|
||||
<label class="checkbox-row" style="margin-top: 6px;">
|
||||
<input type="checkbox" class="calendar-self-toggle" {% if u.included %}checked{% endif %}>
|
||||
{{ u.display_name }} (you)
|
||||
</label>
|
||||
{% else %}
|
||||
<p class="sub" style="margin-top: 6px;">{{ u.display_name }} (you) -- no calendar set, add one in <a href="/settings">Settings</a>.</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="sub" style="margin-top: 6px;">{{ u.display_name }}:
|
||||
{% if not u.has_url %}no calendar set{% elif u.included %}included{% else %}not included{% endif %}</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% if frame.calendar_fetch_summary %}
|
||||
<p class="sub" style="margin-top: 8px; color: var(--danger-text);">Last fetch: {{ frame.calendar_fetch_summary }}</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="side-col">
|
||||
<section class="card">
|
||||
<h2 class="card-title">Preview</h2>
|
||||
<p class="sub">How this frame's calendar currently renders.</p>
|
||||
<img class="preview-img" id="calendar-preview" alt="Calendar preview">
|
||||
<button type="button" class="secondary" id="calendar-preview-refresh">Refresh preview</button>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="result"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>window.FRAME_API = {{ ("/api/frames/" ~ frame.id) | tojson }};</script>
|
||||
<script src="/static/device_status_bar.js"></script>
|
||||
<script src="/static/frame_header.js"></script>
|
||||
<script src="/static/frame_calendar.js"></script>
|
||||
{% endblock %}
|
||||
@@ -1,7 +1,8 @@
|
||||
{% extends "app_base.html" %}
|
||||
|
||||
{% block title %}{{ frame.name or "Frame" }} · Configuration{% endblock %}
|
||||
{% block page_title %}{{ frame.name or "Frame " ~ frame.id }}{% endblock %}
|
||||
{% block page_title %}{% include "_frame_name_edit.html" %}{% endblock %}
|
||||
{% block head_actions %}{% include "_frame_mode_select.html" %}{% endblock %}
|
||||
|
||||
{% block device_status %}{% include "_device_status_bar.html" %}{% endblock %}
|
||||
{% block tabs %}{% include "_frame_tabs.html" %}{% endblock %}
|
||||
@@ -17,15 +18,6 @@
|
||||
<section class="card">
|
||||
<h2 class="card-title">Display settings</h2>
|
||||
<form id="config-form">
|
||||
<label>Frame mode
|
||||
<select id="frame_mode">
|
||||
<option value="photos" {% if frame.mode == "photos" %}selected{% endif %}>Photos</option>
|
||||
<option value="calendar" {% if frame.mode == "calendar" %}selected{% endif %}>Calendar</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Frame name
|
||||
<input type="text" id="frame_name" maxlength="64" value="{{ frame.name }}">
|
||||
</label>
|
||||
<label>Order
|
||||
<select id="order">
|
||||
<option value="sequential" {% if frame.order == "sequential" %}selected{% endif %}>Sequential</option>
|
||||
@@ -83,59 +75,6 @@
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card" id="calendar-card" style="{% if frame.mode != 'calendar' %}display: none;{% endif %}">
|
||||
<h2 class="card-title">Calendar</h2>
|
||||
<form id="calendar-config-form">
|
||||
<label>View
|
||||
<select id="calendar_view">
|
||||
{% for value, label in calendar_views.items() %}
|
||||
<option value="{{ value }}" {% if frame.calendar_view == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<div class="checkbox-row" id="calendar-inlay-row" style="{% if frame.calendar_view != 'agenda' %}display: none;{% endif %}">
|
||||
<input type="checkbox" id="calendar_photo_inlay" {% if frame.calendar_photo_inlay %}checked{% endif %}>
|
||||
<label for="calendar_photo_inlay">Show a photo alongside today's agenda</label>
|
||||
</div>
|
||||
<p class="sub" id="calendar-inlay-hint" style="margin-top: 4px; {% if frame.calendar_view != 'agenda' %}display: none;{% endif %}">
|
||||
Uses the same album configured on the Photos tab -- nothing extra to set up.</p>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
|
||||
<h2 class="card-title" style="margin-top: 20px;">Included calendars</h2>
|
||||
<p class="sub">Each linked person decides whether their own calendar
|
||||
contributes to this frame -- being linked here doesn't include it
|
||||
automatically.</p>
|
||||
<ul class="calendar-user-list">
|
||||
{% for u in calendar_users %}
|
||||
<li>
|
||||
{% if u.user_id == user.id %}
|
||||
{% if u.has_url %}
|
||||
<label class="checkbox-row" style="margin-top: 6px;">
|
||||
<input type="checkbox" class="calendar-self-toggle" {% if u.included %}checked{% endif %}>
|
||||
{{ u.display_name }} (you)
|
||||
</label>
|
||||
{% else %}
|
||||
<p class="sub" style="margin-top: 6px;">{{ u.display_name }} (you) -- no calendar set, add one in <a href="/settings">Settings</a>.</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="sub" style="margin-top: 6px;">{{ u.display_name }}:
|
||||
{% if not u.has_url %}no calendar set{% elif u.included %}included{% else %}not included{% endif %}</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% if frame.calendar_fetch_summary %}
|
||||
<p class="sub" style="margin-top: 8px; color: var(--danger-text);">Last fetch: {{ frame.calendar_fetch_summary }}</p>
|
||||
{% endif %}
|
||||
|
||||
<h2 class="card-title" style="margin-top: 20px;">Preview</h2>
|
||||
<p class="sub">How this frame's calendar currently renders.</p>
|
||||
<img class="preview-img" id="calendar-preview" alt="Calendar preview">
|
||||
<button type="button" class="secondary" id="calendar-preview-refresh">Refresh preview</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="side-col">
|
||||
@@ -267,5 +206,6 @@
|
||||
window.DEFAULT_PALETTE_HEX = {{ palette_to_hex(default_palette_rgb) | tojson }};
|
||||
</script>
|
||||
<script src="/static/device_status_bar.js"></script>
|
||||
<script src="/static/frame_header.js"></script>
|
||||
<script src="/static/frame_config.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{% extends "app_base.html" %}
|
||||
|
||||
{% block title %}{{ frame.name or "Frame" }} · Photos{% endblock %}
|
||||
{% block page_title %}{{ frame.name or "Frame " ~ frame.id }}{% endblock %}
|
||||
{% block page_title %}{% include "_frame_name_edit.html" %}{% endblock %}
|
||||
{% block head_actions %}{% include "_frame_mode_select.html" %}{% endblock %}
|
||||
|
||||
{% block device_status %}{% include "_device_status_bar.html" %}{% endblock %}
|
||||
{% block tabs %}{% include "_frame_tabs.html" %}{% endblock %}
|
||||
@@ -57,6 +58,7 @@
|
||||
{% block scripts %}
|
||||
<script>window.FRAME_API = {{ ("/api/frames/" ~ frame.id) | tojson }};</script>
|
||||
<script src="/static/device_status_bar.js"></script>
|
||||
<script src="/static/frame_header.js"></script>
|
||||
<script src="/static/queue.js"></script>
|
||||
<script src="/static/frame_photos.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{% extends "app_base.html" %}
|
||||
|
||||
{% block title %}{{ frame.name or "Frame" }} · Stats{% endblock %}
|
||||
{% block page_title %}{{ frame.name or "Frame " ~ frame.id }}{% endblock %}
|
||||
{% block page_title %}{% include "_frame_name_edit.html" %}{% endblock %}
|
||||
{% block head_actions %}{% include "_frame_mode_select.html" %}{% endblock %}
|
||||
|
||||
{% block device_status %}{% include "_device_status_bar.html" %}{% endblock %}
|
||||
{% block tabs %}{% include "_frame_tabs.html" %}{% endblock %}
|
||||
@@ -24,5 +25,6 @@
|
||||
<script>window.FRAME_API = {{ ("/api/frames/" ~ frame.id) | tojson }};</script>
|
||||
<script src="/static/battery_chart.js"></script>
|
||||
<script src="/static/device_status_bar.js"></script>
|
||||
<script src="/static/frame_header.js"></script>
|
||||
<script src="/static/frame_stats.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user