diff --git a/public/assets/scripts/ProgressBar.js b/public/assets/scripts/ProgressBar.js index 63ae104..b89e26a 100644 --- a/public/assets/scripts/ProgressBar.js +++ b/public/assets/scripts/ProgressBar.js @@ -4,6 +4,11 @@ export class ProgressBar { this.indicator = document.getElementById("progress-indicator"); } + setStatus(status) { + this.percentage.innerText = status; + this.indicator.style.clipPath = "rect(0 0 100% 0)"; + } + setProgress(progress) { const percent = (progress * 100).toFixed(1) + "%"; diff --git a/public/assets/scripts/VideoCompressor.js b/public/assets/scripts/VideoCompressor.js index b67b29f..5ab4df2 100644 --- a/public/assets/scripts/VideoCompressor.js +++ b/public/assets/scripts/VideoCompressor.js @@ -8,7 +8,7 @@ export class VideoCompressor { } async compress(file, filesizeBytes, duration) { - this.progressBar.setProgress(0); + this.progressBar.setStatus("Initializing..."); this.inProgress = true; const progressCallback = event => this.progressBar.setProgress(event.progress); @@ -47,13 +47,13 @@ export class VideoCompressor { } const video = await this.ffmpeg.readFile("compressed.mp4"); - const url = URL.createObjectURL(new Blob([video.buffer], { type: "video/mp4" })); + const resultFile = new File([video.buffer], this.generateFileName(file.name), { type: "video/mp4" }); // Clean up this.ffmpeg.off("progress", progressCallback); this.inProgress = false; - return url; + return resultFile; } stop() { @@ -61,6 +61,15 @@ export class VideoCompressor { this.inProgress = false; } + generateFileName(filename) { + let name = filename.replace(/\.\w+$/, ""); + + if (/\s/.test(name)) name += " compressed"; + else name += "_compressed"; + + return name + ".mp4"; + } + // https://github.com/ffmpegwasm/ffmpeg.wasm/blob/main/packages/util/src/index.ts readFromBlob(blob) { return new Promise((resolve, reject) => { diff --git a/public/assets/scripts/main.js b/public/assets/scripts/main.js index 4119a6e..d080470 100644 --- a/public/assets/scripts/main.js +++ b/public/assets/scripts/main.js @@ -8,12 +8,16 @@ const videoCompressor = new VideoCompressor(progressBar); document.getElementById("compress").onclick = async () => { document.getElementById("uploaded-video").pause(); - showSection("loading"); const filesize = document.getElementById("filesize").value; const filesizeUnit = document.getElementById("filesize-unit").value; const file = document.getElementById("file-input").files[0]; + if (!file) { + alert("Please select a file"); + return; + } + const videoLength = document.getElementById("uploaded-video").duration; let targetFilesize; @@ -22,19 +26,50 @@ document.getElementById("compress").onclick = async () => { case "mb": targetFilesize = filesize * 1000000; break; } + showSection("loading"); + + let result; try { - const url = await videoCompressor.compress(file, targetFilesize, videoLength); - - if (!url) return; - - notifier.notifyFinished(); - - location.href = url; + result = await videoCompressor.compress(file, targetFilesize, videoLength); } catch (e) { alert(e.message); showSection("file-picker"); + + return; } + + if (!result) return; + + notifier.notifyFinished(); + + const url = URL.createObjectURL(result); + + document.getElementById("compressed-video").src = url; + document.getElementById("compressed-video").load(); + + document.getElementById("result-size").innerText = bytesToSizeString(result.size); + + document.getElementById("download").onclick = () => { + const a = document.createElement("a"); + a.href = url; + a.download = result.name; + a.click(); + }; + + const shareData = { + text: "Compressed using " + window.origin, + files: [result], + }; + + if (navigator.share && (!navigator.canShare || navigator.canShare(shareData))) { + document.getElementById("share").style.display = "inline-block"; + document.getElementById("share").onclick = () => navigator.share(shareData); + } else { + document.getElementById("share").style.display = "none"; + } + + showSection("result"); }; document.getElementById("cancel").onclick = () => { @@ -67,6 +102,17 @@ document.getElementById("file-input").oninput = () => { }, 200); }; +document.getElementById("back").onclick = () => { + showSection("file-picker"); +}; + window.onbeforeunload = event => { if (videoCompressor.inProgress) event.preventDefault(); }; + +function bytesToSizeString(bytes) { + if (bytes >= 1000000000) return (bytes / 1000000000).toFixed(1) + " GB"; + if (bytes >= 1000000) return (bytes / 1000000).toFixed(1) + " MB"; + if (bytes >= 1000) return (bytes / 1000).toFixed(1) + " KB"; + return bytes + " B"; +} diff --git a/public/assets/style/main.css b/public/assets/style/main.css deleted file mode 100644 index 2799223..0000000 --- a/public/assets/style/main.css +++ /dev/null @@ -1,200 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap'); - -body { - background-color: #F0F0F0; - font-family: "Open Sans", sans-serif; - text-align: center; -} - -*::selection { - background-color: #00BCD4; - color: white; -} - -h1 { - background-image: linear-gradient(to right, #4CAF50, #4CAF50 40%, #00BCD4 60%); - background-clip: text; - color: transparent; - font-size: 3rem; - margin-top: 3rem; - margin-bottom: 1rem; -} - -p { - color: #9E9E9E; - font-size: 0.8rem; -} - -* { - opacity: 1; - transition: opacity 300ms; -} - -video { - width: 355px; - height: 200px; - border: 2px solid #212121; - background-color: black; - border-radius: 1rem; - box-shadow: 0 5px 5px rgba(0, 0, 0, 0.2); -} - -#file-input, #change-file { - display: none; - opacity: 0; -} - -#file-input-spacing * { - visibility: hidden; -} - -#file-drop-area { - max-width: 355px; - height: 198px; - border: 3px dotted #BDBDBD; - margin: 3rem auto 1rem auto; - border-radius: 1rem; - display: flex; - justify-content: center; - align-items: center; - transition: all 300ms; - cursor: pointer; -} - -#file-drop-area span { - color: #9E9E9E; -} - -#file-drop-area:hover, #file-drop-area:focus { - background-color: #E0E0E0; - outline: none; -} - -#uploaded-video { - display: none; - opacity: 0; - margin: 3rem auto 1rem auto; -} - -button.primary { - background-image: linear-gradient(to right, #4CAF50, #00BCD4); - border: none; - color: white; - padding: 0.5rem 1rem; - border-radius: 0.5rem; - box-shadow: 0 3px 3px rgba(0, 0, 0, 0.1); - font-weight: bold; - transition: all ease-in 100ms; - cursor: pointer; -} - -button.primary:hover { - filter: brightness(1.1); -} - -button.primary:active { - filter: brightness(0.8); -} - -button.primary:focus { - box-shadow: 0 4px 4px rgba(0, 0, 0, 0.2); - outline: 2px solid black; -} - -button.simple { - border: 1px solid #9E9E9E; - border-radius: 0.25rem; - background-color: transparent; - transition: all 100ms; - cursor: pointer; - color: #9E9E9E; - margin: 1rem auto; -} - -button.simple:hover { - color: black; - border-color: black; -} - -button.simple:focus { - border-color: black; - outline: none; -} - -input, select { - background-color: white; - padding: 0.5rem 1rem; - border-radius: 0.5rem; - border: 1px solid #BDBDBD; - box-shadow: 0 3px 3px rgba(0, 0, 0, 0.1); - transition: all ease-in 100ms; -} - -input:focus, select:focus { - outline: none; - border: 1px solid #00BCD4; - box-shadow: 0 4px 4px rgba(0, 0, 0, 0.2); -} - -input[type=checkbox] { - accent-color: #4CAF50; -} - -label { - color: #757575; -} - -#filesize { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} - -#filesize-unit { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - border-left: 1px solid white; -} - -#filesize-unit:focus { - border-left: 1px solid #00BCD4; -} - -#compress { - margin-left: 1rem; -} - -#loading-title { - color: #757575; - font-size: 2.5rem; - font-weight: bold; - margin-top: 4rem; - margin-bottom: 0; -} - -#loading-description { - font-size: 1rem; -} - -#progress-container { - width: 80vw; - height: 2rem; - max-width: 500px; - border-radius: 2rem; - margin: 3rem auto auto; - background-color: #E0E0E0; - position: relative; -} - -#progress-indicator { - position: absolute; - inset: 0; - background-image: linear-gradient(to right, #4CAF50, #00BCD4); - border-radius: 2rem; - transition: clip-path 200ms; -} - -#progress-percentage { - color: #757575; - font-weight: bold; - font-size: 1.2rem; -} diff --git a/public/assets/styles/file-picker-section.css b/public/assets/styles/file-picker-section.css new file mode 100644 index 0000000..f4a31a7 --- /dev/null +++ b/public/assets/styles/file-picker-section.css @@ -0,0 +1,56 @@ +#file-input, #change-file { + display: none; + opacity: 0; +} + +#file-input-spacing * { + visibility: hidden; +} + +#file-drop-area { + max-width: 355px; + height: 198px; + border: 3px dotted #BDBDBD; + margin: 3rem auto 1rem auto; + border-radius: 1rem; + display: flex; + justify-content: center; + align-items: center; + transition: all 300ms; + cursor: pointer; +} + +#file-drop-area span { + color: #9E9E9E; +} + +#file-drop-area:hover, #file-drop-area:focus { + background-color: #E0E0E0; + outline: none; +} + +#uploaded-video { + display: none; + opacity: 0; + margin: 3rem auto 1rem auto; +} + +#filesize { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + width: 80px; +} + +#filesize-unit { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-left: 1px solid white; +} + +#filesize-unit:focus { + border-left: 1px solid #00BCD4; +} + +#compress { + margin-left: 1rem; +} diff --git a/public/assets/styles/loading-section.css b/public/assets/styles/loading-section.css new file mode 100644 index 0000000..e435833 --- /dev/null +++ b/public/assets/styles/loading-section.css @@ -0,0 +1,35 @@ +#loading-title { + color: #757575; + font-size: 2.5rem; + font-weight: bold; + margin-top: 4rem; + margin-bottom: 0; +} + +#loading-description { + font-size: 1rem; +} + +#progress-container { + width: 80vw; + height: 2rem; + max-width: 500px; + border-radius: 2rem; + margin: 3rem auto auto; + background-color: #E0E0E0; + position: relative; +} + +#progress-indicator { + position: absolute; + inset: 0; + background-image: linear-gradient(to right, #4CAF50, #00BCD4); + border-radius: 2rem; + transition: clip-path 200ms; +} + +#progress-percentage { + color: #757575; + font-weight: bold; + font-size: 1.2rem; +} diff --git a/public/assets/styles/main.css b/public/assets/styles/main.css new file mode 100644 index 0000000..2d6bb60 --- /dev/null +++ b/public/assets/styles/main.css @@ -0,0 +1,41 @@ +@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap'); + +body { + background-color: #F0F0F0; + font-family: "Open Sans", sans-serif; + text-align: center; +} + +*::selection { + background-color: #00BCD4; + color: white; +} + +h1 { + background-image: linear-gradient(to right, #4CAF50, #4CAF50 40%, #00BCD4 60%); + background-clip: text; + color: transparent; + font-size: 3rem; + margin-top: 3rem; + margin-bottom: 1rem; +} + +p { + color: #9E9E9E; + font-size: 0.8rem; +} + +* { + opacity: 1; + transition: opacity 300ms; + -webkit-tap-highlight-color: transparent; +} + +video { + width: 355px; + height: 200px; + border: 2px solid #212121; + background-color: black; + border-radius: 1rem; + box-shadow: 0 5px 5px rgba(0, 0, 0, 0.2); +} diff --git a/public/assets/styles/result-section.css b/public/assets/styles/result-section.css new file mode 100644 index 0000000..4c0e55a --- /dev/null +++ b/public/assets/styles/result-section.css @@ -0,0 +1,25 @@ +#result-section { + display: flex; + gap: 1rem; +} + +#result-title { + color: #757575; + font-weight: bold; + font-size: 1.2rem; + margin-top: 2.5rem; + margin-bottom: 0; +} + +#compressed-video { + margin-bottom: 1rem; +} + +#download, #share { + width: 120px; + margin-top: 0.5rem; +} + +#back { + margin-top: 2rem; +} diff --git a/public/assets/styles/ui.css b/public/assets/styles/ui.css new file mode 100644 index 0000000..9636f6c --- /dev/null +++ b/public/assets/styles/ui.css @@ -0,0 +1,78 @@ +button { + cursor: pointer; +} + +button.primary { + background-image: linear-gradient(to right, #4CAF50, #00BCD4); + border: none; + color: white; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + box-shadow: 0 3px 3px rgba(0, 0, 0, 0.1); + font-weight: bold; + transition: all ease-in 100ms; + height: 37px; +} + +button.primary:hover { + filter: brightness(1.1); +} + +button.primary:active { + filter: brightness(0.8); +} + +button.primary:focus { + box-shadow: 0 4px 4px rgba(0, 0, 0, 0.2); + outline: 2px solid black; +} + +button.simple { + border: 1px solid #9E9E9E; + border-radius: 0.25rem; + background-color: transparent; + transition: all 100ms; + color: #9E9E9E; + margin: 1rem auto; + padding: 0.2rem 0.5rem; +} + +button.simple:hover { + color: black; + border-color: black; +} + +button.simple:focus { + border-color: black; + outline: none; +} + +input:not([type=checkbox]), select, button.secondary { + background-color: white; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + border: 1px solid #BDBDBD; + box-shadow: 0 3px 3px rgba(0, 0, 0, 0.1); + transition: all ease-in 100ms; + box-sizing: border-box; + height: 37px; +} + +input:not([type=checkbox]):focus, select:focus, button.secondary:focus { + outline: none; + border: 1px solid #00BCD4; + box-shadow: 0 4px 4px rgba(0, 0, 0, 0.2); +} + +input[type=checkbox] { + transition: all ease-in 100ms; + accent-color: #4CAF50; +} + +input[type=checkbox]:hover { + box-shadow: 0 3px 3px rgba(0, 0, 0, 0.1); +} + +label { + color: #757575; +} diff --git a/public/index.html b/public/index.html index 22207d3..651c466 100644 --- a/public/index.html +++ b/public/index.html @@ -2,11 +2,16 @@
+