Compare commits
6 Commits
5f8c48ac18
...
a0e06717ac
| Author | SHA1 | Date | |
|---|---|---|---|
| a0e06717ac | |||
| 0851b79e16 | |||
| 8e6342b627 | |||
| 5b6dc1b3e9 | |||
| 59bd2af15e | |||
| 1ca5ca192c |
6
README
6
README
@@ -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
123
files.py
@@ -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()
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
numpy==1.26.4
|
||||
setuptools
|
||||
flask
|
||||
flask_login
|
||||
flask-ldap3-login
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
// 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,6 +43,7 @@
|
||||
getPage(1)
|
||||
</script>
|
||||
|
||||
<div id="files_div">
|
||||
<div class="container-fluid">
|
||||
<input type="hidden" name="cwd" id="cwd" value="{{OPT.cwd}}">
|
||||
{% if search_term is defined %}
|
||||
@@ -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,140 +143,18 @@
|
||||
{% 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 %}
|
||||
{% 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}}">
|
||||
@@ -284,9 +167,273 @@
|
||||
<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>
|
||||
</div class="container">
|
||||
<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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user