Compare commits
97 Commits
master
...
56771308a6
| Author | SHA1 | Date | |
|---|---|---|---|
| 56771308a6 | |||
| 905910ecf0 | |||
| a38c54812c | |||
| dc6b831481 | |||
| 8969cd452e | |||
| d65f3b32d3 | |||
| 0b0035d1d2 | |||
| 80ceb7aaed | |||
| 9cf47f4582 | |||
| a683da13cc | |||
| 9ffb704648 | |||
| 27e06a9462 | |||
| 4556afb3bb | |||
| 0eee594206 | |||
| 78b112d050 | |||
| 97e738dc13 | |||
| b61f048dec | |||
| e3f6b416ce | |||
| 0ac0eedef9 | |||
| cb5ff7e985 | |||
| 517b5c6167 | |||
| 16d28bc02e | |||
| da0019ecdc | |||
| e4bf9606b9 | |||
| 3a053bea49 | |||
| 1e421c3f22 | |||
| 346defde8b | |||
| 6419e20d7e | |||
| b51d9e1776 | |||
| fa2197adbe | |||
| 66344e146e | |||
| ee1c9b5494 | |||
| 846bdc4e52 | |||
| 541cbec0de | |||
| 071164d381 | |||
| 1adb1bc7af | |||
| 747f524343 | |||
| 4feae96511 | |||
| e6c9429466 | |||
| 005b5be2b9 | |||
| f369b6d796 | |||
| eb7bb84e09 | |||
| 7f13d78700 | |||
| e5d6ce9b73 | |||
| e0654edd21 | |||
| 24c2922f74 | |||
| 24c3762c61 | |||
| 40f0b5d369 | |||
| 2f5c6ec949 | |||
| 81ebf6fa01 | |||
| 7a4f5ddb17 | |||
| 00e42447fc | |||
| 50f9fbee20 | |||
| 1b9b4c9c4f | |||
| 86761994df | |||
| 4138f392d3 | |||
| 525b823632 | |||
| d019dc7960 | |||
| a15fbd74d5 | |||
| 3c8babb619 | |||
| cd73c16545 | |||
| efaec00127 | |||
| 0777bbe237 | |||
| b0c738fcc1 | |||
| 2b9e0e19a2 | |||
| e526d99389 | |||
| 5a923359bc | |||
| a147308b64 | |||
| 9e943c7e1f | |||
| 87651e80a0 | |||
| 2e952deda0 | |||
| b9b7a24326 | |||
| a7ce8e66b5 | |||
| 6199c042e3 | |||
| c32b99f53e | |||
| 175e43c9bb | |||
| 4bb99ce589 | |||
| 70ca93b14e | |||
| a0e06717ac | |||
| 0851b79e16 | |||
| 8e6342b627 | |||
| 5b6dc1b3e9 | |||
| 59bd2af15e | |||
| 1ca5ca192c | |||
| 5f8c48ac18 | |||
| b67f2d9dcb | |||
| 5842bf2ab8 | |||
| be218c5049 | |||
| a28f016b8a | |||
| d2db7f6184 | |||
| 9ec8195d0a | |||
| 2325dcd22a | |||
| e0b597c58c | |||
| 0895268df2 | |||
| efceef7e57 | |||
| 21059a6235 | |||
| 4d80fa4e7c |
20
BUGs
20
BUGs
@@ -1,14 +1,7 @@
|
|||||||
### Next: 140
|
### Next: 143
|
||||||
BUG-139: using any large entry list and going next a few times, ends say 4 pages of 50 into 4000 matches (entries from DB < 50)...
|
BUG-142: after transforming, the face data is still in the old spots, really should delete it / make it recalc
|
||||||
- confirmed this is when person has 2 or more refimgs:
|
BUG-141: can currently try to flip a video (in a highlighted group)
|
||||||
- on page "2", we get 49 pulled back in the ORM instead of the 50 expected -- b/c I use that to indicate we must be at the end of the list if not 50 found
|
BUG-140: When db is restarted underneath PA, it crashes job mgr... It should just accept timeouts, and keep trying to reconnect every 2? mins
|
||||||
-- really, need to fix once and for all the eids / re-running query.
|
|
||||||
do GetEntries as we do now, once done however, get all entry ids. Stick those into the DB with a unique query-id and datestamp
|
|
||||||
new func to get all details needed for entries in an eid list (of 1-page) - show this page of entries
|
|
||||||
use current, full eidlist and to work our start/end of list (next/prev), disabling.
|
|
||||||
then client can keep current page of data, if you hit next/prev, use DB unique query id / full list and page of eids, and give full data for new page of entries
|
|
||||||
Implications though, are if a search is invalidated (maybe delete / move a photo), need to remove them from the list on the DB too OR let user know/decide to fix/wait.
|
|
||||||
|
|
||||||
|
|
||||||
BUG-100: I managed to get 2 photos matching mich in the NOT_WORKING photo (probably dif refimgs but same p.tag?)
|
BUG-100: I managed to get 2 photos matching mich in the NOT_WORKING photo (probably dif refimgs but same p.tag?)
|
||||||
= /photos/2012/20120414-damien/IMG_8467.JPG
|
= /photos/2012/20120414-damien/IMG_8467.JPG
|
||||||
@@ -30,11 +23,6 @@ BUG-125: when an image is highlighted, then post the contextmenu on a different
|
|||||||
There is a chance we need to change the document on click to a mouse down (or whatever the context menu
|
There is a chance we need to change the document on click to a mouse down (or whatever the context menu
|
||||||
uses for default), rather than just fix the highlight
|
uses for default), rather than just fix the highlight
|
||||||
|
|
||||||
BUG-130: moving files and then trying to go next page and it got confused...
|
|
||||||
BUG-132: right arrow to go to next photo in viewer ALSO scrolls to the right, needs a return somewhere in the jscript
|
BUG-132: right arrow to go to next photo in viewer ALSO scrolls to the right, needs a return somewhere in the jscript
|
||||||
BUG-133: when rebuilding pa[dev], the first run fails to have symlinks to the right paths for Import/Storage, etc. a simple restart fixes - so potentially the intial copy or some other race condition?
|
|
||||||
BUG-134: when moving set of photos on page, then move another set of photos on page, the first set reappears. Could really delete them from the dom?
|
BUG-134: when moving set of photos on page, then move another set of photos on page, the first set reappears. Could really delete them from the dom?
|
||||||
BUG-135: failed to rotate: 2006/20061215-ITS-xmas-KP/DSC00582.JPG - not sure why && not repeatable, so its not the image, timing/race condition maybe?
|
|
||||||
BUG-137: after moving/refiling photos, the next shift-click is out of order (reload fixes it)
|
BUG-137: after moving/refiling photos, the next shift-click is out of order (reload fixes it)
|
||||||
BUG-138: Placeholder for all the ways we can get the front-end confused:
|
|
||||||
---> JUST fix all these BUGs (relating to confused/lost state) by revisiting the overally complex way I remember state and my position in a list (probably FAR easier, to make an initial sql just save all eids, and then not try to recreate that list ever again and not care how I got into the list). Can attach a "running server-side sequence number", and if old sequence, and the original eid list results in a failure, then just pop up that the saved list is no longer valid, and ask user to re-do their search/list..."
|
|
||||||
|
|||||||
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
|
# 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
|
# 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
|
# pg_dump --user=pa pa > /docker-entrypoint-initdb.d/bkup.sql
|
||||||
### check sql looks good
|
### 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 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 rm /srv/docker/container/padb/docker-entrypoint-initdb.d/users.sql
|
||||||
sudo docker-compose -f /srv/docker/config/docker-compose.yaml build padb
|
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 )
|
( 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:
|
HANDY SQLs/commands:
|
||||||
|
|||||||
52
TODO
52
TODO
@@ -1,32 +1,25 @@
|
|||||||
### major fix - go to everywhere I call GetEntries(), and redo the logic totally...
|
* new viewing model (get ids of query on first load, then paginate only inside that known list)
|
||||||
* firstly, run the query as per normal, but get just the matched eids into an entry_lst
|
- BUT, when we finish a delete, what do I do with pageList / entryList???
|
||||||
* make a unique query_id for this entry_lst, and store entry_ids into "query" table, with a unique query_id
|
- start by showing them as deleted (via amend)
|
||||||
* take most of pa_user_state that relates to query state and move it to the "query" table per query_id
|
- then on success, remove the ids from the *List arrays in js -- but do
|
||||||
* pa_user_state then becomes defaults for next query (so how_many, noo, etc)
|
this via repagination, invalidate page cache fully, then getPage(currentPage)
|
||||||
|
(e.g. assume 1, 2, 3 ... 40 in eList). delete 23, 24,
|
||||||
|
then reset lists to remove 23 and 24, pageList would then
|
||||||
|
get reset to page with: 21,22,25,26 ... 30, 31
|
||||||
|
|
||||||
* we can age out queries form the query_table after a few months?
|
? get rid of style and just use class -- think this should work, so change in
|
||||||
* client side always has query_id. IF DB does not have query_id, then its really old? - just say so...
|
templates/files.html for throbber, etc. and dont set style as much in view_support.js
|
||||||
|
|
||||||
* client side takes query_id, entry_lst, current_eid, offset, first/last_eid, etc. as part of its first route / html creation.
|
|
||||||
* it then decides based on all this to GetEntryDetails( subset of entry_lst ) <- needs new route
|
|
||||||
* IN THEORY some of the subset of entry_lst don't exist -- BUT, we can handle that on response, e.g. say my query used to have 1,2,3, and since then another user/action deleted 2:
|
|
||||||
- I ask for details on 1,2,3 and get back details on 1,3 only.
|
|
||||||
- On client-side, I can say, since you ran this query, data in PA has changed - image#2 is no longer in PA.
|
|
||||||
Please run a new query (or bonus points, maybe have enough of the original query to note this and ask, do you want to ignore changes, or re-run query and get latest data?)
|
|
||||||
* client can go fwd or back in the entry_lst same as now (disabling buttons as needed), BUT as entry_lst is NOT recreated per page move, then no chance to get confused about first/last
|
|
||||||
* client side:
|
|
||||||
* for real chance to stop confusion, instead of removing deleted images from DOM, we should gray them out and put a big Del (red circle with line?) though it as overlay.
|
|
||||||
* Create another table is entry_ammendments - note the deletions, rotations, flips of specific eids - then reproduce that on the client side visually as needed
|
|
||||||
- at least grayed-out, to indicate a pending action is not complete.
|
|
||||||
- When job that flips, rotates, deletes completes then lets update the query details (e.g. remove eids, or remove the ammendments)
|
|
||||||
- this actually is quite an improvement, if someone is deleting 2 as per above, I will see that as a pending change in my unrelated query, ditto flips, etc.
|
|
||||||
|
|
||||||
### GENERAL
|
### GENERAL
|
||||||
* jobs for AI should show path name
|
* jobs for AI should show path name
|
||||||
* rm dups job should show progress bar
|
* rm dups job should show progress bar
|
||||||
* in viewer, there is no move button (maybe add one?)
|
* in viewer, there is no move button (maybe add one?)
|
||||||
|
* think I killed pa_job_manager without passing an eid to a transform job, shouldn't crash
|
||||||
|
- SHOULD JUST get AI to help clean-up and write defensive code here...
|
||||||
* consider doing duplicates before AI, and if there are say 100s+, then maybe pause the AI work
|
* consider doing duplicates before AI, and if there are say 100s+, then maybe pause the AI work
|
||||||
- had 5000+ new photos, took 8 hours to finish, for me to just delete them anyway
|
- had 5000+ new photos, took 8 hours to finish, for me to just delete them anyway
|
||||||
|
* consider how to better version jscript - across all html files, consistently
|
||||||
|
- mtime, didnt work anyway, my phone still wont pick up the change, it was adding any ?v= changed this (once)
|
||||||
* optimisation:
|
* optimisation:
|
||||||
- keep track of just new files since scan (even if we did this from the DB),
|
- keep track of just new files since scan (even if we did this from the DB),
|
||||||
then we could just feed those eid's explicitly into a 'get_file_details_on_new_files'.
|
then we could just feed those eid's explicitly into a 'get_file_details_on_new_files'.
|
||||||
@@ -37,9 +30,7 @@
|
|||||||
(is there a library for this???)
|
(is there a library for this???)
|
||||||
|
|
||||||
* sqlalchemy 2 migration:
|
* sqlalchemy 2 migration:
|
||||||
* fix unmapped (in fact make all the code properly sqlachemy 2.0 compliant)
|
- get AI to help
|
||||||
-- path.py has the __allow_unmapped__ = True
|
|
||||||
* remove all '.execute' from *.py
|
|
||||||
|
|
||||||
* allow actions for wrong person:
|
* allow actions for wrong person:
|
||||||
-> someone else? OR override no match for this person ever for this image?
|
-> someone else? OR override no match for this person ever for this image?
|
||||||
@@ -68,10 +59,6 @@
|
|||||||
- rename (does this work already somehow? see issue below)
|
- rename (does this work already somehow? see issue below)
|
||||||
- dont allow me to stupidly move a folder to itself
|
- dont allow me to stupidly move a folder to itself
|
||||||
|
|
||||||
* browser back/forward buttons dont work -- use POST -> redirect to GET
|
|
||||||
- need some sort of clean-up of pa_user_state -- I spose its triggered by browser session, so maybe just after a week is lazy/good enough
|
|
||||||
- pa_user_state has last_used as a timestamp so can be used to delete old entries
|
|
||||||
|
|
||||||
* back button will fail if we do these POSTs:
|
* back button will fail if we do these POSTs:
|
||||||
job.py:@app.route("/jobs", methods=["GET", "POST"])
|
job.py:@app.route("/jobs", methods=["GET", "POST"])
|
||||||
job.py:@app.route("/job/<id>", methods=["GET","POST"])
|
job.py:@app.route("/job/<id>", methods=["GET","POST"])
|
||||||
@@ -79,8 +66,8 @@
|
|||||||
* if on jobs page and jobs increase, then 'rebuild' the content of the page to show new jobs, and potentially do that every 5 seconds...
|
* if on jobs page and jobs increase, then 'rebuild' the content of the page to show new jobs, and potentially do that every 5 seconds...
|
||||||
- THINK: could also 'refresh' /job/id via Ajax not a reload, to avoid the POST issue above needing to remember job prefs somewhere?
|
- THINK: could also 'refresh' /job/id via Ajax not a reload, to avoid the POST issue above needing to remember job prefs somewhere?
|
||||||
|
|
||||||
files.py:@app.route("/fix_dups", methods=["POST"])
|
* files.py:@app.route("/fix_dups", methods=["POST"])
|
||||||
???
|
- ???
|
||||||
|
|
||||||
* GUI overhaul?
|
* GUI overhaul?
|
||||||
* on a phone, the files.html page header is a mess "Oldest.." line is too large to fit on 1 line (make it a hamburger?)
|
* on a phone, the files.html page header is a mess "Oldest.." line is too large to fit on 1 line (make it a hamburger?)
|
||||||
@@ -115,7 +102,6 @@
|
|||||||
* revisit SymlinkName() and make it simpler (see comment in shared.py)
|
* revisit SymlinkName() and make it simpler (see comment in shared.py)
|
||||||
|
|
||||||
*** Need to use thread-safe sessions per Thread, half-assed version did not work
|
*** Need to use thread-safe sessions per Thread, half-assed version did not work
|
||||||
|
|
||||||
Admin
|
Admin
|
||||||
-> do I want to have admin roles/users?
|
-> do I want to have admin roles/users?
|
||||||
-> purge deleted files (and associated DB data) needs a dbox or privs
|
-> purge deleted files (and associated DB data) needs a dbox or privs
|
||||||
|
|||||||
64
amend.py
Normal file
64
amend.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from flask import request, jsonify
|
||||||
|
from flask_login import login_required
|
||||||
|
|
||||||
|
from shared import PA
|
||||||
|
from main import db, app
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Amendments are used to define types of changes being made to an entry (e.g.
|
||||||
|
# rotate, flip) should contain relatively transient content (e.g. we might be
|
||||||
|
# processing a long-running job now, and then add a rotate, the rotate wont
|
||||||
|
# finish for minutes, so these classes allow the UI to handle that gracefully
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Class describing AmendmentType in the DB (via sqlalchemy)
|
||||||
|
################################################################################
|
||||||
|
class AmendmentType(PA,db.Model):
|
||||||
|
__tablename__ = "amendment_type"
|
||||||
|
id = db.Column(db.Integer, db.Sequence('file_type_id_seq'), primary_key=True )
|
||||||
|
job_name = db.Column(db.String, nullable=False )
|
||||||
|
which = db.Column(db.String, nullable=False )
|
||||||
|
what = db.Column(db.String, nullable=False )
|
||||||
|
colour = db.Column(db.String, nullable=False )
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Class describing which Entry has a pending Amendment in the DB (via sqlalchemy)
|
||||||
|
################################################################################
|
||||||
|
class EntryAmendment(PA,db.Model):
|
||||||
|
__tablename__ = "entry_amendment"
|
||||||
|
eid = db.Column(db.Integer, db.ForeignKey("entry.id"), primary_key=True )
|
||||||
|
job_id = db.Column(db.Integer, db.ForeignKey("job.id"), primary_key=True )
|
||||||
|
amend_type = db.Column(db.Integer, db.ForeignKey("amendment_type.id"))
|
||||||
|
type = db.relationship("AmendmentType", backref="entry_amendment")
|
||||||
|
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# check if this job is something we need to log an EntryAmendment for, based on
|
||||||
|
# job name and potentially amt in extras, to find the type of amendment
|
||||||
|
################################################################################
|
||||||
|
def inAmendmentTypes(job):
|
||||||
|
if not hasattr(job, 'extra' ) or not job.extra:
|
||||||
|
return None
|
||||||
|
amt=None
|
||||||
|
for jex in job.extra:
|
||||||
|
if jex.name == "amt":
|
||||||
|
amt=jex.value
|
||||||
|
|
||||||
|
# FIXME: should just cache this once per build, only would change with code updates
|
||||||
|
for at in getAmendments():
|
||||||
|
# for transform_image, amt=flip*, 90/180/270 - so amt will be set, use it, otherwise just use job.name
|
||||||
|
if (amt and f"{job.name}:{amt}" == at.job_name) or (at.job_name == job.name):
|
||||||
|
return at.id
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Class describing which Entry has a pending Amendment in the DB (via sqlalchemy)
|
||||||
|
################################################################################
|
||||||
|
def getAmendments():
|
||||||
|
# get Amend types (get EAT data once - used in inAmendmentTypes()
|
||||||
|
stmt=select(AmendmentType)
|
||||||
|
eat=db.session.execute(stmt).scalars().all()
|
||||||
|
return eat
|
||||||
12
face.py
12
face.py
@@ -28,9 +28,11 @@ class Face(PA,db.Model):
|
|||||||
face_left = db.Column( db.Integer )
|
face_left = db.Column( db.Integer )
|
||||||
w = db.Column( db.Integer )
|
w = db.Column( db.Integer )
|
||||||
h = db.Column( db.Integer )
|
h = db.Column( db.Integer )
|
||||||
refimg_lnk = db.relationship("FaceRefimgLink", uselist=False, viewonly=True)
|
refimg_lnk = db.relationship("FaceRefimgLink", uselist=False, viewonly=True )
|
||||||
facefile_lnk = db.relationship("FaceFileLink", uselist=False, viewonly=True)
|
facefile_lnk = db.relationship("FaceFileLink", uselist=False, viewonly=True )
|
||||||
refimg =db.relationship("Refimg", secondary="face_refimg_link", uselist=False)
|
refimg =db.relationship("Refimg", secondary="face_refimg_link", uselist=False)
|
||||||
|
fnmo = db.relationship("FaceNoMatchOverride", back_populates="face")
|
||||||
|
ffmo = db.relationship("FaceForceMatchOverride", back_populates="face")
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -62,13 +64,13 @@ class FaceRefimgLink(PA, db.Model):
|
|||||||
Attributes:
|
Attributes:
|
||||||
face_id (int): face id of row in Face table / foreign key - part primary key
|
face_id (int): face id of row in Face table / foreign key - part primary key
|
||||||
refimg_id (int): face id of row in Face table / foreign key - part primary key
|
refimg_id (int): face id of row in Face table / foreign key - part primary key
|
||||||
face_distance (int): distance value (how similar matched Face was)
|
face_distance (float): distance value (how similar matched Face was)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__tablename__ = "face_refimg_link"
|
__tablename__ = "face_refimg_link"
|
||||||
face_id = db.Column(db.Integer, db.ForeignKey("face.id"), primary_key=True )
|
face_id = db.Column(db.Integer, db.ForeignKey("face.id"), primary_key=True )
|
||||||
refimg_id = db.Column(db.Integer, db.ForeignKey("refimg.id"), primary_key=True )
|
refimg_id = db.Column(db.Integer, db.ForeignKey("refimg.id"), primary_key=True )
|
||||||
face_distance = db.Column(db.Integer)
|
face_distance = db.Column(db.Float)
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -104,6 +106,7 @@ class FaceNoMatchOverride(PA, db.Model):
|
|||||||
face_id = db.Column(db.Integer, db.ForeignKey("face.id"), primary_key=True )
|
face_id = db.Column(db.Integer, db.ForeignKey("face.id"), primary_key=True )
|
||||||
type_id = db.Column(db.Integer, db.ForeignKey("face_override_type.id"))
|
type_id = db.Column(db.Integer, db.ForeignKey("face_override_type.id"))
|
||||||
type = db.relationship("FaceOverrideType")
|
type = db.relationship("FaceOverrideType")
|
||||||
|
face = db.relationship("Face", back_populates="fnmo")
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -123,3 +126,4 @@ class FaceForceMatchOverride(PA, db.Model):
|
|||||||
face_id = db.Column(db.Integer, db.ForeignKey("face.id"), primary_key=True )
|
face_id = db.Column(db.Integer, db.ForeignKey("face.id"), primary_key=True )
|
||||||
person_id = db.Column(db.Integer, db.ForeignKey("person.id"), primary_key=True )
|
person_id = db.Column(db.Integer, db.ForeignKey("person.id"), primary_key=True )
|
||||||
person = db.relationship("Person")
|
person = db.relationship("Person")
|
||||||
|
face = db.relationship("Face", back_populates="ffmo")
|
||||||
|
|||||||
@@ -161,13 +161,13 @@
|
|||||||
c4.142,0,7.5-3.357,7.5-7.5S339.642,328,335.5,328z"/>
|
c4.142,0,7.5-3.357,7.5-7.5S339.642,328,335.5,328z"/>
|
||||||
<g style="fill:#00000025;" transform="matrix(16, 0, 0, 16, 120, 115)"><path d="M4.502 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z"/><path d="M14.002 13a2 2 0 0 1-2 2h-10a2 2 0 0 1-2-2V5A2 2 0 0 1 2 3a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v8a2 2 0 0 1-1.998 2zM14 2H4a1 1 0 0 0-1 1h9.002a2 2 0 0 1 2 2v7A1 1 0 0 0 15 11V3a1 1 0 0 0-1-1zM2.002 4a1 1 0 0 0-1 1v8l2.646-2.354a.5.5 0 0 1 .63-.062l2.66 1.773 3.71-3.71a.5.5 0 0 1 .577-.094l1.777 1.947V5a1 1 0 0 0-1-1h-10z"/></g>
|
<g style="fill:#00000025;" transform="matrix(16, 0, 0, 16, 120, 115)"><path d="M4.502 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z"/><path d="M14.002 13a2 2 0 0 1-2 2h-10a2 2 0 0 1-2-2V5A2 2 0 0 1 2 3a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v8a2 2 0 0 1-1.998 2zM14 2H4a1 1 0 0 0-1 1h9.002a2 2 0 0 1 2 2v7A1 1 0 0 0 15 11V3a1 1 0 0 0-1-1zM2.002 4a1 1 0 0 0-1 1v8l2.646-2.354a.5.5 0 0 1 .63-.062l2.66 1.773 3.71-3.71a.5.5 0 0 1 .577-.094l1.777 1.947V5a1 1 0 0 0-1-1h-10z"/></g>
|
||||||
</svg>
|
</svg>
|
||||||
<svg id="flip_h" fill="currentColor" viewBox='0 0 512 512'>
|
<svg id="flip_h" viewBox='0 0 512 512'>
|
||||||
<path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32' d='M304 48l112 112-112 112M398.87 160H96M208 464L96 352l112-112M114 352h302'/>
|
<path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32' d='M304 48l112 112-112 112M398.87 160H96M208 464L96 352l112-112M114 352h302'/>
|
||||||
</svg>
|
</svg>
|
||||||
<svg id="flip_v" fill="currentColor" viewBox='0 0 512 512'>
|
<svg id="flip_v" viewBox='0 0 512 512'>
|
||||||
<path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32' d='M464 208L352 96 240 208M352 113.13V416M48 304l112 112 112-112M160 398V96'/>
|
<path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32' d='M464 208L352 96 240 208M352 113.13V416M48 304l112 112 112-112M160 398V96'/>
|
||||||
</svg>
|
</svg>
|
||||||
<svg id="fullscreen" fill="currentColor" viewBox="0 0 16 16">
|
<svg id="fullscreen" viewBox="0 0 16 16">
|
||||||
<path fill-rule="evenodd" d="M5.828 10.172a.5.5 0 0 0-.707 0l-4.096 4.096V11.5a.5.5 0 0 0-1 0v3.975a.5.5 0 0 0 .5.5H4.5a.5.5 0 0 0 0-1H1.732l4.096-4.096a.5.5 0 0 0 0-.707zm4.344 0a.5.5 0 0 1 .707 0l4.096 4.096V11.5a.5.5 0 1 1 1 0v3.975a.5.5 0 0 1-.5.5H11.5a.5.5 0 0 1 0-1h2.768l-4.096-4.096a.5.5 0 0 1 0-.707zm0-4.344a.5.5 0 0 0 .707 0l4.096-4.096V4.5a.5.5 0 1 0 1 0V.525a.5.5 0 0 0-.5-.5H11.5a.5.5 0 0 0 0 1h2.768l-4.096 4.096a.5.5 0 0 0 0 .707zm-4.344 0a.5.5 0 0 1-.707 0L1.025 1.732V4.5a.5.5 0 0 1-1 0V.525a.5.5 0 0 1 .5-.5H4.5a.5.5 0 0 1 0 1H1.732l4.096 4.096a.5.5 0 0 1 0 .707z"/>
|
<path fill-rule="evenodd" d="M5.828 10.172a.5.5 0 0 0-.707 0l-4.096 4.096V11.5a.5.5 0 0 0-1 0v3.975a.5.5 0 0 0 .5.5H4.5a.5.5 0 0 0 0-1H1.732l4.096-4.096a.5.5 0 0 0 0-.707zm4.344 0a.5.5 0 0 1 .707 0l4.096 4.096V11.5a.5.5 0 1 1 1 0v3.975a.5.5 0 0 1-.5.5H11.5a.5.5 0 0 1 0-1h2.768l-4.096-4.096a.5.5 0 0 1 0-.707zm0-4.344a.5.5 0 0 0 .707 0l4.096-4.096V4.5a.5.5 0 1 0 1 0V.525a.5.5 0 0 0-.5-.5H11.5a.5.5 0 0 0 0 1h2.768l-4.096 4.096a.5.5 0 0 0 0 .707zm-4.344 0a.5.5 0 0 1-.707 0L1.025 1.732V4.5a.5.5 0 0 1-1 0V.525a.5.5 0 0 1 .5-.5H4.5a.5.5 0 0 1 0 1H1.732l4.096 4.096a.5.5 0 0 1 0 .707z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<svg id="unknown_ftype" fill="grey" viewBox="0 0 16 16">
|
<svg id="unknown_ftype" fill="grey" viewBox="0 0 16 16">
|
||||||
@@ -208,4 +208,7 @@
|
|||||||
<path d="M4.406 1.342A5.53 5.53 0 0 1 8 0c2.69 0 4.923 2 5.166 4.579C14.758 4.804 16 6.137 16 7.773 16 9.569 14.502 11 12.687 11H10a.5.5 0 0 1 0-1h2.688C13.979 10 15 8.988 15 7.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 2.825 10.328 1 8 1a4.53 4.53 0 0 0-2.941 1.1c-.757.652-1.153 1.438-1.153 2.055v.448l-.445.049C2.064 4.805 1 5.952 1 7.318 1 8.785 2.23 10 3.781 10H6a.5.5 0 0 1 0 1H3.781C1.708 11 0 9.366 0 7.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383z"/>
|
<path d="M4.406 1.342A5.53 5.53 0 0 1 8 0c2.69 0 4.923 2 5.166 4.579C14.758 4.804 16 6.137 16 7.773 16 9.569 14.502 11 12.687 11H10a.5.5 0 0 1 0-1h2.688C13.979 10 15 8.988 15 7.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 2.825 10.328 1 8 1a4.53 4.53 0 0 0-2.941 1.1c-.757.652-1.153 1.438-1.153 2.055v.448l-.445.049C2.064 4.805 1 5.952 1 7.318 1 8.785 2.23 10 3.781 10H6a.5.5 0 0 1 0 1H3.781C1.708 11 0 9.366 0 7.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383z"/>
|
||||||
<path d="M7.646 15.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 14.293V5.5a.5.5 0 0 0-1 0v8.793l-2.146-2.147a.5.5 0 0 0-.708.708l3 3z"/>
|
<path d="M7.646 15.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 14.293V5.5a.5.5 0 0 0-1 0v8.793l-2.146-2.147a.5.5 0 0 0-.708.708l3 3z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
<svg id="back" viewBox="0 0 16 16">
|
||||||
|
<path d="m7.247 4.86-4.796 5.481c-.566.647-.106 1.659.753 1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z"/>
|
||||||
|
</svg>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
@@ -1,3 +1,16 @@
|
|||||||
|
// GLOBAL ICON array
|
||||||
|
ICON={}
|
||||||
|
ICON["Import"]="import"
|
||||||
|
ICON["Storage"]="db"
|
||||||
|
ICON["Bin"]="trash"
|
||||||
|
|
||||||
|
// function called when we get another page from inside the files view
|
||||||
|
function getPageFigures(res, viewingIdx)
|
||||||
|
{
|
||||||
|
// add all the figures to files_div
|
||||||
|
drawPageOfFigures()
|
||||||
|
}
|
||||||
|
|
||||||
// grab all selected thumbnails and return a <div> containing the thumbnails
|
// grab all selected thumbnails and return a <div> containing the thumbnails
|
||||||
// with extra yr and date attached as attributes so we can set the default
|
// with extra yr and date attached as attributes so we can set the default
|
||||||
// dir name for a move directory - not used in del, but no harm to include them
|
// dir name for a move directory - not used in del, but no harm to include them
|
||||||
@@ -81,14 +94,14 @@ function MoveOrDelCleanUpUI()
|
|||||||
// remove the images being moved (so UI immediately 'sees' the move)
|
// remove the images being moved (so UI immediately 'sees' the move)
|
||||||
$("[name^=eid-]").each( function() { $('#'+$(this).attr('value')).remove() } )
|
$("[name^=eid-]").each( function() { $('#'+$(this).attr('value')).remove() } )
|
||||||
// reorder the images via ecnt again, so future highlighting can work
|
// reorder the images via ecnt again, so future highlighting can work
|
||||||
document.mf_id=0; $('.figure').each( function() { $(this).attr('ecnt', document.mf_id ); document.mf_id++ } )
|
// document.mf_id=0; $('.figure').each( function() { $(this).attr('ecnt', document.mf_id ); document.mf_id++ } )
|
||||||
$('#dbox').modal('hide')
|
$('#dbox').modal('hide')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// show the DBox for a move file, includes all thumbnails of selected files to move
|
// show the DBox for a move file, includes all thumbnails of selected files to move
|
||||||
// and a pre-populated folder to move them into, with text field to add a suffix
|
// and a pre-populated folder to move them into, with text field to add a suffix
|
||||||
function MoveDBox(path_details, db_url)
|
function MoveDBox(path_details)
|
||||||
{
|
{
|
||||||
$('#dbox-title').html('Move Selected File(s) to new directory in Storage Path')
|
$('#dbox-title').html('Move Selected File(s) to new directory in Storage Path')
|
||||||
div =`
|
div =`
|
||||||
@@ -98,12 +111,12 @@ function MoveDBox(path_details, db_url)
|
|||||||
<form id="mv_fm" class="form form-control-inline col-12">
|
<form id="mv_fm" class="form form-control-inline col-12">
|
||||||
<input id="move_path_type" name="move_path_type" type="hidden"
|
<input id="move_path_type" name="move_path_type" type="hidden"
|
||||||
`
|
`
|
||||||
div += ' value="' + path_details[0].type + '"></input>'
|
div += ' value="' + path_details[0].type.name + '"></input>'
|
||||||
div+=GetSelnAsDiv()
|
div+=GetSelnAsDiv()
|
||||||
yr=$('.highlight').first().attr('yr')
|
yr=$('.highlight').first().attr('yr')
|
||||||
dt=$('.highlight').first().attr('date')
|
dt=$('.highlight').first().attr('date')
|
||||||
div+='<div class="row">Use Existing Directory (in the chosen path):</div><div id="existing"></div>'
|
div+='<div class="row">Use Existing Directory (in the chosen path):</div><div id="existing"></div>'
|
||||||
GetExistingDirsAsDiv( dt, "existing", path_details[0].type )
|
GetExistingDirsAsDiv( dt, "existing", path_details[0].type.name )
|
||||||
div+=`
|
div+=`
|
||||||
<div class="input-group my-3">
|
<div class="input-group my-3">
|
||||||
<alert class="alert alert-primary my-auto py-1">
|
<alert class="alert alert-primary my-auto py-1">
|
||||||
@@ -112,7 +125,7 @@ function MoveDBox(path_details, db_url)
|
|||||||
div+= '<svg id="move_path_icon" width="20" height="20" fill="currentColor"><use xlink:href="' + path_details[0].icon_url + '"></svg>'
|
div+= '<svg id="move_path_icon" width="20" height="20" fill="currentColor"><use xlink:href="' + path_details[0].icon_url + '"></svg>'
|
||||||
div+= '<select id="rp_sel" name="rel_path" class="text-primary alert-primary py-1 border border-primary rounded" onChange="change_rp_sel()">'
|
div+= '<select id="rp_sel" name="rel_path" class="text-primary alert-primary py-1 border border-primary rounded" onChange="change_rp_sel()">'
|
||||||
for(p of path_details) {
|
for(p of path_details) {
|
||||||
div+= '<option path_type="'+p.type+'" icon_url="'+p.icon_url+'">'+p.path+'</option>'
|
div+= '<option path_type="'+p.type.name+'" icon_url="'+p.icon_url+'">'+p.root_dir+'</option>'
|
||||||
}
|
}
|
||||||
div+= '</select>'
|
div+= '</select>'
|
||||||
div+=`
|
div+=`
|
||||||
@@ -156,7 +169,7 @@ function DelDBox(del_or_undel)
|
|||||||
div+=`
|
div+=`
|
||||||
'/delete_files',
|
'/delete_files',
|
||||||
success: function(data){
|
success: function(data){
|
||||||
if( $(location).attr('pathname').match('search') !== null ) { window.location='/' }; CheckForJobs() } }); return false" class="btn btn-outline-danger col-2">Ok</button>
|
if( $(location).attr('pathname').match('search') !== null || document.viewing ) { window.location='/' }; CheckForJobs() } }); return false" class="btn btn-outline-danger col-2">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
else
|
else
|
||||||
@@ -165,7 +178,7 @@ function DelDBox(del_or_undel)
|
|||||||
div+=`
|
div+=`
|
||||||
'/restore_files',
|
'/restore_files',
|
||||||
success: function(data){
|
success: function(data){
|
||||||
if( $(location).attr('pathname').match('search') !== null ) { window.location='/' }; CheckForJobs() } }); return false" class="btn btn-outline-success col-2">Ok</button>
|
if( $(location).attr('pathname').match('search') !== null || document.viewing ) { window.location='/' }; CheckForJobs() } }); return false" class="btn btn-outline-success col-2">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
$('#dbox-content').html(div)
|
$('#dbox-content').html(div)
|
||||||
@@ -200,58 +213,82 @@ function DetailsDBox()
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// function to change the size of thumbnails (and resets button bar to newly
|
// DoSel is called when a click event occurs, and sets the selection via adding
|
||||||
// selected size)
|
|
||||||
function ChangeSize(clicked_button,sz)
|
|
||||||
{
|
|
||||||
$('.sz-but.btn-info').removeClass('btn-info text-white').addClass('btn-outline-info')
|
|
||||||
$(clicked_button).addClass('btn-info text-white').removeClass('btn-outline-info')
|
|
||||||
$('.thumb').attr( {height: sz, style: 'font-size:'+sz+'px' } )
|
|
||||||
$('#size').val(sz)
|
|
||||||
sz=sz-22
|
|
||||||
$('.svg').height(sz);
|
|
||||||
$('.svg').width(sz);
|
|
||||||
$('.svg_cap').width(sz);
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoSel is called when a click event occurs, and sets the selection via adding
|
|
||||||
// 'highlight' to the class of the appropriate thumbnails
|
// 'highlight' to the class of the appropriate thumbnails
|
||||||
// e == event (can see if shift/ctrl held down while left-clicking
|
// e == event (can see if shift/ctrl held down while left-clicking
|
||||||
// el == element the click is on
|
// el == element the click is on
|
||||||
// this allows single-click to select, ctrl-click to (de)select 1 item, and
|
// this allows single-click to select, ctrl-click to (de)select 1 item, and
|
||||||
// shift-click to add all elements between highlighted area and clicked area,
|
// shift-click to add all elements between highlighted area and clicked el,
|
||||||
// whether you click after highlight or before
|
// whether you click before highlight or after, or inside a gap and then back
|
||||||
function DoSel(e, el)
|
// or forward to the closest higlighted entry - also, only works on entry class,
|
||||||
{
|
// so it ignores figures that we take entry off while we transform, etc it
|
||||||
if( e.ctrlKey || document.fake_ctrl === 1 )
|
function DoSel(e, el) {
|
||||||
{
|
const id = $(el).attr('id');
|
||||||
$(el).toggleClass('highlight')
|
const entries = $('.entry');
|
||||||
if( document.fake_ctrl === 1 )
|
|
||||||
document.fake_ctrl=0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if( e.shiftKey || document.fake_shift === 1 )
|
|
||||||
{
|
|
||||||
st=Number($('.highlight').first().attr('ecnt'))
|
|
||||||
end=Number($('.highlight').last().attr('ecnt'))
|
|
||||||
clicked=Number($(el).attr('ecnt'))
|
|
||||||
// if we shift-click first element, then st/end are NaN, so just highlightthe one clicked
|
|
||||||
if( isNaN(st) )
|
|
||||||
{
|
|
||||||
$('.entry').slice( clicked, clicked+1 ).addClass('highlight')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if( clicked > end )
|
|
||||||
$('.entry').slice( end, clicked+1 ).addClass('highlight')
|
|
||||||
else
|
|
||||||
$('.entry').slice( clicked, st ).addClass('highlight')
|
|
||||||
|
|
||||||
if( document.fake_shift === 1 )
|
// Collect currently highlighted entries
|
||||||
document.fake_shift=0
|
const currentHighlights = $('.highlight');
|
||||||
return
|
const highlighted = new Set();
|
||||||
|
currentHighlights.each(function() {
|
||||||
|
highlighted.add($(this).attr('id'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ctrl+click: toggle highlight for the clicked entry
|
||||||
|
if (e.ctrlKey || document.fake_ctrl === 1) {
|
||||||
|
$(el).toggleClass('highlight');
|
||||||
|
if (highlighted.has(id)) {
|
||||||
|
highlighted.delete(id);
|
||||||
|
} else {
|
||||||
|
highlighted.add(id);
|
||||||
|
}
|
||||||
|
if (document.fake_ctrl === 1) {
|
||||||
|
document.fake_ctrl = 0;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Shift+click: select a range
|
||||||
|
else if (e.shiftKey || document.fake_shift === 1) {
|
||||||
|
if (currentHighlights.length === 0) {
|
||||||
|
// If no highlights, just highlight the clicked entry
|
||||||
|
$(el).addClass('highlight');
|
||||||
|
highlighted.add(id);
|
||||||
|
} else {
|
||||||
|
// Find the nearest highlighted entry
|
||||||
|
const clickedIndex = entries.index($(el));
|
||||||
|
let nearestHighlightIndex = -1;
|
||||||
|
let minDistance = Infinity;
|
||||||
|
|
||||||
|
currentHighlights.each(function() {
|
||||||
|
const highlightIndex = entries.index($(this));
|
||||||
|
const distance = Math.abs(highlightIndex - clickedIndex);
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance;
|
||||||
|
nearestHighlightIndex = highlightIndex;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Highlight the range between the nearest highlighted entry and the clicked entry
|
||||||
|
const from = Math.min(clickedIndex, nearestHighlightIndex);
|
||||||
|
const to = Math.max(clickedIndex, nearestHighlightIndex);
|
||||||
|
|
||||||
|
for (let i = from; i <= to; i++) {
|
||||||
|
const entryId = entries.eq(i).attr('id');
|
||||||
|
highlighted.add(entryId);
|
||||||
|
entries.eq(i).addClass('highlight');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (document.fake_shift === 1) {
|
||||||
|
document.fake_shift = 0;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Single click: clear all highlights and highlight the clicked entry
|
||||||
|
else {
|
||||||
|
$('.highlight').removeClass('highlight');
|
||||||
|
highlighted.clear();
|
||||||
|
$(el).addClass('highlight');
|
||||||
|
highlighted.add(id);
|
||||||
}
|
}
|
||||||
$('.highlight').removeClass('highlight')
|
|
||||||
$(el).addClass('highlight')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if a selection exists, enable move & del/restore buttons otherwise disable them
|
// if a selection exists, enable move & del/restore buttons otherwise disable them
|
||||||
@@ -316,3 +353,547 @@ function NoSel() {
|
|||||||
else
|
else
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// quick wrapper to add a single <figure> to the #figures div
|
||||||
|
function addFigure( obj )
|
||||||
|
{
|
||||||
|
html=createFigureHtml( obj )
|
||||||
|
$('#figures').append( html )
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a group header or entry based on the object and options.
|
||||||
|
* obj - The object containing file/directory details.
|
||||||
|
* returns {string} - Generated HTML string.
|
||||||
|
*/
|
||||||
|
function createFigureHtml( obj )
|
||||||
|
{
|
||||||
|
// if am is null, no amendment for this obj, otherwise we have one
|
||||||
|
var am=null
|
||||||
|
for (const tmp of document.amendments)
|
||||||
|
if( tmp.eid == obj.id )
|
||||||
|
am=tmp
|
||||||
|
|
||||||
|
let html = "";
|
||||||
|
|
||||||
|
// Image/Video/Unknown entry
|
||||||
|
if (obj.type.name === "Image" || obj.type.name === "Video" || obj.type.name === "Unknown") {
|
||||||
|
const pathType = obj.in_dir.in_path.type.name;
|
||||||
|
const size = obj.file_details.size_mb;
|
||||||
|
const hash = obj.file_details.hash;
|
||||||
|
const inDir = `${obj.in_dir.in_path.path_prefix}/${obj.in_dir.rel_path}`;
|
||||||
|
const fname = obj.name;
|
||||||
|
const yr = obj.file_details.year;
|
||||||
|
const date = `${yr}${String(obj.file_details.month).padStart(2, '0')}${String(obj.file_details.day).padStart(2, '0')}`;
|
||||||
|
const prettyDate = `${obj.file_details.day}/${obj.file_details.month}/${obj.file_details.year}`;
|
||||||
|
const type = obj.type.name;
|
||||||
|
|
||||||
|
// if amendment for this obj, do not add entry class - prevents highlighting
|
||||||
|
if( am ) {
|
||||||
|
ent=""
|
||||||
|
gs="style='filter: grayscale(100%);'"
|
||||||
|
am_html ='<img class="position-absolute top-50 start-50 translate-middle" height="60" src="/internal/white-circle.png">'
|
||||||
|
am_html +='<img class="position-absolute top-50 start-50 translate-middle" height="64" src="/internal/throbber.gif">'
|
||||||
|
if( am.type.which == 'icon' )
|
||||||
|
am_html+=`<svg class="position-absolute top-50 start-50 translate-middle" height="32" style="color:${am.type.colour}" fill="${am.type.colour}"><use xlink:href="/internal/icons.svg#${am.type.what}"></use></svg>`
|
||||||
|
else
|
||||||
|
am_html+=`<img class="position-absolute top-50 start-50 translate-middle" src="/internal/${am.type.what}?v={{js_vers['r270']}}" height="32">`
|
||||||
|
} else {
|
||||||
|
ent="entry"
|
||||||
|
gs=""
|
||||||
|
am_html=""
|
||||||
|
}
|
||||||
|
html += `
|
||||||
|
<figure id="${obj.id}" class="col col-auto g-0 figure ${ent} m-1"
|
||||||
|
path_type="${pathType}" size="${size}" hash="${hash}" in_dir="${inDir}"
|
||||||
|
fname="${fname}" yr="${yr}" date="${date}" pretty_date="${prettyDate}" type="${type}">
|
||||||
|
${renderMedia(obj,gs,am_html)}
|
||||||
|
</figure>`;
|
||||||
|
}
|
||||||
|
// Directory entry
|
||||||
|
else if (obj.type.name === "Directory" && OPT.folders) {
|
||||||
|
const dirname = obj.dir_details.rel_path.length
|
||||||
|
? `${obj.dir_details.in_path.path_prefix}/${obj.dir_details.rel_path}`
|
||||||
|
: obj.dir_details.in_path.path_prefix;
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<figure class="col col-auto g-0 dir entry m-1" id="${obj.id}" dir="${dirname}" type="Directory">
|
||||||
|
<svg class="svg" width="${OPT.size - 22}" height="${OPT.size - 22}" fill="currentColor">
|
||||||
|
<use xlink:href="/internal/icons.svg#Directory"></use>
|
||||||
|
</svg>
|
||||||
|
<figcaption class="svg_cap figure-caption text-center text-wrap text-break">${obj.name}</figcaption>
|
||||||
|
</figure>
|
||||||
|
`;
|
||||||
|
html += `<script>f=$('#${obj.id}'); w=f.find('svg').width(); f.find('figcaption').width(w);</script>`;
|
||||||
|
}
|
||||||
|
// moved the bindings to here as we need to reset them if we recreate this Figure (after a transform job)
|
||||||
|
html += `<script>
|
||||||
|
if( "${obj.type.name}" === "Directory" ) {
|
||||||
|
$("#${obj.id}").click( function(e) { document.back_id=this.id; getDirEntries(this.id,false) } )
|
||||||
|
} else {
|
||||||
|
$('#${obj.id}').click( function(e) { DoSel(e, this ); SetButtonState(); return false; });
|
||||||
|
$('#${obj.id}').dblclick( function(e) { startViewing( $(this).attr('id') ) } )
|
||||||
|
}
|
||||||
|
</script>`
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to render media (image/video/unknown)
|
||||||
|
function renderMedia(obj,gs,am_html) {
|
||||||
|
const isImageOrUnknown = obj.type.name === "Image" || obj.type.name === "Unknown";
|
||||||
|
const isVideo = obj.type.name === "Video";
|
||||||
|
const path = `${obj.in_dir.in_path.path_prefix}/${obj.in_dir.rel_path}/${obj.name}`;
|
||||||
|
const thumb = obj.file_details.thumbnail
|
||||||
|
? `<a href="${path}"><img alt="${obj.name}" ${gs} class="thumb" height="${OPT.size}" src="data:image/jpeg;base64,${obj.file_details.thumbnail}"></a>`
|
||||||
|
: `<a href="${path}"><svg width="${OPT.size}" height="${OPT.size}" fill="white"><use xlink:href="/internal/icons.svg#unknown_ftype"/></svg></a>`;
|
||||||
|
|
||||||
|
let mediaHtml = `<div style="position:relative; width:100%">${thumb}${am_html}`;
|
||||||
|
|
||||||
|
if (isVideo) {
|
||||||
|
mediaHtml += `
|
||||||
|
<div style="position:absolute; top: 0px; left: 2px;">
|
||||||
|
<svg width="16" height="16" fill="white"><use xlink:href="/internal/icons.svg#film"/></svg>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
if (OPT.search_term) {
|
||||||
|
mediaHtml += `
|
||||||
|
<div style="position:absolute; bottom: 0px; left: 2px;">
|
||||||
|
<svg width="16" height="16" fill="white"><use xlink:href="/internal/icons.svg#${getLocationIcon(obj)}"/></svg>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaHtml += `</div>`;
|
||||||
|
return mediaHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Get location icon (placeholder)
|
||||||
|
function getLocationIcon(obj) {
|
||||||
|
return ICON[obj.in_dir.in_path.type.name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST to get entry ids, and then getPage for a specified directory
|
||||||
|
function getDirEntries(dir_id, back)
|
||||||
|
{
|
||||||
|
data={}
|
||||||
|
data.dir_id=dir_id
|
||||||
|
data.back=back
|
||||||
|
data.noo=OPT.noo
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: '/get_dir_eids',
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
contentType: 'application/json',
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(res) {
|
||||||
|
if( res.valid === false )
|
||||||
|
{
|
||||||
|
$('#figures').html( "<alert class='alert alert-danger'>ERROR! directory has changed since you loaded this view. You have to reload and reset your view (probably someone deleted the directory or its parent since you loaded this page)" )
|
||||||
|
return
|
||||||
|
}
|
||||||
|
entryList=res.entry_list
|
||||||
|
pageList=entryList.slice(0, OPT.how_many)
|
||||||
|
// now go get actual data/entries
|
||||||
|
getPage(1,getPageFigures)
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// this function draws all the figures from document.entries - called when we
|
||||||
|
// change pages, but also when we change say grouping/other OPTs
|
||||||
|
function drawPageOfFigures()
|
||||||
|
{
|
||||||
|
$('#figures').empty()
|
||||||
|
var last = { printed: null }
|
||||||
|
|
||||||
|
// something is up, let the user know
|
||||||
|
if( document.alert )
|
||||||
|
$('#figures').append( document.alert )
|
||||||
|
|
||||||
|
if( OPT.folders )
|
||||||
|
{
|
||||||
|
// it root_eid is 0, then no entries in this path - cant go up
|
||||||
|
if( OPT.root_eid == 0 || (document.entries.length && document.entries[0].in_dir.eid == OPT.root_eid ) )
|
||||||
|
{
|
||||||
|
gray="_gray"
|
||||||
|
back=""
|
||||||
|
cl=""
|
||||||
|
back_id=0
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
gray=""
|
||||||
|
back="Back"
|
||||||
|
cl="back"
|
||||||
|
if( document.entries.length > 0 )
|
||||||
|
back_id = document.entries[0].in_dir.eid
|
||||||
|
else
|
||||||
|
back_id = document.back_id
|
||||||
|
}
|
||||||
|
// back button, if gray/back decide if we see grayed out folder and/or the name of the folder we go back to
|
||||||
|
// with clas "back" this gets a different click handler which flags server to return data by 'going back/up' in dir tree
|
||||||
|
// we give the server the id of the first item on the page so it can work out how to go back
|
||||||
|
html=`<div class="col col-auto g-0 m-1">
|
||||||
|
<figure id="${back_id}" class="${cl} entry m-1" type="Directory">
|
||||||
|
<svg class="svg" width="${OPT.size-22}" height="${OPT.size-22}">
|
||||||
|
<use xlink:href="internal/icons.svg#folder_back${gray}"/>
|
||||||
|
</svg>
|
||||||
|
<figcaption class="figure-caption text-center">${back}</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>`
|
||||||
|
$('#figures').append(html)
|
||||||
|
}
|
||||||
|
for (const obj of document.entries) {
|
||||||
|
// Grouping logic
|
||||||
|
if (OPT.grouping === "Day") {
|
||||||
|
if (last.printed !== obj.file_details.day) {
|
||||||
|
$('#figures').append(`<div class="row ps-3"><h6>Day: ${obj.file_details.day} of ${obj.file_details.month}/${obj.file_details.year}</h6></div>` );
|
||||||
|
last.printed = obj.file_details.day;
|
||||||
|
}
|
||||||
|
} else if (OPT.grouping === "Week") {
|
||||||
|
if (last.printed !== obj.file_details.woy) {
|
||||||
|
$('#figures').append(`<div class="row ps-3"><h6>Week #: ${obj.file_details.woy} of ${obj.file_details.year}</h6></div>` );
|
||||||
|
last.printed = obj.file_details.woy;
|
||||||
|
}
|
||||||
|
} else if (OPT.grouping === "Month") {
|
||||||
|
if (last.printed !== obj.file_details.month) {
|
||||||
|
$('#figures').append(`<div class="row ps-3"><h6>Month: ${obj.file_details.month} of ${obj.file_details.year}</h6></div>` );
|
||||||
|
last.printed = obj.file_details.month;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addFigure( obj )
|
||||||
|
}
|
||||||
|
$(".back").click( function(e) { getDirEntries(this.id,true) } )
|
||||||
|
if( document.entries.length == 0 )
|
||||||
|
if( OPT.search_term )
|
||||||
|
$('#figures').append( `<span class="alert alert-danger p-2 col-auto"> No matches for: '${OPT.search_term}'</span>` )
|
||||||
|
else if( OPT.root_eid == 0 )
|
||||||
|
$('#figures').append( `<span class="alert alert-danger p-2 col-auto d-flex align-items-center">No files in Path!</span>` )
|
||||||
|
}
|
||||||
|
|
||||||
|
// emtpy out file_list_div, and repopulate it with new page of content
|
||||||
|
function getPageFileList(res, viewingIdx)
|
||||||
|
{
|
||||||
|
$('#file_list_div').empty()
|
||||||
|
|
||||||
|
// something is up, let the user know
|
||||||
|
if( document.alert )
|
||||||
|
$('#file_list_div').append( '<div class="row">' + document.alert + '</div>' )
|
||||||
|
|
||||||
|
if( OPT.root_eid == 0 )
|
||||||
|
{
|
||||||
|
$('#file_list_div').append( `<span class="alert alert-danger p-2">No files in Path!</span>` )
|
||||||
|
return
|
||||||
|
}
|
||||||
|
html='<table class="table table-striped table-sm col-12">'
|
||||||
|
html+='<thead><tr class="table-primary"><th>Name</th><th>Size (MB)</th><th>Path Prefix</th><th>Hash</th></tr></thead><tbody>'
|
||||||
|
for (const obj of res) {
|
||||||
|
html+=`<tr>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<a href="${obj.in_dir.in_path.path_prefix}/${obj.in_dir.rel_path}/${obj.name}">
|
||||||
|
<img class="img-fluid me-2" style="max-width: 100px;"
|
||||||
|
src="data:image/jpeg;base64,${obj.file_details.thumbnail}"></img>
|
||||||
|
</a>
|
||||||
|
<span>${obj.name}</span>
|
||||||
|
</div>
|
||||||
|
<td>${obj.file_details.size_mb}</td>
|
||||||
|
<td>${obj.in_dir.in_path.path_prefix.replace("static/","")}/${obj.in_dir.rel_path}</td>
|
||||||
|
<td>${obj.file_details.hash}</td>
|
||||||
|
</tr>`
|
||||||
|
}
|
||||||
|
html+='</tbody></table>'
|
||||||
|
$('#file_list_div').append(html)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapper function as we want to handle real DB query success, but also do the
|
||||||
|
// same when we just use cache
|
||||||
|
function getEntriesByIdSuccessHandler(res,pageNumber,successCallback,viewingIdx)
|
||||||
|
{
|
||||||
|
if( res.length != pageList.length )
|
||||||
|
document.alert="<alert class='alert alert-warning'>WARNING: something has changed since viewing this page (likely someone deleted content in another view), strongly suggest a page reload to get the latest data</alert>"
|
||||||
|
|
||||||
|
document.entries=res;
|
||||||
|
// cache this
|
||||||
|
document.page[pageNumber]=res
|
||||||
|
successCallback(res,viewingIdx)
|
||||||
|
resetNextPrevButtons()
|
||||||
|
// if search, disable folders
|
||||||
|
if( OPT.search_term )
|
||||||
|
$('#folders').prop('disabled', 'disabled').removeClass('border-info').addClass('border-secondary').removeClass('text-info').addClass('text-secondary');
|
||||||
|
else if( document.entries.length == 0 )
|
||||||
|
{
|
||||||
|
html=`<span class="alert alert-danger p-2 col-auto">No files in Path</span>`
|
||||||
|
$('#file_list_div').append(html)
|
||||||
|
$('#figures').append(html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to get the 'page' of entry ids out of entryList
|
||||||
|
function getPage(pageNumber, successCallback, viewingIdx=0)
|
||||||
|
{
|
||||||
|
// before we do anything, disabled left/right arrows on viewer to stop
|
||||||
|
// getting another event before we have the data for the page back
|
||||||
|
$('#la').prop('disabled', true)
|
||||||
|
$('#ra').prop('disabled', true)
|
||||||
|
const startIndex = (pageNumber - 1) * OPT.how_many;
|
||||||
|
const endIndex = startIndex + OPT.how_many;
|
||||||
|
pageList = entryList.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
// set up data to send to server to get the entry data for entries in pageList
|
||||||
|
data={}
|
||||||
|
data.ids = pageList
|
||||||
|
|
||||||
|
// assume nothing wrong, but if the data goes odd, then this will be non-null and displayed later (cant add here, as later code does .empty() of file divs)
|
||||||
|
document.alert=null
|
||||||
|
// see if we can use cache, and dont reload from DB
|
||||||
|
if( !OPT.folders && document.page.length && document.page[pageNumber] )
|
||||||
|
{
|
||||||
|
getEntriesByIdSuccessHandler( document.page[pageNumber], pageNumber, successCallback, viewingIdx )
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST', url: '/get_entries_by_ids',
|
||||||
|
data: JSON.stringify(data), contentType: 'application/json',
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(res) {
|
||||||
|
document.amendments=res.amend;
|
||||||
|
// this is only called when we are viewing a page in files/list view, so check for job(s) ending...
|
||||||
|
for (const tmp of document.amendments) {
|
||||||
|
CheckTransformJob(tmp.eid,tmp.job_id,handleTransformFiles)
|
||||||
|
}
|
||||||
|
getEntriesByIdSuccessHandler( res.entries, pageNumber, successCallback, viewingIdx )
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) { console.error("Error:", error); } });
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quick Function to check if we are on the first page
|
||||||
|
function isFirstPage(pageNumber)
|
||||||
|
{
|
||||||
|
return pageNumber <= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to check if we are on the last page
|
||||||
|
function isLastPage(pageNumber)
|
||||||
|
{
|
||||||
|
const totalPages = Math.ceil(entryList.length / OPT.how_many);
|
||||||
|
return pageNumber >= totalPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// given an id in the list, return which page we are on (page 1 is first page)
|
||||||
|
function getPageNumberForId(id) {
|
||||||
|
const idx = entryList.indexOf(id);
|
||||||
|
// should be impossible but jic
|
||||||
|
if (idx === -1) { return -1 }
|
||||||
|
return Math.floor(idx / OPT.how_many) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we are on first page, disable prev, it not ensure next is enabled
|
||||||
|
// if we are on last page, disable next, it not ensure prev is enabled
|
||||||
|
function resetNextPrevButtons()
|
||||||
|
{
|
||||||
|
// no data, so disabled both
|
||||||
|
if( getPageNumberForId(pageList[0]) == -1 )
|
||||||
|
{
|
||||||
|
$('.prev').prop('disabled', true).addClass('disabled');
|
||||||
|
$('.next').prop('disabled', true).addClass('disabled');
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ( isFirstPage( getPageNumberForId(pageList[0]) ) )
|
||||||
|
$('.prev').prop('disabled', true).addClass('disabled');
|
||||||
|
else
|
||||||
|
$('.prev').prop('disabled', false).removeClass('disabled');
|
||||||
|
|
||||||
|
if ( isLastPage( getPageNumberForId(pageList[0]) ) )
|
||||||
|
$('.next').prop('disabled', true).addClass('disabled');
|
||||||
|
else
|
||||||
|
$('.next').prop('disabled', false).removeClass('disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
// get list of eids for the next page, also make sure next/prev buttons make sense for page we are on
|
||||||
|
function nextPage(successCallback)
|
||||||
|
{
|
||||||
|
// pageList[0] is the first entry on this page
|
||||||
|
const currentPage=getPageNumberForId( pageList[0] )
|
||||||
|
// should never happen / just return pageList unchanged
|
||||||
|
if ( currentPage === -1 || isLastPage( currentPage ) )
|
||||||
|
{
|
||||||
|
console.error( "WARNING: seems first on pg=" + pageList[0] + " of how many=" + OPT.how_many + " gives currentPage=" + currentPage + " and we cant go next page?" )
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getPage( currentPage+1, successCallback )
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get list of eids for the prev page, also make sure next/prev buttons make sense for page we are on
|
||||||
|
function prevPage(successCallback)
|
||||||
|
{
|
||||||
|
// pageList[0] is the first entry on this page
|
||||||
|
const currentPage=getPageNumberForId( pageList[0] )
|
||||||
|
// should never happen / just return pageList unchanged
|
||||||
|
if (currentPage === 1 || currentPage === -1 )
|
||||||
|
{
|
||||||
|
console.error( "WARNING: seems first on pg=" + pageList[0] + " of how many=" + OPT.how_many + " gives currentPage=" + currentPage + " and we cant go prev page?" )
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getPage( currentPage-1, successCallback )
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// function to see if we are on a phone or tablet (where we dont have ctrl or shift keys - helps to display fake buttons to allow multiselect on mobiles)
|
||||||
|
function isMobile() {
|
||||||
|
try{ document.createEvent("TouchEvent"); return true; }
|
||||||
|
catch(e){ return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// when we change one of the options (noo, how_many, folders) - then update '{how_many} files' str,
|
||||||
|
// tweak noo menu for folders/flat view then reset the page contents based on current OPT values
|
||||||
|
function changeOPT(successCallback) {
|
||||||
|
OPT.how_many=$('#how_many').val()
|
||||||
|
// changes invalidate page cache so clear it out
|
||||||
|
document.page.length=0
|
||||||
|
new_f=$('#folders').val()
|
||||||
|
new_f=( new_f == 'True' )
|
||||||
|
// if change to/from folders, also fix the noo menu
|
||||||
|
if( new_f != OPT.folders )
|
||||||
|
{
|
||||||
|
if( new_f )
|
||||||
|
{
|
||||||
|
$('#noo option:lt(2)').prop('disabled', true);
|
||||||
|
$('#noo').val(OPT.default_folder_noo)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$('#noo option:lt(2)').prop('disabled', false);
|
||||||
|
$('#noo').val(OPT.default_flat_noo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OPT.noo=$('#noo').val()
|
||||||
|
OPT.folders=new_f
|
||||||
|
OPT.folders=$('#folders').val()
|
||||||
|
OPT.grouping=$('#grouping').val()
|
||||||
|
OPT.size=$('input[name="size"]:checked').val();
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: '/change_file_opts',
|
||||||
|
data: JSON.stringify(OPT),
|
||||||
|
contentType: 'application/json',
|
||||||
|
success: function(resp) {
|
||||||
|
entryList=resp.query_data.entry_list
|
||||||
|
OPT.how_many=parseInt(OPT.how_many)
|
||||||
|
pageList=entryList.slice(0, OPT.how_many)
|
||||||
|
// put data back into booleans, ints, etc
|
||||||
|
OPT.folders=( OPT.folders == 'True' )
|
||||||
|
$('.how_many_text').html( ` ${OPT.how_many} files ` )
|
||||||
|
OPT.size=parseInt(OPT.size)
|
||||||
|
getPage(1,successCallback)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// function to change the size of thumbnails when user clicks xs/s/m/l/xl buttons
|
||||||
|
function changeSize()
|
||||||
|
{
|
||||||
|
sz=$('input[name="size"]:checked').val();
|
||||||
|
OPT.size=sz
|
||||||
|
$('.thumb').attr( {height: sz, style: 'font-size:'+sz+'px' } )
|
||||||
|
$('#size').val(sz)
|
||||||
|
sz=sz-22
|
||||||
|
$('.svg').height(sz);
|
||||||
|
$('.svg').width(sz);
|
||||||
|
$('.svg_cap').width(sz);
|
||||||
|
}
|
||||||
|
|
||||||
|
// different context menu on files
|
||||||
|
$.contextMenu({
|
||||||
|
selector: '.entry',
|
||||||
|
itemClickEvent: "click",
|
||||||
|
build: function($triggerElement, e) {
|
||||||
|
// when right-clicking & no selection add one OR deal with ctrl/shift right-lick as it always changes seln
|
||||||
|
if( NoSel() || e.ctrlKey || e.shiftKey )
|
||||||
|
{
|
||||||
|
DoSel(e, e.currentTarget )
|
||||||
|
SetButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
if( FiguresOrDirsOrBoth() == "figure" )
|
||||||
|
{
|
||||||
|
item_list = {
|
||||||
|
details: { name: "Details..." },
|
||||||
|
view: { name: "View File" },
|
||||||
|
sep: "---",
|
||||||
|
}
|
||||||
|
if( e.currentTarget.getAttribute('type') == 'Image' )
|
||||||
|
{
|
||||||
|
item_list['transform'] = {
|
||||||
|
name: "Transform",
|
||||||
|
items: {
|
||||||
|
"r90": { "name" : "Rotate 90 degrees" },
|
||||||
|
"r180": { "name" : "Rotate 180 degrees" },
|
||||||
|
"r270": { "name" : "Rotate 270 degrees" },
|
||||||
|
"fliph": { "name" : "Flip horizontally" },
|
||||||
|
"flipv": { "name" : "Flip vertically" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
item_list['move'] = { name: "Move selected file(s) to new folder" }
|
||||||
|
item_list['sep2'] = { sep: "---" }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
item_list = {
|
||||||
|
move: { name: "Move selection(s) to new folder" }
|
||||||
|
}
|
||||||
|
|
||||||
|
item_list['ai'] = {
|
||||||
|
name: "Scan file for faces",
|
||||||
|
items: {
|
||||||
|
"ai-all": { name: "all" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dynamically add entries for each person in the `people` array
|
||||||
|
people.forEach(person => {
|
||||||
|
item_list['ai'].items[`ai-${person.tag}`] = { name: person.tag };
|
||||||
|
});
|
||||||
|
|
||||||
|
if( SelContainsBinAndNotBin() ) {
|
||||||
|
item_list['both']= { name: 'Cannot delete and restore at same time', disabled: true }
|
||||||
|
} else {
|
||||||
|
if (e.currentTarget.getAttribute('path_type') == 'Bin' )
|
||||||
|
item_list['undel']= { name: "Restore selected file(s)" }
|
||||||
|
else if( e.currentTarget.getAttribute('type') != 'Directory' )
|
||||||
|
item_list['del']= { name: "Delete Selected file(s)" }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
callback: function( key, options) {
|
||||||
|
if( key == "details" ) { DetailsDBox() }
|
||||||
|
if( key == "view" ) { startViewing( $(this).attr('id') ) }
|
||||||
|
if( key == "move" ) { MoveDBox(move_paths) }
|
||||||
|
if( key == "del" ) { DelDBox('Delete') }
|
||||||
|
if( key == "undel") { DelDBox('Restore') }
|
||||||
|
if( key == "r90" ) { Transform(90) }
|
||||||
|
if( key == "r180" ) { Transform(180) }
|
||||||
|
if( key == "r270" ) { Transform(270) }
|
||||||
|
if( key == "fliph" ) { Transform("fliph") }
|
||||||
|
if( key == "flipv" ) { Transform("flipv") }
|
||||||
|
if( key.startsWith("ai")) { RunAIOnSeln(key) }
|
||||||
|
// dont flow this event through the dom
|
||||||
|
e.stopPropagation()
|
||||||
|
},
|
||||||
|
items: item_list
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// finally, for files_ip/files_sp/files_rbp - set click inside document (NOT an entry) to remove seln
|
||||||
|
$(document).on('click', function(e) { $('.highlight').removeClass('highlight') ; SetButtonState() });
|
||||||
|
document.page=[]
|
||||||
|
|||||||
@@ -1,27 +1,76 @@
|
|||||||
|
// This function will remove the matching amendment for this entry (id)
|
||||||
|
// can only have 1 ammendment per image, its grayed out for other changes
|
||||||
|
function removeAmendment( id )
|
||||||
|
{
|
||||||
|
document.amendments=document.amendments.filter(obj => obj.eid !== id)
|
||||||
|
}
|
||||||
|
|
||||||
// POST to a check URL, that will tell us if the transformation has completed,
|
// POST to a check URL, that will tell us if the transformation has completed,
|
||||||
// if not, try again in 1 second... If it has finished then reset the thumbnail
|
// if not, try again in 1 second... If it has finished then reset the thumbnail
|
||||||
// to full colour, put it back to being an entry and reset the thumbnail to the
|
// to full colour, put it back to being an entry and reset the thumbnail to the
|
||||||
// newly created one that was sent back in the response to the POST
|
// newly created one that was sent back in the response to the POST
|
||||||
function CheckTransformJob(id,job_id)
|
function handleTransformFiles(data,id,job_id)
|
||||||
|
{
|
||||||
|
if( data.finished )
|
||||||
|
{
|
||||||
|
id=parseInt(id)
|
||||||
|
idx = entryList.indexOf(id)
|
||||||
|
// replace data for this entry now its been transformed
|
||||||
|
document.entries[idx]=data.entry
|
||||||
|
// update cache too
|
||||||
|
// document.page[getPageNumberForId(id)][howFarIntoPageCache(id)]=data.entry
|
||||||
|
// FIXME: for now just invalidate whole cache
|
||||||
|
document.page.length=0
|
||||||
|
removeAmendment( id )
|
||||||
|
// redraw into figure html in dom
|
||||||
|
last={ 'printed': 'not required' }
|
||||||
|
html = createFigureHtml( data.entry, last, 9999 )
|
||||||
|
$('#'+id).replaceWith( html )
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setTimeout( function() { CheckTransformJob(id,job_id,handleTransformFiles) }, 1000,id, job_id );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST to a check URL, that will tell us if the transformation has completed,
|
||||||
|
// if not, try again in 1 second... If it has finished then reset the image
|
||||||
|
// to full colour
|
||||||
|
function handleTransformViewing(data,id,job_id)
|
||||||
|
{
|
||||||
|
if( data.finished )
|
||||||
|
{
|
||||||
|
// stop throbber, remove grayscale & then force reload with timestamped version of im.src
|
||||||
|
im.src=im.src + '?t=' + new Date().getTime();
|
||||||
|
removeAmendment( id )
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setTimeout( function() { CheckTransformJob(id,job_id,handleTransformViewing) }, 1000,id, job_id );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST to a check URL, that will tell us if the transformation has completed,
|
||||||
|
// if not, try again in 1 second... If it has finished then reset the thumbnail
|
||||||
|
// to full colour, put it back to being an entry and reset the thumbnail to the
|
||||||
|
// newly created one that was sent back in the response to the POST
|
||||||
|
function CheckTransformJob(id,job_id,successCallback)
|
||||||
{
|
{
|
||||||
CheckForJobs()
|
CheckForJobs()
|
||||||
$.ajax(
|
$.ajax( { type: 'POST', data: '&job_id='+job_id, url: '/check_transform_job',
|
||||||
{
|
success: function(res) { successCallback(res,id,job_id); } } )
|
||||||
type: 'POST', data: '&job_id='+job_id, url: '/check_transform_job', success: function(data) {
|
}
|
||||||
if( data.finished )
|
|
||||||
{
|
// function to add data for document.amendment based on id and amt
|
||||||
$('#s'+id).hide()
|
// used when we transform several images in files_*, or single image in viewer
|
||||||
$('#'+id).find('img.thumb').attr('style', 'filter: color(100%);' );
|
function addTransformAmendment(id,amt)
|
||||||
$('#'+id).addClass('entry')
|
{
|
||||||
$('#'+id).find('.thumb').attr('src', 'data:image/jpeg;base64,'+data.thumbnail)
|
am={}
|
||||||
return false;
|
am.eid=parseInt(id)
|
||||||
}
|
am.type = document.amendTypes.filter(obj => obj.job_name === 'transform_image:'+amt )[0]
|
||||||
else
|
document.amendments.push(am)
|
||||||
{
|
|
||||||
setTimeout( function() { CheckTransformJob(id,job_id) }, 1000,id, job_id );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
} )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// for each highlighted image, POST the transform with amt (90, 180, 270,
|
// for each highlighted image, POST the transform with amt (90, 180, 270,
|
||||||
@@ -31,9 +80,32 @@ function CheckTransformJob(id,job_id)
|
|||||||
// to finish
|
// to finish
|
||||||
function Transform(amt)
|
function Transform(amt)
|
||||||
{
|
{
|
||||||
$('.highlight').each(function( id, e ) {
|
// we are in the viewer with 1 image only...
|
||||||
post_data = '&amt='+amt+'&id='+e.id
|
if( document.viewing )
|
||||||
// send /transform for this image, grayscale the thumbmail, add color spinning wheel overlay, and start checking for job end
|
{
|
||||||
$.ajax({ type: 'POST', data: post_data, url: '/transform', success: function(data){ $('#'+e.id).find('img.thumb').attr('style', 'filter: grayscale(100%);' ); $('#'+e.id).removeClass('entry'); $('#s'+e.id).show(); CheckTransformJob(e.id,data.job_id); return false; } })
|
post_data = '&amt='+amt+'&id='+document.viewing.id
|
||||||
} )
|
// POST /transform for image, grayscale the image, add throbber, & start checking for end of job
|
||||||
|
$.ajax({ type: 'POST', data: post_data, url: '/transform', success: function(data) {
|
||||||
|
addTransformAmendment(document.viewing.id, amt)
|
||||||
|
DrawImg();
|
||||||
|
CheckTransformJob(document.viewing.id,data.job_id,handleTransformViewing);
|
||||||
|
return false;
|
||||||
|
} })
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$('.highlight').each(function( cnt, e ) {
|
||||||
|
post_data = '&amt='+amt+'&id='+e.id
|
||||||
|
// POST /transform for image, grayscale the thumbnail, add throbber, & start checking for end of job
|
||||||
|
$.ajax({ type: 'POST', data: post_data, url: '/transform', success: function(data){
|
||||||
|
addTransformAmendment(e.id, amt)
|
||||||
|
last={ 'printed': 'not required' }
|
||||||
|
idx = pageList.indexOf(parseInt(e.id))
|
||||||
|
html = createFigureHtml( document.entries[idx], last, 9999 )
|
||||||
|
$('#'+e.id).replaceWith( html )
|
||||||
|
CheckTransformJob(e.id,data.job_id,handleTransformFiles);
|
||||||
|
return false;
|
||||||
|
} })
|
||||||
|
} )
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ function NewHeight()
|
|||||||
return im.height*gap / (im.width/window.innerWidth)
|
return im.height*gap / (im.width/window.innerWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// draw 'str' as a label above the bounding box of the face (with a white
|
||||||
|
// transparent background to enhance readability of str)
|
||||||
function DrawLabelOnFace(str)
|
function DrawLabelOnFace(str)
|
||||||
{
|
{
|
||||||
// finish face box, need to clear out new settings for // transparent backed-name tag
|
// finish face box, need to clear out new settings for // transparent backed-name tag
|
||||||
@@ -60,6 +62,11 @@ function DrawImg()
|
|||||||
if( im.width == 0 )
|
if( im.width == 0 )
|
||||||
return
|
return
|
||||||
|
|
||||||
|
// find any matching ammendment
|
||||||
|
am=document.amendments.filter(obj => obj.eid === document.viewing.id)
|
||||||
|
if( am.length )
|
||||||
|
am=am[0]
|
||||||
|
|
||||||
canvas.width=NewWidth(im)
|
canvas.width=NewWidth(im)
|
||||||
canvas.height=NewHeight(im)
|
canvas.height=NewHeight(im)
|
||||||
|
|
||||||
@@ -67,14 +74,29 @@ function DrawImg()
|
|||||||
$('#img-cap').width(canvas.width)
|
$('#img-cap').width(canvas.width)
|
||||||
|
|
||||||
// actually draw the pixel images to the canvas at the right size
|
// actually draw the pixel images to the canvas at the right size
|
||||||
if( grayscale )
|
if (!Array.isArray(am))
|
||||||
context.filter='grayscale(1)'
|
context.filter='grayscale(1)'
|
||||||
context.drawImage(im, 0, 0, canvas.width, canvas.height )
|
context.drawImage(im, 0, 0, canvas.width, canvas.height )
|
||||||
// -50 is a straight up hack, no idea why this works, but its good enough for me
|
// -50 is a straight up hack, no idea why this works, but its good enough for me
|
||||||
if( throbber )
|
if (!Array.isArray(am))
|
||||||
$('#throbber').attr('style', 'display:show; position:absolute; left:'+canvas.width/2+'px; top:'+(canvas.height/2-50)+'px' )
|
{
|
||||||
else
|
const style = 'position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);';
|
||||||
$('#throbber').hide();
|
$('#throbber').attr('style', style + ' height: 96px;');
|
||||||
|
$('#white-circle').attr('style', style + ' height: 72px;');
|
||||||
|
if(am.type.which == 'img' )
|
||||||
|
$('#inside-img').attr('style', style + ' height: 64px;').attr('src', '/internal/'+am.type.what );
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$('#inside-icon').attr('style', `${style} color:${am.type.colour}; height: 64px;`)
|
||||||
|
$('#inside-icon').attr('fill', am.type.colour )
|
||||||
|
$('#inside-icon use').attr('xlink:href', `/internal/icons.svg#${am.type.what}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$('#throbber').hide()
|
||||||
|
$('#white-circle').hide()
|
||||||
|
$('#inside-img').hide()
|
||||||
|
$('#inside-icon').hide()
|
||||||
|
}
|
||||||
|
|
||||||
// show (or not) the whole figcaption with fname in it - based on state of fname_toggle
|
// show (or not) the whole figcaption with fname in it - based on state of fname_toggle
|
||||||
if( $('#fname_toggle').prop('checked' ) )
|
if( $('#fname_toggle').prop('checked' ) )
|
||||||
@@ -85,13 +107,13 @@ function DrawImg()
|
|||||||
else
|
else
|
||||||
$('.figcaption').hide()
|
$('.figcaption').hide()
|
||||||
|
|
||||||
// if we have faces, the enable the toggles, otherwise disable them
|
// if we have faces, the enable the toggles, otherwise disable them and reset model select too
|
||||||
// and reset model select too
|
if( document.viewing.file_details.faces.length )
|
||||||
if( objs[current].faces )
|
|
||||||
{
|
{
|
||||||
$('#faces').attr('disabled', false)
|
$('#faces').attr('disabled', false)
|
||||||
$('#distance').attr('disabled', false)
|
$('#distance').attr('disabled', false)
|
||||||
$('#model').val( Number(objs[current].face_model) )
|
// first face is good enough as whole file has to have used same model
|
||||||
|
$('#model').val( document.viewing.file_details.faces[0].facefile_lnk.model_used )
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -102,33 +124,37 @@ function DrawImg()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// okay, we want faces drawn so lets do it
|
// okay, we want faces drawn so lets do it
|
||||||
if( $('#faces').prop('checked') && objs[current].faces )
|
if( $('#faces').prop('checked') && document.viewing.file_details.faces )
|
||||||
{
|
{
|
||||||
|
faces=document.viewing.file_details.faces
|
||||||
// draw rect on each face
|
// draw rect on each face
|
||||||
for( i=0; i<objs[current].faces.length; i++ )
|
for( i=0; i<faces.length; i++ )
|
||||||
{
|
{
|
||||||
x = objs[current].faces[i].x / ( im.width/canvas.width )
|
x = faces[i].face_left / ( im.width/canvas.width )
|
||||||
y = objs[current].faces[i].y / ( im.height/canvas.height )
|
y = faces[i].face_top / ( im.height/canvas.height )
|
||||||
w = objs[current].faces[i].w / ( im.width/canvas.width )
|
w = faces[i].w / ( im.width/canvas.width )
|
||||||
h = objs[current].faces[i].h / ( im.height/canvas.height )
|
h = faces[i].h / ( im.height/canvas.height )
|
||||||
context.beginPath()
|
context.beginPath()
|
||||||
context.rect( x, y, w, h )
|
context.rect( x, y, w, h )
|
||||||
context.lineWidth = 2
|
context.lineWidth = 2
|
||||||
|
|
||||||
// this face has an override so diff colour
|
// this face has an override so diff colour
|
||||||
if( objs[current].faces[i].override )
|
if( faces[i].fnmo.length || faces[i].ffmo.length )
|
||||||
{
|
{
|
||||||
context.strokeStyle = 'blue'
|
context.strokeStyle = 'blue'
|
||||||
DrawLabelOnFace( objs[current].faces[i].override.who )
|
if( faces[i].ffmo.length )
|
||||||
|
DrawLabelOnFace( faces[i].ffmo[0].person.tag )
|
||||||
|
else
|
||||||
|
DrawLabelOnFace( faces[i].fnmo[0].type.name )
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
context.strokeStyle = 'green'
|
context.strokeStyle = 'green'
|
||||||
if( objs[current].faces[i].who )
|
if( faces[i].refimg )
|
||||||
{
|
{
|
||||||
str=objs[current].faces[i].who
|
str=faces[i].refimg.person.tag
|
||||||
if( $('#distance').prop('checked') )
|
if( $('#distance').prop('checked') )
|
||||||
str += "("+objs[current].faces[i].distance+")"
|
str += "("+faces[i].refimg_lnk.face_distance.toFixed(2)+")"
|
||||||
DrawLabelOnFace( str )
|
DrawLabelOnFace( str )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,13 +182,15 @@ function FaceToggle()
|
|||||||
// also deals with fullsecreen if needed
|
// also deals with fullsecreen if needed
|
||||||
function ViewImageOrVideo()
|
function ViewImageOrVideo()
|
||||||
{
|
{
|
||||||
if( objs[current].type == 'Image' )
|
// can happen if no content to display
|
||||||
|
if( ! document.viewing ) return
|
||||||
|
if( document.viewing.type.name == 'Image' )
|
||||||
{
|
{
|
||||||
im.src='../' + objs[current].url
|
im.src='../' + document.viewing.FullPathOnFS
|
||||||
$('#video_div').hide()
|
$('#video_div').hide()
|
||||||
if( $('#fname_toggle').prop('checked' ) )
|
if( $('#fname_toggle').prop('checked' ) )
|
||||||
$('#img-cap').show()
|
$('#img-cap').show()
|
||||||
$('#fname_i').html(PrettyFname(objs[current].url))
|
$('#fname_i').html(PrettyFname(document.viewing.FullPathOnFS))
|
||||||
$('#figure').show()
|
$('#figure').show()
|
||||||
if( fullscreen )
|
if( fullscreen )
|
||||||
$('#canvas').get(0).requestFullscreen()
|
$('#canvas').get(0).requestFullscreen()
|
||||||
@@ -170,11 +198,11 @@ function ViewImageOrVideo()
|
|||||||
if( document.fullscreen )
|
if( document.fullscreen )
|
||||||
document.exitFullscreen()
|
document.exitFullscreen()
|
||||||
}
|
}
|
||||||
if( objs[current].type == 'Video' )
|
if( document.viewing.type.name == 'Video' )
|
||||||
{
|
{
|
||||||
$('#figure').hide()
|
$('#figure').hide()
|
||||||
$('#video').prop('src', '../' + objs[current].url )
|
$('#video').prop('src', '../' + document.viewing.FullPathOnFS )
|
||||||
$('#fname_v').html(PrettyFname(objs[current].url))
|
$('#fname_v').html(PrettyFname(document.viewing.FullPathOnFS))
|
||||||
if( $('#fname_toggle').prop('checked' ) )
|
if( $('#fname_toggle').prop('checked' ) )
|
||||||
$('#img-cap').hide()
|
$('#img-cap').hide()
|
||||||
ResizeVideo()
|
ResizeVideo()
|
||||||
@@ -189,6 +217,8 @@ function ViewImageOrVideo()
|
|||||||
|
|
||||||
var offsetX,offsetY;
|
var offsetX,offsetY;
|
||||||
|
|
||||||
|
// find the edge of the canvas, so when we have a PAGE event with x,y we can see
|
||||||
|
// where we clicked in it (PAGE.x - canvas.x to see where in canvas, etc)
|
||||||
function reOffset()
|
function reOffset()
|
||||||
{
|
{
|
||||||
var BB=$('#canvas').get(0).getBoundingClientRect();
|
var BB=$('#canvas').get(0).getBoundingClientRect();
|
||||||
@@ -196,23 +226,27 @@ function reOffset()
|
|||||||
offsetY=BB.top;
|
offsetY=BB.top;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.onscroll=function(e){ reOffset(); }
|
// when we are ready,
|
||||||
window.onresize=function(e){ reOffset(); }
|
|
||||||
|
|
||||||
$(document).ready( function()
|
$(document).ready( function()
|
||||||
{
|
{
|
||||||
var cw=$('#canvas').width;
|
var cw=$('#canvas').width;
|
||||||
var ch=$('#canvas').height;
|
var ch=$('#canvas').height;
|
||||||
reOffset();
|
reOffset();
|
||||||
|
// if we scroll or resize the window, the canvas moves on the page, reset the offsets
|
||||||
window.onscroll=function(e){ reOffset(); }
|
window.onscroll=function(e){ reOffset(); }
|
||||||
window.onresize=function(e){ reOffset(); }
|
window.onresize=function(e){ reOffset(); }
|
||||||
|
|
||||||
|
// clicking in the viewer canvas gets its own handlers to handle faces (or not)
|
||||||
$.contextMenu({
|
$.contextMenu({
|
||||||
selector: '#canvas',
|
selector: '#canvas',
|
||||||
trigger: 'left',
|
trigger: 'left',
|
||||||
// trigger: 'none',
|
// trigger: 'none',
|
||||||
hideOnSecondTrigger: true,
|
hideOnSecondTrigger: true,
|
||||||
|
|
||||||
|
// go through each face, and add appropriate 'left-click' menu.
|
||||||
|
// e.g if known face, say name, offer add refimg to person, etc.
|
||||||
|
// this is a bit complex, the item_list var has a key (which is what we
|
||||||
|
// will do if we are chosen from the menu), and data to process the action
|
||||||
build: function($triggerElement, e) {
|
build: function($triggerElement, e) {
|
||||||
reOffset();
|
reOffset();
|
||||||
// get mouse position relative to the canvas (left-click uses page*)
|
// get mouse position relative to the canvas (left-click uses page*)
|
||||||
@@ -221,32 +255,33 @@ $(document).ready( function()
|
|||||||
|
|
||||||
item_list = { not_a_face: { name: "Not a face", which_face: '-1' } }
|
item_list = { not_a_face: { name: "Not a face", which_face: '-1' } }
|
||||||
|
|
||||||
for( i=0; i<objs[current].faces.length; i++ )
|
faces=document.viewing.file_details.faces
|
||||||
|
for( i=0; i<faces.length; i++ )
|
||||||
{
|
{
|
||||||
fx = objs[current].faces[i].x / ( im.width/canvas.width )
|
fx = faces[i].face_left / ( im.width/canvas.width )
|
||||||
fy = objs[current].faces[i].y / ( im.height/canvas.height )
|
fy = faces[i].face_top / ( im.height/canvas.height )
|
||||||
fw = objs[current].faces[i].w / ( im.width/canvas.width )
|
fw = faces[i].w / ( im.width/canvas.width )
|
||||||
fh = objs[current].faces[i].h / ( im.height/canvas.height )
|
fh = faces[i].h / ( im.height/canvas.height )
|
||||||
|
|
||||||
if( x >= fx && x <= fx+fw && y >= fy && y <= fy+fh )
|
if( x >= fx && x <= fx+fw && y >= fy && y <= fy+fh )
|
||||||
{
|
{
|
||||||
if( objs[current].faces[i].override )
|
if( faces[i].ffmo.length || faces[i].fnmo.length )
|
||||||
{
|
{
|
||||||
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': faces[i].id }
|
||||||
}
|
}
|
||||||
else if( objs[current].faces[i].who )
|
else if( faces[i].refimg )
|
||||||
{
|
{
|
||||||
item_list['match']={ 'name': objs[current].faces[i].who, 'which_face': i, 'id': objs[current].faces[i].id }
|
item_list['match']={ 'name': faces[i].refimg.person.tag, 'which_face': i, 'id': faces[i].id }
|
||||||
item_list['match_add_refimg']={ 'name': 'Add this as refimg for ' + objs[current].faces[i].who,
|
item_list['match_add_refimg']={ 'name': 'Add this as refimg for ' + faces[i].refimg.person.tag,
|
||||||
'person_id': objs[current].faces[i].pid, 'who': objs[current].faces[i].who, 'which_face': i, 'id': objs[current].faces[i].id, }
|
'person_id': faces[i].refimg.person.id, 'who': faces[i].refimg.person.tag, 'which_face': i, 'id': faces[i].id, }
|
||||||
item_list['wrong_person']={ 'name': 'wrong person', 'which_face': i, 'id': objs[current].faces[i].id }
|
item_list['wrong_person']={ 'name': 'wrong person', 'which_face': i, 'id': faces[i].id }
|
||||||
}
|
}
|
||||||
else
|
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_person']={ 'name': 'Add as reference image to NEW person', 'which_face': i, 'id': 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_refimg']={ 'name': 'Add as reference image to EXISTING person', 'which_face': i, 'id': faces[i].id }
|
||||||
for( var el in NMO ) {
|
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].id, 'name': 'Override: ' + NMO[el].name, 'which_face': i, 'id': faces[i].id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delete item_list['not_a_face']
|
delete item_list['not_a_face']
|
||||||
@@ -266,26 +301,15 @@ $(document).ready( function()
|
|||||||
} )
|
} )
|
||||||
} );
|
} );
|
||||||
|
|
||||||
// quick wrapper function to make calling this ajax code simpler in SearchForPerson
|
// POST to the server to force a match for this face to person_id
|
||||||
|
// FIXME: could I not pass person_id, and use // ...[item[key].which_face].refimg.person.id
|
||||||
function OverrideForceMatch( person_id, key )
|
function OverrideForceMatch( person_id, key )
|
||||||
{
|
{
|
||||||
// we have type_id passed in, so dig the NMO out, and use that below (its really just for name, but in case we change that in the DB)
|
|
||||||
for( el in NMO )
|
|
||||||
{
|
|
||||||
if( NMO[el].type_id == item[key].type_id )
|
|
||||||
{
|
|
||||||
fm_idx=el
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ofm='&person_id='+person_id+'&face_id='+item[key].id
|
ofm='&person_id='+person_id+'&face_id='+item[key].id
|
||||||
$.ajax({ type: 'POST', data: ofm, url: '/add_force_match_override', success: function(data) {
|
$.ajax({ type: 'POST', data: ofm, url: '/add_force_match_override', success: function(data) {
|
||||||
objs[current].faces[item[key].which_face].override={}
|
document.viewing.file_details.faces[item[key].which_face].ffmo=[]
|
||||||
objs[current].faces[item[key].which_face].override.who=data.person_tag
|
document.viewing.file_details.faces[item[key].which_face].ffmo[0]={}
|
||||||
objs[current].faces[item[key].which_face].override.distance='N/A'
|
document.viewing.file_details.faces[item[key].which_face].ffmo[0].person=data.person
|
||||||
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
|
|
||||||
|
|
||||||
$('#dbox').modal('hide')
|
$('#dbox').modal('hide')
|
||||||
$('#faces').prop('checked',true)
|
$('#faces').prop('checked',true)
|
||||||
DrawImg()
|
DrawImg()
|
||||||
@@ -294,6 +318,23 @@ function OverrideForceMatch( person_id, key )
|
|||||||
} )
|
} )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// function that handles the POSTed data that comes back when we add a
|
||||||
|
// reference image to a new or existing person (right-click on a face)
|
||||||
|
// used in success callbacks from CreatePersonAndRefimg() and AddRefimgTo()
|
||||||
|
function handleAddRefimgData(key, data)
|
||||||
|
{
|
||||||
|
document.viewing.file_details.faces[item[key].which_face].refimg=data.refimg
|
||||||
|
document.viewing.file_details.faces[item[key].which_face].refimg_lnk={}
|
||||||
|
// if we used this img, for now set distance to 0 - it is an exact match!
|
||||||
|
document.viewing.file_details.faces[item[key].which_face].refimg_lnk.face_distance=0.0
|
||||||
|
$('#dbox').modal('hide')
|
||||||
|
$('#faces').prop('checked',true)
|
||||||
|
DrawImg()
|
||||||
|
CheckForJobs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// when we right-click a face and make a new person, this code creates and
|
||||||
|
// associates the face
|
||||||
function CreatePersonAndRefimg( key )
|
function CreatePersonAndRefimg( key )
|
||||||
{
|
{
|
||||||
d='&face_id='+item[key].id
|
d='&face_id='+item[key].id
|
||||||
@@ -302,29 +343,17 @@ function CreatePersonAndRefimg( key )
|
|||||||
+'&surname='+$('#surname').val()
|
+'&surname='+$('#surname').val()
|
||||||
+'&refimg_data='+item[key].refimg_data
|
+'&refimg_data='+item[key].refimg_data
|
||||||
$.ajax({ type: 'POST', data: d, url: '/match_with_create_person',
|
$.ajax({ type: 'POST', data: d, url: '/match_with_create_person',
|
||||||
success: function(data) {
|
success: function(data) { handleAddRefimgData(key, data ) },
|
||||||
objs[current].faces[item[key].which_face].who=data.who
|
|
||||||
objs[current].faces[item[key].which_face].distance=data.distance
|
|
||||||
$('#dbox').modal('hide')
|
|
||||||
$('#faces').prop('checked',true)
|
|
||||||
DrawImg()
|
|
||||||
CheckForJobs()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// when we right-click a face and connect to an existing person, this connects
|
||||||
|
// the refimg and associates the face
|
||||||
function AddRefimgTo( person_id, key, search )
|
function AddRefimgTo( person_id, key, search )
|
||||||
{
|
{
|
||||||
d='&face_id='+item[key].id+'&person_id='+person_id+'&refimg_data='+item[key].refimg_data+'&search='+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',
|
$.ajax({ type: 'POST', data: d, url: '/add_refimg_to_person',
|
||||||
success: function(data) {
|
success: function(data) { handleAddRefimgData(key, data ) },
|
||||||
objs[current].faces[item[key].which_face].who=data.who
|
|
||||||
objs[current].faces[item[key].which_face].distance=data.distance
|
|
||||||
$('#dbox').modal('hide')
|
|
||||||
$('#faces').prop('checked',true)
|
|
||||||
DrawImg()
|
|
||||||
CheckForJobs()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,8 +370,7 @@ function SearchForPerson(content, key, face_id, face_pos, type_id)
|
|||||||
for( var el in data ) {
|
for( var el in data ) {
|
||||||
content+='<div class="row">'
|
content+='<div class="row">'
|
||||||
var person = data[el];
|
var person = data[el];
|
||||||
// NMO_1 is a non-match-override type_id==1 (or force match to existing person)
|
if( item[key].name == "Override: Manual match to existing person" )
|
||||||
if( key == "NMO_1" )
|
|
||||||
{
|
{
|
||||||
func='OverrideForceMatch('+person.id+',\''+key+'\' )'
|
func='OverrideForceMatch('+person.id+',\''+key+'\' )'
|
||||||
content+= '<div class="col">' + person.tag + ' (' + person.firstname+' '+person.surname+ ') </div>'
|
content+= '<div class="col">' + person.tag + ' (' + person.firstname+' '+person.surname+ ') </div>'
|
||||||
@@ -365,17 +393,19 @@ function SearchForPerson(content, key, face_id, face_pos, type_id)
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if we force a match, this func allows us to POST to the server to remove the override
|
||||||
function RemoveOverrideForceMatch(face_pos)
|
function RemoveOverrideForceMatch(face_pos)
|
||||||
{
|
{
|
||||||
if( objs[current].faces[face_pos].override )
|
if( document.viewing.file_details.faces[face_pos].ffmo.length )
|
||||||
who=objs[current].faces[face_pos].override.who
|
who=document.viewing.file_details.faces[face_pos].ffmo[0].person.tag
|
||||||
else
|
else
|
||||||
who=objs[current].faces[face_pos].who
|
who=document.viewing.file_details.faces[face_pos].refimg.person.tag
|
||||||
|
|
||||||
d='&face_id='+objs[current].faces[face_pos].id+'&person_tag='+who+'&file_eid='+current
|
d='&face_id='+document.viewing.file_details.faces[face_pos].id+'&person_tag='+who+'&file_eid='+document.viewing.id
|
||||||
$.ajax({ type: 'POST', data: d, url: '/remove_force_match_override',
|
$.ajax({ type: 'POST', data: d, url: '/remove_force_match_override',
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
delete objs[current].faces[face_pos].override
|
// force/delete the ffmo cleanly
|
||||||
|
document.viewing.file_details.faces[face_pos].ffmo.length=0
|
||||||
$('#dbox').modal('hide')
|
$('#dbox').modal('hide')
|
||||||
DrawImg()
|
DrawImg()
|
||||||
CheckForJobs()
|
CheckForJobs()
|
||||||
@@ -385,12 +415,13 @@ function RemoveOverrideForceMatch(face_pos)
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if we force NO match, this func allows us to POST to the server to remove the override
|
||||||
function RemoveOverrideNoMatch(face_pos, type_id)
|
function RemoveOverrideNoMatch(face_pos, type_id)
|
||||||
{
|
{
|
||||||
d='&face_id='+objs[current].faces[face_pos].id+'&type_id='+type_id
|
d='&face_id='+document.viewing.file_details.faces[face_pos].id+'&type_id='+type_id
|
||||||
$.ajax({ type: 'POST', data: d, url: '/remove_no_match_override',
|
$.ajax({ type: 'POST', data: d, url: '/remove_no_match_override',
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
delete objs[current].faces[face_pos].override
|
document.viewing.file_details.faces[face_pos].fnmo.length=0
|
||||||
$('#dbox').modal('hide')
|
$('#dbox').modal('hide')
|
||||||
DrawImg()
|
DrawImg()
|
||||||
CheckForJobs()
|
CheckForJobs()
|
||||||
@@ -400,16 +431,13 @@ function RemoveOverrideNoMatch(face_pos, type_id)
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST to the server to force NO match for this face
|
||||||
function AddNoMatchOverride(type_id, face_id, face_pos, type_id)
|
function AddNoMatchOverride(type_id, face_id, face_pos, type_id)
|
||||||
{
|
{
|
||||||
d='&type_id='+type_id+'&face_id='+face_id
|
d='&type_id='+type_id+'&face_id='+face_id
|
||||||
$.ajax({ type: 'POST', data: d, url: '/add_no_match_override',
|
$.ajax({ type: 'POST', data: d, url: '/add_no_match_override',
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
objs[current].faces[face_pos].override={}
|
document.viewing.file_details.faces[face_pos].fnmo[0]=data
|
||||||
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
|
|
||||||
$('#dbox').modal('hide')
|
$('#dbox').modal('hide')
|
||||||
$('#faces').prop('checked',true)
|
$('#faces').prop('checked',true)
|
||||||
DrawImg()
|
DrawImg()
|
||||||
@@ -418,6 +446,9 @@ function AddNoMatchOverride(type_id, face_id, face_pos, type_id)
|
|||||||
} )
|
} )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generate html for the appropriate content to search for a person when adding
|
||||||
|
// override DBox. has a button that when clicked calls SeachForPerson() which
|
||||||
|
// POSTs to the server, and fills in the 'search_person_results' div with content
|
||||||
function AddSearch( content, key, face_pos )
|
function AddSearch( content, key, face_pos )
|
||||||
{
|
{
|
||||||
html='<h5>search for existing person:</h5>'
|
html='<h5>search for existing person:</h5>'
|
||||||
@@ -457,17 +488,17 @@ function FaceDBox(key, item)
|
|||||||
div+='</div><div class="col-6">'
|
div+='</div><div class="col-6">'
|
||||||
if ( key == 'remove_force_match_override' )
|
if ( key == 'remove_force_match_override' )
|
||||||
{
|
{
|
||||||
if( objs[current].faces[face_pos].override.type_name == 'Manual match to existing person' )
|
if( document.viewing.file_details.faces[face_pos].ffmo.length )
|
||||||
div+='<div class="row col-12">remove this override (force match to: ' + objs[current].faces[face_pos].override.who + ')</div>'
|
div+='<div class="row col-12">remove this override (force match to: ' + document.viewing.file_details.faces[face_pos].ffmo[0].person.tag + ')</div>'
|
||||||
else
|
else
|
||||||
div+='<div class="row col-12">remove this override (no match)</div>'
|
div+='<div class="row col-12">remove this override (' + document.viewing.file_details.faces[face_pos].fnmo[0].type.name + ')</div>'
|
||||||
div+='<div class="row">'
|
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-info col-6" type="button" onClick="$(\'#dbox\').modal(\'hide\'); return false">Cancel</button>'
|
||||||
div+='<button class="btn btn-outline-danger col-6" type="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.file_details.faces[face_pos].ffmo.length )
|
||||||
div+='onClick="RemoveOverrideForceMatch(' +face_pos+ ')">Remove</button>'
|
div+='onClick="RemoveOverrideForceMatch(' +face_pos+ ')">Remove</button>'
|
||||||
else
|
else
|
||||||
div+='onClick="RemoveOverrideNoMatch(' +face_pos+','+objs[current].faces[face_pos].override.type_id+ ')">Remove</button>'
|
div+='onClick="RemoveOverrideNoMatch(' +face_pos+','+document.viewing.file_details.faces[face_pos].fnmo[0].type.id+ ')">Remove</button>'
|
||||||
div+='</div>'
|
div+='</div>'
|
||||||
}
|
}
|
||||||
if ( key == 'no_match_new_person' )
|
if ( key == 'no_match_new_person' )
|
||||||
@@ -501,7 +532,6 @@ function FaceDBox(key, item)
|
|||||||
func='AddRefimgTo('+item[key]['person_id']+',\''+key+'\''
|
func='AddRefimgTo('+item[key]['person_id']+',\''+key+'\''
|
||||||
func_sn=func+ ', true )'
|
func_sn=func+ ', true )'
|
||||||
func_ao=func+ ', false )'
|
func_ao=func+ ', false )'
|
||||||
div+=`<script>console.log( "AddExistingFaceAsRefimgToMatchedPerson()" )</script>`
|
|
||||||
div+="Confirm you wish to add this face as a reference image for " + item[key]['who']
|
div+="Confirm you wish to add this face as a reference image for " + item[key]['who']
|
||||||
div+= '<div class="col">' + item[key]['who'] + '</div><div class="col input-group">'
|
div+= '<div class="col">' + item[key]['who'] + '</div><div class="col input-group">'
|
||||||
div+= '<button onClick="'+func_sn+'" class="btn btn-success py-1 input-group-prepend">Add & search now</button> '
|
div+= '<button onClick="'+func_sn+'" class="btn btn-success py-1 input-group-prepend">Add & search now</button> '
|
||||||
@@ -542,7 +572,7 @@ function FaceDBox(key, item)
|
|||||||
// pops results up in a dbox
|
// pops results up in a dbox
|
||||||
function JoblogSearch()
|
function JoblogSearch()
|
||||||
{
|
{
|
||||||
data="eid="+current
|
data="eid="+document.viewing.id
|
||||||
$.ajax({ type: 'POST', data: data, url: '/joblog_search', success: function(res) {
|
$.ajax({ type: 'POST', data: data, url: '/joblog_search', success: function(res) {
|
||||||
data = JSON.parse(res)
|
data = JSON.parse(res)
|
||||||
div ='<div><table class="table table-striped table-sm sm-txt">'
|
div ='<div><table class="table table-striped table-sm sm-txt">'
|
||||||
@@ -559,3 +589,166 @@ function JoblogSearch()
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// helper func to resert the src on the video div
|
||||||
|
function setVideoSource(newSrc) {
|
||||||
|
$('#videoSource').attr('src', newSrc);
|
||||||
|
$('#video')[0].load();
|
||||||
|
}
|
||||||
|
|
||||||
|
// function called when we get another page from inside the viewer
|
||||||
|
function getPageViewer(res, viewingIdx)
|
||||||
|
{
|
||||||
|
document.viewing=document.entries[viewingIdx]
|
||||||
|
// update viewing, arrows and image/video too
|
||||||
|
ViewImageOrVideo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// handler used when we double click an entry to show it in the viewer
|
||||||
|
function dblClickToViewEntry(id) {
|
||||||
|
$('#files_div').addClass('d-none')
|
||||||
|
$('#viewer_div').removeClass('d-none')
|
||||||
|
setEntryById( id )
|
||||||
|
ViewImageOrVideo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// quick function that allows us to go out of the viewer and back, the viewercomes from files_ip/sp
|
||||||
|
// so just redraw the page with drawPageOfFigures() as we have all the data
|
||||||
|
function goOutOfViewer()
|
||||||
|
{
|
||||||
|
// if this returns -1, we have used arrows to go onto a new page(s)
|
||||||
|
if( getPageNumberForId( $('#figures').find('.figure').first().prop('id') ) == -1 )
|
||||||
|
drawPageOfFigures()
|
||||||
|
|
||||||
|
// hide viewer div, then show files_div
|
||||||
|
$('#viewer_div').addClass('d-none')
|
||||||
|
$('#files_div').removeClass('d-none')
|
||||||
|
}
|
||||||
|
|
||||||
|
// change the viewer to the previous entry (handle page change too)
|
||||||
|
function getPreviousEntry() {
|
||||||
|
var currentIndex = entryList.indexOf(document.viewing.id);
|
||||||
|
|
||||||
|
oldPageOffset=Math.floor(currentIndex / OPT.how_many)
|
||||||
|
if (currentIndex > 0) {
|
||||||
|
currentIndex--;
|
||||||
|
pageOffset=Math.floor(currentIndex / OPT.how_many)
|
||||||
|
currentIndex=currentIndex-(pageOffset*OPT.how_many)
|
||||||
|
// pref page, load it
|
||||||
|
if( oldPageOffset != pageOffset )
|
||||||
|
// pref page is pageOffset+1 now
|
||||||
|
getPage(pageOffset+1,getPageViewer,currentIndex)
|
||||||
|
else
|
||||||
|
document.viewing=document.entries[currentIndex]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// change the viewer to the next entry (handle page change too)
|
||||||
|
function getNextEntry() {
|
||||||
|
var currentIndex = entryList.indexOf(document.viewing.id);
|
||||||
|
|
||||||
|
oldPageOffset=Math.floor(currentIndex / OPT.how_many)
|
||||||
|
if (currentIndex < entryList.length - 1) {
|
||||||
|
currentIndex++
|
||||||
|
pageOffset=Math.floor(currentIndex / OPT.how_many)
|
||||||
|
currentIndex=currentIndex-(pageOffset*OPT.how_many)
|
||||||
|
// next page, load it
|
||||||
|
if( oldPageOffset != pageOffset )
|
||||||
|
// next page is pageOffset+1 now
|
||||||
|
getPage(pageOffset+1,getPageViewer,currentIndex)
|
||||||
|
else
|
||||||
|
document.viewing=document.entries[currentIndex]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we are viewing the very first entry (helps to disable la)
|
||||||
|
function entryIsAtStart() {
|
||||||
|
return document.viewing.id === entryList[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we are viewing the very last entry (helps to disable ra)
|
||||||
|
function entryIsAtEnd() {
|
||||||
|
return document.viewing.id === entryList[entryList.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper func to ensure document.viewing is the right entry from document.entries array
|
||||||
|
function setEntryById(id) {
|
||||||
|
var currentIndex = entryList.indexOf(parseInt(id));
|
||||||
|
// if we are on a different page, adjust as document.entries only has <= how_many
|
||||||
|
pageOffset=Math.floor(currentIndex / OPT.how_many)
|
||||||
|
currentIndex = currentIndex-(pageOffset*OPT.how_many)
|
||||||
|
document.viewing=document.entries[currentIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
// disable la button if we are viewing first entry and/or ra button if we are viewing last entry
|
||||||
|
function setDisabledForViewingNextPrevBttons()
|
||||||
|
{
|
||||||
|
$('#la').attr('disabled', entryIsAtStart());
|
||||||
|
$('#ra').attr('disabled', entryIsAtEnd());
|
||||||
|
}
|
||||||
|
|
||||||
|
// when we go into the view, the keybindings are set here for items like 'f' for face box/name
|
||||||
|
function addViewerKeyHandler() {
|
||||||
|
// allow a keypress on the viewer_div
|
||||||
|
$(document).keydown(function(event) {
|
||||||
|
// if dbox is visible, dont process this hot-key, we are inputting text into inputs instead
|
||||||
|
if( $("#dbox").is(':visible') )
|
||||||
|
return
|
||||||
|
switch (event.key)
|
||||||
|
{
|
||||||
|
case "Left": // IE/Edge specific value
|
||||||
|
case "ArrowLeft":
|
||||||
|
$('#la').click()
|
||||||
|
break;
|
||||||
|
case "Right": // IE/Edge specific value
|
||||||
|
case "ArrowRight":
|
||||||
|
$('#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.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// left arrow onclick handler to go to prev image from inside the viewer
|
||||||
|
function prevImageInViewer()
|
||||||
|
{
|
||||||
|
getPreviousEntry()
|
||||||
|
setDisabledForViewingNextPrevBttons()
|
||||||
|
ViewImageOrVideo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// right arrow onclick handler to go to next image from inside the viewer
|
||||||
|
function nextImageInViewer()
|
||||||
|
{
|
||||||
|
getNextEntry()
|
||||||
|
setDisabledForViewingNextPrevBttons()
|
||||||
|
ViewImageOrVideo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapper func to start the viewer - needed as we have a dbl-click & View file
|
||||||
|
// to start the viewer
|
||||||
|
function startViewing(eid)
|
||||||
|
{
|
||||||
|
dblClickToViewEntry( eid );
|
||||||
|
setDisabledForViewingNextPrevBttons();
|
||||||
|
addViewerKeyHandler()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
// POST to a check URL, that will tell us if the transformation has completed,
|
|
||||||
// if not, try again in 1 second... If it has finished then reset the thumbnail
|
|
||||||
// to full colour, put it back to being an entry and reset the thumbnail to the
|
|
||||||
// newly created one that was sent back in the response to the POST
|
|
||||||
function CheckTransformJob(id,job_id)
|
|
||||||
{
|
|
||||||
CheckForJobs()
|
|
||||||
$.ajax(
|
|
||||||
{
|
|
||||||
type: 'POST', data: '&job_id='+job_id, url: '/check_transform_job', success: function(data) {
|
|
||||||
if( data.finished )
|
|
||||||
{
|
|
||||||
// stop throbber, remove grayscale & then force reload with timestamped version of im.src
|
|
||||||
grayscale=0
|
|
||||||
throbber=0
|
|
||||||
im.src=im.src + '?t=' + new Date().getTime();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
setTimeout( function() { CheckTransformJob(id,job_id) }, 1000,id, job_id );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
} )
|
|
||||||
}
|
|
||||||
|
|
||||||
// for each highlighted image, POST the transform with amt (90, 180, 270,
|
|
||||||
// fliph, flipv) which will let the job manager know what to do to this file.
|
|
||||||
// we also grayscale the thumbnail out, remove the entry class for now, show
|
|
||||||
// the spinning wheel, and finally kick of the checking for the transform job
|
|
||||||
// to finish
|
|
||||||
function Transform(amt)
|
|
||||||
{
|
|
||||||
post_data = '&amt='+amt+'&id='+current
|
|
||||||
// send /transform for this image, grayscale the thumbmail, add color spinning wheel overlay, and start checking for job end
|
|
||||||
$.ajax({ type: 'POST', data: post_data, url: '/transform', success: function(data) { grayscale=1; throbber=1; DrawImg(); CheckTransformJob(current,data.job_id); return false; } })
|
|
||||||
}
|
|
||||||
BIN
internal/white-circle.png
Normal file
BIN
internal/white-circle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
25
job.py
25
job.py
@@ -3,12 +3,13 @@ from flask_wtf import FlaskForm
|
|||||||
from flask import request, render_template, redirect, make_response, jsonify, url_for
|
from flask import request, render_template, redirect, make_response, jsonify, url_for
|
||||||
from settings import Settings
|
from settings import Settings
|
||||||
from main import db, app, ma
|
from main import db, app, ma
|
||||||
from sqlalchemy import Sequence, func
|
from sqlalchemy import Sequence, func, select
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import pytz
|
import pytz
|
||||||
import socket
|
import socket
|
||||||
from shared import PA, PA_JOB_MANAGER_HOST, PA_JOB_MANAGER_PORT, NEWEST_LOG_LIMIT, OLDEST_LOG_LIMIT
|
from shared import PA, PA_JOB_MANAGER_HOST, PA_JOB_MANAGER_PORT, NEWEST_LOG_LIMIT, OLDEST_LOG_LIMIT
|
||||||
|
from amend import EntryAmendment, inAmendmentTypes
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from sqlalchemy.dialects.postgresql import INTERVAL
|
from sqlalchemy.dialects.postgresql import INTERVAL
|
||||||
from sqlalchemy.sql.functions import concat
|
from sqlalchemy.sql.functions import concat
|
||||||
@@ -114,8 +115,25 @@ def NewJob(name, num_files="0", wait_for=None, jex=None, desc="No description pr
|
|||||||
|
|
||||||
db.session.add(job)
|
db.session.add(job)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
SetFELog( message=f'Created <a class="link-light" href="/job/{job.id}">Job #{job.id}</a> to {desc}', level="success" )
|
|
||||||
|
|
||||||
|
# if this job changes an eid we store that in DB and client shows until it finishes the job
|
||||||
|
at_id = inAmendmentTypes(job)
|
||||||
|
if at_id:
|
||||||
|
if job.name == 'transform_image':
|
||||||
|
id=[jex.value for jex in job.extra if jex.name == "id"][0]
|
||||||
|
ea=EntryAmendment( eid=id, job_id=job.id, amend_type=at_id )
|
||||||
|
print( f"just added an EA for eid={id}, j={job.id}" )
|
||||||
|
db.session.add(ea)
|
||||||
|
elif job.name == 'delete_files':
|
||||||
|
for j in jex:
|
||||||
|
if 'eid-' in j.name:
|
||||||
|
ea=EntryAmendment( eid=j.value, amend_type=at_id )
|
||||||
|
db.session.add(ea)
|
||||||
|
# need to return this to the f/e somehow
|
||||||
|
# this is for removes, really need to think about this more
|
||||||
|
#job.amendment=ea
|
||||||
|
|
||||||
|
SetFELog( message=f'Created <a class="link-light" href="/job/{job.id}">Job #{job.id}</a> to {desc}', level="success" )
|
||||||
WakePAJobManager(job.id)
|
WakePAJobManager(job.id)
|
||||||
return job
|
return job
|
||||||
|
|
||||||
@@ -280,7 +298,8 @@ def joblog_search():
|
|||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
eid=request.form['eid']
|
eid=request.form['eid']
|
||||||
ent=Entry.query.get(eid)
|
stmt = select(Entry).where(Entry.id == eid)
|
||||||
|
ent = db.session.scalars(stmt).one_or_none()
|
||||||
logs=Joblog.query.join(Job).filter(Joblog.log.ilike(text(f"'%%{ent.name}%%'"))).with_entities(Joblog.log, Job.id, Job.name, Job.state, Joblog.log_date).all()
|
logs=Joblog.query.join(Job).filter(Joblog.log.ilike(text(f"'%%{ent.name}%%'"))).with_entities(Joblog.log, Job.id, Job.name, Job.state, Joblog.log_date).all()
|
||||||
|
|
||||||
# turn DB output into json and return it to the f/e
|
# turn DB output into json and return it to the f/e
|
||||||
|
|||||||
4
main.py
4
main.py
@@ -246,6 +246,10 @@ def logout():
|
|||||||
logout_user()
|
logout_user()
|
||||||
return redirect('/login')
|
return redirect('/login')
|
||||||
|
|
||||||
|
# quick health route so traefik knows we are up
|
||||||
|
@app.route('/health')
|
||||||
|
def health():
|
||||||
|
return {"status": "ok"}, 200
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# main to be called via Flask/Gunicorn
|
# main to be called via Flask/Gunicorn
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
#
|
#
|
||||||
# This file controls the 'external' job control manager, that (periodically #
|
# This file controls the 'external' job control manager, that (periodically #
|
||||||
# looks / somehow is pushed an event?) picks up new jobs, and processes them.
|
# looks / somehow is pushed an event?) picks up new jobs, and processes them.
|
||||||
@@ -15,7 +14,7 @@
|
|||||||
|
|
||||||
### SQLALCHEMY IMPORTS ###
|
### SQLALCHEMY IMPORTS ###
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy import Column, Integer, String, Sequence, Float, ForeignKey, DateTime, LargeBinary, Boolean, func, text
|
from sqlalchemy import Column, Integer, String, Sequence, Float, ForeignKey, DateTime, LargeBinary, Boolean, func, text, select
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
@@ -23,7 +22,7 @@ from sqlalchemy.orm import sessionmaker
|
|||||||
from sqlalchemy.orm import scoped_session
|
from sqlalchemy.orm import scoped_session
|
||||||
|
|
||||||
### LOCAL FILE IMPORTS ###
|
### LOCAL FILE IMPORTS ###
|
||||||
from shared import DB_URL, PA_JOB_MANAGER_HOST, PA_JOB_MANAGER_PORT, THUMBSIZE, SymlinkName, GenThumb, SECS_IN_A_DAY, PA_EXIF_ROTATER
|
from shared import DB_URL, PA_JOB_MANAGER_HOST, PA_JOB_MANAGER_PORT, THUMBSIZE, SymlinkName, GenThumb, SECS_IN_A_DAY, PA_EXIF_ROTATER, PA
|
||||||
from datetime import datetime, timedelta, date
|
from datetime import datetime, timedelta, date
|
||||||
|
|
||||||
### PYTHON LIB IMPORTS ###
|
### PYTHON LIB IMPORTS ###
|
||||||
@@ -46,6 +45,8 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
import ffmpeg
|
import ffmpeg
|
||||||
import subprocess
|
import subprocess
|
||||||
|
# FIXME: remove this
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
# global debug setting
|
# global debug setting
|
||||||
@@ -512,42 +513,15 @@ class PA_JobManager_FE_Message(Base):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<id: {}, job_id: {}, level: {}, message: {}".format(self.id, self.job_id, self.level, self.message)
|
return "<id: {}, job_id: {}, level: {}, message: {}".format(self.id, self.job_id, self.level, self.message)
|
||||||
|
|
||||||
|
################################################################################
|
||||||
##############################################################################
|
# Class describing which Entry has a pending Amendment in the DB (via sqlalchemy)
|
||||||
# Class describing PA_UserState and in the DB (via sqlalchemy)
|
################################################################################
|
||||||
# the state for a User defines a series of remembered states for a user
|
class EntryAmendment(PA,Base):
|
||||||
# to optimise their viewing, etc. If we scan and fine new files, we need to
|
__tablename__ = "entry_amendment"
|
||||||
# invalidate these cached values, so we have this class here just for that
|
eid = Column(Integer, ForeignKey("entry.id"), primary_key=True )
|
||||||
##############################################################################
|
job_id = Column(Integer, ForeignKey("job.id"), primary_key=True )
|
||||||
class PA_UserState(Base):
|
# don't over think this, we just use eid to delete this entry anyway
|
||||||
__tablename__ = "pa_user_state"
|
amend_type = Column(Integer)
|
||||||
id = Column(Integer, Sequence('pa_user_state_id_seq'), primary_key=True )
|
|
||||||
pa_user_dn = Column(String, ForeignKey('pa_user.dn'), primary_key=True )
|
|
||||||
last_used = Column(DateTime(timezone=True))
|
|
||||||
path_type = Column(String, primary_key=True, unique=False, nullable=False )
|
|
||||||
noo = Column(String, unique=False, nullable=False )
|
|
||||||
grouping = Column(String, unique=False, nullable=False )
|
|
||||||
how_many = Column(Integer, unique=False, nullable=False )
|
|
||||||
st_offset = Column(Integer, unique=False, nullable=False )
|
|
||||||
size = Column(Integer, unique=False, nullable=False )
|
|
||||||
folders = Column(Boolean, unique=False, nullable=False )
|
|
||||||
root = Column(String, unique=False, nullable=False )
|
|
||||||
cwd = Column(String, unique=False, nullable=False )
|
|
||||||
## for now being lazy and not doing a separate table until I settle on needed fields and when
|
|
||||||
# only used if ptype == View
|
|
||||||
view_eid = Column(Integer, unique=False, nullable=False )
|
|
||||||
orig_ptype = Column(String, unique=False, nullable=False )
|
|
||||||
# only used if view and orig_ptype was search
|
|
||||||
orig_search_term = Column(String, unique=False, nullable=False )
|
|
||||||
orig_url = Column(String, unique=False, nullable=False )
|
|
||||||
current = Column(Integer)
|
|
||||||
first_eid = Column(Integer)
|
|
||||||
last_eid = Column(Integer)
|
|
||||||
num_entries = Column(Integer)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<pa_user_dn: {self.pa_user_dn}, path_type: {self.path_type}, noo: {self.noo}, grouping: {self.grouping}, how_many: {self.how_many}, st_offset: {self.st_offset}, size: {self.size}, folders: {self.folders}, root: {self.root}, cwd: {self.cwd}, view_eid: {self.view_eid}, orig_ptype: {self.orig_ptype}, orig_search_term: {self.orig_search_term}, orig_url: {self.orig_url}, current={self.current}, first_eid={self.first_eid}, last_eid={self.last_eid}, num_entries={self.num_entries}>"
|
|
||||||
|
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
# PAprint(): convenience function to prepend a timestamp to a printed string
|
# PAprint(): convenience function to prepend a timestamp to a printed string
|
||||||
@@ -912,6 +886,7 @@ def RunJob(job):
|
|||||||
elif job.name == "run_ai_on_path":
|
elif job.name == "run_ai_on_path":
|
||||||
JobRunAIOnPath(job)
|
JobRunAIOnPath(job)
|
||||||
elif job.name == "transform_image":
|
elif job.name == "transform_image":
|
||||||
|
#time.sleep(10)
|
||||||
JobTransformImage(job)
|
JobTransformImage(job)
|
||||||
elif job.name == "clean_bin":
|
elif job.name == "clean_bin":
|
||||||
JobCleanBin(job)
|
JobCleanBin(job)
|
||||||
@@ -1131,7 +1106,6 @@ def DisconnectAllOverrides(job):
|
|||||||
def JobForceScan(job):
|
def JobForceScan(job):
|
||||||
JobProgressState( job, "In Progress" )
|
JobProgressState( job, "In Progress" )
|
||||||
DisconnectAllOverrides(job)
|
DisconnectAllOverrides(job)
|
||||||
session.query(PA_UserState).delete()
|
|
||||||
session.query(FaceFileLink).delete()
|
session.query(FaceFileLink).delete()
|
||||||
session.query(FaceRefimgLink).delete()
|
session.query(FaceRefimgLink).delete()
|
||||||
session.query(Face).delete()
|
session.query(Face).delete()
|
||||||
@@ -1668,18 +1642,6 @@ def find_last_successful_ai_scan(job):
|
|||||||
return ai_job.last_update.timestamp()
|
return ai_job.last_update.timestamp()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
####################################################################################################################################
|
|
||||||
# when an import job actually finds new files, then the pa_user_state caches will become invalid (offsets are now wrong)
|
|
||||||
####################################################################################################################################
|
|
||||||
def DeleteOldPA_UserState(job):
|
|
||||||
# clear them out for now - this is 'dumb', just delete ALL. Eventually, can do this based on just the path &/or whether the last_used is
|
|
||||||
# newer than this delete moment (only would be a race condition between an import changing things and someone simultaneously viewing)
|
|
||||||
# path=[jex.value for jex in job.extra if jex.name == "path"][0]
|
|
||||||
session.query(PA_UserState).delete()
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
####################################################################################################################################
|
####################################################################################################################################
|
||||||
# JobImportDir(): job that scan import dir and processes entries in there - key function that uses os.walk() to traverse the
|
# JobImportDir(): job that scan import dir and processes entries in there - key function that uses os.walk() to traverse the
|
||||||
# file system and calls AddFile()/AddDir() as necessary
|
# file system and calls AddFile()/AddDir() as necessary
|
||||||
@@ -1788,8 +1750,6 @@ def JobImportDir(job):
|
|||||||
if found_new_files:
|
if found_new_files:
|
||||||
job.extra.append( JobExtra( name="new_files", value=str(found_new_files) ) )
|
job.extra.append( JobExtra( name="new_files", value=str(found_new_files) ) )
|
||||||
session.add(job)
|
session.add(job)
|
||||||
# this will invalidate pa_user_state for this path's contents (offsets are now wrong), clear them out
|
|
||||||
DeleteOldPA_UserState(job)
|
|
||||||
|
|
||||||
rm_cnt=HandleAnyFSDeletions(job)
|
rm_cnt=HandleAnyFSDeletions(job)
|
||||||
|
|
||||||
@@ -1916,7 +1876,6 @@ def JobRunAIOn(job):
|
|||||||
|
|
||||||
####################################################################################################################################
|
####################################################################################################################################
|
||||||
# JobTransformImage(): transform an image by the amount requested (can also flip horizontal or vertical)
|
# JobTransformImage(): transform an image by the amount requested (can also flip horizontal or vertical)
|
||||||
# TODO: should be JobTransformImage() ;)
|
|
||||||
####################################################################################################################################
|
####################################################################################################################################
|
||||||
def JobTransformImage(job):
|
def JobTransformImage(job):
|
||||||
JobProgressState( job, "In Progress" )
|
JobProgressState( job, "In Progress" )
|
||||||
@@ -1948,6 +1907,15 @@ def JobTransformImage(job):
|
|||||||
e.file_details.hash = md5( job, e )
|
e.file_details.hash = md5( job, e )
|
||||||
PAprint( f"JobTransformImage DONE thumb: job={job.id}, id={id}, amt={amt}" )
|
PAprint( f"JobTransformImage DONE thumb: job={job.id}, id={id}, amt={amt}" )
|
||||||
session.add(e)
|
session.add(e)
|
||||||
|
# now remove the matching amendment for the transform job
|
||||||
|
stmt=select(EntryAmendment).where(EntryAmendment.eid==id)
|
||||||
|
ea=session.execute(stmt).scalars().one_or_none()
|
||||||
|
if ea:
|
||||||
|
session.delete(ea)
|
||||||
|
else:
|
||||||
|
AddLogForJob( job, f"ERROR: failed to remove entry amendment in DB for this transformation? (eid={id})" )
|
||||||
|
PAprint( f"ERROR: failed to remove entry amendment in DB for this transformation? (eid={id}, job={job} )" )
|
||||||
|
|
||||||
FinishJob(job, "Finished Processesing image rotation/flip")
|
FinishJob(job, "Finished Processesing image rotation/flip")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -2758,7 +2726,6 @@ def ScheduledJobs():
|
|||||||
created_jobs=True
|
created_jobs=True
|
||||||
return created_jobs
|
return created_jobs
|
||||||
|
|
||||||
|
|
||||||
####################################################################################################################################
|
####################################################################################################################################
|
||||||
# MAIN - start with validation, then grab any jobs in the DB to process, then
|
# MAIN - start with validation, then grab any jobs in the DB to process, then
|
||||||
# go into waiting on a socket to be woken up (and then if woken, back into HandleJobs()
|
# go into waiting on a socket to be woken up (and then if woken, back into HandleJobs()
|
||||||
|
|||||||
47
path.py
47
path.py
@@ -42,50 +42,3 @@ class Path(db.Model):
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<id: {self.id}, path_prefix: {self.path_prefix}, num_files={self.num_files}, type={self.type}>"
|
return f"<id: {self.id}, path_prefix: {self.path_prefix}, num_files={self.num_files}, type={self.type}>"
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# Class describing PathDetail (quick connvenence class for MovePathDetails())
|
|
||||||
################################################################################
|
|
||||||
class PathDetail(PA):
|
|
||||||
"""Class describing details of a Path [internal class used in MovePathDetais()]"""
|
|
||||||
|
|
||||||
def __init__(self,ptype,path):
|
|
||||||
"""Initialisation function for PathDetail class
|
|
||||||
|
|
||||||
Args:
|
|
||||||
id (int): database id of row in PathDetail table / primary key
|
|
||||||
ptype (int): database id of row in PathType table / foreign key
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.type:int=ptype
|
|
||||||
self.path:str=path
|
|
||||||
# construct icon_url based on type of storage path (icons.svg contains icons for each)
|
|
||||||
self.icon_url:str=url_for("internal", filename="icons.svg") + "#" + ICON[self.type]
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# helper function to find path details for move destinations - used in html
|
|
||||||
# for move DBox to show potential storage paths to move files into
|
|
||||||
################################################################################
|
|
||||||
def MovePathDetails():
|
|
||||||
"""helper function to find path details for move destinations
|
|
||||||
|
|
||||||
used in html/javascript for move Dialog Box to show potential storage paths to move files into
|
|
||||||
|
|
||||||
Args:
|
|
||||||
None
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ret (List[PathDetail]): a list of Path Details for where files can be moved
|
|
||||||
|
|
||||||
"""
|
|
||||||
ret=[]
|
|
||||||
sps=Path.query.join(PathType).filter(PathType.name=="Storage").all()
|
|
||||||
for p in sps:
|
|
||||||
obj = PathDetail( ptype="Storage", path=p.path_prefix.replace("static/Storage/","") )
|
|
||||||
ret.append( obj )
|
|
||||||
ips=Path.query.join(PathType).filter(PathType.name=="Import").all()
|
|
||||||
for p in ips:
|
|
||||||
obj = PathDetail( ptype="Import", path=p.path_prefix.replace("static/Import/","") )
|
|
||||||
ret.append( obj )
|
|
||||||
return ret
|
|
||||||
|
|||||||
71
person.py
71
person.py
@@ -3,8 +3,9 @@ from flask_wtf import FlaskForm
|
|||||||
from flask import request, render_template, redirect, url_for, make_response, jsonify
|
from flask import request, render_template, redirect, url_for, make_response, jsonify
|
||||||
from main import db, app, ma
|
from main import db, app, ma
|
||||||
from settings import Settings, AIModel
|
from settings import Settings, AIModel
|
||||||
from sqlalchemy import Sequence, func
|
from sqlalchemy import Sequence, func, select
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
from sqlalchemy.orm import joinedload
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from shared import GenFace, GenThumb, PA
|
from shared import GenFace, GenThumb, PA
|
||||||
@@ -114,7 +115,7 @@ def AddRefimgToPerson( filename, person ):
|
|||||||
SetFELog( f"<b>Failed to add Refimg:</b> {e.orig}", "danger" )
|
SetFELog( f"<b>Failed to add Refimg:</b> {e.orig}", "danger" )
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
SetFELog( f"<b>Failed to modify Refimg:</b> {e}", "danger" )
|
SetFELog( f"<b>Failed to modify Refimg:</b> {e}", "danger" )
|
||||||
return
|
return refimg
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# TempRefimgFile: helper function that takes data POST'd (from dialog box to
|
# TempRefimgFile: helper function that takes data POST'd (from dialog box to
|
||||||
@@ -182,9 +183,12 @@ def match_with_create_person():
|
|||||||
p = Person( tag=request.form["tag"], surname=request.form["surname"], firstname=request.form["firstname"] )
|
p = Person( tag=request.form["tag"], surname=request.form["surname"], firstname=request.form["firstname"] )
|
||||||
# add this fname (of temp refimg) to person
|
# add this fname (of temp refimg) to person
|
||||||
fname=TempRefimgFile( request.form['refimg_data'], p.tag )
|
fname=TempRefimgFile( request.form['refimg_data'], p.tag )
|
||||||
AddRefimgToPerson( fname, p )
|
r=AddRefimgToPerson( fname, p )
|
||||||
SetFELog( f"Created person: {p.tag}" )
|
SetFELog( f"Created person: {p.tag}" )
|
||||||
return make_response( jsonify( who=p.tag, distance='0.0' ) )
|
refimg_schema=RefimgSchema(many=False)
|
||||||
|
r_data=refimg_schema.dump(r)
|
||||||
|
|
||||||
|
return make_response( jsonify( refimg=r_data, who=p.tag, distance='0.0' ) )
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# /person/<id> -> GET/POST(save or delete) -> shows/edits/delets a single person
|
# /person/<id> -> GET/POST(save or delete) -> shows/edits/delets a single person
|
||||||
@@ -267,7 +271,7 @@ def add_refimg():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
SetFELog( f"<b>Failed to load reference image:</b> {e}", "danger" )
|
SetFELog( f"<b>Failed to load reference image:</b> {e}", "danger" )
|
||||||
|
|
||||||
AddRefimgToPerson( fname, person )
|
r=AddRefimgToPerson( fname, person )
|
||||||
return redirect( url_for( 'person', id=person.id) )
|
return redirect( url_for( 'person', id=person.id) )
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -289,6 +293,29 @@ def find_persons(who):
|
|||||||
|
|
||||||
return make_response( resp )
|
return make_response( resp )
|
||||||
|
|
||||||
|
class FaceRefimgLinkSchema(ma.SQLAlchemyAutoSchema):
|
||||||
|
class Meta: model = FaceRefimgLink
|
||||||
|
face_distance = ma.auto_field() # Explicitly include face_distance
|
||||||
|
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 FaceSchema(ma.SQLAlchemyAutoSchema):
|
||||||
|
class Meta:
|
||||||
|
model=Face
|
||||||
|
exclude = ('face',)
|
||||||
|
load_instance = True
|
||||||
|
refimg = ma.Nested(RefimgSchema,allow_none=True)
|
||||||
|
refimg_lnk = ma.Nested(FaceRefimgLinkSchema,allow_none=True)
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# /add_refimg_to_person/ -> POST
|
# /add_refimg_to_person/ -> POST
|
||||||
@@ -296,12 +323,19 @@ def find_persons(who):
|
|||||||
@app.route("/add_refimg_to_person", methods=["POST"])
|
@app.route("/add_refimg_to_person", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def add_refimg_to_person():
|
def add_refimg_to_person():
|
||||||
f = Face.query.get( request.form['face_id'] )
|
stmt = select(Face).options( joinedload(Face.refimg_lnk) ).where(Face.id == request.form['face_id'])
|
||||||
p = Person.query.get( request.form['person_id'] )
|
f=db.session.execute(stmt).scalars().first()
|
||||||
|
stmt = select(Person).options( joinedload(Person.refimg) ).where(Person.id == request.form['person_id'])
|
||||||
|
p=db.session.execute(stmt).scalars().first()
|
||||||
|
|
||||||
# add this fname (of temp refimg) to person
|
# add this fname (of temp refimg) to person
|
||||||
fname=TempRefimgFile( request.form['refimg_data'], p.tag )
|
fname=TempRefimgFile( request.form['refimg_data'], p.tag )
|
||||||
AddRefimgToPerson( fname, p )
|
r=AddRefimgToPerson( fname, p )
|
||||||
|
|
||||||
|
# connect the refimg to the face in the db, now we have added this refimg to the person
|
||||||
|
frl=FaceRefimgLink( face_id=f.id, refimg_id=r.id, face_distance=0 )
|
||||||
|
db.session.add(frl)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
if request.form['search'] == "true":
|
if request.form['search'] == "true":
|
||||||
jex=[]
|
jex=[]
|
||||||
@@ -316,7 +350,12 @@ def add_refimg_to_person():
|
|||||||
jex.append( JobExtra( name=f"path_type", value=str(ptype.id) ) )
|
jex.append( JobExtra( name=f"path_type", value=str(ptype.id) ) )
|
||||||
job=NewJob( name="run_ai_on_path", num_files=0, wait_for=None, jex=jex, desc="Look for face(s) in storage path(s)" )
|
job=NewJob( name="run_ai_on_path", num_files=0, wait_for=None, jex=jex, desc="Look for face(s) in storage path(s)" )
|
||||||
|
|
||||||
return make_response( jsonify( who=p.tag, distance='0.0' ) )
|
refimg_schema=RefimgSchema(many=False)
|
||||||
|
r_data=refimg_schema.dump(r)
|
||||||
|
frl_schema=FaceRefimgLinkSchema(many=False)
|
||||||
|
frl_data=refimg_schema.dump(r)
|
||||||
|
|
||||||
|
return make_response( jsonify( refimg=r_data, frl=frl_data ) )
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# /add_force_match_override -> POST
|
# /add_force_match_override -> POST
|
||||||
@@ -346,7 +385,9 @@ def add_force_match_override():
|
|||||||
NewJob( "metadata", num_files=0, wait_for=None, jex=jex, desc="create metadata for adding forced match" )
|
NewJob( "metadata", num_files=0, wait_for=None, jex=jex, desc="create metadata for adding forced match" )
|
||||||
|
|
||||||
# this will reply to the Ajax / POST, and cause the page to re-draw with new face override to person_tag
|
# this will reply to the Ajax / POST, and cause the page to re-draw with new face override to person_tag
|
||||||
return make_response( jsonify( person_tag=p.tag ) )
|
person_schema = PersonSchema(many=False)
|
||||||
|
p_data = person_schema.dump(p)
|
||||||
|
return make_response( jsonify( person=p_data ) )
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# /remove_force_match_override -> POST
|
# /remove_force_match_override -> POST
|
||||||
@@ -397,6 +438,11 @@ def remove_no_match_override():
|
|||||||
return make_response( jsonify( face_id=face_id ) )
|
return make_response( jsonify( face_id=face_id ) )
|
||||||
|
|
||||||
|
|
||||||
|
class FaceOverrideTypeSchema(ma.SQLAlchemyAutoSchema):
|
||||||
|
class Meta:
|
||||||
|
model = FaceOverrideType
|
||||||
|
load_instance = True
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# /add_no_match_override -> POST
|
# /add_no_match_override -> POST
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -424,5 +470,6 @@ def add_no_match_override():
|
|||||||
# dont do status update here, the F/E is in the middle of a dbox, just send metadata through to the B/E
|
# dont do status update here, the F/E is in the middle of a dbox, just send metadata through to the B/E
|
||||||
NewJob( "metadata", num_files=0, wait_for=None, jex=jex, desc="create metadata for adding forced non-match" )
|
NewJob( "metadata", num_files=0, wait_for=None, jex=jex, desc="create metadata for adding forced non-match" )
|
||||||
|
|
||||||
# this will reply to the Ajax / POST, and cause the page to re-draw with new face override to person_tag
|
fot_schema = FaceOverrideTypeSchema(many=False)
|
||||||
return make_response( jsonify( type=t.name ) )
|
t_data=fot_schema.dump(t)
|
||||||
|
return make_response( jsonify( type_id=t.id, type=t_data ) )
|
||||||
|
|||||||
41
query.py
Normal file
41
query.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from flask_login import UserMixin, login_required
|
||||||
|
from main import db
|
||||||
|
#from sqlalchemy import Sequence
|
||||||
|
#from flask import request, redirect, make_response, jsonify
|
||||||
|
#from main import db, app, ma
|
||||||
|
#from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=no-member
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Class describing Person in the database and DB via sqlalchemy
|
||||||
|
# id is unique id in DB
|
||||||
|
# dn is ldap distinguised name
|
||||||
|
# any entry in this DB is effectively a record you already authed successfully
|
||||||
|
# so acts as a session marker. If you fail ldap auth, you dont get a row here
|
||||||
|
################################################################################
|
||||||
|
class Query(UserMixin,db.Model):
|
||||||
|
__tablename__ = "query"
|
||||||
|
id = db.Column(db.Integer, db.Sequence('query_id_seq'), primary_key=True)
|
||||||
|
path_type = db.Column(db.String)
|
||||||
|
noo = db.Column(db.String)
|
||||||
|
grouping = db.Column(db.String)
|
||||||
|
q_offset = db.Column(db.Integer)
|
||||||
|
folder = db.Column(db.Boolean)
|
||||||
|
entry_list = db.Column(db.String)
|
||||||
|
root = db.Column(db.String)
|
||||||
|
cwd = db.Column(db.String)
|
||||||
|
search_term = db.Column(db.String)
|
||||||
|
current = db.Column(db.Integer)
|
||||||
|
created = db.Column(db.DateTime(timezone=True))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
str=f"<{self.__class__.__name__}("
|
||||||
|
for k, v in self.__dict__.items():
|
||||||
|
str += f"{k}={v!r}, "
|
||||||
|
str=str.rstrip(", ") + ")>"
|
||||||
|
return str
|
||||||
|
|
||||||
|
def get_id(self):
|
||||||
|
return self.dn
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
numpy==1.26.4
|
numpy==1.26.4
|
||||||
|
setuptools
|
||||||
flask
|
flask
|
||||||
flask_login
|
flask_login
|
||||||
flask-ldap3-login
|
flask-ldap3-login
|
||||||
|
|||||||
@@ -93,8 +93,8 @@ def CreateSelect(name, selected, list, js="", add_class="", vals={} ):
|
|||||||
# TODO: can this be collapsed into using above - probably if the 'selected' passed in was 'In Folder' or 'Flat View' -- but I think that isn't in a var???
|
# TODO: can this be collapsed into using above - probably if the 'selected' passed in was 'In Folder' or 'Flat View' -- but I think that isn't in a var???
|
||||||
# Helper function used in html files to create a bootstrap'd select with options. Same as CreateSelect() really, only contains
|
# Helper function used in html files to create a bootstrap'd select with options. Same as CreateSelect() really, only contains
|
||||||
# hard-coded True/False around the if selected part, but with string based "True"/"False" in the vals={}, and list has "In Folders", "Flat View"
|
# hard-coded True/False around the if selected part, but with string based "True"/"False" in the vals={}, and list has "In Folders", "Flat View"
|
||||||
def CreateFoldersSelect(selected, add_class=""):
|
def CreateFoldersSelect(selected, js="", add_class=""):
|
||||||
str = f'<select id="folders" name="folders" class="{add_class} sm-txt bg-white text-info border-info border-1 p-1" onChange="this.form.submit()">'
|
str = f'<select id="folders" name="folders" class="{add_class} sm-txt bg-white text-info border-info border-1 p-1" onChange="{js};this.form.submit()">'
|
||||||
# if selected is true, then folders == true, so make this the selected option
|
# if selected is true, then folders == true, so make this the selected option
|
||||||
if( selected ):
|
if( selected ):
|
||||||
str += '<option selected value="True">In Folders</option>'
|
str += '<option selected value="True">In Folders</option>'
|
||||||
|
|||||||
305
states.py
305
states.py
@@ -1,10 +1,12 @@
|
|||||||
from flask import request, render_template, redirect, url_for
|
from flask import request, render_template, redirect, url_for
|
||||||
|
from settings import Settings, SettingsIPath, SettingsSPath, SettingsRBPath
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from main import db, app, ma
|
from main import db, app, ma
|
||||||
from shared import PA
|
from shared import PA
|
||||||
from user import PAUser
|
from user import PAUser
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from job import SetFELog
|
from job import SetFELog
|
||||||
|
from shared import SymlinkName
|
||||||
import pytz
|
import pytz
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@@ -17,30 +19,17 @@ class PA_UserState(db.Model):
|
|||||||
__tablename__ = "pa_user_state"
|
__tablename__ = "pa_user_state"
|
||||||
id = db.Column(db.Integer, db.Sequence('pa_user_state_id_seq'), primary_key=True )
|
id = db.Column(db.Integer, db.Sequence('pa_user_state_id_seq'), primary_key=True )
|
||||||
pa_user_dn = db.Column(db.String, db.ForeignKey('pa_user.dn'), primary_key=True )
|
pa_user_dn = db.Column(db.String, db.ForeignKey('pa_user.dn'), primary_key=True )
|
||||||
last_used = db.Column(db.DateTime(timezone=True))
|
|
||||||
path_type = db.Column(db.String, primary_key=True, unique=False, nullable=False )
|
path_type = db.Column(db.String, primary_key=True, unique=False, nullable=False )
|
||||||
noo = db.Column(db.String, unique=False, nullable=False )
|
noo = db.Column(db.String, unique=False, nullable=False )
|
||||||
grouping = db.Column(db.String, unique=False, nullable=False )
|
grouping = db.Column(db.String, unique=False, nullable=False )
|
||||||
how_many = db.Column(db.Integer, unique=False, nullable=False )
|
how_many = db.Column(db.Integer, unique=False, nullable=False )
|
||||||
st_offset = db.Column(db.Integer, unique=False, nullable=False )
|
|
||||||
size = db.Column(db.Integer, unique=False, nullable=False )
|
size = db.Column(db.Integer, unique=False, nullable=False )
|
||||||
folders = db.Column(db.Boolean, unique=False, nullable=False )
|
folders = db.Column(db.Boolean, unique=False, nullable=False )
|
||||||
root = db.Column(db.String, unique=False, nullable=False )
|
root = db.Column(db.String, unique=False, nullable=False )
|
||||||
cwd = db.Column(db.String, unique=False, nullable=False )
|
cwd = db.Column(db.String, unique=False, nullable=False )
|
||||||
## for now being lazy and not doing a separate table until I settle on needed fields and when
|
|
||||||
# only used if ptype == View
|
|
||||||
view_eid = db.Column(db.Integer, unique=False, nullable=False )
|
|
||||||
orig_ptype = db.Column(db.String, unique=False, nullable=False )
|
|
||||||
# only used if view and orig_ptype was search
|
|
||||||
orig_search_term = db.Column(db.String, unique=False, nullable=False )
|
|
||||||
orig_url = db.Column(db.String, unique=False, nullable=False )
|
|
||||||
current = db.Column(db.Integer)
|
|
||||||
first_eid = db.Column(db.Integer)
|
|
||||||
last_eid = db.Column(db.Integer)
|
|
||||||
num_entries = db.Column(db.Integer)
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<pa_user_dn: {self.pa_user_dn}, path_type: {self.path_type}, noo: {self.noo}, grouping: {self.grouping}, how_many: {self.how_many}, st_offset: {self.st_offset}, size: {self.size}, folders: {self.folders}, root: {self.root}, cwd: {self.cwd}, view_eid: {self.view_eid}, orig_ptype: {self.orig_ptype}, orig_search_term: {self.orig_search_term}, orig_url: {self.orig_url}, current={self.current}, first_eid={self.first_eid}, last_eid={self.last_eid}, num_entries={self.num_entries}>"
|
return f"<pa_user_dn: {self.pa_user_dn}, path_type: {self.path_type}, noo: {self.noo}, grouping: {self.grouping}, how_many: {self.how_many}, size: {self.size}, folders: {self.folders}, root: {self.root}, cwd: {self.cwd}>"
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -50,264 +39,58 @@ class PA_UserState(db.Model):
|
|||||||
################################################################################
|
################################################################################
|
||||||
class States(PA):
|
class States(PA):
|
||||||
def __init__(self, request):
|
def __init__(self, request):
|
||||||
self.path_type=''
|
|
||||||
self.orig_search_term = ''
|
|
||||||
self.url = request.path
|
self.url = request.path
|
||||||
self.view_eid = None
|
|
||||||
self.current=0
|
|
||||||
self.first_eid=0
|
|
||||||
self.last_eid=0
|
|
||||||
self.num_entries=0
|
|
||||||
|
|
||||||
# this is any next/prev or noo, grouping, etc. change (so use referrer to work out what to do with this)
|
|
||||||
# because this can happen on a view, or files_up, etc. change this FIRST
|
|
||||||
if 'change_file_opts' in request.path:
|
|
||||||
base=request.base_url
|
|
||||||
base=base.replace("change_file_opts", "")
|
|
||||||
self.url = "/"+request.referrer.replace(base, "" )
|
|
||||||
|
|
||||||
# if view_list, then we really are a view, and view_eid should be in the form
|
|
||||||
if 'view_list' in request.path:
|
|
||||||
self.path_type = 'View'
|
|
||||||
self.view_eid = request.form['view_eid']
|
|
||||||
self.url = request.form['orig_url']
|
|
||||||
# this occurs ONLY when a POST to /view/<id> occurs (at this stage orig_url will be from an import, storage, bin or search)
|
|
||||||
elif 'view' in request.path:
|
|
||||||
self.path_type = 'View'
|
|
||||||
self.view_eid = self.url[6:]
|
|
||||||
# use orig url to define defaults/look up states for 'last' import/storage/bin/search
|
|
||||||
if request.method == "POST":
|
|
||||||
self.url = request.form['orig_url']
|
|
||||||
else:
|
|
||||||
# GET's occur on redirect, and we don't have a form, so get it from pref
|
|
||||||
st=self.url[8:]
|
|
||||||
if request.referrer and 'search' in request.referrer:
|
|
||||||
st=re.sub( '.+/search/', '', request.referrer )
|
|
||||||
else:
|
|
||||||
st=''
|
|
||||||
pref=PA_UserState.query.filter(PA_UserState.pa_user_dn==current_user.dn,PA_UserState.path_type==self.path_type,PA_UserState.view_eid==self.view_eid,PA_UserState.orig_search_term==st).first()
|
|
||||||
if not pref:
|
|
||||||
SetFELog( message=f"ERROR: pref not found - dn={current_user.dn}, st={st}, s={self}????" , level="danger", persistent=True, cant_close=False )
|
|
||||||
SetFELog( message=f"WARNING: I think this error occurred because you reloaded a page and the server had restarted between your original page load and this page reload, is that possible?" , level="warning", persistent=True, cant_close=False )
|
|
||||||
redirect("/")
|
|
||||||
else:
|
|
||||||
if not hasattr( pref, 'orig_url' ):
|
|
||||||
SetFELog( message=f"ERROR: orig_url not in pref - dn={current_user.dn}, st={st}, self={self}, pref={pref}????" , level="danger", persistent=True, cant_close=True )
|
|
||||||
redirect("/")
|
|
||||||
self.url = pref.orig_url
|
|
||||||
|
|
||||||
|
# set the prefix based on path
|
||||||
|
path=None
|
||||||
if 'files_ip' in self.url or 'file_list_ip' in self.url:
|
if 'files_ip' in self.url or 'file_list_ip' in self.url:
|
||||||
if self.path_type == "View":
|
self.path_type = 'Import'
|
||||||
self.orig_ptype = 'Import'
|
path = SettingsIPath()
|
||||||
self.orig_url = self.url
|
|
||||||
else:
|
|
||||||
self.path_type = 'Import'
|
|
||||||
elif 'files_sp' in self.url:
|
elif 'files_sp' in self.url:
|
||||||
if self.path_type == "View":
|
self.path_type = 'Storage'
|
||||||
self.orig_ptype = 'Storage'
|
path = SettingsSPath()
|
||||||
self.orig_url = self.url
|
|
||||||
else:
|
|
||||||
self.path_type = 'Storage'
|
|
||||||
elif 'files_rbp' in self.url:
|
elif 'files_rbp' in self.url:
|
||||||
if self.path_type == "View":
|
self.path_type = 'Bin'
|
||||||
self.orig_ptype = 'Bin'
|
path = SettingsRBPath()
|
||||||
self.orig_url = self.url
|
|
||||||
else:
|
|
||||||
self.path_type = 'Bin'
|
|
||||||
elif 'search' in self.url:
|
elif 'search' in self.url:
|
||||||
# okay if we are a search, but came from a view then get last_search_state form prefs and use it
|
self.path_type = 'Search'
|
||||||
m=re.match( '.*search/(.+)$', self.url )
|
self.search_term = ''
|
||||||
if m == None:
|
|
||||||
SetFELog( message=f"ERROR: DDP messed up, seems we are processing a search, but cant see the search term - is this even possible?" )
|
|
||||||
return
|
|
||||||
self.orig_search_term = m[1]
|
|
||||||
if self.path_type == "View":
|
|
||||||
self.orig_ptype = 'Search'
|
|
||||||
self.orig_url = self.url
|
|
||||||
else:
|
|
||||||
self.path_type = 'Search'
|
|
||||||
elif 'view' in self.url:
|
|
||||||
# use url to get eid of viewed entry
|
|
||||||
self.view_eid = self.url[6:]
|
|
||||||
|
|
||||||
# force this to be a search so rest of code won't totally die, but also not return anything
|
|
||||||
self.path_type="Search"
|
|
||||||
self.orig_url=self.url
|
|
||||||
elif 'change_file_opts' not in self.url:
|
|
||||||
SetFELog( message=f"ERROR: DDP messed up, failed to match URL {self.url} for settings this will fail, redirecting to home" , level="danger", persistent=True, cant_close=True )
|
|
||||||
SetFELog( message=f"referrer={request.referrer}" , level="danger", persistent=True, cant_close=True )
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.path_type == 'View':
|
|
||||||
pref=PA_UserState.query.filter(PA_UserState.pa_user_dn==current_user.dn,PA_UserState.path_type==self.path_type,PA_UserState.view_eid==self.view_eid,PA_UserState.orig_search_term==self.orig_search_term).first()
|
|
||||||
if not hasattr( self, 'orig_ptype' ):
|
|
||||||
self.orig_ptype='View'
|
|
||||||
self.orig_url=''
|
|
||||||
SetFELog( message=f"ERROR: No orig ptype? s={self} - pref={pref}, redirecting to home" , level="danger", persistent=True, cant_close=True )
|
|
||||||
SetFELog( message=f"referrer={request.referrer}" , level="danger", persistent=True, cant_close=True )
|
|
||||||
redirect("/")
|
|
||||||
|
|
||||||
# should find original path or search for this view (if not a search, search_term='')
|
|
||||||
orig_pref=PA_UserState.query.filter(PA_UserState.pa_user_dn==current_user.dn,PA_UserState.path_type==self.orig_ptype,PA_UserState.orig_search_term==self.orig_search_term).first()
|
|
||||||
if not orig_pref:
|
|
||||||
SetFELog( message=f"ERROR: DDP messed up 2, failed to find orig_pref for a view pt={self.path_type} for search={self.orig_search_term}" , level="danger", persistent=True, cant_close=True )
|
|
||||||
SetFELog( message=f"referrer={request.referrer}" , level="danger", persistent=True, cant_close=True )
|
|
||||||
return
|
|
||||||
elif self.path_type == 'Search':
|
|
||||||
pref=PA_UserState.query.filter(PA_UserState.pa_user_dn==current_user.dn,PA_UserState.path_type==self.path_type,PA_UserState.orig_search_term==self.orig_search_term).first()
|
|
||||||
else:
|
else:
|
||||||
pref=PA_UserState.query.filter(PA_UserState.pa_user_dn==current_user.dn,PA_UserState.path_type==self.path_type).first()
|
self.path_type=''
|
||||||
|
|
||||||
if pref:
|
if path:
|
||||||
self.grouping=pref.grouping
|
self.prefix = SymlinkName(self.path_type,path,path+'/')
|
||||||
self.how_many=pref.how_many
|
|
||||||
self.offset=pref.st_offset
|
|
||||||
self.size=pref.size
|
|
||||||
self.cwd=pref.cwd
|
|
||||||
self.orig_ptype=pref.orig_ptype
|
|
||||||
self.orig_search_term=pref.orig_search_term
|
|
||||||
self.orig_url = pref.orig_url
|
|
||||||
self.view_eid = pref.view_eid
|
|
||||||
self.current = pref.current
|
|
||||||
if self.path_type == "View":
|
|
||||||
self.root='static/' + self.orig_ptype
|
|
||||||
self.first_eid=orig_pref.first_eid
|
|
||||||
self.last_eid=orig_pref.last_eid
|
|
||||||
self.num_entries=orig_pref.num_entries
|
|
||||||
self.noo=orig_pref.noo
|
|
||||||
self.folders=orig_pref.folders
|
|
||||||
self.orig_search_term=orig_pref.orig_search_term
|
|
||||||
else:
|
|
||||||
self.root=pref.root
|
|
||||||
self.first_eid = pref.first_eid
|
|
||||||
self.last_eid = pref.last_eid
|
|
||||||
self.num_entries = pref.num_entries
|
|
||||||
self.noo=pref.noo
|
|
||||||
self.folders=pref.folders
|
|
||||||
else:
|
else:
|
||||||
# retreive defaults from 'PAUser' where defaults are stored
|
self.prefix=None
|
||||||
u=PAUser.query.filter(PAUser.dn==current_user.dn).one()
|
|
||||||
self.grouping=u.default_grouping
|
# retreive defaults from 'PAUser' where defaults are stored
|
||||||
self.how_many=u.default_how_many
|
u=PAUser.query.filter(PAUser.dn==current_user.dn).one()
|
||||||
self.offset=0
|
self.grouping=u.default_grouping
|
||||||
self.size=u.default_size
|
self.how_many=u.default_how_many
|
||||||
if self.path_type == "View":
|
self.size=u.default_size
|
||||||
self.root='static/' + self.orig_ptype
|
self.root='static/' + self.path_type
|
||||||
self.first_eid=orig_pref.first_eid
|
if self.path_type == 'Import':
|
||||||
self.last_eid=orig_pref.last_eid
|
self.noo = u.default_import_noo
|
||||||
self.num_entries=orig_pref.num_entries
|
self.folders = u.default_import_folders
|
||||||
self.noo=orig_pref.noo
|
elif self.path_type == 'Storage':
|
||||||
self.folders=orig_pref.folders
|
self.noo = u.default_storage_noo
|
||||||
self.orig_search_term=orig_pref.orig_search_term
|
self.folders = u.default_storage_folders
|
||||||
else:
|
else:
|
||||||
self.root='static/' + self.path_type
|
# search so force folders to be false (rather see images, # than series of folders that dont match search themselves)
|
||||||
if self.path_type == 'Import':
|
self.noo=u.default_search_noo
|
||||||
self.noo = u.default_import_noo
|
self.folders=False
|
||||||
self.folders = u.default_import_folders
|
|
||||||
elif self.path_type == 'Storage':
|
|
||||||
self.noo = u.default_storage_noo
|
|
||||||
self.folders = u.default_storage_folders
|
|
||||||
else:
|
|
||||||
# search so force folders to be false (rather see images, # than series of folders that dont match search themselves)
|
|
||||||
self.noo=u.default_search_noo
|
|
||||||
self.folders=False
|
|
||||||
|
|
||||||
self.cwd=self.root
|
self.default_flat_noo=u.default_import_noo
|
||||||
if not hasattr(self, 'orig_ptype'):
|
self.default_folder_noo=u.default_storage_noo
|
||||||
self.orig_ptype=None
|
self.default_search_noo=u.default_search_noo
|
||||||
if not hasattr(self, 'orig_search_term'):
|
self.cwd=self.root
|
||||||
self.orig_search_term=None
|
|
||||||
self.orig_url = self.url
|
|
||||||
|
|
||||||
# the above are defaults, if we are here, then we have current values, use them instead if they are set -- AI: searches dont set them so then we use those in the DB first
|
|
||||||
if request.method=="POST":
|
|
||||||
if self.path_type != "View" and 'noo' in request.form:
|
|
||||||
# we are changing values based on a POST to the form, if we changed the noo option, we need to reset things
|
|
||||||
if 'change_file_opts' in request.path and self.noo != request.form['noo']:
|
|
||||||
self.noo=request.form['noo']
|
|
||||||
self.first_eid=0
|
|
||||||
self.last_eid=0
|
|
||||||
self.offset=0
|
|
||||||
if 'how_many' in request.form:
|
|
||||||
self.how_many=request.form['how_many']
|
|
||||||
if 'offset' in request.form:
|
|
||||||
self.offset=int(request.form['offset'])
|
|
||||||
if 'grouping' in request.form:
|
|
||||||
self.grouping=request.form['grouping']
|
|
||||||
# this can be null if we come from view by details
|
|
||||||
if 'size' in request.form:
|
|
||||||
self.size = request.form['size']
|
|
||||||
# seems html cant do boolean, but uses strings so convert
|
|
||||||
if self.path_type != "View" and 'folders' in request.form:
|
|
||||||
# we are changing values based on a POST to the form, if we are in folder view and we changed the folders option, we need to reset things
|
|
||||||
if 'change_file_opts' in request.path:
|
|
||||||
if self.folders and self.folders != request.form['folders']:
|
|
||||||
self.num_entries=0
|
|
||||||
self.first_eid=0
|
|
||||||
self.last_eid=0
|
|
||||||
if request.form['folders'] == "False":
|
|
||||||
self.folders=False
|
|
||||||
else:
|
|
||||||
self.folders=True
|
|
||||||
# have to force grouping to None if we flick to folders from a flat view with grouping (otherwise we print out
|
|
||||||
# group headings for child content that is not in the CWD)
|
|
||||||
self.grouping=None
|
|
||||||
if 'orig_url' in request.form:
|
|
||||||
self.orig_url = request.form['orig_url']
|
|
||||||
|
|
||||||
# possible to not be set for an AI: search
|
|
||||||
if 'cwd' in request.form:
|
|
||||||
self.cwd = request.form['cwd']
|
|
||||||
if 'prev' in request.form:
|
|
||||||
self.offset -= int(self.how_many)
|
|
||||||
# just in case we hit prev too fast, stop this...
|
|
||||||
if self.offset < 0:
|
|
||||||
self.offset=0
|
|
||||||
if 'next' in request.form:
|
|
||||||
if (self.offset + int(self.how_many)) < self.num_entries:
|
|
||||||
self.offset += int(self.how_many)
|
|
||||||
else:
|
|
||||||
# tripping this still
|
|
||||||
SetFELog( message=f"WARNING: next image requested, but would go past end of list? - ignore this" , level="warning", persistent=True, cant_close=False )
|
|
||||||
if 'current' in request.form:
|
|
||||||
self.current = int(request.form['current'])
|
|
||||||
|
|
||||||
last_used=datetime.now(pytz.utc)
|
|
||||||
# now save pref
|
|
||||||
if not pref:
|
|
||||||
# insert new pref for this combo (might be a new search or view, or first time for a path)
|
|
||||||
pref=PA_UserState( pa_user_dn=current_user.dn, last_used=last_used, path_type=self.path_type, view_eid=self.view_eid,
|
|
||||||
noo=self.noo, grouping=self.grouping, how_many=self.how_many, st_offset=self.offset, size=self.size,
|
|
||||||
folders=self.folders, root=self.root, cwd=self.cwd, orig_ptype=self.orig_ptype, orig_search_term=self.orig_search_term,
|
|
||||||
orig_url=self.orig_url, current=self.current, first_eid=self.first_eid, last_eid=self.last_eid, num_entries=self.num_entries )
|
|
||||||
else:
|
|
||||||
# update this pref with the values calculated above (most likely from POST to form)
|
|
||||||
pref.pa_user_dn=current_user.dn
|
|
||||||
pref.path_type=self.path_type
|
|
||||||
pref.view_eid=self.view_eid
|
|
||||||
pref.noo=self.noo
|
|
||||||
pref.grouping=self.grouping
|
|
||||||
pref.how_many=self.how_many
|
|
||||||
pref.st_offset=self.offset
|
|
||||||
pref.size=self.size
|
|
||||||
pref.folders=self.folders
|
|
||||||
pref.root = self.root
|
|
||||||
pref.cwd = self.cwd
|
|
||||||
pref.orig_ptype = self.orig_ptype
|
|
||||||
pref.orig_search_term = self.orig_search_term
|
|
||||||
pref.orig_url = self.orig_url
|
|
||||||
pref.last_used = last_used
|
|
||||||
pref.first_eid = self.first_eid
|
|
||||||
pref.last_eid = self.last_eid
|
|
||||||
pref.num_entries = self.num_entries
|
|
||||||
# only passed in (at the moment) in view_list
|
|
||||||
pref.current = self.current
|
|
||||||
|
|
||||||
db.session.add(pref)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
# Automatically include all instance attributes
|
||||||
|
return {key: value for key, value in vars(self).items()}
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# /states -> GET only -> prints out list of all prefs (simple for now)
|
# /states -> GET only -> prints out list of all prefs (simple for now)
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|||||||
326
tables.sql
326
tables.sql
@@ -1,189 +1,209 @@
|
|||||||
alter database PA set timezone to 'Australia/Victoria';
|
ALTER DATABASE pa SET TIMEZONE TO 'Australia/Victoria';
|
||||||
|
|
||||||
create sequence PA_USER_ID_SEQ;
|
CREATE SEQUENCE pa_user_id_seq;
|
||||||
create sequence PA_USER_STATE_ID_SEQ;
|
CREATE SEQUENCE pa_user_state_id_seq;
|
||||||
create sequence FACE_ID_SEQ;
|
CREATE SEQUENCE face_id_seq;
|
||||||
create sequence PATH_ID_SEQ;
|
CREATE SEQUENCE path_id_seq;
|
||||||
create sequence PATH_TYPE_ID_SEQ;
|
CREATE SEQUENCE path_type_id_seq;
|
||||||
create sequence FILE_ID_SEQ;
|
CREATE SEQUENCE file_id_seq;
|
||||||
create sequence FILE_TYPE_ID_SEQ;
|
CREATE SEQUENCE file_type_id_seq;
|
||||||
create sequence JOBEXTRA_ID_SEQ;
|
CREATE SEQUENCE jobextra_id_seq;
|
||||||
create sequence JOBLOG_ID_SEQ;
|
CREATE SEQUENCE joblog_id_seq;
|
||||||
create sequence JOB_ID_SEQ;
|
CREATE SEQUENCE job_id_seq;
|
||||||
create sequence PERSON_ID_SEQ;
|
CREATE SEQUENCE person_id_seq;
|
||||||
create sequence REFIMG_ID_SEQ;
|
CREATE SEQUENCE refimg_id_seq;
|
||||||
create sequence SETTINGS_ID_SEQ;
|
CREATE SEQUENCE settings_id_seq;
|
||||||
create sequence PA_JOB_MANAGER_ID_SEQ;
|
CREATE SEQUENCE pa_job_manager_id_seq;
|
||||||
create sequence PA_JOB_MANAGER_FE_MESSAGE_ID_SEQ;
|
CREATE SEQUENCE pa_job_manager_fe_message_id_seq;
|
||||||
create sequence FACE_OVERRIDE_TYPE_ID_SEQ;
|
CREATE SEQUENCE face_override_type_id_seq;
|
||||||
create sequence FACE_OVERRIDE_ID_SEQ;
|
CREATE SEQUENCE face_override_id_seq;
|
||||||
|
CREATE SEQUENCE query_id_seq;
|
||||||
|
|
||||||
-- these are hard-coded at present, not sure I can reflexively find models from API?
|
-- these are hard-coded at present, not sure I can reflexively find models from API?
|
||||||
create table AI_MODEL ( ID integer, NAME varchar(24), DESCRIPTION varchar(80), constraint PK_AI_MODEL primary key(ID) );
|
CREATE TABLE ai_model ( id INTEGER, name VARCHAR(24), description VARCHAR(80), CONSTRAINT pk_ai_model PRIMARY KEY(id) );
|
||||||
insert into AI_MODEL values ( 1, 'hog', 'normal' );
|
INSERT INTO ai_model VALUES ( 1, 'hog', 'normal' );
|
||||||
insert into AI_MODEL values ( 2, 'cnn', 'more accurate / much slower' );
|
INSERT INTO ai_model VALUES ( 2, 'cnn', 'more accurate / much slower' );
|
||||||
|
|
||||||
create table SETTINGS(
|
CREATE TABLE settings(
|
||||||
ID integer,
|
id INTEGER,
|
||||||
BASE_PATH varchar, IMPORT_PATH varchar, STORAGE_PATH varchar, RECYCLE_BIN_PATH varchar, METADATA_PATH varchar,
|
base_path VARCHAR, import_path VARCHAR, storage_path VARCHAR, recycle_bin_path VARCHAR, metadata_path VARCHAR,
|
||||||
AUTO_ROTATE Boolean,
|
auto_rotate BOOLEAN,
|
||||||
DEFAULT_REFIMG_MODEL integer, DEFAULT_SCAN_MODEL integer, DEFAULT_THRESHOLD float,
|
default_refimg_model INTEGER, default_scan_model INTEGER, default_threshold FLOAT,
|
||||||
FACE_SIZE_LIMIT integer,
|
face_size_limit INTEGER,
|
||||||
SCHEDULED_IMPORT_SCAN integer, SCHEDULED_STORAGE_SCAN integer,
|
scheduled_import_scan INTEGER, scheduled_storage_scan INTEGER,
|
||||||
SCHEDULED_BIN_CLEANUP integer, BIN_CLEANUP_FILE_AGE integer,
|
scheduled_bin_cleanup INTEGER, bin_cleanup_file_age INTEGER,
|
||||||
JOB_ARCHIVE_AGE integer,
|
job_archive_age INTEGER,
|
||||||
constraint PK_SETTINGS_ID primary key(ID),
|
CONSTRAINT pk_settings_id PRIMARY KEY(id),
|
||||||
constraint FK_DEFAULT_REFIMG_MODEL foreign key (DEFAULT_REFIMG_MODEL) references AI_MODEL(ID),
|
CONSTRAINT fk_default_refimg_model FOREIGN KEY (default_refimg_model) REFERENCES ai_model(id),
|
||||||
constraint FK_DEFAULT_SCAN_MODEL foreign key (DEFAULT_SCAN_MODEL) references AI_MODEL(ID) );
|
CONSTRAINT fk_default_scan_model FOREIGN KEY (default_scan_model) REFERENCES ai_model(id) );
|
||||||
|
|
||||||
create table PA_USER(
|
CREATE TABLE pa_user(
|
||||||
ID integer,
|
id INTEGER,
|
||||||
DN varchar unique,
|
dn VARCHAR UNIQUE,
|
||||||
DEFAULT_IMPORT_NOO varchar,
|
default_import_noo VARCHAR,
|
||||||
DEFAULT_STORAGE_NOO varchar,
|
default_storage_noo VARCHAR,
|
||||||
DEFAULT_SEARCH_NOO varchar,
|
default_search_noo VARCHAR,
|
||||||
DEFAULT_GROUPING varchar(16),
|
default_grouping VARCHAR(16),
|
||||||
DEFAULT_HOW_MANY integer,
|
default_how_many INTEGER,
|
||||||
DEFAULT_SIZE integer,
|
default_size INTEGER,
|
||||||
DEFAULT_IMPORT_FOLDERS Boolean,
|
default_import_folders BOOLEAN,
|
||||||
DEFAULT_STORAGE_FOLDERS Boolean,
|
default_storage_folders BOOLEAN,
|
||||||
constraint PK_PA_USER_ID primary key(ID) );
|
CONSTRAINT pk_pa_user_id PRIMARY KEY(id) );
|
||||||
|
|
||||||
-- this is totally not 3rd normal form, but when I made it that, it was so complex, it was stupid
|
-- FIXME: NEED TO RETHINK THIS, not sure this even needs to be in the DB
|
||||||
-- so for the little data here, I'm deliberately doing a redundant data structure
|
CREATE TABLE pa_user_state ( id INTEGER, pa_user_dn VARCHAR(128), path_type VARCHAR(16),
|
||||||
create table PA_USER_STATE ( ID integer, PA_USER_DN varchar(128), PATH_TYPE varchar(16),
|
noo VARCHAR(16), grouping VARCHAR(16), how_many INTEGER, size INTEGER, folders BOOLEAN,
|
||||||
NOO varchar(16), GROUPING varchar(16), HOW_MANY integer, ST_OFFSET integer, SIZE integer, FOLDERS Boolean,
|
root VARCHAR, cwd VARCHAR,
|
||||||
ROOT varchar, CWD varchar,
|
CONSTRAINT fk_pa_user_dn FOREIGN KEY (pa_user_dn) REFERENCES pa_user(dn),
|
||||||
ORIG_PTYPE varchar, ORIG_SEARCH_TERM varchar, ORIG_URL varchar,
|
CONSTRAINT pk_pa_user_states_id PRIMARY KEY(id ) );
|
||||||
VIEW_EID integer, CURRENT integer, FIRST_EID integer, LAST_EID integer, NUM_ENTRIES integer, LAST_USED timestamptz,
|
|
||||||
constraint FK_PA_USER_DN foreign key (PA_USER_DN) references PA_USER(DN),
|
|
||||||
constraint PK_PA_USER_STATES_ID primary key(ID ) );
|
|
||||||
|
|
||||||
create table FILE_TYPE ( ID integer, NAME varchar(32) unique, constraint PK_FILE_TYPE_ID primary key(ID) );
|
CREATE TABLE query ( id INTEGER, path_type VARCHAR(16), noo VARCHAR(16), grouping VARCHAR(16), q_offset INTEGER,
|
||||||
|
entry_list VARCHAR, folders BOOLEAN, root VARCHAR, cwd VARCHAR, search_term VARCHAR, current INTEGER,
|
||||||
create table PATH_TYPE ( ID integer, NAME varchar(16) unique, constraint PK_PATH_TYPE_ID primary key(ID) );
|
created TIMESTAMPTZ,
|
||||||
|
CONSTRAINT pk_query_id PRIMARY KEY(id ) );
|
||||||
create table PATH ( ID integer, TYPE_ID integer, PATH_PREFIX varchar(1024), NUM_FILES integer,
|
|
||||||
constraint PK_PATH_ID primary key(ID),
|
|
||||||
constraint FK_PATH_TYPE_TYPE_ID foreign key (TYPE_ID) references PATH_TYPE(ID) );
|
|
||||||
|
|
||||||
create table ENTRY( ID integer, NAME varchar(128), TYPE_ID integer, EXISTS_ON_FS boolean,
|
|
||||||
constraint PK_ENTRY_ID primary key(ID),
|
|
||||||
constraint FK_FILE_TYPE_TYPE_ID foreign key (TYPE_ID) references FILE_TYPE(ID) );
|
|
||||||
|
|
||||||
create table FILE ( EID integer, SIZE_MB integer, HASH varchar(34), THUMBNAIL varchar, FACES_CREATED_ON float, LAST_HASH_DATE float, LAST_AI_SCAN float, YEAR integer, MONTH integer, DAY integer, WOY integer,
|
|
||||||
constraint PK_FILE_ID primary key(EID),
|
|
||||||
constraint FK_FILE_ENTRY_ID foreign key (EID) references ENTRY(ID) );
|
|
||||||
|
|
||||||
create table DEL_FILE ( FILE_EID integer, ORIG_PATH_PREFIX varchar(1024), constraint PK_DEL_FILE_FILE_EID primary key (FILE_EID),
|
|
||||||
constraint FK_ENTRY_ID foreign key (FILE_EID) references FILE(EID) );
|
|
||||||
|
|
||||||
create table DIR ( EID integer, REL_PATH varchar(256), NUM_FILES integer, LAST_IMPORT_DATE float,
|
|
||||||
constraint PK_DIR_EID primary key(EID),
|
|
||||||
constraint FK_DIR_ENTRY_ID foreign key (EID) references ENTRY(ID) );
|
|
||||||
|
|
||||||
create table PATH_DIR_LINK ( path_id integer, dir_eid integer,
|
|
||||||
constraint PK_PDL_path_id_dir_eid primary key (path_id, dir_eid),
|
|
||||||
constraint FK_PDL_PATH_ID foreign key (PATH_ID) references PATH(ID),
|
|
||||||
constraint FK_PDL_DIR_EID foreign key (DIR_EID) references DIR(EID) );
|
|
||||||
|
|
||||||
create table ENTRY_DIR_LINK ( entry_id integer, dir_eid integer,
|
|
||||||
constraint PK_EDL_entry_id_dir_eid primary key (entry_id, dir_eid),
|
|
||||||
constraint FK_EDL_ENTRY_ID foreign key (ENTRY_ID) references ENTRY(ID),
|
|
||||||
constraint FK_EDL_DIR_EID foreign key (DIR_EID) references DIR(EID) );
|
|
||||||
|
|
||||||
create table PERSON ( ID integer default nextval('PERSON_ID_SEQ'), TAG varchar(48), FIRSTNAME varchar(48), SURNAME varchar(48),
|
|
||||||
constraint PK_PERSON_ID primary key(ID) );
|
|
||||||
alter sequence PERSON_ID_SEQ owned by PERSON.ID;
|
|
||||||
|
|
||||||
|
|
||||||
create table REFIMG ( ID integer, FNAME varchar(128), FACE bytea, ORIG_W integer, ORIG_H integer,
|
CREATE TABLE file_type ( id INTEGER, name VARCHAR(32) UNIQUE, CONSTRAINT pk_file_type_id PRIMARY KEY(id) );
|
||||||
FACE_TOP integer, FACE_RIGHT integer, FACE_BOTTOM integer, FACE_LEFT integer, CREATED_ON float, THUMBNAIL varchar, MODEL_USED integer,
|
|
||||||
constraint PK_REFIMG_ID primary key(ID),
|
|
||||||
constraint FK_REFIMG_MODEL_USED foreign key (MODEL_USED) references AI_MODEL(ID) );
|
|
||||||
alter sequence REFIMG_ID_SEQ owned by REFIMG.ID;
|
|
||||||
|
|
||||||
create table FACE( ID integer, FACE bytea, FACE_TOP integer, FACE_RIGHT integer, FACE_BOTTOM integer, FACE_LEFT integer,
|
CREATE TABLE path_type ( id INTEGER, name VARCHAR(16) UNIQUE, CONSTRAINT pk_path_type_id PRIMARY KEY(id) );
|
||||||
W integer, H integer, constraint PK_FACE_ID primary key(ID) );
|
|
||||||
|
|
||||||
create table FACE_FILE_LINK( FACE_ID integer, FILE_EID integer, MODEL_USED integer,
|
CREATE TABLE path ( id INTEGER, type_id INTEGER, path_prefix VARCHAR(1024), num_files INTEGER,
|
||||||
constraint PK_FFL_FACE_ID_FILE_ID primary key(FACE_ID, FILE_EID),
|
CONSTRAINT pk_path_id PRIMARY KEY(id),
|
||||||
constraint FK_FFL_FACE_ID foreign key (FACE_ID) references FACE(ID) on delete cascade,
|
CONSTRAINT fk_path_type_type_id FOREIGN KEY (type_id) REFERENCES path_type(id) );
|
||||||
constraint FK_FFL_FILE_EID foreign key (FILE_EID) references FILE(EID),
|
|
||||||
constraint FK_FFL_MODEL_USED foreign key (MODEL_USED) references AI_MODEL(ID) );
|
|
||||||
|
|
||||||
create table FACE_REFIMG_LINK( FACE_ID integer, REFIMG_ID integer, FACE_DISTANCE float,
|
CREATE TABLE entry( id INTEGER, name VARCHAR(128), type_id INTEGER, exists_on_fs BOOLEAN,
|
||||||
constraint PK_FRL_FACE_ID_REFIMG_ID primary key(FACE_ID, REFIMG_ID),
|
CONSTRAINT pk_entry_id PRIMARY KEY(id),
|
||||||
constraint FK_FRL_FACE_ID foreign key (FACE_ID) references FACE(ID) on delete cascade,
|
CONSTRAINT fk_file_type_type_id FOREIGN KEY (type_id) REFERENCES file_type(id) );
|
||||||
constraint FK_FRL_REFIMG_ID foreign key (REFIMG_ID) references REFIMG(ID) );
|
|
||||||
|
|
||||||
create table FACE_OVERRIDE_TYPE ( ID integer, NAME varchar unique, constraint PK_FACE_OVERRIDE_TYPE_ID primary key(ID) );
|
CREATE TABLE file ( eid INTEGER, size_mb INTEGER, hash VARCHAR(34), thumbnail VARCHAR, faces_created_on FLOAT, last_hash_date FLOAT, last_ai_scan FLOAT, year INTEGER, month INTEGER, day INTEGER, woy INTEGER,
|
||||||
insert into FACE_OVERRIDE_TYPE values ( (select nextval('FACE_OVERRIDE_TYPE_ID_SEQ')), 'Manual match to existing person' );
|
CONSTRAINT pk_file_id PRIMARY KEY(eid),
|
||||||
insert into FACE_OVERRIDE_TYPE values ( (select nextval('FACE_OVERRIDE_TYPE_ID_SEQ')), 'Not a face' );
|
CONSTRAINT fk_file_entry_id FOREIGN KEY (eid) REFERENCES entry(id) );
|
||||||
insert into FACE_OVERRIDE_TYPE values ( (select nextval('FACE_OVERRIDE_TYPE_ID_SEQ')), 'Too young' );
|
|
||||||
insert into FACE_OVERRIDE_TYPE values ( (select nextval('FACE_OVERRIDE_TYPE_ID_SEQ')), 'Ignore face' );
|
CREATE TABLE del_file ( file_eid INTEGER, orig_path_prefix VARCHAR(1024), CONSTRAINT pk_del_file_file_eid PRIMARY KEY (file_eid),
|
||||||
|
CONSTRAINT fk_entry_id FOREIGN KEY (file_eid) REFERENCES file(eid) );
|
||||||
|
|
||||||
|
CREATE TABLE dir ( eid INTEGER, rel_path VARCHAR(256), num_files INTEGER, last_import_date FLOAT,
|
||||||
|
CONSTRAINT pk_dir_eid PRIMARY KEY(eid),
|
||||||
|
CONSTRAINT fk_dir_entry_id FOREIGN KEY (eid) REFERENCES entry(id) );
|
||||||
|
|
||||||
|
CREATE TABLE path_dir_link ( PATH_ID INTEGER, DIR_EID INTEGER,
|
||||||
|
CONSTRAINT pk_pdl_PATH_ID_DIR_EID PRIMARY KEY (PATH_ID, DIR_EID),
|
||||||
|
CONSTRAINT fk_pdl_path_id FOREIGN KEY (path_id) REFERENCES path(id),
|
||||||
|
CONSTRAINT fk_pdl_dir_eid FOREIGN KEY (dir_eid) REFERENCES dir(eid) );
|
||||||
|
|
||||||
|
CREATE TABLE entry_dir_link ( ENTRY_ID INTEGER, DIR_EID INTEGER,
|
||||||
|
CONSTRAINT pk_edl_ENTRY_ID_DIR_EID PRIMARY KEY (ENTRY_ID, DIR_EID),
|
||||||
|
CONSTRAINT fk_edl_entry_id FOREIGN KEY (entry_id) REFERENCES entry(id),
|
||||||
|
CONSTRAINT fk_edl_dir_eid FOREIGN KEY (dir_eid) REFERENCES dir(eid) );
|
||||||
|
|
||||||
|
CREATE TABLE person ( id INTEGER DEFAULT NEXTVAL('person_id_seq'), tag VARCHAR(48), firstname VARCHAR(48), surname VARCHAR(48),
|
||||||
|
CONSTRAINT pk_person_id PRIMARY KEY(id) );
|
||||||
|
ALTER SEQUENCE person_id_seq OWNED BY person.id;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE refimg ( id INTEGER, fname VARCHAR(128), face BYTEA, orig_w INTEGER, orig_h INTEGER,
|
||||||
|
face_top INTEGER, face_right INTEGER, face_bottom INTEGER, face_left INTEGER, created_on FLOAT, thumbnail VARCHAR, model_used INTEGER,
|
||||||
|
CONSTRAINT pk_refimg_id PRIMARY KEY(id),
|
||||||
|
CONSTRAINT fk_refimg_model_used FOREIGN KEY (model_used) REFERENCES ai_model(id) );
|
||||||
|
ALTER SEQUENCE refimg_id_seq OWNED BY refimg.id;
|
||||||
|
|
||||||
|
CREATE TABLE face( id INTEGER, face BYTEA, face_top INTEGER, face_right INTEGER, face_bottom INTEGER, face_left INTEGER,
|
||||||
|
w INTEGER, h INTEGER, CONSTRAINT pk_face_id PRIMARY KEY(id) );
|
||||||
|
|
||||||
|
CREATE TABLE face_file_link( face_id INTEGER, file_eid INTEGER, model_used INTEGER,
|
||||||
|
CONSTRAINT pk_ffl_face_id_file_id PRIMARY KEY(face_id, file_eid),
|
||||||
|
CONSTRAINT fk_ffl_face_id FOREIGN KEY (face_id) REFERENCES face(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_ffl_file_eid FOREIGN KEY (file_eid) REFERENCES file(eid),
|
||||||
|
CONSTRAINT fk_ffl_model_used FOREIGN KEY (model_used) REFERENCES ai_model(id) );
|
||||||
|
|
||||||
|
CREATE TABLE face_refimg_link( face_id INTEGER, refimg_id INTEGER, face_distance FLOAT,
|
||||||
|
CONSTRAINT pk_frl_face_id_refimg_id PRIMARY KEY(face_id, refimg_id),
|
||||||
|
CONSTRAINT fk_frl_face_id FOREIGN KEY (face_id) REFERENCES face(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_frl_refimg_id FOREIGN KEY (refimg_id) REFERENCES refimg(id) );
|
||||||
|
|
||||||
|
CREATE TABLE face_override_type ( id INTEGER, name VARCHAR UNIQUE, CONSTRAINT pk_face_override_type_id PRIMARY KEY(id) );
|
||||||
|
INSERT INTO face_override_type VALUES ( (SELECT NEXTVAL('face_override_type_id_seq')), 'Manual match to existing person' );
|
||||||
|
INSERT INTO face_override_type VALUES ( (SELECT NEXTVAL('face_override_type_id_seq')), 'Not a face' );
|
||||||
|
INSERT INTO face_override_type VALUES ( (SELECT NEXTVAL('face_override_type_id_seq')), 'Too young' );
|
||||||
|
INSERT INTO face_override_type VALUES ( (SELECT NEXTVAL('face_override_type_id_seq')), 'Ignore face' );
|
||||||
|
|
||||||
-- keep non-redundant FACE because, when we rebuild data we may have a null FACE_ID, but still want to connect to this override
|
-- keep non-redundant FACE because, when we rebuild data we may have a null FACE_ID, but still want to connect to this override
|
||||||
-- from a previous AI pass... (would happen if we delete a file and then reimport/scan it), OR, more likely we change (say) a threshold, etc.
|
-- from a previous AI pass... (would happen if we delete a file and then reimport/scan it), OR, more likely we change (say) a threshold, etc.
|
||||||
-- any reordering of faces, generates new face_ids... (but if the face data was the same, then this override should stand)
|
-- any reordering of faces, generates new face_ids... (but if the face data was the same, then this override should stand)
|
||||||
create table FACE_NO_MATCH_OVERRIDE ( ID integer, FACE_ID integer, TYPE_ID integer,
|
CREATE TABLE face_no_match_override ( id INTEGER, face_id INTEGER, type_id INTEGER,
|
||||||
constraint FK_FNMO_FACE_ID foreign key (FACE_ID) references FACE(ID),
|
CONSTRAINT fk_fnmo_face_id FOREIGN KEY (face_id) REFERENCES face(id),
|
||||||
constraint FK_FNMO_TYPE foreign key (TYPE_ID) references FACE_OVERRIDE_TYPE(ID),
|
CONSTRAINT fk_fnmo_type FOREIGN KEY (type_id) REFERENCES face_override_type(id),
|
||||||
constraint PK_FNMO_ID primary key(ID) );
|
CONSTRAINT pk_fnmo_id PRIMARY KEY(id) );
|
||||||
|
|
||||||
-- manual match goes to person not refimg, so on search, etc. we deal with this anomaly (via sql not ORM)
|
-- manual match goes to person not refimg, so on search, etc. we deal with this anomaly (via sql not ORM)
|
||||||
create table FACE_FORCE_MATCH_OVERRIDE ( ID integer, FACE_ID integer, PERSON_ID integer, constraint PK_FACE_FORCE_MATCH_OVERRIDE_ID primary key(ID) );
|
CREATE TABLE face_force_match_override ( id INTEGER, face_id INTEGER, person_id INTEGER, CONSTRAINT pk_face_force_match_override_id PRIMARY KEY(id) );
|
||||||
|
|
||||||
create table DISCONNECTED_NO_MATCH_OVERRIDE ( FACE bytea, TYPE_ID integer,
|
CREATE TABLE disconnected_no_match_override ( face BYTEA, type_id INTEGER,
|
||||||
constraint FK_DNMO_TYPE_ID foreign key (TYPE_ID) references FACE_OVERRIDE_TYPE(ID),
|
CONSTRAINT fk_dnmo_type_id FOREIGN KEY (type_id) REFERENCES face_override_type(id),
|
||||||
constraint PK_DNMO_FACE primary key (FACE) );
|
CONSTRAINT pk_dnmo_face PRIMARY KEY (face) );
|
||||||
|
|
||||||
create table DISCONNECTED_FORCE_MATCH_OVERRIDE ( FACE bytea, PERSON_ID integer,
|
CREATE TABLE disconnected_force_match_override ( face BYTEA, person_id INTEGER,
|
||||||
constraint FK_DFMO_PERSON_ID foreign key (PERSON_ID) references PERSON(ID),
|
CONSTRAINT fk_dfmo_person_id FOREIGN KEY (person_id) REFERENCES person(id),
|
||||||
constraint PK_DFMO_FACE primary key (FACE) );
|
CONSTRAINT pk_dfmo_face PRIMARY KEY (face) );
|
||||||
|
|
||||||
create table PERSON_REFIMG_LINK ( PERSON_ID integer, REFIMG_ID integer,
|
CREATE TABLE person_refimg_link ( person_id INTEGER, refimg_id INTEGER,
|
||||||
constraint PK_PRL primary key(PERSON_ID, REFIMG_ID),
|
CONSTRAINT pk_prl PRIMARY KEY(person_id, refimg_id),
|
||||||
constraint FK_PRL_PERSON_ID foreign key (PERSON_ID) references PERSON(ID),
|
CONSTRAINT fk_prl_person_id FOREIGN KEY (person_id) REFERENCES person(id),
|
||||||
constraint FK_PRL_REFIMG_ID foreign key (REFIMG_ID) references REFIMG(ID),
|
CONSTRAINT fk_prl_refimg_id FOREIGN KEY (refimg_id) REFERENCES refimg(id),
|
||||||
constraint U_PRL_REFIMG_ID unique(REFIMG_ID) );
|
CONSTRAINT u_prl_refimg_id UNIQUE(refimg_id) );
|
||||||
|
|
||||||
create table JOB (
|
CREATE TABLE job (
|
||||||
ID integer, START_TIME timestamptz, LAST_UPDATE timestamptz, NAME varchar(64), STATE varchar(128),
|
id INTEGER, start_time TIMESTAMPTZ, last_update TIMESTAMPTZ, name VARCHAR(64), state VARCHAR(128),
|
||||||
NUM_FILES integer, CURRENT_FILE_NUM integer, CURRENT_FILE varchar(256), WAIT_FOR integer, PA_JOB_STATE varchar(48),
|
num_files INTEGER, current_file_num INTEGER, current_file VARCHAR(256), wait_for INTEGER, pa_job_state VARCHAR(48),
|
||||||
constraint PK_JOB_ID primary key(ID) );
|
CONSTRAINT pk_job_id PRIMARY KEY(id) );
|
||||||
|
|
||||||
-- used to pass / keep extra values, e.g. num_files for jobs that have sets of files, or out* for adding output from jobs that you want to pass to next job in the chain
|
-- used to pass / keep extra values, e.g. num_files for jobs that have sets of files, or out* for adding output from jobs that you want to pass to next job in the chain
|
||||||
create table JOBEXTRA ( ID integer, JOB_ID integer, NAME varchar(32), VALUE varchar,
|
CREATE TABLE jobextra ( id INTEGER, job_id INTEGER, name VARCHAR(32), value VARCHAR,
|
||||||
constraint PK_JOBEXTRA_ID primary key(ID), constraint FK_JOBEXTRA_JOB_ID foreign key(JOB_ID) references JOB(ID) );
|
CONSTRAINT pk_jobextra_id PRIMARY KEY(id), CONSTRAINT fk_jobextra_job_id FOREIGN KEY(job_id) REFERENCES job(id) );
|
||||||
|
|
||||||
create table JOBLOG ( ID integer, JOB_ID integer, LOG_DATE timestamptz, LOG varchar,
|
CREATE TABLE joblog ( id INTEGER, job_id INTEGER, log_date TIMESTAMPTZ, log VARCHAR,
|
||||||
constraint PK_JL_ID primary key(ID), constraint FK_JL_JOB_ID foreign key(JOB_ID) references JOB(ID) );
|
CONSTRAINT pk_jl_id PRIMARY KEY(id), CONSTRAINT fk_jl_job_id FOREIGN KEY(job_id) REFERENCES job(id) );
|
||||||
|
|
||||||
create table PA_JOB_MANAGER_FE_MESSAGE ( ID integer, JOB_ID integer, LEVEL varchar(16), MESSAGE varchar(8192), PERSISTENT boolean, CANT_CLOSE boolean,
|
CREATE TABLE pa_job_manager_fe_message ( id INTEGER, job_id INTEGER, level VARCHAR(16), message VARCHAR(8192), persistent BOOLEAN, cant_close BOOLEAN,
|
||||||
constraint PA_JOB_MANAGER_FE_ACKS_ID primary key(ID),
|
CONSTRAINT pk_pa_job_manager_fe_acks_id PRIMARY KEY(id),
|
||||||
constraint FK_PA_JOB_MANAGER_FE_MESSAGE_JOB_ID foreign key(JOB_ID) references JOB(ID) );
|
CONSTRAINT fk_pa_job_manager_fe_message_job_id FOREIGN KEY(job_id) REFERENCES job(id) );
|
||||||
|
|
||||||
|
CREATE TABLE amendment_type ( id INTEGER, job_name VARCHAR(64), which VARCHAR(8), what VARCHAR(32), colour VARCHAR(32),
|
||||||
|
CONSTRAINT pk_amendment_type_id PRIMARY KEY(id) );
|
||||||
|
INSERT INTO amendment_type ( id, job_name, which, what, colour ) VALUES ( 1, 'delete_files', 'icon', 'trash', 'var(--bs-danger)' );
|
||||||
|
INSERT INTO amendment_type ( id, job_name, which, what, colour ) VALUES ( 2, 'restore_files', 'icon', 'trash', 'var(--bs-success)' );
|
||||||
|
INSERT INTO amendment_type ( id, job_name, which, what, colour ) VALUES ( 3, 'transform_image:90', 'img', 'rot90.png', '#009EFF' );
|
||||||
|
INSERT INTO amendment_type ( id, job_name, which, what, colour ) VALUES ( 4, 'transform_image:180', 'img', 'rot180.png', '#009EFF' );
|
||||||
|
INSERT INTO amendment_type ( id, job_name, which, what, colour ) VALUES ( 5, 'transform_image:270', 'img', 'rot270.png', '#009EFF' );
|
||||||
|
INSERT INTO amendment_type ( id, job_name, which, what, colour ) VALUES ( 6, 'transform_image:fliph', 'icon', 'flip_h', '#009EFF' );
|
||||||
|
INSERT INTO amendment_type ( id, job_name, which, what, colour ) VALUES ( 7, 'transform_image:flipv', 'icon', 'flip_v', '#009EFF' );
|
||||||
|
INSERT INTO amendment_type ( id, job_name, which, what, colour ) VALUES ( 8, 'move_files', 'icon', 'folder_plus', 'var(--bs-primary)' );
|
||||||
|
|
||||||
|
CREATE TABLE entry_amendment ( amend_type INTEGER, eid INTEGER, job_id INTEGER,
|
||||||
|
CONSTRAINT pk_entry_amendment_eid_job_id PRIMARY KEY(eid,job_id),
|
||||||
|
CONSTRAINT fk_entry_amendment_amendment_type FOREIGN KEY(amend_type) REFERENCES amendment_type(id),
|
||||||
|
CONSTRAINT fk_entry_amendment_job_id FOREIGN KEY(job_id) REFERENCES job(id) );
|
||||||
|
|
||||||
-- default data for types of paths
|
-- default data for types of paths
|
||||||
insert into PATH_TYPE values ( (select nextval('PATH_TYPE_ID_SEQ')), 'Import' );
|
INSERT INTO path_type VALUES ( (SELECT NEXTVAL('path_type_id_seq')), 'Import' );
|
||||||
insert into PATH_TYPE values ( (select nextval('PATH_TYPE_ID_SEQ')), 'Storage' );
|
INSERT INTO path_type VALUES ( (SELECT NEXTVAL('path_type_id_seq')), 'Storage' );
|
||||||
insert into PATH_TYPE values ( (select nextval('PATH_TYPE_ID_SEQ')), 'Bin' );
|
INSERT INTO path_type VALUES ( (SELECT NEXTVAL('path_type_id_seq')), 'Bin' );
|
||||||
insert into PATH_TYPE values ( (select nextval('PATH_TYPE_ID_SEQ')), 'Metadata' );
|
INSERT INTO path_type VALUES ( (SELECT NEXTVAL('path_type_id_seq')), 'Metadata' );
|
||||||
|
|
||||||
-- default data for types of files
|
-- default data for types of files
|
||||||
insert into FILE_TYPE values ( (select nextval('FILE_TYPE_ID_SEQ')), 'Image' );
|
INSERT INTO file_type VALUES ( (SELECT NEXTVAL('file_type_id_seq')), 'Image' );
|
||||||
insert into FILE_TYPE values ( (select nextval('FILE_TYPE_ID_SEQ')), 'Video' );
|
INSERT INTO file_type VALUES ( (SELECT NEXTVAL('file_type_id_seq')), 'Video' );
|
||||||
insert into FILE_TYPE values ( (select nextval('FILE_TYPE_ID_SEQ')), 'Directory' );
|
INSERT INTO file_type VALUES ( (SELECT NEXTVAL('file_type_id_seq')), 'Directory' );
|
||||||
insert into FILE_TYPE values ( (select nextval('FILE_TYPE_ID_SEQ')), 'Unknown' );
|
INSERT INTO file_type VALUES ( (SELECT NEXTVAL('file_type_id_seq')), 'Unknown' );
|
||||||
|
|
||||||
-- fake data only for making testing easier
|
-- fake data only for making testing easier
|
||||||
--insert into PERSON values ( (select nextval('PERSON_ID_SEQ')), 'dad', 'Damien', 'De Paoli' );
|
--INSERT INTO person VALUES ( (SELECT NEXTVAL('person_id_seq')), 'dad', 'Damien', 'De Paoli' );
|
||||||
--insert into PERSON values ( (select nextval('PERSON_ID_SEQ')), 'mum', 'Mandy', 'De Paoli' );
|
--INSERT INTO person VALUES ( (SELECT NEXTVAL('person_id_seq')), 'mum', 'Mandy', 'De Paoli' );
|
||||||
--insert into PERSON values ( (select nextval('PERSON_ID_SEQ')), 'cam', 'Cameron', 'De Paoli' );
|
--INSERT INTO person VALUES ( (SELECT NEXTVAL('person_id_seq')), 'cam', 'Cameron', 'De Paoli' );
|
||||||
--insert into PERSON values ( (select nextval('PERSON_ID_SEQ')), 'mich', 'Michelle', 'De Paoli' );
|
--INSERT INTO person VALUES ( (SELECT NEXTVAL('person_id_seq')), 'mich', 'Michelle', 'De Paoli' );
|
||||||
-- DEV(ddp):
|
-- DEV(ddp):
|
||||||
insert into SETTINGS ( id, base_path, import_path, storage_path, recycle_bin_path, metadata_path, auto_rotate, default_refimg_model, default_scan_model, default_threshold, face_size_limit, scheduled_import_scan, scheduled_storage_scan, scheduled_bin_cleanup, bin_cleanup_file_age, job_archive_age ) values ( (select nextval('SETTINGS_ID_SEQ')), '/home/ddp/src/photoassistant/', 'images_to_process/', 'photos/', '.pa_bin/', '.pa_metadata/', true, 1, 1, '0.55', 43, 1, 1, 7, 30, 3 );
|
INSERT INTO settings ( id, base_path, import_path, storage_path, recycle_bin_path, metadata_path, auto_rotate, default_refimg_model, default_scan_model, default_threshold, face_size_limit, scheduled_import_scan, scheduled_storage_scan, scheduled_bin_cleanup, bin_cleanup_file_age, job_archive_age ) VALUES ( (SELECT NEXTVAL('settings_id_seq')), '/home/ddp/src/photoassistant/', 'images_to_process/', 'photos/', '.pa_bin/', '.pa_metadata/', true, 1, 1, '0.55', 43, 1, 1, 7, 30, 3 );
|
||||||
-- DEV(cam):
|
-- DEV(cam):
|
||||||
--insert into SETTINGS ( id, base_path, import_path, storage_path, recycle_bin_path, metadata_path, auto_rotate, default_refimg_model, default_scan_model, default_threshold, face_size_limit, scheduled_import_scan, scheduled_storage_scan, scheduled_bin_cleanup, bin_cleanup_file_age, job_archive_age ) values ( (select nextval('SETTINGS_ID_SEQ')), 'c:/Users/cam/Desktop/code/python/photoassistant/', 'c:\images_to_process', 'photos/', '.pa_bin/', '.pa_metadata/', true, 1, 1, '0.55', 43, 1, 1, 7, 30, 3 );
|
--INSERT INTO settings ( id, base_path, import_path, storage_path, recycle_bin_path, metadata_path, auto_rotate, default_refimg_model, default_scan_model, default_threshold, face_size_limit, scheduled_import_scan, scheduled_storage_scan, scheduled_bin_cleanup, bin_cleanup_file_age, job_archive_age ) VALUES ( (select nextval('SETTINGS_ID_SEQ')), 'c:/Users/cam/Desktop/code/python/photoassistant/', 'c:\images_to_process', 'photos/', '.pa_bin/', '.pa_metadata/', TRUE, 1, 1, '0.55', 43, 1, 1, 7, 30, 3 );
|
||||||
-- PROD:
|
-- PROD:
|
||||||
--insert into SETTINGS ( id, base_path, import_path, storage_path, recycle_bin_path, metadata_path, auto_rotate, default_refimg_model, default_scan_model, default_threshold, face_size_limit, scheduled_import_scan, scheduled_storage_scan, scheduled_bin_cleanup, bin_cleanup_file_age, job_archive_age ) values ( (select nextval('SETTINGS_ID_SEQ')), '/export/docker/storage/', 'Camera_uploads/', 'photos/', '.pa_bin/', '.pa_metadata/', true, 1, 1, '0.55', 43, 1, 1, 7, 30, 4 );
|
--INSERT INTO settings ( id, base_path, import_path, storage_path, recycle_bin_path, metadata_path, auto_rotate, default_refimg_model, default_scan_model, default_threshold, face_size_limit, scheduled_import_scan, scheduled_storage_scan, scheduled_bin_cleanup, bin_cleanup_file_age, job_archive_age ) VALUES ( (SELECT NEXTVAL('settings_id_seq')), '/export/docker/storage/', 'Camera_uploads/', 'photos/', '.pa_bin/', '.pa_metadata/', TRUE, 1, 1, '0.55', 43, 1, 1, 7, 30, 4 );
|
||||||
|
|||||||
@@ -136,7 +136,7 @@
|
|||||||
|
|
||||||
{% if not InDBox %}
|
{% if not InDBox %}
|
||||||
{%block script_content %}{% endblock script_content %}
|
{%block script_content %}{% endblock script_content %}
|
||||||
<div id="status_container" class="position-fixed top-0 end-0 p-0 my-5" "z-index: 11"> </div>
|
<div id="status_container" class="position-fixed top-0 end-0 p-0 my-5" style="z-index: 9999"> </div>
|
||||||
<!-- CheckForJobs(), will see if there are any messages/jobs and keep doing this until there are 0 more and then stop -->
|
<!-- CheckForJobs(), will see if there are any messages/jobs and keep doing this until there are 0 more and then stop -->
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() { CheckForJobs() } )
|
$(document).ready(function() { CheckForJobs() } )
|
||||||
|
|||||||
@@ -1,64 +1,52 @@
|
|||||||
{% extends "base.html" %} {% block main_content %}
|
{% extends "base.html" %} {% block main_content %}
|
||||||
|
<script src="{{ url_for( 'internal', filename='js/files_support.js')}}?v={{js_vers['fs']}}"></script>
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<h3 class="offset-2">{{page_title}}</h3>
|
<h3 class="offset-2">{{page_title}}</h3>
|
||||||
<form id="main_form" method="POST">
|
|
||||||
<input id="offset" type="hidden" name="offset" value="{{OPT.offset}}">
|
|
||||||
<input id="grouping" type="hidden" name="grouping" value="">
|
|
||||||
<input id="folders" type="hidden" name="folders" value="False">
|
|
||||||
<div class="col col-auto">
|
<div class="col col-auto">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
{{CreateSelect( "noo", OPT.noo, ["Oldest", "Newest","A to Z", "Z to A"], "$('#offset').val(0)", "rounded-start py-1 my-1")|safe }}
|
{{CreateSelect( "noo", OPT.noo, ["Oldest", "Newest","A to Z", "Z to A"], "changeOPT(getPageFileList); return false", "rounded-start py-1 my-1")|safe }}
|
||||||
{{CreateSelect( "how_many", OPT.how_many|string, ["10", "25", "50", "75", "100", "150", "200", "500"], "", "rounded-end py-1 my-1" )|safe }}
|
{{CreateSelect( "how_many", OPT.how_many|string, ["10", "25", "50", "75", "100", "150", "200", "500"], "changeOPT(getPageFileList); return false", "rounded-end py-1 my-1" )|safe }}
|
||||||
<div class="mb-1 col my-auto d-flex justify-content-center">
|
<div class="mb-1 col my-auto d-flex justify-content-center">
|
||||||
{% set prv_disabled="" %}
|
<button id="prev" name="prev" class="prev sm-txt btn btn-outline-secondary" onClick="prevPage(getPageFileList)">
|
||||||
{% if OPT.offset|int == 0 %}
|
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#prev"/></svg>
|
||||||
{% set prv_disabled="disabled" %}
|
|
||||||
{% endif %}
|
|
||||||
<button id="prev" {{prv_disabled}} name="prev" class="prev sm-txt btn btn-outline-secondary">
|
|
||||||
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#prev"/></svg>
|
|
||||||
</button>
|
</button>
|
||||||
<span class="sm-txt my-auto"> {{OPT.how_many}} files </span>
|
<span class="how_many_text sm-txt my-auto"> {{OPT.how_many}} files </span>
|
||||||
{% set nxt_disabled="" %}
|
<button id="next" name="next" class="next sm-txt btn btn-outline-secondary" onClick="nextPage(getPageFileList)">
|
||||||
{% if entry_data|length < OPT.how_many|int %}
|
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#next"/></svg>
|
||||||
{% set nxt_disabled="disabled" %}
|
|
||||||
{% endif %}
|
|
||||||
<button id="next" {{nxt_disabled}} name="next" class="next sm-txt btn btn-outline-secondary">
|
|
||||||
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#next"/></svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div class="col...">
|
</div class="col...">
|
||||||
</div class="input-group...">
|
</div class="input-group...">
|
||||||
</div class="col col-auto">
|
</div class="col col-auto">
|
||||||
</form
|
|
||||||
<div class="row">
|
|
||||||
<table class="table table-striped table-sm col-xl-12">
|
|
||||||
<thead><tr class="table-primary"><th>Name</th><th>Size (MB)</th><th>Path Prefix</th><th>Hash</th></tr></thead><tbody>
|
|
||||||
{% for obj in entry_data %}
|
|
||||||
<tr><td>
|
|
||||||
{% if obj.type.name == "Image" or obj.type.name == "Video" %}
|
|
||||||
<figure class="figure" font-size: 24px;>
|
|
||||||
<div style="position:relative; width:100%">
|
|
||||||
{% if obj.type.name=="Image" %}
|
|
||||||
<a href="{{obj.in_dir.in_path.path_prefix}}/{{obj.in_dir.rel_path}}/{{obj.name}}">
|
|
||||||
{% elif obj.type.name == "Video" %}
|
|
||||||
<a href="{{obj.in_dir.in_path.path_prefix}}/{{obj.in_dir.rel_path}}/{{obj.name}}">
|
|
||||||
{% endif %}
|
|
||||||
<img class="thumb" style="display:block" height="48" src="data:image/jpeg;base64,{{obj.file_details.thumbnail}}"></img>
|
|
||||||
{% if obj.type.name=="Image" or obj.type.name == "Video" %}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<figcaption class="figure-caption">{{obj.name}}</figcaption>
|
|
||||||
</figure>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
{% if obj.type.name != "Directory" %}
|
|
||||||
<td>{{obj.file_details.size_mb}}</td><td>{{obj.in_dir.in_path.path_prefix.replace("static/","")}}/{{obj.in_dir.rel_path}}</td><td>{{obj.file_details.hash}}</td>
|
|
||||||
{% else %}
|
|
||||||
<td></td><td></td><td></td>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody></table>
|
|
||||||
</div class="row">
|
|
||||||
</div class="container">
|
</div class="container">
|
||||||
|
<div id="file_list_div" class="container-fluid pt-2">
|
||||||
|
</div class="container">
|
||||||
|
<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(getPageFileList)" disabled>
|
||||||
|
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#prev"/></svg>
|
||||||
|
</button>
|
||||||
|
<span class="how_many_text sm-txt my-auto"> {{OPT.how_many}} files </span>
|
||||||
|
<button aria-label="next" id="next" name="next" class="next sm-txt btn btn-outline-secondary" onClick="nextPage(getPageFileList)">
|
||||||
|
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#next"/></svg>
|
||||||
|
</button>
|
||||||
|
</div class="col my-auto"> </div class="row">
|
||||||
|
</div class="container-fluid">
|
||||||
{% endblock main_content %}
|
{% endblock main_content %}
|
||||||
|
{% block script_content %}
|
||||||
|
<script>
|
||||||
|
// this is the list of entry ids for the images for ALL matches for this query
|
||||||
|
var entryList={{query_data.entry_list}}
|
||||||
|
var OPT = {{ OPT.to_dict()|tojson }};
|
||||||
|
// set from query data and stored in OPT for convenience. It can be 0 -
|
||||||
|
// this implies no content in the Path at all
|
||||||
|
OPT.root_eid = {{ query_data.root_eid }};
|
||||||
|
|
||||||
|
// pageList is just those entries shown on this page from the full entryList
|
||||||
|
var pageList=[]
|
||||||
|
// force pageList to set pageList for & render the first page
|
||||||
|
getPage( 1, getPageFileList )
|
||||||
|
</script>
|
||||||
|
{% endblock script_content %}
|
||||||
|
|||||||
@@ -1,45 +1,43 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block main_content %}
|
{% block main_content %}
|
||||||
|
|
||||||
<script src="{{ url_for( 'internal', filename='js/files_support.js')}}"></script>
|
<style>
|
||||||
<script src="{{ url_for( 'internal', filename='js/files_transform.js')}}"></script>
|
@media (max-width: 576px) {
|
||||||
|
#la, #ra {
|
||||||
|
padding: 5% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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/files_transform.js')}}?v={{ js_vers['ft'] }}"></script>
|
||||||
|
<script src="{{ url_for( 'internal', filename='js/files_support.js')}}?v={{ js_vers['fs'] }}"></script>
|
||||||
|
<script src="{{ url_for( 'internal', filename='js/view_support.js')}}?v={{ js_vers['vs'] }}"></script>
|
||||||
|
|
||||||
<script>
|
<div id="files_div">
|
||||||
document.fake_shift=0
|
<div class="container-fluid">
|
||||||
document.fake_ctrl=0
|
|
||||||
var move_paths=[]
|
|
||||||
{% for p in move_paths %}
|
|
||||||
p = new Object()
|
|
||||||
p.type = '{{p.type}}'
|
|
||||||
p.path = '{{p.path}}'
|
|
||||||
p.icon_url = '{{p.icon_url}}'
|
|
||||||
move_paths.push(p)
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
document.OPT = '{{OPT}}'
|
|
||||||
document.entries = '{{entry_data}}'
|
|
||||||
document.how_many = '{{OPT.how_many}}'
|
|
||||||
document.entries_len = '{{entry_data|length}}'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="container-fluid">
|
|
||||||
<form id="main_form" method="POST" action="/change_file_opts">
|
|
||||||
<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}}">
|
|
||||||
{% endif %}
|
|
||||||
<div class="d-flex row mb-2">
|
<div class="d-flex row mb-2">
|
||||||
{% if OPT.folders %}
|
{% if OPT.folders %}
|
||||||
<div class="my-auto col col-auto">
|
<div class="my-auto col col-auto">
|
||||||
<span class="alert alert-primary py-2">
|
<span class="alert alert-primary py-2">
|
||||||
{% if "files_ip" in request.url %}
|
{% if "files_ip" in request.url %}
|
||||||
<svg width="20" height="20" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#import"/></svg>
|
<svg width="20" height="20" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#import"/></svg>
|
||||||
{% set tmp_path=OPT.cwd | replace( "static/Import", "" ) + "/" %}
|
{% set tmp_path=OPT.cwd | replace( "static/Import", "" ) + "/" %}
|
||||||
{% elif "files_sp" in request.url %}
|
{% elif "files_sp" in request.url %}
|
||||||
<svg width="20" height="20" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#db"/></svg>
|
<svg width="20" height="20" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#db"/></svg>
|
||||||
{% set tmp_path=OPT.cwd | replace( "static/Storage", "" ) + "/" %}
|
{% set tmp_path=OPT.cwd | replace( "static/Storage", "" ) + "/" %}
|
||||||
{% elif "files_rbp" in request.url %}
|
{% elif "files_rbp" in request.url %}
|
||||||
<svg width="20" height="20" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#trash"/></svg>
|
<svg width="20" height="20" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#trash"/></svg>
|
||||||
{% set tmp_path=OPT.cwd | replace( "static/Bin", "" ) + "/" %}
|
{% set tmp_path=OPT.cwd | replace( "static/Bin", "" ) + "/" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{tmp_path}}</span>
|
{{tmp_path}}</span>
|
||||||
@@ -47,15 +45,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="col col-auto">
|
<div class="col col-auto">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
{{CreateSelect( "noo", OPT.noo, ["Oldest", "Newest","A to Z", "Z to A"], "$('#offset').val(0)", "rounded-start py-2")|safe }}
|
{{CreateSelect( "noo", OPT.noo, ["Oldest", "Newest","A to Z", "Z to A"], "changeOPT(getPageFigures); return false", "rounded-start py-2")|safe }}
|
||||||
{{CreateSelect( "how_many", OPT.how_many|string, ["10", "25", "50", "75", "100", "150", "200", "500"])|safe }}
|
{{CreateSelect( "how_many", OPT.how_many|string, ["10", "25", "50", "75", "100", "150", "200", "500"], "changeOPT(getPageFigures); return false" )|safe }}
|
||||||
{% if OPT.folders %}
|
{% if OPT.folders %}
|
||||||
<input type="hidden" name="grouping" id="grouping" value="{{OPT.grouping}}">
|
{{CreateFoldersSelect( OPT.folders, "changeOPT(getPageFigures); return false", "rounded-end" )|safe }}
|
||||||
{{CreateFoldersSelect( OPT.folders, "rounded-end" )|safe }}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{{CreateFoldersSelect( OPT.folders )|safe }}
|
{{CreateFoldersSelect( OPT.folders, "changeOPT(getPageFigures); return false" )|safe }}
|
||||||
<span class="sm-txt my-auto btn btn-outline-info disabled border-top border-bottom">grouped by:</span>
|
<span class="sm-txt my-auto btn btn-outline-info disabled border-top border-bottom">grouped by:</span>
|
||||||
{{CreateSelect( "grouping", OPT.grouping, ["None", "Day", "Week", "Month"], "", "rounded-end")|safe }}
|
{{CreateSelect( "grouping", OPT.grouping, ["None", "Day", "Week", "Month"], "OPT.grouping=$('#grouping').val();drawPageOfFigures();return false", "rounded-end")|safe }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div class="input-group">
|
</div class="input-group">
|
||||||
</div class="col">
|
</div class="col">
|
||||||
@@ -63,368 +60,272 @@
|
|||||||
<div class="col col-auto my-auto">
|
<div class="col col-auto my-auto">
|
||||||
<span class="alert alert-primary p-2">Searched for: '{{search_term}}'</span>
|
<span class="alert alert-primary p-2">Searched for: '{{search_term}}'</span>
|
||||||
</div class="col my-auto">
|
</div class="col my-auto">
|
||||||
<script>
|
|
||||||
$('#folders').prop('disabled', 'disabled').removeClass('border-info').addClass('border-secondary').removeClass('text-info').addClass('text-secondary');
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="col flex-grow-1 my-auto d-flex justify-content-center w-100">
|
<div class="col flex-grow-1 my-auto d-flex justify-content-center w-100">
|
||||||
<button aria-label="prev" id="prev" name="prev" class="prev sm-txt btn btn-outline-secondary">
|
<button aria-label="prev" id="prev" name="prev" class="prev sm-txt btn btn-outline-secondary disabled" onClick="prevPage(getPageFigures)" disabled>
|
||||||
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#prev"/></svg>
|
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#prev"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<span class="sm-txt my-auto"> {{OPT.how_many}} files </span>
|
<span class="how_many_text sm-txt my-auto"> {{OPT.how_many}} files </span>
|
||||||
{% set nxt_disabled="" %}
|
<button aria-label="next" id="next" name="next" class="next sm-txt btn btn-outline-secondary" onClick="nextPage(getPageFigures)">
|
||||||
{% if not entry_data or entry_data|length < OPT.how_many|int %}
|
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#next"/></svg>
|
||||||
{% set nxt_disabled="disabled" %}
|
|
||||||
{% endif %}
|
|
||||||
<button aria-label="next" id="next" {{nxt_disabled}} name="next" class="next sm-txt btn btn-outline-secondary">
|
|
||||||
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#next"/></svg>
|
|
||||||
</button>
|
</button>
|
||||||
<button aria-label="move" id="move" disabled name="move" class="sm-txt btn btn-outline-primary ms-4" onClick="MoveDBox(move_paths,'{{url_for('internal', filename='icons.svg')}}'); return false;">
|
<button aria-label="move" id="move" disabled name="move" class="sm-txt btn btn-outline-primary ms-4" onClick="MoveDBox(move_paths); return false;">
|
||||||
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#folder_plus"/></svg>
|
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#folder_plus"/></svg>
|
||||||
</button>
|
</button>
|
||||||
{% if "files_rbp" in request.url %}
|
{% if "files_rbp" in request.url %}
|
||||||
<button aria-label="delete" id="del" disabled name="del" class="sm-txt btn btn-outline-success mx-1" onClick="DelDBox('Restore'); return false;">
|
<button aria-label="delete" id="del" disabled name="del" class="sm-txt btn btn-outline-success mx-1" onClick="DelDBox('Restore'); return false;">
|
||||||
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#trash-fill"/></svg>
|
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#trash-fill"/></svg>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button aria-label="delete" id="del" disabled name="del" class="sm-txt btn btn-outline-danger mx-1" onClick="DelDBox('Delete'); return false;">
|
<button aria-label="delete" id="del" disabled name="del" class="sm-txt btn btn-outline-danger mx-1" onClick="DelDBox('Delete'); return false;">
|
||||||
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#trash-fill"/></svg>
|
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#trash-fill"/></svg>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</button>
|
</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="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>
|
<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="d-flex col col-auto justify-content-end">
|
||||||
<div class="btn-group">
|
<div class="btn-group" role="group" aria-label="Size radio button group">
|
||||||
{% if OPT.size == 64 %}
|
<input type="radio" class="btn-check" name="size" id="size-xs" onCLick="changeSize()" autocomplete="off" value="64">
|
||||||
{% set bt="btn-info text-white" %}
|
<label class="btn btn-outline-info btn-radio" for="size-xs">XS</label>
|
||||||
{% else %}
|
|
||||||
{% set bt="btn-outline-info" %}
|
<input type="radio" class="btn-check" name="size" id="size-s" onCLick="changeSize()" autocomplete="off" value="96">
|
||||||
{% endif %}
|
<label class="btn btn-outline-info btn-radio" for="size-s">S</label>
|
||||||
<button aria-label="extra small" id="64" class="px-2 sm-txt sz-but btn {{bt}}" onClick="$('#size').val(64)">XS</button>
|
|
||||||
{% if OPT.size == 96 %}
|
<input type="radio" class="btn-check" name="size" id="size-m" onCLick="changeSize()" autocomplete="off" value="128">
|
||||||
{% set bt="btn-info text-white" %}
|
<label class="btn btn-outline-info btn-radio" for="size-m">M</label>
|
||||||
{% else %}
|
|
||||||
{% set bt="btn-outline-info" %}
|
<input type="radio" class="btn-check" name="size" id="size-l" onCLick="changeSize()" autocomplete="off" value="192">
|
||||||
{% endif %}
|
<label class="btn btn-outline-info btn-radio" for="size-l">L</label>
|
||||||
<button aria-label="small" id="96" class="px-2 sm-txt sz-but btn {{bt}}" onClick="$('#size').val(96)">S</button>
|
|
||||||
{% if OPT.size == 128 %}
|
<input type="radio" class="btn-check" name="size" id="size-xl" onCLick="changeSize()" autocomplete="off" value="256">
|
||||||
{% set bt="btn-info text-white" %}
|
<label class="btn btn-outline-info btn-radio" for="size-xl">XL</label>
|
||||||
{% else %}
|
|
||||||
{% set bt="btn-outline-info" %}
|
|
||||||
{% endif %}
|
|
||||||
<button aria-label="medium" id="128" class="px-2 sm-txt sz-but btn {{bt}}" onClick="$('#size').val(128)">M</button>
|
|
||||||
{% if OPT.size == 192 %}
|
|
||||||
{% set bt="btn-info text-white" %}
|
|
||||||
{% else %}
|
|
||||||
{% set bt="btn-outline-info" %}
|
|
||||||
{% endif %}
|
|
||||||
<button aria-label="large" id="192" class="px-2 sm-txt sz-but btn {{bt}}" onClick="$('#size').val(192)">L</button>
|
|
||||||
{% if OPT.size == 256 %}
|
|
||||||
{% set bt="btn-info text-white" %}
|
|
||||||
{% else %}
|
|
||||||
{% set bt="btn-outline-info" %}
|
|
||||||
{% 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">
|
|
||||||
<input id="offset" type="hidden" name="offset" value="{{OPT.offset}}">
|
|
||||||
<input id="size" type="hidden" name="size" value="{{OPT.size}}">
|
|
||||||
</div class="form-row">
|
|
||||||
{% set eids=namespace( str="" ) %}
|
|
||||||
{# gather all the file eids and collect them in case we go gallery mode #}
|
|
||||||
{% for obj in entry_data %}
|
|
||||||
{% if obj.type.name != "Directory" %}
|
|
||||||
{% set eids.str = eids.str + obj.id|string +"," %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
<input name="eids" id="eids" type="hidden" value="{{eids.str}}">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% set ecnt=namespace( val=0 ) %}
|
|
||||||
<div 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>
|
</div>
|
||||||
{% endif %}
|
</div class="d-flex col">
|
||||||
{% endif %}
|
</div class="d-flex row mb-2">
|
||||||
{% if not OPT.folders and obj.type.name == "Directory" %}
|
</div container="fluid">
|
||||||
{% continue %}
|
<div id="figures" class="row ms-2">
|
||||||
{% 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 %}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<form id="nav_form" method="POST" action="/change_file_opts">
|
<div class="row">
|
||||||
<input type="hidden" name="cwd" id="cwd" value="{{OPT.cwd}}">
|
<div class="col my-auto d-flex justify-content-center">
|
||||||
<div class="row">
|
<button aria-label="prev" id="prev" name="prev" class="prev sm-txt btn btn-outline-secondary disabled" onClick="prevPage(getPageFigures)" disabled>
|
||||||
<div class="col my-auto d-flex justify-content-center">
|
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#prev"/></svg>
|
||||||
<button aria-label="prev" id="prev" name="prev" class="prev sm-txt btn btn-outline-secondary">
|
</button>
|
||||||
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#prev"/></svg>
|
<span class="how_many_text sm-txt my-auto"> {{OPT.how_many}} files </span>
|
||||||
</button>
|
<button aria-label="next" id="next" name="next" class="next sm-txt btn btn-outline-secondary" onClick="nextPage(getPageFigures)">
|
||||||
<span class="sm-txt my-auto"> {{OPT.how_many}} files </span>
|
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#next"/></svg>
|
||||||
<button aria-label="next" id="next" {{nxt_disabled}} name="next" class="next sm-txt btn btn-outline-secondary">
|
</button>
|
||||||
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#next"/></svg>
|
</div class="col my-auto">
|
||||||
</button>
|
</div class="row">
|
||||||
</div>
|
</div class="container-fluid">
|
||||||
|
</div id="files_div">
|
||||||
|
<div id="viewer_div" class="d-none">
|
||||||
|
<div id="viewer" class="container-fluid">
|
||||||
|
<div class="row flex-nowrap">
|
||||||
|
<!-- Left Buttons Column -->
|
||||||
|
<div class="col-auto d-flex flex-column min-width-0">
|
||||||
|
<!-- Up Button (Small) -->
|
||||||
|
<button title="Back to list" class="btn btn-outline-info btn-sm p-1 mb-1" onclick="goOutOfViewer()">
|
||||||
|
<svg width="16" height="16" fill="currentColor">
|
||||||
|
<use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#back"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<!-- Left Button (large/flex-grow-1) -->
|
||||||
|
<button title="Show previous image" class="btn btn-outline-info px-2 flex-grow-1 overflow-hidden"
|
||||||
|
style="padding: 10%" id="la" onClick="prevImageInViewer()">
|
||||||
|
<svg width="16" height="16" fill="currentColor">
|
||||||
|
<use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#prev"/></svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<figure style="position: relative;" class="col col-auto border border-info rounded m-0 p-1" id="figure">
|
||||||
</div class="container">
|
<canvas id="canvas"></canvas>
|
||||||
|
<!-- next 4 are placeholders and called on during amendments only in viewer code -->
|
||||||
|
<img id="throbber" src="{{url_for('internal', filename='throbber.gif')}}?v={{js_vers[th]}}" style="display:none;">
|
||||||
|
<img id="white-circle" src="{{url_for('internal', filename='white-circle.png')}}?v={{js_vers[th]}}" style="display:none;">
|
||||||
|
<img id="inside-img" style="display:none;">
|
||||||
|
<svg id="inside-icon" style="display:none;" fill="currentColor">
|
||||||
|
<use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#flip_v">
|
||||||
|
</use></svg>
|
||||||
|
<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>
|
||||||
|
<!-- Right-hand Buttons Column -->
|
||||||
|
<div class="col-auto d-flex flex-column min-width-0">
|
||||||
|
<!-- Up Button (Small) -->
|
||||||
|
<button title="Back to list" class="btn btn-outline-info btn-sm p-1 mb-1" onclick="goOutOfViewer()">
|
||||||
|
<svg width="16" height="16" fill="currentColor">
|
||||||
|
<use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#back"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<!-- Right Button (large/flex-grow-1) -->
|
||||||
|
<button title="Show next image" class="btn btn-outline-info px-2 flex-grow-1 overflow-hidden"
|
||||||
|
style="padding: 10%" id="ra" onClick="nextImageInViewer()">
|
||||||
|
<svg width="16" height="16" fill="currentColor">
|
||||||
|
<use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#next"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div class="row">
|
||||||
|
<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')}}?v={{js_vers['ic']}}#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')}}?v={{js_vers['r90']}}" width="32" height="32" onMouseOver="this.src='{{url_for('internal', filename='rot90-invert.png')}}'"
|
||||||
|
onMouseOut="this.src='{{url_for('internal', filename='rot90.png')}}?v={{js_vers['r90']}}'" />
|
||||||
|
</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')}}?v={{js_vers['r180']}}" width="32" height="32" onMouseOver="this.src='{{url_for('internal', filename='rot180-invert.png')}}'"
|
||||||
|
onMouseOut="this.src='{{url_for('internal', filename='rot180.png')}}?v={{js_vers['r180']}}'" />
|
||||||
|
</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')}}?v={{js_vers['r270']}}" width="32" height="32" onMouseOver="this.src='{{url_for('internal', filename='rot270-invert.png')}}'"
|
||||||
|
onMouseOut="this.src='{{url_for('internal', filename='rot270.png')}}?v={{js_vers['r270']}}'" />
|
||||||
|
</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')}}?v={{js_vers['ic']}}#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')}}?v={{js_vers['ic']}}#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')}}?v={{js_vers['ic']}}#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')}}?v={{js_vers['ic']}}#log"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-info p-1" title="View Original" onClick="window.location='/'+document.viewing.FullPathOnFS">
|
||||||
|
<svg width="32" height="32" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#download"/></svg>
|
||||||
|
</button>
|
||||||
|
<button id="viewer_del" class="btn btn-outline-danger p-1" title="Delete (hotkey: Del)" onClick="DelDBox('Delete')">
|
||||||
|
<svg id="viewer_bin" width="32" height="32" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#trash"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div class="row">
|
||||||
|
</div id="viewer">
|
||||||
|
</div id="viewer_div">
|
||||||
{% endblock main_content %}
|
{% endblock main_content %}
|
||||||
|
|
||||||
{% block script_content %}
|
{% block script_content %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// GLOBALS
|
||||||
|
document.fake_shift=0
|
||||||
|
document.fake_ctrl=0
|
||||||
|
|
||||||
$('.figure').click( function(e) { DoSel(e, this ); SetButtonState(); return false; });
|
// FIXME: used by viewer code - should probably get rid of this?
|
||||||
$(document).on('click', function(e) { $('.highlight').removeClass('highlight') ; SetButtonState() });
|
var fullscreen=false;
|
||||||
|
|
||||||
function CallViewRouteWrapper()
|
// this is the current entry (object) we are viewing - an image/video (used when we dbl-click to view & then in next/prev in view)
|
||||||
{
|
document.viewing=null;
|
||||||
CallViewRoute( $(this).attr("id") )
|
|
||||||
}
|
|
||||||
|
|
||||||
function CallViewRoute(id)
|
var OPT = {{ OPT.to_dict()|tojson }};
|
||||||
{
|
// set from query data and stored in OPT for convenience. It can be 0 -
|
||||||
s='<form id="_fm" method="POST" action="/view/' + id + '">'
|
// this implies no content in the Path at all
|
||||||
s+='<input type="hidden" name="eids" value="'+$("#eids").val() + '">'
|
OPT.root_eid = {{ query_data.root_eid }};
|
||||||
s+='<input type="hidden" name="cwd" value="{{OPT.cwd}}">'
|
|
||||||
s+='<input type="hidden" name="root" value="{{OPT.root}}">'
|
|
||||||
s+='<input type="hidden" name="offset" value="{{OPT.offset}}">'
|
|
||||||
s+='<input type="hidden" name="how_many" value="{{OPT.how_many}}">'
|
|
||||||
s+='<input type="hidden" name="orig_url" value="{{request.path}}">'
|
|
||||||
s+='<input type="hidden" name="view_eid" value="'+id+'">'
|
|
||||||
{% if search_term is defined %}
|
|
||||||
s+='<input type="hidden" name="search_term" value="{{search_term}}">'
|
|
||||||
{% endif %}
|
|
||||||
s+='</form>'
|
|
||||||
$(s).appendTo('body').submit();
|
|
||||||
}
|
|
||||||
|
|
||||||
$('.figure').dblclick( CallViewRouteWrapper )
|
// amendment types are stable per code release, store them once and use as
|
||||||
|
// needed when we amend entrys in Transforms, removes, etc.
|
||||||
|
document.amendTypes = {{ query_data.amendTypes|tojson }};
|
||||||
|
|
||||||
// different context menu on files
|
// get items out of query_data into convenience javascript vars...
|
||||||
$.contextMenu({
|
var move_paths = {{ query_data.move_paths|tojson }};
|
||||||
selector: '.entry',
|
var NMO={{query_data.NMO|tojson}}
|
||||||
itemClickEvent: "click",
|
var people={{query_data.people|tojson}}
|
||||||
build: function($triggerElement, e) {
|
|
||||||
// when right-clicking & no selection add one OR deal with ctrl/shift right-lick as it always changes seln
|
|
||||||
if( NoSel() || e.ctrlKey || e.shiftKey )
|
|
||||||
{
|
|
||||||
DoSel(e, e.currentTarget )
|
|
||||||
SetButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
if( FiguresOrDirsOrBoth() == "figure" )
|
// this is the list of entry ids for the images for ALL matches for this query
|
||||||
{
|
var entryList={{query_data.entry_list}}
|
||||||
item_list = {
|
|
||||||
details: { name: "Details..." },
|
|
||||||
view: { name: "View File" },
|
|
||||||
sep: "---",
|
|
||||||
}
|
|
||||||
if( e.currentTarget.getAttribute('type') == 'Image' )
|
|
||||||
{
|
|
||||||
item_list['transform'] = {
|
|
||||||
name: "Transform",
|
|
||||||
items: {
|
|
||||||
"r90": { "name" : "Rotate 90 degrees" },
|
|
||||||
"r180": { "name" : "Rotate 180 degrees" },
|
|
||||||
"r270": { "name" : "Rotate 270 degrees" },
|
|
||||||
"fliph": { "name" : "Flip horizontally" },
|
|
||||||
"flipv": { "name" : "Flip vertically" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
// pageList is just those entries shown on this page from the full entryList
|
||||||
item_list['move'] = { name: "Move selected file(s) to new folder" }
|
var pageList=[]
|
||||||
item_list['sep2'] = { sep: "---" }
|
// force pageList to set pageList for & render the first page
|
||||||
}
|
getPage(1,getPageFigures)
|
||||||
else
|
|
||||||
item_list = {
|
|
||||||
move: { name: "Move selection(s) to new folder" }
|
|
||||||
}
|
|
||||||
|
|
||||||
item_list['ai'] = {
|
|
||||||
name: "Scan file for faces",
|
|
||||||
items: {
|
|
||||||
"ai-all": {"name": "all"},
|
|
||||||
{% for p in people %}
|
|
||||||
"ai-{{p.tag}}": {"name": "{{p.tag}}"},
|
|
||||||
{% endfor %}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if( SelContainsBinAndNotBin() ) {
|
|
||||||
item_list['both']= { name: 'Cannot delete and restore at same time', disabled: true }
|
|
||||||
} else {
|
|
||||||
if (e.currentTarget.getAttribute('path_type') == 'Bin' )
|
|
||||||
item_list['undel']= { name: "Restore selected file(s)" }
|
|
||||||
else if( e.currentTarget.getAttribute('type') != 'Directory' )
|
|
||||||
item_list['del']= { name: "Delete Selected file(s)" }
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
// FIXME: doco, but also gather all globals together, many make them all document. to be obviously global (and add fullscreen)
|
||||||
callback: function( key, options) {
|
var gap=0.8
|
||||||
if( key == "details" ) { DetailsDBox() }
|
var grayscale=0
|
||||||
if( key == "view" ) { CallViewRoute( $(this).attr('id') ) }
|
var throbber=0
|
||||||
if( key == "move" ) { MoveDBox(move_paths, "{{url_for('internal', filename='icons.svg')}}") }
|
|
||||||
if( key == "del" ) { DelDBox('Delete') }
|
|
||||||
if( key == "undel") { DelDBox('Restore') }
|
|
||||||
if( key == "r90" ) { Transform(90) }
|
|
||||||
if( key == "r180" ) { Transform(180) }
|
|
||||||
if( key == "r270" ) { Transform(270) }
|
|
||||||
if( key == "fliph" ) { Transform("fliph") }
|
|
||||||
if( key == "flipv" ) { Transform("flipv") }
|
|
||||||
if( key.startsWith("ai")) { RunAIOnSeln(key) }
|
|
||||||
// dont flow this event through the dom
|
|
||||||
e.stopPropagation()
|
|
||||||
},
|
|
||||||
items: item_list
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
function PrettyFname(fname)
|
||||||
$(document).ready(function() {
|
|
||||||
if( {{OPT.offset}} == 0 )
|
|
||||||
{
|
{
|
||||||
$('.prev').addClass('disabled')
|
s='<span class="alert alert-secondary py-2">'
|
||||||
$('.prev').prop('disabled', true)
|
if( fname.indexOf( "static/Import" ) == 0 )
|
||||||
|
{
|
||||||
|
s+='<svg width="20" height="20" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#import"/></svg>'
|
||||||
|
tmp_path=fname.replace("statuc/Import","" )
|
||||||
|
}
|
||||||
|
if( fname.indexOf( "static/Storage" ) == 0 )
|
||||||
|
{
|
||||||
|
s+='<svg width="20" height="20" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#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')}}?v={{js_vers['ic']}}#trash-fill"/></svg>'
|
||||||
|
tmp_path=fname.replace("static/Bin","" )
|
||||||
|
}
|
||||||
|
s+=tmp_path+'</span>'
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
$(".dir").click( function(e) { $('#offset').val(0) ; $('#cwd').val( $(this).attr('dir') ) ; $('#main_form').submit() } )
|
|
||||||
} )
|
|
||||||
|
|
||||||
$( document ).keydown(function(event) {
|
// check the size radiobutton
|
||||||
switch (event.key)
|
$(`input[name="size"][value="${OPT.size}"]`).prop('checked', true)
|
||||||
|
|
||||||
|
window.addEventListener('resize', DrawImg, false);
|
||||||
|
window.addEventListener('resize', ResizeVideo, false);
|
||||||
|
|
||||||
|
// when we are in recycle bin, change colours to green & func to restore
|
||||||
|
if( window.location.href.includes('files_rbp') )
|
||||||
{
|
{
|
||||||
case "Delete":
|
$('#viewer_bin').attr('fill', 'var(--bs-success)')
|
||||||
{% if "files_rbp" in request.url %}
|
// fill with bg-success colour
|
||||||
if( ! NoSel() ) DelDBox('Restore');
|
$('#viewer_bin use').attr('fill', 'var(--bs-success)')
|
||||||
{% else %}
|
$('#viewer_del').removeClass('btn-outline-danger').addClass('btn-outline-success')
|
||||||
if( ! NoSel() ) DelDBox('Delete');
|
$('#viewer_del').on('mouseenter', function() {
|
||||||
{% endif %}
|
// Set the SVG fill to white
|
||||||
break;
|
$('#viewer_bin use').attr('fill', 'white');
|
||||||
} })
|
});
|
||||||
|
|
||||||
function isMobile() {
|
// When mouse leaves the button
|
||||||
try{ document.createEvent("TouchEvent"); return true; }
|
$('#viewer_del').on('mouseleave', function() {
|
||||||
catch(e){ return false; }
|
// Revert the SVG fill to the bg-success colour
|
||||||
}
|
$('#viewer_bin use').attr('fill', 'var(--bs-success)');
|
||||||
|
});
|
||||||
if( isMobile() )
|
$('#viewer_del').on('click', function() { DelDBox('Restore') } )
|
||||||
{
|
}
|
||||||
$('#shift-key').css('visibility', 'visible');
|
|
||||||
$('#ctrl-key').css('visibility', 'visible');
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock script_content %}
|
{% endblock script_content %}
|
||||||
|
|||||||
@@ -34,9 +34,7 @@
|
|||||||
<!-- browsers can put the fakepath in for security, remove it -->
|
<!-- browsers can put the fakepath in for security, remove it -->
|
||||||
function DoMagic() {
|
function DoMagic() {
|
||||||
str=$("#new_file_chooser").val()
|
str=$("#new_file_chooser").val()
|
||||||
console.log(str)
|
|
||||||
str=str.replace('C:\\fakepath\\', '' )
|
str=str.replace('C:\\fakepath\\', '' )
|
||||||
console.log(str)
|
|
||||||
$("#fname").val(str)
|
$("#fname").val(str)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -56,44 +56,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div class="col-7">
|
</div class="col-7">
|
||||||
|
|
||||||
<div class="row pt-5">
|
|
||||||
<alert class="alert alert-warning">The following values are based on the defaults above and subsequent changes as you navigate the application and are not set by hand. The following content is for checking/debugging only.</alert>
|
|
||||||
</div class="row">
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<table id="pa_user_state_tbl" class="table table-striped table-sm" data-toolbar="#toolbar" data-search="true">
|
|
||||||
<thead>
|
|
||||||
<tr class="table-primary"><th>Path</th><th>New or Oldest</th><th>How Many</th><th>Folders?</th><th>Group by</th><th>Thumb size</th><th>DB retrieve offset</th><th>Root</th><th>cwd</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for st in states %}
|
|
||||||
<tr>
|
|
||||||
<td>{{st.path_type}}
|
|
||||||
{% if st.path_type == 'Search' %}
|
|
||||||
"{{st.orig_search_term}}"
|
|
||||||
{% endif %}
|
|
||||||
{% if st.path_type == 'View' %}
|
|
||||||
(orig: id={{st.view_eid}} in {{st.orig_ptype}})
|
|
||||||
{% if st.orig_ptype == 'Search' %}
|
|
||||||
"{{st.orig_search_term}}"
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</td>
|
|
||||||
<td>{{st.noo}}</td>
|
|
||||||
<td>{{st.how_many}}</td>
|
|
||||||
<td>{{st.folders}}</td>
|
|
||||||
<td>{{st.grouping}}</td>
|
|
||||||
<td>{{st.size}}</td>
|
|
||||||
<td>{{st.st_offset}}</td>
|
|
||||||
<td>{{st.root}}</td>
|
|
||||||
<td>{{st.cwd}}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div class="row">
|
|
||||||
</div class="container-fluid">
|
</div class="container-fluid">
|
||||||
{% endblock main_content %}
|
{% endblock main_content %}
|
||||||
{% block script_content %}
|
{% block script_content %}
|
||||||
|
|||||||
@@ -1,332 +0,0 @@
|
|||||||
{% extends "base.html" %} {% block main_content %}
|
|
||||||
{# make the form-switch / toggle info color set, give or take #}
|
|
||||||
<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 current={{current}}
|
|
||||||
var eids="{{eids}}"
|
|
||||||
var eid_lst=eids.split(",")
|
|
||||||
var offset={{OPT.offset}}
|
|
||||||
var first_eid={{OPT.first_eid}}
|
|
||||||
var last_eid={{OPT.last_eid}}
|
|
||||||
var imp_path="static/Import/{{imp_path}}"
|
|
||||||
var st_path="static/Storage/{{st_path}}"
|
|
||||||
var bin_path="static/Bin/{{bin_path}}"
|
|
||||||
|
|
||||||
{% for id in objs %}
|
|
||||||
e=new Object()
|
|
||||||
e.url = "{{objs[id].FullPathOnFS()|safe}}"
|
|
||||||
e.type = "{{objs[id].type.name}}"
|
|
||||||
{% if objs[id].file_details.faces %}
|
|
||||||
e.face_model="{{objs[id].file_details.faces[0].facefile_lnk.model_used}}"
|
|
||||||
{% endif %}
|
|
||||||
e.faces=[]
|
|
||||||
{% for face in objs[id].file_details.faces %}
|
|
||||||
data = { 'id': '{{face.id}}', 'x': '{{face.face_left}}', 'y': '{{face.face_top}}', 'w': '{{face.w}}', 'h':'{{face.h}}' }
|
|
||||||
{% if face.refimg %}
|
|
||||||
data['pid']='{{face.refimg.person.id}}'
|
|
||||||
data['who']='{{face.refimg.person.tag}}'
|
|
||||||
data['distance']="{{face.refimg_lnk.face_distance|round(2)}}"
|
|
||||||
{% endif %}
|
|
||||||
{% if face.no_match_override %}
|
|
||||||
data['override'] = {
|
|
||||||
'face_id' : '{{face.no_match_override.face_id}}',
|
|
||||||
'type_id' : '{{face.no_match_override.type.id}}',
|
|
||||||
'type_name': '{{face.no_match_override.type.name}}',
|
|
||||||
'who' : '{{face.no_match_override.type.name}}',
|
|
||||||
'distance' : 'N/A'
|
|
||||||
}
|
|
||||||
{% endif %}
|
|
||||||
{% if face.manual_override %}
|
|
||||||
data['override'] = {
|
|
||||||
'face_id' : '{{face.manual_override.face_id}}',
|
|
||||||
'type_id' : '{{face.manual_override.type.id}}',
|
|
||||||
'type_name': '{{face.manual_override.type.name}}',
|
|
||||||
'who' : '{{face.manual_override.person.tag}}',
|
|
||||||
'distance' : 'N/A'
|
|
||||||
}
|
|
||||||
{% endif %}
|
|
||||||
e.faces.push( data )
|
|
||||||
{% endfor %}
|
|
||||||
objs[{{id}}]=e
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% for el in NMO_data %}
|
|
||||||
NMO[{{el.id}}] = { 'type_id': {{el.id}}, 'name': '{{el.name}}' }
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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 CallViewListRoute(dir)
|
|
||||||
{
|
|
||||||
// dont allow mad spamming of arrows
|
|
||||||
$("#la").prop("disabled", true)
|
|
||||||
$("#ra").prop("disabled", true)
|
|
||||||
data="eids="+$("#eids").val()
|
|
||||||
data+="&cwd={{OPT.cwd}}"
|
|
||||||
data+="&root={{OPT.root}}"
|
|
||||||
data+="&orig_url={{OPT.orig_url}}"
|
|
||||||
data+="&view_eid={{OPT.view_eid}}"
|
|
||||||
// direction (next/prev)
|
|
||||||
data+="&"+dir+ "=1"
|
|
||||||
{% if search_term is defined %}
|
|
||||||
data+="&search_term={{search_term}}"
|
|
||||||
{% endif %}
|
|
||||||
$.ajax({ type: 'POST', data: data, url: '/view_list', success: function(res){
|
|
||||||
current=res.current
|
|
||||||
eids=res.eids
|
|
||||||
objs=res.objs
|
|
||||||
eid_lst=eids.split(",")
|
|
||||||
offset=res.offset
|
|
||||||
// okay, we now have results back, can reset next/prev buttons
|
|
||||||
if( current != first_eid )
|
|
||||||
$("#la").prop("disabled", false)
|
|
||||||
if( current != last_eid )
|
|
||||||
$("#ra").prop("disabled", false)
|
|
||||||
ViewImageOrVideo()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div id="viewer" class="container-fluid">
|
|
||||||
|
|
||||||
{% set max=eids.split(',')|length %}
|
|
||||||
<input type="hidden" name="eids" value={{eids}}>
|
|
||||||
<div class="row">
|
|
||||||
<button title="Show previous image" class="col-auto btn btn-outline-info px-2" style="padding: 10%" id="la"
|
|
||||||
{% if OPT.first_eid == current %}
|
|
||||||
disabled
|
|
||||||
{% endif %}
|
|
||||||
onClick="
|
|
||||||
cidx = eid_lst.indexOf(current.toString())
|
|
||||||
prev=cidx-1
|
|
||||||
if( prev < 0 )
|
|
||||||
{
|
|
||||||
if( offset )
|
|
||||||
{
|
|
||||||
CallViewListRoute('prev')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$('#la').attr('disabled', true )
|
|
||||||
prev=0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$('#ra').attr('disabled', false )
|
|
||||||
current=eid_lst[prev]
|
|
||||||
ViewImageOrVideo()
|
|
||||||
if( current == first_eid )
|
|
||||||
$('#la').attr('disabled', true )
|
|
||||||
">
|
|
||||||
<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
|
|
||||||
im.src="../" + objs[current].url
|
|
||||||
var context = canvas.getContext('2d')
|
|
||||||
window.addEventListener('resize', DrawImg, false);
|
|
||||||
</script>
|
|
||||||
<figcaption id="img-cap" class="figure-caption text-center text-wrap text-break"><span id="fname_i"></span></figcaption>
|
|
||||||
</figure>
|
|
||||||
<script>$('#fname_i').html(PrettyFname(objs[current].url))</script>
|
|
||||||
{% if objs[current].type.name != "Image" %}
|
|
||||||
<script>$('#figure').hide()</script>
|
|
||||||
{% endif %}
|
|
||||||
<div id="video_div" class="col col-auto">
|
|
||||||
<video id="video" class="col col-auto" controls>
|
|
||||||
<source src="../{{objs[current].FullPathOnFS()}}" 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>
|
|
||||||
<script>$('#fname_v').html(PrettyFname(objs[current].url))</script>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
window.addEventListener('resize', ResizeVideo, false);
|
|
||||||
ResizeVideo()
|
|
||||||
{% if objs[current].type.name != "Video" %}
|
|
||||||
$('#video_div').hide()
|
|
||||||
{% endif %}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button title="Show next image" class="col-auto btn btn-outline-info px-2" style="padding: 10%" id="ra"
|
|
||||||
{% if OPT.last_eid == current %}
|
|
||||||
disabled
|
|
||||||
{% endif %}
|
|
||||||
onClick="
|
|
||||||
cidx = eid_lst.indexOf(current.toString())
|
|
||||||
if( cidx < eid_lst.length-1 )
|
|
||||||
{
|
|
||||||
current=eid_lst[cidx+1]
|
|
||||||
ViewImageOrVideo()
|
|
||||||
if( current != first_eid )
|
|
||||||
$('#la').attr('disabled', false )
|
|
||||||
}
|
|
||||||
else
|
|
||||||
CallViewListRoute('next')
|
|
||||||
|
|
||||||
if( current == last_eid )
|
|
||||||
{
|
|
||||||
$('#ra').attr('disabled', true )
|
|
||||||
return
|
|
||||||
}
|
|
||||||
">
|
|
||||||
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#next"/></svg>
|
|
||||||
</button>
|
|
||||||
</div id="/form-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" disabled style="visibility:hidden">
|
|
||||||
<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">
|
|
||||||
{% endblock main_content %}
|
|
||||||
{% block script_content %}
|
|
||||||
<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 script_content %}
|
|
||||||
Reference in New Issue
Block a user