Files
mediaplayer/admin/edit.php
T
Ty Clifford dc71d26c2a -Implemented YouTube timestamp correction and added the admin Upkeeping section.
What changed:
Added shared YouTube publish-date scraping helpers in 
[includes/db.php](/Users/tyemeclifford/Documents/GH/mediaplayer/includes/db.php).
New YouTube imports now set videos.created_at to the original YouTube 
publish date when available.
Manual Add/Edit saves now also try to correct timestamps for 
YouTube-backed videos.
Added Admin Dashboard → Upkeeping → Correct YouTube timestamps in 
[admin/index.php](/Users/tyemeclifford/Documents/GH/mediaplayer/admin/index.php), 
with batch size control for backfilling existing records.
Renamed the admin table column from Added to Date, since it may now 
reflect original publish date.
Documented the behavior in 
[README.md](/Users/tyemeclifford/Documents/GH/mediaplayer/README.md).
2026-06-24 13:40:03 -04:00

514 lines
27 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);
$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=?, published=?,
updated_at=datetime('now')
WHERE id=?
")->execute([$title, $description, $slug, $thumbnail, $duration, $sort_order, $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">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>