From 00ecbee8b93e80e4ccb39249c33a8afebef6cdb6 Mon Sep 17 00:00:00 2001 From: James Mills Date: Fri, 27 Mar 2020 14:24:44 +1000 Subject: Added UI/UX for Importing artbitrary videos from a URL --- static/import.css | 145 +++++++++++++++++++++++++++++++++++++++ static/import.js | 199 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 344 insertions(+) create mode 100644 static/import.css create mode 100644 static/import.js (limited to 'static') diff --git a/static/import.css b/static/import.css new file mode 100644 index 0000000..a12b5b9 --- /dev/null +++ b/static/import.css @@ -0,0 +1,145 @@ +.import-container { + display: inline-block; + max-width: 100%; + margin-top: 50px; +} + +.import-wrapper { + padding: 20px; + background-color: #282a2e; + border: 1px solid #1e1e1e; + border-radius: 20px; + font-family: 'Trebuchet MS', Arial, sans-serif; +} + +.import-form { + border: 5px dashed #676867; + border-radius: 10px; + cursor: pointer; +} + +.import-box { + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + justify-content: center; + align-items: center; + min-width: 300px; + min-height: 300px; +} + +.import-details { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 0 0 20px 0; +} + +.import-details > * { + margin-top: 20px; +} + +.import-details span { + color: #c5c8c6; + font-weight: bold; +} + +.import-message { + white-space: normal; + padding: 0 20px; + word-wrap: break-word; + max-width: 300px; +} + +.import-message.error { + color: #e82e57; +} + +.import-button-wrapper { + display: none; + width: 100%; + padding: 0 20px; +} + +.import-button { + border: 0; + box-shadow: none; + border-radius: 0px; + + width: 100%; + padding: 15px; + background: #191919 !important; + font-family: 'Trebuchet MS', Arial, sans-serif; + color: #c5c8c6; + border-radius: 10px; + cursor: pointer; +} + +.import-button.transparent { + background: transparent !important; + padding: 10px; +} + +.import-progress-container { + display: none; + width: 100%; + padding: 0 20px; +} + +.import-progress { + height: 30px; + background: linear-gradient(45deg, #c7c7c7, #ae81ff); + border-radius: 5px; + display: flex; + justify-content: center; + align-items: center; +} + +.import-progress-label { + color: #282a2e !important; +} + +/* SPINNER */ + +.loader, +.loader:after { + border-radius: 50%; + width: 3em; + height: 3em; +} +.loader { + font-size: 10px; + position: relative; + text-indent: -9999em; + border-top: 0.5em solid rgba(255, 255, 255, 0.2); + border-right: 0.5em solid rgba(255, 255, 255, 0.2); + border-bottom: 0.5em solid rgba(255, 255, 255, 0.2); + border-left: 0.5em solid #ffffff; + -webkit-transform: translateZ(0); + -ms-transform: translateZ(0); + transform: translateZ(0); + -webkit-animation: load8 1.1s infinite linear; + animation: load8 1.1s infinite linear; +} +@-webkit-keyframes load8 { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@keyframes load8 { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} diff --git a/static/import.js b/static/import.js new file mode 100644 index 0000000..9f1eff6 --- /dev/null +++ b/static/import.js @@ -0,0 +1,199 @@ +// common variables +let iBytesImported = 0 +let iBytesTotal = 0 +let iPreviousBytesLoaded = 0 +let iMaxFilesize = 104857600 // 100MB +let timer = 0 +let importInProgress = 'n/a' +let isProcessing = false +let url = null + +/* CACHED ELEMENTS */ + +const importForm = document.getElementById('import-form') +const importInput = document.getElementById('import-input') +const importMessageLabel = document.getElementById('import-message') +const importButtonWrapper = document.getElementById('import-button-wrapper') +const importButton = document.getElementById('import-button') +const importProgressContainer = document.getElementById('import-progress-container') +const importProgressBar = document.getElementById('import-progress') +const importProgressLabel = document.getElementById('import-progress-label') +const importStopped = document.getElementById('import-stopped') +const importStarted = document.getElementById('import-started') + +/* HELPERS */ + +const setProgress = (_progress) => { + importProgressContainer.style.display = _progress > 0 ? 'flex' : 'none' + importProgressBar.style.width = `${_progress}%` + importProgressLabel.innerText = _progress >= 15 ? `${_progress}%` : '' +} + +const setMessage = (_message, isError) => { + importMessageLabel.style.display = _message ? 'block' : 'none' + importMessageLabel.innerHTML = _message + if (isError) { + importMessageLabel.classList.add('error') + } else { + importMessageLabel.classList.remove('error') + } +} + +const setImportState = (_importInProgress) => { + importInProgress = _importInProgress + + importStarted.style.display = _importInProgress ? 'inline-block' : 'none' + importStopped.style.display = _importInProgress ? 'none' : 'block' + + if (_importInProgress) { + importButton.classList.add('transparent') + timer = setInterval(doInnerUpdates, 300) + } else { + importButton.classList.remove('transparent') + clearInterval(timer) + } +} + +const secondsToTime = (secs) => { + let hr = Math.floor(secs / 3600) + let min = Math.floor((secs - (hr * 3600)) / 60) + let sec = Math.floor(secs - (hr * 3600) - (min * 60)) + if (hr < 10) hr = `0${hr}` + if (min < 10) min = `0${min}` + if (sec < 10) sec = `0${sec}` + if (hr) hr = '00' + return `${hr}:${min}:${sec}` +} + +const bytesToSize = (bytes) => { + const sizes = ['Bytes', 'KB', 'MB'] + if (bytes == 0) return 'n/a' + const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))) + return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i] +} + +const determineDragAndDropCapable = () => { + const div = document.createElement('div') + return (('draggable' in div) + || ('ondragstart' in div && 'ondrop' in div)) + && 'FormData' in window + && 'FileReader' in window +} + +/* MAIN */ + +document.addEventListener('DOMContentLoaded', () => { + ['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave'] + .forEach((evt) => { + importForm.addEventListener(evt, (e) => { + e.preventDefault() + e.stopPropagation() + }) + }) +}, false) + +const labelClicked = (e) => { + if (importInProgress === true) { + e.preventDefault() + return false + } +} + +const urlSelected = (_url) => { + url = _url || importInput.value + + setMessage('') + setProgress(0) + + importButtonWrapper.style.display = 'block' + setImportState(false) +} + +const startImporting = () => { + if (importInProgress === true) return + if (!url) return + + isProcessing = false + iPreviousBytesLoaded = 0 + setMessage('') + setProgress(0) + setImportState(true) + + const formData = new FormData() + formData.append('url', url) + const xhr = new XMLHttpRequest() + + xhr.upload.addEventListener('progress', importProgress, false) + xhr.addEventListener('load', importFinish, false) + xhr.addEventListener('error', importError, false) + xhr.addEventListener('abort', importAbort, false) + + xhr.open('POST', '/import') + xhr.send(formData) + + timer = setInterval(doInnerUpdates, 300) +} + +const doInnerUpdates = () => { // we will use this function to display import speed + if (isProcessing) { + clearInterval(timer) + return + } + + let iDiff = iBytesImported - iPreviousBytesLoaded + // if nothing new loaded - exit + if (iDiff == 0) + return + iPreviousBytesLoaded = iBytesImported + iDiff = iDiff * 2 + const iBytesRem = iBytesTotal - iPreviousBytesLoaded + const secondsRemaining = iBytesRem / iDiff + // update speed info + let iSpeed = iDiff.toString() + 'B/s' + if (iDiff > 1024 * 1024) { + iSpeed = (Math.round(iDiff * 100/(1024*1024))/100).toString() + 'MB/s' + } else if (iDiff > 1024) { + iSpeed = (Math.round(iDiff * 100/1024)/100).toString() + 'KB/s' + } + + const speedMessage = `${iSpeed} | ${secondsToTime(secondsRemaining)}` + setMessage(speedMessage) +} + +function importProgress(e) { // import process in progress + if (e.lengthComputable) { + iBytesImported = e.loaded + iBytesTotal = e.total + + const iPercentComplete = Math.round(iBytesImported / iBytesTotal * 100) + setProgress(iPercentComplete) + if (iPercentComplete === 100) { + isProcessing = true + setMessage('Processing video... please wait') + } + } else { + setMessage('Unable to compute progress.') + } +} + +const importFinish = (e) => { // import successfully finished + const message = e.target.responseText + const isSuccess = e.target.status < 400 + + setProgress(isSuccess ? 100 : 0) + setMessage(message, !isSuccess) + setImportState(false) + if (isSuccess) removeFile(null, true) +} + +const importError = () => { // import error + setMessage('An error occurred while importing the url.', true) + setProgress(0) + setImportState(false) +} + +const importAbort = () => { // import abort + setMessage('The import has been canceled by the user or the browser dropped the connection.', true) + setProgress(0) + setImportState(false) +} -- cgit v1.2.3