diff --git a/docker-compose.jsonnet b/docker-compose.jsonnet index 3031ee9..efd93b4 100644 --- a/docker-compose.jsonnet +++ b/docker-compose.jsonnet @@ -66,7 +66,9 @@ // Thrimbletrimmer (and probably not useful otherwise) to enable live updates // to Thrimbletrimmer without restarting/rebuilding Wubloader. // If you wish to use this, set this to the path containing the Thrimbletrimmer - // web (HTML, CSS, JavaScript) files to serve (e.g. "/path/to/wubloader/thrimbletrimmer/"). + // web (HTML, CSS, JavaScript) files to serve (e.g. "/path/to/wubloader/thrimbletrimmer/dist/"). + // This directory should be the build target when building Thrimbletrimmer manually; by default, + // this is the /dist/ subdirectory. thrimbletrimmer_web_dev_path:: null, // The host's port to expose each service on. diff --git a/thrimbletrimmer/.gitignore b/thrimbletrimmer/.gitignore new file mode 100644 index 0000000..3a8ec2b --- /dev/null +++ b/thrimbletrimmer/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +package-lock.json \ No newline at end of file diff --git a/thrimbletrimmer/.prettierignore b/thrimbletrimmer/.prettierignore index 36beca7..6c8051d 100644 --- a/thrimbletrimmer/.prettierignore +++ b/thrimbletrimmer/.prettierignore @@ -1,4 +1 @@ -scripts/hls.min.js -scripts/luxon.min.js -scripts/jcrop.js -styles/jcrop.css +src/external \ No newline at end of file diff --git a/thrimbletrimmer/clock.html b/thrimbletrimmer/clock.html deleted file mode 100644 index c99e542..0000000 --- a/thrimbletrimmer/clock.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - Stream Time - - - -
-
seconds of delay
- - - diff --git a/thrimbletrimmer/driveclock/bus_day.png b/thrimbletrimmer/driveclock/bus_day.png deleted file mode 100644 index c85bccd..0000000 Binary files a/thrimbletrimmer/driveclock/bus_day.png and /dev/null differ diff --git a/thrimbletrimmer/driveclock/bus_night.png b/thrimbletrimmer/driveclock/bus_night.png deleted file mode 100644 index bf58c3f..0000000 Binary files a/thrimbletrimmer/driveclock/bus_night.png and /dev/null differ diff --git a/thrimbletrimmer/driveclock/db_dawn.png b/thrimbletrimmer/driveclock/db_dawn.png deleted file mode 100644 index 552dc35..0000000 Binary files a/thrimbletrimmer/driveclock/db_dawn.png and /dev/null differ diff --git a/thrimbletrimmer/driveclock/db_day.png b/thrimbletrimmer/driveclock/db_day.png deleted file mode 100644 index 5cf7d3a..0000000 Binary files a/thrimbletrimmer/driveclock/db_day.png and /dev/null differ diff --git a/thrimbletrimmer/driveclock/db_dusk.png b/thrimbletrimmer/driveclock/db_dusk.png deleted file mode 100644 index b58fb45..0000000 Binary files a/thrimbletrimmer/driveclock/db_dusk.png and /dev/null differ diff --git a/thrimbletrimmer/driveclock/db_night.png b/thrimbletrimmer/driveclock/db_night.png deleted file mode 100644 index c444a53..0000000 Binary files a/thrimbletrimmer/driveclock/db_night.png and /dev/null differ diff --git a/thrimbletrimmer/driveclock/drive.html b/thrimbletrimmer/driveclock/drive.html deleted file mode 100644 index 6d79160..0000000 --- a/thrimbletrimmer/driveclock/drive.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - -
-
-
-
-
-
- - - - - diff --git a/thrimbletrimmer/driveclock/drive.js b/thrimbletrimmer/driveclock/drive.js deleted file mode 100644 index 788bde4..0000000 --- a/thrimbletrimmer/driveclock/drive.js +++ /dev/null @@ -1,122 +0,0 @@ - -const PAGE_WIDTH = 1920; -const MINUTES_PER_PAGE = 60; -const POINT_WIDTH = PAGE_WIDTH * 8 * 60 / MINUTES_PER_PAGE; -const MILES_PER_PAGE = 45; -const BUS_POSITION_X = 93; -const BASE_ODO = 109.3; -const UPDATE_INTERVAL_MS = 5000 -const WUBLOADER_URL = ""; -const SKY_URLS = { - day: "db_day.png", - dawn: "db_dawn.png", - dusk: "db_dusk.png", - night: "db_night.png", -}; -const BUS_URLS = { - day: "bus_day.png", - dawn: "bus_day.png", - dusk: "bus_day.png", - night: "bus_night.png", -}; - -function setSkyElements(left, right, timeToTransition) { - const leftElement = document.getElementById("timeofday-left"); - const rightElement = document.getElementById("timeofday-right"); - const busElement = document.getElementById("bus"); - - leftElement.style.backgroundImage = `url(${SKY_URLS[left]})`; - rightElement.style.backgroundImage = `url(${SKY_URLS[right]})`; - - if (left === right) { - leftElement.style.width = "100%"; - } else { - const transitionPercent = timeToTransition / MINUTES_PER_PAGE; - leftElement.style.width = `${transitionPercent * 100}%` - } - - bus.style.backgroundImage = `url(${BUS_URLS[left]})`; -} - -function nextSkyTransition(timeofday, clock) { - switch (timeofday) { - case "dawn": - case "day": - return [19 * 60, "dusk"]; // 7pm - case "dusk": - return [20 * 60, "night"]; // 8pm - case "night": - return [6 * 60 + 40, "dawn"]; // 6:40am - } -} - -function setSky(timeofday, clock) { - const [transition, newSky] = nextSkyTransition(timeofday, clock); - // 1440 minutes in 24h, this code will return time remaining even if - // the transition is in the morning and we're currently in the evening. - const timeToTransition = (1440 + transition - clock) % 1440; - if (timeToTransition < MINUTES_PER_PAGE) { - // Transition on screen - setSkyElements(timeofday, newSky, timeToTransition); - } else { - // No transition on screen - setSkyElements(timeofday, timeofday, undefined); - } -} - -function setOdo(odo) { - const distancePixels = PAGE_WIDTH * (odo - BASE_ODO) / MILES_PER_PAGE; - const offset = (BUS_POSITION_X - distancePixels) % POINT_WIDTH; - - const stopsElement = document.getElementById("stops"); - stopsElement.style.backgroundPosition = `${offset}px 0px`; -} - -async function update() { - const busDataResponse = await fetch(`${WUBLOADER_URL}/thrimshim/bus/buscam`); - if (!busDataResponse.ok) { - return; - } - const busData = await busDataResponse.json(); - console.log("Got data:", busData); - setOdo(busData.odometer); - setSky(busData.timeofday, busData.clock_minutes); -} - -// Initial conditions, before the first refresh finishes -setSky("day", 7 * 60); -setOdo(BASE_ODO); - -// Testing mode. Set true to enable. -const test = false; -if (test) { - let h = 0; - // Set to how long 1h of in-game time should take in real time - const hourTimeMs = 1 * 1000; - // Set to how often to update the screen - const interval = 30; - setInterval(() => { - h += interval / hourTimeMs; - setOdo(BASE_ODO + 45 * h); - if (h < 19) { - setSky("day", 60 * h); - } else { - m = (h % 24) * 60; - let tod; - if (m < 6 * 60 + 40) { - tod = "night"; - } else if (m < 19 * 60) { - tod = "dawn"; - } else if (m < 20 * 60) { - tod = "dusk"; - } else { - tod = "night"; - } - setSky(tod, m); - } - }, interval); -} else { - // Do first update immediately, then every UPDATE_INTERVAL_MS - setInterval(update, UPDATE_INTERVAL_MS); - update(); -} diff --git a/thrimbletrimmer/driveclock/stops.png b/thrimbletrimmer/driveclock/stops.png deleted file mode 100644 index 552266e..0000000 Binary files a/thrimbletrimmer/driveclock/stops.png and /dev/null differ diff --git a/thrimbletrimmer/edit.html b/thrimbletrimmer/edit.html index ef79512..b5621e7 100644 --- a/thrimbletrimmer/edit.html +++ b/thrimbletrimmer/edit.html @@ -1,485 +1,12 @@ - + - VST Video Editor - - - - - - - - - - - - - - + Thrimbletrimmer - Editor -
-
-
- Keyboard Shortcuts - -
-
-
- Stream - -
-
- - - -
-
- - - -
-
- -
-
- - - -
-
-
- -
-
- - / - -
-
-
- - -
-
- -
-
- -
-
- -
-
- -
- -
-
- Waveform for the video -
-
- -
- - -
-
-
-
-
- - Set range start point to the current video time - Play from start point -
- - Set range end point to the current video time - Play from end point -
- Range affected by keyboard shortcuts -
- - -
-
- Add range -
- -
- - -
- - -
- -
- - - - - -
-
- -
-
- -
-
- - Set video thumbnail frame to current video time - Set video time to video thumbnail frame -
-
-
- Advanced Templating Options - Crop specifies the region of the video frame to capture.
- Location specifies the region within the template image where the cropped image will - be placed.
- Regions are given as pixel coordinates of the top-left and bottom-right corners. -
- Note that if the regions are different sizes, the image will be stretched.
- - - -
- -
-
- -
- Crop: - - - to - - -
-
- - - -
- -
- Location: - - - to - - -
-
-
-
-
- -
- -
- -
-
-
-
- -
-
- - - - Advanced Submission Options -
- -
-
- -
- - - Download Video - Download Current Frame as Image -
- -
- - - -
-
- -
-
- -
+
-
-
+ diff --git a/thrimbletrimmer/images/arrow.png b/thrimbletrimmer/images/arrow.png deleted file mode 100644 index cf40987..0000000 Binary files a/thrimbletrimmer/images/arrow.png and /dev/null differ diff --git a/thrimbletrimmer/images/minus.png b/thrimbletrimmer/images/minus.png deleted file mode 100644 index 6bf7fe7..0000000 Binary files a/thrimbletrimmer/images/minus.png and /dev/null differ diff --git a/thrimbletrimmer/images/pencil.png b/thrimbletrimmer/images/pencil.png deleted file mode 100644 index 3cd7dab..0000000 Binary files a/thrimbletrimmer/images/pencil.png and /dev/null differ diff --git a/thrimbletrimmer/images/play_to.png b/thrimbletrimmer/images/play_to.png deleted file mode 100644 index 7df95b0..0000000 Binary files a/thrimbletrimmer/images/play_to.png and /dev/null differ diff --git a/thrimbletrimmer/images/plus.png b/thrimbletrimmer/images/plus.png deleted file mode 100644 index 5d86af7..0000000 Binary files a/thrimbletrimmer/images/plus.png and /dev/null differ diff --git a/thrimbletrimmer/images/video-controls/fullscreen.png b/thrimbletrimmer/images/video-controls/fullscreen.png deleted file mode 100644 index 664bc2a..0000000 Binary files a/thrimbletrimmer/images/video-controls/fullscreen.png and /dev/null differ diff --git a/thrimbletrimmer/images/video-controls/pause.png b/thrimbletrimmer/images/video-controls/pause.png deleted file mode 100644 index 8de238f..0000000 Binary files a/thrimbletrimmer/images/video-controls/pause.png and /dev/null differ diff --git a/thrimbletrimmer/images/video-controls/play.png b/thrimbletrimmer/images/video-controls/play.png deleted file mode 100644 index 30d233a..0000000 Binary files a/thrimbletrimmer/images/video-controls/play.png and /dev/null differ diff --git a/thrimbletrimmer/images/video-controls/volume-mute.png b/thrimbletrimmer/images/video-controls/volume-mute.png deleted file mode 100644 index 1f907e4..0000000 Binary files a/thrimbletrimmer/images/video-controls/volume-mute.png and /dev/null differ diff --git a/thrimbletrimmer/images/video-controls/volume.png b/thrimbletrimmer/images/video-controls/volume.png deleted file mode 100644 index aeb1254..0000000 Binary files a/thrimbletrimmer/images/video-controls/volume.png and /dev/null differ diff --git a/thrimbletrimmer/index.html b/thrimbletrimmer/index.html index fe5eab3..08c6c8c 100644 --- a/thrimbletrimmer/index.html +++ b/thrimbletrimmer/index.html @@ -1,170 +1,12 @@ - + - VST Restreamer - - - - - - - - - + Thrimbletrimmer - Restreamer -
-
-
- Keyboard Shortcuts - -
-
-
- - -
-
- - -
-
- - -
-
-
- - - - - - -
-
-
- -
-
- Link to this time range -
-
- - - -
-
-
- -
-
- - / - -
-
-
- - -
-
- -
-
- -
-
- -
-
- -
- -
- Download Video - Download Current Frame as Image - Convert Times -
- +
-
-
+ diff --git a/thrimbletrimmer/package.json b/thrimbletrimmer/package.json new file mode 100644 index 0000000..cec134e --- /dev/null +++ b/thrimbletrimmer/package.json @@ -0,0 +1,24 @@ +{ + "name": "thrimbletrimmer", + "version": "4.0.0", + "description": "Video editor frontend for Wubloader", + "type": "module", + "scripts": { + "start": "vite", + "dev": "vite", + "build": "vite build", + "serve": "vite preview" + }, + "license": "MIT", + "devDependencies": { + "sass": "1.80.6", + "solid-devtools": "0.30.1", + "typescript": "5.6.3", + "url": "0.11.4", + "vite": "5.4.10", + "vite-plugin-solid": "2.10.2" + }, + "dependencies": { + "solid-js": "1.9.3" + } +} diff --git a/thrimbletrimmer/scripts/chat-load.js b/thrimbletrimmer/scripts/chat-load.js deleted file mode 100644 index 0f6b4ee..0000000 --- a/thrimbletrimmer/scripts/chat-load.js +++ /dev/null @@ -1,49 +0,0 @@ -self.importScripts("luxon.min.js", "common-worker.js"); - -var DateTime = luxon.DateTime; -luxon.Settings.defaultZone = "utc"; - -self.onmessage = async (event) => { - const chatLoadData = event.data; - - const segmentMetadata = chatLoadData.segmentMetadata; - for (const segmentData of segmentMetadata) { - segmentData.rawStart = DateTime.fromMillis(segmentData.rawStart); - segmentData.rawEnd = DateTime.fromMillis(segmentData.rawEnd); - } - - const fetchURL = `/${chatLoadData.stream}/chat.json?start=${chatLoadData.start}&end=${chatLoadData.end}`; - const chatResponse = await fetch(fetchURL); - if (!chatResponse.ok) { - return; - } - const chatRawData = await chatResponse.json(); - - const chatData = []; - for (const chatLine of chatRawData) { - if ( - chatLine.command !== "PRIVMSG" && - chatLine.command !== "CLEARMSG" && - chatLine.command !== "CLEARCHAT" && - chatLine.command !== "USERNOTICE" - ) { - continue; - } - const when = DateTime.fromSeconds(chatLine.time); - const displayWhen = videoHumanTimeFromDateTimeWithFragments(segmentMetadata, when); - // Here, we just push each line successively into the list. This assumes data is provided to us in chronological order. - chatData.push({ message: chatLine, when: when.toMillis(), displayWhen: displayWhen }); - } - self.postMessage(chatData); -}; - -function videoHumanTimeFromDateTimeWithFragments(fragmentMetadata, dateTime) { - for (const segmentData of fragmentMetadata) { - if (dateTime >= segmentData.rawStart && dateTime <= segmentData.rawEnd) { - const playerTime = - segmentData.playerStart + dateTime.diff(segmentData.rawStart).as("seconds"); - return videoHumanTimeFromVideoPlayerTime(playerTime); - } - } - return null; -} diff --git a/thrimbletrimmer/scripts/common-worker.js b/thrimbletrimmer/scripts/common-worker.js deleted file mode 100644 index 532d556..0000000 --- a/thrimbletrimmer/scripts/common-worker.js +++ /dev/null @@ -1,21 +0,0 @@ -function videoHumanTimeFromVideoPlayerTime(videoPlayerTime) { - const hours = Math.floor(videoPlayerTime / 3600); - let minutes = Math.floor((videoPlayerTime % 3600) / 60); - let seconds = Math.floor(videoPlayerTime % 60); - let milliseconds = Math.floor((videoPlayerTime * 1000) % 1000); - - while (minutes.toString().length < 2) { - minutes = `0${minutes}`; - } - while (seconds.toString().length < 2) { - seconds = `0${seconds}`; - } - while (milliseconds.toString().length < 3) { - milliseconds = `0${milliseconds}`; - } - - if (hours > 0) { - return `${hours}:${minutes}:${seconds}.${milliseconds}`; - } - return `${minutes}:${seconds}.${milliseconds}`; -} diff --git a/thrimbletrimmer/scripts/common.js b/thrimbletrimmer/scripts/common.js deleted file mode 100644 index 852da85..0000000 --- a/thrimbletrimmer/scripts/common.js +++ /dev/null @@ -1,736 +0,0 @@ -var DateTime = luxon.DateTime; -var Interval = luxon.Interval; -luxon.Settings.defaultZone = "utc"; - -var globalBusStartTime = DateTime.fromISO("1970-01-01T00:00:00"); -var globalStreamName = ""; -var globalStartTimeString = ""; -var globalEndTimeString = ""; - -var globalPlayer = null; -var globalSetUpControls = false; -var globalSeekTimer = null; - -var globalChatData = []; -var globalLoadChatWorker = null; - -Hls.DefaultConfig.maxBufferHole = 600; - -const VIDEO_FRAMES_PER_SECOND = 30; - -const PLAYBACK_RATES = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4, 8]; - -function commonPageSetup() { - if (!Hls.isSupported()) { - addError( - "Your browser doesn't support MediaSource extensions. Video playback and editing won't work.", - ); - } - - globalLoadChatWorker = new Worker("scripts/chat-load.js"); -} - -function addError(errorText) { - const errorElement = document.createElement("div"); - errorElement.innerText = errorText; - - const dismissElement = document.createElement("a"); - dismissElement.classList.add("error-dismiss"); - dismissElement.innerText = "[X]"; - errorElement.appendChild(dismissElement); - dismissElement.addEventListener("click", (event) => { - const errorHost = document.getElementById("errors"); - errorHost.removeChild(errorElement); - }); - - const errorHost = document.getElementById("errors"); - errorHost.appendChild(errorElement); -} - -async function loadVideoPlayer(playlistURL) { - let rangedPlaylistURL = assembleVideoPlaylistURL(playlistURL); - const videoElement = document.getElementById("video"); - - videoElement.addEventListener("loadedmetadata", (_event) => { - setUpVideoControls(); - sendChatLogLoadData(); - }); - - videoElement.addEventListener("loadeddata", (_event) => { - const qualitySelector = document.getElementById("video-controls-quality"); - globalPlayer.currentLevel = +qualitySelector.value; - }); - - globalPlayer = new Hls(); - globalPlayer.attachMedia(video); - return new Promise((resolve, _reject) => { - globalPlayer.on(Hls.Events.MEDIA_ATTACHED, () => { - const startTime = getStartTime(); - const endTime = getEndTime(); - if (endTime && endTime.diff(startTime).milliseconds < 0) { - addError( - "End time is before the start time. This will prevent video loading and cause other problems.", - ); - } - globalPlayer.loadSource(rangedPlaylistURL); - - globalPlayer.on(Hls.Events.ERROR, (_event, data) => { - if (data.fatal) { - switch (data.type) { - case Hls.ErrorTypes.NETWORK_ERROR: - if (data.reason === "no level found in manifest") { - addError( - "There is no video data between the specified start and end times. Change the times so that there is video content to play.", - ); - } else { - console.log("A fatal network error occurred; retrying", data); - globalPlayer.startLoad(); - } - break; - case Hls.ErrorTypes.MEDIA_ERROR: - console.log("A fatal media error occurred; retrying", data); - globalPlayer.recoverMediaError(); - break; - default: - console.log("A fatal error occurred; resetting video player", data); - addError( - "Some sort of video player error occurred. Thrimbletrimmer is resetting the video player.", - ); - resetVideoPlayer(); - } - } else { - console.log("A non-fatal video player error occurred; HLS.js will retry", data); - } - }); - - resolve(); - }); - }); -} - -async function loadVideoPlayerFromDefaultPlaylist() { - const playlistURL = `/playlist/${globalStreamName}.m3u8`; - await loadVideoPlayer(playlistURL); -} - -function resetVideoPlayer() { - updateSegmentPlaylist(); -} - -function updateSegmentPlaylist() { - const videoElement = document.getElementById("video"); - const currentPlaybackRate = videoElement.playbackRate; - globalPlayer.destroy(); - loadVideoPlayerFromDefaultPlaylist(); - // The playback rate isn't maintained when destroying and reattaching hls.js - videoElement.playbackRate = currentPlaybackRate; -} - -function setUpVideoControls() { - // Setting this up so it's removed from the event doesn't work; loadedmetadata fires twice anyway. - // We still need to prevent double-setup, so here we are. - if (globalSetUpControls) { - return; - } - globalSetUpControls = true; - - const videoElement = document.getElementById("video"); - - const playPauseButton = document.getElementById("video-controls-play-pause"); - if (videoElement.paused) { - playPauseButton.src = "images/video-controls/play.png"; - } else { - playPauseButton.src = "images/video-controls/pause.png"; - } - - const togglePlayState = (_event) => { - if (videoElement.paused) { - videoElement.play(); - } else { - videoElement.pause(); - } - }; - playPauseButton.addEventListener("click", togglePlayState); - videoElement.addEventListener("click", (event) => { - if (!videoElement.controls) { - togglePlayState(event); - } - }); - - videoElement.addEventListener("play", (_event) => { - playPauseButton.src = "images/video-controls/pause.png"; - }); - videoElement.addEventListener("pause", (_event) => { - playPauseButton.src = "images/video-controls/play.png"; - }); - - const currentTime = document.getElementById("video-controls-current-time"); - currentTime.innerText = videoHumanTimeFromVideoPlayerTime(videoElement.currentTime); - videoElement.addEventListener("timeupdate", (_event) => { - currentTime.innerText = videoHumanTimeFromVideoPlayerTime(videoElement.currentTime); - }); - - const duration = document.getElementById("video-controls-duration"); - duration.innerText = videoHumanTimeFromVideoPlayerTime(videoElement.duration); - videoElement.addEventListener("durationchange", (_event) => { - duration.innerText = videoHumanTimeFromVideoPlayerTime(videoElement.duration); - }); - - const volumeMuted = document.getElementById("video-controls-volume-mute"); - if (videoElement.muted) { - volumeMuted.src = "images/video-controls/volume-mute.png"; - } else { - volumeMuted.src = "images/video-controls/volume.png"; - } - const volumeLevel = document.getElementById("video-controls-volume-level"); - const defaultVolume = +(localStorage.getItem("volume") ?? 0.5); - if (isNaN(defaultVolume)) { - defaultVolume = 0.5; - } else if (defaultVolume < 0) { - defaultVolume = 0; - } else if (defaultVolume > 1) { - defaultVolume = 1; - } - videoElement.volume = defaultVolume; - volumeLevel.value = videoElement.volume; - - volumeMuted.addEventListener("click", (_event) => { - videoElement.muted = !videoElement.muted; - }); - volumeLevel.addEventListener("click", (event) => { - videoElement.volume = event.offsetX / event.target.offsetWidth; - videoElement.muted = false; - }); - videoElement.addEventListener("volumechange", (_event) => { - if (videoElement.muted) { - volumeMuted.src = "images/video-controls/volume-mute.png"; - } else { - volumeMuted.src = "images/video-controls/volume.png"; - } - volumeLevel.value = videoElement.volume; - localStorage.setItem("volume", videoElement.volume); - }); - - const playbackSpeed = document.getElementById("video-controls-playback-speed"); - for (const speed of PLAYBACK_RATES) { - const speedOption = document.createElement("option"); - speedOption.value = speed; - speedOption.innerText = `${speed}x`; - if (speed === 1) { - speedOption.selected = true; - } - playbackSpeed.appendChild(speedOption); - } - playbackSpeed.addEventListener("change", (_event) => { - const speed = +playbackSpeed.value; - videoElement.playbackRate = speed; - }); - - const quality = document.getElementById("video-controls-quality"); - const defaultQuality = localStorage.getItem("quality"); - for (const [qualityIndex, qualityLevel] of globalPlayer.levels.entries()) { - const qualityOption = document.createElement("option"); - qualityOption.value = qualityIndex; - qualityOption.innerText = qualityLevel.name; - if (qualityLevel.name === defaultQuality) { - qualityOption.selected = true; - } - quality.appendChild(qualityOption); - } - localStorage.setItem("quality", quality.options[quality.options.selectedIndex].innerText); - quality.addEventListener("change", (_event) => { - globalPlayer.currentLevel = +quality.value; - localStorage.setItem("quality", quality.options[quality.options.selectedIndex].innerText); - }); - - const fullscreen = document.getElementById("video-controls-fullscreen"); - fullscreen.addEventListener("click", (_event) => { - if (document.fullscreenElement) { - document.exitFullscreen(); - } else { - videoElement.requestFullscreen(); - } - }); - videoElement.addEventListener("fullscreenchange", (_event) => { - if (document.fullscreenElement) { - videoElement.controls = true; - } else { - videoElement.controls = false; - } - }); - - const playbackPosition = document.getElementById("video-controls-playback-position"); - playbackPosition.max = videoElement.duration; - playbackPosition.value = videoElement.currentTime; - videoElement.addEventListener("durationchange", (_event) => { - playbackPosition.max = videoElement.duration; - }); - videoElement.addEventListener("timeupdate", (_event) => { - playbackPosition.value = videoElement.currentTime; - }); - playbackPosition.addEventListener("click", (event) => { - const newPosition = (event.offsetX / event.target.offsetWidth) * videoElement.duration; - videoElement.currentTime = newPosition; - playbackPosition.value = newPosition; - }); - - /* Sometimes a mysterious issue occurs loading segments of the video when seeking. - * When this happens, twiddling the qualities tends to fix it. Here, we attempt to - * detect this situation and fix it automatically. - */ - videoElement.addEventListener("seeking", (_event) => { - // If we don't get a "seeked" event soon after the "seeking" event, we assume there's - // a loading error. - // To handle this, we set up a timed handler to pick this up. - if (globalSeekTimer !== null) { - clearTimeout(globalSeekTimer); - globalSeekTimer = null; - } - globalSeekTimer = setTimeout(() => { - const currentLevel = globalPlayer.currentLevel; - globalPlayer.currentLevel = -1; - globalPlayer.currentLevel = currentLevel; - }, 500); - }); - videoElement.addEventListener("seeked", (_event) => { - // Since we got the seek, cancel the timed twiddling of qualities - if (globalSeekTimer !== null) { - clearTimeout(globalSeekTimer); - globalSeekTimer = null; - } - }); -} - -function dateTimeMathObjectFromBusTime(busTime) { - // We need to handle inputs like "-0:10:15" in a way that consistently makes the time negative. - // Since we can't assign the negative sign to any particular part, we'll check for the whole thing here. - let direction = 1; - if (busTime.startsWith("-")) { - busTime = busTime.slice(1); - direction = -1; - } - - const parts = busTime.split(":", 3); - const hours = parseInt(parts[0]) * direction; - const minutes = (parts[1] || 0) * direction; - const seconds = (parts[2] || 0) * direction; - return { hours: hours, minutes: minutes, seconds: seconds }; -} - -function dateTimeFromBusTime(busTime) { - return globalBusStartTime.plus(dateTimeMathObjectFromBusTime(busTime)); -} - -function busTimeFromDateTime(dateTime) { - const diff = dateTime.diff(globalBusStartTime); - return formatIntervalForDisplay(diff); -} - -function formatIntervalForDisplay(interval) { - if (interval.milliseconds < 0) { - const negativeInterval = interval.negate(); - return `-${negativeInterval.toFormat("hh:mm:ss.SSS")}`; - } - return interval.toFormat("hh:mm:ss.SSS"); -} - -function dateTimeFromWubloaderTime(wubloaderTime) { - return DateTime.fromISO(wubloaderTime); -} - -function wubloaderTimeFromDateTime(dateTime) { - if (!dateTime) { - return null; - } - // Not using ISO here because Luxon doesn't give us a quick way to print an ISO8601 string with no offset. - return dateTime.toFormat("yyyy-LL-dd'T'HH:mm:ss.SSS"); -} - -function busTimeFromWubloaderTime(wubloaderTime) { - if (wubloaderTime === "") { - return ""; - } - const dt = dateTimeFromWubloaderTime(wubloaderTime); - return busTimeFromDateTime(dt); -} - -function videoHumanTimeFromVideoPlayerTime(videoPlayerTime) { - const hours = Math.floor(videoPlayerTime / 3600); - let minutes = Math.floor((videoPlayerTime % 3600) / 60); - let seconds = Math.floor(videoPlayerTime % 60); - let milliseconds = Math.floor((videoPlayerTime * 1000) % 1000); - - while (minutes.toString().length < 2) { - minutes = `0${minutes}`; - } - while (seconds.toString().length < 2) { - seconds = `0${seconds}`; - } - while (milliseconds.toString().length < 3) { - milliseconds = `0${milliseconds}`; - } - - if (hours > 0) { - return `${hours}:${minutes}:${seconds}.${milliseconds}`; - } - return `${minutes}:${seconds}.${milliseconds}`; -} - -function videoPlayerTimeFromVideoHumanTime(videoHumanTime) { - let timeParts = videoHumanTime.split(":", 3); - let hours; - let minutes; - let seconds; - - if (timeParts.length < 2) { - hours = 0; - minutes = 0; - seconds = +timeParts[0]; - } else if (timeParts.length < 3) { - hours = 0; - minutes = parseInt(timeParts[0]); - seconds = +timeParts[1]; - } else { - hours = parseInt(timeParts[0]); - minutes = parseInt(timeParts[1]); - seconds = +timeParts[2]; - } - if (isNaN(hours) || isNaN(minutes) || isNaN(seconds)) { - return null; - } - - return hours * 3600 + minutes * 60 + seconds; -} - -function dateTimeFromVideoPlayerTime(videoPlayerTime) { - const segmentList = getSegmentList(); - let segmentStartTime; - let segmentStartISOTime; - for (const segment of segmentList) { - const segmentEndTime = segment.start + segment.duration; - if (videoPlayerTime >= segment.start && videoPlayerTime < segmentEndTime) { - segmentStartTime = segment.start; - segmentStartISOTime = segment.rawProgramDateTime; - break; - } - } - if (segmentStartISOTime === undefined) { - return null; - } - const wubloaderDateTime = DateTime.fromISO(segmentStartISOTime); - const offset = videoPlayerTime - segmentStartTime; - return wubloaderDateTime.plus({ seconds: offset }); -} - -function videoPlayerTimeFromDateTime(dateTime) { - const segmentTimes = getSegmentTimes(); - for (const segmentData of segmentTimes) { - const segmentStart = segmentData.rawStart; - const segmentEnd = segmentData.rawEnd; - if (dateTime >= segmentStart && dateTime <= segmentEnd) { - return segmentData.playerStart + dateTime.diff(segmentStart).as("seconds"); - } - } - return null; -} - -function videoHumanTimeFromDateTime(dateTime) { - const videoPlayerTime = videoPlayerTimeFromDateTime(dateTime); - if (videoPlayerTime === null) { - return null; - } - return videoHumanTimeFromVideoPlayerTime(videoPlayerTime); -} - -function assembleVideoPlaylistURL(basePlaylistURL) { - let playlistURL = basePlaylistURL; - - const query = startAndEndTimeQuery(); - if (query.toString() !== "") { - playlistURL += "?" + query.toString(); - } - return playlistURL; -} - -function startAndEndTimeQuery() { - const startTime = getStartTime(); - const endTime = getEndTime(); - - const query = new URLSearchParams(); - if (startTime) { - query.append("start", wubloaderTimeFromDateTime(startTime)); - } - if (endTime) { - query.append("end", wubloaderTimeFromDateTime(endTime)); - } - return query; -} - -function getSegmentList() { - return globalPlayer.latencyController.levelDetails.fragments; -} - -function hasSegmentList() { - if ( - globalPlayer && - globalPlayer.latencyController && - globalPlayer.latencyController.levelDetails && - globalPlayer.latencyController.levelDetails.fragments - ) { - return true; - } - return false; -} - -function getSegmentTimes() { - const segmentList = getSegmentList(); - const segmentTimes = []; - for (const segment of segmentList) { - const segmentStart = DateTime.fromISO(segment.rawProgramDateTime); - const segmentEnd = segmentStart.plus({ seconds: segment.duration }); - segmentTimes.push({ rawStart: segmentStart, rawEnd: segmentEnd, playerStart: segment.start }); - } - return segmentTimes; -} - -function downloadFrame() { - const videoElement = document.getElementById("video"); - const dateTime = dateTimeFromVideoPlayerTime(videoElement.currentTime); - const url = `/frame/${globalStreamName}/source.png?timestamp=${wubloaderTimeFromDateTime( - dateTime, - )}`; - // Avoid : as it causes problems on Windows - const filename = `${dateTime.toFormat("yyyy-LL-dd'T'HH-mm-ss.SSS")}.png`; - triggerDownload(url, filename); -} - -function triggerDownload(url, filename) { - // URL must be same-origin. - const link = document.createElement("a"); - link.setAttribute("download", filename); - link.href = url; - link.setAttribute("target", "_blank"); - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); -} - -function sendChatLogLoadData() { - let startTime = getStartTime(); - let endTime = getEndTime(); - if (!startTime || !endTime) { - return; - } - startTime = wubloaderTimeFromDateTime(startTime); - endTime = wubloaderTimeFromDateTime(endTime); - const segmentMetadata = getSegmentTimes(); - for (const segmentData of segmentMetadata) { - segmentData.rawStart = segmentData.rawStart.toMillis(); - segmentData.rawEnd = segmentData.rawEnd.toMillis(); - } - - const message = { - stream: globalStreamName, - start: startTime, - end: endTime, - segmentMetadata: segmentMetadata, - }; - globalLoadChatWorker.postMessage(message); -} - -function updateChatDataFromWorkerResponse(chatData) { - for (const chatLine of chatData) { - chatLine.when = DateTime.fromMillis(chatLine.when); - } - globalChatData = chatData; -} - -function renderChatMessage(chatMessageData) { - const chatMessage = chatMessageData.message; - if (chatMessage.command !== "PRIVMSG" || chatMessage.params[0] !== `#${globalStreamName}`) { - return null; - } - - const sendTimeElement = document.createElement("div"); - sendTimeElement.classList.add("chat-replay-message-time"); - sendTimeElement.innerText = chatMessageData.displayWhen; - - const senderNameElement = createMessageSenderElement(chatMessageData); - - const messageTextElement = document.createElement("div"); - messageTextElement.classList.add("chat-replay-message-text"); - - if (chatMessage.tags.hasOwnProperty("reply-parent-msg-id")) { - const replyParentID = chatMessage.tags["reply-parent-msg-id"]; - const replyParentSender = chatMessage.tags["reply-parent-display-name"]; - let replyParentMessageText = chatMessage.tags["reply-parent-msg-body"]; - const replyContainer = document.createElement("div"); - const replyTextContainer = document.createElement("a"); - - if (replyParentMessageText.startsWith("\u0001ACTION")) { - replyContainer.classList.add("chat-replay-message-text-action"); - const substringEnd = replyParentMessageText.endsWith("\u0001") - ? replyParentMessageText.length - 1 - : replyParentMessageText; - replyParentMessageText = replyParentMessageText.substring(7, substringEnd); - } - - replyTextContainer.href = `#chat-replay-message-${replyParentID}`; - replyTextContainer.innerText = `Replying to ${replyParentSender}: ${replyParentMessageText}`; - replyContainer.appendChild(replyTextContainer); - replyContainer.classList.add("chat-replay-message-reply"); - messageTextElement.appendChild(replyContainer); - } - - addChatMessageTextToElement(chatMessageData, messageTextElement); - - const messageContainer = createMessageContainer(chatMessageData, false); - messageContainer.appendChild(sendTimeElement); - messageContainer.appendChild(senderNameElement); - messageContainer.appendChild(messageTextElement); - return messageContainer; -} - -function renderSystemMessages(chatMessageData) { - const chatMessage = chatMessageData.message; - if (chatMessage.command !== "USERNOTICE" || chatMessage.params[0] != `#${globalStreamName}`) { - return []; - } - - const messages = []; - - const sendTimeElement = document.createElement("div"); - sendTimeElement.classList.add("chat-replay-message-time"); - sendTimeElement.innerText = chatMessageData.displayWhen; - - const systemTextElement = document.createElement("div"); - systemTextElement.classList.add("chat-replay-message-text"); - systemTextElement.classList.add("chat-replay-message-system"); - let systemMsg = chatMessage.tags["system-msg"]; - if (!systemMsg && chatMessage.tags["msg-id"] === "announcement") { - systemMsg = "Announcement"; - } - systemTextElement.appendChild(document.createTextNode(systemMsg)); - - const firstMessageContainer = createMessageContainer(chatMessageData, true); - firstMessageContainer.appendChild(sendTimeElement); - firstMessageContainer.appendChild(systemTextElement); - messages.push(firstMessageContainer); - - if (chatMessage.params.length === 1) { - return messages; - } - - const emptySendTimeElement = document.createElement("div"); - emptySendTimeElement.classList.add("chat-replay-message-time"); - - const senderNameElement = createMessageSenderElement(chatMessageData); - - const messageTextElement = document.createElement("div"); - messageTextElement.classList.add("chat-replay-message-text"); - addChatMessageTextToElement(chatMessageData, messageTextElement); - - const secondMessageContainer = createMessageContainer(chatMessageData, false); - secondMessageContainer.appendChild(emptySendTimeElement); - secondMessageContainer.appendChild(senderNameElement); - secondMessageContainer.appendChild(messageTextElement); - messages.push(secondMessageContainer); - - return messages; -} - -function createMessageContainer(chatMessageData, isSystemMessage) { - const chatMessage = chatMessageData.message; - const messageContainer = document.createElement("div"); - messageContainer.classList.add("chat-replay-message"); - if (chatMessage.tags.hasOwnProperty("id")) { - if (isSystemMessage) { - messageContainer.id = `chat-replay-message-system-${chatMessage.tags.id}`; - } else { - messageContainer.id = `chat-replay-message-${chatMessage.tags.id}`; - } - } - messageContainer.dataset.sender = chatMessage.sender; - return messageContainer; -} - -function getMessageDisplayName(chatMessageData) { - const chatMessage = chatMessageData.message; - if (chatMessage.tags.hasOwnProperty("display-name")) { - return chatMessage.tags["display-name"]; - } - return chatMessage.sender; -} - -function createMessageSenderElement(chatMessageData) { - const chatMessage = chatMessageData.message; - const senderNameElement = document.createElement("div"); - senderNameElement.classList.add("chat-replay-message-sender"); - if (chatMessage.tags.hasOwnProperty("color")) { - senderNameElement.style.color = chatMessage.tags.color; - } - senderNameElement.innerText = getMessageDisplayName(chatMessageData); - return senderNameElement; -} - -function addChatMessageTextToElement(chatMessageData, messageTextElement) { - const chatMessage = chatMessageData.message; - - let chatMessageText = chatMessage.params[1]; - if (chatMessageText.startsWith("\u0001ACTION")) { - messageTextElement.classList.add("chat-replay-message-text-action"); - const substringEnd = chatMessageText.endsWith("\u0001") - ? chatMessageText.length - 1 - : chatMessageText.length; - chatMessageText = chatMessageText.substring(7, substringEnd); - } - - if (chatMessage.tags.emotes) { - const emoteDataStrings = chatMessage.tags.emotes.split("/"); - let emotePositions = []; - for (const emoteDataString of emoteDataStrings) { - const emoteData = emoteDataString.split(":", 2); - const emoteID = emoteData[0]; - const emotePositionList = emoteData[1].split(",").map((val) => { - const positions = val.split("-"); - return { emote: emoteID, start: +positions[0], end: +positions[1] }; - }); - emotePositions = emotePositions.concat(emotePositionList); - } - emotePositions.sort((a, b) => a.start - b.start); - - let messageText = [chatMessageText]; - while (emotePositions.length > 0) { - const emoteData = emotePositions.pop(); // Pop the highest-index element from the array - let text = messageText.shift(); - const textAndEmote = [text.substring(0, emoteData.start)]; - - const emoteImg = document.createElement("img"); - emoteImg.src = `https://static-cdn.jtvnw.net/emoticons/v2/${emoteData.emote}/default/dark/1.0`; - const emoteText = text.substring(emoteData.start, emoteData.end + 1); - emoteImg.alt = emoteText; - emoteImg.title = emoteText; - const emoteContainer = document.createElement("span"); - emoteContainer.classList.add("chat-replay-message-emote"); - emoteContainer.appendChild(emoteImg); - textAndEmote.push(emoteContainer); - - const remainingText = text.substring(emoteData.end + 1); - if (remainingText !== "") { - textAndEmote.push(remainingText); - } - messageText = textAndEmote.concat(messageText); - } - - for (const messagePart of messageText) { - if (typeof messagePart === "string") { - const node = document.createTextNode(messagePart); - messageTextElement.appendChild(node); - } else { - messageTextElement.appendChild(messagePart); - } - } - } else { - messageTextElement.appendChild(document.createTextNode(chatMessageText)); - } -} diff --git a/thrimbletrimmer/scripts/edit.js b/thrimbletrimmer/scripts/edit.js deleted file mode 100644 index 5e976cf..0000000 --- a/thrimbletrimmer/scripts/edit.js +++ /dev/null @@ -1,2406 +0,0 @@ -var googleUser = null; -var videoInfo; -var currentRange = 1; -let knownTransitions = []; -let thumbnailTemplates = {}; -let globalPageState = 0; - -const CHAPTER_MARKER_DELIMITER = "\n==========\n"; -const CHAPTER_MARKER_DELIMITER_PARTIAL = "=========="; - -const PAGE_STATE = { - CLEAN: 0, - DIRTY: 1, - SUBMITTING: 2, - CONFIRMING: 3, -}; - -// References to Jcrop "stages" for the advanced thumbnail editor crop tool -let videoFrameStage; -let templateStage; - -window.addEventListener("DOMContentLoaded", async (event) => { - commonPageSetup(); - globalLoadChatWorker.onmessage = (event) => { - updateChatDataFromWorkerResponse(event.data); - renderChatLog(); - }; - window.addEventListener("beforeunload", handleLeavePage); - - const timeUpdateForm = document.getElementById("stream-time-settings"); - timeUpdateForm.addEventListener("submit", async (event) => { - event.preventDefault(); - - if (!videoInfo) { - addError( - "Time updates are ignored before the video metadata has been retrieved from Wubloader.", - ); - return; - } - - const newStartField = document.getElementById("stream-time-setting-start"); - const newStart = dateTimeFromBusTime(newStartField.value); - if (!newStart) { - addError("Failed to parse start time"); - return; - } - - const newEndField = document.getElementById("stream-time-setting-end"); - let newEnd = null; - if (newEndField.value !== "") { - newEnd = dateTimeFromBusTime(newEndField.value); - if (!newEnd) { - addError("Failed to parse end time"); - return; - } - } - - const oldStart = getStartTime(); - const startAdjustment = newStart.diff(oldStart).as("seconds"); - let newDuration = newEnd === null ? Infinity : newEnd.diff(newStart).as("seconds"); - - // The video duration isn't precisely the video times, but can be padded by up to the - // segment length on either side. - const segmentList = getSegmentList(); - newDuration += segmentList[0].duration; - newDuration += segmentList[segmentList.length - 1].duration; - - // Abort for ranges that exceed new times - const rangeDefinitionsElements = document.getElementById("range-definitions").children; - for (const rangeContainer of rangeDefinitionsElements) { - const rangeStartField = rangeContainer.getElementsByClassName("range-definition-start")[0]; - const rangeEndField = rangeContainer.getElementsByClassName("range-definition-end")[0]; - const rangeStart = videoPlayerTimeFromVideoHumanTime(rangeStartField.value); - const rangeEnd = videoPlayerTimeFromVideoHumanTime(rangeEndField.value); - - if (rangeStart !== null && rangeStart < startAdjustment) { - addError("The specified video load time excludes part of an edited clip range."); - return; - } - if (rangeEnd !== null && rangeEnd + startAdjustment > newDuration) { - addError("The specified video load time excludes part of an edited clip range."); - return; - } - } - - const rangesData = []; - for (const rangeContainer of rangeDefinitionsElements) { - const rangeStartField = rangeContainer.getElementsByClassName("range-definition-start")[0]; - const rangeEndField = rangeContainer.getElementsByClassName("range-definition-end")[0]; - - const rangeStartTimeString = rangeStartField.value; - const rangeEndTimeString = rangeEndField.value; - - const rangeStartTime = dateTimeFromVideoHumanTime(rangeStartTimeString); - const rangeEndTime = dateTimeFromVideoHumanTime(rangeEndTimeString); - - rangesData.push({ start: rangeStartTime, end: rangeEndTime }); - } - - const videoElement = document.getElementById("video"); - const currentVideoPosition = dateTimeFromVideoPlayerTime(videoElement.currentTime); - - globalStartTimeString = wubloaderTimeFromDateTime(newStart); - globalEndTimeString = wubloaderTimeFromDateTime(newEnd); - - updateSegmentPlaylist(); - - globalPlayer.once(Hls.Events.LEVEL_LOADED, (_data) => { - const newVideoPosition = videoPlayerTimeFromDateTime(currentVideoPosition); - if (newVideoPosition !== null) { - videoElement.currentTime = newVideoPosition; - } - - let rangeErrorCount = 0; - for (const [rangeIndex, rangeData] of rangesData.entries()) { - const rangeContainer = rangeDefinitionsElements[rangeIndex]; - const rangeStartField = rangeContainer.getElementsByClassName("range-definition-start")[0]; - const rangeEndField = rangeContainer.getElementsByClassName("range-definition-end")[0]; - - if (rangeData.start) { - rangeStartField.value = videoHumanTimeFromDateTime(rangeData.start); - } else { - rangeErrorCount++; - } - - if (rangeData.end) { - rangeEndField.value = videoHumanTimeFromDateTime(rangeData.end); - } else { - rangeErrorCount++; - } - } - if (rangeErrorCount > 0) { - addError( - "Some ranges couldn't be updated for the new video time endpoints. Please verify the time range values.", - ); - } - - rangeDataUpdated(); - }); - - const waveformImage = document.getElementById("waveform"); - if (newEnd === null) { - waveformImage.classList.add("hidden"); - } else { - updateWaveform(); - waveformImage.classList.remove("hidden"); - } - }); - - loadTransitions(); // Intentionally not awaiting, fire and forget - await loadVideoInfo(); - - document.getElementById("stream-time-setting-start-pad").addEventListener("click", (_event) => { - const startTimeField = document.getElementById("stream-time-setting-start"); - let startTime = startTimeField.value; - startTime = dateTimeFromBusTime(startTime); - startTime = startTime.minus({ minutes: 1 }); - startTimeField.value = busTimeFromDateTime(startTime); - }); - - document.getElementById("stream-time-setting-end-pad").addEventListener("click", (_event) => { - const endTimeField = document.getElementById("stream-time-setting-end"); - let endTime = endTimeField.value; - endTime = dateTimeFromBusTime(endTime); - endTime = endTime.plus({ minutes: 1 }); - endTimeField.value = busTimeFromDateTime(endTime); - }); - - const addRangeIcon = document.getElementById("add-range-definition"); - if (canEditVideo()) { - addRangeIcon.addEventListener("click", (_event) => { - addRangeDefinition(); - handleFieldChange(event); - }); - addRangeIcon.addEventListener("keypress", (event) => { - if (event.key === "Enter") { - addRangeDefinition(); - handleFieldChange(event); - } - }); - } else { - addRangeIcon.classList.add("hidden"); - } - - const enableChaptersElem = document.getElementById("enable-chapter-markers"); - enableChaptersElem.addEventListener("change", (event) => { - changeEnableChaptersHandler(); - handleFieldChange(event); - }); - - if (canEditVideo()) { - for (const rangeStartSet of document.getElementsByClassName("range-definition-set-start")) { - rangeStartSet.addEventListener("click", getRangeSetClickHandler("start")); - } - for (const rangeEndSet of document.getElementsByClassName("range-definition-set-end")) { - rangeEndSet.addEventListener("click", getRangeSetClickHandler("end")); - } - } - for (const rangeStartPlay of document.getElementsByClassName("range-definition-play-start")) { - rangeStartPlay.addEventListener("click", rangePlayFromStartHandler); - } - for (const rangeEndPlay of document.getElementsByClassName("range-definition-play-end")) { - rangeEndPlay.addEventListener("click", rangePlayFromEndHandler); - } - for (const rangeStart of document.getElementsByClassName("range-definition-start")) { - rangeStart.addEventListener("change", (event) => { - rangeDataUpdated(); - handleFieldChange(event); - }); - } - for (const rangeEnd of document.getElementsByClassName("range-definition-end")) { - rangeEnd.addEventListener("change", (event) => { - rangeDataUpdated(); - handleFieldChange(event); - }); - } - if (canEditMetadata()) { - for (const addChapterMarker of document.getElementsByClassName( - "add-range-definition-chapter-marker", - )) { - addChapterMarker.addEventListener("click", addChapterMarkerHandler); - } - } - - document - .getElementById("range-definition-chapter-marker-first-description") - .addEventListener("input", (event) => { - validateChapterDescription(event.target); - }); - document.getElementById("video-info-title").addEventListener("input", (event) => { - validateVideoTitle(); - document.getElementById("video-info-title-abbreviated").innerText = - videoInfo.title_prefix + document.getElementById("video-info-title").value; - handleFieldChange(event); - }); - document.getElementById("video-info-description").addEventListener("input", (event) => { - validateVideoDescription(); - handleFieldChange(event); - }); - document - .getElementById("video-info-thumbnail-template") - .addEventListener("change", handleFieldChange); - document - .getElementById("video-info-thumbnail-mode") - .addEventListener("change", updateThumbnailInputState); - document - .getElementById("video-info-thumbnail-time") - .addEventListener("change", handleFieldChange); - - if (canEditMetadata()) { - document.getElementById("video-info-thumbnail-time-set").addEventListener("click", (_event) => { - const field = document.getElementById("video-info-thumbnail-time"); - const videoPlayer = document.getElementById("video"); - const videoPlayerTime = videoPlayer.currentTime; - field.value = videoHumanTimeFromVideoPlayerTime(videoPlayerTime); - }); - document - .getElementById("video-info-thumbnail-time-play") - .addEventListener("click", (_event) => { - const field = document.getElementById("video-info-thumbnail-time"); - const thumbnailTime = videoPlayerTimeFromVideoHumanTime(field.value); - if (thumbnailTime === null) { - addError("Couldn't play from thumbnail frame; failed to parse time"); - return; - } - const videoPlayer = document.getElementById("video"); - videoPlayer.currentTime = thumbnailTime; - }); - } - - document - .getElementById("video-info-thumbnail-template-source-image-update") - .addEventListener("click", async (_event) => { - const videoFrameImageElement = document.getElementById( - "video-info-thumbnail-template-video-source-image", - ); - - const timeEntryElement = document.getElementById("video-info-thumbnail-time"); - const imageTime = wubloaderTimeFromVideoHumanTime(timeEntryElement.value); - if (imageTime === null) { - videoFrameImageElement.classList.add("hidden"); - addError("Couldn't preview thumbnail; couldn't parse thumbnail frame timestamp"); - return; - } - const videoFrameQuery = new URLSearchParams({ - timestamp: imageTime, - }); - videoFrameImageElement.src = `/frame/${globalStreamName}/source.png?${videoFrameQuery}`; - videoFrameImageElement.classList.remove("hidden"); - - const templateImageElement = document.getElementById( - "video-info-thumbnail-template-overlay-image", - ); - - const thumbnailMode = document.getElementById("video-info-thumbnail-mode").value; - if (thumbnailMode === "TEMPLATE") { - const imageTemplate = document.getElementById("video-info-thumbnail-template").value; - templateImageElement.src = `/thrimshim/template/${imageTemplate}.png`; - } else if (thumbnailMode === "ONEOFF") { - const templateData = await uploadedImageToBase64(); - templateImageElement.src = `data:image/png;base64,${templateData}`; - } else { - console.log(`WARNING: Source images updated but thumbnailMode = ${thumbnailMode}`); - } - templateImageElement.classList.remove("hidden"); - - const aspectRatioControls = document.getElementById( - "video-info-thumbnail-aspect-ratio-controls", - ); - aspectRatioControls.classList.remove("hidden"); - - createTemplateCropWidgets(); - }); - - document - .getElementById("video-info-thumbnail-crop-0") - .addEventListener("input", updateTemplateCropWidgets); - document - .getElementById("video-info-thumbnail-crop-1") - .addEventListener("input", updateTemplateCropWidgets); - document - .getElementById("video-info-thumbnail-crop-2") - .addEventListener("input", updateTemplateCropWidgets); - document - .getElementById("video-info-thumbnail-crop-3") - .addEventListener("input", updateTemplateCropWidgets); - document - .getElementById("video-info-thumbnail-location-0") - .addEventListener("input", updateTemplateCropWidgets); - document - .getElementById("video-info-thumbnail-location-1") - .addEventListener("input", updateTemplateCropWidgets); - document - .getElementById("video-info-thumbnail-location-2") - .addEventListener("input", updateTemplateCropWidgets); - document - .getElementById("video-info-thumbnail-location-3") - .addEventListener("input", updateTemplateCropWidgets); - - document - .getElementById("video-info-thumbnail-lock-aspect-ratio") - .addEventListener("change", updateTemplateCropAspectRatio); - - document - .getElementById("video-info-thumbnail-aspect-ratio-match-right") - .addEventListener("click", function () { - // Calculate and copy the aspect ratio from the video field to the template - const videoFieldX1 = document.getElementById("video-info-thumbnail-crop-0"); - const videoFieldY1 = document.getElementById("video-info-thumbnail-crop-1"); - const videoFieldX2 = document.getElementById("video-info-thumbnail-crop-2"); - const videoFieldY2 = document.getElementById("video-info-thumbnail-crop-3"); - const videoFieldAspectRatio = - (videoFieldX2.value - videoFieldX1.value) / (videoFieldY2.value - videoFieldY1.value); - - templateStage.setOptions({ aspectRatio: videoFieldAspectRatio }); - - // Re-apply the locked/unlocked status - updateTemplateCropAspectRatio(); - }); - - document - .getElementById("video-info-thumbnail-aspect-ratio-match-left") - .addEventListener("click", function () { - // Calculate and copy the aspect ratio from the template to the video field - const templateFieldX1 = document.getElementById("video-info-thumbnail-location-0"); - const templateFieldY1 = document.getElementById("video-info-thumbnail-location-1"); - const templateFieldX2 = document.getElementById("video-info-thumbnail-location-2"); - const templateFieldY2 = document.getElementById("video-info-thumbnail-location-3"); - const templateFieldAspectRatio = - (templateFieldX2.value - templateFieldX1.value) / - (templateFieldY2.value - templateFieldY1.value); - - videoFrameStage.setOptions({ aspectRatio: templateFieldAspectRatio }); - - // Re-apply the locked/unlocked status - updateTemplateCropAspectRatio(); - }); - - document - .getElementById("video-info-thumbnail-template-preview-generate") - .addEventListener("click", async (_event) => { - const imageElement = document.getElementById("video-info-thumbnail-template-preview-image"); - const thumbnailMode = document.getElementById("video-info-thumbnail-mode").value; - - if (thumbnailMode === "ONEOFF") { - try { - const data = await renderThumbnail(); - imageElement.src = `data:image/png;base64,${data}`; - } catch (e) { - imageElement.classList.add("hidden"); - addError(`${e}`); - return; - } - } else { - const timeEntryElement = document.getElementById("video-info-thumbnail-time"); - const imageTime = wubloaderTimeFromVideoHumanTime(timeEntryElement.value); - if (imageTime === null) { - imageElement.classList.add("hidden"); - addError("Couldn't preview thumbnail; couldn't parse thumbnail frame timestamp"); - return; - } - const imageTemplate = document.getElementById("video-info-thumbnail-template").value; - const [crop, loc] = getTemplatePosition(); - const query = new URLSearchParams({ - timestamp: imageTime, - template: imageTemplate, - crop: crop.join(","), - location: loc.join(","), - }); - imageElement.src = `/thumbnail/${globalStreamName}/source.png?${query}`; - } - imageElement.classList.remove("hidden"); - }); - - const thumbnailTemplateSelection = document.getElementById("video-info-thumbnail-template"); - const thumbnailTemplatesListResponse = await fetch("/thrimshim/templates"); - if (thumbnailTemplatesListResponse.ok) { - const thumbnailTemplatesList = await thumbnailTemplatesListResponse.json(); - const templateNames = thumbnailTemplatesList.map((t) => t.name); - templateNames.sort(); - for (const template of thumbnailTemplatesList) { - thumbnailTemplates[template.name] = template; - } - for (const templateName of templateNames) { - const templateOption = document.createElement("option"); - templateOption.innerText = templateName; - templateOption.value = templateName; - templateOption.title = thumbnailTemplates[templateName].description; - if (templateName === videoInfo.thumbnail_template) { - templateOption.selected = true; - } - thumbnailTemplateSelection.appendChild(templateOption); - } - setDefaultCrop(false); - } else { - addError("Failed to load thumbnail templates list"); - } - if (videoInfo.thumbnail_crop !== null) { - for (let i = 0; i < 4; i++) { - document.getElementById(`video-info-thumbnail-crop-${i}`).value = videoInfo.thumbnail_crop[i]; - } - } - if (videoInfo.thumbnail_location !== null) { - for (let i = 0; i < 4; i++) { - document.getElementById(`video-info-thumbnail-location-${i}`).value = - videoInfo.thumbnail_location[i]; - } - } - document.getElementById("video-info-thumbnail-mode").value = videoInfo.thumbnail_mode; - updateThumbnailInputState(); - // Ensure that changing values on load doesn't set keep the page dirty. - globalPageState = PAGE_STATE.CLEAN; - - document.getElementById("video-info-thumbnail-template-default-crop").addEventListener("click", (_event) => { - setDefaultCrop(true); - }); - - document.getElementById("submit-button").addEventListener("click", (_event) => { - submitVideo(); - }); - document.getElementById("save-button").addEventListener("click", (_event) => { - saveVideoDraft(); - }); - document.getElementById("submit-changes-button").addEventListener("click", (_event) => { - submitVideoChanges(); - }); - - document.getElementById("advanced-submission").addEventListener("click", (_event) => { - const advancedOptionsContainer = document.getElementById("advanced-submission-options"); - advancedOptionsContainer.classList.toggle("hidden"); - }); - - document - .getElementById("advanced-submission-option-allow-holes") - .addEventListener("change", () => { - updateDownloadLink(); - }); - document.getElementById("download-type-select").addEventListener("change", () => { - updateDownloadLink(); - }); - - document.getElementById("download-frame").addEventListener("click", (_event) => { - downloadFrame(); - }); - - document.getElementById("manual-link-update").addEventListener("click", (_event) => { - const manualLinkDataContainer = document.getElementById("data-correction-manual-link"); - manualLinkDataContainer.classList.toggle("hidden"); - }); - document - .getElementById("data-correction-manual-link-submit") - .addEventListener("click", (_event) => { - setManualVideoLink(); - }); - - document.getElementById("cancel-video-upload").addEventListener("click", (_event) => { - cancelVideoUpload(); - }); - - document.getElementById("reset-entire-video").addEventListener("click", (_event) => { - const forceResetConfirmationContainer = document.getElementById( - "data-correction-force-reset-confirm", - ); - forceResetConfirmationContainer.classList.remove("hidden"); - }); - document.getElementById("data-correction-force-reset-yes").addEventListener("click", (_event) => { - resetVideoRow(); - }); - document.getElementById("data-correction-force-reset-no").addEventListener("click", (_event) => { - const forceResetConfirmationContainer = document.getElementById( - "data-correction-force-reset-confirm", - ); - forceResetConfirmationContainer.classList.add("hidden"); - }); - - document.getElementById("google-auth-sign-out").addEventListener("click", (_event) => { - googleSignOut(); - }); -}); - -async function loadTransitions() { - const response = await fetch("/thrimshim/transitions"); - if (!response.ok) { - addError( - "Failed to fetch possible transition types. This probably means the wubloader host is down.", - ); - return; - } - knownTransitions = await response.json(); - updateTransitionTypes(); -} - -// Update the given list of transition type setDelay(+event.currentTarget.value)} + /> + seconds of delay + + + ); +}; + +export default Clock; diff --git a/thrimbletrimmer/src/utilities/Utilities.tsx b/thrimbletrimmer/src/utilities/Utilities.tsx new file mode 100644 index 0000000..cff6175 --- /dev/null +++ b/thrimbletrimmer/src/utilities/Utilities.tsx @@ -0,0 +1,12 @@ +import { Component } from "solid-js"; +import Clock from "./Clock"; + +const Utilities: Component = () => { + return ( + <> + + + ); +}; + +export default Utilities; diff --git a/thrimbletrimmer/src/utils.tsx b/thrimbletrimmer/src/utils.tsx new file mode 100644 index 0000000..6ed0dda --- /dev/null +++ b/thrimbletrimmer/src/utils.tsx @@ -0,0 +1,7 @@ +import { render } from "solid-js/web"; + +import Utilities from "./utilities/Utilities"; + +const root = document.getElementById("root"); + +render(() => , root!); diff --git a/thrimbletrimmer/styles/jcrop.css b/thrimbletrimmer/styles/jcrop.css deleted file mode 100644 index 7bc80d2..0000000 --- a/thrimbletrimmer/styles/jcrop.css +++ /dev/null @@ -1,173 +0,0 @@ -.jcrop-widget .jcrop-handle { - display: none; - position: absolute; - border: 1px rgba(127, 127, 127, 0.8) solid; - width: 10px; - height: 10px; - box-sizing: border-box; - background: rgba(255, 255, 255, 0.8) -} - -.jcrop-widget .jcrop-handle.nw { - top: -3px; - left: -3px; - cursor: nwse-resize -} - -.jcrop-widget .jcrop-handle.w { - top: 50%; - transform: translateY(-50%); - left: -3px; - cursor: ew-resize -} - -.jcrop-widget .jcrop-handle.sw { - bottom: -3px; - left: -3px; - cursor: nesw-resize -} - -.jcrop-widget .jcrop-handle.ne { - top: -3px; - right: -3px; - cursor: nesw-resize -} - -.jcrop-widget .jcrop-handle.e { - top: 50%; - transform: translateY(-50%); - right: -3px; - cursor: ew-resize -} - -.jcrop-widget .jcrop-handle.se { - bottom: -3px; - right: -3px; - cursor: nwse-resize -} - -.jcrop-widget .jcrop-handle.n { - left: 50%; - transform: translateX(-50%); - top: -3px; - cursor: ns-resize -} - -.jcrop-widget .jcrop-handle.s { - left: 50%; - transform: translateX(-50%); - bottom: -3px; - cursor: ns-resize -} - -.jcrop-widget.active .jcrop-handle { - display: block -} - -.jcrop-widget { - position: absolute; - box-sizing: border-box; - border: 1px white dashed; - opacity: 0.7; - background: transparent; - transition: opacity 1s; - padding: 0; - margin: 0; - cursor: move -} - -.jcrop-widget:hover { - transition: opacity 0.8s; - opacity: 0.8 -} - -.jcrop-shade { - background: rgba(0, 0, 0, 0.5); - transition: opacity 0.4s, background-color 0.7s; - position: absolute -} - -.jcrop-shade.l { - top: 0px; - left: 0px; - height: 100% -} - -.jcrop-shade.r { - top: 0px; - right: 0px; - height: 100% -} - -.jcrop-shade.t { - top: 0px -} - -.jcrop-shade.b { - bottom: 0px -} - -.jcrop-stage { - position: relative; - width: 100% -} - -.jcrop-image-stage img { - position: absolute; - z-index: -1 -} - -.jcrop-ux-inactive-handles .jcrop-widget .jcrop-handle { - display: block -} - -.jcrop-widget img { - width: 100%; - height: auto -} - -.jcrop-ux-fade-more .jcrop-widget { - opacity: 0.25 -} - -.jcrop-ux-fade-more .jcrop-widget:hover { - transition: opacity 0.4s; - opacity: 0.8 -} - -.jcrop-ux-fade-more .jcrop-widget:focus { - transition: opacity 0.5s; - opacity: 1; - outline-style: auto; - outline-width: 3px; - outline-color: rgba(0, 0, 0, 0.3) -} - -.jcrop-ux-fade-more .jcrop-widget { - opacity: 0.25 -} - -.jcrop-ux-fade-more .jcrop-widget:hover { - opacity: 0.65 -} - -.jcrop-ux-keep-current .jcrop-widget.active { - opacity: 1; - outline-style: auto; - outline-width: 3px; - outline-color: rgba(0, 0, 0, 0.3) -} - -.jcrop-ux-no-outline .jcrop-widget { - outline: none !important -} - -.jcrop-disable.jcrop-stage { - opacity: .8 -} - -.jcrop-disable.jcrop-stage .jcrop-widget { - outline: none !important -} - -/*# sourceMappingURL=jcrop.css.map */ diff --git a/thrimbletrimmer/styles/thrimbletrimmer.css b/thrimbletrimmer/styles/thrimbletrimmer.css deleted file mode 100644 index 005381a..0000000 --- a/thrimbletrimmer/styles/thrimbletrimmer.css +++ /dev/null @@ -1,496 +0,0 @@ -body { - /* Firefox has a weird default font, which is a different size from the one in Chrome - * and makes some renderings bad. - */ - font-family: "Arial", sans-serif; - - background: #222; - color: #fff; - height: 100vh; - margin: 0; -} - -a { - color: #ccf; -} - -input, -textarea { - background: #222; - color: #fff; - border-color: #444; -} - -textarea { - /* This will look better if it's consistent with input fields */ - border-style: inset; - border-width: 2px; -} - -button, -select { - background: #333; - color: #fff; -} - -button:active { - background: #000; -} - -a, -.click { - cursor: pointer; -} - -a.click { - text-decoration: underline; -} - -.input-error { - border-color: #b00; -} - -.input-error:focus { - outline: #d00 solid 1px; -} - -#errors { - color: #f33; - display: flex; - flex-direction: column; -} - -#errors > div { - border-bottom: 1px solid #f33; - background: #300; - padding: 4px; -} - -.error-dismiss { - float: right; -} - -#page-container { - position: relative; - max-height: 100vh; - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: stretch; -} - -#page-container > * { - flex: 0 1 auto; -} - -#editor-help { - position: absolute; - top: 0; - right: 0; - background: #222; - padding: 5px; -} - -#stream-time-settings { - display: flex; - align-items: flex-end; - gap: 5px; - margin-bottom: 10px; - margin-top: 5px; -} - -#stream-time-settings > div { - margin: 0 2px; -} - -.field-label { - display: block; -} - -#video { - width: 100%; - max-height: 50vh; -} - -#video-controls { - font-size: 110%; -} - -#video-controls select { - appearance: none; - font-size: inherit; - background: inherit; - border: none; - text-align: center; -} - -#video-controls option { - background: #222; - padding: 2px; -} - -#video-controls-bar { - display: flex; - align-items: center; - gap: 8px; -} - -#video-controls-spacer { - flex-grow: 1; -} - -#video-controls-volume { - display: flex; - align-items: center; - gap: 2px; -} - -#video-controls-volume-level { - width: 100px; - height: 8px; - border-radius: 0; - border: 0; -} - -#video-controls-playback-position { - height: 10px; - border-radius: 0; - border: 0; -} - -/* For some reason, there's not a cross-browser way to style elements. - * This should be replaced with a cross-browser way of doing this when possible. - * I only implemented WebKit/Blink and Firefox here because if you still use IE, - * I quite frankly don't care about you. - */ - -/* WEBKIT/BLINK SECTION */ -#video-controls-volume-level::-webkit-progress-bar { - background: #ffffff30; -} - -#video-controls-volume-level::-webkit-progress-value { - background: #ffffffc0; -} - -#video-controls-playback-position::-webkit-progress-bar { - background: #ffffff30; -} - -#video-controls-playback-position::-webkit-progress-value { - background: #ffffffc0; -} -/* END WEBKIT/BLINK */ - -/* FIREFOX SECTION */ -#video-controls-volume-level { - background: #ffffff30; -} - -#video-controls-volume-level::-moz-progress-bar { - background: #ffffffc0; -} - -#video-controls-playback-position { - background: #ffffff30; -} - -#video-controls-playback-position::-moz-progress-bar { - background: #ffffffc0; -} -/* END FIREFOX */ - -#video-controls-playback-position { - width: 100%; -} - -#clip-bar { - width: 100%; - min-height: 7px; - background-color: #bbb; - position: relative; -} - -#clip-bar > div { - position: absolute; - background-color: #d80; - height: 100%; -} - -#waveform-container { - position: relative; -} - -#waveform { - width: 100%; - - /* With an unbound height, the waveform can appear a bit away from the video. - * The intended effect still works if we scrunch the height a bit, so here's - * a height bound for the waveform image. - */ - max-height: 100px; - filter: invert(90%); -} - -#waveform-marker { - width: 1px; - height: 100%; - background: #dd8800a0; - position: absolute; - top: 0; -} - -#range-definitions { - display: flex; - flex-direction: column; - gap: 1px; -} - -.range-transition-duration-section { - display: inline-block; -} - -.range-transition-duration { - width: 50px; -} - -.range-definition-times { - display: flex; - align-items: center; - gap: 4px; -} - -.range-definition-start, -.range-definition-end { - width: 100px; - text-align: right; -} - -.range-definition-between-time-gap { - width: 5px; -} - -.range-definition-icon-gap { - width: 16px; -} - -#add-range-definition { - margin-top: 2px; -} - -.range-definition-chapter-markers > div { - display: flex; - align-items: center; - gap: 10px; - margin-left: 30px; -} - -.range-definition-chapter-marker-start-field { - display: flex; - align-items: center; - gap: 4px; -} - -.range-definition-chapter-marker-start { - width: 100px; - text-align: right; -} - -.range-definition-chapter-marker-edit-gap { - width: 16px; -} - -input.range-definition-chapter-marker-description { - width: 500px; -} - -.add-range-definition-chapter-marker { - margin-left: 30px; - margin-bottom: 7px; -} - -#video-info { - margin: 5px 0; - display: grid; - grid-template-columns: 200px 1fr; - grid-template-rows: minmax(min-content, max-content) 1.25em 3em minmax(4em, max-content) 1.25em; - gap: 2px; -} - -#video-info-editor-notes-container { - border: 1px solid #666; - background-color: #125; - grid-column-end: span 2; -} - -/* In order to maintain the grid dimensions, when we hide the editors notes (for there not being them), - * they still need to take up a grid slot. As such, we replace `display: none` in this context with - * an effective equivalent that doesn't remove its rendering entirely. - */ -#video-info-editor-notes-container.hidden { - display: block; - visibility: hidden; - height: 0; -} - -#video-info-title-full { - display: flex; - align-items: center; - white-space: pre; -} - -#video-info-title { - flex-grow: 1; -} - -#video-info-title-abbreviated { - width: 200px; - overflow: hidden; - font-size: 1em; - line-height: 1em; - height: 2em; - text-overflow: ellipsis; - - /* For some reason, all this Webkit-specific-looking stuff is required to show ellipses - on wrapped text. - It also somehow works on Firefox. */ - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; -} - -.video-info-thumbnail-mode-options { - margin: 2px 0; -} - -.video-info-thumbnail-position { - width: 50px; -} - -#video-info-thumbnail-template-preview-image { - max-width: 320px; -} - -#video-info-thumbnail-template-video-source-image { - max-width: 640px; -} - -#video-info-thumbnail-template-overlay-image { - max-width: 640px; -} - -.video-info-thumbnail-advanced-crop-flex-wrapper { - display: flex; - align-items: center; -} - -.video-info-thumbnail-advanced-crop-flex-column { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -.video-info-thumbnail-advanced-crop-flex-column div { - margin: 0.5em; -} - -.submission-response-error { - white-space: pre-wrap; -} - -.hidden { - display: none; -} - -#submission { - margin: 5px 0; -} - -#download { - margin: 5px 0; -} - -#data-correction { - margin: 5px 0; -} - -#data-correction-force-reset-confirm p { - margin: 5px 0; -} - -.submission-response-pending { - color: #cc0; -} - -.submission-response-error { - color: #c00; -} - -.submission-response-success { - color: #0c0; -} - -.time-converter-time { - display: block; - width: 200px; -} - -#chat-replay { - overflow-y: auto; - min-height: 250px; -} - -.chat-replay-message { - display: flex; - align-items: baseline; - gap: 10px; -} - -.chat-replay-message-time { - flex-basis: 110px; - color: #ccc; - text-align: right; -} - -.chat-replay-message-text { - flex-basis: 200px; - flex-grow: 1; -} - -.chat-replay-message-text-action { - font-style: italic; -} - -.chat-replay-message-system { - color: #aaf; -} - -.chat-replay-message-text-action .chat-replay-message-reply:not(.chat-replay-message-text-action) { - font-style: normal; /* Clear the italics from the action */ -} - -.chat-replay-message-emote { - /* - This size is set based on Twitch's 1.0 emote size. - This will need to be updated if that changes. (Otherwise, auto-scrolling will break.) - */ - width: 28px; - height: 28px; -} - -.chat-replay-message-reply { - font-size: 80%; -} - -.chat-replay-message-reply a { - text-decoration: none; -} - -.chat-replay-message-cleared { - opacity: 0.5; -} - -.chat-replay-message-cleared .chat-replay-message-text { - text-decoration: line-through; -} diff --git a/thrimbletrimmer/styles/thumbnails.css b/thrimbletrimmer/styles/thumbnails.css deleted file mode 100644 index c84a6fd..0000000 --- a/thrimbletrimmer/styles/thumbnails.css +++ /dev/null @@ -1,43 +0,0 @@ -.hidden { - display: none; -} - -table > form { - display: table-row; -} - -#template-list-data { - border-collapse: collapse; -} - -#template-list-data td { - border: 1px solid #000; - padding: 2px; -} - -.template-list-preview { - width: 480px; -} - -#template-new-errors, -.template-data-edit-errors { - color: #c00; -} - -#template-new-form-fields { - display: grid; - grid-template-columns: max-content max-content; - gap: 1px; -} - -#template-new-form-fields > div { - display: contents; -} - -.template-coord { - width: 50px; -} - -#google-authentication { - margin-top: 5px; -} diff --git a/thrimbletrimmer/thumbnail_manager.html b/thrimbletrimmer/thumbnail_manager.html deleted file mode 100644 index 166735c..0000000 --- a/thrimbletrimmer/thumbnail_manager.html +++ /dev/null @@ -1,155 +0,0 @@ - - - - - VST Thumbnail Template Management - - - - - - - -
-

Template List

- - - - - - - - - - -
NameDescriptionAttributionCrop CoordinatesLocation CoordinatesPreview
-
- -
-

Add New Template

-
    -
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - Crop: - - - - to - - - -
    -
    - Location: - - - - to - - - -
    -
    - - -
    -
    - - - - diff --git a/thrimbletrimmer/thumbnails.html b/thrimbletrimmer/thumbnails.html new file mode 100644 index 0000000..6960870 --- /dev/null +++ b/thrimbletrimmer/thumbnails.html @@ -0,0 +1,12 @@ + + + + + Thrimbletrimmer - Utilities + + +
    + + + + diff --git a/thrimbletrimmer/tsconfig.json b/thrimbletrimmer/tsconfig.json new file mode 100644 index 0000000..25838eb --- /dev/null +++ b/thrimbletrimmer/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["vite/client"], + "noEmit": true, + "isolatedModules": true + } +} diff --git a/thrimbletrimmer/utils.html b/thrimbletrimmer/utils.html new file mode 100644 index 0000000..321b4ce --- /dev/null +++ b/thrimbletrimmer/utils.html @@ -0,0 +1,12 @@ + + + + + Thrimbletrimmer - Utilities + + +
    + + + + diff --git a/thrimbletrimmer/vite.config.ts b/thrimbletrimmer/vite.config.ts new file mode 100644 index 0000000..92dc5b5 --- /dev/null +++ b/thrimbletrimmer/vite.config.ts @@ -0,0 +1,23 @@ +import { fileURLToPath } from "url"; +import { defineConfig } from "vite"; +import solidPlugin from "vite-plugin-solid"; +import devtools from "solid-devtools/vite"; + +export default defineConfig({ + base: "/thrimbletrimmer/", + plugins: [devtools(), solidPlugin()], + server: { + port: 3000, + }, + build: { + target: "esnext", + rollupOptions: { + input: { + index: fileURLToPath(new URL("index.html", import.meta.url)), + edit: fileURLToPath(new URL("edit.html", import.meta.url)), + utils: fileURLToPath(new URL("utils.html", import.meta.url)), + thumbnails: fileURLToPath(new URL("thumbnails.html", import.meta.url)), + }, + }, + }, +});