summaryrefslogtreecommitdiff
path: root/static
diff options
context:
space:
mode:
authorJames Mills <prologic@shortcircuit.net.au>2020-03-27 14:24:44 +1000
committerJames Mills <prologic@shortcircuit.net.au>2020-03-27 14:24:44 +1000
commit00ecbee8b93e80e4ccb39249c33a8afebef6cdb6 (patch)
tree2dc3fb2d3e3b5a262bb08354dd0428cbe0f3782a /static
parente6d35fd8c6624f84c38040015266ecb90a155d61 (diff)
Added UI/UX for Importing artbitrary videos from a URL
Diffstat (limited to 'static')
-rw-r--r--static/import.css145
-rw-r--r--static/import.js199
2 files changed, 344 insertions, 0 deletions
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)
+}