From e5d6ce9b7317ac8f07077c4a65f30b577afcec60 Mon Sep 17 00:00:00 2001 From: Damien De Paoli Date: Sun, 5 Oct 2025 23:20:17 +1100 Subject: [PATCH] move MoveDBox into full javascript and fold it into internal/js/files_support.js, also remove unused parameter for MoveDBox, and use marshmallow to pass people in query_data - overall just cleaner more consistent code for existing functionality --- TODO | 21 +++++--- files.py | 61 ++++++++++++++++----- internal/js/files_support.js | 86 ++++++++++++++++++++++++++++- templates/files.html | 102 ++--------------------------------- 4 files changed, 149 insertions(+), 121 deletions(-) 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 @@ - {% if "files_rbp" in request.url %} @@ -237,6 +237,7 @@ // get items out of query_data into convenience javascript vars... var move_paths = {{ query_data.move_paths|tojson }}; var NMO={{query_data.NMO|tojson}} + var people={{query_data.people|tojson}} // this is the list of entry ids for the images for ALL matches for this query var entryList={{query_data.entry_list}} @@ -277,108 +278,11 @@ return s } - // 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"}, - {% 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 { - callback: function( key, options) { - if( key == "details" ) { DetailsDBox() } - if( key == "view" ) { dblClickToViewEntry( $(this).attr('id') ) } - 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 - }; - } - }); - - $( document ).keydown(function(event) { switch (event.key) - { - case "Delete": - {% if "files_rbp" in request.url %} - if( ! NoSel() ) DelDBox('Restore'); - {% else %} - if( ! NoSel() ) DelDBox('Delete'); - {% endif %} - break; - } }) - - if( isMobile() ) - { - $('#shift-key').css('visibility', 'visible'); - $('#ctrl-key').css('visibility', 'visible'); - } - // check the size radiobutton $(`input[name="size"][value="${OPT.size}"]`).prop('checked', true) window.addEventListener('resize', DrawImg, false); window.addEventListener('resize', ResizeVideo, false); + {% endblock script_content %}