1052 lines
40 KiB
HTML
Executable File
1052 lines
40 KiB
HTML
Executable File
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>NETV MAM</title>
|
|
<style>
|
|
body {
|
|
background-color: black;
|
|
color: white;
|
|
height: 100vh;
|
|
width: 100vw;
|
|
}
|
|
|
|
header, footer {
|
|
display: flex;
|
|
width: 100vw;
|
|
}
|
|
|
|
header {
|
|
height: 16vh;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
main {
|
|
width: 100%;
|
|
height: 74vh;
|
|
border-top: 3px solid white;
|
|
border-bottom: 3px solid white;
|
|
}
|
|
|
|
footer {
|
|
height: 2vh;
|
|
}
|
|
|
|
#cue_video, #live_video {
|
|
width: 100% !important;
|
|
height: auto !important;
|
|
}
|
|
|
|
#tabs {
|
|
width: 100%;
|
|
}
|
|
|
|
#tabs ul {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
#tabs li {
|
|
display: inline-block;
|
|
border: solid;
|
|
border-width: 1px 1px 0 1px;
|
|
border-color: white;
|
|
margin: 0 5px 0 0;
|
|
background-color: white;
|
|
color: black;
|
|
}
|
|
|
|
#tabs li a {
|
|
padding: 0 10px;
|
|
color: black;
|
|
text-decoration: none;
|
|
}
|
|
|
|
#tab_content {
|
|
display: flex;
|
|
border-top: 1px solid white;
|
|
width: 100%;
|
|
}
|
|
|
|
.active_tab {
|
|
padding-bottom: 1px;
|
|
background-color: green;
|
|
}
|
|
|
|
#upload_target {
|
|
position: fixed;
|
|
bottom: 3vh;
|
|
left: 0;
|
|
width: 25vw;
|
|
height: 10vh;
|
|
border: 1px solid white;
|
|
}
|
|
|
|
.readyToDrop {
|
|
background-color: green;
|
|
}
|
|
|
|
::backdrop {
|
|
background-image: linear-gradient(
|
|
45deg,
|
|
magenta,
|
|
rebeccapurple,
|
|
dodgerblue,
|
|
green
|
|
);
|
|
opacity: 0.75;
|
|
}
|
|
|
|
#media_library_contents {
|
|
width: 25vw;
|
|
height: 52vh;
|
|
overflow-y: scroll;
|
|
}
|
|
|
|
#media_item_details {
|
|
position: absolute;
|
|
right: 0;
|
|
bottom: 3vh;
|
|
width: 70vw;
|
|
height: 45vh;
|
|
border: 1px solid white;
|
|
display: flex;
|
|
}
|
|
|
|
#media_item_preview {
|
|
width: 720px;
|
|
height: 480px;
|
|
}
|
|
|
|
#media_item_form {
|
|
float: right;
|
|
}
|
|
|
|
.media_item_list_element {
|
|
width: 24vw;
|
|
border: 1px solid white;
|
|
}
|
|
|
|
#media_item_editor_tags {
|
|
width: 30vw;
|
|
}
|
|
|
|
.tag {
|
|
display: inline-block;
|
|
border: 1px solid white;
|
|
padding: 2px;
|
|
margin: 4px;
|
|
}
|
|
|
|
.tag.enabled {
|
|
background-color: green;
|
|
}
|
|
|
|
.tag.disabled {
|
|
background-color: red;
|
|
}
|
|
|
|
#media_item_edl_list {
|
|
margin: 0;
|
|
}
|
|
|
|
#media_item_edl_list label {
|
|
display: inline-block;
|
|
width: 5vw;
|
|
}
|
|
|
|
#media_item_edl_list_container {
|
|
display: inline-flex;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
#media_item_edl_list_add_insert_button {
|
|
width: 3vw;
|
|
}
|
|
|
|
#current_day_indicator {
|
|
display: inline-block;
|
|
width: 300px;
|
|
}
|
|
|
|
#schedule_grid {
|
|
border: 2px solid white;
|
|
width: 90vw;
|
|
max-height: 52vh;
|
|
overflow-y: scroll;
|
|
}
|
|
|
|
#schedule_grid td {
|
|
border: 1px solid lightgray;
|
|
}
|
|
|
|
#schedule_grid .program_block {
|
|
background-color: rgba(0,255,0,0.5);
|
|
border-radius: 10px;
|
|
}
|
|
|
|
#schedule_grid .program_block .unsaved {
|
|
background-color: rgba(255,0,0,0.5);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<section id="infobox">
|
|
<h1>NETV MAM</h1>
|
|
<p id="current_date"></p>
|
|
<p id="current_time"></p>
|
|
</section>
|
|
<!-- we don't need these yet
|
|
<section id="cue_deck">
|
|
<video id="cue_video" controls></video>
|
|
<p>Preview Deck</p>
|
|
</section>
|
|
<section id="live_deck">
|
|
<video id="live_video" controls="off"></video>
|
|
<p>Live on NETV</p>
|
|
</section>
|
|
-->
|
|
</header>
|
|
<main>
|
|
<nav id="tabs">
|
|
<ul>
|
|
<li class="active_tab"><a href="#" id="library_tab">Media Library</a></li>
|
|
<li><a href="#" id="tags_tab">Tag Editor</a></li>
|
|
<li><a href="#" id="schedules_tab">Schedules</a></li>
|
|
<li><a href="#" id="epg_tab">EPG</a></li>
|
|
</ul>
|
|
</nav>
|
|
<section id="tab_content">
|
|
|
|
</section>
|
|
</main>
|
|
<footer>
|
|
<section id="colophon">
|
|
Copyright © 2024 Mountaintown Technology - Version 0.8.0
|
|
</section>
|
|
</footer>
|
|
<template id="media_library_template">
|
|
<section id="media_library_browser">
|
|
<h2>Media Files</h2>
|
|
<section id="upload_target"><p>Drag files here to upload</p></section>
|
|
<p>Filter: <input type="text" id="media_library_filter" /></p>
|
|
<section id="media_library_contents">
|
|
|
|
</section>
|
|
<dialog id="media_details_dialog">
|
|
<form id="media_details_dialog_form" method="dialog" />
|
|
<h2 id="media_details_dialog_header">Metadata</h2>
|
|
<p><label for="media_details_dialog_id">ID: </label><input name="id" id="media_details_dialog_id" type="text" disabled /></p>
|
|
<p><label for="media_details_dialog_title">Title: </label><input name="title" id="media_details_dialog_title" type="text" /></p>
|
|
<p><label for="media_details_dialog_description">Description: </label><input name="description" id="media_details_dialog_description" type="text" /></p>
|
|
<p><label for="media_details_dialog_season">Season number: </label><input name="season" id="media_details_dialog_season" type="number" /></p>
|
|
<p><label for="media_details_dialog_episode_number">Episode number: </label><input name="episode_number" id="media_details_dialog_episode_number" type="number" /></p>
|
|
<p>Duration: <span id="media_details_dialog_duration_secs"></span></p>
|
|
<button id="media_details_dialog_save_button" value="save">Save Metadata</button><button id="media_details_dialog_close_button" value="close">Close Without Saving</button>
|
|
</form>
|
|
</dialog>
|
|
</section>
|
|
<section id="media_library_details">
|
|
|
|
</section>
|
|
</template>
|
|
<template id="media_item_template">
|
|
<div class="media_item_list_element">
|
|
<img class="media_item_thumbnail">
|
|
<strong class="media_item_title"></strong>
|
|
<p class="media_item_info"></p>
|
|
</template>
|
|
<template id="tag_editor_template">
|
|
<section id="tag_editor">
|
|
<h2>Tags</h2>
|
|
<p>Current tags:</p>
|
|
<p class="tags_list"></p>
|
|
<form id="add_tag_form" action="tags.php" method="POST">
|
|
<p><label for="new_tag">New tag: </label><input type="text" name="new_tag" id="new_tag_input" /></p>
|
|
<button id="save_tag_button" value="save">Save Tag</button>
|
|
</form>
|
|
</section>
|
|
</template>
|
|
<template id="media_item_editor">
|
|
<div class="media_item_details">
|
|
<video id="media_item_preview" controls></video>
|
|
<div id="media_item_form">
|
|
<form id="media_item_form_data">
|
|
<p><label for="id">ID: </label><input type="text" name="id" disabled /></p>
|
|
<p><label for="title">Title: </label><input type="text" name="title" /></p>
|
|
<p><label for="description">Description: </label><input type="text" name="description" /></p>
|
|
<p><label for="season">Season number: </label><input type="number" name="season" /></p>
|
|
<p><label for="episode_number">Episode number: </label><input type="number" name="episode_number" /></p>
|
|
<p>Tags:</p>
|
|
<div id="media_item_editor_tags"></div>
|
|
<button id="media_item_editor_save_button" value="save">Save Metadata</button><button id="media_item_editor_cancel_button" value="cancel">Cancel</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div id="media_item_edl">
|
|
<h2>EDL</h2>
|
|
<div id="media_item_edl_list_container">
|
|
<div id="media_item_edl_add_insert_container">
|
|
<input type="button" id="media_item_edl_add_insert_button" name="media_item_edl_add_insert_button" value="Add Insert Point"></button>
|
|
</div>
|
|
<ul id="media_item_edl_list">
|
|
<li id="media_item_edl_inpoint_listitem"><label for="media_item_edl_inpoint">In: </label><input type="text" name="media_item_edl_inpoint" value="" /></li>
|
|
|
|
<li id="media_item_edl_outpoint_listitem"><label for="media_item_edl_outpoint">Out: </label><input type="text" name="media_item_edl_outpoint" value="" /></li>
|
|
</ul>
|
|
</div>
|
|
<button id="media_item_edl_save_button" value="Save EDL">Save EDL</button>
|
|
</div>
|
|
</template>
|
|
<template id="media_item_edl_insert_listitem">
|
|
<li class="media_item_edl_insert">
|
|
<label for="media_item_edl_insert">Insert: </label>
|
|
<input type="text" name="media_item_edl_insert" />
|
|
<button class="media_item_edl_remove_insert">X</button>
|
|
<button class="media_item_edl_jump_insert">-></button>
|
|
</li>
|
|
</template>
|
|
<template id="schedules_template">
|
|
<section id="schedule_pane">
|
|
<h2>Schedules</h2>
|
|
<section id="schedules_time_range_section">
|
|
<button id="previous_day_button"><</button>
|
|
<span id="current_day_indicator"></span>
|
|
<button id="next_day_button">></button>
|
|
</section>
|
|
<table id="schedule_grid">
|
|
|
|
</table>
|
|
</section>
|
|
<dialog id="new_block_dialog">
|
|
<form id="new_block_dialog_form" method="dialog" />
|
|
<h2 id="new_block_dialog_header">Adding a New Schedule Block</h2>
|
|
<p><label for="new_block_dialog_id">ID: </label><input name="id" id="new_block_dialog_id" type="text" disabled /></p>
|
|
<p><label for="new_block_dialog_name">Name: </label><input name="name" id="new_block_dialog_name" type="text" /></p>
|
|
<p>Tag(s) for block:</p>
|
|
<div id="new_block_program_tags"></div>
|
|
<p><label for="new_block_program_selector">Select program by </label><select id="new_block_program_selector">
|
|
<option value="newest" selected>newest in tag</option>
|
|
<option value="random">random (least played in tag)</option>
|
|
</select></p>
|
|
<p><label for="new_block_allow_reruns">Allow reruns: </label><input name="new_block_allow_reruns" id="new_block_allow_reruns" type="checkbox" checked /></p>
|
|
<p><label for="new_block_duration_minutes">Block duration (in minutes): </label><input name="new_block_duration_minutes" id="new_block_duration_minutes" type="number" /></p>
|
|
<p><label for="new_block_enable_commercials">Play commercials: </label><input name="new_block_enable_commercials" id="new_block_enable_commercials" type="checkbox" checked /></p>
|
|
<p>Tag(s) for commercials:</p>
|
|
<div id="new_block_commercials_tags"></div>
|
|
<p><label for="new_block_enable_promos">Play promos: </label><input name="new_block_enable_promos" id="new_block_enable_promos" type="checkbox" checked /></p>
|
|
<p>Tag(s) for promos:</p>
|
|
<div id="new_block_promos_tags"></div>
|
|
<p><label for="new_block_enable_bumpers">Play bumpers: </label><input name="new_block_enable_bumpers" id="new_block_enable_bumpers" type="checkbox" checked /></p>
|
|
<p>Tag(s) for bumpers:</p>
|
|
<div id="new_block_bumpers_tags"></div>
|
|
<button id="new_block_dialog_save_button" value="save">Save Block</button><button id="new_block_dialog_close_button" value="close">Close Without Saving</button>
|
|
</form>
|
|
</dialog>
|
|
</template>
|
|
<template id="schedule_header_row_template">
|
|
<tr>
|
|
<td class="schedule_row_time_header">Time</td>
|
|
<td class="schedule_row_program_header">Program</td>
|
|
</tr>
|
|
</template>
|
|
<template id="schedule_row_template">
|
|
<tr class="schedule_row">
|
|
<td class="schedule_row_time"></td>
|
|
<td class="schedule_row_program"></td>
|
|
</tr>
|
|
</template>
|
|
<script>
|
|
const init_library = () => {
|
|
// set up handler for library filtering
|
|
const filter_box = document.querySelector("#media_library_filter");
|
|
filter_box.addEventListener("keypress", (e) => {
|
|
if (e.key === 'Enter') {
|
|
fetch_filtered_media_items();
|
|
}
|
|
});
|
|
// set up drag'n'drop file upload handling
|
|
const upload_target = document.getElementById("upload_target");
|
|
upload_target.addEventListener("dragenter", (event) => {
|
|
event.target.classList.add("readyToDrop");
|
|
});
|
|
upload_target.addEventListener("dragleave", (event) => {
|
|
event.target.classList.remove("readyToDrop");
|
|
});
|
|
upload_target.addEventListener("dragover", (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
});
|
|
upload_target.addEventListener("drop", (event) => {
|
|
console.log("Objects dropped on upload target.");
|
|
event.preventDefault();
|
|
event.target.classList.remove("readyToDrop");
|
|
|
|
if (event.dataTransfer.items) {
|
|
console.log(event.dataTransfer.items.length + " items dropped.");
|
|
[...event.dataTransfer.items].forEach((item, i) => {
|
|
// If dropped items aren't files, reject them
|
|
if (item.kind === "file") {
|
|
const file = item.getAsFile();
|
|
console.log(`… file[${i}].name = ${file.name}`);
|
|
const uploadURL = "upload.php";
|
|
let data = new FormData();
|
|
data.append('file', file);
|
|
fetch(uploadURL, {
|
|
method: "POST",
|
|
body: data
|
|
})
|
|
.then((response) => {
|
|
if (response.ok) {
|
|
response.json().then((json) => {
|
|
// show dialog to get media metadata for new upload
|
|
console.log("Upload response: ");
|
|
console.dir(json);
|
|
const metadata_dialog = document.getElementById("media_details_dialog");
|
|
const metadata_header = document.getElementById("media_details_dialog_header");
|
|
metadata_header.innerHTML = "Metadata for " + json.source_path;
|
|
const metadata_title = document.getElementById("media_details_dialog_title");
|
|
metadata_title.value = json.title === "unknown" ? json.source_path : json.title;
|
|
const dur_span = document.getElementById("media_details_dialog_duration_secs");
|
|
dur_span.innerHTML = new Date(json.duration_secs * 1000).toISOString().substring(11, 19);
|
|
const id_input = document.getElementById("media_details_dialog_id");
|
|
id_input.value = json.id;
|
|
const metadata_close_button = document.getElementById("media_details_dialog_close_button");
|
|
metadata_close_button.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
metadata_dialog.close();
|
|
});
|
|
const metadata_save_button = document.getElementById("media_details_dialog_save_button");
|
|
metadata_save_button.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
const updateURL = "update.php?id=" + json.id;
|
|
let data = new FormData(document.getElementById("media_details_dialog_form"));
|
|
console.dir(data);
|
|
fetch(updateURL, {
|
|
method: "POST",
|
|
body: data
|
|
})
|
|
.then((response) => {
|
|
if (response.ok) {
|
|
response.json().then((json) => {
|
|
metadata_dialog.close();
|
|
fetch_media_items();
|
|
});
|
|
}
|
|
});
|
|
});
|
|
metadata_dialog.showModal();
|
|
});
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
|
|
}
|
|
});
|
|
} else {
|
|
// Use DataTransfer interface to access the file(s)
|
|
[...event.dataTransfer.files].forEach((file, i) => {
|
|
console.log(`… file[${i}].name = ${file.name}`);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
const fetch_media_items = () => {
|
|
const api_url = "media.php";
|
|
fetch(api_url).then((response) => {
|
|
if (response.ok) {
|
|
response.json().then(populate_media_library);
|
|
}
|
|
});
|
|
}
|
|
|
|
const fetch_filtered_media_items = () => {
|
|
const filter_term = document.querySelector("#media_library_filter").value;
|
|
const api_url = "media.php?filter=" + filter_term;
|
|
|
|
fetch(api_url).then((response) => {
|
|
if (response.ok) {
|
|
response.json().then(populate_media_library);
|
|
}
|
|
});
|
|
}
|
|
|
|
const secs_to_human_duration = (secs) => {
|
|
// date and time munging using substring? it's more likely than you think
|
|
return new Date(secs * 1000).toISOString().substring(11, 19);
|
|
}
|
|
|
|
const populate_media_library = (rows) => {
|
|
const media_library_target = document.querySelector("#media_library_contents");
|
|
media_library_target.innerHTML = "";
|
|
|
|
rows.forEach((row) => {
|
|
const duration = secs_to_human_duration(row['duration_secs']);
|
|
const template = document.getElementById("media_item_template");
|
|
const template_clone = template.content.firstElementChild.cloneNode(true);
|
|
template_clone.querySelector(".media_item_title").innerHTML = row['title'];
|
|
template_clone.querySelector(".media_item_info").innerHTML = `<p>${row['description']}</p><p>Season ${row['season']} Episode ${row['episode_number']} - (Duration ${duration})</p>`;
|
|
const thisRow = media_library_target.appendChild(template_clone);
|
|
thisRow.setAttribute("data-id", row['id']);
|
|
thisRow.setAttribute("data-title", row['title']);
|
|
thisRow.setAttribute("data-description", row['description']);
|
|
thisRow.setAttribute("data-season", row['season']);
|
|
thisRow.setAttribute("data-episode-number", row['episode_number']);
|
|
thisRow.setAttribute("data-duration", row['duration_secs']);
|
|
if (row['edl_id']) thisRow.setAttribute("data-edl-id", row['edl_id']);
|
|
if (row['edl_name']) thisRow.setAttribute("data-edl-name", row['edl_name']);
|
|
if (row['edl_definition'] !== null) {
|
|
const edl = JSON.parse(row['edl_definition']);
|
|
thisRow.setAttribute("data-inpoint", edl.inpoint);
|
|
thisRow.setAttribute("data-outpoint", edl.outpoint);
|
|
thisRow.setAttribute("data-edl-definition", row['edl_definition']);
|
|
} else {
|
|
// no EDL yet, use start as inpoint and duration as outpoint
|
|
thisRow.setAttribute("data-inpoint", 0);
|
|
thisRow.setAttribute("data-outpoint", row['duration_secs']);
|
|
}
|
|
});
|
|
const nodes = document.querySelectorAll(".media_item_list_element");
|
|
nodes.forEach((node) => {
|
|
node.addEventListener("click", (event) => {
|
|
// open media item details editing pane
|
|
const media_item = event.target.classList.contains(".media_item_list_element") ? event.target : event.target.closest(".media_item_list_element");
|
|
edit_media_item(media_item);
|
|
});
|
|
});
|
|
}
|
|
|
|
const edit_media_item = (media_item_element) => {
|
|
const template = document.getElementById("media_item_editor");
|
|
const template_clone = template.content.cloneNode(true);
|
|
const target = document.querySelector("#media_library_details");
|
|
const item_id = media_item_element.getAttribute("data-id");
|
|
template_clone.querySelector('#media_item_preview').setAttribute('src', 'play.php?id=' + item_id);
|
|
template_clone.querySelector('input[name="id"]').value = item_id;
|
|
template_clone.querySelector('input[name="title"]').value = media_item_element.getAttribute("data-title");
|
|
template_clone.querySelector('input[name="description"]').value = media_item_element.getAttribute("data-description");
|
|
template_clone.querySelector('input[name="season"]').value = media_item_element.getAttribute("data-season");
|
|
template_clone.querySelector('input[name="episode_number"]').value = media_item_element.getAttribute("data-episode-number");
|
|
|
|
const formatted_inpoint = new Date(media_item_element.getAttribute("data-inpoint") * 1000).toISOString().substring(11, 19);
|
|
const formatted_outpoint = new Date(media_item_element.getAttribute("data-outpoint") * 1000).toISOString().substring(11, 19);
|
|
template_clone.querySelector('input[name="media_item_edl_inpoint"]').value = formatted_inpoint;
|
|
template_clone.querySelector('input[name="media_item_edl_outpoint"]').value = formatted_outpoint;
|
|
template_clone.firstElementChild.setAttribute("data-duration", media_item_element.getAttribute("data-duration"));
|
|
template_clone.firstElementChild.setAttribute("data-media-id", media_item_element.getAttribute("data-id"));
|
|
|
|
target.innerHTML = "";
|
|
target.appendChild(template_clone);
|
|
fetch_item_tags(item_id);
|
|
const save_button = document.getElementById("media_item_editor_save_button");
|
|
|
|
// populate EDL if it exists
|
|
const thisRowEDL = media_item_element.getAttribute("data-edl-definition");
|
|
const edl_id = media_item_element.getAttribute("data-edl-id");
|
|
const edl_name = media_item_element.getAttribute("data-edl-name");
|
|
|
|
if (thisRowEDL && thisRowEDL !== "") {
|
|
edl = JSON.parse(thisRowEDL);
|
|
edl.inserts.forEach((insert) => {
|
|
const current_timecode = insert;
|
|
const template = document.getElementById("media_item_edl_insert_listitem");
|
|
const template_clone = template.content.cloneNode(true);
|
|
const formatted_timestamp = secs_to_human_duration(current_timecode);
|
|
template_clone.querySelector('input[name="media_item_edl_insert"]').value = formatted_timestamp;
|
|
template_clone.firstElementChild.setAttribute('data-timestamp', current_timecode);
|
|
const target = document.querySelector("#media_item_edl_outpoint_listitem");
|
|
const parent = document.querySelector("#media_item_edl_list");
|
|
parent.insertBefore(template_clone, target);
|
|
parent.setAttribute("data-edl-id", edl_id);
|
|
parent.setAttribute("data-edl-name", edl_name);
|
|
});
|
|
sort_inserts();
|
|
// set up jump/remove handlers
|
|
const remove_buttons = document.querySelectorAll("button.media_item_edl_remove_insert");
|
|
remove_buttons.forEach((btn) => {
|
|
btn.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
e.target.parentNode.remove();
|
|
});
|
|
});
|
|
const jump_buttons = document.querySelectorAll("button.media_item_edl_jump_insert");
|
|
jump_buttons.forEach((btn) => {
|
|
btn.removeEventListener("click", jumpHandler);
|
|
btn.addEventListener("click", jumpHandler);
|
|
});
|
|
}
|
|
|
|
save_button.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
const api_url = "update.php?id=" + item_id;
|
|
const item_data = new FormData(document.getElementById("media_item_form_data"));
|
|
fetch(api_url, {
|
|
method: "POST",
|
|
body: item_data
|
|
})
|
|
.then((response) => {
|
|
if (response.ok) {
|
|
response.json().then((json) => {
|
|
fetch_media_items();
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
const edl_insert_button = document.getElementById("media_item_edl_add_insert_button");
|
|
edl_insert_button.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
const current_timecode = document.getElementById("media_item_preview").currentTime;
|
|
const template = document.getElementById("media_item_edl_insert_listitem");
|
|
const template_clone = template.content.cloneNode(true);
|
|
const formatted_timestamp = secs_to_human_duration(current_timecode);
|
|
template_clone.querySelector('input[name="media_item_edl_insert"]').value = formatted_timestamp;
|
|
template_clone.firstElementChild.setAttribute('data-timestamp', current_timecode);
|
|
const target = document.querySelector("#media_item_edl_outpoint_listitem");
|
|
const parent = document.querySelector("#media_item_edl_list");
|
|
parent.insertBefore(template_clone, target);
|
|
sort_inserts();
|
|
const remove_buttons = document.querySelectorAll("button.media_item_edl_remove_insert");
|
|
remove_buttons.forEach((btn) => {
|
|
btn.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
e.target.parentNode.remove();
|
|
});
|
|
});
|
|
const jump_buttons = document.querySelectorAll("button.media_item_edl_jump_insert");
|
|
jump_buttons.forEach((btn) => {
|
|
btn.removeEventListener("click", jumpHandler);
|
|
btn.addEventListener("click", jumpHandler);
|
|
});
|
|
});
|
|
|
|
const edl_save_button = document.getElementById("media_item_edl_save_button");
|
|
edl_save_button.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
const api_url = "edl.php";
|
|
const edl_definition = {};
|
|
const payload = {};
|
|
const inpoint = document.querySelector("#media_item_edl_inpoint_listitem").getAttribute("data-start");
|
|
const outpoint = document.querySelector("#media_item_edl_outpoint_listitem").getAttribute("data-stop");
|
|
payload.media_id = document.querySelector("#media_library_details > .media_item_details").getAttribute("data-media-id");
|
|
edl_definition.inpoint = inpoint || "0";
|
|
edl_definition.outpoint = outpoint || document.querySelector("#media_library_details > .media_item_details").getAttribute("data-duration");
|
|
edl_definition.inserts = [];
|
|
const list_parent = document.querySelector("#media_item_edl_list");
|
|
const edl_id = list_parent.getAttribute("data-edl-id") || null;
|
|
const edl_name = list_parent.getAttribute("data-edl-name") || null;
|
|
const current_items = list_parent.querySelectorAll("li.media_item_edl_insert");
|
|
current_items.forEach((insertPoint) => {
|
|
edl_definition.inserts.push(insertPoint.getAttribute("data-timestamp"));
|
|
});
|
|
payload.edl_definition = JSON.stringify(edl_definition);
|
|
payload.edl_name = edl_name || "default"; // TODO: handle this better once we have multiple EDL support
|
|
if (edl_id) payload.edl_id = edl_id;
|
|
|
|
fetch(api_url, {
|
|
method: "POST",
|
|
body: JSON.stringify(payload)
|
|
})
|
|
.then((response) => {
|
|
if (response.ok) {
|
|
response.json().then((payload) => {
|
|
// TODO: repopulate based on returned data
|
|
fetch_media_items();
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
const jumpHandler = (e) => {
|
|
e.preventDefault();
|
|
const player = document.getElementById("media_item_preview");
|
|
if (!player.paused) player.pause();
|
|
player.currentTime = e.target.parentNode.getAttribute("data-timestamp");
|
|
}
|
|
|
|
const sort_inserts = () => {
|
|
const list_parent = document.querySelector("#media_item_edl_list");
|
|
let new_parent = list_parent.cloneNode(false);
|
|
const current_items = list_parent.querySelectorAll("li");
|
|
let items = [];
|
|
current_items.forEach((item) => {
|
|
items.push(item);
|
|
});
|
|
items.sort((a, b) => {
|
|
return a.querySelector("input[type='text']").value > b.querySelector("input[type='text']").value
|
|
});
|
|
items.forEach((item) => {
|
|
new_parent.appendChild(item);
|
|
});
|
|
list_parent.parentNode.replaceChild(new_parent, list_parent);
|
|
}
|
|
|
|
const fetch_tags = async () => {
|
|
const api_url = "tags.php";
|
|
fetch(api_url).then((response) => {
|
|
if (response.ok) {
|
|
response.json().then((tags) => {
|
|
const container = document.querySelector(".tags_list");
|
|
container.innerHTML = "";
|
|
for (var i = 0; i < tags.length; i++) {
|
|
container.innerHTML += "<span class='tag' data-id=" + tags[i]['id'] + ">" + tags[i]['tag'] + "</span>";
|
|
}
|
|
tag_editor_handlers();
|
|
});
|
|
} else {
|
|
console.log('err fetching tags:');
|
|
console.dir(response);
|
|
}
|
|
});
|
|
}
|
|
|
|
const fetch_item_tags = async (media_id) => {
|
|
const api_url = "tags.php?media_id=" + media_id;
|
|
fetch(api_url).then((response) => {
|
|
if (response.ok) {
|
|
response.json().then((tags) => {
|
|
const container = document.querySelector("#media_item_editor_tags");
|
|
container.innerHTML = "";
|
|
for (var i = 0; i < tags.length; i++) {
|
|
const enabled_class = tags[i]['enabled'] === 'true' ? 'enabled' : 'disabled';
|
|
const new_span = `<span class='tag ${enabled_class}' data-id='${tags[i]['id']}' data-media-id=${media_id}>${tags[i]['tag']}</span>`;
|
|
container.innerHTML += new_span;
|
|
}
|
|
item_tag_editor_handlers();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
const fetch_block_tags = async (block_id) => {
|
|
const id = block_id || crypto.randomUUID();
|
|
const api_url = "tags.php?block_id=" + id;
|
|
fetch(api_url).then((response) => {
|
|
if (response.ok) {
|
|
response.json().then((tags) => {
|
|
const container = document.querySelector("#new_block_program_tags");
|
|
container.innerHTML = "";
|
|
for (var i = 0; i < tags.program.length; i++) {
|
|
// tags for program itself
|
|
const enabled_class = tags.program[i]['enabled'] === 'true' ? 'enabled' : 'disabled';
|
|
const new_span = `<span class='tag ${enabled_class}' data-id='${tags.program[i]['id']}'>${tags.program[i]['tag']}</span>`;
|
|
container.innerHTML += new_span;
|
|
}
|
|
const promo_container = document.querySelector("#new_block_promos_tags");
|
|
promo_container.innerHTML = "";
|
|
for (var i = 0; i < tags.promos.length; i++) {
|
|
// tags for promos run during program
|
|
const enabled_class = tags.promos[i]['enabled'] === 'true' ? 'enabled' : 'disabled';
|
|
const new_span = `<span class='tag ${enabled_class}' data-id='${tags.promos[i]['id']}'>${tags.promos[i]['tag']}</span>`;
|
|
promo_container.innerHTML += new_span;
|
|
}
|
|
const commercial_container = document.querySelector("#new_block_commercials_tags");
|
|
commercial_container.innerHTML = "";
|
|
for (var i = 0; i < tags.commercials.length; i++) {
|
|
// tags for commercials run during program
|
|
const enabled_class = tags.commercials[i]['enabled'] === 'true' ? 'enabled' : 'disabled';
|
|
const new_span = `<span class='tag ${enabled_class}' data-id='${tags.commercials[i]['id']}'>${tags.commercials[i]['tag']}</span>`;
|
|
commercial_container.innerHTML += new_span;
|
|
}
|
|
const bumper_container = document.querySelector("#new_block_bumpers_tags");
|
|
bumper_container.innerHTML = "";
|
|
for (var i = 0; i < tags.bumpers.length; i++) {
|
|
// tags for bumpers run during program
|
|
const enabled_class = tags.bumpers[i]['enabled'] === 'true' ? 'enabled' : 'disabled';
|
|
const new_span = `<span class='tag ${enabled_class}' data-id='${tags.bumpers[i]['id']}'>${tags.bumpers[i]['tag']}</span>`;
|
|
bumper_container.innerHTML += new_span;
|
|
}
|
|
block_tag_editor_handlers();
|
|
});
|
|
} else {
|
|
console.log("fetch_block_tags failed!");
|
|
console.dir(response);
|
|
}
|
|
});
|
|
}
|
|
|
|
const fetch_slot_tags = async (slot_id) => {
|
|
|
|
}
|
|
|
|
// tab handlers
|
|
document.getElementById("library_tab").addEventListener("click", (e) => {
|
|
setActiveTab(e);
|
|
const tab_content = document.getElementById("tab_content");
|
|
tab_content.innerHTML = "";
|
|
const template = document.getElementById("media_library_template");
|
|
const template_clone = template.content.cloneNode(true);
|
|
tab_content.appendChild(template_clone);
|
|
// fetch media and populate
|
|
fetch_media_items();
|
|
// attach event handlers
|
|
init_library();
|
|
});
|
|
|
|
document.getElementById("tags_tab").addEventListener("click", (e) => {
|
|
setActiveTab(e);
|
|
const tab_content = document.getElementById("tab_content");
|
|
tab_content.innerHTML = "";
|
|
const template = document.getElementById("tag_editor_template");
|
|
const template_clone = template.content.cloneNode(true);
|
|
tab_content.appendChild(template_clone);
|
|
fetch_tags();
|
|
});
|
|
|
|
document.getElementById("schedules_tab").addEventListener("click", (e) => {
|
|
setActiveTab(e);
|
|
const tab_content = document.querySelector("#tab_content");
|
|
tab_content.innerHTML = "";
|
|
const template = document.querySelector("#schedules_template");
|
|
const template_clone = template.content.cloneNode(true);
|
|
tab_content.appendChild(template_clone);
|
|
init_schedules();
|
|
});
|
|
|
|
const init_schedules = (day) => {
|
|
const day_of_schedule = day ? new Date(Date.parse(day)) : new Date();
|
|
day_of_schedule.setHours(0);
|
|
day_of_schedule.setMinutes(0);
|
|
day_of_schedule.setSeconds(0);
|
|
|
|
// set up header
|
|
const current_day_indicator = document.querySelector("#current_day_indicator");
|
|
current_day_indicator.innerHTML = `<center>Schedule for <strong>${day_of_schedule.toDateString()}</strong></center>`;
|
|
current_day_indicator.setAttribute("data-day", day_of_schedule.toISOString());
|
|
// set up previous/next buttons
|
|
document.querySelector('#previous_day_button').addEventListener("click", previous_day_schedule);
|
|
document.querySelector('#next_day_button').addEventListener("click", next_day_schedule);
|
|
// init grid structure itself
|
|
const template = document.querySelector("#schedule_header_row_template");
|
|
const template_clone = template.content.cloneNode(true);
|
|
const grid = document.querySelector("#schedule_grid");
|
|
grid.innerHTML = ``;
|
|
grid.appendChild(template_clone);
|
|
|
|
for (var i = 0; i < 24 * 2; i++) {
|
|
const hour = Math.floor(i / 2);
|
|
const minutes = i % 2 === 0 ? 0 : 30;
|
|
const template = document.querySelector("#schedule_row_template");
|
|
const template_clone = template.content.cloneNode(true);
|
|
const grid = document.querySelector("#schedule_grid");
|
|
template_clone.firstElementChild.innerHTML = `<td class="schedule_grid_time_cell"><strong>${hour.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}</strong></td><td class="schedule_grid_program_cell"></td>`;
|
|
template_clone.firstElementChild.setAttribute('data-time', hour + ":" + minutes);
|
|
grid.appendChild(template_clone);
|
|
}
|
|
|
|
load_schedule(day_of_schedule);
|
|
}
|
|
|
|
const previous_day_schedule = (e) => {
|
|
e.preventDefault();
|
|
const day = new Date(Date.parse(document.querySelector("#current_day_indicator").getAttribute("data-day")));
|
|
day.setDate(day.getDate() - 1);
|
|
init_schedules(day);
|
|
}
|
|
const next_day_schedule = (e) => {
|
|
e.preventDefault();
|
|
const day = new Date(Date.parse(document.querySelector("#current_day_indicator").getAttribute("data-day")));
|
|
day.setDate(day.getDate() + 1);
|
|
init_schedules(day);
|
|
}
|
|
|
|
const load_schedule = (day) => {
|
|
// TODO: fetch schedule data from api
|
|
if (!day) {
|
|
day = new Date();
|
|
}
|
|
const api_url = "schedule.php?date=" + day.toISOString();
|
|
fetch(api_url)
|
|
.then((response) => {
|
|
if (response.ok) {
|
|
response.json().then((json) => {
|
|
// TODO: populate schedule grid
|
|
|
|
// add event handlers
|
|
const block_cells = document.querySelectorAll(".schedule_grid_program_cell");
|
|
block_cells.forEach((cell) => {
|
|
cell.addEventListener("click", schedule_grid_click_handler);
|
|
});
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
const schedule_grid_click_handler = (e) => {
|
|
e.preventDefault();
|
|
const new_block_dialog = document.getElementById("new_block_dialog");
|
|
const new_block_header = document.getElementById("new_block_dialog_header");
|
|
const block_start_time = e.target.parentNode.firstElementChild.innerHTML;
|
|
new_block_header.innerHTML = "New program block for " + block_start_time;
|
|
|
|
const new_block_dialog_id = document.getElementById("new_block_dialog_id");
|
|
new_block_dialog_id.value = self.crypto.randomUUID();
|
|
|
|
const new_block_close_button = document.getElementById("new_block_dialog_close_button");
|
|
new_block_close_button.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
new_block_dialog.close();
|
|
});
|
|
const new_block_save_button = document.getElementById("new_block_dialog_save_button");
|
|
new_block_save_button.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
const updateURL = "block.php";
|
|
let data = new FormData(document.getElementById("new_block_dialog_form"));
|
|
data.set("id", new_block_dialog_id.value);
|
|
console.log("new block:");
|
|
console.dir(data);
|
|
fetch(updateURL, {
|
|
method: "POST",
|
|
body: data
|
|
})
|
|
.then((response) => {
|
|
if (response.ok) {
|
|
response.json().then((json) => {
|
|
const day = new Date(Date.parse(document.querySelector("#current_day_indicator").getAttribute("data-day")));
|
|
new_block_dialog.close();
|
|
load_schedule(day);
|
|
});
|
|
}
|
|
});
|
|
});
|
|
new_block_dialog.showModal();
|
|
fetch_block_tags();
|
|
}
|
|
|
|
const schedule_block_click_handler = (e) => {
|
|
e.preventDefault();
|
|
// TODO: open dialog for edit block
|
|
|
|
}
|
|
|
|
const remove_schedule_block = (e) => {
|
|
e.preventDefault();
|
|
// TODO: delete block from schedule grid
|
|
|
|
}
|
|
|
|
const save_schedule = () => {
|
|
// TODO: post schedule data to api endpoint
|
|
|
|
}
|
|
|
|
const tag_editor_handlers = () => {
|
|
document.querySelector("#save_tag_button").addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
const api_url = "tags.php";
|
|
fetch(api_url, {
|
|
method: "POST",
|
|
body: new FormData(document.getElementById("add_tag_form"))
|
|
})
|
|
.then((response) => {
|
|
if (response.ok) {
|
|
response.json().then((row) => {
|
|
document.querySelector("#new_tag_input").value = "";
|
|
fetch_tags();
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
const item_tag_editor_handlers = () => {
|
|
const enabled_tags = document.querySelectorAll(".tag.enabled");
|
|
const disabled_tags = document.querySelectorAll(".tag.disabled");
|
|
|
|
enabled_tags.forEach((tag) => {
|
|
tag.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
const tag_id = e.target.getAttribute("data-id");
|
|
const media_id = e.target.getAttribute("data-media-id");
|
|
const api_url = "tags.php?enable=false&media_id=" + media_id + "&tag_id=" + tag_id;
|
|
fetch(api_url).then((response) => {
|
|
if (response.ok) {
|
|
response.json().then((json) => {
|
|
if (json.ok) {
|
|
fetch_item_tags(media_id);
|
|
} else {
|
|
console.dir(json);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|
|
disabled_tags.forEach((tag) => {
|
|
tag.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
const tag_id = e.target.getAttribute("data-id");
|
|
const media_id = e.target.getAttribute("data-media-id");
|
|
const api_url = "tags.php?enable=true&media_id=" + media_id + "&tag_id=" + tag_id;
|
|
fetch(api_url).then((response) => {
|
|
if (response.ok) {
|
|
response.json().then((json) => {
|
|
if (json.ok) {
|
|
fetch_item_tags(media_id);
|
|
} else {
|
|
console.dir(json);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
const block_tag_editor_handlers = () => {
|
|
const tags = document.querySelectorAll("#new_block_program_tags > .tag, #new_block_commercials_tags > .tag, #new_block_promos_tags > .tag, #new_block_bumpers_tags > .tag");
|
|
tags.forEach((tag) => {
|
|
tag.addEventListener("click", block_tag_editor_click_handler);
|
|
});
|
|
}
|
|
|
|
const block_tag_editor_click_handler = (e) => {
|
|
e.preventDefault();
|
|
if (e.target.classList.contains('disabled')) {
|
|
e.target.classList.remove('disabled');
|
|
e.target.classList.add('enabled');
|
|
} else {
|
|
e.target.classList.remove('enabled');
|
|
e.target.classList.add('disabled');
|
|
}
|
|
}
|
|
|
|
document.getElementById("epg_tab").addEventListener("click", (e) => {
|
|
setActiveTab(e);
|
|
document.querySelector("#tab_content").innerHTML = "<h2>EPG</h2>"
|
|
+ "<p>EPG goes here.</p>";
|
|
});
|
|
|
|
const setActiveTab = (e) => {
|
|
e.preventDefault();
|
|
document.querySelector(".active_tab").classList.remove("active_tab");
|
|
e.target.classList.add("active_tab");
|
|
}
|
|
|
|
const updateDate = () => {
|
|
document.getElementById("current_date").innerHTML = new Date().toDateString();
|
|
}
|
|
|
|
const updateTime = () => {
|
|
document.getElementById("current_time").innerHTML = new Date().toTimeString();
|
|
}
|
|
|
|
window.addEventListener("load", () => {
|
|
document.getElementById("library_tab").click();
|
|
updateDate();
|
|
updateTime();
|
|
const dateTimer = setInterval(updateDate, 1000 * 60);
|
|
const timeTimer = setInterval(updateTime, 1000);
|
|
|
|
window.addEventListener("dragenter", () => {
|
|
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|