998 lines
39 KiB
JavaScript
998 lines
39 KiB
JavaScript
// GLOBAL ICON array
|
|
ICON={}
|
|
ICON["Import"]="import"
|
|
ICON["Storage"]="db"
|
|
ICON["Bin"]="trash"
|
|
|
|
// function called when we get another page from inside the files view
|
|
function getPageFigures(res, viewingIdx)
|
|
{
|
|
// add all the figures to files_div
|
|
drawPageOfFigures()
|
|
}
|
|
|
|
// grab all selected thumbnails and return a <div> containing the thumbnails
|
|
// with extra yr and date attached as attributes so we can set the default
|
|
// dir name for a move directory - not used in del, but no harm to include them
|
|
function GetSelnAsDiv()
|
|
{
|
|
seln=''
|
|
$('.highlight').each(function( index ) {
|
|
seln+='<div fname="' + $(this).attr('fname') + '" yr="' + $(this).attr('yr') +
|
|
'" date="' + $(this).attr('date') +
|
|
'" class="px-1 col col-auto">' + $(this).children().parent().html() + '</div>'
|
|
seln+='<input type="hidden" name="eid-'+index+'" value="'+$(this).attr('id')+'">'
|
|
} )
|
|
return '<div class="row col-12">'+seln+'</div>'
|
|
}
|
|
|
|
// return a list of eid=<id> for each selected thumbnail
|
|
function GetSelnAsData()
|
|
{
|
|
to_del=''
|
|
$('.highlight').each(function( index ) { to_del+='&eid-'+index+'='+$(this).attr('id') } )
|
|
return to_del
|
|
}
|
|
|
|
// use an ajax POST to force an AI scan on the selected image(s)
|
|
function RunAIOnSeln(person)
|
|
{
|
|
post_data = GetSelnAsData()
|
|
post_data += '&person='+person.replace('ai-','')
|
|
$.ajax({ type: 'POST', data: post_data, url: '/run_ai_on', success: function(data){ CheckForJobs() } })
|
|
}
|
|
|
|
// code to change the associated image/icon when the move to selection box (import or storage paths) is changed
|
|
function change_rp_sel()
|
|
{
|
|
icon_url = $('option:selected', '#rp_sel').attr('icon_url')
|
|
$('#move_path_icon').html( '<svg id="move_path_icon" width="20" height="20" fill="currentColor"><use xlink:href="'
|
|
+ icon_url + '"></svg>' )
|
|
seld_ptype=$('option:selected', '#rp_sel').attr('path_type')
|
|
$('#move_path_type').val( seld_ptype )
|
|
// clear all 'existing' buttons
|
|
$('.move_Import').addClass('d-none')
|
|
$('.move_Storage').addClass('d-none')
|
|
// show just selected path's (relevant) buttons
|
|
$('.move_'+seld_ptype).removeClass('d-none')
|
|
}
|
|
|
|
// POST to see if there are any other existing directories named around this date
|
|
// (if so display them as options for a move)
|
|
function GetExistingDirsAsDiv( dt, divname, ptype )
|
|
{
|
|
$.ajax({
|
|
type: 'POST', data: null, url: '/get_existing_paths/'+dt,
|
|
success: function(data) {
|
|
$('#'+divname).html(data)
|
|
dirs = JSON.parse(data)
|
|
s=''
|
|
dirs.forEach( function(item, index) {
|
|
if( item.ptype != ptype )
|
|
vis = 'd-none '
|
|
else
|
|
vis = ''
|
|
s+= '<button class="btn btn-outline-primary move_'+item.ptype+ ' ' + vis + '" '
|
|
s+= 'onClick="$(\'#prefix\').val(\''+item.prefix.replace("\'","\\\'")+'\'); '
|
|
s+='$(\'#suffix\').val(\''+item.suffix.replace("\'","\\\'")+'\');return false;">'
|
|
s+=item.prefix+item.suffix
|
|
s+='</button>'
|
|
} )
|
|
if( s == '' )
|
|
$('#existing').html('')
|
|
else
|
|
$('#move_'+ptype).removeClass('invisible')
|
|
$('#'+divname).html(s)
|
|
}
|
|
} )
|
|
}
|
|
|
|
// wrapper to do some clean up before POST to /move_files or /delete_files
|
|
// used to remove the highlighted item(s) && reset the numbering so highlighting continues to work
|
|
function MoveOrDelCleanUpUI()
|
|
{
|
|
// remove the images being moved (so UI immediately 'sees' the move)
|
|
$("[name^=eid-]").each( function() { $('#'+$(this).attr('value')).remove() } )
|
|
// reorder the images via ecnt again, so future highlighting can work
|
|
// document.mf_id=0; $('.figure').each( function() { $(this).attr('ecnt', document.mf_id ); document.mf_id++ } )
|
|
$('#dbox').modal('hide')
|
|
}
|
|
|
|
|
|
// show the DBox for a move file, includes all thumbnails of selected files to move
|
|
// and a pre-populated folder to move them into, with text field to add a suffix
|
|
function MoveDBox(path_details)
|
|
{
|
|
$('#dbox-title').html('Move Selected File(s) to new directory in Storage Path')
|
|
div =`
|
|
<div class="form-row col-12">
|
|
<p class="col">Moving the following files?</p>
|
|
</div>
|
|
<form id="mv_fm" class="form form-control-inline col-12">
|
|
<input id="move_path_type" name="move_path_type" type="hidden"
|
|
`
|
|
div += ' value="' + path_details[0].type.name + '"></input>'
|
|
div+=GetSelnAsDiv()
|
|
yr=$('.highlight').first().attr('yr')
|
|
dt=$('.highlight').first().attr('date')
|
|
div+='<div class="row">Use Existing Directory (in the chosen path):</div><div id="existing"></div>'
|
|
GetExistingDirsAsDiv( dt, "existing", path_details[0].type.name )
|
|
div+=`
|
|
<div class="input-group my-3">
|
|
<alert class="alert alert-primary my-auto py-1">
|
|
`
|
|
// NB: alert-primary here is a hack to get the bg the same color as the alert primary by
|
|
div+= '<svg id="move_path_icon" width="20" height="20" fill="currentColor"><use xlink:href="' + path_details[0].icon_url + '"></svg>'
|
|
div+= '<select id="rp_sel" name="rel_path" class="text-primary alert-primary py-1 border border-primary rounded" onChange="change_rp_sel()">'
|
|
for(p of path_details) {
|
|
div+= '<option path_type="'+p.type.name+'" icon_url="'+p.icon_url+'">'+p.root_dir+'</option>'
|
|
}
|
|
div+= '</select>'
|
|
div+=`
|
|
</alert>
|
|
<input id="prefix" type="text" name="prefix" class="text-primary text-right form-control"
|
|
`
|
|
div+="value="+yr+'/'+dt+"-"
|
|
div+=`
|
|
"></input>
|
|
<input id="suffix" type="text" name="suffix" class="form-control" placeholder="name"> </input>
|
|
</div>
|
|
<div class="form-row col-12 mt-2">
|
|
<button onClick="$('#dbox').modal('hide'); return false;" class="btn btn-outline-secondary offset-1 col-2">Cancel</button>
|
|
<button id="move_submit" onClick="MoveOrDelCleanUpUI(); $.ajax({ type: 'POST', data: $('#mv_fm').serialize(), url: '/move_files', success: function(data) {
|
|
if( $(location).attr('pathname').match('search') !== null ) { window.location='/' }; CheckForJobs() } }); return false" class="btn btn-outline-primary col-2">Ok</button>
|
|
</div>
|
|
</form>
|
|
`
|
|
|
|
$('#dbox-content').html(div)
|
|
$('#dbox').modal('show')
|
|
$("#prefix").keypress(function (e) { if (e.which == 13) { $("#move_submit").click(); return false; } } )
|
|
$("#suffix").keypress(function (e) { if (e.which == 13) { $("#move_submit").click(); return false; } } )
|
|
}
|
|
|
|
// This function is called anytime we have a job that returns amendments
|
|
// (visually we want to show this entry is being amended by a job)
|
|
// as we check for a job to end every second, we can call this multiple times
|
|
// during the runtime of a job, so only redraw/react to a new amendment
|
|
// NOTE: we update all views, as we might go into one via jscript before the job ends
|
|
function processAmendments( ams )
|
|
{
|
|
for (const am of ams)
|
|
{
|
|
// if we return anything here, we already have this amendment, so continue to next
|
|
if( document.amendments.filter(obj => obj.eid === am.eid).length > 0 )
|
|
continue
|
|
|
|
document.amendments.push(am)
|
|
|
|
if( document.viewing && document.viewing.id == am.eid )
|
|
{
|
|
im.src=im.src + '?t=' + new Date().getTime();
|
|
DrawImg()
|
|
}
|
|
|
|
// find where in the page this image is being viewed
|
|
idx = pageList.indexOf(am.eid)
|
|
// createFigureHtml uses matching document.amendments to show thobber, etc
|
|
html = createFigureHtml( document.entries[idx] )
|
|
$('#'+am.eid).replaceWith( html )
|
|
}
|
|
}
|
|
|
|
// function to add data for document.amendment based on id and amt
|
|
// used when we transform several images in files_*, or single image in viewer
|
|
// show the DBox for a delete/restore file, includes all thumbnails of selected files
|
|
// with appropriate coloured button to Delete or Restore files`
|
|
function DelDBox(del_or_undel)
|
|
{
|
|
to_del = GetSelnAsData()
|
|
$('#dbox-title').html(del_or_undel+' Selected File(s)')
|
|
div ='<div class="row col-12"><p class="col">' + del_or_undel + ' the following files?</p></div>'
|
|
div+=GetSelnAsDiv()
|
|
if( del_or_undel == "Delete" )
|
|
{
|
|
which="delete"
|
|
col="danger"
|
|
}
|
|
else
|
|
{
|
|
which="restore"
|
|
col="sucess"
|
|
}
|
|
|
|
document.ents_to_del=[]
|
|
$('.highlight').each(function( cnt ) { document.ents_to_del[cnt]=parseInt($(this).attr('id')) } )
|
|
div+=`<div class="row col-12 mt-3">
|
|
<button onClick="$('#dbox').modal('hide')" class="btn btn-outline-secondary col-2">Cancel</button>
|
|
<button onClick="
|
|
$.ajax({ type: 'POST', data: to_del, url: '/${which}_files',
|
|
success: function(data) {
|
|
// FIXME: what is the ! search stuff for???
|
|
// FIXME: really, also why not show 'delete' throbber, and on success of actual delete go back to /
|
|
if( $(location).attr('pathname').match('search') !== null || document.viewing ) { window.location='/' }
|
|
|
|
processAmendments( data.job.amendments )
|
|
checkForAmendmentJobToComplete(data.job.id)
|
|
}
|
|
});
|
|
$('#dbox').modal('hide')
|
|
return false"
|
|
class="btn btn-outline-${col} col-2">Ok</button>
|
|
</div>`
|
|
$('#dbox-content').html(div)
|
|
$('#dbox').modal('show')
|
|
}
|
|
|
|
// show the DBox for a lame quick version of file details
|
|
function DetailsDBox()
|
|
{
|
|
$('#dbox-title').html('Details of Selected File(s)')
|
|
var div ='<div class="row col-12">'
|
|
$('.highlight').each(function( index ) {
|
|
div += "<div class='col-3' style='background-color:lightgray'>Name:</div><div class='col-9' style='background-color:lightgray'>" + $(this).attr('fname') + "</div>"
|
|
div += "<div class='col-3'>Date:</div><div class='col-9'>" + $(this).attr('pretty_date') + "</div>"
|
|
dir = $(this).attr('in_dir')
|
|
if( dir.slice(-1) != "/" )
|
|
dir=dir.concat('/')
|
|
div += "<div class='col-3'>Dir:</div><div class='col-9'>" + dir + "</div>"
|
|
div += "<div class='col-3'>Size:</div><div class='col-9'>" + $(this).attr('size') + " MB</div>"
|
|
div += "<div class='col-3'>Hash:</div><div class='col-9'>" + $(this).attr('hash') + "</div>"
|
|
div += "<div class='col-3'>Path Type:</div><div class='col-9'>" + $(this).attr('path_type') + "</div>"
|
|
} )
|
|
div += `
|
|
</div>
|
|
<br>
|
|
<div class="form-row col-12">
|
|
<button onClick="$('#dbox').modal('hide'); return false;" class="btn btn-outline-secondary offset-1 col-2">Cancel</button>
|
|
</div>
|
|
`
|
|
$('#dbox-content').html(div)
|
|
$('#dbox').modal('show')
|
|
}
|
|
|
|
|
|
// DoSel is called when a click event occurs, and sets the selection via adding
|
|
// 'highlight' to the class of the appropriate thumbnails
|
|
// e == event (can see if shift/ctrl held down while left-clicking
|
|
// el == element the click is on
|
|
// this allows single-click to select, ctrl-click to (de)select 1 item, and
|
|
// shift-click to add all elements between highlighted area and clicked el,
|
|
// whether you click before highlight or after, or inside a gap and then back
|
|
// or forward to the closest higlighted entry - also, only works on entry class,
|
|
// so it ignores figures that we take entry off while we transform, etc it
|
|
function DoSel(e, el) {
|
|
const id = $(el).attr('id');
|
|
const entries = $('.entry');
|
|
|
|
// Collect currently highlighted entries
|
|
const currentHighlights = $('.highlight');
|
|
const highlighted = new Set();
|
|
currentHighlights.each(function() {
|
|
highlighted.add($(this).attr('id'));
|
|
});
|
|
|
|
// Ctrl+click: toggle highlight for the clicked entry
|
|
if (e.ctrlKey || document.fake_ctrl === 1) {
|
|
$(el).toggleClass('highlight');
|
|
if (highlighted.has(id)) {
|
|
highlighted.delete(id);
|
|
} else {
|
|
highlighted.add(id);
|
|
}
|
|
if (document.fake_ctrl === 1) {
|
|
document.fake_ctrl = 0;
|
|
}
|
|
return;
|
|
}
|
|
// Shift+click: select a range
|
|
else if (e.shiftKey || document.fake_shift === 1) {
|
|
if (currentHighlights.length === 0) {
|
|
// If no highlights, just highlight the clicked entry
|
|
$(el).addClass('highlight');
|
|
highlighted.add(id);
|
|
} else {
|
|
// Find the nearest highlighted entry
|
|
const clickedIndex = entries.index($(el));
|
|
let nearestHighlightIndex = -1;
|
|
let minDistance = Infinity;
|
|
|
|
currentHighlights.each(function() {
|
|
const highlightIndex = entries.index($(this));
|
|
const distance = Math.abs(highlightIndex - clickedIndex);
|
|
if (distance < minDistance) {
|
|
minDistance = distance;
|
|
nearestHighlightIndex = highlightIndex;
|
|
}
|
|
});
|
|
|
|
// Highlight the range between the nearest highlighted entry and the clicked entry
|
|
const from = Math.min(clickedIndex, nearestHighlightIndex);
|
|
const to = Math.max(clickedIndex, nearestHighlightIndex);
|
|
|
|
for (let i = from; i <= to; i++) {
|
|
const entryId = entries.eq(i).attr('id');
|
|
highlighted.add(entryId);
|
|
entries.eq(i).addClass('highlight');
|
|
}
|
|
}
|
|
if (document.fake_shift === 1) {
|
|
document.fake_shift = 0;
|
|
}
|
|
return;
|
|
}
|
|
// Single click: clear all highlights and highlight the clicked entry
|
|
else {
|
|
$('.highlight').removeClass('highlight');
|
|
highlighted.clear();
|
|
$(el).addClass('highlight');
|
|
highlighted.add(id);
|
|
}
|
|
}
|
|
|
|
// if a selection exists, enable move & del/restore buttons otherwise disable them
|
|
function SetButtonState() {
|
|
var sel=false
|
|
$('.highlight').each(function( index ) { sel=true } )
|
|
if( sel ) {
|
|
$('#move').attr('disabled', false )
|
|
$('#del').attr('disabled', false )
|
|
} else {
|
|
$('#move').attr('disabled', true )
|
|
$('#del').attr('disabled', true )
|
|
}
|
|
}
|
|
|
|
// Check if the set of highlights are either only figures, dirs or both
|
|
// used to work out what options are shown in the file context menu
|
|
function FiguresOrDirsOrBoth() {
|
|
var figure=false
|
|
var dir=false
|
|
$('.highlight').each(function( index ) {
|
|
if( $(this).hasClass('figure') ) {
|
|
figure=true
|
|
}
|
|
if( $(this).hasClass('dir') ) {
|
|
dir=true
|
|
}
|
|
} )
|
|
if( figure & ! dir )
|
|
return "figure"
|
|
if( ! figure & dir )
|
|
return "dir"
|
|
return "both"
|
|
}
|
|
|
|
// Check if the set of highlights contain Bin and Not Bin...
|
|
// if its both, then no del/restore is possible in context menu
|
|
// otherwise can either set del or restore appropriately
|
|
function SelContainsBinAndNotBin() {
|
|
var bin=false
|
|
var not_bin=false
|
|
$('.highlight').each(function( index ) {
|
|
if( $(this).attr('path_type') == "Bin" ) {
|
|
bin=true
|
|
} else {
|
|
not_bin=true
|
|
}
|
|
} )
|
|
if( bin && not_bin )
|
|
return true
|
|
else
|
|
return false
|
|
}
|
|
|
|
// checks to see if there is no selection active
|
|
function NoSel() {
|
|
var sel=false
|
|
$('.highlight').each(function( index ) { sel=true } )
|
|
// func looks for No Selection, so if sel is true and we have a sel, return false (i.e. NOT No Sel -> Sel )
|
|
if( sel )
|
|
return false
|
|
else
|
|
return true
|
|
}
|
|
|
|
// quick wrapper to add a single <figure> to the #figures div
|
|
function addFigure( obj )
|
|
{
|
|
html=createFigureHtml( obj )
|
|
$('#figures').append( html )
|
|
}
|
|
|
|
/**
|
|
* Renders a group header or entry based on the object and options.
|
|
* obj - The object containing file/directory details.
|
|
* returns {string} - Generated HTML string.
|
|
*/
|
|
function createFigureHtml( obj )
|
|
{
|
|
// if am is null, no amendment for this obj, otherwise we have one
|
|
var am=null
|
|
for (const tmp of document.amendments)
|
|
if( tmp.eid == obj.id )
|
|
am=tmp
|
|
|
|
let html = "";
|
|
|
|
// Image/Video/Unknown entry
|
|
if (obj.type.name === "Image" || obj.type.name === "Video" || obj.type.name === "Unknown") {
|
|
const pathType = obj.in_dir.in_path.type.name;
|
|
const size = obj.file_details.size_mb;
|
|
const hash = obj.file_details.hash;
|
|
const inDir = `${obj.in_dir.in_path.path_prefix}/${obj.in_dir.rel_path}`;
|
|
const fname = obj.name;
|
|
const yr = obj.file_details.year;
|
|
const date = `${yr}${String(obj.file_details.month).padStart(2, '0')}${String(obj.file_details.day).padStart(2, '0')}`;
|
|
const prettyDate = `${obj.file_details.day}/${obj.file_details.month}/${obj.file_details.year}`;
|
|
const type = obj.type.name;
|
|
|
|
// if amendment for this obj, do not add entry class - prevents highlighting
|
|
if( am ) {
|
|
ent=""
|
|
gs="style='filter: grayscale(100%);'"
|
|
am_html ='<img class="position-absolute top-50 start-50 translate-middle" height="60" src="/internal/white-circle.png">'
|
|
am_html+='<img class="position-absolute top-50 start-50 translate-middle" height="64" src="/internal/throbber.gif">'
|
|
if( am.type.which == 'icon' )
|
|
am_html+=`<svg class="position-absolute top-50 start-50 translate-middle" height="32" style="color:${am.type.colour}" fill="${am.type.colour}"><use xlink:href="/internal/icons.svg#${am.type.what}"></use></svg>`
|
|
else
|
|
am_html+=`<img class="position-absolute top-50 start-50 translate-middle" src="/internal/${am.type.what}?v={{js_vers['r270']}}" height="32">`
|
|
} else {
|
|
ent="entry"
|
|
gs=""
|
|
am_html=""
|
|
}
|
|
html += `
|
|
<figure id="${obj.id}" class="col col-auto g-0 figure ${ent} m-1"
|
|
path_type="${pathType}" size="${size}" hash="${hash}" in_dir="${inDir}"
|
|
fname="${fname}" yr="${yr}" date="${date}" pretty_date="${prettyDate}" type="${type}">
|
|
${renderMedia(obj,gs,am_html)}
|
|
`
|
|
}
|
|
// Directory entry
|
|
else if (obj.type.name === "Directory" && OPT.folders) {
|
|
const dirname = obj.dir_details.rel_path.length
|
|
? `${obj.dir_details.in_path.path_prefix}/${obj.dir_details.rel_path}`
|
|
: obj.dir_details.in_path.path_prefix;
|
|
|
|
html += `
|
|
<figure class="col col-auto g-0 dir entry m-1" id="${obj.id}" dir="${dirname}" type="Directory">
|
|
<svg class="svg" width="${OPT.size - 22}" height="${OPT.size - 22}" fill="currentColor">
|
|
<use xlink:href="/internal/icons.svg#Directory"></use>
|
|
</svg>
|
|
<figcaption class="svg_cap figure-caption text-center text-wrap text-break">${obj.name}</figcaption>
|
|
`;
|
|
html += `<script>f=$('#${obj.id}'); w=f.find('svg').width(); f.find('figcaption').width(w);</script>`;
|
|
}
|
|
// moved the bindings to here as we need to reset them if we recreate this Figure (after a transform job)
|
|
html += `<script>
|
|
if( "${obj.type.name}" === "Directory" ) {
|
|
$("#${obj.id}").click( function(e) { document.back_id=this.id; getDirEntries(this.id,false) } )
|
|
} else {
|
|
$('#${obj.id}').click( function(e) { DoSel(e, this ); SetButtonState(); return false; });
|
|
$('#${obj.id}').dblclick( function(e) { startViewing( $(this).attr('id') ) } )
|
|
}
|
|
</script>
|
|
</figure>`
|
|
return html
|
|
}
|
|
|
|
// Helper function to render media (image/video/unknown)
|
|
function renderMedia(obj,gs,am_html) {
|
|
const isImageOrUnknown = obj.type.name === "Image" || obj.type.name === "Unknown";
|
|
const isVideo = obj.type.name === "Video";
|
|
const path = `${obj.in_dir.in_path.path_prefix}/${obj.in_dir.rel_path}/${obj.name}`;
|
|
const thumb = obj.file_details.thumbnail
|
|
? `<a href="${path}"><img alt="${obj.name}" ${gs} class="thumb" height="${OPT.size}" src="data:image/jpeg;base64,${obj.file_details.thumbnail}"></a>`
|
|
: `<a href="${path}"><svg width="${OPT.size}" height="${OPT.size}" fill="white"><use xlink:href="/internal/icons.svg#unknown_ftype"/></svg></a>`;
|
|
|
|
let mediaHtml = `<div style="position:relative; width:100%">${thumb}${am_html}`;
|
|
|
|
if (isVideo) {
|
|
mediaHtml += `
|
|
<div style="position:absolute; top: 0px; left: 2px;">
|
|
<svg width="16" height="16" fill="white"><use xlink:href="/internal/icons.svg#film"/></svg>
|
|
</div>
|
|
`;
|
|
}
|
|
if (OPT.search_term) {
|
|
mediaHtml += `
|
|
<div style="position:absolute; bottom: 0px; left: 2px;">
|
|
<svg width="16" height="16" fill="white"><use xlink:href="/internal/icons.svg#${getLocationIcon(obj)}"/></svg>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
mediaHtml += `</div>`;
|
|
return mediaHtml;
|
|
}
|
|
|
|
// Helper: Get location icon (placeholder)
|
|
function getLocationIcon(obj) {
|
|
return ICON[obj.in_dir.in_path.type.name]
|
|
}
|
|
|
|
// POST to get entry ids, and then getPage for a specified directory
|
|
function getDirEntries(dir_id, back)
|
|
{
|
|
data={}
|
|
data.dir_id=dir_id
|
|
data.back=back
|
|
data.noo=OPT.noo
|
|
|
|
$.ajax({
|
|
type: 'POST',
|
|
url: '/get_dir_eids',
|
|
data: JSON.stringify(data),
|
|
contentType: 'application/json',
|
|
dataType: 'json',
|
|
success: function(res) {
|
|
if( res.valid === false )
|
|
{
|
|
$('#figures').html( "<alert class='alert alert-danger'>ERROR! directory has changed since you loaded this view. You have to reload and reset your view (probably someone deleted the directory or its parent since you loaded this page)" )
|
|
return
|
|
}
|
|
entryList=res.entry_list
|
|
pageList=entryList.slice(0, OPT.how_many)
|
|
// now go get actual data/entries
|
|
getPage(1,getPageFigures)
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error("Error:", error);
|
|
}
|
|
});
|
|
}
|
|
|
|
// this function draws all the figures from document.entries - called when we
|
|
// change pages, but also when we change say grouping/other OPTs
|
|
function drawPageOfFigures()
|
|
{
|
|
$('#figures').empty()
|
|
var last = { printed: null }
|
|
|
|
// something is up, let the user know
|
|
if( document.alert )
|
|
$('#figures').append( document.alert )
|
|
|
|
if( OPT.folders )
|
|
{
|
|
// it root_eid is 0, then no entries in this path - cant go up
|
|
if( OPT.root_eid == 0 || (document.entries.length && document.entries[0].in_dir.eid == OPT.root_eid ) )
|
|
{
|
|
gray="_gray"
|
|
back=""
|
|
cl=""
|
|
back_id=0
|
|
}
|
|
else
|
|
{
|
|
gray=""
|
|
back="Back"
|
|
cl="back"
|
|
if( document.entries.length > 0 )
|
|
back_id = document.entries[0].in_dir.eid
|
|
else
|
|
back_id = document.back_id
|
|
}
|
|
// back button, if gray/back decide if we see grayed out folder and/or the name of the folder we go back to
|
|
// with clas "back" this gets a different click handler which flags server to return data by 'going back/up' in dir tree
|
|
// we give the server the id of the first item on the page so it can work out how to go back
|
|
html=`<div class="col col-auto g-0 m-1">
|
|
<figure id="${back_id}" class="${cl} entry m-1" type="Directory">
|
|
<svg class="svg" width="${OPT.size-22}" height="${OPT.size-22}">
|
|
<use xlink:href="internal/icons.svg#folder_back${gray}"/>
|
|
</svg>
|
|
<figcaption class="figure-caption text-center">${back}</figcaption>
|
|
</figure>
|
|
</div>`
|
|
$('#figures').append(html)
|
|
}
|
|
for (const obj of document.entries) {
|
|
// Grouping logic
|
|
if (OPT.grouping === "Day") {
|
|
if (last.printed !== obj.file_details.day) {
|
|
$('#figures').append(`<div class="row ps-3"><h6>Day: ${obj.file_details.day} of ${obj.file_details.month}/${obj.file_details.year}</h6></div>` );
|
|
last.printed = obj.file_details.day;
|
|
}
|
|
} else if (OPT.grouping === "Week") {
|
|
if (last.printed !== obj.file_details.woy) {
|
|
$('#figures').append(`<div class="row ps-3"><h6>Week #: ${obj.file_details.woy} of ${obj.file_details.year}</h6></div>` );
|
|
last.printed = obj.file_details.woy;
|
|
}
|
|
} else if (OPT.grouping === "Month") {
|
|
if (last.printed !== obj.file_details.month) {
|
|
$('#figures').append(`<div class="row ps-3"><h6>Month: ${obj.file_details.month} of ${obj.file_details.year}</h6></div>` );
|
|
last.printed = obj.file_details.month;
|
|
}
|
|
}
|
|
addFigure( obj )
|
|
}
|
|
$(".back").click( function(e) { getDirEntries(this.id,true) } )
|
|
if( document.entries.length == 0 )
|
|
if( OPT.search_term )
|
|
$('#figures').append( `<span class="alert alert-danger p-2 col-auto"> No matches for: '${OPT.search_term}'</span>` )
|
|
else if( OPT.root_eid == 0 )
|
|
$('#figures').append( `<span class="alert alert-danger p-2 col-auto d-flex align-items-center">No files in Path!</span>` )
|
|
}
|
|
|
|
// emtpy out file_list_div, and repopulate it with new page of content
|
|
function getPageFileList(res, viewingIdx)
|
|
{
|
|
$('#file_list_div').empty()
|
|
|
|
// something is up, let the user know
|
|
if( document.alert )
|
|
$('#file_list_div').append( '<div class="row">' + document.alert + '</div>' )
|
|
|
|
if( OPT.root_eid == 0 )
|
|
{
|
|
$('#file_list_div').append( `<span class="alert alert-danger p-2">No files in Path!</span>` )
|
|
return
|
|
}
|
|
html='<table class="table table-striped table-sm col-12">'
|
|
html+='<thead><tr class="table-primary"><th>Name</th><th>Size (MB)</th><th>Path Prefix</th><th>Hash</th></tr></thead><tbody>'
|
|
for (const obj of res) {
|
|
html+=`<tr>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<a href="${obj.in_dir.in_path.path_prefix}/${obj.in_dir.rel_path}/${obj.name}">
|
|
<img class="img-fluid me-2" style="max-width: 100px;"
|
|
src="data:image/jpeg;base64,${obj.file_details.thumbnail}"></img>
|
|
</a>
|
|
<span>${obj.name}</span>
|
|
</div>
|
|
<td>${obj.file_details.size_mb}</td>
|
|
<td>${obj.in_dir.in_path.path_prefix.replace("static/","")}/${obj.in_dir.rel_path}</td>
|
|
<td>${obj.file_details.hash}</td>
|
|
</tr>`
|
|
}
|
|
html+='</tbody></table>'
|
|
$('#file_list_div').append(html)
|
|
}
|
|
|
|
// wrapper function as we want to handle real DB query success, but also do the
|
|
// same when we just use cache
|
|
function getEntriesByIdSuccessHandler(res,pageNumber,successCallback,viewingIdx)
|
|
{
|
|
if( res.length != pageList.length )
|
|
document.alert="<alert class='alert alert-warning'>WARNING: something has changed since viewing this page (likely someone deleted content in another view), strongly suggest a page reload to get the latest data</alert>"
|
|
|
|
document.entries=res;
|
|
// cache this
|
|
document.page[pageNumber]=res
|
|
// FIXME: I want to remove successCallback, instead: if viewing, or files_*, or file_list, then call relevant draw routine
|
|
successCallback(res,viewingIdx)
|
|
resetNextPrevButtons()
|
|
// if search, disable folders
|
|
if( OPT.search_term )
|
|
$('#folders').prop('disabled', 'disabled').removeClass('border-info').addClass('border-secondary').removeClass('text-info').addClass('text-secondary');
|
|
else if( document.entries.length == 0 )
|
|
{
|
|
html=`<span class="alert alert-danger p-2 col-auto">No files in Path</span>`
|
|
$('#file_list_div').append(html)
|
|
$('#figures').append(html)
|
|
}
|
|
}
|
|
|
|
// Function to get the 'page' of entry ids out of entryList
|
|
function getPage(pageNumber, successCallback, viewingIdx=0)
|
|
{
|
|
// before we do anything, disabled left/right arrows on viewer to stop
|
|
// getting another event before we have the data for the page back
|
|
$('#la').prop('disabled', true)
|
|
$('#ra').prop('disabled', true)
|
|
const startIndex = (pageNumber - 1) * OPT.how_many;
|
|
const endIndex = startIndex + OPT.how_many;
|
|
pageList = entryList.slice(startIndex, endIndex);
|
|
|
|
// set up data to send to server to get the entry data for entries in pageList
|
|
data={}
|
|
data.ids = pageList
|
|
|
|
// assume nothing wrong, but if the data goes odd, then this will be non-null and displayed later (cant add here, as later code does .empty() of file divs)
|
|
document.alert=null
|
|
// see if we can use cache, and dont reload from DB
|
|
if( !OPT.folders && document.page.length && document.page[pageNumber] )
|
|
{
|
|
getEntriesByIdSuccessHandler( document.page[pageNumber], pageNumber, successCallback, viewingIdx )
|
|
return
|
|
}
|
|
|
|
$.ajax({
|
|
type: 'POST', url: '/get_entries_by_ids',
|
|
data: JSON.stringify(data), contentType: 'application/json',
|
|
dataType: 'json',
|
|
success: function(res) {
|
|
document.amendments=res.amendments;
|
|
// only called when an amendment is pending & we are viewing a page in files/list view
|
|
// so check for amendment job(s) ending...
|
|
for (const tmp of document.amendments)
|
|
checkForAmendmentJobToComplete(tmp.job_id)
|
|
getEntriesByIdSuccessHandler( res.entries, pageNumber, successCallback, viewingIdx )
|
|
},
|
|
error: function(xhr, status, error) { console.error("Error:", error); } });
|
|
return
|
|
}
|
|
|
|
// Quick Function to check if we are on the first page
|
|
function isFirstPage(pageNumber)
|
|
{
|
|
return pageNumber <= 1;
|
|
}
|
|
|
|
// Function to check if we are on the last page
|
|
function isLastPage(pageNumber)
|
|
{
|
|
const totalPages = Math.ceil(entryList.length / OPT.how_many);
|
|
return pageNumber >= totalPages;
|
|
}
|
|
|
|
// given an id in the list, return which page we are on (page 1 is first page)
|
|
function getPageNumberForId(id) {
|
|
const idx = entryList.indexOf(id);
|
|
// should be impossible but jic
|
|
if (idx === -1) { return -1 }
|
|
return Math.floor(idx / OPT.how_many) + 1;
|
|
}
|
|
|
|
// if we are on first page, disable prev, it not ensure next is enabled
|
|
// if we are on last page, disable next, it not ensure prev is enabled
|
|
function resetNextPrevButtons()
|
|
{
|
|
// no data, so disabled both
|
|
if( getPageNumberForId(pageList[0]) == -1 )
|
|
{
|
|
$('.prev').prop('disabled', true).addClass('disabled');
|
|
$('.next').prop('disabled', true).addClass('disabled');
|
|
return
|
|
}
|
|
if ( isFirstPage( getPageNumberForId(pageList[0]) ) )
|
|
$('.prev').prop('disabled', true).addClass('disabled');
|
|
else
|
|
$('.prev').prop('disabled', false).removeClass('disabled');
|
|
|
|
if ( isLastPage( getPageNumberForId(pageList[0]) ) )
|
|
$('.next').prop('disabled', true).addClass('disabled');
|
|
else
|
|
$('.next').prop('disabled', false).removeClass('disabled');
|
|
}
|
|
|
|
// get list of eids for the next page, also make sure next/prev buttons make sense for page we are on
|
|
function nextPage(successCallback)
|
|
{
|
|
// pageList[0] is the first entry on this page
|
|
const currentPage=getPageNumberForId( pageList[0] )
|
|
// should never happen / just return pageList unchanged
|
|
if ( currentPage === -1 || isLastPage( currentPage ) )
|
|
{
|
|
console.error( "WARNING: seems first on pg=" + pageList[0] + " of how many=" + OPT.how_many + " gives currentPage=" + currentPage + " and we cant go next page?" )
|
|
return
|
|
}
|
|
getPage( currentPage+1, successCallback )
|
|
return
|
|
}
|
|
|
|
// get list of eids for the prev page, also make sure next/prev buttons make sense for page we are on
|
|
function prevPage(successCallback)
|
|
{
|
|
// pageList[0] is the first entry on this page
|
|
const currentPage=getPageNumberForId( pageList[0] )
|
|
// should never happen / just return pageList unchanged
|
|
if (currentPage === 1 || currentPage === -1 )
|
|
{
|
|
console.error( "WARNING: seems first on pg=" + pageList[0] + " of how many=" + OPT.how_many + " gives currentPage=" + currentPage + " and we cant go prev page?" )
|
|
return
|
|
}
|
|
getPage( currentPage-1, successCallback )
|
|
return
|
|
}
|
|
|
|
// function to see if we are on a phone or tablet (where we dont have ctrl or shift keys - helps to display fake buttons to allow multiselect on mobiles)
|
|
function isMobile() {
|
|
try{ document.createEvent("TouchEvent"); return true; }
|
|
catch(e){ return false; }
|
|
}
|
|
|
|
// when we change one of the options (noo, how_many, folders) - then update '{how_many} files' str,
|
|
// tweak noo menu for folders/flat view then reset the page contents based on current OPT values
|
|
function changeOPT(successCallback) {
|
|
OPT.how_many=$('#how_many').val()
|
|
// changes invalidate page cache so clear it out
|
|
document.page.length=0
|
|
new_f=$('#folders').val()
|
|
new_f=( new_f == 'True' )
|
|
// if change to/from folders, also fix the noo menu
|
|
if( new_f != OPT.folders )
|
|
{
|
|
if( new_f )
|
|
{
|
|
$('#noo option:lt(2)').prop('disabled', true);
|
|
$('#noo').val(OPT.default_folder_noo)
|
|
}
|
|
else
|
|
{
|
|
$('#noo option:lt(2)').prop('disabled', false);
|
|
$('#noo').val(OPT.default_flat_noo)
|
|
}
|
|
}
|
|
OPT.noo=$('#noo').val()
|
|
OPT.folders=new_f
|
|
OPT.folders=$('#folders').val()
|
|
OPT.grouping=$('#grouping').val()
|
|
OPT.size=$('input[name="size"]:checked').val();
|
|
$.ajax({
|
|
type: 'POST',
|
|
url: '/change_file_opts',
|
|
data: JSON.stringify(OPT),
|
|
contentType: 'application/json',
|
|
success: function(resp) {
|
|
entryList=resp.query_data.entry_list
|
|
OPT.how_many=parseInt(OPT.how_many)
|
|
pageList=entryList.slice(0, OPT.how_many)
|
|
// put data back into booleans, ints, etc
|
|
OPT.folders=( OPT.folders == 'True' )
|
|
$('.how_many_text').html( ` ${OPT.how_many} files ` )
|
|
OPT.size=parseInt(OPT.size)
|
|
getPage(1,successCallback)
|
|
}
|
|
})
|
|
}
|
|
|
|
// function to change the size of thumbnails when user clicks xs/s/m/l/xl buttons
|
|
function changeSize()
|
|
{
|
|
sz=$('input[name="size"]:checked').val();
|
|
OPT.size=sz
|
|
$('.thumb').attr( {height: sz, style: 'font-size:'+sz+'px' } )
|
|
$('#size').val(sz)
|
|
sz=sz-22
|
|
$('.svg').height(sz);
|
|
$('.svg').width(sz);
|
|
$('.svg_cap').width(sz);
|
|
}
|
|
|
|
// when a delete or restore files job has completed successfullly, then get ids
|
|
// find the page we are on, remove amendments & ids from entryList and re-get page
|
|
// which will reset pageList and the UI of images for that page
|
|
function handleDeleteOrRestoreFileJobCompleted(job)
|
|
{
|
|
// this grabs the values from the object attributes of eid-0, eid-1, etc.
|
|
const ids = job.extra.map(item => item.value)
|
|
|
|
// find page number of first element to delete (this is the page we will return too)
|
|
pnum=getPageNumberForId( ids[0] )
|
|
|
|
// remove amendment data
|
|
for (const ent of ids)
|
|
{
|
|
id=parseInt(ent)
|
|
removeAmendment( id )
|
|
// remove the item in the entryList
|
|
index=entryList.indexOf(id);
|
|
if( index != -1 )
|
|
entryList.splice(index, 1); // Remove the element
|
|
else
|
|
{
|
|
return; // have to get out of here, or calling getPage() below will loop forever
|
|
}
|
|
}
|
|
|
|
// re-create pageList by reloading the page
|
|
getPage(pnum,getPageFigures)
|
|
}
|
|
|
|
// POST to a check URL, that will tell us if the amendment job has completed,
|
|
// it also calls CheckForJobs() which will fix up the Active Jobs badge,
|
|
function checkForAmendmentJobToComplete(job_id)
|
|
{
|
|
CheckForJobs()
|
|
$.ajax( { type: 'POST', data: '&job_id='+job_id, url: '/check_amend_job_status',
|
|
success: function(res) { handleCheckAmendmentJobStatus(res); } } )
|
|
}
|
|
|
|
// the status of a Amendment Job has been returned, finished is True/False
|
|
// if not finished try again in 1 second... If finished then invalidate page
|
|
// cache and based on job type call code correct func to update the UI appropriately
|
|
function handleCheckAmendmentJobStatus(data)
|
|
{
|
|
if( data.finished )
|
|
{
|
|
// invalidate the cache
|
|
document.page.length=0
|
|
|
|
// transforms contain the single transformed entry data for convenience
|
|
if( data.job.name == 'transform_image' )
|
|
handleTransformImageJobCompleted(data.job, data.entry)
|
|
else if ( data.job.name == 'delete_files' || data.job.name == 'restore_files' )
|
|
handleDeleteOrRestoreFileJobCompleted(data.job)
|
|
}
|
|
else { setTimeout( function() { checkForAmendmentJobToComplete(data.job.id) }, 1000 ); }
|
|
}
|
|
|
|
// different context menu on files
|
|
$.contextMenu({
|
|
selector: '.entry',
|
|
itemClickEvent: "click",
|
|
build: function($triggerElement, e) {
|
|
// when right-clicking & no selection add one OR deal with ctrl/shift right-lick as it always changes seln
|
|
if( NoSel() || e.ctrlKey || e.shiftKey )
|
|
{
|
|
DoSel(e, e.currentTarget )
|
|
SetButtonState();
|
|
}
|
|
|
|
if( FiguresOrDirsOrBoth() == "figure" )
|
|
{
|
|
item_list = {
|
|
details: { name: "Details..." },
|
|
view: { name: "View File" },
|
|
sep: "---",
|
|
}
|
|
if( e.currentTarget.getAttribute('type') == 'Image' )
|
|
{
|
|
item_list['transform'] = {
|
|
name: "Transform",
|
|
items: {
|
|
"r90": { "name" : "Rotate 90 degrees" },
|
|
"r180": { "name" : "Rotate 180 degrees" },
|
|
"r270": { "name" : "Rotate 270 degrees" },
|
|
"fliph": { "name" : "Flip horizontally" },
|
|
"flipv": { "name" : "Flip vertically" }
|
|
}
|
|
}
|
|
|
|
}
|
|
item_list['move'] = { name: "Move selected file(s) to new folder" }
|
|
item_list['sep2'] = { sep: "---" }
|
|
}
|
|
else
|
|
item_list = {
|
|
move: { name: "Move selection(s) to new folder" }
|
|
}
|
|
|
|
item_list['ai'] = {
|
|
name: "Scan file for faces",
|
|
items: {
|
|
"ai-all": { name: "all" }
|
|
}
|
|
};
|
|
|
|
// Dynamically add entries for each person in the `people` array
|
|
people.forEach(person => {
|
|
item_list['ai'].items[`ai-${person.tag}`] = { name: person.tag };
|
|
});
|
|
|
|
if( SelContainsBinAndNotBin() ) {
|
|
item_list['both']= { name: 'Cannot delete and restore at same time', disabled: true }
|
|
} else {
|
|
if (e.currentTarget.getAttribute('path_type') == 'Bin' )
|
|
item_list['undel']= { name: "Restore selected file(s)" }
|
|
else if( e.currentTarget.getAttribute('type') != 'Directory' )
|
|
item_list['del']= { name: "Delete Selected file(s)" }
|
|
}
|
|
|
|
return {
|
|
callback: function( key, options) {
|
|
if( key == "details" ) { DetailsDBox() }
|
|
if( key == "view" ) { startViewing( $(this).attr('id') ) }
|
|
if( key == "move" ) { MoveDBox(move_paths) }
|
|
if( key == "del" ) { DelDBox('Delete') }
|
|
if( key == "undel") { DelDBox('Restore') }
|
|
if( key == "r90" ) { Transform(90) }
|
|
if( key == "r180" ) { Transform(180) }
|
|
if( key == "r270" ) { Transform(270) }
|
|
if( key == "fliph" ) { Transform("fliph") }
|
|
if( key == "flipv" ) { Transform("flipv") }
|
|
if( key.startsWith("ai")) { RunAIOnSeln(key) }
|
|
// dont flow this event through the dom
|
|
e.stopPropagation()
|
|
},
|
|
items: item_list
|
|
};
|
|
}
|
|
});
|
|
|
|
// finally, for files_ip/files_sp/files_rbp - set click inside document (NOT an entry) to remove seln
|
|
$(document).on('click', function(e) { $('.highlight').removeClass('highlight') ; SetButtonState() });
|
|
document.page=[]
|