feat(playlist): import UI with drag reorder + random/sequential ID toggle (STEP 5)

Adds /op/folder/:top/:sub/playlist/new page that lets the operator paste a
YouTube playlist URL, probe entries via yt-dlp, reorder them by drag, edit
title/ID per row, and choose between random IDs or zero-padded sequential
IDs before registering all jobs in one batch.
This commit is contained in:
Claude (owner)
2026-06-01 00:15:39 +09:00
parent 36c9899040
commit 31c754a3ad
4 changed files with 610 additions and 0 deletions

View File

@@ -312,3 +312,90 @@ body.siteBody.centerLayout { display: flex; align-items: center; justify-content
}
.adminFolder { position: relative; }
.adminVideo { position: relative; }
/* ─── 플레이리스트 import ────────────────────────────────────────────── */
.playlistProbeSection {
background: var(--bg-card); border: 1px solid var(--border);
border-radius: 12px; padding: 18px 20px;
}
.playlistProbeRow {
display: flex; gap: 10px; margin-top: 10px; flex-wrap: wrap;
}
.playlistProbeRow input[type="text"] {
flex: 1; min-width: 320px;
background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
color: var(--text); padding: 8px 12px; font-size: 14px;
}
.playlistEntriesSection {
margin-top: 18px;
display: flex; flex-direction: column; gap: 14px;
}
.playlistControlsRow {
display: flex; align-items: center; justify-content: space-between;
flex-wrap: wrap; gap: 12px;
}
.idModeBox {
display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
background: var(--bg-card); border: 1px solid var(--border);
border-radius: 10px; padding: 10px 14px;
}
.idModeLabel { color: var(--text-muted); font-size: 13px; }
.idModeBox label { color: var(--text); font-size: 14px; cursor: pointer; }
.zeroPadWrap { margin-left: 8px; color: var(--text-muted) !important; font-size: 13px; }
.playlistSummary { color: var(--text-muted); font-size: 13px; }
.playlistEntryList {
list-style: none; margin: 0; padding: 0;
display: flex; flex-direction: column; gap: 6px;
}
.playlistEntryRow {
display: grid;
grid-template-columns: 26px 36px 1fr 70px 28px;
align-items: center; gap: 10px;
background: var(--bg-card); border: 1px solid var(--border);
border-radius: 10px; padding: 8px 10px;
}
.playlistEntryRow.dragGhost { opacity: 0.5; }
.dragHandle {
cursor: grab; color: var(--text-muted); font-size: 18px;
user-select: none; text-align: center;
}
.entryNum {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
color: var(--text-muted); font-size: 13px; text-align: right;
}
.entryFields {
display: flex; flex-direction: column; gap: 4px; min-width: 0;
}
.entryTitleInput {
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
color: var(--text); padding: 5px 8px; font-size: 14px; width: 100%;
}
.entryIdRow {
display: flex; align-items: center; gap: 8px;
}
.entryIdInput {
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
color: var(--text); padding: 4px 8px; font-size: 13px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
width: 240px; max-width: 100%;
}
.entryIdStatus { font-size: 12px; }
.entryDuration {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px; text-align: right;
}
.entryRemove {
background: transparent; border: 1px solid var(--border);
color: var(--text-muted); border-radius: 6px; cursor: pointer;
width: 28px; height: 28px; padding: 0; font-size: 14px;
}
.entryRemove:hover { color: #e57373; border-color: #e57373; }
.playlistFooter {
display: flex; align-items: center; justify-content: space-between;
gap: 12px; flex-wrap: wrap; margin-top: 8px;
}