Merge branch 'dev' into 'dev'

# Conflicts:
#   plugins/tensorflow-coral/detect_image.py
merge-requests/289/head
Mitch Ross 2021-02-08 16:03:07 +00:00
commit e8fdfcc9f5
35 changed files with 2838 additions and 339 deletions

View File

@ -4719,7 +4719,7 @@ module.exports = function(s,config,lang){
}
}
},
"ONVIF Device Manager": {
"ONVIF Device Manager": {
"section": "ONVIF Device Manager",
"blocks": {
"Notice": {
@ -5186,6 +5186,175 @@ module.exports = function(s,config,lang){
]
},
}
}
},
"Sub-Account Manager": {
"section": "Sub-Account Manager",
"blocks": {
"Sub-Accounts": {
"name": lang['Sub-Accounts'],
"color": "grey",
"isSection": true,
"id":"monSectionAccountList",
"info": [
{
"fieldType": "table",
id: "subAccountsList",
}
]
},
"Account Information": {
"name": lang['Account Information'],
"color": "grey",
"isSection": true,
"isForm": true,
"id":"monSectionAccountInformation",
"info": [
{
hidden: true,
"name": "uid",
"field": "UID",
"fieldType": "text"
},
{
"name": "mail",
"field": lang.Email,
"fieldType": "text",
"default": "",
"possible": ""
},
{
"name": "pass",
"field": lang.Password,
"fieldType": "password",
"default": "",
"possible": ""
},
{
"name": "password_again",
"field": lang['Password Again'],
"fieldType": "password",
"default": "",
"possible": ""
},
{
forForm: true,
"fieldType": "btn",
"attribute": `type="reset"`,
"class": `btn-default reset-form`,
"btnContent": `<i class="fa fa-undo"></i> &nbsp; ${lang['Clear']}`,
},
{
"fieldType": "btn",
"class": `btn-success submit-form`,
"btnContent": `<i class="fa fa-plus"></i> &nbsp; ${lang['Add New']}`,
},
{
hidden: true,
"name": "details",
"preFill": "{}",
},
]
},
"Account Privileges": {
"name": lang['Account Privileges'],
"color": "grey",
"isSection": true,
"id":"monSectionAccountPrivileges",
"info": [
{
"name": "detail=allmonitors",
"field": lang['All Monitors and Privileges'],
"default": "0",
"fieldType": "select",
"selector": "h_perm_allmonitors",
"possible": [
{
"name": lang.No,
"value": "0"
},
{
"name": lang.Yes,
"value": "1"
}
]
},
{
"name": "detail=monitor_create",
"field": lang['Can Create and Delete Monitors'],
"default": "0",
"fieldType": "select",
"possible": [
{
"name": lang.No,
"value": "0"
},
{
"name": lang.Yes,
"value": "1"
}
]
},
{
"name": "detail=user_change",
"field": lang['Can Change User Settings'],
"default": "0",
"fieldType": "select",
"possible": [
{
"name": lang.No,
"value": "0"
},
{
"name": lang.Yes,
"value": "1"
}
]
},
{
"name": "detail=view_logs",
"field": lang['Can View Logs'],
"default": "0",
"fieldType": "select",
"possible": [
{
"name": lang.No,
"value": "0"
},
{
"name": lang.Yes,
"value": "1"
}
]
},
{
"name": "detail=landing_page",
"field": lang['Landing Page'],
"default": "",
"fieldType": "select",
"possible": [
{
"name": lang.Default,
"value": ""
},
{
"name": lang.Timelapse,
"value": "timelapse"
}
]
},
{
"fieldType": "div",
"class": "h_perm_allmonitors_input h_perm_allmonitors_1",
id: "sub_accounts_permissions",
},
{
"fieldType": "btn",
"class": `btn-success submit-form`,
"btnContent": `<i class="fa fa-plus"></i> &nbsp; ${lang['Add New']}`,
},
]
},
}
}
}
}

View File

@ -5,7 +5,19 @@
"failedLoginText2": "Please check your login credentials.",
"Time Left": "Time Left",
"Inverse Trigger": "Inverse Trigger",
"Account Privileges": "Account Privileges",
"Account Information": "Account Information",
"subAccountManager": "Sub-Account Manager",
"contactAdmin": "Contact the maintainer of your Shinobi installation.",
"accountAdded": "Account Added",
"accountAddedText": "Account has been added.",
"accountDeleted": "Account Deleted",
"accountDeletedText": "Account has been deleted.",
"accountActionFailed": "Account Action Failed",
"deleteSubAccount": "Delete Sub-Account",
"deleteSubAccountText": "Do you want to delete this Sub-Account? You cannot recover it.",
"Turn Speed": "Turn Speed",
"Session Key": "Session Key",
"Login": "Login",
"Authenticate": "Authenticate",
"Dashboard": "Dashboard",
@ -171,6 +183,8 @@
"Started Building": "Started Building",
"Add": "Add",
"Save": "Save",
"Save New": "Save New",
"Save Changes": "Save Changes",
"Close": "Close",
"Don't Show for 1 Week": "Don't Show for 1 Week",
"Secure": "Secure",
@ -451,6 +465,7 @@
"NoVideosFoundForDateRange": "No Videos found in this date range. Try setting the start date further back.",
"NoLogsFoundForDateRange": "No Logs found in this date range. Try widening the date range.",
"monitorEditFailedMaxReached": "Your account has reached the maximum number of cameras that can be created. Speak to an administrator if you would like this changed.",
"Sub-Accounts": "Sub-Accounts",
"in": "in",
"ago": "ago",
"a few seconds": "a few seconds",
@ -502,6 +517,7 @@
"Detector Recording Process Exited Prematurely. Restarting.": "Detector Recording Process Exited Prematurely. Restarting.",
"Detector Recording Complete": "Detector Recording Complete",
"Clear Recorder Process": "Clear Recorder Process",
"Clear": "Clear",
"Logging": "Logging",
"Timelapse": "Timelapse",
"Nothing exists": "Nothing exists",

View File

