Added UI/UX for Importing artbitrary videos from a URL
This commit is contained in:
parent
e6d35fd8c6
commit
00ecbee8b9
7 changed files with 602 additions and 47 deletions
124
app/app.go
124
app/app.go
|
@ -83,10 +83,16 @@ func NewApp(cfg *Config) (*App, error) {
|
||||||
template.Must(uploadTemplate.Parse(box.MustString("base.html")))
|
template.Must(uploadTemplate.Parse(box.MustString("base.html")))
|
||||||
a.Templates.Add("upload", uploadTemplate)
|
a.Templates.Add("upload", uploadTemplate)
|
||||||
|
|
||||||
|
importTemplate := template.New("import")
|
||||||
|
template.Must(importTemplate.Parse(box.MustString("import.html")))
|
||||||
|
template.Must(importTemplate.Parse(box.MustString("base.html")))
|
||||||
|
a.Templates.Add("import", importTemplate)
|
||||||
|
|
||||||
// Setup Router
|
// Setup Router
|
||||||
r := mux.NewRouter().StrictSlash(true)
|
r := mux.NewRouter().StrictSlash(true)
|
||||||
r.HandleFunc("/", a.indexHandler).Methods("GET", "OPTIONS")
|
r.HandleFunc("/", a.indexHandler).Methods("GET", "OPTIONS")
|
||||||
r.HandleFunc("/upload", a.uploadHandler).Methods("GET", "OPTIONS", "POST")
|
r.HandleFunc("/upload", a.uploadHandler).Methods("GET", "OPTIONS", "POST")
|
||||||
|
r.HandleFunc("/import", a.importHandler).Methods("GET", "OPTIONS", "POST")
|
||||||
r.HandleFunc("/v/{id}.mp4", a.videoHandler).Methods("GET")
|
r.HandleFunc("/v/{id}.mp4", a.videoHandler).Methods("GET")
|
||||||
r.HandleFunc("/v/{prefix}/{id}.mp4", a.videoHandler).Methods("GET")
|
r.HandleFunc("/v/{prefix}/{id}.mp4", a.videoHandler).Methods("GET")
|
||||||
r.HandleFunc("/t/{id}", a.thumbHandler).Methods("GET")
|
r.HandleFunc("/t/{id}", a.thumbHandler).Methods("GET")
|
||||||
|
@ -296,6 +302,124 @@ func (a *App) uploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTP handler for /import
|
||||||
|
func (a *App) importHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == "GET" {
|
||||||
|
ctx := &struct{}{}
|
||||||
|
a.render("import", w, ctx)
|
||||||
|
} else if r.Method == "POST" {
|
||||||
|
r.ParseMultipartForm(1024)
|
||||||
|
|
||||||
|
url := r.FormValue("url")
|
||||||
|
if url == "" {
|
||||||
|
err := fmt.Errorf("error, no url supplied")
|
||||||
|
log.Error(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Make collection user selectable from drop-down in Form
|
||||||
|
// XXX: Assume we can put uploaded videos into the first collection (sorted) we find
|
||||||
|
keys := make([]string, 0, len(a.Library.Paths))
|
||||||
|
for k := range a.Library.Paths {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
//collection := keys[0]
|
||||||
|
|
||||||
|
/*
|
||||||
|
uf, err := ioutil.TempFile(
|
||||||
|
a.Config.Server.UploadPath,
|
||||||
|
fmt.Sprintf("tube-import-*%s.mp4",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("error creating temporary file for importing: %w", err)
|
||||||
|
log.Error(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.Remove(uf.Name())
|
||||||
|
|
||||||
|
_, err = io.Copy(uf, file)
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("error writing file: %w", err)
|
||||||
|
log.Error(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tf, err := ioutil.TempFile(
|
||||||
|
a.Config.Server.UploadPath,
|
||||||
|
fmt.Sprintf("tube-transcode-*.mp4"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("error creating temporary file for transcoding: %w", err)
|
||||||
|
log.Error(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vf := filepath.Join(
|
||||||
|
a.Library.Paths[collection].Path,
|
||||||
|
fmt.Sprintf("%s.mp4", shortuuid.New()),
|
||||||
|
)
|
||||||
|
thumbFn1 := fmt.Sprintf("%s.jpg", strings.TrimSuffix(tf.Name(), filepath.Ext(tf.Name())))
|
||||||
|
thumbFn2 := fmt.Sprintf("%s.jpg", strings.TrimSuffix(vf, filepath.Ext(vf)))
|
||||||
|
|
||||||
|
// TODO: Use a proper Job Queue and make this async
|
||||||
|
if err := utils.RunCmd(
|
||||||
|
a.Config.Transcoder.Timeout,
|
||||||
|
"ffmpeg",
|
||||||
|
"-y",
|
||||||
|
"-i", uf.Name(),
|
||||||
|
"-vcodec", "h264",
|
||||||
|
"-acodec", "aac",
|
||||||
|
"-strict", "-2",
|
||||||
|
"-loglevel", "quiet",
|
||||||
|
tf.Name(),
|
||||||
|
); err != nil {
|
||||||
|
err := fmt.Errorf("error transcoding video: %w", err)
|
||||||
|
log.Error(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := utils.RunCmd(
|
||||||
|
a.Config.Thumbnailer.Timeout,
|
||||||
|
"mt",
|
||||||
|
"-b",
|
||||||
|
"-s",
|
||||||
|
"-n", "1",
|
||||||
|
tf.Name(),
|
||||||
|
); err != nil {
|
||||||
|
err := fmt.Errorf("error generating thumbnail: %w", err)
|
||||||
|
log.Error(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Rename(thumbFn1, thumbFn2); err != nil {
|
||||||
|
err := fmt.Errorf("error renaming generated thumbnail: %w", err)
|
||||||
|
log.Error(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Rename(tf.Name(), vf); err != nil {
|
||||||
|
err := fmt.Errorf("error renaming transcoded video: %w", err)
|
||||||
|
log.Error(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "Video successfully uploaded!")
|
||||||
|
*/
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "Not Implemented (yet)")
|
||||||
|
} else {
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// HTTP handler for /v/id
|
// HTTP handler for /v/id
|
||||||
func (a *App) pageHandler(w http.ResponseWriter, r *http.Request) {
|
func (a *App) pageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
|
|
150
app/rice-box.go
150
app/rice-box.go
File diff suppressed because one or more lines are too long
145
static/import.css
vendored
Normal file
145
static/import.css
vendored
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
199
static/import.js
Normal file
199
static/import.js
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -7,6 +7,7 @@
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="/static/favicon.ico">
|
<link rel="shortcut icon" type="image/x-icon" href="/static/favicon.ico">
|
||||||
<link rel="stylesheet" type="text/css" href="/static/theme.css">
|
<link rel="stylesheet" type="text/css" href="/static/theme.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/static/upload.css">
|
<link rel="stylesheet" type="text/css" href="/static/upload.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/import.css">
|
||||||
|
|
||||||
{{ template "stylesheets" . }}
|
{{ template "stylesheets" . }}
|
||||||
{{ template "css" . }}
|
{{ template "css" . }}
|
||||||
|
|
29
templates/import.html
Normal file
29
templates/import.html
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{{define "content"}}
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<label class="import-container" onclick="labelClicked(event)">
|
||||||
|
<div class="import-wrapper">
|
||||||
|
<form id="import-form" class="import-form" enctype="multipart/form-data" method="POST" action="/import">
|
||||||
|
<input id="import-input" type="url" required onchange="urlSelected()" />
|
||||||
|
<div class="import-details">
|
||||||
|
<span id="import-message" class="import-message">No URL entered</span>
|
||||||
|
<div id="import-button-wrapper" class="import-button-wrapper">
|
||||||
|
<button id="import-button" class="import-button" onclick="startImporting()" type="button">
|
||||||
|
<span id="import-stopped">Import</span>
|
||||||
|
<span id="import-started" class="loader" style="display: none;"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="import-progress-container" class="import-progress-container">
|
||||||
|
<div id="import-progress" class="import-progress">
|
||||||
|
<span id="import-progress-label" class="import-progress-label"></span>
|
||||||
|
</div>
|
||||||
|
<div style="flex-grow: 1;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{define "scripts"}}
|
||||||
|
<script type="application/javascript" src="/static/import.js"></script>
|
||||||
|
{{end}}
|
|
@ -30,6 +30,7 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
<p>Need to import a video from another source? Click <a href="/import">Import</a></p>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{define "scripts"}}
|
{{define "scripts"}}
|
||||||
|
|
Loading…
Reference in a new issue