Files
photoassistant/internal/js/files_support.js
Damien De Paoli 56771308a6 updated BUGs in general to remove older / fixed BUGs relating to the confusion of current/eids, etc.
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
2025-10-20 19:31:57 +11:00

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( `&nbsp;${OPT.how_many} files&nbsp;` )
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=[]