@ -1,3 +1,4 @@
var fs = require('fs')
var P2P = require('pipe2pam')
var PamDiff = require('pam-diff')
module.exports = function(jsonData,pamDiffResponder){
@ -50,7 +51,7 @@ module.exports = function(jsonData,pamDiffResponder){
}
globalThreshold = parseInt(jsonData.rawMonitorConfig.details.detector_threshold) || 0
const regionsAreMasks = jsonData.rawMonitorConfig.details.detector_frame !== '1' && jsonData.rawMonitorConfig.details.inverse_trigger === '1';
var regionJson
try{
regionJson = JSON.parse(jsonData.rawMonitorConfig.details.cords)
@ -61,8 +62,8 @@ module.exports = function(jsonData,pamDiffResponder){
if(Object.keys(regionJson).length === 0 || jsonData.rawMonitorConfig.details.detector_frame === '1'){
fullFrame = {
name:'FULL_FRAME',
sensitivity:globalSensitivity,
color_threshold:globalColorThreshold,
sensitivity: globalSensitivity,
color_threshold: globalColorThreshold,
points:[
[0,0],
[0,height],
@ -72,12 +73,18 @@ module.exports = function(jsonData,pamDiffResponder){
}
}
const mask = {
max_sensitivity : globalSensitivity,
threshold : globalThreshold,
}
var regions = createPamDiffRegionArray(regionJson,globalColorThreshold,globalSensitivity,fullFrame)
var pamDiffOptions = {
mask: jsonData.rawMonitorConfig.details.detector_frame !== '1' && jsonData.rawMonitorConfig.details.inverse_trigger === '1',
mask: regionsAreMasks,
grayscale: 'luminosity',
regions : regions.forPam
regions : regions.forPam,
percent : globalSensitivity,
difference : globalColorThreshold,
}
if(jsonData.rawMonitorConfig.details.detector_show_matrix==='1'){
pamDiffOptions.response = 'bounds'
@ -108,7 +115,7 @@ module.exports = function(jsonData,pamDiffResponder){
var filteredCount = 0
var filteredCountSuccess = 0
trigger.merged.forEach(function(triggerPiece){
var region = regionArray.find(x => x.name == triggerPiece.name)
var region = regionsAreMasks ? mask : regionArray.find(x => x.name == triggerPiece.name)
checkMaximumSensitivity(region, detectorObject, function(err1) {
checkTriggerThreshold(region, detectorObject, function(err2) {
++filteredCount
@ -122,7 +129,7 @@ module.exports = function(jsonData,pamDiffResponder){
})
}else{
if(trigger.matrix)detectorObject.details.matrices = [trigger.matrix]
var region = regionArray.find(x => x.name == detectorObject.name)
var region = regionsAreMasks ? mask : regionArray.find(x => x.name == detectorObject.name)
checkMaximumSensitivity(region, detectorObject, function(err1) {
checkTriggerThreshold(region, detectorObject, function(err2) {
if(!err1 && !err2){
@ -175,7 +182,7 @@ module.exports = function(jsonData,pamDiffResponder){
imgWidth:jsonData.rawMonitorConfig.details.detector_scale_x
}
if(trigger.matrix)detectorObject.details.matrices = [trigger.matrix]
var region = Object.values(regionJson).find(x => x.name == detectorObject.name)
var region = regionsAreMasks ? mask : Object.values(regionJson).find(x => x.name == detectorObject.name)
checkMaximumSensitivity(region, detectorObject, function(err1) {
checkTriggerThreshold(region, detectorObject, function(err2) {
if(!err1 && ! err2){

View File

@ -83,12 +83,12 @@ module.exports = function(s,config,lang){
})
}else{
var onvifDirections = {
"left": [-1.0,'x'],
"right": [1.0,'x'],
"down": [invertedVerticalAxis ? 1.0 : -1.0,'y'],
"up": [invertedVerticalAxis ? -1.0 : 1.0,'y'],
"zoom_in": [1.0,'z'],
"zoom_out": [-1.0,'z']
"left": [-turnSpeed,'x'],
"right": [turnSpeed,'x'],
"down": [invertedVerticalAxis ? turnSpeed : -turnSpeed,'y'],
"up": [invertedVerticalAxis ? -turnSpeed : turnSpeed,'y'],
"zoom_in": [turnSpeed,'z'],
"zoom_out": [-turnSpeed,'z']
}
var direction = onvifDirections[options.direction]
controlOptions.Velocity[direction[1]] = direction[0]

View File

@ -403,11 +403,23 @@ module.exports = function(s,config){
}
})
}
const knexQueryPromise = (options) => {
return new Promise((resolve,reject) => {
knexQuery(options,(err,rows) => {
resolve({
ok: !err,
err: err,
rows: rows,
})
})
})
}
const connectDatabase = function(){
s.databaseEngine = require('knex')(s.databaseOptions)
}
return {
knexQuery: knexQuery,
knexQueryPromise: knexQueryPromise,
knexError: knexError,
cleanSqlWhereObject: cleanSqlWhereObject,
processSimpleWhereCondition: processSimpleWhereCondition,

View File

@ -372,111 +372,113 @@ module.exports = (s,config,lang) => {
const buildMainStream = function(e){
//e = monitor object
//x = temporary values
const isCudaEnabled = hasCudaEnabled(e)
const streamFlags = []
const streamFilters = []
const videoCodecisCopy = e.details.stream_vcodec === 'copy'
const videoCodec = e.details.stream_vcodec ? e.details.stream_vcodec : 'no'
const audioCodec = e.details.stream_acodec ? e.details.stream_acodec : 'no'
const videoQuality = e.details.stream_quality ? e.details.stream_quality : '1'
const streamType = e.details.stream_type ? e.details.stream_type : 'hls'
const videoFps = !isNaN(parseFloat(e.details.stream_fps)) && e.details.stream_fps !== '0' ? parseFloat(e.details.stream_fps) : null
const inputMap = buildInputMap(e,e.details.input_map_choices.stream)
const outputCanHaveAudio = (streamType === 'hls' || streamType === 'mp4' || streamType === 'flv' || streamType === 'h265')
const outputRequiresEncoding = streamType === 'mjpeg' || streamType === 'b64'
const outputIsPresetCapable = outputCanHaveAudio
const { videoWidth, videoHeight } = validateDimensions(e.details.stream_scale_x,e.details.stream_scale_y)
if(inputMap)streamFlags.push(inputMap)
if(e.details.cust_stream)streamFlags.push(e.details.cust_stream)
if(streamFlags.indexOf('-strict -2') === -1)streamFlags.push(`-strict -2`)
//stream - timestamp
if(e.details.stream_timestamp === "1" && !videoCodecisCopy){
streamFilters.push(buildTimestampFiltersFromConfiguration('stream_',e))
}
if(e.details.stream_watermark === "1" && e.details.stream_watermark_location){
streamFilters.push(buildWatermarkFiltersFromConfiguration(`stream_`,e))
}
//stream - rotation
if(e.details.stream_rotate && e.details.stream_rotate !== "no" && e.details.stream_vcodec !== 'copy'){
streamFilters.push(buildRotationFiltersFromConfiguration(`stream_`,e))
}
if(outputCanHaveAudio && audioCodec !== 'no'){
streamFlags.push(`-c:a ` + audioCodec)
}else{
streamFlags.push(`-an`)
}
if(videoCodec === 'h264_vaapi'){
streamFilters.push('format=nv12,hwupload');
if(e.details.stream_scale_x && e.details.stream_scale_y){
streamFilters.push('scale_vaapi=w='+e.details.stream_scale_x+':h='+e.details.stream_scale_y)
if(streamType !== 'jpeg'){
const isCudaEnabled = hasCudaEnabled(e)
const streamFilters = []
const videoCodecisCopy = e.details.stream_vcodec === 'copy'
const videoCodec = e.details.stream_vcodec ? e.details.stream_vcodec : 'no'
const audioCodec = e.details.stream_acodec ? e.details.stream_acodec : 'no'
const videoQuality = e.details.stream_quality ? e.details.stream_quality : '1'
const videoFps = !isNaN(parseFloat(e.details.stream_fps)) && e.details.stream_fps !== '0' ? parseFloat(e.details.stream_fps) : null
const inputMap = buildInputMap(e,e.details.input_map_choices.stream)
const outputCanHaveAudio = (streamType === 'hls' || streamType === 'mp4' || streamType === 'flv' || streamType === 'h265')
const outputRequiresEncoding = streamType === 'mjpeg' || streamType === 'b64'
const outputIsPresetCapable = outputCanHaveAudio
const { videoWidth, videoHeight } = validateDimensions(e.details.stream_scale_x,e.details.stream_scale_y)
if(inputMap)streamFlags.push(inputMap)
if(e.details.cust_stream)streamFlags.push(e.details.cust_stream)
if(streamFlags.indexOf('-strict -2') === -1)streamFlags.push(`-strict -2`)
//stream - timestamp
if(e.details.stream_timestamp === "1" && !videoCodecisCopy){
streamFilters.push(buildTimestampFiltersFromConfiguration('stream_',e))
}
}
if(isCudaEnabled && (streamType === 'mjpeg' || streamType === 'b64')){
streamFilters.push('hwdownload,format=nv12')
}
if(!outputRequiresEncoding && videoCodec !== 'no'){
streamFlags.push(`-c:v ` + videoCodec)
}
if(!videoCodecisCopy || outputRequiresEncoding){
if(videoWidth && videoHeight)streamFlags.push(`-s ${videoWidth}x${videoHeight}`)
if(videoFps && streamType === 'mjpeg' || streamType === 'b64'){
streamFilters.push(`fps=${videoFps}`)
if(e.details.stream_watermark === "1" && e.details.stream_watermark_location){
streamFilters.push(buildWatermarkFiltersFromConfiguration(`stream_`,e))
}
}
if(e.details.stream_vf){
streamFilters.push(e.details.stream_vf)
}
if(outputIsPresetCapable){
const streamPreset = streamType !== 'h265' && e.details.preset_stream ? e.details.preset_stream : null
if(streamPreset){
streamFlags.push(`-preset ${streamPreset}`)
//stream - rotation
if(e.details.stream_rotate && e.details.stream_rotate !== "no" && e.details.stream_vcodec !== 'copy'){
streamFilters.push(buildRotationFiltersFromConfiguration(`stream_`,e))
}
if(!videoCodecisCopy){
streamFlags.push(`-crf ${videoQuality}`)
if(outputCanHaveAudio && audioCodec !== 'no'){
streamFlags.push(`-c:a ` + audioCodec)
}else{
streamFlags.push(`-an`)
}
}else{
streamFlags.push(`-q:v ${videoQuality}`)
}
if((!videoCodecisCopy || outputRequiresEncoding) && streamFilters.length > 0){
streamFlags.push(`-vf "${streamFilters.join(',')}"`)
}
switch(streamType){
case'mp4':
streamFlags.push('-f mp4 -movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Poseidon Stream from Shinobi" -reset_timestamps 1 pipe:1')
break;
case'flv':
streamFlags.push(`-f flv`,'pipe:1')
break;
case'hls':
const hlsTime = !isNaN(parseInt(e.details.hls_time)) ? `${parseInt(e.details.hls_time)}` : '2'
const hlsListSize = !isNaN(parseInt(e.details.hls_list_size)) ? `${parseInt(e.details.hls_list_size)}` : '2'
if(videoCodec !== 'h264_vaapi' && !videoCodecisCopy){
if(!arrayContains('-tune',streamFlags)){
streamFlags.push(`-tune zerolatency`)
}
if(!arrayContains('-g ',streamFlags)){
streamFlags.push(`-g 1`)
}
if(videoCodec === 'h264_vaapi'){
streamFilters.push('format=nv12,hwupload');
if(e.details.stream_scale_x && e.details.stream_scale_y){
streamFilters.push('scale_vaapi=w='+e.details.stream_scale_x+':h='+e.details.stream_scale_y)
}
streamFlags.push(`-f hls -hls_time ${hlsTime} -hls_list_size ${hlsListSize} -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "${e.sdir}s.m3u8"`)
break;
case'mjpeg':
streamFlags.push(`-an -c:v mjpeg -f mpjpeg -boundary_tag shinobi pipe:1`)
break;
case'h265':
streamFlags.push(`-movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Shinobi H.265 Stream" -reset_timestamps 1 -f hevc pipe:1`)
break;
case'b64':case'':case undefined:case null://base64
streamFlags.push(`-an -c:v mjpeg -f image2pipe pipe:1`)
break;
}
if(e.details.custom_output){
streamFlags.push(e.details.custom_output)
}
if(e.details.stream_channels){
e.details.stream_channels.forEach(function(v,n){
streamFlags.push(createStreamChannel(e,n + config.pipeAddition,v))
})
}
if(isCudaEnabled && (streamType === 'mjpeg' || streamType === 'b64')){
streamFilters.push('hwdownload,format=nv12')
}
if(!outputRequiresEncoding && videoCodec !== 'no'){
streamFlags.push(`-c:v ` + videoCodec)
}
if(!videoCodecisCopy || outputRequiresEncoding){
if(videoWidth && videoHeight)streamFlags.push(`-s ${videoWidth}x${videoHeight}`)
if(videoFps && streamType === 'mjpeg' || streamType === 'b64'){
streamFilters.push(`fps=${videoFps}`)
}
}
if(e.details.stream_vf){
streamFilters.push(e.details.stream_vf)
}
if(outputIsPresetCapable){
const streamPreset = streamType !== 'h265' && e.details.preset_stream ? e.details.preset_stream : null
if(streamPreset){
streamFlags.push(`-preset ${streamPreset}`)
}
if(!videoCodecisCopy){
streamFlags.push(`-crf ${videoQuality}`)
}
}else{
streamFlags.push(`-q:v ${videoQuality}`)
}
if((!videoCodecisCopy || outputRequiresEncoding) && streamFilters.length > 0){
streamFlags.push(`-vf "${streamFilters.join(',')}"`)
}
switch(streamType){
case'mp4':
streamFlags.push('-f mp4 -movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Poseidon Stream from Shinobi" -reset_timestamps 1 pipe:1')
break;
case'flv':
streamFlags.push(`-f flv`,'pipe:1')
break;
case'hls':
const hlsTime = !isNaN(parseInt(e.details.hls_time)) ? `${parseInt(e.details.hls_time)}` : '2'
const hlsListSize = !isNaN(parseInt(e.details.hls_list_size)) ? `${parseInt(e.details.hls_list_size)}` : '2'
if(videoCodec !== 'h264_vaapi' && !videoCodecisCopy){
if(!arrayContains('-tune',streamFlags)){
streamFlags.push(`-tune zerolatency`)
}
if(!arrayContains('-g ',streamFlags)){
streamFlags.push(`-g 1`)
}
}
streamFlags.push(`-f hls -hls_time ${hlsTime} -hls_list_size ${hlsListSize} -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "${e.sdir}s.m3u8"`)
break;
case'mjpeg':
streamFlags.push(`-an -c:v mjpeg -f mpjpeg -boundary_tag shinobi pipe:1`)
break;
case'h265':
streamFlags.push(`-movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Shinobi H.265 Stream" -reset_timestamps 1 -f hevc pipe:1`)
break;
case'b64':case'':case undefined:case null://base64
streamFlags.push(`-an -c:v mjpeg -f image2pipe pipe:1`)
break;
}
if(e.details.custom_output){
streamFlags.push(e.details.custom_output)
}
if(e.details.stream_channels){
e.details.stream_channels.forEach(function(v,n){
streamFlags.push(createStreamChannel(e,n + config.pipeAddition,v))
})
}
}
return streamFlags.join(' ')
}
@ -716,7 +718,7 @@ module.exports = (s,config,lang) => {
}
if(audioCodec === 'no'){
outputFlags.push(`-an`)
}else if(audioCodec){
}else if(audioCodec && audioCodec !== 'auto'){
outputFlags.push(`-c:a ` + audioCodec)
}
if(outputFilters.length > 0){

View File

@ -166,8 +166,13 @@ module.exports = function(s,config,lang){
screenShot: buffer,
isStaticFile: false
})
fs.unlink(temporaryImageFile,function(){})
}else{
resolve({
screenShot: null,
isStaticFile: false
})
}
fs.unlink(temporaryImageFile,function(){})
})
}
try{
@ -483,7 +488,7 @@ module.exports = function(s,config,lang){
if(s.group[e.ke] && s.group[e.ke].rawMonitorConfigurations && s.group[e.ke].rawMonitorConfigurations[e.mid] && s.group[e.ke].rawMonitorConfigurations[e.mid].mode !== 'stop'){
var pathDir = s.dir.streams+e.ke+'/'+e.mid+'/'
const {screenShot, isStaticFile} = await s.getRawSnapshotFromMonitor(s.group[e.ke].rawMonitorConfigurations[e.mid],options)
if(screenShot && (screenShot[screenShot.length-2] === 0xFF && screenShot[screenShot.length-1] === 0xD9)){
if(screenShot){
s.tx({
f: 'monitor_snapshot',
snapshot: screenShot.toString('base64'),
@ -513,7 +518,7 @@ module.exports = function(s,config,lang){
if(s.group[e.ke] && s.group[e.ke].rawMonitorConfigurations && s.group[e.ke].rawMonitorConfigurations[e.mid] && s.group[e.ke].rawMonitorConfigurations[e.mid].mode !== 'stop'){
var pathDir = s.dir.streams+e.ke+'/'+e.mid+'/'
const {screenShot, isStaticFile} = await s.getRawSnapshotFromMonitor(s.group[e.ke].rawMonitorConfigurations[e.mid],options)
if(screenShot && (screenShot[screenShot.length-2] === 0xFF && screenShot[screenShot.length-1] === 0xD9)){
if(screenShot){
return screenShot
}else{
return await getDefaultImage()

View File

@ -54,14 +54,13 @@ module.exports = function(s,config,lang){
if(filter.discord && s.group[d.ke].discordBot && monitorConfig.details.detector_discordbot === '1' && !s.group[d.ke].activeMonitors[d.id].detector_discordbot){
var detector_discordbot_timeout
if(!monitorConfig.details.detector_discordbot_timeout||monitorConfig.details.detector_discordbot_timeout===''){
detector_discordbot_timeout = 1000*60*10;
detector_discordbot_timeout = 1000 * 60 * 10;
}else{
detector_discordbot_timeout = parseFloat(monitorConfig.details.detector_discordbot_timeout)*1000*60;
detector_discordbot_timeout = parseFloat(monitorConfig.details.detector_discordbot_timeout) * 1000 * 60;
}
//lock mailer so you don't get emailed on EVERY trigger event.
s.group[d.ke].activeMonitors[d.id].detector_discordbot = setTimeout(function(){
clearTimeout(s.group[d.ke].activeMonitors[d.id].detector_discordbot);
delete(s.group[d.ke].activeMonitors[d.id].detector_discordbot);
s.group[d.ke].activeMonitors[d.id].detector_discordbot = null
},detector_discordbot_timeout)
if(monitorConfig.details.detector_discordbot_send_video === '1'){
// change to function that captures on going video capture, waits, grabs new video file, slices portion (max for transmission) and prepares for delivery
@ -89,7 +88,7 @@ module.exports = function(s,config,lang){
const {screenShot, isStaticFile} = await s.getRawSnapshotFromMonitor(monitorConfig,{
secondsInward: monitorConfig.details.snap_seconds_inward
})
if(screenShot[screenShot.length - 2] === 0xFF && screenShot[screenShot.length - 1] === 0xD9){
if(screenShot){
d.screenshotBuffer = screenShot
sendMessage({
author: {

View File

@ -113,15 +113,13 @@ module.exports = function(s,config,lang){
r = r[0];
var detector_mail_timeout
if(!monitorConfig.details.detector_mail_timeout||monitorConfig.details.detector_mail_timeout===''){
detector_mail_timeout = 1000*60*10;
detector_mail_timeout = 1000 * 60 * 10;
}else{
detector_mail_timeout = parseFloat(monitorConfig.details.detector_mail_timeout)*1000*60;
detector_mail_timeout = parseFloat(monitorConfig.details.detector_mail_timeout) * 1000 * 60;
}
//lock mailer so you don't get emailed on EVERY trigger event.
s.group[d.ke].activeMonitors[d.id].detector_mail = setTimeout(function(){
//unlock so you can mail again.
clearTimeout(s.group[d.ke].activeMonitors[d.id].detector_mail);
delete(s.group[d.ke].activeMonitors[d.id].detector_mail);
s.group[d.ke].activeMonitors[d.id].detector_mail = null
},detector_mail_timeout);
const sendMail = function(files){
const infoRows = []
@ -183,7 +181,7 @@ module.exports = function(s,config,lang){
const {screenShot, isStaticFile} = await s.getRawSnapshotFromMonitor(monitorConfig,{
secondsInward: monitorConfig.details.snap_seconds_inward
})
d.screenshotBuffer = screenShot
if(screenShot)d.screenshotBuffer = screenShot
}
sendMail([
{

View File

@ -20,6 +20,7 @@ module.exports = function(s,config){
}
const {
knexQuery,
knexQueryPromise,
knexError,
cleanSqlWhereObject,
processSimpleWhereCondition,
@ -34,6 +35,7 @@ module.exports = function(s,config){
extender(config)
})
s.knexQuery = knexQuery
s.knexQueryPromise = knexQueryPromise
s.getDatabaseRows = getDatabaseRows
s.sqlQuery = sqlQuery
s.connectDatabase = connectDatabase

View File

@ -127,7 +127,8 @@ module.exports = function(s,config,lang,io){
var userDetails = JSON.parse(user.details)
s.group[user.ke].sizeLimit = parseFloat(userDetails.size) || 10000
s.group[user.ke].sizeLimitVideoPercent = parseFloat(userDetails.size_video_percent) || 90
s.group[user.ke].sizeLimitTimelapseFramesPercent = parseFloat(userDetails.size_timelapse_percent) || 10
s.group[user.ke].sizeLimitTimelapseFramesPercent = parseFloat(userDetails.size_timelapse_percent) || 5
s.group[user.ke].sizeLimitFileBinPercent = parseFloat(userDetails.size_filebin_percent) || 5
s.knexQuery({
action: "select",
columns: "*",

View File

@ -361,6 +361,7 @@ module.exports = function(s,config,lang){
const value = form[key]
updateQuery[key] = value
})
updateQuery.details = formDetails
s.knexQuery({
action: "update",
table: "Users",

View File

@ -11,7 +11,7 @@ module.exports = function(s,config,lang,app){
* API : Administrator : Edit Sub-Account (Account to share cameras with)
*/
app.all(config.webPaths.adminApiPrefix+':auth/accounts/:ke/edit', function (req,res){
s.auth(req.params,function(user){
s.auth(req.params,async (user) => {
var endData = {
ok : false
}
@ -25,10 +25,35 @@ module.exports = function(s,config,lang,app){
var mail = form.mail || s.getPostData(req,'mail',false)
if(form){
var keys = ['details']
form.details = s.parseJSON(form.details) || {"sub": 1, "allmonitors": "1"}
form.details.sub = 1
const updateQuery = {
details: s.stringJSON(form.details)
}
s.knexQuery({
if(form.pass && form.pass === form.password_again){
updateQuery.pass = s.createHash(form.pass)
}
if(form.mail){
const userCheck = await s.knexQueryPromise({
action: "select",
columns: "*",
table: "Users",
where: [
['mail','=',form.mail],
]
})
if(userCheck.rows[0]){
const foundUser = userCheck.rows[0]
if(foundUser.uid === form.uid){
updateQuery.mail = form.mail
}else{
endData.msg = lang['Email address is in use.']
s.closeJsonResponse(res,endData)
return
}
}
}
await s.knexQueryPromise({
action: "update",
table: "Users",
update: updateQuery,
@ -125,6 +150,35 @@ module.exports = function(s,config,lang,app){
},res,req)
})
/**
* API : Administrator : Get Sub-Account List
*/
app.get(config.webPaths.adminApiPrefix+':auth/accounts/:ke', function (req,res){
s.auth(req.params,function(user){
var endData = {
ok : false
}
if(user.details.sub){
endData.msg = user.lang['Not Permitted']
s.closeJsonResponse(res,endData)
return
}else{
endData.ok = true
s.knexQuery({
action: "select",
columns: "ke,uid,mail,details",
table: "Users",
where: [
['ke','=',req.params.ke],
['details','LIKE','%"sub"%']
]
},function(err,rows){
endData.accounts = rows
s.closeJsonResponse(res,endData)
})
}
},res,req)
})
/**
* API : Administrator : Add Sub-Account (Account to share cameras with)
*/
app.post([
@ -156,16 +210,17 @@ module.exports = function(s,config,lang,app){
},function(err,r){
if(r && r[0]){
//found one exist
endData.msg = 'Email address is in use.'
endData.msg = lang['Email address is in use.']
}else{
//create new
endData.msg = 'New Account Created'
endData.ok = true
var newId = s.gid()
var details = s.s({
sub: "1",
var details = s.s(Object.assign({
allmonitors: "1"
})
},s.parseJSON(form.details) || {
sub: "1",
}))
s.knexQuery({
action: "insert",
table: "Users",

View File

@ -171,9 +171,9 @@ module.exports = function(s,config,lang,app,io){
return false
}
switch(true){
case search(config.webPaths.admin):
return 'admin'
break;
// case search(config.webPaths.admin):
// return 'admin'
// break;
case search(config.webPaths.super):
return 'super'
break;
@ -321,53 +321,6 @@ module.exports = function(s,config,lang,app,io){
})
break;
case'admin':
if(!r.details.sub){
s.knexQuery({
action: "select",
columns: "uid,mail,details",
table: "Users",
where: [
['ke','=',r.ke],
['details','LIKE','%"sub"%'],
]
},(err,rr) => {
s.knexQuery({
action: "select",
columns: "*",
table: "Monitors",
where: [
['ke','=',r.ke],
]
},(err,rrr) => {
renderPage(config.renderPaths.admin,{
config: s.getConfigWithBranding(req.hostname),
$user: response,
$subs: rr,
$mons: rrr,
lang: r.lang,
define: s.getDefinitonFile(r.details.lang),
customAutoLoad: s.customAutoLoadTree
})
})
})
}else{
//not admin user
var chosenRender = 'home'
if(r.details.landing_page && r.details.landing_page !== '' && config.renderPaths[r.details.landing_page]){
chosenRender = r.details.landing_page
}
renderPage(config.renderPaths[chosenRender],{
$user:response,
config: s.getConfigWithBranding(req.hostname),
lang:r.lang,
define:s.getDefinitonFile(r.details.lang),
addStorage:s.dir.addStorage,
fs:fs,
__dirname:s.mainDirectory,
customAutoLoad: s.customAutoLoadTree
});
}
break;
default:
var chosenRender = 'home'
if(r.details.sub && r.details.landing_page && r.details.landing_page !== '' && config.renderPaths[r.details.landing_page]){

View File

@ -1,5 +1,6 @@
#!/bin/bash
DIR=`dirname $0`
DIR=$(dirname $0)
rm -rf $DIR/node_modules
if [ -x "$(command -v apt)" ]; then
sudo apt update -y
fi
@ -94,9 +95,6 @@ echo "-----------------------------------"
echo "Adding Random Plugin Key to Main Configuration"
node $DIR/../../tools/modifyConfigurationForPlugin.js face key=$(head -c 64 < /dev/urandom | sha256sum | awk '{print substr($1,1,60)}') tfjsBuild=$tfjsBuildVal
echo "-----------------------------------"
echo "Updating Node Package Manager"
sudo npm install npm -g --unsafe-perm
echo "-----------------------------------"
echo "Getting node-gyp to build C++ modules"
if [ ! -x "$(command -v node-gyp)" ]; then
# Check if Ubuntu
@ -110,39 +108,35 @@ if [ ! -x "$(command -v node-gyp)" ]; then
sudo yum install gcc-c++ cairo-devel libjpeg-turbo-devel pango-devel giflib-devel -y
fi
fi
sudo npm install --unsafe-perm
sudo npm install node-gyp -g --unsafe-perm --force
echo "-----------------------------------"
npm uninstall @tensorflow/tfjs-node-gpu --unsafe-perm
npm uninstall @tensorflow/tfjs-node --unsafe-perm
echo "Getting C++ module : @tensorflow/tfjs-node@0.1.21"
echo "https://github.com/tensorflow/tfjs-node"
npm install @tensorflow/tfjs-core@1.7.3 --unsafe-perm --force
npm install @tensorflow/tfjs-converter@1.7.3 --unsafe-perm --force
npm install @tensorflow/tfjs-layers@1.7.3 --unsafe-perm --force
echo "Getting C++ module : face-api.js"
echo "https://github.com/justadudewhohacks/face-api.js"
sudo npm install --unsafe-perm --force
# echo "Getting C++ module : @tensorflow/tfjs-node@0.1.21"
# echo "https://github.com/tensorflow/tfjs-node"
# npm install @tensorflow/tfjs-converter@1.7.4 @tensorflow/tfjs-layers@1.7.4 --unsafe-perm
if [ "$INSTALL_WITH_GPU" = "1" ]; then
echo "GPU version of tjfs : https://github.com/tensorflow/tfjs-node-gpu"
else
echo "CPU version of tjfs : https://github.com/tensorflow/tfjs-node"
fi
sudo npm install @tensorflow/tfjs-node$TFJS_SUFFIX@1.7.3 --unsafe-perm --force
npm install @tensorflow/tfjs-node$TFJS_SUFFIX --unsafe-perm
if [ "$INSTALL_FOR_ARM" = "1" ]; then
cd node_modules/@tensorflow/tfjs-node$TFJS_SUFFIX
BINARY_LOCATION="node_modules/@tensorflow/tfjs-node$TFJS_SUFFIX/scripts/custom-binary.json"
if [ "$INSTALL_FOR_ARM64" = "1" ]; then
echo "{
\"tf-lib\": \"https://cdn.shinobi.video/binaries/libtensorflow-gpu-linux-arm64-1.15.0.tar.gz\"
}" > scripts/custom-binary.json
}" > $BINARY_LOCATION
else
echo "{
\"tf-lib\": \"https://cdn.shinobi.video/binaries/libtensorflow-cpu-linux-arm-1.15.0.tar.gz\"
}" > scripts/custom-binary.json
}" > $BINARY_LOCATION
fi
npm install --unsafe-perm
cd ../../..
npm rebuild @tensorflow/tfjs-node$TFJS_SUFFIX --build-addon-from-source --unsafe-perm
fi
sudo npm audit fix --force
rm -rf $DIR/node_modules/@tensorflow/tfjs-backend-cpu
rm -rf $DIR/node_modules/@tensorflow/tfjs-backend-webgl
echo "-----------------------------------"
echo "Start the plugin with pm2 like so :"
echo "pm2 start shinobi-face.js"

1682
plugins/face/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,10 @@
"description": "Face Recognition plugin for Shinobi that uses C++ functions for detection. Based on @justadudewhohacks' face-api.js.",
"main": "shinobi-face.js",
"dependencies": {
"@tensorflow/tfjs-converter": "^1.7.4",
"@tensorflow/tfjs-core": "^1.7.4",
"@tensorflow/tfjs-layers": "^1.7.4",
"@tensorflow/tfjs-node": "^1.7.4",
"socket.io-client": "^1.7.4",
"express": "^4.16.2",
"moment": "^2.19.2",

View File

@ -1,21 +1,14 @@
#!/bin/bash
DIR=`dirname $0`
echo "Installing coral dependencies..."
echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | sudo tee /etc/apt/sources.list.d/coral-edgetpu.list
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
sudo apt-get update
sudo apt-get install libedgetpu1-max
sudo apt-get install libatlas-base-dev
echo "Coral dependencies installed."
echo "Getting coral object detection models..."
mkdir -p models
wget "https://github.com/google-coral/edgetpu/raw/master/test_data/ssd_mobilenet_v2_coco_quant_postprocess_edgetpu.tflite"
wget "https://cdn.shinobi.video/binaries/tensorflow/coral/models-2021-01-26/ssd_mobilenet_v2_coco_quant_postprocess_edgetpu.tflite"
mv ssd_mobilenet_v2_coco_quant_postprocess_edgetpu.tflite models/
wget "https://dl.google.com/coral/canned_models/coco_labels.txt"
wget "https://cdn.shinobi.video/binaries/tensorflow/coral/models-2021-01-26/plugins_tensorflow-coral_models_coco_labels.txt"
mv plugins_tensorflow-coral_models_coco_labels.txt coco_labels.txt
mv coco_labels.txt models/
echo "Models downloaded."
npm install yarn -g --unsafe-perm --force
npm install --unsafe-perm
if [ ! -e "./conf.json" ]; then

View File

@ -1,32 +1,10 @@
# TensorFlowCoral.js
**Ubuntu and CentOS only**
Follow the Official Coral PCI install instructions carefully. https://coral.ai/docs/m2/get-started/#2-install-the-pcie-driver-and-edge-tpu-runtime
Go to the Shinobi directory. **/home/Shinobi** is the default directory.
# Install
```
cd /home/Shinobi/plugins/tensorflow
```
Install TensorFlows python version first:
https://www.tensorflow.org/lite/guide/python
Make sure that you are downloading the correct file for your system architecture and python version.
Install other python dependencies
```
pip install pillow
pip install numpy
```
Copy the config file.
```
sh INSTALL.sh
```
IF YOU DON'T HAVE INSTALLED CORAL DEPENDENCIES BEFORE, YOU NEED TO PLUG OUT AND THEN PLUG IN YOUR CORAL USB ACCELERATOR BEFORE USING THIS PLUGIN!
Start the plugin.
Run INSTALL.sh
```
pm2 start shinobi-tensorflow-coral.js

View File

@ -12,6 +12,7 @@
var fs = require('fs');
var config = require('./conf.json')
var dotenv = require('dotenv').config()
const currentDirectory = process && process.cwd ? process.cwd() + '/' : __dirname + '/'
var s
const {
workerData
@ -49,7 +50,7 @@ var child = null
function respawn() {
console.log("respawned python",(new Date()))
const theChild = spawn('python3', ['-u', 'detect_image.py']);
const theChild = spawn('python3', ['-u', currentDirectory + 'detect_image.py']);
var lastStatusLog = new Date();

File diff suppressed because one or more lines are too long

View File

@ -943,7 +943,7 @@ if (typeof jQuery === 'undefined') {
if (this.options.remote) {
this.$element
.find('.modal-content')
.load(this.options.remote, $.proxy(function () {
.on('load',this.options.remote, $.proxy(function () {
this.$element.trigger('loaded.bs.modal')
}, this))
}

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,7 @@ $.apM.md.change($.ccio.form.details).first().change();
$.apM.f.submit(function(e){
e.preventDefault();e.e=$(this),e.s=e.e.serializeObject();
e.er=[];
if(!e.s.ip||e.s.ip.length<7){e.er.push('Enter atleast one IP')}
if(!e.s.ip||e.s.ip.length<7){e.er.push('Enter at least one IP')}
if(e.er.length>0){$.apM.e.find('.msg').html(e.er.join('<br>'));return;}
$.each(e.s,function(n,v){e.s[n]=v.trim()})
// e.s = {

View File

@ -420,7 +420,7 @@ $(document).ready(function(e){
}
break;
case'trigger-event':
$.getJSON(getApiPrefix() + '/motion/'+e.ke+'/'+e.mid+'/?data={"plug":"camera1","name":"stairs","reason":"motion","confidence":100}',function(d){
$.getJSON(getApiPrefix() + '/motion/'+e.ke+'/'+e.mid+'/?data={"plug":"manual_trigger","name":"Manual Trigger","reason":"Manual","confidence":100}',function(d){
$.ccio.log(d)
})
break;

View File

@ -336,8 +336,8 @@ $.ccio.init=function(x,d,user,k){
case'drawMatrices':
d.height=d.stream.height()
d.width=d.stream.width()
if(!d.details.imgWidth && d.monitorDetails.detector_scale_x===''){d.monitorDetails.detector_scale_x=320}
if(!d.details.imgHeight && d.monitorDetails.detector_scale_y===''){d.monitorDetails.detector_scale_y=240}
if(!d.details.imgWidth && d.monitorDetails.detector_scale_x===''){d.monitorDetails.detector_scale_x=640}
if(!d.details.imgHeight && d.monitorDetails.detector_scale_y===''){d.monitorDetails.detector_scale_y=480}
d.widthRatio=d.width/d.details.imgWidth || d.monitorDetails.detector_scale_x
d.heightRatio=d.height/d.details.imgHeight || d.monitorDetails.detector_scale_y
@ -426,9 +426,9 @@ $.ccio.init=function(x,d,user,k){
k.run=function(){
k.e.attr('src',$.ccio.init('location',user)+user.auth_token+'/jpeg/'+d.ke+'/'+d.mid+'/s.jpg?time='+(new Date()).getTime())
}
k.e.load(function(){
k.e.on('load',function(){
$.ccio.mon[d.ke+d.mid+user.auth_token].jpegInterval=setTimeout(k.run,1000/k.jpegInterval);
}).error(function(){
}).on('error',function(){
$.ccio.mon[d.ke+d.mid+user.auth_token].jpegInterval=setTimeout(k.run,1000/k.jpegInterval);
})
k.run()

View File

@ -1,4 +1,5 @@
$(document).ready(function(e){
var selectedApiKey = `${$user.auth_token}`
var getUrlPieces = function(url){
var el = document.createElement('a');
el.href = url
@ -13,41 +14,70 @@ $(document).ready(function(e){
// el.search // ?filter=a
}
//multi monitor manager
$.multimon={e:$('#multi_mon')};
$.multimon.table=$.multimon.e.find('.tableData tbody');
$.multimon.f=$.multimon.e.find('form');
$.multimon.f.on('change','#multimon_select_all',function(e){
e.e=$(this);
e.p=e.e.prop('checked')
e.a=$.multimon.f.find('input[type=checkbox][name]')
if(e.p===true){
e.a.prop('checked',true)
var theWindow = $('#multi_mon')
var apiKeySelector = $('#multi_mon_api_key_selector')
var theTable = theWindow.find('.tableData tbody');
var theForm = theWindow.find('form');
var drawApiKeyList = function(){
$.get($.ccio.init('location',$user)+$user.auth_token+'/api/'+$user.ke+'/list',function(d){
var html = ''
$.each(d.keys || [],function(n,key){
html += `<option value="${key.code}">${key.code}</option>`
})
apiKeySelector.find('optgroup').html(html)
})
}
var drawTable = function(){
var tmp=''
$.each($.ccio.mon,function(n,v){
var streamURL = $.ccio.init('streamURL',v).replace($user.auth_token,selectedApiKey)
if(streamURL!=='Websocket'&&v.mode!==('idle'&&'stop')){
streamURL='<a target="_blank" href="'+streamURL+'">'+streamURL+'</a>'
}
var img = $('#left_menu [mid="'+v.mid+'"] [monitor="watch"]').attr('src')
tmp+='<tr mid="'+v.mid+'" ke="'+v.ke+'" auth="'+selectedApiKey+'">'
tmp+='<td><div class="checkbox"><input id="multimonCheck_'+v.ke+v.mid+selectedApiKey+'" type="checkbox" name="'+v.ke+v.mid+selectedApiKey+'" value="1"><label for="multimonCheck_'+v.ke+v.mid+selectedApiKey+'"></label></div></td>'
tmp+='<td><a monitor="watch"><img class="small-square-img" src="'+img+'"></a></td><td>'+v.name+'<br><small>'+v.mid+'</small></td><td class="monitor_status">'+v.status+'</td><td><small>'+streamURL+'</small></td>'
//buttons
tmp+='<td class="text-right"><a title="'+lang.Pop+'" monitor="pop" class="btn btn-primary"><i class="fa fa-external-link"></i></a> <a title="'+lang.Calendar+'" monitor="calendar" class="btn btn-default"><i class="fa fa-calendar"></i></a> <a title="'+lang['Power Viewer']+'" class="btn btn-default" monitor="powerview"><i class="fa fa-map-marker"></i></a> <a title="'+lang['Time-lapse']+'" class="btn btn-default" monitor="timelapse"><i class="fa fa-angle-double-right"></i></a> <a title="'+lang['Videos List']+'" monitor="videos_table" class="btn btn-default"><i class="fa fa-film"></i></a> <a title="'+lang['Monitor Settings']+'" class="btn btn-default" monitor="edit"><i class="fa fa-wrench"></i></a></td>'
tmp+='</tr>'
})
theTable.html(tmp)
}
var getSelectedMonitors = function(unclean){
var arr = [];
if(unclean === true){
var monitors = $.ccio.mon
}else{
e.a.prop('checked',false)
var monitors = $.ccio.init('cleanMons','object')
}
$.each(theForm.serializeObject(),function(n,v){
console.log(monitors,n)
arr.push(monitors[n])
})
return arr;
}
theForm.on('change','#multimon_select_all',function(e){
var el = $(this);
var isChecked = el.prop('checked')
var nameField = theForm.find('input[type=checkbox][name]')
if(isChecked === true){
nameField.prop('checked',true)
}else{
nameField.prop('checked',false)
}
})
$.multimon.e.find('.import_config').click(function(){
var e={};e.e=$(this);e.mid=e.e.parents('[mid]').attr('mid');
$.confirm.e.modal('show');
$.confirm.title.text(lang['Import Monitor Configuration'])
e.html=lang.ImportMultiMonitorConfigurationText+'<div style="margin-top:15px"><div class="form-group"><textarea placeholder="'+lang['Paste JSON here.']+'" class="form-control"></textarea></div><label class="upload_file btn btn-primary btn-block">'+lang['Upload File']+'<input class="upload" type=file name="files[]"></label></div>';
$.confirm.body.html(e.html)
$.confirm.e.find('.upload').change(function(e){
var files = e.target.files; // FileList object
f = files[0];
var reader = new FileReader();
reader.onload = function(ee) {
$.confirm.e.find('textarea').val(ee.target.result);
}
reader.readAsText(f);
});
$.confirm.click({title:lang['Import'],class:'btn-primary'},function(){
// setTimeout(function(){
// $.confirm.e.modal('show');
// },1000)
// $.confirm.title.text(lang['Are you sure?'])
// $.confirm.body.html(lang.ImportMultiMonitorConfigurationText)
// $.confirm.click({title:'Save Set',class:'btn-danger'},function(){
theWindow.find('.import_config').click(function(){
var el = $(this);
var html = lang.ImportMultiMonitorConfigurationText+'<div style="margin-top:15px"><div class="form-group"><textarea placeholder="'+lang['Paste JSON here.']+'" class="form-control"></textarea></div><label class="upload_file btn btn-primary btn-block">'+lang['Upload File']+'<input class="upload" type=file name="files[]"></label></div>';
$.confirm.create({
title: lang['Import Monitor Configuration'],
body: html,
clickOptions: {
title: lang['Import'],
class: 'btn-primary'
},
clickCallback: function(){
try{
var postMonitor = function(v){
$.post($.ccio.init('location',$user)+$user.auth_token+'/configureMonitor/'+$user.ke+'/'+v.mid,{data:JSON.stringify(v,null,3)},function(d){
@ -149,31 +179,28 @@ $.multimon.e.find('.import_config').click(function(){
$.ccio.init('note',{title:lang['Invalid JSON'],text:lang.InvalidJSONText,type:'error'})
}
}
// });
}
})
$.confirm.e.find('.upload').change(function(e){
var files = e.target.files; // FileList object
f = files[0];
var reader = new FileReader();
reader.onload = function(ee) {
$.confirm.e.find('textarea').val(ee.target.result);
}
reader.readAsText(f);
});
})
$.multimon.getSelectedMonitors = function(unclean){
var arr=[];
if(unclean === true){
var monitors = $.ccio.mon
}else{
var monitors = $.ccio.init('cleanMons','object')
}
$.each($.multimon.f.serializeObject(),function(n,v){
arr.push(monitors[n])
})
return arr;
}
$.multimon.e.find('.delete').click(function(){
var arr=$.multimon.getSelectedMonitors(true);
theWindow.find('.delete').click(function(){
var arr = getSelectedMonitors(true);
if(arr.length===0){
$.ccio.init('note',{title:lang['No Monitors Selected'],text:lang['Select atleast one monitor to delete'],type:'error'});
return
}
$.confirm.e.modal('show');
$.confirm.title.text(lang['Delete']+' '+lang['Monitors'])
e.html='<p>'+lang.DeleteMonitorsText+'</p>';
$.confirm.body.html(e.html)
var html = '<p>'+lang.DeleteMonitorsText+'</p>';
$.confirm.body.html(html)
$.confirm.click([
{
title:lang['Delete']+' '+lang['Monitors'],
@ -199,45 +226,28 @@ $.multimon.e.find('.delete').click(function(){
}
]);
})
//$.multimon.e.find('.edit_all').click(function(){
// var arr=$.multimon.getSelectedMonitors();
// var arrObject={}
// if(arr.length===0){
// $.ccio.init('note',{title:lang['No Monitors Selected'],text:lang['Select atleast one monitor to delete'],type:'error'});
// return
// }
// $.multimonedit.selectedList = arr;
// $.multimonedit.e.modal('show')
//})
$.multimon.e.find('.save_config').click(function(){
var e={};e.e=$(this);
var arr=$.multimon.getSelectedMonitors();
theWindow.find('.save_config').click(function(){
var el = $(this);
var arr = getSelectedMonitors();
if(arr.length===0){
$.ccio.init('note',{title:lang['No Monitors Selected'],text:lang['Select atleast one monitor to delete'],type:'error'});
return
}
e.dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(arr));
var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(arr));
$('#temp').html('<a></a>')
.find('a')
.attr('href',e.dataStr)
.attr('href',dataStr)
.attr('download','Shinobi_Monitors_'+(new Date())+'.json')
[0].click()
})
$.multimon.e.on('shown.bs.modal',function() {
var tmp=''
$.each($.ccio.mon,function(n,v){
var streamURL = $.ccio.init('streamURL',v)
if(streamURL!=='Websocket'&&v.mode!==('idle'&&'stop')){
streamURL='<a target="_blank" href="'+streamURL+'">'+streamURL+'</a>'
}
var img = $('#left_menu [mid="'+v.mid+'"][auth="'+v.user.auth_token+'"] [monitor="watch"]').attr('src')
tmp+='<tr mid="'+v.mid+'" ke="'+v.ke+'" auth="'+v.user.auth_token+'">'
tmp+='<td><div class="checkbox"><input id="multimonCheck_'+v.ke+v.mid+v.user.auth_token+'" type="checkbox" name="'+v.ke+v.mid+v.user.auth_token+'" value="1"><label for="multimonCheck_'+v.ke+v.mid+v.user.auth_token+'"></label></div></td>'
tmp+='<td><a monitor="watch"><img class="small-square-img" src="'+img+'"></a></td><td>'+v.name+'<br><small>'+v.mid+'</small></td><td class="monitor_status">'+v.status+'</td><td>'+streamURL+'</td>'
//buttons
tmp+='<td class="text-right"><a title="'+lang.Pop+'" monitor="pop" class="btn btn-primary"><i class="fa fa-external-link"></i></a> <a title="'+lang.Calendar+'" monitor="calendar" class="btn btn-default"><i class="fa fa-calendar"></i></a> <a title="'+lang['Power Viewer']+'" class="btn btn-default" monitor="powerview"><i class="fa fa-map-marker"></i></a> <a title="'+lang['Time-lapse']+'" class="btn btn-default" monitor="timelapse"><i class="fa fa-angle-double-right"></i></a> <a title="'+lang['Videos List']+'" monitor="videos_table" class="btn btn-default"><i class="fa fa-film"></i></a> <a title="'+lang['Monitor Settings']+'" class="btn btn-default" monitor="edit"><i class="fa fa-wrench"></i></a></td>'
tmp+='</tr>'
})
$.multimon.table.html(tmp)
theWindow.on('shown.bs.modal',function() {
drawTable()
drawApiKeyList()
})
apiKeySelector.change(function(){
var value = $(this).val()
console.log(value)
selectedApiKey = `${value}`
drawTable()
})
})

View File

@ -0,0 +1,299 @@
$(document).ready(function(){
var apiPrefix = getAdminApiPrefix()
var theWindow = $('#subAccountManager');
var accountTable = $('#subAccountsList tbody');
var theWindowForm = $('#monSectionAccountInformation');
var permissionsSection = $('#monSectionAccountPrivileges');
var permissionsMonitorSection = $('#sub_accounts_permissions');
var submitButtons = theWindow.find('.submit-form')
var loadedSubAccounts = {}
var clearTable = function(){
accountTable.empty()
loadedSubAccounts = {}
}
var getSubAccounts = function(){
$.get(`${apiPrefix}accounts/${$user.ke}`,function(data){
clearTable()
$.each(data.accounts,function(n,account){
loadedSubAccounts[account.uid] = account;
drawSubAccountRow(account)
})
})
}
var deleteSubAccount = function(email,uid){
$.confirm.create({
title: lang.deleteSubAccount,
body: lang.deleteSubAccountText + '\n' + email,
clickOptions: {
class: 'btn-danger',
title: lang.Delete,
},
clickCallback: function(){
$.post(apiPrefix+'accounts/'+$user.ke+'/delete',{
uid: uid,
mail: email
},function(data){
var notifyTitle = lang.accountDeleted
var notifyText = lang.accountDeletedText + '\n' + email
var notifyColor = 'info'
if(data.ok){
loadedSubAccounts[uid] = null;
accountTable.find('tr[uid="' + uid + '"]').remove()
}else{
notifyTitle = lang.accountActionFailed
notifyText = lang.contactAdmin
notifyColor = 'warning'
}
new PNotify({
title : notifyTitle,
text : notifyText,
type : notifyColor
})
})
}
})
}
var addSubAccount = function(newAccount,callback){
$.post(apiPrefix+'accounts/'+$user.ke+'/register',{
data: JSON.stringify(newAccount)
},function(data){
var notifyTitle
var notifyText
var notifyColor
if(!data.ok && data.msg){
notifyTitle = lang.accountActionFailed
notifyText = data.msg
notifyColor = 'warning'
}
if(data.user){
notifyTitle = lang.accountAdded
notifyText = lang.accountAddedText + '\n' + data.user.mail
notifyColor = 'success'
if(data.user){
var account = data.user
loadedSubAccounts[account.uid] = account;
drawSubAccountRow(account)
theWindowForm.find('[name="uid"]').val(account.uid)
setSubmitButtonState(lang['Save Changes'],'check')
}else{
notifyTitle = lang.accountActionFailed
notifyText = lang.contactAdmin
notifyColor = 'warning'
}
}
new PNotify({
title : notifyTitle,
text : notifyText,
type : notifyColor
})
callback(data)
});
}
var editSubaccount = function(uid,form,callback){
var account = loadedSubAccounts[uid]
$.post(apiPrefix+'accounts/'+$user.ke+'/edit',{
uid: uid,
mail: form.mail,
data: form
},function(data){
if(data.ok){
$.each(form,function(n,v){
account[n] = v
});
accountTable.find(`[uid="${account.uid}"] .mail`).text(form.mail)
new PNotify({
title : 'Account Edited',
text : '<b>' + account.mail + '</b> has been updated.',
type : 'success'
})
}else{
new PNotify({
title : 'Failed to Add Account',
text : data.msg,
type : 'error'
})
}
callback(data)
})
}
var drawSubAccountRow = function(account){
var html = `<tr uid="${account.uid}">
<td>
<span class="badge btn-primary mail">${account.mail}</span>
</td>
<td>
<code class="uid">${account.uid}</code>
</td>
<td class="text-right">
<a class="permission btn btn-sm btn-primary"><i class="fa fa-lock"></i></a>
</td>
<td class="text-right">
<a class="delete btn btn-sm btn-danger"><i class="fa fa-trash-o"></i></a>
</td>
</tr>`;
accountTable.prepend(html)
}
var permissionTypeNames = [
{
name: 'monitors',
label: lang['Can View Monitor'],
},
{
name: 'monitor_edit',
label: lang['Can Edit Monitor'],
},
{
name: 'video_view',
label: lang['Can View Videos and Events'],
},
{
name: 'video_delete',
label: lang['Can Delete Videos and Events'],
},
];
var drawSelectableForPermissionForm = function(){
var html = ''
$.each($.ccio.mon,function(n,monitor){
html += `<div class="form-group permission-view">`
html += `<div><label>${monitor.name} (${monitor.mid})</label></div>`
html += `<div><select class="form-control" multiple monitor="${monitor.mid}">`
$.each(permissionTypeNames,function(n,permission){
html += `<option value="${permission.name}">${permission.label}</option>`
})
html += `</select></div>`
html += `</div>`
})
permissionsMonitorSection.html(html)
}
var setPermissionSelectionsToFields = function(uid){
var account = loadedSubAccounts[uid]
var details = $.parseJSON(account.details)
// load values to Account Information : email, password, etc.
$.each(account,function(n,v){
theWindowForm.find('[name="'+n+'"]').val(v)
})
// load base privileges
permissionsSection.find('[detail]').each(function(n,v){
var el = $(v)
var key = el.attr('detail')
var defaultValue = el.attr('data-default')
el.val(details[key] || defaultValue)
})
permissionsSection.find('[detail="allmonitors"]').change()
// load montior specific privileges
$.each($.ccio.mon,function(m,monitor){
$.each(permissionTypeNames,function(m,permission){
if((details[permission.name] || []).indexOf(monitor.mid) > -1){
permissionsSection.find(`[monitor="${monitor.mid}"] option[value="${permission.name}"]`).attr("selected", "selected")
}
})
})
}
var openSubAccountEditor = function(uid){
var account = loadedSubAccounts[uid]
drawSelectableForPermissionForm()
setPermissionSelectionsToFields(uid)
theWindowForm.find('[name="pass"],[name="password_again"]').val('')
permissionsSection.show()
}
var writePermissionsFromFieldsToString = function(){
var foundSelected = {}
var detailsElement = theWindowForm.find('[name="details"]')
var details = JSON.parse(detailsElement.val())
details = details ? details : {sub: 1, allmonitors: "1"}
// base privileges
permissionsSection.find('[detail]').each(function(n,v){
var el = $(v)
details[el.attr('detail')] = el.val()
})
// monitor specific privileges
permissionsSection.find('.permission-view select').each(function(n,v){
var el = $(v)
var monitorId = el.attr('monitor')
var value = el.val()
$.each(value,function(n,permissionNameSelected){
if(!foundSelected[permissionNameSelected])foundSelected[permissionNameSelected] = []
foundSelected[permissionNameSelected].push(monitorId)
})
})
details = Object.assign(details,foundSelected)
detailsElement.val(JSON.stringify(details))
}
var getCompleteForm = function(){
writePermissionsFromFieldsToString()
return theWindowForm.serializeObject()
}
var setSubmitButtonState = function(text,icon){
submitButtons.html(`<i class="fa fa-${icon}"></i> ${text}`)
}
//add new
submitButtons.click(function(){
theWindowForm.submit()
})
theWindowForm.submit(function(e){
e.preventDefault();
var formValues = getCompleteForm()
var uid = formValues.uid
console.log(formValues)
if(formValues.uid){
console.log('edit')
editSubaccount(uid,formValues,function(data){
console.log(data)
})
}else{
addSubAccount(formValues,function(data){
console.log(data)
})
}
return false;
});
//sub simple lister
theWindow.on('click','.delete',function(e){
var el = $(this).parents('tr')
var subAccountEmail = el.find('.mail').text()
var subAccountUid = el.attr('uid')
deleteSubAccount(subAccountEmail,subAccountUid)
})
theWindow.on('click','.permission',function(e){
var el = $(this).parents('tr')
var uid = el.attr('uid')
openSubAccountEditor(uid)
setSubmitButtonState(lang['Save Changes'],'check')
})
theWindow.on('click','.reset-form',function(e){
permissionsSection.find('[detail]').each(function(n,v){
var el = $(v)
var key = el.attr('detail')
var defaultValue = el.attr('data-default')
el.val(defaultValue)
})
drawSelectableForPermissionForm()
setSubmitButtonState(lang['Add New'],'plus')
theWindowForm.find('[name="pass"],[name="password_again"]').val('')
})
permissionsSection.on('click','[check]',function(e){
$(this).parents('.form-group-group').find('select').val($(this).attr('check')).first().change()
})
// permissionsSection.on('change','[monitor]',function(e){
// writePermissionsFromFieldsToString()
// });
theWindow.on('shown.bs.modal',function() {
getSubAccounts()
if(theWindowForm.find('[name="uid"]').val() === '')drawSelectableForPermissionForm()
})
theWindow.on('hidden.bs.modal',function() {
clearTable()
})
permissionsSection.on('change','[detail="allmonitors"]',function(e){
var value = $(this).val()
var el = $('.permission-view')
if(value === '1'){
el.hide();
}else{
el.show()
}
})
// TEST
window.getCompleteForm = getCompleteForm
})

View File

@ -82,7 +82,7 @@
</div>
</div>
<div class="col-md-6">
<table class="table table-striped" id="api_list"></table>
<table class="table table-striped" id="api_list" style="color: #fff;"></table>
</div>
<input type="hidden" name="details">
</div>

View File

@ -26,7 +26,13 @@
</form>
<div class="modal-footer">
<button type="button" class="btn btn-default pull-left" data-dismiss="modal"><%-lang.Close%></button>
<div>
<div style="display:inline-block;margin-right:5px;font-family: monospace">
<div><select class="form-control btn-default" id="multi_mon_api_key_selector">
<option value="<%- $user.auth_token %>" selected><%-lang['Session Key']%></option>
<optgroup label="<%- lang['API Keys'] %>"></optgroup>
</select></div>
</div>
<div style="display:inline-block">
<a class="btn btn-danger delete"><%-lang['Delete']%></a>
<a class="btn btn-primary save_config"><%-lang['Export']%></a>
<a class="btn btn-default import_config"><%-lang['Import']%></a>

View File

@ -68,9 +68,11 @@
if(monitorSettings.attribute){
attributes.push(monitorSettings.attribute)
}
if(monitorSettings.id){
attributes.push(`id="${monitorSettings.id}"`)
if(!monitorSettings.id){
var userSettingsId = monitorSettings.name.replace(/[^a-zA-Z ]/g, '').replace(/[^a-zA-Z ]/g, '').replace(/ /g, '')
monitorSettings.id = userSettingsId
}
attributes.push(`id="${monitorSettings.id}"`)
if(monitorSettings.color){
sectionClass.push(monitorSettings.color)
}

View File

@ -0,0 +1,290 @@
<!--Sub Account Manager Window-->
<div class="modal fade" id="subAccountManager" tabindex="-1" role="dialog" aria-labelledby="subAccountManagerLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="subAccountManagerLabel"><i class="fa fa-group"></i> &nbsp; <%-lang.subAccountManager%></h4>
</div>
<div class="modal-body">
<%
var buildOptions = function(field,possiblities){
if(!field)console.error('field',field)
var fieldElement = ''
possiblities.forEach(function(option){
if(option.optgroup){
fieldElement += '<optgroup label="' + option.name + '">'
fieldElement += buildOptions(field,option.optgroup)
fieldElement += '</optgroup>'
}else{
var selected = ''
if(option.value === field.default){
selected = 'selected'
}
fieldElement += '<option value="' + option.value + '" ' + selected + '>' + option.name + '</option>'
}
})
return fieldElement
}
var drawBlock = function(monitorSettings){
if(monitorSettings.evaluation && !eval(monitorSettings.evaluation)){
return
}
var attributes = []
var styles = []
var sectionClass = []
var headerTitle = monitorSettings.headerTitle || lang[monitorSettings.name] || monitorSettings.name
if(monitorSettings.hidden === true){
styles.push('display:none')
}
if(monitorSettings.style){
styles.push(monitorSettings.style)
}
if(monitorSettings.isSection === true){
attributes.push('section')
}
if(monitorSettings.attribute){
attributes.push(monitorSettings.attribute)
}
if(!monitorSettings.id){
var userSettingsId = monitorSettings.name.replace(/[^a-zA-Z ]/g, '').replace(/[^a-zA-Z ]/g, '').replace(/ /g, '')
monitorSettings.id = userSettingsId
}
attributes.push(`id="${monitorSettings.id}"`)
if(monitorSettings.color){
sectionClass.push(monitorSettings.color)
}
if(monitorSettings['section-class']){
sectionClass.push(monitorSettings['section-class'])
}
if(monitorSettings.isAdvanced){ %>
<div class="h_us_input h_us_advanced" style="display:none">
<% }
if(monitorSettings['section-pre-pre-class']){ %>
<div class="<%- monitorSettings['section-pre-pre-class'] %>">
<% }
if(monitorSettings['section-pre-class']){ %>
<div class="<%- monitorSettings['section-pre-class'] %>">
<% }
%>
<<%- monitorSettings.isForm ? 'form' : 'div' %> <%- attributes.join(' ') %> style="<%- styles.join(';') %>" class="form-group-group <%- sectionClass.join(' ') %>">
<h4 class="monitor-section-header <%- monitorSettings.headerClass %>"><%- headerTitle %>
<% if(monitorSettings.headerButtons){ %>
<div class="pull-right">
<% monitorSettings.headerButtons.forEach(function(button){ %>
<a class="btn btn-success btn-xs <%- button.class %>">
<% if(button.icon){ %><i class="fa fa-<%- button.icon %>"></i><% } %>
<% if(button.text){ %><%- button.text %><% } %>
</a>
<% }) %>
</div>
<% } %>
</h4>
<div class="box-wrapper">
<% if(monitorSettings['input-mapping']){ %>
<div class="form-group-group forestgreen" style="display:none" input-mapping="<%- monitorSettings['input-mapping'] %>">
<h4><%-lang['Input Feed']%>
<div class="pull-right">
<a class="btn btn-success btn-xs add_map_row"><i class="fa fa-plus-square-o"></i></a>
</div>
</h4>
<div class="choices"></div>
</div>
<% } %>
<% if(monitorSettings.blockquote){ %>
<blockquote class="<%- monitorSettings.blockquoteClass || '' %>">
<%- monitorSettings.blockquote %>
</blockquote>
<% } %>
<% if(monitorSettings.blocks){
monitorSettings.blocks.forEach(function(settingsBlock){
drawBlock(settingsBlock)
})
}
if(monitorSettings.info){
monitorSettings.info.forEach(function(field){
if(field.isFormGroupGroup === true){
drawBlock(field)
}else{
if(field.notForSubAccount === true){
var notForSubAccount = '!details.sub'
if(!field.evaluation){
field.evaluation = notForSubAccount
}else{
field.evaluation += ' && ' + notForSubAccount
}
}
if(field.evaluation && !eval(field.evaluation)){
return
}
var hidden = ''
if(field.hidden === true){
hidden = 'style="display:none"'
}
var fieldClass = []
var attributes = []
if(field.name && field.name.indexOf('detail=') > -1){
attributes.push(field.name)
}else if(field.name){
attributes.push("name=" + field.name)
}
if(field.placeholder || field.default){
attributes.push(`placeholder="${field.placeholder || field.default}"`)
}else if(field.example){
attributes.push(`placeholder="Example : ${field.example}"`)
}
if(field.default){
attributes.push(`data-default="${field.default}"`)
}
if(field.attribute){
attributes.push(field.attribute)
}
if(field.selector){
attributes.push(`selector="${field.selector}"`)
}
if(field.id){
attributes.push(`id="${field.id}"`)
}
if(field.class){
fieldClass.push(`${field.class}`)
}
var possiblities = field.possible || []
var fieldType = field.fieldType || 'text'
var fieldElement = ''
var preFill = field.preFill || ''
switch(fieldType){
case'btn':
baseElement = field.forForm ? 'button' : 'a'
fieldElement = `<${baseElement} class="btn btn-block ${fieldClass.join(' ')}" ${attributes.join(' ')}>${field.btnContent}</${baseElement}>`
break;
case'ul':
fieldElement = `<ul ${attributes.join(' ')} class="${fieldClass.join(' ')}" ></ul>`
break;
case'div':
fieldElement = `<div ${attributes.join(' ')} class="${fieldClass.join(' ')}" ></div>`
break;
case'form':
fieldElement = `<form ${attributes.join(' ')} class="${fieldClass.join(' ')}" ></form>`
break;
case'table':
fieldElement = `<table ${attributes.join(' ')} class="${fieldClass.join(' ')}"><tbody></tbody></table>`
break;
case'number':
if(field.numberMin){
attributes.push(`min="${field.numberMin}"`)
}
if(field.numberMax){
attributes.push(`max="${field.numberMax}"`)
}
fieldElement = '<div><input type="number" class="form-control" ' + attributes.join(' ') + '></div>'
break;
case'password':
fieldElement = '<div><input type="password" class="form-control" ' + attributes.join(' ') + '></div>'
break;
case'text':
fieldElement = `<div><input class="form-control" ${attributes.join(' ')} value="${preFill}"></div>`
break;
case'textarea':
fieldElement = '<div><textarea class="form-control" ' + attributes.join(' ') + '></textarea></div>'
break;
case'select':
fieldElement = '<div><select class="form-control" ' + attributes.join(' ') + '>'
fieldElement += buildOptions(field,possiblities)
fieldElement += '</select></div>'
break;
}
if(field['form-group-class-pre-pre-layer']){ %>
<div class="<%- field['form-group-class-pre-pre-layer'] %>">
<% }
if(field['form-group-class-pre-layer']){ %>
<div class="<%- field['form-group-class-pre-layer'] %>">
<% }
if(fieldType === 'ul' || fieldType === 'div' || fieldType === 'btn' || fieldType === 'table' || fieldType === 'form'){ %>
<%- fieldElement %>
<% }else{ %>
<div <%- hidden %> class="form-group <%- field['form-group-class'] %>">
<label><div><span><%- field.field %>
<% if(field.description){ %>
<small><%- field.description %></small>
<% } %>
</span></div>
<%- fieldElement %>
</label>
</div>
<% }
}
if(field['form-group-class-pre-layer']){ %>
</div>
<% }
if(field['form-group-class-pre-pre-layer']){ %>
</div>
<% }
})
}
%>
</div>
</<%- monitorSettings.isForm ? 'form' : 'div' %>>
<%
if(monitorSettings['section-pre-class']){ %>
</div>
<% }
if(monitorSettings['section-pre-pre-class']){ %>
</div>
<% }
if(monitorSettings.isAdvanced){ %>
</div>
<% }
}
%>
<% Object.keys(define['Sub-Account Manager'].blocks).forEach(function(blockKey){
var accountSettings = define['Sub-Account Manager'].blocks[blockKey]
drawBlock(accountSettings)
}) %>
<!-- <div class="row">
<div class="col-md-4">
<table class="table table-striped" id="sub_accounts">
<tbody></tbody>
</table>
</div>
<div class="col-md-4">
<form id="sub_accounts_add_form">
<div class="form-group-group forestgreen">
<h4>Add<small id="msg" class="pull-right" style="color:#fff"></small></h4>
<div class="form-group">
<label><div><span>Email</span></div>
<div><input class="form-control" type="email" name="mail"></div>
</label>
</div>
<div class="form-group">
<label><div><span>Password</span></div>
<div><input class="form-control" type="password" name="pass"></div>
</label>
</div>
<div class="form-group">
<label><div><span>Password Again</span></div>
<div><input class="form-control" type="password" name="password_again"></div>
</label>
</div>
<div>
<button type="reset" class="btn btn-danger"><i class="fa fa-undo"></i> Clear</button>
<div class="pull-right">
<button type="submit" class="btn btn-success"><i class="fa fa-check"></i> Submit</button>
</div>
</div>
</div>
</form>
</div>
<div class="col-md-4">
</div>
</div> -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default pull-left" data-dismiss="modal"><i class="fa fa-times"></i> <%-lang.Close%></button>
</div>
</div>
</div>
</div>
<script src="<%-window.libURL%>libs/js/dash2.subAccountManager.js"></script>

View File

@ -1,4 +1,4 @@
<!--Confirmation Window-->
<!--Sub Account Permission Editor Window-->
<div class="modal fade" id="permissions" tabindex="-1" role="dialog" aria-labelledby="permissionsLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<form class="modal-content">

View File

@ -10,6 +10,7 @@
<li class="mdl-menu__item" data-toggle="modal" data-target="#shinobihub_viewer"><div><i class="fa fa-home"></i><div><%- lang.ShinobiHub %></div></div></li>
<li class="mdl-menu__item" data-toggle="modal" data-target="#eventCounts"><div><i class="fa fa-eye"></i><div><%- lang['Event Counts'] %></div></div></li>
<% if(!details.sub){ %>
<li class="mdl-menu__item" data-toggle="modal" data-target="#subAccountManager"><div><i class="fa fa-group"></i><div><%- lang.subAccountManager %></div></div></li>
<li class="mdl-menu__item" data-toggle="modal" data-target="#onvif_probe"><div><i class="fa fa-rss"></i><div><%- lang.ONVIF %></div></div></li>
<li class="mdl-menu__item" data-toggle="modal" data-target="#probe"><div><i class="fa fa-search"></i><div><%- lang.FFprobe %></div></div></li>
<li class="mdl-menu__item" data-toggle="modal" data-target="#monitorStates"><div><i class="fa fa-align-right"></i><div><%- lang['Monitor States'] %></div></div></li>

View File

@ -1,5 +1,12 @@
<%
var details = JSON.parse($user.details)
if(!details.sub){ %>
<script>
window.getAdminApiPrefix = function(piece){
return location.search === '?p2p=1' ? location.pathname + '/' : "<%=originalURL%><%=config.webPaths.adminApiPrefix%><%- $user.auth_token %>/"
}
</script>
<% }
%>
<% include blocks/header %>
<script>var $user = <%- JSON.stringify($user) %>;</script>
@ -172,6 +179,7 @@
<% include blocks/schedules.ejs %>
<% include blocks/confirm.ejs %>
<% include blocks/shinobiHub.ejs %>
<% include blocks/subAccountManager.ejs %>
<% customAutoLoad.PageBlocks.forEach(function(block){ %>
<%- include(block) %>
<% }) %>
@ -231,3 +239,14 @@
<% } %>
<% include blocks/onvifDeviceManager.ejs %>
<%
if(!details.sub){ %>
<script>
$(document).ready(function(){
if(`${location.pathname}/` === `<%=config.webPaths.adminApiPrefix%>`){
$('#subAccountManager').modal('show')
}
})
</script>
<% }
%>