feat: Light mode default, auth, Posts rename, display scaling, TinyMCE improvements
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
<div class="stat-icon"><i class="bi bi-display"></i></div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">@ViewBag.DeviceCount</div>
|
||||
<div class="stat-label">Kittens (Devices)</div>
|
||||
<div class="stat-label">Posts (Devices)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,7 +37,7 @@
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><i class="bi bi-display me-2"></i>Active Kittens</h5>
|
||||
<h5 class="mb-0"><i class="bi bi-display me-2"></i>Active Posts</h5>
|
||||
<a href="/admin/devices/create" class="btn btn-sm btn-primary"><i class="bi bi-plus-lg me-1"></i>Add</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@@ -67,8 +67,8 @@
|
||||
{
|
||||
<div class="p-4 text-center text-muted">
|
||||
<i class="bi bi-display" style="font-size:2em;"></i>
|
||||
<p class="mt-2">No kittens configured yet.</p>
|
||||
<a href="/admin/devices/create" class="btn btn-primary btn-sm">Create your first kitten</a>
|
||||
<p class="mt-2">No posts configured yet.</p>
|
||||
<a href="/admin/devices/create" class="btn btn-primary btn-sm">Create your first post</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -83,7 +83,7 @@
|
||||
<tr><td><strong>Meow</strong></td><td>An individual slide</td></tr>
|
||||
<tr><td><strong>PURR</strong></td><td>Persistent User Requests & Reminders — a playlist</td></tr>
|
||||
<tr><td><strong>ASK</strong></td><td>Attention Seeking Kitty — priority override</td></tr>
|
||||
<tr><td><strong>Kitten</strong></td><td>A display node/device</td></tr>
|
||||
<tr><td><strong>Post</strong></td><td>A display node/device</td></tr>
|
||||
<tr><td><strong>Scratching Post</strong></td><td>This backend</td></tr>
|
||||
<tr><td><strong>Sunbeam</strong></td><td>The framework</td></tr>
|
||||
</tbody>
|
||||
|
||||
+12
-16
@@ -1,20 +1,16 @@
|
||||
@model Device
|
||||
@{ ViewData["Title"] = "New Kitten"; }
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<div class="card"><div class="card-body">
|
||||
<form asp-action="Create" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<div class="mb-3"><label asp-for="Name" class="form-label">Display Name *</label><input asp-for="Name" class="form-control" placeholder="e.g. Front of House" required /><span asp-validation-for="Name" class="text-danger"></span></div>
|
||||
<div class="mb-3"><label asp-for="Slug" class="form-label">URL Slug *</label><div class="input-group"><span class="input-group-text">/</span><input asp-for="Slug" class="form-control font-monospace" placeholder="frontofhouse" /></div><div class="form-text">Lowercase, no spaces. Auto-generated from name if left empty.</div><span asp-validation-for="Slug" class="text-danger"></span></div>
|
||||
<div class="row g-3 mb-3"><div class="col-6"><label asp-for="ResolutionWidth" class="form-label">Width (px)</label><input asp-for="ResolutionWidth" class="form-control" type="number" min="320" max="7680" /></div><div class="col-6"><label asp-for="ResolutionHeight" class="form-label">Height (px)</label><input asp-for="ResolutionHeight" class="form-control" type="number" min="240" max="4320" /></div></div>
|
||||
<div class="mb-3"><label class="form-label">Quick Presets</label><div class="d-flex flex-wrap gap-2"><button type="button" class="btn btn-sm btn-outline-secondary" onclick="setRes(1920,1080)">1080p</button><button type="button" class="btn btn-sm btn-outline-secondary" onclick="setRes(3840,2160)">4K</button><button type="button" class="btn btn-sm btn-outline-secondary" onclick="setRes(1080,1920)">Portrait</button><button type="button" class="btn btn-sm btn-outline-secondary" onclick="setRes(1280,720)">720p</button></div></div>
|
||||
<div class="mb-4"><label asp-for="Transition" class="form-label">Transition Effect</label><select asp-for="Transition" class="form-select"><option value="fade">Fade</option><option value="slide">Slide</option><option value="none">None</option></select></div>
|
||||
<div class="d-grid gap-2"><button type="submit" class="btn btn-primary btn-lg"><i class="bi bi-check-lg me-1"></i>Create Kitten</button><a href="/admin/devices" class="btn btn-outline-secondary">Cancel</a></div>
|
||||
</form>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
@{ ViewData["Title"] = "New Post"; }
|
||||
<div class="row justify-content-center"><div class="col-md-8 col-lg-6"><div class="card"><div class="card-body">
|
||||
<form asp-action="Create" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<div class="mb-3"><label asp-for="Name" class="form-label">Display Name *</label><input asp-for="Name" class="form-control" placeholder="e.g. Front of House" required /><span asp-validation-for="Name" class="text-danger"></span></div>
|
||||
<div class="mb-3"><label asp-for="Slug" class="form-label">URL Slug *</label><div class="input-group"><span class="input-group-text">/</span><input asp-for="Slug" class="form-control font-monospace" placeholder="frontofhouse" /></div><div class="form-text">Lowercase, no spaces. Auto-generated from name if left empty.</div><span asp-validation-for="Slug" class="text-danger"></span></div>
|
||||
<div class="row g-3 mb-3"><div class="col-6"><label asp-for="ResolutionWidth" class="form-label">Width (px)</label><input asp-for="ResolutionWidth" class="form-control" type="number" min="320" max="7680" /></div><div class="col-6"><label asp-for="ResolutionHeight" class="form-label">Height (px)</label><input asp-for="ResolutionHeight" class="form-control" type="number" min="240" max="4320" /></div></div>
|
||||
<div class="mb-3"><label class="form-label">Quick Presets</label><div class="d-flex flex-wrap gap-2"><button type="button" class="btn btn-sm btn-outline-secondary" onclick="setRes(1920,1080)">1080p</button><button type="button" class="btn btn-sm btn-outline-secondary" onclick="setRes(3840,2160)">4K</button><button type="button" class="btn btn-sm btn-outline-secondary" onclick="setRes(1080,1920)">Portrait</button><button type="button" class="btn btn-sm btn-outline-secondary" onclick="setRes(1280,720)">720p</button></div></div>
|
||||
<div class="mb-4"><label asp-for="Transition" class="form-label">Transition Effect</label><select asp-for="Transition" class="form-select"><option value="fade">Fade</option><option value="slide">Slide</option><option value="none">None</option></select></div>
|
||||
<div class="d-grid gap-2"><button type="submit" class="btn btn-primary btn-lg"><i class="bi bi-check-lg me-1"></i>Create Post</button><a href="/admin/devices" class="btn btn-outline-secondary">Cancel</a></div>
|
||||
</form>
|
||||
</div></div></div></div>
|
||||
@section Scripts {
|
||||
<script>
|
||||
function setRes(w,h){document.getElementById('ResolutionWidth').value=w;document.getElementById('ResolutionHeight').value=h;}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row justify-content-center"><div class="col-md-6"><div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white"><h5 class="mb-0"><i class="bi bi-exclamation-triangle me-2"></i>Confirm Deletion</h5></div>
|
||||
<div class="card-body">
|
||||
<p>Are you sure you want to delete the kitten <strong>"@Model.Name"</strong>?</p>
|
||||
<p>Are you sure you want to delete the post <strong>"@Model.Name"</strong>?</p>
|
||||
<p class="text-muted">URL: <code>/@Model.Slug</code> · @Model.DeviceSlides.Count slide(s) assigned</p>
|
||||
<p class="text-muted">This will remove the device and its playlist. Slides themselves will not be deleted.</p>
|
||||
<form asp-action="Delete" method="post">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@model List<Device>
|
||||
@{ ViewData["Title"] = "Kittens (Devices)"; }
|
||||
@{ ViewData["Title"] = "Posts (Devices)"; }
|
||||
|
||||
@section HeaderActions {
|
||||
<a href="/admin/devices/create" class="btn btn-primary"><i class="bi bi-plus-lg me-1"></i>New Kitten</a>
|
||||
<a href="/admin/devices/create" class="btn btn-primary"><i class="bi bi-plus-lg me-1"></i>New Post</a>
|
||||
}
|
||||
|
||||
@if (Model.Any())
|
||||
@@ -42,8 +42,8 @@ else
|
||||
{
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-display" style="font-size:3em;"></i>
|
||||
<h4 class="mt-3">No kittens yet</h4>
|
||||
<h4 class="mt-3">No posts yet</h4>
|
||||
<p class="text-muted">Create your first display device.</p>
|
||||
<a href="/admin/devices/create" class="btn btn-primary"><i class="bi bi-plus-lg me-1"></i>Create Kitten</a>
|
||||
<a href="/admin/devices/create" class="btn btn-primary"><i class="bi bi-plus-lg me-1"></i>Create Post</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card mb-4"><div class="card-header"><h6 class="mb-0">Kitten Info</h6></div><div class="card-body"><p class="mb-1"><strong>@Model.Name</strong></p><p class="mb-1 text-muted"><code>/@Model.Slug</code></p><p class="mb-0 text-muted">@(Model.ResolutionWidth)x@(Model.ResolutionHeight) · @Model.Transition</p></div></div>
|
||||
<div class="card mb-4"><div class="card-header"><h6 class="mb-0">Post Info</h6></div><div class="card-body"><p class="mb-1"><strong>@Model.Name</strong></p><p class="mb-1 text-muted"><code>/@Model.Slug</code></p><p class="mb-0 text-muted">@(Model.ResolutionWidth)x@(Model.ResolutionHeight) · @Model.Transition</p></div></div>
|
||||
<div class="card"><div class="card-header"><h6 class="mb-0"><i class="bi bi-plus-circle me-1"></i>Add Meow</h6></div><div class="card-body">
|
||||
@if (availableSlides != null && availableSlides.Any())
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Device Not Found — Sunbeam</title>
|
||||
<title>Post Not Found — Sunbeam</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<style>
|
||||
body { background: #0f0f1a; color: #e0e0e0; min-height: 100vh; display: flex; align-items: center; justify-content: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
||||
@@ -17,9 +17,9 @@
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Meow?</h1>
|
||||
<p class="text-muted mb-4">No kitten found at this address.</p>
|
||||
<p class="text-muted mb-4">No post found at this address.</p>
|
||||
@if (Model.Any()) {
|
||||
<h5 class="mb-3">Available Kittens:</h5>
|
||||
<h5 class="mb-3">Available Posts:</h5>
|
||||
<div class="device-list">@foreach (var d in Model) { <a href="/@d.Slug"><strong>@d.Name</strong><br /><code>/@d.Slug</code></a> }</div>
|
||||
} else { <p>No devices configured yet.</p> }
|
||||
<p class="mt-4"><a href="/admin" class="btn btn-outline-light btn-sm">Go to Admin</a></p>
|
||||
|
||||
@@ -7,18 +7,25 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=@(Model.ResolutionWidth), height=@(Model.ResolutionHeight), initial-scale=1" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>@Model.Name — Sunbeam Display</title>
|
||||
<link href="/css/display.css" rel="stylesheet" />
|
||||
<style>:root { --display-width: @(Model.ResolutionWidth)px; --display-height: @(Model.ResolutionHeight)px; }</style>
|
||||
<style>
|
||||
:root {
|
||||
--display-width: @(Model.ResolutionWidth)px;
|
||||
--display-height: @(Model.ResolutionHeight)px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body data-slug="@Model.Slug" data-transition="@Model.Transition">
|
||||
<div id="display-container">
|
||||
<div id="slide-a" class="slide-layer active"></div>
|
||||
<div id="slide-b" class="slide-layer"></div>
|
||||
<body data-slug="@Model.Slug" data-transition="@Model.Transition" data-width="@Model.ResolutionWidth" data-height="@Model.ResolutionHeight">
|
||||
<div id="scale-wrapper">
|
||||
<div id="display-container">
|
||||
<div id="slide-a" class="slide-layer active"></div>
|
||||
<div id="slide-b" class="slide-layer"></div>
|
||||
</div>
|
||||
<div id="clock-overlay"><span id="clock-time"></span><span id="clock-date"></span></div>
|
||||
</div>
|
||||
<div id="progress-bar"><div id="progress-fill"></div></div>
|
||||
<div id="clock-overlay"><span id="clock-time"></span><span id="clock-date"></span></div>
|
||||
<div id="status-dot" title="Connected"></div>
|
||||
<script>
|
||||
var playlistData = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(slides.Select(ds => new {
|
||||
|
||||
@@ -8,9 +8,8 @@
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet" />
|
||||
<link href="/css/admin.css" rel="stylesheet" />
|
||||
<script>
|
||||
// Apply saved theme immediately to prevent flash
|
||||
(function() {
|
||||
var t = localStorage.getItem('sb-theme') || 'dark';
|
||||
var t = localStorage.getItem('sb-theme') || 'light';
|
||||
document.documentElement.setAttribute('data-theme', t);
|
||||
})();
|
||||
</script>
|
||||
@@ -42,17 +41,25 @@
|
||||
<li>
|
||||
<a href="/admin/devices" class="@(ViewContext.RouteData.Values["controller"]?.ToString() == "Devices" ? "active" : "")">
|
||||
<i class="bi bi-display"></i>
|
||||
<span>Kittens</span>
|
||||
<span>Posts</span>
|
||||
<small class="nav-hint">Devices</small>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<small>Sunbeam v1.0</small>
|
||||
<button id="themeToggle" class="theme-toggle" title="Toggle theme">
|
||||
<i class="bi bi-sun"></i>
|
||||
</button>
|
||||
<div>
|
||||
<small>Sunbeam v1.0</small><br />
|
||||
<small style="opacity:0.5;">© Jess Rogerson — 2026</small>
|
||||
</div>
|
||||
<div class="d-flex gap-1">
|
||||
<button id="themeToggle" class="theme-toggle" title="Toggle theme">
|
||||
<i class="bi bi-moon-stars"></i>
|
||||
</button>
|
||||
<a href="/account/logout" class="theme-toggle" title="Sign Out">
|
||||
<i class="bi bi-box-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
+77
-10
@@ -35,7 +35,6 @@
|
||||
<div class="mb-3">
|
||||
<label asp-for="IcsSource" class="form-label">ICS Calendar Source</label>
|
||||
<input asp-for="IcsSource" class="form-control" placeholder="https://calendar.example.com/feed.ics" />
|
||||
<div class="form-text">Enter a URL to an .ics calendar feed, or upload an ICS file below.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Or Upload ICS File</label>
|
||||
@@ -65,13 +64,12 @@
|
||||
<div class="mb-3">
|
||||
<label asp-for="BackgroundSize" class="form-label">Image Sizing</label>
|
||||
<select asp-for="BackgroundSize" class="form-select">
|
||||
<option value="cover">Cover — fill entire slide, crop if needed</option>
|
||||
<option value="contain">Contain — fit whole image, may show background</option>
|
||||
<option value="fill">Fill — stretch to fit exactly</option>
|
||||
<option value="scale-down">Scale Down — shrink to fit, never enlarge</option>
|
||||
<option value="cover">Cover — fill, crop if needed</option>
|
||||
<option value="contain">Contain — fit whole image</option>
|
||||
<option value="fill">Fill — stretch to fit</option>
|
||||
<option value="scale-down">Scale Down — shrink only</option>
|
||||
<option value="auto">Auto — original size</option>
|
||||
</select>
|
||||
<div class="form-text">How the background image fills the slide area.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label asp-for="CustomCss" class="form-label">Custom CSS</label>
|
||||
@@ -104,19 +102,88 @@
|
||||
function initTinyMCE() {
|
||||
if (tinymce.get('contentEditor')) return;
|
||||
tinymce.init({
|
||||
selector: '#contentEditor', height: 500,
|
||||
selector: '#contentEditor',
|
||||
height: 500,
|
||||
license_key: 'gpl',
|
||||
menubar: 'file edit view insert format table',
|
||||
plugins: 'advlist autolink lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table help wordcount',
|
||||
toolbar: 'undo redo | blocks | bold italic forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | image media table | removeformat code fullscreen',
|
||||
toolbar: 'undo redo | blocks fontsize | bold italic forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | image media table | removeformat code fullscreen',
|
||||
font_size_formats: '8pt 10pt 12pt 14pt 16pt 18pt 20pt 24pt 28pt 32pt 36pt 48pt 64pt 72pt 96pt',
|
||||
images_upload_handler: function (blobInfo) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var fd = new FormData(); fd.append('file', blobInfo.blob(), blobInfo.filename());
|
||||
fetch('/api/upload', { method: 'POST', body: fd }).then(r => r.json()).then(d => resolve(d.location)).catch(e => reject(e));
|
||||
});
|
||||
},
|
||||
content_style: 'body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 16px; color: #fff; background: #1a1a2e; }',
|
||||
skin: 'oxide-dark', content_css: 'dark', promotion: false, branding: false
|
||||
file_picker_types: 'image',
|
||||
file_picker_callback: function (cb, value, meta) {
|
||||
fetch('/api/listuploads').then(r => r.json()).then(function (files) {
|
||||
var input = document.createElement('input');
|
||||
input.type = 'file'; input.accept = 'image/*';
|
||||
|
||||
// If we have existing uploads, show a picker dialog
|
||||
if (files && files.length > 0) {
|
||||
var dialog = document.createElement('div');
|
||||
dialog.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);z-index:99999;display:flex;align-items:center;justify-content:center;';
|
||||
var panel = document.createElement('div');
|
||||
panel.style.cssText = 'background:#fff;border-radius:12px;padding:1.5em;max-width:700px;max-height:80vh;overflow-y:auto;width:90%;';
|
||||
panel.innerHTML = '<h4 style="margin:0 0 1em;color:#333;">Select Image or Upload New</h4>';
|
||||
|
||||
var grid = document.createElement('div');
|
||||
grid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px;margin-bottom:1em;';
|
||||
|
||||
files.forEach(function (f) {
|
||||
var thumb = document.createElement('div');
|
||||
thumb.style.cssText = 'cursor:pointer;border:2px solid #ddd;border-radius:8px;overflow:hidden;aspect-ratio:1;background:#f0f0f0;';
|
||||
thumb.innerHTML = '<img src="' + f.value + '" style="width:100%;height:100%;object-fit:cover;" />';
|
||||
thumb.title = f.title;
|
||||
thumb.addEventListener('click', function () {
|
||||
cb(f.value, { title: f.title });
|
||||
document.body.removeChild(dialog);
|
||||
});
|
||||
thumb.addEventListener('mouseenter', function () { this.style.borderColor = '#3b6fd4'; });
|
||||
thumb.addEventListener('mouseleave', function () { this.style.borderColor = '#ddd'; });
|
||||
grid.appendChild(thumb);
|
||||
});
|
||||
|
||||
panel.appendChild(grid);
|
||||
|
||||
var uploadBtn = document.createElement('button');
|
||||
uploadBtn.textContent = 'Upload New Image';
|
||||
uploadBtn.style.cssText = 'background:#3b6fd4;color:#fff;border:none;padding:0.6em 1.5em;border-radius:6px;cursor:pointer;margin-right:0.5em;';
|
||||
uploadBtn.addEventListener('click', function () {
|
||||
document.body.removeChild(dialog);
|
||||
input.click();
|
||||
});
|
||||
|
||||
var cancelBtn = document.createElement('button');
|
||||
cancelBtn.textContent = 'Cancel';
|
||||
cancelBtn.style.cssText = 'background:#eee;color:#333;border:none;padding:0.6em 1.5em;border-radius:6px;cursor:pointer;';
|
||||
cancelBtn.addEventListener('click', function () { document.body.removeChild(dialog); });
|
||||
|
||||
panel.appendChild(uploadBtn);
|
||||
panel.appendChild(cancelBtn);
|
||||
dialog.appendChild(panel);
|
||||
dialog.addEventListener('click', function (e) { if (e.target === dialog) document.body.removeChild(dialog); });
|
||||
document.body.appendChild(dialog);
|
||||
} else {
|
||||
input.click();
|
||||
}
|
||||
|
||||
input.addEventListener('change', function () {
|
||||
if (!this.files[0]) return;
|
||||
var fd = new FormData(); fd.append('file', this.files[0]);
|
||||
fetch('/api/upload', { method: 'POST', body: fd })
|
||||
.then(r => r.json())
|
||||
.then(d => cb(d.location, { title: this.files[0].name }));
|
||||
});
|
||||
}).catch(function () { input.click(); });
|
||||
},
|
||||
content_style: 'body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 16px; color: #000; background: #fff; }',
|
||||
skin: 'oxide',
|
||||
content_css: 'default',
|
||||
promotion: false,
|
||||
branding: false
|
||||
});
|
||||
}
|
||||
if (document.getElementById('slideType').value === '0') initTinyMCE();
|
||||
|
||||
@@ -65,13 +65,12 @@
|
||||
<div class="mb-3">
|
||||
<label asp-for="BackgroundSize" class="form-label">Image Sizing</label>
|
||||
<select asp-for="BackgroundSize" class="form-select">
|
||||
<option value="cover">Cover — fill entire slide, crop if needed</option>
|
||||
<option value="contain">Contain — fit whole image, may show background</option>
|
||||
<option value="fill">Fill — stretch to fit exactly</option>
|
||||
<option value="scale-down">Scale Down — shrink to fit, never enlarge</option>
|
||||
<option value="cover">Cover — fill, crop if needed</option>
|
||||
<option value="contain">Contain — fit whole image</option>
|
||||
<option value="fill">Fill — stretch to fit</option>
|
||||
<option value="scale-down">Scale Down — shrink only</option>
|
||||
<option value="auto">Auto — original size</option>
|
||||
</select>
|
||||
<div class="form-text">How the background image fills the slide area.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label asp-for="CustomCss" class="form-label">Custom CSS</label>
|
||||
@@ -103,19 +102,68 @@
|
||||
function initTinyMCE() {
|
||||
if (tinymce.get('contentEditor')) return;
|
||||
tinymce.init({
|
||||
selector: '#contentEditor', height: 500,
|
||||
selector: '#contentEditor',
|
||||
height: 500,
|
||||
license_key: 'gpl',
|
||||
menubar: 'file edit view insert format table',
|
||||
plugins: 'advlist autolink lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table help wordcount',
|
||||
toolbar: 'undo redo | blocks | bold italic forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | image media table | removeformat code fullscreen',
|
||||
toolbar: 'undo redo | blocks fontsize | bold italic forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | image media table | removeformat code fullscreen',
|
||||
font_size_formats: '8pt 10pt 12pt 14pt 16pt 18pt 20pt 24pt 28pt 32pt 36pt 48pt 64pt 72pt 96pt',
|
||||
images_upload_handler: function (blobInfo) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var fd = new FormData(); fd.append('file', blobInfo.blob(), blobInfo.filename());
|
||||
fetch('/api/upload', { method: 'POST', body: fd }).then(r => r.json()).then(d => resolve(d.location)).catch(e => reject(e));
|
||||
});
|
||||
},
|
||||
content_style: 'body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 16px; color: #fff; background: #1a1a2e; }',
|
||||
skin: 'oxide-dark', content_css: 'dark', promotion: false, branding: false
|
||||
file_picker_types: 'image',
|
||||
file_picker_callback: function (cb, value, meta) {
|
||||
fetch('/api/listuploads').then(r => r.json()).then(function (files) {
|
||||
var input = document.createElement('input');
|
||||
input.type = 'file'; input.accept = 'image/*';
|
||||
if (files && files.length > 0) {
|
||||
var dialog = document.createElement('div');
|
||||
dialog.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);z-index:99999;display:flex;align-items:center;justify-content:center;';
|
||||
var panel = document.createElement('div');
|
||||
panel.style.cssText = 'background:#fff;border-radius:12px;padding:1.5em;max-width:700px;max-height:80vh;overflow-y:auto;width:90%;';
|
||||
panel.innerHTML = '<h4 style="margin:0 0 1em;color:#333;">Select Image or Upload New</h4>';
|
||||
var grid = document.createElement('div');
|
||||
grid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px;margin-bottom:1em;';
|
||||
files.forEach(function (f) {
|
||||
var thumb = document.createElement('div');
|
||||
thumb.style.cssText = 'cursor:pointer;border:2px solid #ddd;border-radius:8px;overflow:hidden;aspect-ratio:1;background:#f0f0f0;';
|
||||
thumb.innerHTML = '<img src="' + f.value + '" style="width:100%;height:100%;object-fit:cover;" />';
|
||||
thumb.title = f.title;
|
||||
thumb.addEventListener('click', function () { cb(f.value, { title: f.title }); document.body.removeChild(dialog); });
|
||||
thumb.addEventListener('mouseenter', function () { this.style.borderColor = '#3b6fd4'; });
|
||||
thumb.addEventListener('mouseleave', function () { this.style.borderColor = '#ddd'; });
|
||||
grid.appendChild(thumb);
|
||||
});
|
||||
panel.appendChild(grid);
|
||||
var uploadBtn = document.createElement('button');
|
||||
uploadBtn.textContent = 'Upload New Image';
|
||||
uploadBtn.style.cssText = 'background:#3b6fd4;color:#fff;border:none;padding:0.6em 1.5em;border-radius:6px;cursor:pointer;margin-right:0.5em;';
|
||||
uploadBtn.addEventListener('click', function () { document.body.removeChild(dialog); input.click(); });
|
||||
var cancelBtn = document.createElement('button');
|
||||
cancelBtn.textContent = 'Cancel';
|
||||
cancelBtn.style.cssText = 'background:#eee;color:#333;border:none;padding:0.6em 1.5em;border-radius:6px;cursor:pointer;';
|
||||
cancelBtn.addEventListener('click', function () { document.body.removeChild(dialog); });
|
||||
panel.appendChild(uploadBtn); panel.appendChild(cancelBtn);
|
||||
dialog.appendChild(panel);
|
||||
dialog.addEventListener('click', function (e) { if (e.target === dialog) document.body.removeChild(dialog); });
|
||||
document.body.appendChild(dialog);
|
||||
} else { input.click(); }
|
||||
input.addEventListener('change', function () {
|
||||
if (!this.files[0]) return;
|
||||
var fd = new FormData(); fd.append('file', this.files[0]);
|
||||
fetch('/api/upload', { method: 'POST', body: fd }).then(r => r.json()).then(d => cb(d.location, { title: this.files[0].name }));
|
||||
});
|
||||
}).catch(function () { input.click(); });
|
||||
},
|
||||
content_style: 'body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 16px; color: #000; background: #fff; }',
|
||||
skin: 'oxide',
|
||||
content_css: 'default',
|
||||
promotion: false,
|
||||
branding: false
|
||||
});
|
||||
}
|
||||
if (document.getElementById('slideType').value === '0') initTinyMCE();
|
||||
|
||||
Reference in New Issue
Block a user