Add onboarding weather and template gradients
This commit is contained in:
@@ -138,7 +138,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end align-items-center">
|
||||
<div class="me-auto">{{#if isEdit}}{{#if (hasPermission currentUser "api-sources.update")}}<button type="submit" form="api-source-toggle-form" class="btn {{#if apiSource.enabled}}btn-danger{{else}}btn-success{{/if}}"><i class="bi {{#if apiSource.enabled}}bi-pause-fill{{else}}bi-play-fill{{/if}} me-1" aria-hidden="true"></i>{{#if apiSource.enabled}}Disable{{else}}Enable{{/if}}</button>{{/if}} {{#if (hasPermission currentUser "api-sources.allow")}}<button type="button" data-manual-refresh-url="/data-sources/api-sources/{{apiSource.id}}/refresh" class="btn btn-outline-secondary"><i class="bi bi-arrow-clockwise me-1" aria-hidden="true"></i>Refresh now</button>{{/if}}{{/if}}</div>
|
||||
<div class="me-auto">{{#if isEdit}}{{#if (hasPermission currentUser "api-sources.update")}}<button type="submit" form="api-source-toggle-form" data-async-data-source-toggle data-enabled="{{#if apiSource.enabled}}true{{else}}false{{/if}}" class="btn {{#if apiSource.enabled}}btn-danger{{else}}btn-success{{/if}}"><i class="bi {{#if apiSource.enabled}}bi-pause-fill{{else}}bi-play-fill{{/if}} me-1" aria-hidden="true"></i>{{#if apiSource.enabled}}Disable{{else}}Enable{{/if}}</button>{{/if}} {{#if (hasPermission currentUser "api-sources.allow")}}<button type="button" data-manual-refresh-url="/data-sources/api-sources/{{apiSource.id}}/refresh" class="btn btn-outline-secondary"><i class="bi bi-arrow-clockwise me-1" aria-hidden="true"></i>Refresh now</button>{{/if}}{{/if}}</div>
|
||||
{{{saveActionButtons formId="api-source-form" saveUrl=formAction cancelUrl=cancelUrl deleteUrl=deleteUrl deleteDisabled=deleteDisabled showSaveAndClose=showSaveSecondaryActions showSaveAndNew=showSaveSecondaryActions}}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end align-items-center">
|
||||
<div class="me-auto">{{#if isEdit}}{{#if (hasPermission currentUser "rss-feeds.update")}}<button type="submit" form="rss-feed-toggle-form" class="btn {{#if rssFeed.enabled}}btn-danger{{else}}btn-success{{/if}}"><i class="bi {{#if rssFeed.enabled}}bi-pause-fill{{else}}bi-play-fill{{/if}} me-1" aria-hidden="true"></i>{{#if rssFeed.enabled}}Disable{{else}}Enable{{/if}}</button>{{/if}} {{#if (hasPermission currentUser "rss-feeds.allow")}}<button type="button" data-manual-refresh-url="/data-sources/rss-feeds/{{rssFeed.id}}/refresh" class="btn btn-outline-secondary"><i class="bi bi-arrow-clockwise me-1" aria-hidden="true"></i>Refresh now</button>{{/if}}{{/if}}</div>
|
||||
<div class="me-auto">{{#if isEdit}}{{#if (hasPermission currentUser "rss-feeds.update")}}<button type="submit" form="rss-feed-toggle-form" data-async-data-source-toggle data-enabled="{{#if rssFeed.enabled}}true{{else}}false{{/if}}" class="btn {{#if rssFeed.enabled}}btn-danger{{else}}btn-success{{/if}}"><i class="bi {{#if rssFeed.enabled}}bi-pause-fill{{else}}bi-play-fill{{/if}} me-1" aria-hidden="true"></i>{{#if rssFeed.enabled}}Disable{{else}}Enable{{/if}}</button>{{/if}} {{#if (hasPermission currentUser "rss-feeds.allow")}}<button type="button" data-manual-refresh-url="/data-sources/rss-feeds/{{rssFeed.id}}/refresh" class="btn btn-outline-secondary"><i class="bi bi-arrow-clockwise me-1" aria-hidden="true"></i>Refresh now</button>{{/if}}{{/if}}</div>
|
||||
{{{saveActionButtons formId="rss-feed-form" saveUrl=formAction cancelUrl=cancelUrl deleteUrl=deleteUrl deleteDisabled=deleteDisabled showSaveAndClose=showSaveSecondaryActions showSaveAndNew=showSaveSecondaryActions}}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<div class="card card-outline card-secondary mt-3">
|
||||
<div class="card-header d-flex align-items-center flex-wrap"><h3 class="card-title mb-0 flex-grow-1">Forecast preview</h3><div class="btn-group btn-group-sm flex-shrink-0 ms-auto" role="group" aria-label="Forecast preview mode"><button type="button" class="btn btn-primary" data-weather-forecast-mode="daily" aria-pressed="true">Daily</button><button type="button" class="btn btn-outline-secondary" data-weather-forecast-mode="hourly" aria-pressed="false">24 hours</button></div></div>
|
||||
<div class="card-body">
|
||||
<div class="small text-body-secondary mb-3">Daily and hourly forecasts will appear here after the first successful fetch.</div>
|
||||
{{#unless weatherPreview.hasSnapshot}}<div class="small text-body-secondary mb-3">Daily and hourly forecasts will appear here after the first successful fetch.</div>{{/unless}}
|
||||
<div id="weather-daily-forecast" class="weather-daily-forecast">{{#each weatherPreview.forecast}}<div class="border rounded p-2 h-100 text-center"><div class="small fw-semibold">{{day}}</div><i class="bi {{icon}} weather-preview-forecast-icon" aria-hidden="true"></i><div class="small mb-2">{{condition}}</div><strong>{{high}}°</strong><span class="text-body-secondary ms-2">{{low}}°</span><div class="small text-body-secondary mt-2"><i class="bi bi-droplet me-1"></i>{{rain}} rain</div></div>{{/each}}</div>
|
||||
<div id="weather-hourly-forecast" class="border-top mt-4 pt-3 d-none"><div class="small text-body-secondary mb-2">Hourly forecast · next 24 hours</div><div class="d-flex gap-2 overflow-auto pb-2">{{#each weatherPreview.hourly}}<div class="border rounded p-2 text-center flex-shrink-0 weather-preview-hourly-item"><div class="small fw-semibold">{{day}}</div><i class="bi {{icon}} weather-preview-forecast-icon" aria-hidden="true"></i><strong>{{temperature}}°</strong><div class="small text-body-secondary mt-1"><i class="bi bi-droplet me-1"></i>{{rain}}</div></div>{{/each}}</div></div>
|
||||
<div id="weather-hourly-forecast"><div class="small text-body-secondary mb-2">Hourly forecast · next 24 hours</div><div class="d-flex gap-2 overflow-auto pb-2">{{#each weatherPreview.hourly}}<div class="border rounded p-2 text-center flex-shrink-0 weather-preview-hourly-item"><div class="small fw-semibold">{{day}}</div><i class="bi {{icon}} weather-preview-forecast-icon" aria-hidden="true"></i><strong>{{temperature}}°</strong><div class="small text-body-secondary mt-1"><i class="bi bi-droplet me-1"></i>{{rain}}</div></div>{{/each}}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<div class="row g-3"><div class="col-md-3"><label for="weather-provider" class="form-label">Provider</label><select id="weather-provider" name="provider" class="form-select"><option value="open-meteo" {{#if (eq weatherLocation.provider 'open-meteo')}}selected{{/if}}>Open-Meteo</option><option value="pirate-weather" {{#if (eq weatherLocation.provider 'pirate-weather')}}selected{{/if}} {{#unless providerAvailability.pirateWeather}}disabled{{/unless}}>Pirate Weather{{#unless providerAvailability.pirateWeather}} (API key not configured){{/unless}}</option></select></div><div class="col-md-3"><label for="weather-temperature-unit" class="form-label">Temperature</label><select id="weather-temperature-unit" name="temperature_unit" class="form-select"><option value="celsius" {{#if (eq weatherLocation.temperature_unit 'celsius')}}selected{{/if}}>Celsius</option><option value="fahrenheit" {{#if (eq weatherLocation.temperature_unit 'fahrenheit')}}selected{{/if}}>Fahrenheit</option></select></div><div class="col-md-3"><label for="weather-wind-unit" class="form-label">Wind</label><select id="weather-wind-unit" name="wind_unit" class="form-select"><option value="kmh" {{#if (eq weatherLocation.wind_unit 'kmh')}}selected{{/if}}>km/h</option><option value="mph" {{#if (eq weatherLocation.wind_unit 'mph')}}selected{{/if}}>mph</option><option value="ms" {{#if (eq weatherLocation.wind_unit 'ms')}}selected{{/if}}>m/s</option></select></div><div class="col-md-3"><label for="weather-precipitation-unit" class="form-label">Precipitation</label><select id="weather-precipitation-unit" name="precipitation_unit" class="form-select"><option value="mm" {{#if (eq weatherLocation.precipitation_unit 'mm')}}selected{{/if}}>Millimetres</option><option value="inch" {{#if (eq weatherLocation.precipitation_unit 'inch')}}selected{{/if}}>Inches</option></select></div></div>
|
||||
<div class="row g-3 mt-1"><div class="col-md-6"><label for="weather-interval" class="form-label">Update interval</label><input id="weather-interval" name="update_interval_value" type="number" min="1" max="1440" class="form-control" value="{{weatherLocation.update_interval_value}}" required /></div><div class="col-md-6"><label for="weather-interval-unit" class="form-label">Unit</label><select id="weather-interval-unit" name="update_interval_unit" class="form-select"><option value="minutes" {{#if (eq weatherLocation.update_interval_unit 'minutes')}}selected{{/if}}>Minutes</option><option value="hours" {{#if (eq weatherLocation.update_interval_unit 'hours')}}selected{{/if}}>Hours</option></select></div></div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end align-items-center"><div class="me-auto">{{#if isEdit}}{{#if (hasPermission currentUser "weather.update")}}<button type="submit" form="weather-location-toggle-form" class="btn {{#if weatherLocation.enabled}}btn-danger{{else}}btn-success{{/if}}"><i class="bi {{#if weatherLocation.enabled}}bi-pause-fill{{else}}bi-play-fill{{/if}} me-1" aria-hidden="true"></i>{{#if weatherLocation.enabled}}Disable{{else}}Enable{{/if}}</button>{{/if}} {{#if (hasPermission currentUser "weather.allow")}}<button type="button" data-weather-refresh-url="/data-sources/weather/{{weatherLocation.id}}/refresh" class="btn btn-outline-secondary"><i class="bi bi-arrow-clockwise me-1" aria-hidden="true"></i>Refresh now</button>{{/if}}{{/if}}</div>{{{saveActionButtons formId="weather-location-form" saveUrl=formAction cancelUrl=cancelUrl deleteUrl=deleteUrl deleteDisabled=deleteDisabled showSaveAndClose=showSaveSecondaryActions showSaveAndNew=showSaveSecondaryActions}}}</div>
|
||||
<div class="card-footer d-flex justify-content-end align-items-center"><div class="me-auto">{{#if isEdit}}{{#if (hasPermission currentUser "weather.update")}}<button type="submit" form="weather-location-toggle-form" data-async-data-source-toggle data-enabled="{{#if weatherLocation.enabled}}true{{else}}false{{/if}}" class="btn {{#if weatherLocation.enabled}}btn-danger{{else}}btn-success{{/if}}"><i class="bi {{#if weatherLocation.enabled}}bi-pause-fill{{else}}bi-play-fill{{/if}} me-1" aria-hidden="true"></i>{{#if weatherLocation.enabled}}Disable{{else}}Enable{{/if}}</button>{{/if}} {{#if (hasPermission currentUser "weather.allow")}}<button type="button" data-weather-refresh-url="/data-sources/weather/{{weatherLocation.id}}/refresh" class="btn btn-outline-secondary"><i class="bi bi-arrow-clockwise me-1" aria-hidden="true"></i>Refresh now</button>{{/if}}{{/if}}</div>{{{saveActionButtons formId="weather-location-form" saveUrl=formAction cancelUrl=cancelUrl deleteUrl=deleteUrl deleteDisabled=deleteDisabled showSaveAndClose=showSaveSecondaryActions showSaveAndNew=showSaveSecondaryActions}}}</div>
|
||||
</div>
|
||||
</form>
|
||||
{{#if isEdit}}<form id="weather-location-toggle-form" method="post" action="/data-sources/weather/{{weatherLocation.id}}" data-async-command><input type="hidden" name="data_source_action" value="toggle" /></form>{{/if}}
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
<td data-label="Name">{{name}}</td><td data-label="Location">{{location_label}}<br><small class="text-muted">{{latitude}}, {{longitude}} · {{timezone}}</small></td><td data-label="Provider">{{provider}}</td><td data-label="Refresh interval">{{intervalLabel}}</td><td data-label="Status"><span class="badge text-bg-{{#if enabled}}success{{else}}danger{{/if}}">{{#if enabled}}Enabled{{else}}Disabled{{/if}}</span></td>
|
||||
<td data-label="Actions"><div class="actions">
|
||||
{{#if (hasPermission ../currentUser 'weather.update')}}<a class="btn btn-sm btn-primary" href="/data-sources/weather/{{id}}/edit">Edit</a>{{/if}}
|
||||
{{#if (hasPermission ../currentUser 'weather.delete')}}<form class="inline-form" method="post" action="/data-sources/weather/{{id}}/delete" data-confirm-message="Delete this weather location?"><button class="btn btn-sm btn-danger" type="submit">Delete</button></form>{{/if}}
|
||||
{{#if (hasPermission ../currentUser 'weather.create')}}<a class="btn btn-sm btn-secondary" href="/data-sources/weather/{{id}}/duplicate">Dupe</a>{{/if}}
|
||||
{{#if (hasPermission ../currentUser 'weather.delete')}}<form class="inline-form" method="post" action="/data-sources/weather/{{id}}/delete" data-confirm-message="Delete this weather location?">{{#if inUse}}<button class="btn btn-sm btn-outline-danger" type="submit" disabled>Delete</button>{{else}}<button class="btn btn-sm btn-danger" type="submit">Delete</button>{{/if}}</form>{{/if}}
|
||||
</div></td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Weather source</h2>
|
||||
<p>Configure a remote weather feed once, then serve its cached snapshot to every player.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-xl-7">
|
||||
<div class="card card-outline card-primary h-100 weather-mock-card">
|
||||
<div class="card-header"><h3 class="card-title">Source configuration</h3></div>
|
||||
<div class="card-body weather-mock-card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="weather-name">Name</label>
|
||||
<input id="weather-name" class="form-control" value="{{weather.name}}" />
|
||||
</div>
|
||||
<div class="col-12 col-md-8">
|
||||
<label class="form-label" for="weather-location">Location</label>
|
||||
<input id="weather-location" class="form-control" value="{{weather.location}}" />
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label" for="weather-provider">Provider</label>
|
||||
<select id="weather-provider" class="form-select">
|
||||
<option selected>{{weather.provider}} (no key)</option>
|
||||
<option>Custom JSON endpoint</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12"><div class="form-text"><i class="bi bi-info-circle me-1"></i>Location is the display name. Coordinates identify the weather point and can be set from a map picker, browser location, or manual entry.</div></div>
|
||||
<div class="col-6">
|
||||
<label class="form-label" for="weather-latitude">Latitude</label>
|
||||
<input id="weather-latitude" class="form-control" value="{{weather.latitude}}" />
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label" for="weather-longitude">Longitude</label>
|
||||
<input id="weather-longitude" class="form-control" value="{{weather.longitude}}" />
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label" for="weather-temperature-unit">Temperature</label>
|
||||
<select id="weather-temperature-unit" class="form-select"><option selected>{{weather.temperatureUnit}}</option><option>Fahrenheit (°F)</option></select>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label" for="weather-wind-unit">Wind speed</label>
|
||||
<select id="weather-wind-unit" class="form-select"><option selected>{{weather.windUnit}}</option><option>Miles per hour (mph)</option><option>Metres per second (m/s)</option></select>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label" for="weather-precipitation-unit">Precipitation</label>
|
||||
<select id="weather-precipitation-unit" class="form-select"><option selected>{{weather.precipitationUnit}}</option><option>Inches (in)</option></select>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="weather-refresh">Refresh interval</label>
|
||||
<select id="weather-refresh" class="form-select"><option>15 minutes</option><option selected>{{weather.refreshInterval}}</option><option>1 hour</option></select>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="weather-forecast-mode">Forecast display</label>
|
||||
<select id="weather-forecast-mode" class="form-select"><option selected value="daily">Daily forecast</option><option value="hourly">Hourly forecast (next 24 hours)</option></select>
|
||||
<div class="form-text">The source can cache both datasets; this controls what the region displays.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer weather-mock-card-footer d-flex justify-content-between align-items-center">
|
||||
<button type="button" class="btn btn-outline-secondary"><i class="bi bi-arrow-clockwise me-1"></i>Refresh now</button>
|
||||
<button type="button" class="btn btn-primary"><i class="bi bi-check2 me-1"></i>Save source</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-5">
|
||||
<div class="card card-outline card-secondary h-100 weather-mock-card">
|
||||
<div class="card-header d-flex align-items-center gap-2"><h3 class="card-title mb-0 flex-grow-1">Cached snapshot</h3><span class="badge text-bg-success flex-shrink-0"><i class="bi bi-check-circle me-1"></i>Last fetch succeeded</span></div>
|
||||
<div class="card-body weather-mock-card-body">
|
||||
<div class="d-flex align-items-start justify-content-between border-bottom pb-3 mb-3">
|
||||
<div><div class="text-body-secondary small">{{weather.location}}</div><div class="display-4 fw-semibold">{{weather.temperature}}°</div><div class="fw-medium">{{weather.condition}}</div></div>
|
||||
<i class="bi bi-cloud-sun weather-mock-icon" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="row g-3 small">
|
||||
<div class="col-6"><span class="text-body-secondary d-block">Feels like</span><strong>{{weather.feelsLike}}°</strong></div>
|
||||
<div class="col-6"><span class="text-body-secondary d-block">Humidity</span><strong>{{weather.humidity}}</strong></div>
|
||||
<div class="col-6"><span class="text-body-secondary d-block">Wind</span><strong>{{weather.wind}}</strong></div>
|
||||
<div class="col-6"><span class="text-body-secondary d-block">UV index</span><strong>{{weather.uvIndex}}</strong></div>
|
||||
<div class="col-6"><span class="text-body-secondary d-block">Sunrise</span><strong>{{weather.sunrise}}</strong></div>
|
||||
<div class="col-6"><span class="text-body-secondary d-block">Sunset</span><strong>{{weather.sunset}}</strong></div>
|
||||
</div>
|
||||
<div class="alert alert-light border mt-4 mb-0 small"><i class="bi bi-database-check me-1"></i> Cached {{weather.cacheAge}}. Players keep using this snapshot if the next request fails.</div>
|
||||
</div>
|
||||
<div class="card-footer weather-mock-card-footer small text-body-secondary">Last successful fetch: {{weather.fetchedAt}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-outline card-secondary mt-3">
|
||||
<div class="card-header d-flex align-items-center justify-content-between gap-2 flex-wrap"><h3 class="card-title mb-0">Forecast preview</h3><div class="btn-group btn-group-sm flex-shrink-0" role="group" aria-label="Forecast preview mode"><button type="button" class="btn btn-primary">Daily</button><button type="button" class="btn btn-outline-secondary">24 hours</button></div></div>
|
||||
<div class="card-body">
|
||||
<div class="small text-body-secondary mb-3">{{weather.forecastModeLabel}} · timezone {{weather.timezone}}</div>
|
||||
<div class="row row-cols-2 row-cols-md-4 g-2">
|
||||
{{#each weather.forecast}}
|
||||
<div class="col"><div class="border rounded p-3 h-100 text-center"><div class="small fw-semibold">{{day}}</div><i class="bi {{icon}} weather-mock-forecast-icon" aria-hidden="true"></i><div class="small mb-2">{{condition}}</div><strong>{{high}}°</strong><span class="text-body-secondary ms-2">{{low}}°</span><div class="small text-body-secondary mt-2"><i class="bi bi-droplet me-1"></i>{{rain}} rain</div></div></div>
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="border-top mt-4 pt-3">
|
||||
<div class="small text-body-secondary mb-2">Hourly option preview · next 24 hours</div>
|
||||
<div class="d-flex gap-2 overflow-auto pb-2">
|
||||
{{#each weather.hourly}}
|
||||
<div class="border rounded p-2 text-center flex-shrink-0" style="width:7.5rem"><div class="small fw-semibold">{{day}}</div><i class="bi {{icon}} weather-mock-forecast-icon" aria-hidden="true"></i><strong>{{temperature}}°</strong><div class="small text-body-secondary mt-1"><i class="bi bi-droplet me-1"></i>{{rain}}</div></div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.weather-mock-card { display: flex; flex-direction: column; }
|
||||
.weather-mock-card-body { flex: 1 1 auto; }
|
||||
.weather-mock-card-footer { min-height: 3.5rem; }
|
||||
.weather-mock-icon { font-size: 4rem; color: #e0a11a; }
|
||||
.weather-mock-forecast-icon { display: block; font-size: 2rem; color: #e0a11a; margin: 1rem 0 .65rem; }
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Connect a screen</h2>
|
||||
<p>Pair a player with a screen using its six-character PIN.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-lg-7 col-xl-6">
|
||||
<div class="card card-outline card-primary admin-form-card onboarding-pairing-card" id="onboarding-pair-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Screen setup</h3>
|
||||
</div>
|
||||
<form id="onboarding-pair-form" method="post" action="/pairing" novalidate>
|
||||
<div class="card-body">
|
||||
<div class="onboarding-pairing-progress d-none" id="onboarding-pair-progress" role="status" aria-live="polite">
|
||||
<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
|
||||
<span>Completing pairing...</span>
|
||||
</div>
|
||||
<div id="onboarding-pair-message" class="alert d-none" role="alert"></div>
|
||||
<input type="hidden" id="onboarding-pair-client-id" name="clientId" value="{{clientId}}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="onboarding-pair-code-1">Player PIN</label>
|
||||
<div class="onboarding-pin-inputs" id="onboarding-pair-code-inputs" data-pairing-code="{{pairingCode}}" role="group" aria-label="Player PIN" aria-required="true">
|
||||
<input class="form-control form-control-lg text-uppercase font-weight-bold" id="onboarding-pair-code-1" data-pairing-code-input type="text" maxlength="1" inputmode="text" autocomplete="one-time-code" aria-label="Player PIN character 1">
|
||||
<input class="form-control form-control-lg text-uppercase font-weight-bold" id="onboarding-pair-code-2" data-pairing-code-input type="text" maxlength="1" inputmode="text" aria-label="Player PIN character 2">
|
||||
<input class="form-control form-control-lg text-uppercase font-weight-bold" id="onboarding-pair-code-3" data-pairing-code-input type="text" maxlength="1" inputmode="text" aria-label="Player PIN character 3">
|
||||
<input class="form-control form-control-lg text-uppercase font-weight-bold" id="onboarding-pair-code-4" data-pairing-code-input type="text" maxlength="1" inputmode="text" aria-label="Player PIN character 4">
|
||||
<input class="form-control form-control-lg text-uppercase font-weight-bold" id="onboarding-pair-code-5" data-pairing-code-input type="text" maxlength="1" inputmode="text" aria-label="Player PIN character 5">
|
||||
<input class="form-control form-control-lg text-uppercase font-weight-bold" id="onboarding-pair-code-6" data-pairing-code-input type="text" maxlength="1" inputmode="text" aria-label="Player PIN character 6">
|
||||
</div>
|
||||
<input id="onboarding-pair-code" type="hidden" name="pairingCode">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="onboarding-pair-name">Client name</label>
|
||||
<input class="form-control" id="onboarding-pair-name" name="clientName" maxlength="255" required placeholder="Lobby player" autocomplete="off">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label" for="onboarding-pair-screen">Screen</label>
|
||||
<select class="form-select" id="onboarding-pair-screen" name="screenSlug" required>
|
||||
<option value="">Select a screen</option>
|
||||
{{#each screens}}
|
||||
<option value="{{slug}}">{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex flex-wrap align-items-center">
|
||||
<button class="btn btn-outline-secondary" id="onboarding-pair-another" type="button" aria-label="Scan player QR code" title="Scan player QR code">
|
||||
<i class="bi bi-camera" aria-hidden="true"></i>
|
||||
<span class="d-none" id="onboarding-pair-another-label">Pair another</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary d-none ms-auto" id="onboarding-pair-manual" type="button">Enter PIN manually</button>
|
||||
<button class="btn btn-primary ms-auto" id="onboarding-pair-connect" type="submit">Connect</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="onboarding-scanner d-none" id="onboarding-scanner" aria-hidden="true">
|
||||
<div class="onboarding-scanner-panel" role="dialog" aria-modal="true" aria-labelledby="onboarding-scanner-title">
|
||||
<div class="d-flex align-items-center justify-content-between gap-3 mb-3">
|
||||
<h2 class="h5 mb-0" id="onboarding-scanner-title">Scan player QR code</h2>
|
||||
<button class="btn btn-sm btn-outline-secondary" id="onboarding-scanner-close" type="button">Close</button>
|
||||
</div>
|
||||
<video id="onboarding-scanner-video" class="onboarding-scanner-video" autoplay playsinline></video>
|
||||
<input id="onboarding-scanner-capture" type="file" accept="image/*" capture="environment" class="d-none">
|
||||
<p class="text-body-secondary small mb-0" id="onboarding-scanner-message">Point your camera at the player QR code.</p>
|
||||
<pre class="d-none small mt-2 mb-0" id="onboarding-scanner-debug" aria-live="polite"></pre>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,7 +5,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if (hasPermission currentUser 'clients.allow')}}
|
||||
{{#if (anyPermission currentUser 'clients.allow' 'pairing.allow')}}
|
||||
<div class="card card-outline card-secondary mb-4 screen-command-card">
|
||||
<div class="card-header">
|
||||
<div class="dashboard-card-heading">
|
||||
@@ -13,9 +13,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{#if screens.length}}
|
||||
<div class="screen-command-panel">
|
||||
<div class="screen-command-panel-left">
|
||||
<div class="screen-command-panel{{#if (hasPermission currentUser 'clients.allow')}}{{else}} screen-command-panel-pairing-only{{/if}}">
|
||||
{{#if (hasPermission currentUser 'clients.allow')}}
|
||||
{{#if screens.length}}
|
||||
<div class="screen-command-panel-left">
|
||||
<div class="screen-command-selector-field">
|
||||
<label class="screen-command-selector-label" for="screen-command-select">Target screen group</label>
|
||||
<div class="input-group input-group-sm">
|
||||
@@ -55,18 +56,28 @@
|
||||
<button type="submit" class="btn btn-sm btn-secondary" disabled><i class="bi bi-eye-slash me-1" aria-hidden="true"></i>Blackout screen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="screen-command-panel-right">
|
||||
<div class="screen-command-summary">
|
||||
<span class="screen-command-pill is-live" data-screen-command-pill>Choose a screen</span>
|
||||
<strong data-screen-command-name>Select a target screen group</strong>
|
||||
<span class="screen-command-summary-meta" data-screen-command-meta>Commands update automatically for the selected screen group.</span>
|
||||
</div>
|
||||
<div class="screen-command-panel-right">
|
||||
<div class="screen-command-summary">
|
||||
<span class="screen-command-pill is-live" data-screen-command-pill>Choose a screen</span>
|
||||
<strong data-screen-command-name>Select a target screen group</strong>
|
||||
<span class="screen-command-summary-meta" data-screen-command-meta>Commands update automatically for the selected screen group.</span>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="empty">No screen groups are available to target yet.</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if (hasPermission currentUser 'pairing.allow')}}
|
||||
<div class="screen-command-pairing">
|
||||
<div>
|
||||
<strong>New player?</strong>
|
||||
<span class="screen-command-summary-meta">Open the pairing page to connect another display.</span>
|
||||
</div>
|
||||
<a class="btn btn-primary btn-sm" href="/pairing"><i class="bi bi-qr-code-scan me-1" aria-hidden="true"></i>Pair a player</a>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="empty">No screen groups are available to target yet.</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
@@ -96,7 +107,7 @@
|
||||
<tbody id="dashboard-clients-table-body">
|
||||
{{#if clients.length}}
|
||||
{{#each clients}}
|
||||
<tr data-table-search-row data-client-key="{{id}}" data-client-id="{{clientId}}" data-client-device-id="{{deviceId}}" data-client-screen-slug="{{screen_slug}}" data-client-player-base-url="{{player_url}}">
|
||||
<tr data-table-search-row data-client-key="{{id}}" data-client-id="{{id}}" data-client-client-id="{{clientId}}" data-client-device-id="{{deviceId}}" data-client-screen-slug="{{screen_slug}}" data-client-player-base-url="{{player_url}}">
|
||||
<td data-label="Client" class="client-rename-cell" title="Double-click to rename">
|
||||
<div>{{#if client_name}}{{client_name}}{{else}}Unknown{{/if}}</div>
|
||||
</td>
|
||||
@@ -186,7 +197,7 @@
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="command" value="moveclient" />
|
||||
<input type="hidden" name="connectionId" value="" data-client-move-connection-id />
|
||||
<input type="hidden" name="deviceId" value="" data-client-move-device-id />
|
||||
<input type="hidden" name="clientId" value="" data-client-move-client-id />
|
||||
<input type="hidden" name="clientName" value="" data-client-move-client-name />
|
||||
<input type="hidden" name="playerBaseUrl" value="" data-client-move-player-base-url />
|
||||
<label class="form-label" for="client-move-screen-target">Target screen group</label>
|
||||
|
||||
@@ -11,13 +11,6 @@
|
||||
<span class="dashboard-hero-kicker">Live overview</span>
|
||||
<h3 class="dashboard-hero-title">Keep the control surface focused on live state and actions.</h3>
|
||||
<p class="dashboard-hero-copy">Use the cards below for the current totals, then open the screen group snapshot when you want a quick read on playlist assignment and live connections.</p>
|
||||
{{#if primaryPlayerUrl}}
|
||||
<div class="alert alert-info mb-0 mt-3 py-2">
|
||||
<strong>Begin onboarding:</strong>
|
||||
open <a href="{{primaryPlayerUrl}}" target="_blank" rel="noreferrer">{{primaryPlayerUrl}}</a>
|
||||
on the public display to get started.
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="dashboard-hero-stats">
|
||||
{{#if (hasPermission currentUser "playlists.read")}}
|
||||
@@ -194,7 +187,9 @@
|
||||
<h3 class="card-title">Screen group snapshot</h3>
|
||||
<p class="dashboard-card-subtitle">Playlist assignment and live connection state without the spreadsheet feel.</p>
|
||||
</div>
|
||||
<a class="btn btn-sm btn-primary" href="/screens">Manage screen groups</a>
|
||||
{{#if (hasPermission currentUser "pairing.allow")}}
|
||||
<a class="btn btn btn-primary" href="/pairing"><i class="bi bi-qr-code-scan me-1" aria-hidden="true"></i>Pair a player</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="dashboard-screens-grid" class="dashboard-screen-grid">
|
||||
|
||||
@@ -180,13 +180,27 @@
|
||||
|
||||
var backgroundColor = String(payload.backgroundColor || '#111111').trim() || '#111111';
|
||||
var backgroundImagePath = String(payload.backgroundImagePath || '').trim();
|
||||
var backgroundGradient = '';
|
||||
try {
|
||||
var gradientData = typeof payload.backgroundGradient === 'string' ? JSON.parse(payload.backgroundGradient) : payload.backgroundGradient;
|
||||
if (gradientData && gradientData.type === 'linear' && Array.isArray(gradientData.colors) && gradientData.colors.length >= 2) {
|
||||
var gradientStops = Array.isArray(gradientData.stops) ? gradientData.stops : (gradientData.colors || []).map(function (color, index, colors) { return { color: color, position: colors.length > 1 ? Math.round(index * 100 / (colors.length - 1)) : 0 }; });
|
||||
gradientStops = gradientStops.filter(function (stop) { return stop && /^#[0-9a-fA-F]{3,8}$/.test(String(stop.color || '').trim()); }).slice(0, 12);
|
||||
if (gradientStops.length >= 2) {
|
||||
var gradientAngle = Number(gradientData.angle);
|
||||
backgroundGradient = 'linear-gradient(' + (Number.isFinite(gradientAngle) ? Math.max(0, Math.min(360, gradientAngle)) : 90) + 'deg,' + gradientStops.map(function (stop) { return stop.color + ' ' + Math.max(0, Math.min(100, Number(stop.position) || 0)) + '%'; }).join(',') + ')';
|
||||
}
|
||||
}
|
||||
} catch (_error) {
|
||||
backgroundGradient = '';
|
||||
}
|
||||
|
||||
var canvasWidth = Math.max(1, Number(payload.canvasWidth || 1920));
|
||||
var canvasHeight = Math.max(1, Number(payload.canvasHeight || 1080));
|
||||
canvas.style.backgroundColor = backgroundColor;
|
||||
canvas.style.backgroundImage = backgroundImagePath ? 'url("' + encodeURI(backgroundImagePath) + '")' : 'none';
|
||||
canvas.style.backgroundImage = [backgroundImagePath ? 'url("' + encodeURI(backgroundImagePath) + '")' : '', backgroundGradient].filter(Boolean).join(',') || 'none';
|
||||
canvas.style.backgroundPosition = 'center';
|
||||
canvas.style.backgroundSize = '100% 100%';
|
||||
canvas.style.backgroundSize = backgroundImagePath ? 'contain, cover' : 'cover';
|
||||
canvas.style.backgroundRepeat = 'no-repeat';
|
||||
canvas.style.transformOrigin = 'center center';
|
||||
canvas.style.width = canvasWidth + 'px';
|
||||
|
||||
@@ -110,6 +110,10 @@
|
||||
<label for="background-color" class="form-label">Background colour</label>
|
||||
<input type="color" name="background_color" id="background-color" class="form-control form-control-color w-100" value="{{#if template.background_color}}{{template.background_color}}{{else}}#111111{{/if}}" title="Choose a background colour" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<input type="hidden" name="background_gradient" id="background-gradient" value="{{template.background_gradient}}" />
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#background-advanced-modal"><i class="bi bi-sliders me-1" aria-hidden="true"></i>Advanced background</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -117,6 +121,23 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{#> modal-shell modalId="background-advanced-modal" modalLabelId="background-advanced-modal-label" modalDialogClass="modal-dialog-centered"}}
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title fs-5" id="background-advanced-modal-label">Advanced background</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="background-gradient-enabled" {{#if template.background_gradient}}checked{{/if}} />
|
||||
<label class="form-check-label" for="background-gradient-enabled">Use linear gradient</label>
|
||||
</div>
|
||||
<div class="row g-3" id="background-gradient-options" {{#if template.background_gradient}}{{else}}hidden{{/if}}>
|
||||
<div class="col-12"><label for="background-gradient-angle" class="form-label">Gradient angle <output id="background-gradient-angle-output">90</output>°</label><input type="range" id="background-gradient-angle" class="form-range" min="0" max="360" value="90" /></div>
|
||||
<div class="col-12"><div class="gradient-stop-bar mb-3" id="background-gradient-bar" role="slider" aria-label="Gradient colour stops" aria-valuemin="0" aria-valuemax="100"><div class="gradient-stop-bar-track"></div><div class="gradient-stop-bar-handles" id="background-gradient-bar-handles"></div></div><div class="d-flex justify-content-between align-items-center mb-2"><strong>Colour stops</strong><button type="button" class="btn btn-sm btn-outline-secondary" id="background-gradient-add-stop"><i class="bi bi-plus-lg me-1" aria-hidden="true"></i>Add stop</button></div><div class="d-grid gap-2" id="background-gradient-stops"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
{{/modal-shell}}
|
||||
|
||||
<script src="/assets/js/shared/qr-code-svg.js?v={{appVersion}}"></script>
|
||||
<script src="/assets/vendor/qr-code-styling/qr-code-styling-loader.js?v={{appVersion}}"></script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user