update amendments in tables.sql to include job_id in entry_ammendment added amend.py to move amendment-related code into its own file when we create a job (NewJob) and that job matches an amendmentType (via job_name or job_name:amt <- where amt relates to how we do a transform_image), then we enter a new EntryAmendment pa_job_mgr knows when a Transform job ends, and removes relevant EntryAmendment files*.js use EntryAmendment data to render thumbnails with relevant AmendmentType if a normal page load (like /files_ip), and there is an EntryAmendment, mark up the thumb, run the check jobs to look for completion of the job, removeal of the EntryAmendment and update the entry based on 'transformed' image OVERALL: this is a functioning version that uses EntryAmendments and can handle loading a new page with outstanding amendments and 'deals' with it. This is a good base, but does not cater for remove_files or move_files
900 lines
36 KiB
JavaScript
900 lines
36 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; } } )
|
|
}
|
|
|
|
// 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()
|
|
div+=`<div class="row col-12 mt-3">
|
|
<button onClick="$('#dbox').modal('hide')" class="btn btn-outline-secondary col-2">Cancel</button>
|
|
`
|
|
div+=`
|
|
<button onClick="MoveOrDelCleanUpUI(); $.ajax({ type: 'POST', data: to_del, url:
|
|
`
|
|
if( del_or_undel == "Delete" )
|
|
div+=`
|
|
'/delete_files',
|
|
success: function(data){
|
|
if( $(location).attr('pathname').match('search') !== null || document.viewing ) { window.location='/' }; CheckForJobs() } }); return false" class="btn btn-outline-danger col-2">Ok</button>
|
|
</div>
|
|
`
|
|
else
|
|
// just force page reload to / for now if restoring files from a search path -- a search (by name)
|
|
// would match the deleted/restored file, so it would be complex to clean up the UI (and can't reload, as DB won't be changed yet)
|
|
div+=`
|
|
'/restore_files',
|
|
success: function(data){
|
|
if( $(location).attr('pathname').match('search') !== null || document.viewing ) { window.location='/' }; CheckForJobs() } }); return false" class="btn btn-outline-success 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)}
|
|
</figure>`;
|
|
}
|
|
// 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>
|
|
</figure>
|
|
`;
|
|
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>`
|
|
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
|
|
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.amend;
|
|
// this is only called when we are viewing a page in files/list view, so check for job(s) ending...
|
|
for (const tmp of document.amendments) {
|
|
CheckTransformJob(tmp.eid,tmp.job_id,handleTransformFiles)
|
|
}
|
|
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);
|
|
}
|
|
|
|
// 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=[]
|