Compare commits

...

6 Commits

7 changed files with 490 additions and 237 deletions

6
README
View File

@@ -95,13 +95,13 @@ To get back a 'working' but scanned set of data:
# pg_dump --user=pa -a -t person -t refimg -t person_refimg_link > /docker-entrypoint-initdb.d/users.sql
# export all content so we can upgrade versions of postgres
sudo docker exec -it padb bash
docker exec -it padb bash
# pg_dump --user=pa pa > /docker-entrypoint-initdb.d/bkup.sql
### check sql looks good
sudo mv /srv/docker/container/padb/docker-entrypoint-initdb.d/bkup.sql /srv/docker/container/padb/docker-entrypoint-initdb.d/tables.sql
sudo rm /srv/docker/container/padb/docker-entrypoint-initdb.d/users.sql
sudo docker-compose -f /srv/docker/config/docker-compose.yaml build padb
( cd /srv/docker/config/ ; sudo docker-compose stop padb ; yes | sudo docker-compose rm padb ; sudo rm -rf /srv/docker/container/padb/data/ ; sudo docker-compose up -d padb ; sudo docker-compose restart paweb )
docker-compose -f /srv/docker/config/docker-compose.yaml build padb
( cd /srv/docker/config/ ; docker-compose stop padb ; yes | docker-compose rm padb ; sudo rm -rf /srv/docker/container/padb/data/ ; docker-compose up -d padb ; docker-compose restart paweb )
HANDY SQLs/commands:

123
files.py
View File

@@ -1,8 +1,10 @@
from flask_wtf import FlaskForm
from flask import request, render_template, redirect, send_from_directory, url_for, jsonify, make_response
from marshmallow import Schema, fields
from main import db, app, ma
from sqlalchemy import Sequence, text, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import joinedload
import os
import glob
import json
@@ -172,16 +174,43 @@ class FileTypeSchema(ma.SQLAlchemyAutoSchema):
class Meta: model = FileType
load_instance = True
class FileSchema(ma.SQLAlchemyAutoSchema):
class Meta: model = File
load_instance = True
class DirSchema(ma.SQLAlchemyAutoSchema):
class Meta: model = Dir
load_instance = True
eid = ma.auto_field() # Explicitly include eid
in_path = ma.Nested(PathSchema)
class FaceFileLinkSchema(ma.SQLAlchemyAutoSchema):
class Meta: model = FaceFileLink
load_instance = True
class PersonSchema(ma.SQLAlchemyAutoSchema):
class Meta: model=Person
load_instance = True
class RefimgSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = Refimg
exclude = ('face',)
load_instance = True
person = ma.Nested(PersonSchema)
class FaceRefimgLinkSchema(ma.SQLAlchemyAutoSchema):
class Meta: model = FaceRefimgLink
load_instance = True
class FaceSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model=Face
exclude = ('face',)
load_instance = True
refimg = ma.Nested(RefimgSchema,allow_none=True)
class FileSchema(ma.SQLAlchemyAutoSchema):
class Meta: model = File
load_instance = True
faces = ma.Nested(FaceSchema,many=True,allow_none=True)
################################################################################
# Schema for Entry so we can json for data to the client
################################################################################
@@ -191,11 +220,19 @@ class EntrySchema(ma.SQLAlchemyAutoSchema):
load_instance = True
type = ma.Nested(FileTypeSchema)
file_details = ma.Nested(FileSchema)
file_details = ma.Nested(FileSchema,allow_none=True)
# noting dir_details needs in_path to work
dir_details = ma.Nested(DirSchema)
# noting in_dir needs in_path and in_path.type to work
in_dir = ma.Nested(DirSchema)
# allow us to use FullPathOnFS()
FullPathOnFS = fields.Method("get_full_path")
def get_full_path(self, obj):
return obj.FullPathOnFS()
# global - this will be use more than once below, so do it once for efficiency
entries_schema = EntrySchema(many=True)
################################################################################
# util function to just update the current/first/last positions needed for
@@ -396,14 +433,26 @@ def process_ids():
# DDP: debate here, do I get query_id, do I validate whether we are asking
# for ids not in the query? OR, dont even make/store/have query?
# marshmallow will allow us to json the data the way we need for the client
entries_schema = EntrySchema(many=True)
# Query DB for matching entries
entries = Entry.query.filter(Entry.id.in_(ids)).all()
stmt = (
select(Entry)
.options(
joinedload(Entry.file_details).joinedload(File.faces),
joinedload(Entry.file_details).joinedload(File.faces).joinedload(Face.refimg).joinedload(Refimg.person)
)
.where(Entry.id.in_(ids))
)
# return entries as json
return jsonify(entries_schema.dump(entries))
# unique as the ORM query returns a Cartesian product for the joins. E.g if file has 3 faces, the result has 3 rows of the same entry and file data, but different face data
data=db.session.execute(stmt).unique().scalars().all()
# data is now in whatever order the DB returns- faster in python than DB supposedly. So, create a mapping from id to entry for quick lookup
entry_map = {entry.id: entry for entry in data}
# Sort the entries according to the order of ids
sorted_data = [entry_map[id_] for id_ in ids if id_ in entry_map]
return jsonify(entries_schema.dump(sorted_data))
################################################################################
@@ -806,6 +855,40 @@ def view_list():
return make_response( resp )
@login_required
@app.route("/newview/", methods=["POST"])
def newview():
data = request.get_json() # Parse JSON body
eid = data.get('eid', 0) # Extract list of ids
# need appropriate schema? to get FaceData with entry, lists should just be
# what we have in entryList so it can help with next/prev
# include Entry for name/path, ffl (model_used), frl (distance), Face (for w/h, etc), Person (id,tag)
#stmt=select(Entry).filter(Entry.id==eid)
stmt = (
select(Entry)
.options(
joinedload(Entry.file_details).joinedload(File.faces),
joinedload(Entry.file_details).joinedload(File.faces).joinedload(Face.refimg).joinedload(Refimg.person)
)
.where(Entry.id == eid)
)
print( stmt )
# this needs unique because:
# entry (one row for id=660)
# file (one row, since file_details is a one-to-one relationship)
# face (many rows, since a file can have many faces)
# refimg and person (one row per face, via the link tables)
# The SQL query returns a Cartesian product for the joins involving collections (like faces). For example, if your file has 3 faces,
# the result set will have 3 rows, each with the same entry and file data, but different face, refimg, and person data.
data=db.session.execute(stmt).unique().scalars().all()
print( data )
return jsonify(entries_schema.dump(data))
################################################################################
# /view/id -> grabs data from DB and views it (GET)
################################################################################
@@ -836,14 +919,16 @@ def view(id):
eids=eids.rstrip(",")
# jic, sometimes we trip this, and rather than show broken pages / destroy
if id not in eids:
SetFELog( message=f"ERROR: viewing an id, but its not in eids OPT={OPT}, id={id}, eids={eids}", level="danger", persistent=True, cant_close=False)
msg="Sorry, viewing data is confused, cannot view this image now"
if os.environ['ENV'] == "production":
msg += "Clearing out all states. This means browser back buttons will not work, please start a new tab and try again"
PA_UserState.query.delete()
db.session.commit()
SetFELog( msg, "warning", persistent=True, cant_close=False )
return redirect("/")
# SetFELog( message=f"ERROR: viewing an id, but its not in eids OPT={OPT}, id={id}, eids={eids}", level="danger", persistent=True, cant_close=False)
# msg="Sorry, viewing data is confused, cannot view this image now"
# if os.environ['ENV'] == "production":
# msg += "Clearing out all states. This means browser back buttons will not work, please start a new tab and try again"
# PA_UserState.query.delete()
# db.session.commit()
# SetFELog( msg, "warning", persistent=True, cant_close=False )
# return redirect("/")
print( f"id={id}, eids={eids}" )
return "200"
else:
NMO_data = FaceOverrideType.query.all()
setting = Settings.query.first()

