commit 18078d1d3b1565006b6a169e4d7ae4c7e7379ff5 Author: Milarin Date: Sun Sep 15 14:16:45 2024 +0200 initial commit diff --git a/api.go b/api.go new file mode 100644 index 0000000..da0ee4a --- /dev/null +++ b/api.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + "io" + "net/http" + "net/url" + + "git.milar.in/milarin/adverr" + "git.milar.in/milarin/ezhttp" +) + +var BaseURL = adverr.Must(url.Parse("http://desktop:30124")) + +func ProxyRequest(w http.ResponseWriter, r *http.Request) { + req := ezhttp.Request( + ezhttp.Method(r.Method), + ezhttp.URL(BaseURL.JoinPath(r.URL.Path).String()), + ezhttp.Body(ezhttp.Reader(r.Body)), + ezhttp.ContentType("application/json"), + ) + + resp, err := ezhttp.Do(req) + if err != nil { + fmt.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + defer resp.Body.Close() + + w.WriteHeader(resp.StatusCode) + w.Header().Add("Content-Type", "application/json") + io.Copy(w, resp.Body) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..956ca8c --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module git.milar.in/milarin/yt-remote-control + +go 1.23.1 + +require ( + git.milar.in/milarin/adverr v1.1.1 + git.milar.in/milarin/ezhttp v0.0.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..78906d4 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +git.milar.in/milarin/adverr v1.1.1 h1:ENtBcqT7CncLsVfaLC3KzX8QSSGiSpsC7I7wDqladu8= +git.milar.in/milarin/adverr v1.1.1/go.mod h1:joU9sBb7ySyNv4SpTXB0Z4o1mjXsArBw4N27wjgzj9E= +git.milar.in/milarin/ezhttp v0.0.2 h1:9aVBd3AW6XAqWLPDrWrJr2r4JFa21n/B1MpAVN80opU= +git.milar.in/milarin/ezhttp v0.0.2/go.mod h1:7v6RMtAvImUllDDsYGVP4C0BhO1yxLarVorttRGGyJ0= diff --git a/main.go b/main.go new file mode 100644 index 0000000..992d732 --- /dev/null +++ b/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "embed" + _ "embed" + "flag" + "fmt" + "io/fs" + "net/http" +) + +//go:embed static +var StaticFilesDir embed.FS + +var ( // flags + FlagInterface = flag.String("i", "", "network interface") + FlagPort = flag.Uint("p", 8080, "network port") +) + +func main() { + flag.Parse() + + staticFiles, err := fs.Sub(StaticFilesDir, "static") + if err != nil { + panic(err) + } + + http.Handle("/api/", http.StripPrefix("/api", http.HandlerFunc(ProxyRequest))) + http.Handle("/", http.FileServer(http.FS(staticFiles))) + + http.ListenAndServe(fmt.Sprintf("%s:%d", *FlagInterface, *FlagPort), nil) +} diff --git a/static/css/dialog.css b/static/css/dialog.css new file mode 100644 index 0000000..ced4fef --- /dev/null +++ b/static/css/dialog.css @@ -0,0 +1,117 @@ +dialog { + margin: auto; + background-color: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(10px); + color: white; + box-shadow: 1px 1px 10px 10px rgba(0, 0, 0, 0.25); + border-radius: 0.25em; +} + +dialog .header { + display: grid; + grid-template-columns: 1fr auto; + padding: 0.5em; +} + +dialog .header h1 { + font-size: 1.5rem; + text-align: center; +} + +dialog .header span.close { + font-size: 2rem; + margin-left: 0.5em; + transition: all 0.1s ease-in-out; +} + +dialog .header span.close:hover { + transform: scale(1.2); + transition: all 0.1s ease-in-out; +} + +dialog .body form { + padding: 0.5em; + display: flex; + flex-direction: column; +} + +dialog .body form input[type="radio"], +dialog .body form input[type="checkbox"] { + display: none; +} + +dialog .body form label { + padding: 0.75em; + text-align: center; +} + +dialog .body form input[type="radio"]:checked + label { + background-color: rgba(255, 255, 255, 0.1); +} + +dialog .body form input[type="url"] { + background-color: #111; + color: white; + padding: 0.75em; + border: 1px solid #333; + margin-bottom: 1em; + transition: all 0.25s ease-in-out; +} + +dialog .body form input[type="url"]:focus { + border: 1px solid #555; + transition: all 0.25s ease-in-out; +} + +dialog .body form button { + background-color: transparent; + color: white; + padding: 0.75em; + font-size: 1rem; + transition: all 0.25s ease-in-out; +} + +dialog .body form button:hover { + background-color: rgba(255, 255, 255, 0.1); + transition: all 0.25s ease-in-out; +} + +dialog:not([open]) { + pointer-events: none; + opacity: 0; +} + +dialog::backdrop { + background-color: rgba(0, 0, 0, 0); + backdrop-filter: blur(3px); +} + +/* var(--video-card-width) * 2 + var(--video-card-gap) * 3 */ +@media (max-width: 51.5em) { + dialog { + margin: 0; + width: 100%; + height: 100%; + min-width: 100%; + min-height: 100%; + border: none; + background-color: rgba(0, 0, 0, 0.85); + } +} + +/* dialog-specific rules */ + +#close-video-dialog { + max-width: 30em; +} + +#close-video-dialog .body p { + text-align: center; + padding: 0.25em; +} + +#close-video-dialog-title { + background-color: #111; + border: 1px solid #333; + margin-bottom: 0.5em; +} \ No newline at end of file diff --git a/static/css/main.css b/static/css/main.css new file mode 100644 index 0000000..ffc81b1 --- /dev/null +++ b/static/css/main.css @@ -0,0 +1,173 @@ +* { + margin: 0; + padding: 0; + border: 0; + outline: none; +} + +:root { + /* + when changing these values, + both media queries in dialog.css and video.css + should be changed accordingly as well. + */ + --video-card-width: 25em; + --video-card-gap: 0.5em; + + --header-height: 3em; + --footer-height: 6em; + --footer-padding: 0.5em; +} + +.material-symbols-outlined { + font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; + user-select: none; +} + +html { + min-height: 100%; +} + +body { + background-color: #111; + font-size: 100%; + display: block; + min-height: calc(100vh - var(--header-height)); +} + +header { + display: absolute; + position: fixed; + top: 0; + left: 0; + right: 0; + background-color: rgba(200, 55, 55, 0.9); + box-shadow: 0 0 0.5em 0.5em rgba(0, 0, 0, 0.3); + backdrop-filter: blur(5px); + color: white; + height: var(--header-height); + padding: 0 0.25em; + z-index: 11; +} + +header span.material-symbols-outlined { + font-size: 2em; + line-height: 3rem; + float: right; + padding: 0 0.25em; + transition: all 0.1s ease-in-out; +} + +header span.material-symbols-outlined:hover { + transform: scale(1.2); + transition: all 0.1s ease-in-out; +} + +#video-container { + margin-top: var(--header-height); + margin-bottom: calc(var(--footer-height) + var(--footer-padding) * 2); +} + +footer { + display: grid; + grid-template-columns: auto 1fr auto auto auto 1fr auto; + grid-template-rows: auto 1fr; + grid-template-areas: + "current-timestamp seekbar seekbar seekbar seekbar seekbar video-length" + ". . replay pause forward volume-bar volume-bar"; + position: fixed; + bottom: 0; + left: 0; + right: 0; + background-color: rgba(17, 17, 17, 0.95); + box-shadow: 0 0 0.5em 0.5em rgba(0, 0, 0, 0.3); + backdrop-filter: blur(3px); + padding: var(--footer-padding); + z-index: 11; + height: var(--footer-height); +} + +#seekbar { + --margin: 0.75em; + grid-area: seekbar; + align-self: center; + width: calc(100% - var(--margin) * 2); + margin: 0.5em var(--margin); +} + +#replay { + grid-area: replay; + background-color: transparent; + color: white; + text-align: right; + cursor: pointer; +} + +#play-pause { + grid-area: pause; + background-color: transparent; + color: white; + text-align: center; + margin: 0 0.5em; + cursor: pointer; +} + +#forward { + grid-area: forward; + background-color: transparent; + color: white; + text-align: left; + cursor: pointer; +} + +#volume-bar { + --margin: 0.75em; + grid-area: volume-bar; + max-width: 15em; + width: calc(100% - var(--margin) * 2); + margin: 0 var(--margin); + justify-self: right; + align-self: center; +} + +#play-pause .material-symbols-outlined { + font-size: 4rem; +} + +#play-pause:disabled, +#replay:disabled, +#forward:disabled { + color: gray; +} + +#current-timestamp { + grid-area: current-timestamp; + color: white; +} + +#video-length { + grid-area: video-length; + color: white; +} + +footer .material-symbols-outlined { + font-size: 2.5rem; +} + +input[type="range"] { + appearance: none; + height: 0.5em; + background-color: #333; + border-radius: 0.25em; +} + +input[type="range"]::-webkit-slider-thumb, +input[type="range"]::-moz-range-thumb { + cursor: pointer; + appearance: none; + width: 1.5em; + height: 1.5em; + border-radius: 50%; + background-color: rgb(200, 55, 55); + border: none; +} \ No newline at end of file diff --git a/static/css/video.css b/static/css/video.css new file mode 100644 index 0000000..ea2b1bc --- /dev/null +++ b/static/css/video.css @@ -0,0 +1,87 @@ +#video-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + gap: var(--video-card-gap); + padding: var(--video-card-gap); +} + +.video-item { + width: var(--video-card-width); + max-width: 100%; + position: relative; + cursor: pointer; +} + +.video-image { + display: block; + width: 100%; +} + +.video-image:not(.shown-on-display) { + filter: brightness(50%); +} + +.video-bottom { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background-color: rgba(0, 0, 0, 0.5); + color: white; +} + +.video-title { + user-select: none; + padding: 0.5em; +} + +.video-progress { + height: 3px; + background-color: rgba(0, 0, 0, 0.5); + position: relative; +} + +.video-progress .primary { + position: absolute; + top: 0; + bottom: 0; + left: 0; + z-index: 10; + height: 100%; + background-color: #ee1111; +} + +.video-progress .secondary { + position: absolute; + top: 0; + bottom: 0; + left: 0; + height: 100%; + background-color: #994444; +} + +.video-close { + color: white; + position: absolute; + top: 0; + right: 0; + font-size: 1.75rem; + padding: 0.1em; + text-shadow: 0px 0px 0.5em black; + transition: all 0.1s ease-in-out; +} + +.video-close:hover { + transform: scale(1.2); + text-shadow: 0px 0px 0.25em black; + transition: all 0.1s ease-in-out; +} + +/* var(--video-card-width) * 2 + var(--video-card-gap) * 3 */ +@media (max-width: 51.5em) { + .video-item { + width: 100%; + } +} \ No newline at end of file diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..c992d43 --- /dev/null +++ b/static/index.html @@ -0,0 +1,102 @@ + + + + + + + Youtube RemoteControl + + + + + + + + + + + + + + +
+

Select Active Display

+ close +
+
+
+
+
+ + +
+

Select Active Audio Sink

+ close +
+
+
+
+
+ + +
+

Open New Video

+ close +
+
+
+ + + +
+
+
+ + +
+

Close Video

+ close +
+
+
+

Do you want to close the following video?

+

+ + +
+
+
+ +
+ tv_displays + speaker_group + open_in_new +
+ +
+ + + + + + diff --git a/static/js/element.js b/static/js/element.js new file mode 100644 index 0000000..9d55a68 --- /dev/null +++ b/static/js/element.js @@ -0,0 +1,48 @@ +Element.prototype.insertChildAtIndex = function(child, index) { + if (!index) index = 0; + + if (index >= this.children.length) { + this.appendChild(child); + } else { + this.insertBefore(child, this.children[index]); + } +} + +Element.prototype.clearChildren = function() { + while (this.firstChild) { + this.removeChild(this.lastChild); + } +} + +Number.prototype.pad = function(size) { + var s = String(this); + while (s.length < (size || 2)) {s = "0" + s;} + return s; +} + +window.addEventListener("load", init); + +function init() { + const sliders = document.querySelectorAll('input[type="range"]'); + for (let slider of sliders) { + let changeFunction = event => { + const progress = event.target.value; + const max = event.target.max; + const value = Math.round(progress / max * 100); + slider.style.background = `linear-gradient(90deg, rgba(200,55,55,1) ${value}%, #333 ${value}%)`; + }; + + slider.setValue = value => { + if (slider.mousedown) return; + slider.value = value; + changeFunction({target: slider}); + } + + slider.addEventListener("mousedown", event => slider.mousedown = true); + slider.addEventListener("mouseup", event => slider.mousedown = false); + + slider.addEventListener("input", changeFunction); + slider.addEventListener("change", changeFunction); + changeFunction({target: slider}); + } +} \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..5625092 --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,270 @@ +window.addEventListener("load", main); + +let displaySelectDialog; +let audioSinkSelectDialog; +let openNewVideoDialog; +let closeVideoDialog; + +let closeVideoDialogID; + +let videoTemplate; +let videoContainer; +let currentVideos = []; +let currentVideoData; + +let seekbar; +let volumeBar; +let playPauseButton; +let playPauseButtonIcon; +let replayButton; +let forwardButton; +let currentTimestamp; +let videoLength; + +async function main() { + displaySelectDialog = document.querySelector("#display-select-dialog"); + audioSinkSelectDialog = document.querySelector("#audio-sink-select-dialog"); + openNewVideoDialog = document.querySelector("#open-new-video-dialog"); + closeVideoDialog = document.querySelector("#close-video-dialog"); + + videoTemplate = document.querySelector("#video-template"); + videoContainer = document.querySelector("#video-container"); + + seekbar = document.querySelector("#seekbar"); + volumeBar = document.querySelector("#volume-bar"); + playPauseButton = document.querySelector("#play-pause"); + playPauseButtonIcon = playPauseButton.querySelector(".material-symbols-outlined"); + replayButton = document.querySelector("#replay"); + forwardButton = document.querySelector("#forward"); + currentTimestamp = document.querySelector("#current-timestamp"); + videoLength = document.querySelector("#video-length"); + + playPauseButton.addEventListener("click", onPlayPauseButtonClicked); + replayButton.addEventListener("click", onReplayButtonClicked); + forwardButton.addEventListener("click", onForwardButtonClicked); + seekbar.addEventListener("change", onSeekbarChanged); + volumeBar.addEventListener("input", onVolumeBarChanged); + + updateUI(); + setInterval(updateUI, 1000); +} + +async function updateUI() { + const videoDatas = await listVideos(); + const currentDisplay = localStorage.getItem("display"); + currentVideoData = videoDatas.find(videoData => videoData.display == currentDisplay); + + if (currentVideoData !== undefined) { + console.log(currentVideoData); + + seekbar.disabled = false; + volumeBar.disabled = false; + playPauseButton.disabled = false; + replayButton.disabled = false; + forwardButton.disabled = false; + + const {progress, duration} = formatTimestamps(currentVideoData.progress, currentVideoData.duration); + currentTimestamp.textContent = progress; + videoLength.textContent = duration; + seekbar.max = currentVideoData.duration; + seekbar.setValue(currentVideoData.progress); + volumeBar.setValue(currentVideoData.volume); + playPauseButtonIcon.textContent = currentVideoData.paused ? "play_circle" : "pause_circle"; + } else { + seekbar.disabled = true; + seekbar.setValue(0); + volumeBar.disabled = true; + playPauseButton.disabled = true; + playPauseButtonIcon.textContent = "play_circle"; + replayButton.disabled = true; + forwardButton.disabled = true; + currentTimestamp.textContent = ""; + videoLength.textContent = ""; + } + + await refreshVideoList(videoDatas); +} + +async function refreshVideoList(videoDatas) { + const videoIDs = videoDatas.map(videoData => videoData.id); + const shownVideoIDs = currentVideos.map(video => video.videoID); + const missingVideoIDs = videoIDs.filter(videoID => !shownVideoIDs.includes(videoID)); + const unneededVideoIDs = shownVideoIDs.filter(shownVideoID => !videoIDs.includes(shownVideoID)); + + for (let unneededVideoID of unneededVideoIDs) { + const index = currentVideos.findIndex(video => video.videoID == unneededVideoID); + const video = currentVideos[index]; + video.hide(); + currentVideos.splice(index, 1); + } + + for (let index in videoDatas) { + const videoData = videoDatas[index]; + + if (missingVideoIDs.includes(videoData.id)) { + const video = new Video(videoData); + video.showAt(index); + currentVideos.splice(index, 0, video); + } else { + const video = currentVideos.find(currentVideo => currentVideo.videoID == videoData.id); + video.update(videoData); + } + } +} + +async function onShowSelectDisplayDialogClicked() { + const displays = await listDisplays(); + const currentDisplay = localStorage.getItem("display"); + + const displaySelectForm = document.querySelector("#display-select-dialog-form"); + displaySelectForm.clearChildren(); + + for (let display of displays) { + const input = document.createElement("input"); + input.setAttribute("type", "radio"); + input.setAttribute("name", "display"); + input.setAttribute("id", `display_${display}`); + input.setAttribute("value", display); + input.addEventListener("change", onCloseSelectDisplayDialogClicked); + if (display == currentDisplay) input.setAttribute("checked", "checked"); + displaySelectForm.appendChild(input); + + const label = document.createElement("label"); + label.setAttribute("for", `display_${display}`); + label.textContent = display; + displaySelectForm.appendChild(label); + } + + displaySelectDialog.showModal(); +} + +function onCloseSelectDisplayDialogClicked() { + const selectedDisplay = document.forms["display-select-dialog-form"].elements["display"].value; + localStorage.setItem("display", selectedDisplay); + setTimeout(() => displaySelectDialog.close(), 100); +} + +async function onShowSelectAudioSinkDialogClicked() { + const audioSinks = await listAudioSinks(); + const currentAudioSink = localStorage.getItem("audio-sink"); + + const audioSinkSelectForm = document.querySelector("#audio-sink-select-dialog-form"); + audioSinkSelectForm.clearChildren(); + + for (let index in audioSinks) { + const audioSink = audioSinks[index]; + + const input = document.createElement("input"); + input.setAttribute("type", "radio"); + input.setAttribute("name", "audiosink"); + input.setAttribute("id", `audiosink_${index}`); + input.setAttribute("value", audioSink.description); + input.addEventListener("change", onCloseSelectAudioSinkDialogClicked); + if (audioSink.name == currentAudioSink) input.setAttribute("checked", "checked"); + audioSinkSelectForm.appendChild(input); + + const label = document.createElement("label"); + label.setAttribute("for", `audiosink_${index}`); + label.textContent = audioSink.description; + audioSinkSelectForm.appendChild(label); + } + + audioSinkSelectDialog.showModal(); +} + +function onCloseSelectAudioSinkDialogClicked() { + const selectedAudioSink = document.forms["audio-sink-select-dialog-form"].elements["audiosink"].value; + localStorage.setItem("audio-sink", selectedAudioSink); + changeAudioSink(selectedAudioSink); + setTimeout(() => audioSinkSelectDialog.close(), 100); +} + +function onShowOpenNewVideoDialogClicked() { + const link = document.querySelector("#open-new-video-dialog-link"); + link.value = ""; + openNewVideoDialog.showModal(); +} + +function onOpenVideoButtonClicked() { + const link = document.forms["open-new-video-dialog-form"].elements["link"].value; + openVideo(link); + openNewVideoDialog.close(); +} + +function onOpenVideoInBackgroundButtonClicked() { + const link = document.forms["open-new-video-dialog-form"].elements["link"].value; + openVideoInBackground(link); + openNewVideoDialog.close(); +} + +function onCloseOpenNewVideoDialogClicked() { + openNewVideoDialog.close(); +} + +function askForCloseVideoConfirmation(videoID, videoTitle) { + const videoTitleElement = document.querySelector("#close-video-dialog-title"); + videoTitleElement.textContent = videoTitle; + closeVideoDialogID = videoID; + closeVideoDialog.showModal(); +} + +function onCloseCloseVideoDialogClicked() { + closeVideoDialog.close(); +} + +function onCloseVideoButtonClicked() { + closeVideo(closeVideoDialogID) + closeVideoDialog.close(); +} + +function onCancelCloseVideoButtonClicked() { + closeVideoDialog.close(); +} + +function formatTimestamps(progress, duration) { + const progressHours = Math.floor(progress / 3600); + const progressRemaining = progress % 3600; + const progressMinutes = Math.floor(progressRemaining / 60); + const progressSeconds = progressRemaining % 60; + + const durationHours = Math.floor(duration / 3600); + const durationRemaining = duration % 3600; + const durationMinutes = Math.floor(durationRemaining / 60); + const durationSeconds = durationRemaining % 60; + + if (durationHours > 0) { + return { + progress: `${progressHours.pad(2)}:${progressMinutes.pad(2)}:${progressSeconds.pad(2)}`, + duration: `${durationHours.pad(2)}:${durationMinutes.pad(2)}:${durationSeconds.pad(2)}` + }; + } else { + return { + progress: `${progressMinutes.pad(2)}:${progressSeconds.pad(2)}`, + duration: `${durationMinutes.pad(2)}:${durationSeconds.pad(2)}` + }; + } +} + +function onPlayPauseButtonClicked() { + if (currentVideoData.paused) { + unpauseVideo(currentVideoData.id); + } else { + pauseVideo(currentVideoData.id); + } +} + +function onReplayButtonClicked() { + seekVideoRelative(currentVideoData.id, -10); +} + +function onForwardButtonClicked() { + seekVideoRelative(currentVideoData.id, 10); +} + +function onSeekbarChanged() { + seekVideo(currentVideoData.id, parseInt(seekbar.value)); +} + +function onVolumeBarChanged() { + setVolume(parseFloat(volumeBar.value)); +} \ No newline at end of file diff --git a/static/js/video.js b/static/js/video.js new file mode 100644 index 0000000..5bacbeb --- /dev/null +++ b/static/js/video.js @@ -0,0 +1,79 @@ +class Video { + + constructor(videoData) { + this.videoID = videoData.id; + this.shown = false; + + this.root = videoTemplate.content.cloneNode(true); + this.item = this.root.querySelector(".video-item"); + this.closeButton = this.root.querySelector(".video-close"); + this.img = this.root.querySelector(".video-item .video-image"); + this.title = this.root.querySelector(".video-item .video-title"); + this.videoProgressPrimary = this.root.querySelector(".video-item .video-progress .primary"); + this.videoProgressSecondary = this.root.querySelector(".video-item .video-progress .secondary"); + + this.item.addEventListener("click", this.enable.bind(this)); + this.closeButton.addEventListener("click", this.close.bind(this)); + + this.update(videoData); + } + + enable() { + const currentDisplay = localStorage.getItem("display"); + if (this.videoData.display != currentDisplay) { + showVideo(this.videoID, currentDisplay); + } else { + hideVideo(this.videoID); + } + } + + update(videoData) { + if (videoData.id != this.videoID) { + throw `tried to update video instance ${this.videoID} with data from video ${videoData.id}`; + } + + this.videoData = videoData; + + const progressPercentage = videoData.progress / videoData.duration * 100; + const bufferPercentage = videoData.buffered / videoData.duration * 100; + + this.img.src = videoData.metadata.thumbnail; + this.title.textContent = videoData.title; + this.videoProgressPrimary.style.width = `${progressPercentage}%`; + this.videoProgressSecondary.style.width = `${bufferPercentage}%`; + + const currentDisplay = localStorage.getItem("display"); + if (videoData.display == currentDisplay) { + this.img.classList.add("shown-on-display"); + } else { + this.img.classList.remove("shown-on-display"); + } + } + + shownOnDisplay() { + const currentDisplay = localStorage.getItem("display"); + return this.videoData.display == currentDisplay; + } + + show() { + videoContainer.appendChild(this.item); + this.shown = true; + } + + showAt(index) { + videoContainer.insertChildAtIndex(this.item, index); + this.shown = true; + } + + hide() { + videoContainer.removeChild(this.item); + this.shown = false; + } + + close(event) { + console.log(event); + event.stopPropagation(); + askForCloseVideoConfirmation(this.videoID, this.videoData.title); + } + +} \ No newline at end of file diff --git a/static/js/youtube.js b/static/js/youtube.js new file mode 100644 index 0000000..7574564 --- /dev/null +++ b/static/js/youtube.js @@ -0,0 +1,94 @@ + +async function listVideos() { + const resp = await fetch("/api/video/list/"); + return await resp.json(); +} + +async function listDisplays() { + const resp = await fetch("/api/display/list/"); + return await resp.json(); +} + +async function listAudioSinks() { + const resp = await fetch("/api/audio/list/"); + return await resp.json(); +} + +function changeAudioSink(sink) { + fetch("/api/audio/change/", { + method: "POST", + body: JSON.stringify({sink}), + }); +} + +function showVideo(videoID, display) { + fetch("/api/video/show/", { + method: "POST", + body: JSON.stringify({id: videoID, display: display}), + }).then(updateUI); +} + +function hideVideo(videoID) { + fetch("/api/video/hide/", { + method: "POST", + body: JSON.stringify({id: videoID}), + }).then(updateUI); +} + +function openVideo(link) { + const display = localStorage.getItem("display"); + fetch("/api/video/open/", { + method: "POST", + body: JSON.stringify({link, display}), + }).then(updateUI); +} + +function openVideoInBackground(link) { + fetch("/api/video/open/hide/", { + method: "POST", + body: JSON.stringify({link}), + }).then(updateUI); +} + +function closeVideo(videoID) { + fetch("/api/video/close/", { + method: "POST", + body: JSON.stringify({id: videoID}), + }).then(updateUI); +} + +function pauseVideo(videoID) { + fetch("/api/video/pause/", { + method: "POST", + body: JSON.stringify({id: videoID}), + }).then(updateUI); +} + +function unpauseVideo(videoID) { + fetch("/api/video/play/", { + method: "POST", + body: JSON.stringify({id: videoID}), + }).then(updateUI); +} + +function seekVideo(videoID, second) { + fetch("/api/video/seek/", { + method: "POST", + body: JSON.stringify({id: videoID, second: second}), + }).then(updateUI); +} + +function seekVideoRelative(videoID, second) { + fetch("/api/video/seek/relative/", { + method: "POST", + body: JSON.stringify({id: videoID, second: second}), + }).then(updateUI); +} + + +function setVolume(volume) { + fetch("/api/video/volume/", { + method: "POST", + body: JSON.stringify({volume}), + }).then(updateUI); +} \ No newline at end of file diff --git a/yt-remote-control b/yt-remote-control new file mode 100755 index 0000000..d7cc4af Binary files /dev/null and b/yt-remote-control differ