diff --git a/TODO b/TODO
index c05f3f2..a073a5f 100644
--- a/TODO
+++ b/TODO
@@ -1,15 +1,20 @@
###
-# get override data into view
-# should start with an empty DB and test
+#
+#1 get override data into view
+# also all the add ref img/add override, etc are non-functional - FIX the override* stuff first to get table/naming consistency as that is half the problem
+# NMO data -> there is an NMO object (just NMO names/types - |json), then there is per face level data - this should be a reference from Face and Schema/marshmallow
+#
+#2 should start with an empty DB and test
# definitely no dirs in storage_sp I now pass root_eid=0 for this
# BUT - need GUI to work - may even be good to put an alert up - its so odd to have not root dir ONLY happens when no data
-# empty directories (2017/20171015-test/...) showing "No matches for: 'undefined'" <- should only comes up for search in URL???
-# think I killed pa_job_manager without passing an eid to a transform job, shouldn't crash
+#3 empty directories (2017/20171015-test/...) showing "No matches for: 'undefined'" <- should only comes up for search in URL???
+#
+#4 TEST everything (don't forget keybindings,e.g. delete)
+# -- go into viewer code from a files_rbp - had red bin, bot green on viewer.
+#
+#5 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...
-# also all the add ref img/add override, etc are non-functional - FIX the override* stuff first to get table/naming consistency as that is half the problem
-# convert move_paths to a json setup
-# ALSO revisit this move_paths to be as safe as possible ultimately, triple-check there are no leading / or .. 's
-# TEST everything (don't forget keybindings,e.g. delete)
+#
###
### major fix - go to everywhere I call GetEntries(), and redo the logic totally...
diff --git a/files.py b/files.py
index 2b11d87..646c907 100644
--- a/files.py
+++ b/files.py
@@ -2,7 +2,7 @@ from flask_wtf import FlaskForm
from flask import request, render_template, redirect, send_from_directory, url_for, jsonify, make_response
from marshmallow import Schema, fields
from main import db, app, ma
-from sqlalchemy import Sequence, text, select, union
+from sqlalchemy import Sequence, text, select, union, or_
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import joinedload
import os
@@ -28,10 +28,10 @@ from types import SimpleNamespace
from states import States, PA_UserState
from query import Query
from job import Job, JobExtra, Joblog, NewJob, SetFELog
-from path import PathType, Path, MovePathDetails
+from path import PathType, Path
from person import Refimg, Person, PersonRefimgLink
from settings import Settings, SettingsIPath, SettingsSPath, SettingsRBPath
-from shared import SymlinkName
+from shared import SymlinkName, ICON
from dups import Duplicates
from face import Face, FaceFileLink, FaceRefimgLink, FaceOverrideType, FaceNoMatchOverride, FaceForceMatchOverride
@@ -170,6 +170,14 @@ class PathSchema(ma.SQLAlchemyAutoSchema):
class Meta: model = Path
load_instance = True
type = ma.Nested(PathType)
+ root_dir = fields.Method("get_root_dir")
+ icon_url = fields.Method("get_icon_url")
+ def get_icon_url(self, obj):
+ return url_for("internal", filename="icons.svg") + "#" + ICON[obj.type.name]
+ def get_root_dir(self, obj):
+ parts = obj.path_prefix.split('/')
+ return ''.join(parts[2:])
+
class FileTypeSchema(ma.SQLAlchemyAutoSchema):
class Meta: model = FileType
@@ -216,6 +224,11 @@ class FileSchema(ma.SQLAlchemyAutoSchema):
load_instance = True
faces = ma.Nested(FaceSchema,many=True,allow_none=True)
+# used just in NMO var
+class FaceOverrideTypeSchema(ma.SQLAlchemyAutoSchema):
+ class Meta: model = FaceOverrideType
+ load_instance = True
+
################################################################################
# Schema for Entry so we can json for data to the client
################################################################################
@@ -238,6 +251,9 @@ class EntrySchema(ma.SQLAlchemyAutoSchema):
# global - this will be use more than once below, so do it once for efficiency
entries_schema = EntrySchema(many=True)
+FOT_Schema = FaceOverrideTypeSchema(many=True)
+path_Schema = PathSchema(many=True)
+person_Schema = PersonSchema(many=True)
################################################################################
# util function to just update the current/first/last positions needed for
@@ -314,6 +330,25 @@ def get_dir_entries():
entries = Entry.query.filter(Entry.id.in_(ids)).all()
return jsonify(entries_schema.dump(entries))
+# get Face overrid details
+def getFOT():
+ stmt = select(FaceOverrideType)
+ fot=db.session.execute(stmt).scalars().all()
+ return FOT_Schema.dump(fot)
+
+
+# get import/storage path details for move dbox
+def getMoveDetails():
+ stmt = select(Path).where( or_( Path.type.has(name="Import"), Path.type.has(name="Storage")))
+ mp=db.session.execute(stmt).scalars().all()
+ return path_Schema.dump(mp)
+
+# get people data for the menu for AI matching (of person.tag)
+def getPeople():
+ stmt = select(Person)
+ people=db.session.execute(stmt).scalars().all()
+ return person_Schema.dump(people)
+
################################################################################
# Get all relevant Entry.ids based on search_term passed in and OPT visuals
@@ -322,6 +357,9 @@ def GetSearchQueryData(OPT):
query_data={}
query_data['entry_list']=None
query_data['root_eid']=0
+ query_data['NMO'] = getFOT()
+ query_data['move_paths'] = getMoveDetails()
+ query_data['people'] = getPeople()
search_term = OPT.search_term
# turn * wildcard into sql wildcard of %
@@ -352,13 +390,15 @@ def GetSearchQueryData(OPT):
query_data['entry_list']=all_entries
return query_data
-
#################################################################################
# Get all relevant Entry.ids based on files_ip/files_sp/files_rbp and OPT visuals
#################################################################################
def GetQueryData( OPT ):
query_data={}
query_data['entry_list']=None
+ query_data['NMO'] = getFOT()
+ query_data['move_paths'] = getMoveDetails()
+ query_data['people'] = getPeople()
# always get the top of the (OPT.prefix) Path's eid and keep it for OPT.folders toggling/use
dir_stmt=(
@@ -388,7 +428,6 @@ def GetQueryData( OPT ):
stmt=stmt.order_by(*order_map.get(OPT.noo) )
query_data['entry_list']=db.session.execute(stmt).scalars().all()
-
return query_data
################################################################################
@@ -428,9 +467,8 @@ def file_list_ip():
def files_ip():
OPT=States( request )
people = Person.query.all()
- move_paths = MovePathDetails()
query_data = GetQueryData( OPT )
- return render_template("files.html", page_title=f"View Files ({OPT.path_type} Path)", OPT=OPT, people=people, move_paths=move_paths, query_data=query_data )
+ return render_template("files.html", page_title=f"View Files ({OPT.path_type} Path)", OPT=OPT, people=people, query_data=query_data )
################################################################################
# /files -> show thumbnail view of files from storage_path
@@ -440,9 +478,8 @@ def files_ip():
def files_sp():
OPT=States( request )
people = Person.query.all()
- move_paths = MovePathDetails()
query_data = GetQueryData( OPT )
- return render_template("files.html", page_title=f"View Files ({OPT.path_type} Path)", OPT=OPT, people=people, move_paths=move_paths, query_data=query_data )
+ return render_template("files.html", page_title=f"View Files ({OPT.path_type} Path)", OPT=OPT, people=people, query_data=query_data )
################################################################################
@@ -453,9 +490,8 @@ def files_sp():
def files_rbp():
OPT=States( request )
people = Person.query.all()
- move_paths = MovePathDetails()
query_data = GetQueryData( OPT )
- return render_template("files.html", page_title=f"View Files ({OPT.path_type} Path)", OPT=OPT, people=people, move_paths=move_paths, query_data=query_data )
+ return render_template("files.html", page_title=f"View Files ({OPT.path_type} Path)", OPT=OPT, people=people, query_data=query_data )
################################################################################
# search -> GET version -> has search_term in the URL and is therefore able to
@@ -470,8 +506,7 @@ def search(search_term):
OPT.folders = False
query_data=GetSearchQueryData( OPT )
- move_paths = MovePathDetails()
- return render_template("files.html", page_title='View Files', search_term=search_term, query_data=query_data, OPT=OPT, move_paths=move_paths )
+ return render_template("files.html", page_title='View Files', search_term=search_term, query_data=query_data, OPT=OPT )
################################################################################
# /files/scan_ip -> allows us to force a check for new files
diff --git a/internal/js/files_support.js b/internal/js/files_support.js
index 5fb994a..a45a94d 100644
--- a/internal/js/files_support.js
+++ b/internal/js/files_support.js
@@ -109,7 +109,7 @@ function MoveOrDelCleanUpUI()
// show the DBox for a move file, includes all thumbnails of selected files to move
// and a pre-populated folder to move them into, with text field to add a suffix
-function MoveDBox(path_details, db_url)
+function MoveDBox(path_details)
{
$('#dbox-title').html('Move Selected File(s) to new directory in Storage Path')
div =`
@@ -838,3 +838,87 @@ function dblClickToViewEntry(id) {
setEntryById( id )
ViewImageOrVideo()
}
+
+
+// different context menu on files
+$.contextMenu({
+ selector: '.entry',
+ itemClickEvent: "click",
+ build: function($triggerElement, e) {
+ // when right-clicking & no selection add one OR deal with ctrl/shift right-lick as it always changes seln
+ if( NoSel() || e.ctrlKey || e.shiftKey )
+ {
+ DoSel(e, e.currentTarget )
+ SetButtonState();
+ }
+
+ if( FiguresOrDirsOrBoth() == "figure" )
+ {
+ item_list = {
+ details: { name: "Details..." },
+ view: { name: "View File" },
+ sep: "---",
+ }
+ if( e.currentTarget.getAttribute('type') == 'Image' )
+ {
+ item_list['transform'] = {
+ name: "Transform",
+ items: {
+ "r90": { "name" : "Rotate 90 degrees" },
+ "r180": { "name" : "Rotate 180 degrees" },
+ "r270": { "name" : "Rotate 270 degrees" },
+ "fliph": { "name" : "Flip horizontally" },
+ "flipv": { "name" : "Flip vertically" }
+ }
+ }
+
+ }
+ item_list['move'] = { name: "Move selected file(s) to new folder" }
+ item_list['sep2'] = { sep: "---" }
+ }
+ else
+ item_list = {
+ move: { name: "Move selection(s) to new folder" }
+ }
+
+ item_list['ai'] = {
+ name: "Scan file for faces",
+ items: {
+ "ai-all": { name: "all" }
+ }
+ };
+
+ // Dynamically add entries for each person in the `people` array
+ people.forEach(person => {
+ item_list['ai'].items[`ai-${person.tag}`] = { name: person.tag };
+ });
+
+ if( SelContainsBinAndNotBin() ) {
+ item_list['both']= { name: 'Cannot delete and restore at same time', disabled: true }
+ } else {
+ if (e.currentTarget.getAttribute('path_type') == 'Bin' )
+ item_list['undel']= { name: "Restore selected file(s)" }
+ else if( e.currentTarget.getAttribute('type') != 'Directory' )
+ item_list['del']= { name: "Delete Selected file(s)" }
+ }
+
+ return {
+ callback: function( key, options) {
+ if( key == "details" ) { DetailsDBox() }
+ if( key == "view" ) { dblClickToViewEntry( $(this).attr('id') ) }
+ if( key == "move" ) { MoveDBox(move_paths) }
+ if( key == "del" ) { DelDBox('Delete') }
+ if( key == "undel") { DelDBox('Restore') }
+ if( key == "r90" ) { Transform(90) }
+ if( key == "r180" ) { Transform(180) }
+ if( key == "r270" ) { Transform(270) }
+ if( key == "fliph" ) { Transform("fliph") }
+ if( key == "flipv" ) { Transform("flipv") }
+ if( key.startsWith("ai")) { RunAIOnSeln(key) }
+ // dont flow this event through the dom
+ e.stopPropagation()
+ },
+ items: item_list
+ };
+ }
+});
diff --git a/templates/files.html b/templates/files.html
index ab0ac71..ff1e889 100644
--- a/templates/files.html
+++ b/templates/files.html
@@ -64,7 +64,7 @@
-