Files
mediaplayer/admin/edit.php
T
Ty Clifford 2d52255b93 - What changed:
Added external_view_count so provider totals from YouTube/Vimeo/etc are 
added to the site’s total without double-counting local plays.
Updated public totals and per-video public view displays to use site 
views + external views.
Changed Upkeeping action to Correct external view counts with a Correct 
Views button.
Expanded provider scraping beyond YouTube with generic 
page/JSON-LD/oEmbed patterns for readable Vimeo, Dailymotion, and 
similar pages.
Improved Add Video:Title no longer has to be manually entered first.
External URL rows now have Fetch Info.
Metadata can fill title, description, duration, source label, and 
external view count.
Save also retries metadata extraction server-side.

Added authenticated metadata endpoint: 
[admin/media_info.php](/Users/tyemeclifford/Documents/GH/mediaplayer/admin/media_info.php).
Updated docs in 
[README.md](/Users/tyemeclifford/Documents/GH/mediaplayer/README.md).
2026-06-25 04:40:59 -04:00

528 lines
28 KiB
PHP

<?php
require_once __DIR__ . '/auth.php';
require_once __DIR__ . '/../includes/layout.php';
admin_require_auth();
$db = get_db();
$id = (int)($_GET['id'] ?? 0);
$video = get_video_by_id($id);
if (!$video) {
header('Location: index.php');
exit;
}
$error = '';
$msg = '';
if (isset($_GET['saved'])) $msg = 'Video saved successfully.';
// Handle delete source
if (isset($_GET['del_src'])) {
$sid = (int)$_GET['del_src'];
$src = $db->prepare("SELECT * FROM video_sources WHERE id=? AND video_id=?");
$src->execute([$sid, $id]);
$s = $src->fetch();
if ($s) {
if (source_is_local($s)) {
$f = MEDIA_DIR . $s['file_path'];
if ($s['file_path'] && file_exists($f)) unlink($f);
}
$db->prepare("DELETE FROM video_sources WHERE id=?")->execute([$sid]);
$msg = 'Source removed.';
}
$video = get_video_by_id($id); // refresh
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? 'save';
if ($action === 'save') {
$title = trim($_POST['title'] ?? '');
$description = trim($_POST['description'] ?? '');
$duration = (int)($_POST['duration'] ?? 0);
$sort_order = (int)($_POST['sort_order'] ?? 0);
$view_count = max(0, (int)($_POST['view_count'] ?? $video['view_count']));
$external_view_count = max(0, (int)($_POST['external_view_count'] ?? $video['external_view_count']));
$published = isset($_POST['published']) ? 1 : 0;
$auto_thumb = isset($_POST['auto_thumbnail']);
if (!$title) {
$error = 'Title is required.';
} else {
$slug = make_slug($title, $id);
// Handle thumbnail replacement
$thumbnail = $video['thumbnail'];
$uploaded_thumbnail = false;
if (!empty($_FILES['thumbnail']['tmp_name'])) {
$ext = strtolower(pathinfo($_FILES['thumbnail']['name'], PATHINFO_EXTENSION));
$allowed = ['jpg','jpeg','png','webp','gif'];
if (in_array($ext, $allowed)) {
$thumb_dir = MEDIA_DIR . 'thumbs/';
if (!is_dir($thumb_dir)) mkdir($thumb_dir, 0755, true);
// Remove old thumb
if ($thumbnail && file_exists(MEDIA_DIR . $thumbnail)) unlink(MEDIA_DIR . $thumbnail);
$fname = $slug . '_thumb_' . time() . '.' . $ext;
if (move_uploaded_file($_FILES['thumbnail']['tmp_name'], $thumb_dir . $fname)) {
$thumbnail = 'thumbs/' . $fname;
$uploaded_thumbnail = true;
}
} else {
$error = 'Thumbnail must be JPG, PNG, WebP, or GIF.';
}
}
if (!$error) {
$db->prepare("
UPDATE videos SET title=?, description=?, slug=?, thumbnail=?,
duration=?, sort_order=?, view_count=?, external_view_count=?, published=?,
updated_at=datetime('now')
WHERE id=?
")->execute([$title, $description, $slug, $thumbnail, $duration, $sort_order, $view_count, $external_view_count, $published, $id]);
// Update existing sources sort/label/quality
$upd_labels = $_POST['src_label'] ?? [];
$upd_quality = $_POST['src_quality'] ?? [];
$upd_sort = $_POST['src_sort'] ?? [];
$upd_urls = $_POST['src_url'] ?? [];
$upd_types = $_POST['src_type'] ?? [];
$upd_ids = $_POST['src_id'] ?? [];
foreach ($upd_ids as $i => $sid) {
$source_type = ($upd_types[$i] ?? 'local') === 'remote' ? 'remote' : 'local';
$source_url = '';
if ($source_type === 'remote') {
$source_url = normalize_media_url($upd_urls[$i] ?? '');
if ($source_url === '') {
$error = 'External video URLs must start with http:// or https://.';
break;
}
}
$params = [
$upd_labels[$i] ?? '',
$upd_quality[$i] ?? '',
(int)($upd_sort[$i] ?? $i),
];
$sql = "UPDATE video_sources SET label=?, quality=?, sort_order=?";
if ($source_type === 'remote') {
$sql .= ", source_url=?, mime_type=?";
$params[] = $source_url;
$params[] = source_storage_mime_from_url($source_url);
}
$sql .= " WHERE id=? AND video_id=?";
$params[] = (int)$sid;
$params[] = $id;
$db->prepare($sql)->execute($params);
}
// Add new sources
if (!$error) {
$new_types = $_POST['new_source_type'] ?? [];
$new_labels = $_POST['new_source_label'] ?? [];
$new_quality = $_POST['new_source_quality'] ?? [];
$new_sort = $_POST['new_source_sort'] ?? [];
$new_urls = $_POST['new_source_url'] ?? [];
$new_files = $_FILES['new_source_file'] ?? [];
$videos_dir = MEDIA_DIR . 'videos/';
if (!is_dir($videos_dir)) mkdir($videos_dir, 0755, true);
$allowed_video = ['mp4','webm','ogv','ogg','mov','mkv','avi'];
$new_count = max(
count($new_types),
count($new_labels),
count($new_urls),
isset($new_files['tmp_name']) && is_array($new_files['tmp_name']) ? count($new_files['tmp_name']) : 0
);
$moved_files = [];
$inserted_source_ids = [];
for ($i = 0; $i < $new_count; $i++) {
$type = ($new_types[$i] ?? 'local') === 'remote' ? 'remote' : 'local';
$label = trim($new_labels[$i] ?? '');
$quality = trim($new_quality[$i] ?? '720p');
$sort = (int)($new_sort[$i] ?? $i);
if ($type === 'remote') {
$url = normalize_media_url($new_urls[$i] ?? '');
if ($url === '') {
if (trim($new_urls[$i] ?? '') !== '') $error = 'External video URLs must start with http:// or https://.';
continue;
}
$db->prepare("
INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order)
VALUES (?,?,?,?,?,?,?,?)
")->execute([$id, $label, 'remote', '', $url, source_storage_mime_from_url($url), $quality, $sort]);
$inserted_source_ids[] = (int)$db->lastInsertId();
continue;
}
$tmp = $new_files['tmp_name'][$i] ?? '';
if (empty($tmp) || !is_uploaded_file($tmp)) continue;
$orig = $new_files['name'][$i] ?? '';
$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed_video, true)) continue;
$fname = $slug . '_' . time() . '_' . $i . '.' . $ext;
if (move_uploaded_file($tmp, $videos_dir . $fname)) {
$moved_files[] = $videos_dir . $fname;
$db->prepare("
INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order)
VALUES (?,?,?,?,?,?,?,?)
")->execute([$id, $label, 'local', 'videos/' . $fname, '', mime_from_ext($fname), $quality, $sort]);
$inserted_source_ids[] = (int)$db->lastInsertId();
}
}
if ($error) {
foreach ($inserted_source_ids as $inserted_id) {
$db->prepare("DELETE FROM video_sources WHERE id=? AND video_id=?")->execute([$inserted_id, $id]);
}
foreach ($moved_files as $moved) {
if (file_exists($moved)) unlink($moved);
}
} else {
if ($auto_thumb && !$uploaded_thumbnail) {
$auto_thumbnail = media_auto_thumbnail_for_video($id, $slug);
if ($auto_thumbnail !== '') {
if ($thumbnail && file_exists(MEDIA_DIR . $thumbnail)) unlink(MEDIA_DIR . $thumbnail);
$thumbnail = $auto_thumbnail;
$db->prepare("UPDATE videos SET thumbnail=?, updated_at=datetime('now') WHERE id=?")->execute([$thumbnail, $id]);
}
}
media_apply_youtube_timestamp($id);
header('Location: edit.php?id=' . $id . '&saved=1');
exit;
}
}
}
}
}
}
// Refresh
$video = get_video_by_id($id);
render_head('Edit Video — Admin', '
.form-grid { display:grid; grid-template-columns:1fr; gap:1.2rem; }
@media(min-width:700px) { .form-grid { grid-template-columns:1fr 1fr; } }
.form-grid .span2 { grid-column:1/-1; }
.section-heading { font-family:"Share Tech Mono",monospace; font-size:.65rem; letter-spacing:.15em; text-transform:uppercase; color:var(--text-3); margin-bottom:.75rem; }
.hint { font-size:.72rem; color:var(--text-3); margin-top:.2rem; }
.source-list { display:flex; flex-direction:column; gap:.75rem; }
.source-item {
background:var(--surface-2); border:1px solid var(--border);
border-radius:var(--r); padding:.85rem 1rem;
}
.source-item-header { display:flex; align-items:center; justify-content:space-between; gap:.75rem; margin-bottom:.75rem; flex-wrap:wrap; }
.source-item-file { font-family:"Share Tech Mono",monospace; font-size:.68rem; color:var(--text-2); word-break:break-all; }
.source-item-mime { font-family:"Share Tech Mono",monospace; font-size:.6rem; color:var(--text-3); }
.source-item-fields { display:grid; grid-template-columns:1.5fr 1fr 1fr 80px; gap:.6rem; }
.source-item-fields .span4 { grid-column:1/-1; }
@media(max-width:760px) { .source-item-fields { grid-template-columns:1fr 1fr; } }
@media(max-width:560px) { .source-item-fields { grid-template-columns:1fr; } }
.source-badge { display:inline-flex; align-items:center; padding:.16em .5em; border-radius:99px; font-family:"Share Tech Mono",monospace; font-size:.56rem; letter-spacing:.08em; text-transform:uppercase; border:1px solid var(--border-2); color:var(--text-3); margin-bottom:.25rem; }
.source-badge-remote { color:var(--blue); border-color:rgba(0,200,255,.35); background:rgba(0,200,255,.08); }
.source-badge-local { color:var(--green); border-color:rgba(0,232,122,.3); background:rgba(0,232,122,.08); }
.add-source-row { background:var(--surface-2); border:1px solid var(--border-2); border-radius:var(--r); padding:1rem; display:grid; grid-template-columns:140px minmax(220px,1.5fr) 1fr 1fr 80px auto; gap:.75rem; align-items:end; }
@media(max-width:900px) { .add-source-row { grid-template-columns:1fr 1fr; } }
@media(max-width:560px) { .add-source-row { grid-template-columns:1fr; } }
.source-location-panel[hidden] { display:none; }
.admin-layout { display:grid; grid-template-columns:1fr; gap:1.5rem; }
@media(min-width:900px) { .admin-layout { grid-template-columns:220px 1fr; } }
.admin-nav { background:var(--surface); border:1px solid var(--border); border-radius:calc(var(--r)+2px); padding:1.2rem; display:flex; flex-direction:column; gap:.4rem; align-self:start; position:sticky; top:1rem; }
.admin-nav-label { font-family:"Share Tech Mono",monospace; font-size:.58rem; letter-spacing:.18em; text-transform:uppercase; color:var(--text-3); margin-bottom:.3rem; padding-left:.5rem; }
.admin-nav a { display:flex; align-items:center; gap:.6rem; padding:.55rem .75rem; border-radius:var(--r); font-size:.82rem; color:var(--text-2); text-decoration:none; transition:background .15s,color .15s; }
.admin-nav a:hover { background:rgba(255,255,255,.05); color:var(--text); }
.nav-divider { border:none; border-top:1px solid var(--border); margin:.4rem 0; }
.thumb-preview { width:100%; max-width:240px; aspect-ratio:16/9; object-fit:cover; border-radius:var(--r); border:1px solid var(--border); background:var(--surface-2); display:block; margin-bottom:.5rem; }
');
?>
<main class="page" role="main">
<header class="topbar">
<div class="topbar-brand">
<div>
<div class="brand-name">Ty Clifford</div>
<div class="brand-sub">admin / edit video</div>
</div>
</div>
<nav class="topbar-social">
<a class="social-pill" href="<?= h(public_absolute_url(public_video_url($video['slug']))) ?>" target="_blank">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15,3 21,3 21,9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
Preview
</a>
<a class="social-pill" href="logout.php">Logout</a>
</nav>
</header>
<?php if ($msg): ?>
<div class="notice notice-success"><?= h($msg) ?></div>
<?php endif; ?>
<?php if ($error): ?>
<div class="notice notice-error"><?= h($error) ?></div>
<?php endif; ?>
<div class="admin-layout">
<aside class="admin-nav">
<div class="admin-nav-label">Navigation</div>
<a href="index.php">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
Videos
</a>
<a href="add.php">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
Add Video
</a>
<a href="youtube_import.php">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22.54 6.42a2.78 2.78 0 00-1.95-2C18.88 4 12 4 12 4s-6.88 0-8.59.46a2.78 2.78 0 00-1.95 2A29 29 0 001 12a29 29 0 00.46 5.58 2.78 2.78 0 001.95 2C5.12 20 12 20 12 20s6.88 0 8.59-.46a2.78 2.78 0 001.95-2A29 29 0 0023 12a29 29 0 00-.46-5.58z"/><polygon points="10,15 15,12 10,9"/></svg>
YouTube Importer
</a>
<hr class="nav-divider">
<a href="settings.php">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93l-1.41 1.41M4.93 4.93l1.41 1.41M19.07 19.07l-1.41-1.41M4.93 19.07l1.41-1.41M12 2v2M12 20v2M2 12h2M20 12h2"/></svg>
Settings
</a>
</aside>
<div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.25rem;flex-wrap:wrap;gap:.75rem;">
<div>
<p class="eyebrow eyebrow-purple" style="margin-bottom:.3rem">Editing</p>
<h1 style="font-size:1.05rem;font-weight:700;color:var(--text);"><?= h($video['title']) ?></h1>
</div>
<a href="index.php" class="btn btn-secondary btn-sm">← Back</a>
</div>
<form method="post" action="edit.php?id=<?= $id ?>" enctype="multipart/form-data">
<input type="hidden" name="action" value="save">
<!-- ── Details ── -->
<div class="card" style="margin-bottom:1rem;">
<p class="section-heading">Video Details</p>
<div class="form-grid">
<div class="form-group span2">
<label class="form-label" for="title">Title *</label>
<input class="form-input" type="text" id="title" name="title"
value="<?= h($video['title']) ?>" required>
</div>
<div class="form-group span2">
<label class="form-label" for="description">Description</label>
<textarea class="form-textarea" id="description" name="description" rows="3"><?= h($video['description']) ?></textarea>
</div>
<div class="form-group">
<label class="form-label">Slug (auto-generated)</label>
<input class="form-input" type="text" value="<?= h($video['slug']) ?>" disabled style="opacity:.5;">
</div>
<div class="form-group">
<label class="form-label" for="duration">Duration (seconds)</label>
<input class="form-input" type="number" id="duration" name="duration"
value="<?= (int)$video['duration'] ?>" min="0">
</div>
<div class="form-group">
<label class="form-label" for="sort_order">Sort Order</label>
<input class="form-input" type="number" id="sort_order" name="sort_order"
value="<?= (int)$video['sort_order'] ?>" min="0">
</div>
<div class="form-group">
<label class="form-label" for="view_count">Site View Count</label>
<input class="form-input" type="number" id="view_count" name="view_count"
value="<?= (int)$video['view_count'] ?>" min="0">
<span class="hint">Local media-site plays. Use this to restore older local videos manually.</span>
</div>
<div class="form-group">
<label class="form-label" for="external_view_count">External View Count</label>
<input class="form-input" type="number" id="external_view_count" name="external_view_count"
value="<?= (int)$video['external_view_count'] ?>" min="0">
<span class="hint">Provider views from YouTube, Vimeo, and other readable external pages. Upkeeping can correct this automatically.</span>
</div>
<div class="form-group">
<label class="form-label">Published</label>
<label style="display:flex;align-items:center;gap:.6rem;cursor:pointer;margin-top:.35rem;">
<input type="checkbox" name="published" value="1" <?= $video['published'] ? 'checked' : '' ?>>
<span style="font-size:.82rem;color:var(--text-2);">Published</span>
</label>
</div>
<div class="form-group span2">
<label class="form-label">Thumbnail</label>
<?php if ($video['thumbnail']): ?>
<img class="thumb-preview" src="<?= h('../' . MEDIA_URL . $video['thumbnail']) ?>" alt="Current thumbnail">
<span class="hint" style="display:block;margin-bottom:.5rem;">Current: <?= h($video['thumbnail']) ?></span>
<?php endif; ?>
<input class="form-input" type="file" id="thumbnail" name="thumbnail"
accept="image/jpeg,image/png,image/webp,image/gif" style="padding:.45rem .6rem;">
<label style="display:flex;align-items:center;gap:.55rem;margin-top:.55rem;cursor:pointer;">
<input type="checkbox" name="auto_thumbnail" value="1" <?= isset($_POST['auto_thumbnail']) || ($_SERVER['REQUEST_METHOD'] !== 'POST' && !$video['thumbnail']) ? 'checked' : '' ?>>
<span style="font-size:.78rem;color:var(--text-2);">Auto-grab/refresh thumbnail from source on save</span>
</label>
<span class="hint">Upload a new image to override auto-grab. Auto-grab supports YouTube, Vimeo, Dailymotion and other oEmbed providers; local uploads use ffmpeg when available.</span>
</div>
</div>
</div>
<!-- ── Existing sources ── -->
<?php if (!empty($video['sources'])): ?>
<div class="card" style="margin-bottom:1rem;">
<p class="section-heading">Existing Sources</p>
<div class="source-list">
<?php foreach ($video['sources'] as $i => $s): ?>
<div class="source-item">
<div class="source-item-header">
<div>
<span class="source-badge <?= source_is_remote($s) ? 'source-badge-remote' : 'source-badge-local' ?>">
<?= h(source_provider_label($s)) ?>
</span>
<div class="source-item-file"><?= h(source_location($s)) ?></div>
<div class="source-item-mime"><?= h($s['mime_type']) ?></div>
</div>
<a href="edit.php?id=<?= $id ?>&del_src=<?= $s['id'] ?>"
class="btn btn-danger btn-sm"
onclick="return confirm('Remove this source?<?= source_is_local($s) ? ' Uploaded files are also deleted from disk.' : '' ?>')">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3,6 5,6 21,6"/><path d="M19 6l-1 14H6L5 6"/><path d="M9 6V4h6v2"/></svg>
Remove
</a>
</div>
<div class="source-item-fields">
<input type="hidden" name="src_id[]" value="<?= $s['id'] ?>">
<input type="hidden" name="src_type[]" value="<?= h($s['source_type'] ?? 'local') ?>">
<?php if (source_is_remote($s)): ?>
<div class="form-group span4">
<label class="form-label">External URL</label>
<input class="form-input" type="url" name="src_url[]" value="<?= h($s['source_url']) ?>">
</div>
<?php else: ?>
<input type="hidden" name="src_url[]" value="">
<?php endif; ?>
<div class="form-group">
<label class="form-label">Quality</label>
<select class="form-select" name="src_quality[]">
<?php foreach (['1080p','720p','480p','360p','4K',''] as $q): ?>
<option value="<?= $q ?>" <?= $s['quality']===$q ? 'selected' : '' ?>><?= $q ?: '—' ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">Label</label>
<input class="form-input" type="text" name="src_label[]" value="<?= h($s['label']) ?>" placeholder="e.g. HD">
</div>
<div class="form-group">
<label class="form-label">Sort</label>
<input class="form-input" type="number" name="src_sort[]" value="<?= (int)$s['sort_order'] ?>" min="0">
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- ── Add new sources ── -->
<div class="card" style="margin-bottom:1rem;">
<p class="section-heading">Add New Sources</p>
<p class="hint" style="margin-bottom:.85rem;">Upload another file, link a direct media file, or paste a provider page URL such as YouTube or Vimeo.</p>
<div class="source-list" id="new-sources-container">
<div class="add-source-row" data-new-row="0">
<div class="form-group">
<label class="form-label">Source</label>
<select class="form-select source-type-select" name="new_source_type[]">
<option value="local" selected>Upload</option>
<option value="remote">External URL / Embed</option>
</select>
</div>
<div class="form-group source-location-panel source-location-local">
<label class="form-label">Video File</label>
<input class="form-input" type="file" name="new_source_file[]"
accept="video/mp4,video/webm,video/ogg,.mp4,.webm,.ogv,.mov,.mkv,.avi"
style="padding:.45rem .6rem;">
</div>
<div class="form-group source-location-panel source-location-remote" hidden>
<label class="form-label">External URL</label>
<input class="form-input" type="url" name="new_source_url[]" placeholder="https://www.youtube.com/watch?v=...">
</div>
<div class="form-group">
<label class="form-label">Quality</label>
<select class="form-select" name="new_source_quality[]">
<option value="1080p">1080p</option>
<option value="720p" selected>720p</option>
<option value="480p">480p</option>
<option value="360p">360p</option>
<option value="4K">4K</option>
<option value="">—</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Label</label>
<input class="form-input" type="text" name="new_source_label[]" placeholder="e.g. HD">
</div>
<div class="form-group">
<label class="form-label">Sort</label>
<input class="form-input" type="number" name="new_source_sort[]" value="0" min="0" style="width:70px;">
</div>
</div>
</div>
<button type="button" id="add-new-source" class="btn btn-secondary btn-sm" style="margin-top:.75rem;align-self:flex-start;">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Add Another Source
</button>
</div>
<div style="display:flex;gap:.75rem;flex-wrap:wrap;">
<button class="btn btn-primary" type="submit">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17,21 17,13 7,13 7,21"/><polyline points="7,3 7,8 15,8"/></svg>
Save Changes
</button>
<a href="index.php" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>
<?php render_footer(); ?>
</main>
<script>
function toggleSourceRow(row) {
var select = row.querySelector('.source-type-select');
var isRemote = select && select.value === 'remote';
row.querySelectorAll('.source-location-local').forEach(function(el){ el.hidden = isRemote; });
row.querySelectorAll('.source-location-remote').forEach(function(el){ el.hidden = !isRemote; });
}
document.querySelectorAll('[data-new-row]').forEach(function(row) {
toggleSourceRow(row);
var select = row.querySelector('.source-type-select');
if (select) select.addEventListener('change', function(){ toggleSourceRow(row); });
});
document.getElementById('add-new-source').addEventListener('click', function() {
var container = document.getElementById('new-sources-container');
var rows = container.querySelectorAll('[data-new-row]');
var idx = rows.length;
var clone = rows[0].cloneNode(true);
clone.setAttribute('data-new-row', idx);
clone.querySelectorAll('input[type="file"]').forEach(function(el){ el.value=''; });
clone.querySelectorAll('input[type="text"]').forEach(function(el){ el.value=''; });
clone.querySelectorAll('input[type="url"]').forEach(function(el){ el.value=''; });
clone.querySelectorAll('input[type="number"]').forEach(function(el){ el.value=idx; });
clone.querySelectorAll('.source-type-select').forEach(function(el){ el.value='local'; });
if (!clone.querySelector('.remove-row')) {
var btn = document.createElement('button');
btn.type='button'; btn.className='btn btn-danger btn-sm remove-row'; btn.style='align-self:flex-end'; btn.innerHTML='✕';
btn.onclick=function(){ this.closest('[data-new-row]').remove(); };
clone.appendChild(btn);
}
toggleSourceRow(clone);
var select = clone.querySelector('.source-type-select');
if (select) select.addEventListener('change', function(){ toggleSourceRow(clone); });
container.appendChild(clone);
});
</script>
</body>
</html>