Merge branch 'video-slicer' into 'dev'
# Conflicts: # web/assets/js/bs5.videosTable.jsfix-timezone-by-ui
commit
a8c7d8182e
|
@ -8142,6 +8142,81 @@ module.exports = function(s,config,lang){
|
|||
},
|
||||
}
|
||||
},
|
||||
"Studio": {
|
||||
"section": lang["Studio"],
|
||||
"blocks": {
|
||||
"Video Playback": {
|
||||
id: "studioVideoPlayback",
|
||||
noHeader: true,
|
||||
noDefaultSectionClasses: true,
|
||||
"color": "green",
|
||||
"section-pre-class": "col-md-8 search-parent",
|
||||
"info": [
|
||||
{
|
||||
"id": "studioViewingCanvas",
|
||||
"fieldType": "div",
|
||||
},
|
||||
{
|
||||
"id": "studioMonitorControls",
|
||||
"color": "blue",
|
||||
noHeader: true,
|
||||
isSection: true,
|
||||
isFormGroupGroup: true,
|
||||
'section-class': 'text-center',
|
||||
"info": [
|
||||
{
|
||||
"fieldType": "btn-group",
|
||||
"normalWidth": true,
|
||||
"btns": [
|
||||
{
|
||||
"fieldType": "btn",
|
||||
"class": `btn-default btn-sm play-preview`,
|
||||
"attribute": `title="${lang['Play']}"`,
|
||||
"btnContent": `<i class="fa fa-play"></i>`,
|
||||
},
|
||||
{
|
||||
"fieldType": "btn",
|
||||
"class": `btn-default btn-sm slice-video`,
|
||||
"attribute": `title="${lang['Slice']}"`,
|
||||
"btnContent": `<i class="fa fa-scissors"></i>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"color": "bg-gradient-blue text-white",
|
||||
noHeader: true,
|
||||
isSection: true,
|
||||
isFormGroupGroup: true,
|
||||
'section-class': 'text-center',
|
||||
"info": [
|
||||
{
|
||||
"id": "studioTimelineStrip",
|
||||
"fieldType": "div",
|
||||
"divContent": `
|
||||
<div id="studio-time-ticks"></div>
|
||||
<div id="studio-seek-tick"></div>
|
||||
<div id="studio-slice-selection" style="width: 80%;left: 10%;"></div>
|
||||
`,
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
"Container2": {
|
||||
noHeader: true,
|
||||
"section-pre-class": "col-md-4",
|
||||
"noDefaultSectionClasses": true,
|
||||
"info": [
|
||||
{
|
||||
"id": "studio-completed-videos",
|
||||
"fieldType": "div",
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Calendar": {
|
||||
"section": "Calendar",
|
||||
"blocks": {
|
||||
|
|
|
@ -98,6 +98,9 @@
|
|||
"No API Key": "No API Key",
|
||||
"Use Camera Timestamps": "Use Camera Timestamps",
|
||||
"Power Viewer": "Power Viewer",
|
||||
"Slice": "Slice",
|
||||
"Stitch": "Stitch",
|
||||
"Studio": "Studio",
|
||||
"Power Video Viewer": "Power Video Viewer",
|
||||
"Time-lapse": "Time-lapse",
|
||||
"Montage": "Montage",
|
||||
|
|
|
@ -192,15 +192,22 @@ module.exports = (s,config,lang) => {
|
|||
const monitorId = options.mid
|
||||
const groupKey = options.ke
|
||||
const cutLength = options.cutLength || 10
|
||||
const startTime = options.startTime
|
||||
const tempDirectory = s.getStreamsDirectory(options)
|
||||
let fileExt = inputFilePath.split('.')
|
||||
fileExt = fileExt[fileExt.length -1]
|
||||
const filename = `${s.gid(10)}.${fileExt}`
|
||||
const videoOutPath = `${tempDirectory}${filename}`
|
||||
const cuttingProcess = spawn(config.ffmpegDir,['-loglevel','warning','-i', inputFilePath, '-c','copy','-t',`${cutLength}`,videoOutPath])
|
||||
const ffmpegCmd = ['-loglevel','warning','-i', inputFilePath, '-c','copy','-t',`${cutLength}`,videoOutPath]
|
||||
if(startTime){
|
||||
ffmpegCmd.splice(2, 0, "-ss")
|
||||
ffmpegCmd.splice(3, 0, `${startTime}`)
|
||||
s.debugLog(`cutVideoLength ffmpegCmd with startTime`,ffmpegCmd)
|
||||
}
|
||||
const cuttingProcess = spawn(config.ffmpegDir,ffmpegCmd)
|
||||
cuttingProcess.stderr.on('data',(data) => {
|
||||
const err = data.toString()
|
||||
s.debugLog('cutVideoLength',options,err)
|
||||
s.debugLog('cutVideoLength STDERR',options,err)
|
||||
})
|
||||
cuttingProcess.on('close',(data) => {
|
||||
fs.stat(videoOutPath,(err) => {
|
||||
|
@ -534,6 +541,58 @@ module.exports = (s,config,lang) => {
|
|||
})
|
||||
})
|
||||
}
|
||||
async function sliceVideo(video,{
|
||||
startTime,
|
||||
endTime,
|
||||
}){
|
||||
const response = {ok: false}
|
||||
if(!startTime || !endTime){
|
||||
response.msg = 'Missing startTime or endTime!'
|
||||
return response
|
||||
}
|
||||
try{
|
||||
const groupKey = video.ke
|
||||
const monitorId = video.mid
|
||||
const filename = s.formattedTime(video.time) + '.' + video.ext
|
||||
const finalFilename = s.formattedTime(video.time) + `-sliced-${s.gid(5)}.` + video.ext
|
||||
const videoFolder = s.getVideoDirectory(video)
|
||||
const fileBinFolder = s.getFileBinDirectory(video)
|
||||
const inputFilePath = `${videoFolder}${filename}`
|
||||
const fileBinFilePath = `${fileBinFolder}${finalFilename}`
|
||||
const cutLength = parseFloat(endTime) - parseFloat(startTime);
|
||||
s.debugLog(`sliceVideo start slice...`)
|
||||
const cutProcessResponse = await cutVideoLength({
|
||||
ke: groupKey,
|
||||
mid: monitorId,
|
||||
cutLength,
|
||||
startTime,
|
||||
filePath: inputFilePath,
|
||||
});
|
||||
s.debugLog(`sliceVideo cutProcessResponse`,cutProcessResponse)
|
||||
const newFilePath = cutProcessResponse.filePath
|
||||
const copyResponse = await copyFile(newFilePath,fileBinFilePath)
|
||||
s.debugLog(`sliceVideo copyResponse`,copyResponse)
|
||||
const fileBinInsertQuery = {
|
||||
ke: groupKey,
|
||||
mid: monitorId,
|
||||
name: finalFilename,
|
||||
size: video.size,
|
||||
details: video.details,
|
||||
status: 1,
|
||||
time: video.time,
|
||||
}
|
||||
await s.insertFileBinEntry(fileBinInsertQuery)
|
||||
s.tx(Object.assign({
|
||||
f: 'fileBin_item_added',
|
||||
slicedVideo: true,
|
||||
},fileBinInsertQuery),'GRP_'+video.ke);
|
||||
response.ok = true
|
||||
}catch(err){
|
||||
response.err = err
|
||||
s.debugLog('sliceVideo ERROR',err)
|
||||
}
|
||||
return response
|
||||
}
|
||||
return {
|
||||
reEncodeVideoAndReplace,
|
||||
stitchMp4Files,
|
||||
|
@ -544,5 +603,6 @@ module.exports = (s,config,lang) => {
|
|||
reEncodeVideoAndBinOriginal,
|
||||
reEncodeVideoAndBinOriginalAddToQueue,
|
||||
archiveVideo,
|
||||
sliceVideo,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ module.exports = function(s,config,lang,app,io){
|
|||
destroySubstreamProcess,
|
||||
} = require('./monitor/utils.js')(s,config,lang)
|
||||
const {
|
||||
sliceVideo,
|
||||
archiveVideo,
|
||||
reEncodeVideoAndReplace,
|
||||
reEncodeVideoAndBinOriginalAddToQueue,
|
||||
|
@ -1816,6 +1817,15 @@ module.exports = function(s,config,lang,app,io){
|
|||
const originalFileName = `${s.formattedTime(r.time)+'.'+r.ext}`
|
||||
var details = s.parseJSON(r.details) || {}
|
||||
switch(req.params.mode){
|
||||
case'slice':
|
||||
const startTime = s.getPostData(req,'startTime');
|
||||
const endTime = s.getPostData(req,'endTime');
|
||||
const sliceResponse = await sliceVideo(r,{
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
});
|
||||
response = sliceResponse
|
||||
break;
|
||||
case'archive':
|
||||
response.ok = true
|
||||
const unarchive = s.getPostData(req,'unarchive') == '1';
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
#tab-studio {
|
||||
|
||||
}
|
||||
#studioTimelineStrip {
|
||||
height: 60px;
|
||||
background: rgba(0,0,0,0.3);
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
}
|
||||
#studio-slice-selection {
|
||||
transition: none;
|
||||
height: 100%;
|
||||
background: rgb(216 230 249 / 70%);
|
||||
border-radius: 5px;
|
||||
position: absolute;
|
||||
cursor: e-resize;
|
||||
}
|
||||
#studio-time-ticks {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
#studio-time-ticks .tick {
|
||||
position: absolute;
|
||||
border-left:1px solid rgba(0,0,0,0.1);
|
||||
height: 100%;
|
||||
}
|
||||
#studio-seek-tick {
|
||||
position: absolute;
|
||||
border-left: 2px solid #fffc00;
|
||||
border-radius: 5px;
|
||||
transition: none;
|
||||
height: 80%;
|
||||
top: 10%;
|
||||
left: 10%;
|
||||
}
|
||||
#studio-time-ticks .tick span {
|
||||
left: -50%;
|
||||
position: relative;
|
||||
font-size: 80%;
|
||||
font-family: monospace;
|
||||
}
|
|
@ -0,0 +1,270 @@
|
|||
$(document).ready(function(){
|
||||
var theEnclosure = $('#tab-studio')
|
||||
var viewingCanvas = $('#studioViewingCanvas')
|
||||
var timelineStrip = $('#studioTimelineStrip')
|
||||
var seekTick = $('#studio-seek-tick')
|
||||
var completedVideosList = $('#studio-completed-videos')
|
||||
var timelineStripTimeTicksContainer = $('#studio-time-ticks')
|
||||
var timelineStripSliceSelection = $('#studio-slice-selection')
|
||||
var stripWidth = timelineStrip.width()
|
||||
var loadedVideoForSlicer = null
|
||||
var loadedVideoElement = null
|
||||
var timelineStripMousemoveX = 0
|
||||
var timelineStripMousemoveY = 0
|
||||
var userInvokedPlayState = false
|
||||
var slicerQueue = {}
|
||||
var changeTimeout
|
||||
function setMoveTimeout(){
|
||||
clearTimeout(changeTimeout)
|
||||
changeTimeout = setTimeout(function(){
|
||||
changeTimeout = null
|
||||
},500)
|
||||
}
|
||||
function initStudio(){
|
||||
var lastStartTime = 0
|
||||
var lastEndTime = 0
|
||||
function onChange(){
|
||||
setMoveTimeout()
|
||||
var data = getSliceSelection()
|
||||
var startTime = data.startTimeSeconds
|
||||
var endTime = data.endTimeSeconds
|
||||
if(lastStartTime === startTime && lastEndTime !== endTime){
|
||||
setSeekPosition(endTime)
|
||||
}else{
|
||||
setSeekPosition(startTime)
|
||||
}
|
||||
lastStartTime = parseFloat(startTime)
|
||||
lastEndTime = parseFloat(endTime)
|
||||
setSeekRestraintOnVideo()
|
||||
}
|
||||
timelineStripSliceSelection.resizable({
|
||||
containment: '#studioTimelineStrip',
|
||||
handles: "e,w",
|
||||
});
|
||||
timelineStripSliceSelection.resize(function(){
|
||||
setMoveTimeout()
|
||||
onChange()
|
||||
})
|
||||
timelineStripSliceSelection.draggable({
|
||||
containment: '#studioTimelineStrip',
|
||||
axis: "x",
|
||||
start: function(){
|
||||
changeTimeout = true
|
||||
},
|
||||
drag: onChange,
|
||||
stop: function(){
|
||||
changeTimeout = null
|
||||
}
|
||||
});
|
||||
}
|
||||
function validateTimeSlot(timeValue){
|
||||
var roundedValue = Math.round(timeValue)
|
||||
return `${roundedValue}`.length === 1 ? `0${roundedValue}` : roundedValue
|
||||
}
|
||||
function getTimeInfo(timePx){
|
||||
var timeDifference = loadedVideoElement.duration
|
||||
var timePercent = timePx / stripWidth * 100
|
||||
var timeSeconds = (timeDifference * (timePercent / 100))
|
||||
// var timeMinutes = parseInt(timeSeconds / 60)
|
||||
// var timeLastSeconds = timeSeconds - (timeMinutes * 60)
|
||||
// var timestamp = `00:${validateTimeSlot(timeMinutes)}:${validateTimeSlot(timeLastSeconds)}`
|
||||
return {
|
||||
timePercent,
|
||||
timeSeconds,
|
||||
// timeMinutes,
|
||||
// timeLastSeconds,
|
||||
// timestamp,
|
||||
}
|
||||
}
|
||||
function getSliceSelection(){
|
||||
var amountOfSecondsBetween = loadedVideoElement.duration
|
||||
//
|
||||
var startTimePx = timelineStripSliceSelection.position().left
|
||||
var startTimeInfo = getTimeInfo(startTimePx)
|
||||
//
|
||||
var endTimePx = startTimePx + timelineStripSliceSelection.width()
|
||||
var endTimeInfo = getTimeInfo(endTimePx)
|
||||
return {
|
||||
// startTimestamp: startTimeInfo.timestamp,
|
||||
// endTimestamp: endTimeInfo.timestamp,
|
||||
startTimeSeconds: startTimeInfo.timeSeconds,
|
||||
endTimeSeconds: endTimeInfo.timeSeconds
|
||||
}
|
||||
}
|
||||
function sliceVideo(){
|
||||
var video = Object.assign({},loadedVideoForSlicer)
|
||||
var monitorId = video.mid
|
||||
var filename = video.filename
|
||||
var groupKey = video.ke
|
||||
const sliceInfo = getSliceSelection()
|
||||
$.get(`${getApiPrefix('videos')}/${monitorId}/${filename}/slice?startTime=${sliceInfo.startTimeSeconds}&endTime=${sliceInfo.endTimeSeconds}`,function(data){
|
||||
console.log('sliceVideo',data)
|
||||
})
|
||||
}
|
||||
function drawTimeTicks(video){
|
||||
var amountOfSecondsBetween = loadedVideoElement.duration
|
||||
var tickDivisor = amountOfSecondsBetween > 60 * 60 ? 500 : amountOfSecondsBetween > 60 ? 100 : amountOfSecondsBetween > 30 ? 20 : 2
|
||||
var numberOfTicks = amountOfSecondsBetween / tickDivisor
|
||||
var tickStripWidth = timelineStripTimeTicksContainer.width()
|
||||
var tickSpacingWidth = tickStripWidth / numberOfTicks
|
||||
var tickSpacingPercent = tickSpacingWidth / tickStripWidth * 100
|
||||
var tickHtml = ''
|
||||
for (let i = 1; i < numberOfTicks; i++) {
|
||||
var tickPercent = tickSpacingPercent * i
|
||||
var numberOfSecondsForTick = parseInt(amountOfSecondsBetween / numberOfTicks) * i;
|
||||
|
||||
tickHtml += `<div class="tick" style="left:${tickPercent}%"><span>${numberOfSecondsForTick}s</span></div>`;
|
||||
}
|
||||
timelineStripTimeTicksContainer.html(tickHtml)
|
||||
}
|
||||
function createVideoElement(video){
|
||||
var html = `<video class="video_video" controls src="${video.href}"></video>`
|
||||
viewingCanvas.html(html)
|
||||
var videoElement = theEnclosure.find('video')
|
||||
loadedVideoElement = videoElement[0]
|
||||
}
|
||||
function updateSeekTickPosition(){
|
||||
const percentMoved = loadedVideoElement.currentTime / loadedVideoElement.duration * 100
|
||||
seekTick.css('left',`${percentMoved}%`)
|
||||
}
|
||||
function setSeekRestraintOnVideo(){
|
||||
var data = getSliceSelection()
|
||||
var startTime = data.startTimeSeconds
|
||||
var endTime = data.endTimeSeconds
|
||||
console.log(data)
|
||||
|
||||
loadedVideoElement.ontimeupdate = (event) => {
|
||||
updateSeekTickPosition()
|
||||
if(loadedVideoElement.currentTime <= startTime){
|
||||
loadedVideoElement.currentTime = startTime
|
||||
}else if(loadedVideoElement.currentTime >= endTime){
|
||||
loadedVideoElement.currentTime = startTime
|
||||
// pauseVideo()
|
||||
}
|
||||
};
|
||||
loadedVideoElement.onplay = (event) => {
|
||||
userInvokedPlayState = true
|
||||
togglePlayPauseIcon()
|
||||
}
|
||||
loadedVideoElement.onpause = (event) => {
|
||||
userInvokedPlayState = false
|
||||
togglePlayPauseIcon()
|
||||
}
|
||||
}
|
||||
function pauseVideo(){
|
||||
loadedVideoElement.pause()
|
||||
}
|
||||
function playVideo(){
|
||||
loadedVideoElement.play()
|
||||
}
|
||||
function togglePlayPause(){
|
||||
try{
|
||||
if(userInvokedPlayState){
|
||||
pauseVideo()
|
||||
}else{
|
||||
playVideo()
|
||||
}
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
function togglePlayPauseIcon(){
|
||||
var iconEl = theEnclosure.find('.play-preview i')
|
||||
if(!userInvokedPlayState){
|
||||
iconEl.addClass('fa-play').removeClass('fa-pause')
|
||||
}else{
|
||||
iconEl.addClass('fa-pause').removeClass('fa-play')
|
||||
}
|
||||
}
|
||||
function setSeekPosition(secondsIn){
|
||||
loadedVideoElement.currentTime = parseFloat(secondsIn) || 1
|
||||
try{
|
||||
loadedVideoElement.play()
|
||||
}catch(err){
|
||||
|
||||
}
|
||||
pauseVideo()
|
||||
}
|
||||
function loadVideoIntoSlicer(video){
|
||||
loadedVideoForSlicer = Object.assign({},video)
|
||||
var startTime = new Date(loadedVideoForSlicer.time)
|
||||
var endTime = new Date(loadedVideoForSlicer.end)
|
||||
createVideoElement(video)
|
||||
drawTimeTicks(video)
|
||||
completedVideosList.empty()
|
||||
setSeekRestraintOnVideo()
|
||||
}
|
||||
function drawCompletedVideoRow(file){
|
||||
var videoEndpoint = getApiPrefix(`fileBin`) + '/' + file.mid + '/' + file.name
|
||||
var html = `<div class="card bg-dark mb-3" data-mid="${file.mid}" data-ke="${file.ke}" data-filename="${file.name}">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-row">
|
||||
<div class="flex-grow-1 text-white">
|
||||
${file.name}
|
||||
</div>
|
||||
<div>
|
||||
<a class="btn btn-sm btn-primary open-fileBin-video" href="${videoEndpoint}" title="${lang.Play}"><i class="fa fa-play"></i></a>
|
||||
<a class="btn btn-sm btn-success" href="${videoEndpoint}" title="${lang.Download}" download><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
completedVideosList.append(html)
|
||||
}
|
||||
function seekByTimelineClick(e){
|
||||
if(!changeTimeout){
|
||||
var currentlyPlaying = !!userInvokedPlayState
|
||||
var leftOffset = e.pageX - timelineStripSliceSelection.offset().left
|
||||
var tickPx = timelineStripSliceSelection.position().left + leftOffset
|
||||
var timeSeconds = getTimeInfo(tickPx).timeSeconds
|
||||
setSeekPosition(timeSeconds)
|
||||
if(currentlyPlaying){
|
||||
playVideo()
|
||||
}
|
||||
}
|
||||
}
|
||||
onWebSocketEvent(function(data){
|
||||
switch(data.f){
|
||||
case'fileBin_item_added':
|
||||
if(data.slicedVideo){
|
||||
drawCompletedVideoRow(data)
|
||||
}
|
||||
break;
|
||||
}
|
||||
})
|
||||
addOnTabAway('studio', function () {
|
||||
loadedVideoElement.pause()
|
||||
})
|
||||
$(window).resize(function(){
|
||||
drawTimeTicks(loadedVideoForSlicer)
|
||||
})
|
||||
timelineStrip.resize(function(){
|
||||
stripWidth = timelineStrip.width()
|
||||
})
|
||||
timelineStripSliceSelection.mousedown(seekByTimelineClick)
|
||||
$('body')
|
||||
.on('click','.open-video-studio',function(e){
|
||||
e.preventDefault()
|
||||
var el = $(this).parents('[data-mid]')
|
||||
var monitorId = el.attr('data-mid')
|
||||
var videoTime = el.attr('data-time')
|
||||
var video = loadedVideosInMemory[`${monitorId}${videoTime}`]
|
||||
openTab('studio')
|
||||
loadVideoIntoSlicer(video)
|
||||
return false;
|
||||
});
|
||||
theEnclosure
|
||||
.on('click','.slice-video',function(){
|
||||
sliceVideo()
|
||||
})
|
||||
.on('click','.play-preview',function(){
|
||||
togglePlayPause()
|
||||
togglePlayPauseIcon()
|
||||
})
|
||||
.on('mouseup','.ui-resizable-handle.ui-resizable-e',function(){
|
||||
var data = getSliceSelection()
|
||||
var startTime = data.startTimeSeconds
|
||||
setSeekPosition(startTime)
|
||||
});
|
||||
initStudio()
|
||||
})
|
|
@ -376,18 +376,20 @@ $(document).ready(function(e){
|
|||
window.askedForTimelapseVideoBuild = false
|
||||
break;
|
||||
case'fileBin_item_added':
|
||||
var saveBuiltVideo = dashboardOptions().switches.timelapseSaveBuiltVideo
|
||||
let statusText = `${lang['Done!']}`
|
||||
onTimelapseVideoBuildComplete(data)
|
||||
if(data.timelapseVideo && saveBuiltVideo === 1){
|
||||
downloadTimelapseVideo(data)
|
||||
statusText = lang['Downloaded!']
|
||||
if(data.timelapseVideo){
|
||||
var saveBuiltVideo = dashboardOptions().switches.timelapseSaveBuiltVideo
|
||||
let statusText = `${lang['Done!']}`
|
||||
onTimelapseVideoBuildComplete(data)
|
||||
if(saveBuiltVideo === 1){
|
||||
downloadTimelapseVideo(data)
|
||||
statusText = lang['Downloaded!']
|
||||
}
|
||||
setDownloadButtonLabel(statusText, '')
|
||||
var progressItem = sideLinkListBox.find(`[data-mid="${data.mid}"][data-ke="${data.mid}"][data-name="${data.name}"]`)
|
||||
progressItem.find('.row-status').text(statusText)
|
||||
progressItem.find('.dot').removeClass('dot-orange').addClass('dot-green')
|
||||
progressItem.find('.download-button').show()
|
||||
}
|
||||
setDownloadButtonLabel(statusText, '')
|
||||
var progressItem = sideLinkListBox.find(`[data-mid="${data.mid}"][data-ke="${data.mid}"][data-name="${data.name}"]`)
|
||||
progressItem.find('.row-status').text(statusText)
|
||||
progressItem.find('.dot').removeClass('dot-orange').addClass('dot-green')
|
||||
progressItem.find('.download-button').show()
|
||||
break;
|
||||
case'timelapse_build_percent':
|
||||
var progressItem = sideLinkListBox.find(`[data-mid="${data.mid}"][data-ke="${data.mid}"][data-name="${data.name}"]`)
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -30,6 +30,7 @@
|
|||
<link rel="stylesheet" href="<%-window.libURL%>assets/vendor/daterangepicker.css">
|
||||
<link rel="stylesheet" href="<%-window.libURL%>assets/vendor/gridstack.min.css">
|
||||
<link rel="stylesheet" href="<%-window.libURL%>assets/vendor/gridstack-extra.min.css">
|
||||
<link rel="stylesheet" href="<%-window.libURL%>assets/vendor/jquery-ui.min.css">
|
||||
<link rel="stylesheet" href="<%-window.libURL%>assets/css/clock.css">
|
||||
<link rel="stylesheet" href="<%-window.libURL%>assets/css/bs5.liveGrid.css">
|
||||
<link rel="stylesheet" href="<%-window.libURL%>assets/css/bs5.regionEditor.css">
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
'home/eventListWithPics',
|
||||
'home/fileBin',
|
||||
'home/videosTable',
|
||||
'home/studio',
|
||||
'confirm',
|
||||
'home/help',
|
||||
]).forEach(function(block){ %>
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<main class="page-tab pt-3" id="tab-studio">
|
||||
<div class="row <%- define.Theme.isDark ? `dark` : '' %>" id="studio">
|
||||
<%
|
||||
var drawBlock
|
||||
var buildOptions
|
||||
%>
|
||||
<%
|
||||
include fieldBuilders.ejs
|
||||
%>
|
||||
<% Object.keys(define['Studio'].blocks).forEach(function(blockKey){
|
||||
var theBlock = define['Studio'].blocks[blockKey]
|
||||
drawBlock(theBlock)
|
||||
}) %>
|
||||
</div>
|
||||
</main>
|
||||
<link rel="stylesheet" href="<%-window.libURL%>assets/css/bs5.studio.css">
|
||||
<script src="<%-window.libURL%>assets/js/bs5.studio.js"></script>
|
Loading…
Reference in New Issue