// 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()
}
// function called when we get another page from inside the viewer
function getPageViewer(res, viewingIdx)
{
document.viewing=document.entries[viewingIdx]
// update viewing, arrows and image/video too
ViewImageOrVideo()
}
// grab all selected thumbnails and return a
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+='
' + $(this).children().parent().html() + '
'
seln+=''
} )
return '
'+seln+'
'
}
// return a list of eid= 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( '' )
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+= ''
} )
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 =`
Moving the following files?
`
$('#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 ='
' + del_or_undel + ' the following files?
'
div+=GetSelnAsDiv()
div+=`
`
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 ) { window.location='/' }; CheckForJobs() } }); return false" class="btn btn-outline-success col-2">Ok
`
$('#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 ='
'
$('.highlight').each(function( index ) {
div += "
Name:
" + $(this).attr('fname') + "
"
div += "
Date:
" + $(this).attr('pretty_date') + "
"
dir = $(this).attr('in_dir')
if( dir.slice(-1) != "/" )
dir=dir.concat('/')
div += "
Dir:
" + dir + "
"
div += "
Size:
" + $(this).attr('size') + " MB
"
div += "
Hash:
" + $(this).attr('hash') + "
"
div += "
Path Type:
" + $(this).attr('path_type') + "
"
} )
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 area,
// whether you click after highlight or before
function DoSel(e, el)
{
if( e.ctrlKey || document.fake_ctrl === 1 )
{
$(el).toggleClass('highlight')
if( document.fake_ctrl === 1 )
document.fake_ctrl=0
return
}
if( e.shiftKey || document.fake_shift === 1 )
{
st=Number($('.highlight').first().attr('ecnt'))
end=Number($('.highlight').last().attr('ecnt'))
clicked=Number($(el).attr('ecnt'))
// if we shift-click first element, then st/end are NaN, so just highlightthe one clicked
if( isNaN(st) )
{
$('.entry').slice( clicked, clicked+1 ).addClass('highlight')
return
}
if( clicked > end )
$('.entry').slice( end, clicked+1 ).addClass('highlight')
else
$('.entry').slice( clicked, st ).addClass('highlight')
if( document.fake_shift === 1 )
document.fake_shift=0
return
}
$('.highlight').removeClass('highlight')
$(el).addClass('highlight')
}
// 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
}
/**
* Renders a group header or entry based on the object and options.
* @param {Object} obj - The object containing file/directory details.
* @param {Object} last - Tracks the last printed group (e.g., { printed: null }).
* @param {Object} ecnt - Entry counter (e.g., { val: 0 }).
* @returns {string} - Generated HTML string.
*/
function addFigure( obj, last, ecnt)
{
let html = "";
// Grouping logic
if (OPT.grouping === "Day") {
if (last.printed !== obj.file_details.day) {
html += `
Day: ${obj.file_details.day} of ${obj.file_details.month}/${obj.file_details.year}
`;
last.printed = obj.file_details.day;
}
} else if (OPT.grouping === "Week") {
if (last.printed !== obj.file_details.woy) {
html += `
Week #: ${obj.file_details.woy} of ${obj.file_details.year}
`;
last.printed = obj.file_details.woy;
}
} else if (OPT.grouping === "Month") {
if (last.printed !== obj.file_details.month) {
html += `
Month: ${obj.file_details.month} of ${obj.file_details.year}
`;
last.printed = obj.file_details.month;
}
}
// Image/Video/Unknown entry
if (obj.type.name === "Image" || obj.type.name === "Video" || obj.type.name === "Unknown") {
if (!OPT.folders || isTopLevelFolder(obj.in_dir.in_path.path_prefix + '/' + obj.in_dir.rel_path + '/' + obj.name, OPT.cwd)) {
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;
html += `
${renderMedia(obj)}
`;
}
}
// 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;
if (isTopLevelFolder(dirname, OPT.cwd)) {
html += `
${obj.name}
`;
html += ``;
}
}
$('#figures').append( html )
return
}
// Helper function to render media (image/video/unknown)
function renderMedia(obj) {
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
? ``
: ``;
let mediaHtml = `
${thumb}`;
if (isImageOrUnknown) {
if (OPT.search_term) {
mediaHtml += `
`;
}
mediaHtml += `
`;
} else if (isVideo) {
mediaHtml += `
`;
if (OPT.search_term) {
mediaHtml += `
`;
}
}
mediaHtml += `
`;
return mediaHtml;
}
// Helper: Check if path is a top-level folder of cwd
function isTopLevelFolder(path, cwd) {
// Implement your logic here
return true; // Placeholder
}
// 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
$.ajax({
type: 'POST',
url: '/get_dir_entries',
data: JSON.stringify(data),
contentType: 'application/json',
dataType: 'json',
success: function(res) {
document.entries=res
// rebuild entryList/pageList as each dir comes with new entries
entryList=res.map(obj => obj.id);
pageList=entryList.slice(0, OPT.how_many)
if( back )
document.back_id = res[0].in_dir.eid
drawPageOfFigures()
},
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 }
var ecnt=0
if( OPT.folders )
{
if( (document.entries.length && document.entries[0].in_dir.rel_path == '' ) || OPT.root_eid == 0 )
{
gray="_gray"
back=""
cl=""
}
else
{
gray=""
back="Back"
cl="back"
}
// back button, if gray/back decide if we see grayed out folder and/or the name of the folder we go back to
html=`
${back}
`
ecnt++
$('#figures').append(html)
}
for (const obj of document.entries) {
addFigure( obj, last, ecnt )
ecnt++
}
if( document.entries.length == 0 )
if( OPT.search_term )
$('#figures').append( ` No matches for: '${OPT.search_term}'` )
else if( OPT.root_eid == 0 )
$('#figures').append( `No files in Path!` )
$('.figure').click( function(e) { DoSel(e, this ); SetButtonState(); return false; });
$('.figure').dblclick( function(e) { dblClickToViewEntry( $(this).attr('id') ); setDisabledForViewingNextPrevBttons(); addViewerKeyHandler() } )
// for dir, getDirEntries 2nd param is back (or "up" a dir)
$(".dir").click( function(e) { document.back_id=this.id; getDirEntries(this.id,false) } )
$(".back").click( function(e) { getDirEntries(this.id,true) } )
}
function getPageFileList(res, viewingIdx)
{
$('#file_list_div').empty()
if( OPT.root_eid == 0 )
{
$('#file_list_div').append( `No files in Path!` )
return
}
html='
'
$('#file_list_div').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
$.ajax({
type: 'POST', url: '/get_entries_by_ids',
data: JSON.stringify(data), contentType: 'application/json',
dataType: 'json',
success: function(res) {
document.entries=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=`No files in Path`
$('#file_list_div').append(html)
$('#figures').append(html)
}
},
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=" + firstEntryOnPage + " 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=" + firstEntryOnPage + " 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()
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
// put data back into booleans, ints, etc
OPT.folders=( OPT.folders == 'True' )
OPT.how_many=parseInt(OPT.how_many)
console.log('OPT.size='+OPT.size)
$('.how_many_text').html( ` ${OPT.how_many} files ` )
OPT.root_eid=parseInt(OPT.root_eid)
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);
}
function getPreviousEntry() {
var currentIndex = entryList.indexOf(document.viewing.id);
oldPageOffset=Math.floor(currentIndex / OPT.how_many)
if (currentIndex > 0) {
currentIndex--;
pageOffset=Math.floor(currentIndex / OPT.how_many)
currentIndex=currentIndex-(pageOffset*OPT.how_many)
// pref page, load it
if( oldPageOffset != pageOffset )
// pref page is pageOffset+1 now
getPage(pageOffset+1,getPageViewer,currentIndex)
else
document.viewing=document.entries[currentIndex]
}
}
function getNextEntry() {
var currentIndex = entryList.indexOf(document.viewing.id);
oldPageOffset=Math.floor(currentIndex / OPT.how_many)
if (currentIndex < entryList.length - 1) {
currentIndex++
pageOffset=Math.floor(currentIndex / OPT.how_many)
currentIndex=currentIndex-(pageOffset*OPT.how_many)
// next page, load it
if( oldPageOffset != pageOffset )
// next page is pageOffset+1 now
getPage(pageOffset+1,getPageViewer,currentIndex)
else
document.viewing=document.entries[currentIndex]
}
}
function entryIsAtStart() {
return document.viewing.id === entryList[0];
}
function entryIsAtEnd() {
return document.viewing.id === entryList[entryList.length - 1];
}
function setEntryById(id) {
var currentIndex = entryList.indexOf(parseInt(id));
// if we are on a different page, adjust as document.entries only has <= how_many
pageOffset=Math.floor(currentIndex / OPT.how_many)
currentIndex = currentIndex-(pageOffset*OPT.how_many)
document.viewing=document.entries[currentIndex]
}
function setDisabledForViewingNextPrevBttons()
{
$('#la').attr('disabled', entryIsAtStart());
$('#ra').attr('disabled', entryIsAtEnd());
}
function addViewerKeyHandler() {
// allow a keypress on the viewer_div
$(document).keydown(function(event) {
// if dbox is visible, dont process this hot-key, we are inputting text
// into inputs instead
if( $("#dbox").is(':visible') )
return
switch (event.key)
{
case "Left": // IE/Edge specific value
case "ArrowLeft":
if( $('#la').prop('disabled') == false )
$('#la').click()
break;
case "Right": // IE/Edge specific value
case "ArrowRight":
if( $('#ra').prop('disabled') == false )
$('#ra').click()
break;
case "d":
$('#distance').click()
break;
case "f":
$('#faces').click()
break;
case "n":
$('#fname_toggle').click()
break;
case "F":
fullscreen=!document.fullscreen
ViewImageOrVideo()
break;
case "l":
JoblogSearch()
break;
case "Delete":
$('#del').click()
default:
return; // Quit when this doesn't handle the key event.
}
});
}
$(document).on('click', function(e) { $('.highlight').removeClass('highlight') ; SetButtonState() });
function dblClickToViewEntry(id) {
$('#files_div').addClass('d-none')
$('#viewer_div').removeClass('d-none')
setEntryById( id )
ViewImageOrVideo()
}
// 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" ) { dblClickToViewEntry( $(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
};
}
});