View File

@@ -523,15 +523,19 @@ function drawPageOfFigures()
ecnt++
}
$('.figure').click( function(e) { DoSel(e, this ); SetButtonState(); return false; });
$('.figure').dblclick( CallViewRouteWrapper )
$('.figure').dblclick( function(e) { dblClickToViewEntry( $(this).attr('id') ) } )
// 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 to get the 'page' of entry ids out of entryList
function getPage(pageNumber)
function getPage(pageNumber,viewing_idx=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.howMany;
const endIndex = startIndex + OPT.howMany;
pageList = entryList.slice(startIndex, endIndex);
@@ -549,7 +553,13 @@ function getPage(pageNumber)
dataType: 'json', // Expect JSON response
success: function(res) {
document.entries=res
// add all the figures to files_div
drawPageOfFigures()
// noting we could have been in files_div, or viewer_div, update both jic
// and fix viewer_div - update viewing, arrows and image/video too
document.viewing=document.entries[viewing_idx]
resetNextPrevButtons()
ViewImageOrVideo()
},
error: function(xhr, status, error) {
console.error("Error:", error);
@@ -628,3 +638,8 @@ function prevPage()
resetNextPrevButtons()
return
}
function isMobile() {
try{ document.createEvent("TouchEvent"); return true; }
catch(e){ return false; }
}

View File

@@ -87,11 +87,11 @@ function DrawImg()
// if we have faces, the enable the toggles, otherwise disable them
// and reset model select too
if( objs[current].faces )
if( document.viewing.faces )
{
$('#faces').attr('disabled', false)
$('#distance').attr('disabled', false)
$('#model').val( Number(objs[current].face_model) )
$('#model').val( Number(document.viewing.face_model) )
}
else
{
@@ -102,33 +102,33 @@ function DrawImg()
}
// okay, we want faces drawn so lets do it
if( $('#faces').prop('checked') && objs[current].faces )
if( $('#faces').prop('checked') && document.viewing.faces )
{
// draw rect on each face
for( i=0; i<objs[current].faces.length; i++ )
for( i=0; i<document.viewing.faces.length; i++ )
{
x = objs[current].faces[i].x / ( im.width/canvas.width )
y = objs[current].faces[i].y / ( im.height/canvas.height )
w = objs[current].faces[i].w / ( im.width/canvas.width )
h = objs[current].faces[i].h / ( im.height/canvas.height )
x = document.viewing.faces[i].x / ( im.width/canvas.width )
y = document.viewing.faces[i].y / ( im.height/canvas.height )
w = document.viewing.faces[i].w / ( im.width/canvas.width )
h = document.viewing.faces[i].h / ( im.height/canvas.height )
context.beginPath()
context.rect( x, y, w, h )
context.lineWidth = 2
// this face has an override so diff colour
if( objs[current].faces[i].override )
if( document.viewing.faces[i].override )
{
context.strokeStyle = 'blue'
DrawLabelOnFace( objs[current].faces[i].override.who )
DrawLabelOnFace( document.viewing.faces[i].override.who )
}
else
{
context.strokeStyle = 'green'
if( objs[current].faces[i].who )
if( document.viewing.faces[i].who )
{
str=objs[current].faces[i].who
str=document.viewing.faces[i].who
if( $('#distance').prop('checked') )
str += "("+objs[current].faces[i].distance+")"
str += "("+document.viewing.faces[i].distance+")"
DrawLabelOnFace( str )
}
}
@@ -156,13 +156,13 @@ function FaceToggle()
// also deals with fullsecreen if needed
function ViewImageOrVideo()
{
if( objs[current].type == 'Image' )
if( document.viewing.type.name == 'Image' )
{
im.src='../' + objs[current].url
im.src='../' + document.viewing.FullPathOnFS
$('#video_div').hide()
if( $('#fname_toggle').prop('checked' ) )
$('#img-cap').show()
$('#fname_i').html(PrettyFname(objs[current].url))
$('#fname_i').html(PrettyFname(document.viewing.FullPathOnFS))
$('#figure').show()
if( fullscreen )
$('#canvas').get(0).requestFullscreen()
@@ -170,11 +170,11 @@ function ViewImageOrVideo()
if( document.fullscreen )
document.exitFullscreen()
}
if( objs[current].type == 'Video' )
if( document.viewing.type.name == 'Video' )
{
$('#figure').hide()
$('#video').prop('src', '../' + objs[current].url )
$('#fname_v').html(PrettyFname(objs[current].url))
$('#video').prop('src', '../' + document.viewing.FullPathOnFS )
$('#fname_v').html(PrettyFname(document.viewing.FullPathOnFS))
if( $('#fname_toggle').prop('checked' ) )
$('#img-cap').hide()
ResizeVideo()
@@ -221,32 +221,32 @@ $(document).ready( function()
item_list = { not_a_face: { name: "Not a face", which_face: '-1' } }
for( i=0; i<objs[current].faces.length; i++ )
for( i=0; i<document.viewing.faces.length; i++ )
{
fx = objs[current].faces[i].x / ( im.width/canvas.width )
fy = objs[current].faces[i].y / ( im.height/canvas.height )
fw = objs[current].faces[i].w / ( im.width/canvas.width )
fh = objs[current].faces[i].h / ( im.height/canvas.height )
fx = document.viewing.faces[i].x / ( im.width/canvas.width )
fy = document.viewing.faces[i].y / ( im.height/canvas.height )
fw = document.viewing.faces[i].w / ( im.width/canvas.width )
fh = document.viewing.faces[i].h / ( im.height/canvas.height )
if( x >= fx && x <= fx+fw && y >= fy && y <= fy+fh )
{
if( objs[current].faces[i].override )
if( document.viewing.faces[i].override )
{
item_list['remove_force_match_override']={ 'name': 'Remove override for this face', 'which_face': i, 'id': objs[current].faces[i].id }
item_list['remove_force_match_override']={ 'name': 'Remove override for this face', 'which_face': i, 'id': document.viewing.faces[i].id }
}
else if( objs[current].faces[i].who )
else if( document.viewing.faces[i].who )
{
item_list['match']={ 'name': objs[current].faces[i].who, 'which_face': i, 'id': objs[current].faces[i].id }
item_list['match_add_refimg']={ 'name': 'Add this as refimg for ' + objs[current].faces[i].who,
'person_id': objs[current].faces[i].pid, 'who': objs[current].faces[i].who, 'which_face': i, 'id': objs[current].faces[i].id, }
item_list['wrong_person']={ 'name': 'wrong person', 'which_face': i, 'id': objs[current].faces[i].id }
item_list['match']={ 'name': document.viewing.faces[i].who, 'which_face': i, 'id': document.viewing.faces[i].id }
item_list['match_add_refimg']={ 'name': 'Add this as refimg for ' + document.viewing.faces[i].who,
'person_id': document.viewing.faces[i].pid, 'who': document.viewing.faces[i].who, 'which_face': i, 'id': document.viewing.faces[i].id, }
item_list['wrong_person']={ 'name': 'wrong person', 'which_face': i, 'id': document.viewing.faces[i].id }
}
else
{
item_list['no_match_new_person']={ 'name': 'Add as reference image to NEW person', 'which_face': i, 'id': objs[current].faces[i].id }
item_list['no_match_new_refimg']={ 'name': 'Add as reference image to EXISTING person', 'which_face': i, 'id': objs[current].faces[i].id }
item_list['no_match_new_person']={ 'name': 'Add as reference image to NEW person', 'which_face': i, 'id': document.viewing.faces[i].id }
item_list['no_match_new_refimg']={ 'name': 'Add as reference image to EXISTING person', 'which_face': i, 'id': document.viewing.faces[i].id }
for( var el in NMO ) {
item_list['NMO_'+el]={'type_id': NMO[el].type_id, 'name': 'Override: ' + NMO[el].name, 'which_face': i, 'id': objs[current].faces[i].id }
item_list['NMO_'+el]={'type_id': NMO[el].type_id, 'name': 'Override: ' + NMO[el].name, 'which_face': i, 'id': document.viewing.faces[i].id }
}
}
delete item_list['not_a_face']
@@ -280,11 +280,11 @@ function OverrideForceMatch( person_id, key )
}
ofm='&person_id='+person_id+'&face_id='+item[key].id
$.ajax({ type: 'POST', data: ofm, url: '/add_force_match_override', success: function(data) {
objs[current].faces[item[key].which_face].override={}
objs[current].faces[item[key].which_face].override.who=data.person_tag
objs[current].faces[item[key].which_face].override.distance='N/A'
objs[current].faces[item[key].which_face].override.type_id=NMO[fm_idx].id
objs[current].faces[item[key].which_face].override.type_name=NMO[fm_idx].name
document.viewing.faces[item[key].which_face].override={}
document.viewing.faces[item[key].which_face].override.who=data.person_tag
document.viewing.faces[item[key].which_face].override.distance='N/A'
document.viewing.faces[item[key].which_face].override.type_id=NMO[fm_idx].id
document.viewing.faces[item[key].which_face].override.type_name=NMO[fm_idx].name
$('#dbox').modal('hide')
$('#faces').prop('checked',true)
@@ -303,8 +303,8 @@ function CreatePersonAndRefimg( key )
+'&refimg_data='+item[key].refimg_data
$.ajax({ type: 'POST', data: d, url: '/match_with_create_person',
success: function(data) {
objs[current].faces[item[key].which_face].who=data.who
objs[current].faces[item[key].which_face].distance=data.distance
document.viewing.faces[item[key].which_face].who=data.who
document.viewing.faces[item[key].which_face].distance=data.distance
$('#dbox').modal('hide')
$('#faces').prop('checked',true)
DrawImg()
@@ -318,8 +318,8 @@ function AddRefimgTo( person_id, key, search )
d='&face_id='+item[key].id+'&person_id='+person_id+'&refimg_data='+item[key].refimg_data+'&search='+search
$.ajax({ type: 'POST', data: d, url: '/add_refimg_to_person',
success: function(data) {
objs[current].faces[item[key].which_face].who=data.who
objs[current].faces[item[key].which_face].distance=data.distance
document.viewing.faces[item[key].which_face].who=data.who
document.viewing.faces[item[key].which_face].distance=data.distance
$('#dbox').modal('hide')
$('#faces').prop('checked',true)
DrawImg()
@@ -367,15 +367,15 @@ function SearchForPerson(content, key, face_id, face_pos, type_id)
function RemoveOverrideForceMatch(face_pos)
{
if( objs[current].faces[face_pos].override )
who=objs[current].faces[face_pos].override.who
if( document.viewing.faces[face_pos].override )
who=document.viewing.faces[face_pos].override.who
else
who=objs[current].faces[face_pos].who
who=document.viewing.faces[face_pos].who
d='&face_id='+objs[current].faces[face_pos].id+'&person_tag='+who+'&file_eid='+current
d='&face_id='+document.viewing.faces[face_pos].id+'&person_tag='+who+'&file_eid='+current
$.ajax({ type: 'POST', data: d, url: '/remove_force_match_override',
success: function(data) {
delete objs[current].faces[face_pos].override
delete document.viewing.faces[face_pos].override
$('#dbox').modal('hide')
DrawImg()
CheckForJobs()
@@ -387,10 +387,10 @@ function RemoveOverrideForceMatch(face_pos)
function RemoveOverrideNoMatch(face_pos, type_id)
{
d='&face_id='+objs[current].faces[face_pos].id+'&type_id='+type_id
d='&face_id='+document.viewing.faces[face_pos].id+'&type_id='+type_id
$.ajax({ type: 'POST', data: d, url: '/remove_no_match_override',
success: function(data) {
delete objs[current].faces[face_pos].override
delete document.viewing.faces[face_pos].override
$('#dbox').modal('hide')
DrawImg()
CheckForJobs()
@@ -405,11 +405,11 @@ function AddNoMatchOverride(type_id, face_id, face_pos, type_id)
d='&type_id='+type_id+'&face_id='+face_id
$.ajax({ type: 'POST', data: d, url: '/add_no_match_override',
success: function(data) {
objs[current].faces[face_pos].override={}
objs[current].faces[face_pos].override.who=NMO[type_id].name
objs[current].faces[face_pos].override.distance='N/A'
objs[current].faces[face_pos].override.type_id=type_id
objs[current].faces[face_pos].override.type_name=NMO[type_id].name
document.viewing.faces[face_pos].override={}
document.viewing.faces[face_pos].override.who=NMO[type_id].name
document.viewing.faces[face_pos].override.distance='N/A'
document.viewing.faces[face_pos].override.type_id=type_id
document.viewing.faces[face_pos].override.type_name=NMO[type_id].name
$('#dbox').modal('hide')
$('#faces').prop('checked',true)
DrawImg()
@@ -457,17 +457,17 @@ function FaceDBox(key, item)
div+='</div><div class="col-6">'
if ( key == 'remove_force_match_override' )
{
if( objs[current].faces[face_pos].override.type_name == 'Manual match to existing person' )
div+='<div class="row col-12">remove this override (force match to: ' + objs[current].faces[face_pos].override.who + ')</div>'
if( document.viewing.faces[face_pos].override.type_name == 'Manual match to existing person' )
div+='<div class="row col-12">remove this override (force match to: ' + document.viewing.faces[face_pos].override.who + ')</div>'
else
div+='<div class="row col-12">remove this override (no match)</div>'
div+='<div class="row">'
div+='<button class="btn btn-outline-info col-6" type="button" onClick="$(\'#dbox\').modal(\'hide\'); return false">Cancel</button>'
div+='<button class="btn btn-outline-danger col-6" type="button" '
if( objs[current].faces[face_pos].override.type_name == 'Manual match to existing person' )
if( document.viewing.faces[face_pos].override.type_name == 'Manual match to existing person' )
div+='onClick="RemoveOverrideForceMatch(' +face_pos+ ')">Remove</button>'
else
div+='onClick="RemoveOverrideNoMatch(' +face_pos+','+objs[current].faces[face_pos].override.type_id+ ')">Remove</button>'
div+='onClick="RemoveOverrideNoMatch(' +face_pos+','+document.viewing.faces[face_pos].override.type_id+ ')">Remove</button>'
div+='</div>'
}
if ( key == 'no_match_new_person' )
@@ -559,3 +559,8 @@ function JoblogSearch()
}
})
}
function setVideoSource(newSrc) {
$('#videoSource').attr('src', newSrc);
$('#video')[0].load();
}

View File

@@ -1,4 +1,5 @@
numpy==1.26.4
setuptools
flask
flask_login
flask-ldap3-login

View File

@@ -16,11 +16,15 @@
move_paths.push(p)
{% endfor %}
// GLOBALS
// GLOBALS
// OPTions set via GUI, will change if we alter drop-downs, etc. in GUI
// TODO: reference these from GUI, so we can finally ditch the form to submit/change them.
// BUT -- must handle noo changing with a form/post as it requires a new ordering
// this is which eid we are viewing an image/video (when we dbl-click & then next/prev)
document.viewing_eid=null;
document.viewing=null;
var OPT={}
OPT.grouping='{{OPT.grouping}}'
OPT.cwd='{{OPT.cwd}}'
@@ -39,7 +43,8 @@
getPage(1)
</script>
<div class="container-fluid">
<div id="files_div">
<div class="container-fluid">
<input type="hidden" name="cwd" id="cwd" value="{{OPT.cwd}}">
{% if search_term is defined %}
<input type="hidden" name="search_term" id="view_term" value="{{search_term}}">
@@ -104,7 +109,7 @@
</button>
<button style="visibility:hidden" class="btn btn-outline-secondary" aria-label="shift-key" id="shift-key" onclick="document.fake_shift=1-document.fake_shift; event.stopPropagation(); return false">shift</button>
<button style="visibility:hidden" class="btn btn-outline-secondary" aria-label="ctrl-key" id="ctrl-key" onclick="document.fake_ctrl=1-document.fake_ctrl; event.stopPropagation(); return false">ctrl</button>
</div>
</div class="col flex-grow-1">
<div class="d-flex col col-auto justify-content-end">
<div class="btn-group">
{% if OPT.size == 64 %}
@@ -138,155 +143,297 @@
{% endif %}
<button aria-label="extra large" id="256" class="px-2 sm-txt sz-but btn {{bt}}" onClick="$('#size').val(256)">XL</button>
</div class="btn-group">
</div class="col">
</div class="d-flex col">
<input id="offset" type="hidden" name="offset" value="{{OPT.offset}}">
<input id="size" type="hidden" name="size" value="{{OPT.size}}">
</div class="form-row">
</div class="d-flex row mb-2">
{% set eids=namespace( str="" ) %}
{# gather all the file eids and collect them in case we go gallery mode #}
<input name="eids" id="eids" type="hidden" value="{{query_data.entry_list}}">
</div>
{% set ecnt=namespace( val=0 ) %}
<div id="figures" class="row ms-2">
<!--
{% set last = namespace(printed=0) %}
{# rare event of empty folder, still need to show back button #}
{% if OPT.folders and entry_data|length == 0 %}
{% if OPT.cwd != OPT.root %}
<figure id="_back" class="dir entry m-1" ecnt="{{ecnt.val}}" dir="{{OPT.cwd|ParentPath}}" type="Directory">
<svg class="svg" width="{{OPT.size|int-22}}" height="{{OPT.size|int-22}}"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#folder_back"/></svg>
<figcaption class="figure-caption text-center">Back</figcaption>
</figure class="figure">
{% set ecnt.val=ecnt.val+1 %}
<script>f=$('#_back'); w=f.find('svg').width(); f.find('figcaption').width(w);</script>
{% else %}
<div class="col col-auto g-0 m-1">
<svg class="svg" width="{{OPT.size|int-22}}" height="{{OPT.size|int-22}}"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#folder_back_gray"/></svg>
</div>
{% endif %}
{% endif %}
{% if not entry_data %}
<span class="alert alert-danger p-2 col-auto"> No matches for: '{{search_term}}'</span>
{% endif %}
{% for obj in entry_data %}
{% if loop.index==1 and OPT.folders %}
{% if OPT.cwd != OPT.root %}
<figure class="col col-auto g-0 dir entry m-1" ecnt="{{ecnt.val}}" dir="{{OPT.cwd|ParentPath}}" type="Directory">
<svg class="svg" width="{{OPT.size|int-22}}" height="{{OPT.size|int-22}}" fill="currentColor">
<use xlink:href="{{url_for('internal', filename='icons.svg')}}#folder_back"/></svg>
<figcaption class="svg_cap figure-caption text-center">Back</figcaption>
</figure class="figure">
{% set ecnt.val=ecnt.val+1 %}
{% else %}
{# create an even lighter-grey, unclickable back button - so folders dont jump around when you go into them #}
<div class="col col-auto g-0 m-1">
<svg class="svg" width="{{OPT.size|int-22}}" height="{{OPT.size|int-22}}"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#folder_back_gray"/></svg>
</div>
{% endif %}
{% endif %}
{% if not OPT.folders and obj.type.name == "Directory" %}
{% continue %}
{% endif %}
{% if OPT.grouping == "Day" %}
{% if last.printed != obj.file_details.day %}
<div class="row ps-3"><h6>Day: {{obj.file_details.day}} of {{obj.file_details.month}}/{{obj.file_details.year}}</h6></div>
{% set last.printed = obj.file_details.day %}
{% endif %}
{% elif OPT.grouping == "Week" %}
{% if last.printed != obj.file_details.woy %}
<div class="row ps-3"><h6>Week #: {{obj.file_details.woy}} of {{obj.file_details.year}}</h6></div>
{% set last.printed = obj.file_details.woy %}
{% endif %}
{% elif OPT.grouping == "Month" %}
{% if last.printed != obj.file_details.month %}
<div class="row ps-3"><h6>Month: {{obj.file_details.month}} of {{obj.file_details.year}}</h6></div>
{% set last.printed = obj.file_details.month %}
{% endif %}
{% endif %}
{% if obj.type.name == "Image" or obj.type.name == "Video" or obj.type.name == "Unknown" %}
{% if (not OPT.folders) or ((obj.in_dir.in_path.path_prefix+'/'+obj.in_dir.rel_path+'/'+obj.name) | TopLevelFolderOf(OPT.cwd)) %}
<figure id="{{obj.id}}" ecnt="{{ecnt.val}}" class="col col-auto g-0 figure entry m-1" path_type="{{obj.in_dir.in_path.type.name}}" size="{{obj.file_details.size_mb}}" hash="{{obj.file_details.hash}}" in_dir="{{obj.in_dir.in_path.path_prefix}}/{{obj.in_dir.rel_path}}" fname="{{obj.name}}" yr="{{obj.file_details.year}}" date="{{obj.file_details.year}}{{"%02d" % obj.file_details.month}}{{"%02d" % obj.file_details.day}}" pretty_date="{{obj.file_details.day}}/{{obj.file_details.month}}/{{obj.file_details.year}}" type="{{obj.type.name}}">
{% if obj.type.name=="Image" or obj.type.name=="Unknown" %}
<div style="position:relative; width:100%">
{% if obj.file_details.thumbnail %}
<a href="{{obj.in_dir.in_path.path_prefix}}/{{obj.in_dir.rel_path}}/{{obj.name}}">
<img alt="{{obj.name}}" class="thumb" height="{{OPT.size}}" src="data:image/jpeg;base64,{{obj.file_details.thumbnail}}"></img></a>
{% else %}
<a href="{{obj.in_dir.in_path.path_prefix}}/{{obj.in_dir.rel_path}}/{{obj.name}}">
<svg width="{{OPT.size}}" height="{{OPT.size}}" fill="white"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#unknown_ftype"/></svg>
</a>
{% endif %}
{% if search_term is defined %}
<div style="position:absolute; bottom: 0px; left: 2px;">
<svg width="16" height="16" fill="white"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#{{LocationIcon(obj)}}"/></svg>
</div>
{% endif %}
<div id="s{{obj.id}}" style="display:none; position:absolute; top: 50%; left:50%; transform:translate(-50%, -50%);">
<img height="64px" src="{{url_for('internal', filename='throbber.gif')}}"></img>
</div>
</div>
{% elif obj.type.name == "Video" %}
<div style="position:relative; width:100%">
{% if obj.file_details.thumbnail %}
<a href="{{obj.in_dir.in_path.path_prefix}}/{{obj.in_dir.rel_path}}/{{obj.name}}">
<img alt="{{obj.name}}" class="thumb" height="{{OPT.size}}" src="data:image/jpeg;base64,{{obj.file_details.thumbnail}}"></img></a>
{% else %}
<a href="{{obj.in_dir.in_path.path_prefix}}/{{obj.in_dir.rel_path}}/{{obj.name}}">
<svg width="{{OPT.size}}" height="{{OPT.size}}" fill="white"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#unknown_ftype"/></svg>
</a>
{% endif %}
<div style="position:absolute; top: 0px; left: 2px;">
<svg width="16" height="16" fill="white"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#film"/></svg>
</div>
{% if search_term is defined %}
<div style="position:absolute; bottom: 0px; left: 2px;">
<svg width="16" height="16" fill="white"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#{{LocationIcon(obj)}}"/></svg>
</div>
{% endif %}
</div>
{% endif %}
</figure>
{% set ecnt.val=ecnt.val+1 %}
{% endif %}
{% elif obj.type.name == "Directory" %}
{% if OPT.folders %}
{% if obj.dir_details.rel_path | length %}
{% set dirname=obj.dir_details.in_path.path_prefix+'/'+obj.dir_details.rel_path %}
{% else %}
{% set dirname=obj.dir_details.in_path.path_prefix %}
{% endif %}
{# if this dir is the toplevel of the cwd, show the folder icon #}
{% if dirname| TopLevelFolderOf(OPT.cwd) %}
<figure class="col col-auto g-0 dir entry m-1" id={{obj.id}} ecnt={{ecnt.val}} dir="{{dirname}}" type="Directory">
<svg class="svg" width="{{OPT.size|int-22}}" height="{{OPT.size|int-22}}" fill="currentColor">
<use xlink:href="{{url_for('internal', filename='icons.svg')}}#Directory"/></svg>
<figcaption class="svg_cap figure-caption text-center text-wrap text-break">{{obj.name}}</figcaption>
</figure class="figure">
{% set ecnt.val=ecnt.val+1 %}
<script>f=$('#{{obj.id}}'); w=f.find('svg').width(); f.find('figcaption').width(w);</script>
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
-->
{% for obj in query_data.entry_list %}
{% set eids.str = eids.str + obj|string +"," %}
{% endfor %}
<input name="eids" id="eids" type="hidden" value="{{eids.str}}">
</div container="fluid">
<div id="figures" class="row ms-2">
</div>
<div class="container-fluid">
<input type="hidden" name="cwd" id="cwd" value="{{OPT.cwd}}">
<div class="row">
<div class="col my-auto d-flex justify-content-center">
<button aria-label="prev" id="prev" name="prev" class="prev sm-txt btn btn-outline-secondary disabled" onClick="prevPage()" disabled>
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#prev"/></svg>
</button>
<span class="sm-txt my-auto">&nbsp;{{OPT.how_many}} files&nbsp;</span>
<button aria-label="next" id="next" name="next" class="next sm-txt btn btn-outline-secondary" onClick="nextPage()">
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#next"/></svg>
</button>
</div>
</div>
</div class="container">
<input type="hidden" name="cwd" id="cwd" value="{{OPT.cwd}}">
<div class="row">
<div class="col my-auto d-flex justify-content-center">
<button aria-label="prev" id="prev" name="prev" class="prev sm-txt btn btn-outline-secondary disabled" onClick="prevPage()" disabled>
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#prev"/></svg>
</button>
<span class="sm-txt my-auto">&nbsp;{{OPT.how_many}} files&nbsp;</span>
<button aria-label="next" id="next" name="next" class="next sm-txt btn btn-outline-secondary" onClick="nextPage()">
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#next"/></svg>
</button>
</div class="col my-auto">
</div class="row">
</div class="container-fluid">
</div id="files_div">
<div id="viewer_div" class="d-none">
<style>
.norm-txt { font-size: 1.0rem }
.form-check-input:checked {
background-color: #39C0ED;
border-color: #CFF4FC;
}
.form-switch .form-check-input {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2339C0ED'/%3e%3c/svg%3e");
}
.form-switch .form-check-input:focus {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23CFF4FC'/%3e%3c/svg%3e");
}
#tst90:hover,#tst90:focus { filter: invert(73%) sepia(27%) saturate(3970%) hue-rotate(146deg) brightness(94%) contrast(100%); }
</style>
<script src="{{ url_for( 'internal', filename='js/view_transform.js')}}"></script>
<script src="{{ url_for( 'internal', filename='js/view_support.js')}}"></script>
<script>
var gap=0.8
var grayscale=0
var throbber=0
var objs=[]
var NMO=[]
var imp_path="static/Import/{{imp_path}}"
var st_path="static/Storage/{{st_path}}"
var bin_path="static/Bin/{{bin_path}}"
function PrettyFname(fname)
{
s='<span class="alert alert-secondary py-2">'
if( fname.indexOf( "static/Import" ) == 0 )
{
s+='<svg width="20" height="20" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#import"/></svg>'
tmp_path=fname.replace(imp_path,"" )
}
if( fname.indexOf( "static/Storage" ) == 0 )
{
s+='<svg width="20" height="20" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#db"/></svg>'
tmp_path=fname.replace("static/Storage","" )
}
if( fname.indexOf( "static/Bin" ) == 0 )
{
s+='<svg width="20" height="20" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#trash-fill"/></svg>'
tmp_path=fname.replace("static/Bin","" )
}
s+=tmp_path+'</span>'
return s
}
function getPreviousEntry() {
var currentIndex = entryList.indexOf(document.viewing.id);
oldPageOffset=Math.floor(currentIndex / OPT.howMany)
if (currentIndex > 0) {
currentIndex--;
pageOffset=Math.floor(currentIndex / OPT.howMany)
currentIndex=currentIndex-(pageOffset*OPT.howMany)
// pref page, load it
if( oldPageOffset != pageOffset )
// pref page is pageOffset+1 now
getPage(pageOffset+1,currentIndex)
else
document.viewing=document.entries[currentIndex]
}
}
function getNextEntry() {
var currentIndex = entryList.indexOf(document.viewing.id);
oldPageOffset=Math.floor(currentIndex / OPT.howMany)
if (currentIndex < entryList.length - 1) {
currentIndex++
pageOffset=Math.floor(currentIndex / OPT.howMany)
currentIndex=currentIndex-(pageOffset*OPT.howMany)
// next page, load it
if( oldPageOffset != pageOffset )
// next page is pageOffset+1 now
getPage(pageOffset+1,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 <= howMany
pageOffset=Math.floor(currentIndex / OPT.howMany)
currentIndex = currentIndex-(pageOffset*OPT.howMany)
document.viewing=document.entries[currentIndex]
}
function setDisabledForViewingNextPrevBttons()
{
$('#la').attr('disabled', entryIsAtStart());
$('#ra').attr('disabled', entryIsAtEnd());
}
</script>
<div id="viewer" class="container-fluid">
<div class="row">
<button title="Show previous image" class="col-auto btn btn-outline-info px-2"
style="padding: 10%" id="la"
onClick="
getPreviousEntry()
setDisabledForViewingNextPrevBttons()
ViewImageOrVideo()
">
<svg width="16" height="16" fill="currentColor">
<use xlink:href="{{url_for('internal', filename='icons.svg')}}#prev"/></svg>
</button>
<figure class="col col-auto border border-info rounded m-0 p-1" id="figure">
<canvas id="canvas"></canvas>
<img id="throbber" src="{{url_for('internal', filename='throbber.gif')}}" style="display:none;">
<script>
var im=new Image();
im.onload=DrawImg
var context = canvas.getContext('2d')
</script>
<figcaption id="img-cap" class="figure-caption text-center text-wrap text-break">
<span id="fname_i"></span></figcaption>
</figure>
<div id="video_div" class="col col-auto">
<video id="video" class="col col-auto" controls>
<source id="videoSource" src="" type="video/mp4">
Your browser does not support the video tag.
</video>
<figcaption id="vid-cap" class="figure-caption text-center text-wrap text-break">
<span id="fname_v"></span></figcaption>
</div>
<script>
window.addEventListener('resize', DrawImg, false);
window.addEventListener('resize', ResizeVideo, false);
</script>
<button title="Show next image" class="col-auto btn btn-outline-info px-2" style="padding: 10%" id="ra"
onClick="
getNextEntry()
setDisabledForViewingNextPrevBttons()
ViewImageOrVideo()
">
<svg width="16" height="16" fill="currentColor">
<use xlink:href="{{url_for('internal', filename='icons.svg')}}#next"/></svg>
</button>
</div class="row">
{# use this for color of toggles: https://www.codeply.com/p/4sL9uhevwJ #}
<div class="row">
{# this whole div, just takes up the same space as the left button and is hidden for alignment only #}
<div class="col-auto px-0">
<button class="btn btn-outline-info px-2 invisible" disabled>
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#next"/></svg>
</button>
</div>
<span class="col-auto my-auto">Show:</span>
<div title="Toggle showing filename (hotkey: n)" class="d-flex form-check form-switch border border-info rounded col col-auto my-auto py-1 justify-content-center ps-5">
<input class="form-check-input" type="checkbox" id="fname_toggle" onChange="$('.figure-caption').toggle()" checked>
<label class="form-check-label ps-1" for="fname_toggle">Filename</label>
</div>
<div title="Toggle showing matched faces (hotkey: f)" class="d-flex form-check form-switch border border-info rounded col col-auto my-auto py-1 justify-content-center ps-5">
<input class="form-check-input" type="checkbox" onChange="FaceToggle()" id="faces">
<label class="form-check-label ps-1" for="faces">Faces</label>
</div>
<div title="Toggle showing 'distance' on matched faces (hotkey: d)" class="d-flex form-check form-switch border border-info rounded col col-auto my-auto py-1 justify-content-center ps-5">
<input class="form-check-input" type="checkbox" onChange="DrawImg()" id="distance">
<label class="form-check-label ps-1" for="distance">Distance</label>
</div>
<div title="Change the model used to detect faces" class="col col-auto my-auto">
AI Model:
{# can use 0 as default, it will be (re)set correctly in DrawImg() anyway #}
{{CreateSelect( "model", 0, ["N/A", "normal", "slow/accurate"], "", "rounded norm-txt", [0,1,2])|safe }}
</div>
<div class="col col-auto pt-1">
<button class="btn btn-outline-info p-1" title="Rotate by 90 degrees" onClick="Transform(90)">
<img src="{{url_for('internal', filename='rot90.png')}}" width="32" height="32" onMouseOver="this.src='{{url_for('internal', filename='rot90-invert.png')}}'"
onMouseOut="this.src='{{url_for('internal', filename='rot90.png')}}'" />
</button>
<button class="btn btn-outline-info p-1" title="Rotate by 180 degrees" onClick="Transform(180)">
<img src="{{url_for('internal', filename='rot180.png')}}" width="32" height="32" onMouseOver="this.src='{{url_for('internal', filename='rot180-invert.png')}}'"
onMouseOut="this.src='{{url_for('internal', filename='rot180.png')}}'" />
</button>
<button class="btn btn-outline-info p-1" title="Rotate by 270 degrees" onClick="Transform(270)">
<img src="{{url_for('internal', filename='rot270.png')}}" width="32" height="32" onMouseOver="this.src='{{url_for('internal', filename='rot270-invert.png')}}'"
onMouseOut="this.src='{{url_for('internal', filename='rot270.png')}}'" />
</button>
<button class="btn btn-outline-info p-1" title="Flip horizontally" onClick="Transform('fliph')">
<svg width="32" height="32" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#flip_h"/></svg>
</button>
<button class="btn btn-outline-info p-1" title="Flip vertically" onClick="Transform('flipv')">
<svg width="32" height="32" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#flip_v"/></svg>
</button>
<button class="btn btn-outline-info p-1" title="View in Fullscreen mode (hotkey: F)" onClick="fullscreen=true; ViewImageOrVideo()">
<svg width="32" height="32" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#fullscreen"/></svg>
</button>
<button class="btn btn-outline-info p-1" title="Show logs relating to this filename (hotkey: l)" onClick="JoblogSearch()">
<svg width="32" height="32" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#log"/></svg>
</button>
<button class="btn btn-outline-info p-1" title="View Original" onClick="window.location='/'+objs[current].url">
<svg width="32" height="32" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#download"/></svg>
</button>
<button id="del" class="btn btn-outline-danger p-1" title="Delete (hotkey: Del)"
onClick="$.ajax({ type: 'POST', data: '&eid-0={{current}}', url: '/delete_files', success: function(data){ window.location='/'; return false; } })">
<svg width="32" height="32" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#trash"/></svg>
</button>
</div>
</div class="row">
</div id="viewer">
</div id="viewer_div">
<script>
$( 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.
}
});
var fullscreen=false;
</script>
{% endblock main_content %}
{% block script_content %}
@@ -294,9 +441,16 @@
$(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()
}
function CallViewRouteWrapper()
{
CallViewRoute( $(this).attr("id") )
// CallViewRoute( document.viewing.id )
}
function CallViewRoute(id)
@@ -419,11 +573,6 @@ $( document ).keydown(function(event) {
break;
} })
function isMobile() {
try{ document.createEvent("TouchEvent"); return true; }
catch(e){ return false; }
}
if( isMobile() )
{
$('#shift-key').css('visibility', 'visible');

View File

@@ -34,9 +34,7 @@
<!-- browsers can put the fakepath in for security, remove it -->
function DoMagic() {
str=$("#new_file_chooser").val()
console.log(str)
str=str.replace('C:\\fakepath\\', '' )
console.log(str)
$("#fname").val(str)
}
</script>