Release 1.6.1
This commit is contained in:
14
README.md
14
README.md
@@ -191,13 +191,12 @@ The player keeps itself up-to-date automatically:
|
||||
|
||||
Each display can optionally show a **bottom ticker tape** with scrolling news headlines.
|
||||
|
||||
Configure it as a company user via:
|
||||
Configure RSS + styling as a company user via:
|
||||
|
||||
- **Dashboard → Displays → Configure display → Ticker tape**
|
||||
- **My Company → Ticker tape (RSS)**
|
||||
|
||||
Options:
|
||||
Company-level options:
|
||||
|
||||
- Enable/disable ticker entirely
|
||||
- RSS feed URL (public http/https)
|
||||
- Text color (picker)
|
||||
- Background color + opacity
|
||||
@@ -205,10 +204,14 @@ Options:
|
||||
- Font size
|
||||
- Speed
|
||||
|
||||
Per-display option:
|
||||
|
||||
- Enable/disable ticker on that display (Dashboard → Displays → Configure display)
|
||||
|
||||
Implementation notes:
|
||||
|
||||
- Headlines are fetched server-side via `GET /api/display/<token>/ticker` and cached briefly.
|
||||
- The player applies styles and refreshes headlines periodically.
|
||||
- The player reads the company ticker settings via `GET /api/display/<token>/playlist` and refreshes headlines periodically.
|
||||
|
||||
## SMTP / Forgot password
|
||||
|
||||
@@ -286,5 +289,6 @@ If the reset email is not received:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -128,6 +128,29 @@ def create_app():
|
||||
db.session.execute(db.text("ALTER TABLE company ADD COLUMN overlay_file_path VARCHAR(400)"))
|
||||
db.session.commit()
|
||||
|
||||
# Companies: ticker tape settings (RSS + styling)
|
||||
if "ticker_rss_url" not in company_cols:
|
||||
db.session.execute(db.text("ALTER TABLE company ADD COLUMN ticker_rss_url VARCHAR(1000)"))
|
||||
db.session.commit()
|
||||
if "ticker_color" not in company_cols:
|
||||
db.session.execute(db.text("ALTER TABLE company ADD COLUMN ticker_color VARCHAR(32)"))
|
||||
db.session.commit()
|
||||
if "ticker_bg_color" not in company_cols:
|
||||
db.session.execute(db.text("ALTER TABLE company ADD COLUMN ticker_bg_color VARCHAR(32)"))
|
||||
db.session.commit()
|
||||
if "ticker_bg_opacity" not in company_cols:
|
||||
db.session.execute(db.text("ALTER TABLE company ADD COLUMN ticker_bg_opacity INTEGER"))
|
||||
db.session.commit()
|
||||
if "ticker_font_family" not in company_cols:
|
||||
db.session.execute(db.text("ALTER TABLE company ADD COLUMN ticker_font_family VARCHAR(120)"))
|
||||
db.session.commit()
|
||||
if "ticker_font_size_px" not in company_cols:
|
||||
db.session.execute(db.text("ALTER TABLE company ADD COLUMN ticker_font_size_px INTEGER"))
|
||||
db.session.commit()
|
||||
if "ticker_speed" not in company_cols:
|
||||
db.session.execute(db.text("ALTER TABLE company ADD COLUMN ticker_speed INTEGER"))
|
||||
db.session.commit()
|
||||
|
||||
# AppSettings: create settings table if missing.
|
||||
# (PRAGMA returns empty if the table doesn't exist.)
|
||||
settings_cols = [
|
||||
|
||||
23
app/cli.py
23
app/cli.py
@@ -65,6 +65,29 @@ def _ensure_schema_and_settings() -> None:
|
||||
db.session.execute(db.text("ALTER TABLE company ADD COLUMN overlay_file_path VARCHAR(400)"))
|
||||
db.session.commit()
|
||||
|
||||
# Companies: ticker tape settings (RSS + styling)
|
||||
if "ticker_rss_url" not in company_cols:
|
||||
db.session.execute(db.text("ALTER TABLE company ADD COLUMN ticker_rss_url VARCHAR(1000)"))
|
||||
db.session.commit()
|
||||
if "ticker_color" not in company_cols:
|
||||
db.session.execute(db.text("ALTER TABLE company ADD COLUMN ticker_color VARCHAR(32)"))
|
||||
db.session.commit()
|
||||
if "ticker_bg_color" not in company_cols:
|
||||
db.session.execute(db.text("ALTER TABLE company ADD COLUMN ticker_bg_color VARCHAR(32)"))
|
||||
db.session.commit()
|
||||
if "ticker_bg_opacity" not in company_cols:
|
||||
db.session.execute(db.text("ALTER TABLE company ADD COLUMN ticker_bg_opacity INTEGER"))
|
||||
db.session.commit()
|
||||
if "ticker_font_family" not in company_cols:
|
||||
db.session.execute(db.text("ALTER TABLE company ADD COLUMN ticker_font_family VARCHAR(120)"))
|
||||
db.session.commit()
|
||||
if "ticker_font_size_px" not in company_cols:
|
||||
db.session.execute(db.text("ALTER TABLE company ADD COLUMN ticker_font_size_px INTEGER"))
|
||||
db.session.commit()
|
||||
if "ticker_speed" not in company_cols:
|
||||
db.session.execute(db.text("ALTER TABLE company ADD COLUMN ticker_speed INTEGER"))
|
||||
db.session.commit()
|
||||
|
||||
settings_cols = [r[1] for r in db.session.execute(db.text("PRAGMA table_info(app_settings)")).fetchall()]
|
||||
if settings_cols and "public_domain" not in settings_cols:
|
||||
db.session.execute(db.text("ALTER TABLE app_settings ADD COLUMN public_domain VARCHAR(255)"))
|
||||
|
||||
@@ -20,6 +20,16 @@ class Company(db.Model):
|
||||
# Example: uploads/<company_id>/overlay_<uuid>.png
|
||||
overlay_file_path = db.Column(db.String(400), nullable=True)
|
||||
|
||||
# Optional per-company ticker tape (RSS headlines) settings.
|
||||
# Displays can enable/disable the ticker individually.
|
||||
ticker_rss_url = db.Column(db.String(1000), nullable=True)
|
||||
ticker_color = db.Column(db.String(32), nullable=True)
|
||||
ticker_bg_color = db.Column(db.String(32), nullable=True)
|
||||
ticker_bg_opacity = db.Column(db.Integer, nullable=True) # 0-100
|
||||
ticker_font_family = db.Column(db.String(120), nullable=True)
|
||||
ticker_font_size_px = db.Column(db.Integer, nullable=True)
|
||||
ticker_speed = db.Column(db.Integer, nullable=True) # 1-100
|
||||
|
||||
users = db.relationship("User", back_populates="company", cascade="all, delete-orphan")
|
||||
displays = db.relationship("Display", back_populates="company", cascade="all, delete-orphan")
|
||||
playlists = db.relationship("Playlist", back_populates="company", cascade="all, delete-orphan")
|
||||
|
||||
@@ -303,10 +303,11 @@ def display_playlist(token: str):
|
||||
if not display:
|
||||
abort(404)
|
||||
|
||||
company = Company.query.filter_by(id=display.company_id).first()
|
||||
|
||||
# Optional overlay URL (per-company) when enabled on this display.
|
||||
overlay_src = None
|
||||
if display.show_overlay:
|
||||
company = Company.query.filter_by(id=display.company_id).first()
|
||||
if company and company.overlay_file_path and is_valid_upload_relpath(company.overlay_file_path):
|
||||
overlay_src = url_for("static", filename=company.overlay_file_path)
|
||||
|
||||
@@ -317,15 +318,16 @@ def display_playlist(token: str):
|
||||
if not ok:
|
||||
return resp
|
||||
|
||||
# Ticker settings are configured per-company; displays can enable/disable individually.
|
||||
ticker_cfg = {
|
||||
"enabled": bool(display.ticker_enabled),
|
||||
"rss_url": display.ticker_rss_url,
|
||||
"color": display.ticker_color,
|
||||
"bg_color": display.ticker_bg_color,
|
||||
"bg_opacity": display.ticker_bg_opacity,
|
||||
"font_family": display.ticker_font_family,
|
||||
"font_size_px": display.ticker_font_size_px,
|
||||
"speed": display.ticker_speed,
|
||||
"rss_url": (company.ticker_rss_url if company else None),
|
||||
"color": (company.ticker_color if company else None),
|
||||
"bg_color": (company.ticker_bg_color if company else None),
|
||||
"bg_opacity": (company.ticker_bg_opacity if company else None),
|
||||
"font_family": (company.ticker_font_family if company else None),
|
||||
"font_size_px": (company.ticker_font_size_px if company else None),
|
||||
"speed": (company.ticker_speed if company else None),
|
||||
}
|
||||
|
||||
# Determine active playlists. If display_playlist has any rows, use those.
|
||||
@@ -432,6 +434,8 @@ def display_ticker(token: str):
|
||||
if not display:
|
||||
abort(404)
|
||||
|
||||
company = Company.query.filter_by(id=display.company_id).first()
|
||||
|
||||
# Enforce concurrent session limit the same way as /playlist.
|
||||
sid = request.args.get("sid")
|
||||
ok, resp = _enforce_and_touch_display_session(display, sid)
|
||||
@@ -441,7 +445,7 @@ def display_ticker(token: str):
|
||||
if not display.ticker_enabled:
|
||||
return jsonify({"enabled": False, "headlines": []})
|
||||
|
||||
rss_url = (display.ticker_rss_url or "").strip()
|
||||
rss_url = ((company.ticker_rss_url if company else None) or "").strip()
|
||||
if not rss_url:
|
||||
return jsonify({"enabled": True, "headlines": []})
|
||||
|
||||
|
||||
@@ -315,6 +315,44 @@ def _storage_limit_error_message(*, storage_max_human: str | None) -> str:
|
||||
return "Storage limit reached. Please delete items to free space."
|
||||
|
||||
|
||||
def _normalize_css_color(val: str | None) -> str | None:
|
||||
"""Accept a limited set of CSS color inputs (primarily hex + a few keywords)."""
|
||||
|
||||
v = (val or "").strip()
|
||||
if not v:
|
||||
return None
|
||||
|
||||
low = v.lower()
|
||||
if low in {"white", "black", "red", "green", "blue", "yellow", "orange", "purple", "gray", "grey"}:
|
||||
return low
|
||||
|
||||
if low.startswith("#"):
|
||||
h = low[1:]
|
||||
if len(h) in {3, 6, 8} and all(c in "0123456789abcdef" for c in h):
|
||||
return "#" + h
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_font_family(val: str | None) -> str | None:
|
||||
v = (val or "").strip()
|
||||
if not v:
|
||||
return None
|
||||
v = v.replace("\n", " ").replace("\r", " ").replace('"', "").replace("'", "")
|
||||
v = " ".join(v.split())
|
||||
return v[:120] if v else None
|
||||
|
||||
|
||||
def _normalize_int(val, *, min_val: int, max_val: int) -> int | None:
|
||||
if val in (None, ""):
|
||||
return None
|
||||
try:
|
||||
n = int(val)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return min(max_val, max(min_val, n))
|
||||
|
||||
|
||||
@bp.get("/my-company")
|
||||
@login_required
|
||||
def my_company():
|
||||
@@ -376,6 +414,37 @@ def my_company():
|
||||
)
|
||||
|
||||
|
||||
@bp.post("/my-company/ticker")
|
||||
@login_required
|
||||
def update_company_ticker_settings():
|
||||
"""Update per-company ticker tape settings.
|
||||
|
||||
Note: per-display enable/disable remains on the Display model.
|
||||
"""
|
||||
|
||||
company_user_required()
|
||||
|
||||
company = db.session.get(Company, current_user.company_id)
|
||||
if not company:
|
||||
abort(404)
|
||||
|
||||
rss_url = (request.form.get("ticker_rss_url") or "").strip() or None
|
||||
if rss_url is not None:
|
||||
rss_url = rss_url[:1000]
|
||||
|
||||
company.ticker_rss_url = rss_url
|
||||
company.ticker_color = _normalize_css_color(request.form.get("ticker_color"))
|
||||
company.ticker_bg_color = _normalize_css_color(request.form.get("ticker_bg_color"))
|
||||
company.ticker_bg_opacity = _normalize_int(request.form.get("ticker_bg_opacity"), min_val=0, max_val=100)
|
||||
company.ticker_font_family = _normalize_font_family(request.form.get("ticker_font_family"))
|
||||
company.ticker_font_size_px = _normalize_int(request.form.get("ticker_font_size_px"), min_val=10, max_val=200)
|
||||
company.ticker_speed = _normalize_int(request.form.get("ticker_speed"), min_val=1, max_val=100)
|
||||
|
||||
db.session.commit()
|
||||
flash("Ticker settings updated.", "success")
|
||||
return redirect(url_for("company.my_company"))
|
||||
|
||||
|
||||
@bp.post("/my-company/overlay")
|
||||
@login_required
|
||||
def upload_company_overlay():
|
||||
@@ -1268,7 +1337,8 @@ def update_display(display_id: int):
|
||||
if raw is not None:
|
||||
display.show_overlay = (raw or "").strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
# Ticker tape settings
|
||||
# Ticker tape: per-display enable/disable only.
|
||||
# RSS + styling is configured per-company (see /my-company).
|
||||
if request.is_json:
|
||||
if payload is None:
|
||||
return _json_error("Invalid JSON")
|
||||
@@ -1282,61 +1352,11 @@ def update_display(display_id: int):
|
||||
else:
|
||||
s = ("" if raw is None else str(raw)).strip().lower()
|
||||
display.ticker_enabled = s in {"1", "true", "yes", "on"}
|
||||
|
||||
if "ticker_rss_url" in payload:
|
||||
u = (payload.get("ticker_rss_url") or "").strip() or None
|
||||
# Keep within column limit and avoid whitespace-only.
|
||||
if u is not None:
|
||||
u = u[:1000]
|
||||
display.ticker_rss_url = u
|
||||
|
||||
if "ticker_color" in payload:
|
||||
display.ticker_color = _normalize_css_color(payload.get("ticker_color"))
|
||||
|
||||
if "ticker_bg_color" in payload:
|
||||
display.ticker_bg_color = _normalize_css_color(payload.get("ticker_bg_color"))
|
||||
|
||||
if "ticker_bg_opacity" in payload:
|
||||
display.ticker_bg_opacity = _normalize_percent(payload.get("ticker_bg_opacity"))
|
||||
|
||||
if "ticker_font_family" in payload:
|
||||
display.ticker_font_family = _normalize_font_family(payload.get("ticker_font_family"))
|
||||
|
||||
if "ticker_font_size_px" in payload:
|
||||
display.ticker_font_size_px = _normalize_font_size_px(payload.get("ticker_font_size_px"))
|
||||
|
||||
if "ticker_speed" in payload:
|
||||
display.ticker_speed = _normalize_speed(payload.get("ticker_speed"))
|
||||
else:
|
||||
# Form POST implies full update
|
||||
raw = request.form.get("ticker_enabled")
|
||||
if raw is not None:
|
||||
display.ticker_enabled = (raw or "").strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
if "ticker_rss_url" in request.form:
|
||||
u = (request.form.get("ticker_rss_url") or "").strip() or None
|
||||
if u is not None:
|
||||
u = u[:1000]
|
||||
display.ticker_rss_url = u
|
||||
|
||||
if "ticker_color" in request.form:
|
||||
display.ticker_color = _normalize_css_color(request.form.get("ticker_color"))
|
||||
|
||||
if "ticker_bg_color" in request.form:
|
||||
display.ticker_bg_color = _normalize_css_color(request.form.get("ticker_bg_color"))
|
||||
|
||||
if "ticker_bg_opacity" in request.form:
|
||||
display.ticker_bg_opacity = _normalize_percent(request.form.get("ticker_bg_opacity"))
|
||||
|
||||
if "ticker_font_family" in request.form:
|
||||
display.ticker_font_family = _normalize_font_family(request.form.get("ticker_font_family"))
|
||||
|
||||
if "ticker_font_size_px" in request.form:
|
||||
display.ticker_font_size_px = _normalize_font_size_px(request.form.get("ticker_font_size_px"))
|
||||
|
||||
if "ticker_speed" in request.form:
|
||||
display.ticker_speed = _normalize_speed(request.form.get("ticker_speed"))
|
||||
|
||||
# Playlist assignment
|
||||
if request.is_json:
|
||||
if "playlist_id" in payload:
|
||||
@@ -1379,13 +1399,13 @@ def update_display(display_id: int):
|
||||
"transition": display.transition,
|
||||
"show_overlay": bool(display.show_overlay),
|
||||
"ticker_enabled": bool(display.ticker_enabled),
|
||||
"ticker_rss_url": display.ticker_rss_url,
|
||||
"ticker_color": display.ticker_color,
|
||||
"ticker_bg_color": display.ticker_bg_color,
|
||||
"ticker_bg_opacity": display.ticker_bg_opacity,
|
||||
"ticker_font_family": display.ticker_font_family,
|
||||
"ticker_font_size_px": display.ticker_font_size_px,
|
||||
"ticker_speed": display.ticker_speed,
|
||||
"ticker_rss_url": None,
|
||||
"ticker_color": None,
|
||||
"ticker_bg_color": None,
|
||||
"ticker_bg_opacity": None,
|
||||
"ticker_font_family": None,
|
||||
"ticker_font_size_px": None,
|
||||
"ticker_speed": None,
|
||||
"assigned_playlist_id": display.assigned_playlist_id,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -104,13 +104,6 @@
|
||||
data-current-transition="{{ d.transition or 'none' }}"
|
||||
data-current-show-overlay="{{ '1' if d.show_overlay else '0' }}"
|
||||
data-current-ticker-enabled="{{ '1' if d.ticker_enabled else '0' }}"
|
||||
data-current-ticker-rss-url="{{ d.ticker_rss_url or '' }}"
|
||||
data-current-ticker-color="{{ d.ticker_color or '' }}"
|
||||
data-current-ticker-bg-color="{{ d.ticker_bg_color or '' }}"
|
||||
data-current-ticker-bg-opacity="{{ d.ticker_bg_opacity or '' }}"
|
||||
data-current-ticker-font-family="{{ d.ticker_font_family or '' }}"
|
||||
data-current-ticker-font-size-px="{{ d.ticker_font_size_px or '' }}"
|
||||
data-current-ticker-speed="{{ d.ticker_speed or '' }}"
|
||||
data-legacy-playlist-id="{{ d.assigned_playlist_id or '' }}"
|
||||
data-active-playlist-ids="{{ d.display_playlists | map(attribute='playlist_id') | list | join(',') }}"
|
||||
>
|
||||
@@ -179,61 +172,13 @@
|
||||
<div class="form-text">If your company has an overlay uploaded, it will be displayed on top of the content.</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="border: 1px solid rgba(0,0,0,0.10);">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="fw-bold">Ticker tape</div>
|
||||
<div class="text-muted small">Scroll RSS headlines at the bottom of the display.</div>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="editTickerEnabled" />
|
||||
<label class="form-check-label" for="editTickerEnabled">Enabled</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<label class="form-label" for="editTickerRssUrl">RSS feed URL</label>
|
||||
<input class="form-control" id="editTickerRssUrl" type="url" placeholder="https://example.com/feed.xml" />
|
||||
<div class="form-text">Tip: use a public RSS/Atom feed. Headlines are fetched server-side.</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mt-2">
|
||||
<div class="col-12 col-md-5">
|
||||
<label class="form-label" for="editTickerColor">Text color</label>
|
||||
<input class="form-control form-control-color" id="editTickerColor" type="color" value="#ffffff" title="Choose text color" />
|
||||
</div>
|
||||
<div class="col-12 col-md-5">
|
||||
<label class="form-label" for="editTickerFontSize">Font size (px)</label>
|
||||
<input class="form-control" id="editTickerFontSize" type="number" min="10" max="200" step="1" placeholder="28" />
|
||||
</div>
|
||||
<div class="col-12 col-md-7">
|
||||
<label class="form-label" for="editTickerFontFamily">Font</label>
|
||||
<select class="form-select" id="editTickerFontFamily">
|
||||
<option value="system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif">System (default)</option>
|
||||
<option value="Arial, Helvetica, sans-serif">Arial</option>
|
||||
<option value="Segoe UI, Arial, sans-serif">Segoe UI</option>
|
||||
<option value="Roboto, Arial, sans-serif">Roboto</option>
|
||||
<option value="Georgia, serif">Georgia</option>
|
||||
<option value="Times New Roman, Times, serif">Times New Roman</option>
|
||||
<option value="ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">Monospace</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-7">
|
||||
<label class="form-label" for="editTickerBgColor">Background color</label>
|
||||
<input class="form-control form-control-color" id="editTickerBgColor" type="color" value="#000000" title="Choose background color" />
|
||||
</div>
|
||||
<div class="col-12 col-md-5">
|
||||
<label class="form-label" for="editTickerBgOpacity">Background opacity</label>
|
||||
<input class="form-range" id="editTickerBgOpacity" type="range" min="0" max="100" step="1" />
|
||||
<div class="form-text"><span id="editTickerBgOpacityLabel">75</span>%</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="editTickerSpeed">Speed</label>
|
||||
<input class="form-range" id="editTickerSpeed" type="range" min="1" max="100" step="1" />
|
||||
<div class="form-text">Slower ⟷ Faster</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="form-check-label" for="editTickerEnabled">
|
||||
Enable ticker tape on this display
|
||||
</label>
|
||||
<div class="form-text">
|
||||
RSS feed + styling is configured in <a href="{{ url_for('company.my_company') }}">My Company</a>.
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-3" />
|
||||
@@ -382,31 +327,9 @@
|
||||
const plTransitionEl = document.getElementById('editPlaylistsTransitionSelect');
|
||||
const plShowOverlayEl = document.getElementById('editPlaylistsShowOverlayCheck');
|
||||
const tickerEnabledEl = document.getElementById('editTickerEnabled');
|
||||
const tickerRssUrlEl = document.getElementById('editTickerRssUrl');
|
||||
const tickerColorEl = document.getElementById('editTickerColor');
|
||||
const tickerBgColorEl = document.getElementById('editTickerBgColor');
|
||||
const tickerBgOpacityEl = document.getElementById('editTickerBgOpacity');
|
||||
const tickerBgOpacityLabelEl = document.getElementById('editTickerBgOpacityLabel');
|
||||
const tickerFontFamilyEl = document.getElementById('editTickerFontFamily');
|
||||
const tickerFontSizeEl = document.getElementById('editTickerFontSize');
|
||||
const tickerSpeedEl = document.getElementById('editTickerSpeed');
|
||||
let activePlDisplayId = null;
|
||||
let activePlButton = null;
|
||||
|
||||
function setRangeValue(rangeEl, labelEl, value, fallback) {
|
||||
if (!rangeEl) return;
|
||||
const n = parseInt(value || '', 10);
|
||||
const v = Number.isFinite(n) ? n : fallback;
|
||||
rangeEl.value = String(v);
|
||||
if (labelEl) labelEl.textContent = String(v);
|
||||
}
|
||||
|
||||
function onOpacityInput() {
|
||||
if (!tickerBgOpacityEl || !tickerBgOpacityLabelEl) return;
|
||||
tickerBgOpacityLabelEl.textContent = String(tickerBgOpacityEl.value || '0');
|
||||
}
|
||||
if (tickerBgOpacityEl) tickerBgOpacityEl.addEventListener('input', onOpacityInput);
|
||||
|
||||
function updatePlDescCount() {
|
||||
if (!plDescInputEl || !plDescCountEl) return;
|
||||
plDescCountEl.textContent = String((plDescInputEl.value || '').length);
|
||||
@@ -477,14 +400,6 @@
|
||||
const raw = (btn.dataset.currentTickerEnabled || '').toLowerCase();
|
||||
tickerEnabledEl.checked = raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on';
|
||||
}
|
||||
if (tickerRssUrlEl) tickerRssUrlEl.value = btn.dataset.currentTickerRssUrl || '';
|
||||
if (tickerColorEl) tickerColorEl.value = btn.dataset.currentTickerColor || '#ffffff';
|
||||
if (tickerBgColorEl) tickerBgColorEl.value = btn.dataset.currentTickerBgColor || '#000000';
|
||||
setRangeValue(tickerBgOpacityEl, tickerBgOpacityLabelEl, btn.dataset.currentTickerBgOpacity, 75);
|
||||
onOpacityInput();
|
||||
if (tickerFontFamilyEl) tickerFontFamilyEl.value = btn.dataset.currentTickerFontFamily || 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif';
|
||||
if (tickerFontSizeEl) tickerFontSizeEl.value = btn.dataset.currentTickerFontSizePx || '';
|
||||
setRangeValue(tickerSpeedEl, null, btn.dataset.currentTickerSpeed, 25);
|
||||
|
||||
const selected = computeActiveIdsFromDataset(btn);
|
||||
renderPlaylistCheckboxes(selected);
|
||||
@@ -502,13 +417,6 @@
|
||||
const transition = plTransitionEl ? (plTransitionEl.value || 'none') : 'none';
|
||||
const showOverlay = plShowOverlayEl ? !!plShowOverlayEl.checked : false;
|
||||
const tickerEnabled = tickerEnabledEl ? !!tickerEnabledEl.checked : false;
|
||||
const tickerRssUrl = tickerRssUrlEl ? (tickerRssUrlEl.value || '').trim() : '';
|
||||
const tickerColor = tickerColorEl ? (tickerColorEl.value || '').trim() : '';
|
||||
const tickerBgColor = tickerBgColorEl ? (tickerBgColorEl.value || '').trim() : '';
|
||||
const tickerBgOpacity = tickerBgOpacityEl ? (tickerBgOpacityEl.value || '').trim() : '';
|
||||
const tickerFontFamily = tickerFontFamilyEl ? (tickerFontFamilyEl.value || '').trim() : '';
|
||||
const tickerFontSizePx = tickerFontSizeEl ? (tickerFontSizeEl.value || '').trim() : '';
|
||||
const tickerSpeed = tickerSpeedEl ? (tickerSpeedEl.value || '').trim() : '';
|
||||
plSaveBtn.disabled = true;
|
||||
try {
|
||||
const [updatedPlaylists, updatedDesc] = await Promise.all([
|
||||
@@ -517,14 +425,7 @@
|
||||
description: desc,
|
||||
transition,
|
||||
show_overlay: showOverlay,
|
||||
ticker_enabled: tickerEnabled,
|
||||
ticker_rss_url: tickerRssUrl,
|
||||
ticker_color: tickerColor,
|
||||
ticker_bg_color: tickerBgColor,
|
||||
ticker_bg_opacity: tickerBgOpacity,
|
||||
ticker_font_family: tickerFontFamily,
|
||||
ticker_font_size_px: tickerFontSizePx,
|
||||
ticker_speed: tickerSpeed
|
||||
ticker_enabled: tickerEnabled
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -553,34 +454,6 @@
|
||||
: tickerEnabled;
|
||||
activePlButton.dataset.currentTickerEnabled = newTickerEnabled ? '1' : '0';
|
||||
|
||||
activePlButton.dataset.currentTickerRssUrl = (updatedDesc && typeof updatedDesc.ticker_rss_url === 'string')
|
||||
? (updatedDesc.ticker_rss_url || '')
|
||||
: tickerRssUrl;
|
||||
|
||||
activePlButton.dataset.currentTickerColor = (updatedDesc && typeof updatedDesc.ticker_color === 'string')
|
||||
? (updatedDesc.ticker_color || '')
|
||||
: tickerColor;
|
||||
|
||||
activePlButton.dataset.currentTickerBgColor = (updatedDesc && typeof updatedDesc.ticker_bg_color === 'string')
|
||||
? (updatedDesc.ticker_bg_color || '')
|
||||
: tickerBgColor;
|
||||
|
||||
activePlButton.dataset.currentTickerBgOpacity = (updatedDesc && (typeof updatedDesc.ticker_bg_opacity === 'number' || typeof updatedDesc.ticker_bg_opacity === 'string'))
|
||||
? String(updatedDesc.ticker_bg_opacity || '')
|
||||
: String(tickerBgOpacity || '');
|
||||
|
||||
activePlButton.dataset.currentTickerFontFamily = (updatedDesc && typeof updatedDesc.ticker_font_family === 'string')
|
||||
? (updatedDesc.ticker_font_family || '')
|
||||
: tickerFontFamily;
|
||||
|
||||
activePlButton.dataset.currentTickerFontSizePx = (updatedDesc && (typeof updatedDesc.ticker_font_size_px === 'number' || typeof updatedDesc.ticker_font_size_px === 'string'))
|
||||
? String(updatedDesc.ticker_font_size_px || '')
|
||||
: String(tickerFontSizePx || '');
|
||||
|
||||
activePlButton.dataset.currentTickerSpeed = (updatedDesc && (typeof updatedDesc.ticker_speed === 'number' || typeof updatedDesc.ticker_speed === 'string'))
|
||||
? String(updatedDesc.ticker_speed || '')
|
||||
: String(tickerSpeed || '');
|
||||
|
||||
showToast('Display updated', 'text-bg-success');
|
||||
refreshPreviewIframe(activePlDisplayId);
|
||||
if (plModal) plModal.hide();
|
||||
|
||||
@@ -184,4 +184,131 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-elevated mt-4">
|
||||
<div class="card-header">
|
||||
<h2 class="h5 mb-0">Ticker tape (RSS)</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-muted small mb-3">
|
||||
Configure the RSS feed and styling for the ticker tape. Individual displays can enable/disable the ticker from the Dashboard.
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ url_for('company.update_company_ticker_settings') }}" class="d-flex flex-column gap-3">
|
||||
<div>
|
||||
<label class="form-label" for="companyTickerRssUrl">RSS feed URL</label>
|
||||
<input
|
||||
class="form-control"
|
||||
id="companyTickerRssUrl"
|
||||
name="ticker_rss_url"
|
||||
type="url"
|
||||
value="{{ company.ticker_rss_url or '' }}"
|
||||
placeholder="https://example.com/feed.xml"
|
||||
/>
|
||||
<div class="form-text">Leave empty to disable headlines (even if displays have ticker enabled).</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label" for="companyTickerColor">Text color</label>
|
||||
<input
|
||||
class="form-control form-control-color"
|
||||
id="companyTickerColor"
|
||||
name="ticker_color"
|
||||
type="color"
|
||||
value="{{ company.ticker_color or '#ffffff' }}"
|
||||
title="Choose text color"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label" for="companyTickerBgColor">Background color</label>
|
||||
<input
|
||||
class="form-control form-control-color"
|
||||
id="companyTickerBgColor"
|
||||
name="ticker_bg_color"
|
||||
type="color"
|
||||
value="{{ company.ticker_bg_color or '#000000' }}"
|
||||
title="Choose background color"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label" for="companyTickerBgOpacity">Background opacity</label>
|
||||
<input
|
||||
class="form-range"
|
||||
id="companyTickerBgOpacity"
|
||||
name="ticker_bg_opacity"
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
value="{{ company.ticker_bg_opacity if company.ticker_bg_opacity is not none else 75 }}"
|
||||
/>
|
||||
<div class="form-text"><span id="companyTickerBgOpacityLabel">{{ company.ticker_bg_opacity if company.ticker_bg_opacity is not none else 75 }}</span>%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="companyTickerFontFamily">Font</label>
|
||||
<select class="form-select" id="companyTickerFontFamily" name="ticker_font_family">
|
||||
{% set ff = company.ticker_font_family or 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif' %}
|
||||
<option value="system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif" {{ 'selected' if ff == 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif' else '' }}>System (default)</option>
|
||||
<option value="Arial, Helvetica, sans-serif" {{ 'selected' if ff == 'Arial, Helvetica, sans-serif' else '' }}>Arial</option>
|
||||
<option value="Segoe UI, Arial, sans-serif" {{ 'selected' if ff == 'Segoe UI, Arial, sans-serif' else '' }}>Segoe UI</option>
|
||||
<option value="Roboto, Arial, sans-serif" {{ 'selected' if ff == 'Roboto, Arial, sans-serif' else '' }}>Roboto</option>
|
||||
<option value="Georgia, serif" {{ 'selected' if ff == 'Georgia, serif' else '' }}>Georgia</option>
|
||||
<option value="Times New Roman, Times, serif" {{ 'selected' if ff == 'Times New Roman, Times, serif' else '' }}>Times New Roman</option>
|
||||
<option value="ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace" {{ 'selected' if ff == 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace' else '' }}>Monospace</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<label class="form-label" for="companyTickerFontSize">Font size (px)</label>
|
||||
<input
|
||||
class="form-control"
|
||||
id="companyTickerFontSize"
|
||||
name="ticker_font_size_px"
|
||||
type="number"
|
||||
min="10"
|
||||
max="200"
|
||||
step="1"
|
||||
value="{{ company.ticker_font_size_px if company.ticker_font_size_px is not none else 28 }}"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<label class="form-label" for="companyTickerSpeed">Speed</label>
|
||||
<input
|
||||
class="form-range"
|
||||
id="companyTickerSpeed"
|
||||
name="ticker_speed"
|
||||
type="range"
|
||||
min="1"
|
||||
max="100"
|
||||
step="1"
|
||||
value="{{ company.ticker_speed if company.ticker_speed is not none else 25 }}"
|
||||
/>
|
||||
<div class="form-text">Slower ⟷ Faster</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-brand" type="submit">Save ticker settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_scripts %}
|
||||
<script>
|
||||
(function () {
|
||||
const opacityEl = document.getElementById('companyTickerBgOpacity');
|
||||
const opacityLabelEl = document.getElementById('companyTickerBgOpacityLabel');
|
||||
function syncOpacity() {
|
||||
if (!opacityEl || !opacityLabelEl) return;
|
||||
opacityLabelEl.textContent = String(opacityEl.value || '0');
|
||||
}
|
||||
if (opacityEl) opacityEl.addEventListener('input', syncOpacity);
|
||||
syncOpacity();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user