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

This commit is contained in:
2025-10-05 23:20:17 +11:00
parent e0654edd21
commit e5d6ce9b73
4 changed files with 149 additions and 121 deletions

21
TODO
View File

@@ -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...

View File

@@ -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

View File

@@ -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
};
}
});

View File

@@ -64,7 +64,7 @@
<button aria-label="next" id="next" name="next" class="next sm-txt btn btn-outline-secondary" onClick="nextPage(getPageFigures)">
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#next"/></svg>
</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>
</button>
{% 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);
</script>
{% endblock script_content %}