Compare commits

...

7 Commits

9 changed files with 254 additions and 693 deletions

2
TODO
View File

@@ -1,5 +1,5 @@
###
# get search to work
# get override data into view
###
### major fix - go to everywhere I call GetEntries(), and redo the logic totally...

451
files.py
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
from sqlalchemy import Sequence, text, select, union
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import joinedload
import os
@@ -252,173 +252,6 @@ def UpdatePref( pref, OPT ):
pref.last_used=last_used
db.session.add(pref)
db.session.commit()
################################################################################
# GetEntriesInFlatView: func. to retrieve DB entries appropriate for flat view
################################################################################
def GetEntriesInFlatView( OPT, prefix ):
entries=[]
num_entries=0
join = "Entry.query.join(File).join(EntryDirLink).join(Dir).join(PathDirLink).join(Path).filter(Path.path_prefix==prefix)"
entries = eval( f"{join}.{OPT.order}.offset({OPT.offset}).limit({OPT.how_many}).all()" )
if OPT.first_eid == 0 and OPT.offset == 0 and len(entries):
OPT.first_eid = entries[0].id
if OPT.last_eid==0:
num_entries = eval( f"{join}.count()" )
last_entry = eval( f"{join}.{OPT.last_order}.limit(1).first()" )
if last_entry:
OPT.last_eid = last_entry.id
return entries, num_entries
################################################################################
# GetEntriesInFolderView: func. to retrieve DB entries appropriate for folder view
# read inline comments to deal with variations / ordering...
################################################################################
def GetEntriesInFolderView( OPT, prefix ):
entries=[]
num_entries=0
# okay the root cwd is fake, so treat it specially - its Dir can be found by path with dir.rel_path=''
if os.path.dirname(OPT.cwd) == 'static':
dir=Entry.query.join(Dir).join(PathDirLink).join(Path).filter(Dir.rel_path=='').filter(Path.path_prefix==prefix).order_by(Entry.name).first()
# this can occur if the path in settings does not exist as it wont be in # the DB
if not dir:
return entries, num_entries
# although this is 1 entry, needs to come back via all() to be iterable
entries+= Entry.query.filter(Entry.id==dir.id).all()
else:
rp = OPT.cwd.replace( prefix, '' )
# when in subdirs, replacing prefix will leave the first char as /, get rid of it
if len(rp) and rp[0] == '/':
rp=rp[1:]
dir=Entry.query.join(Dir).join(PathDirLink).join(Path).filter(Dir.rel_path==rp).filter(Path.path_prefix==prefix).order_by(Entry.name).first()
# this can occur if the path in settings does not exist as it wont be in # the DB
if not dir:
return entries, 0
# dirs cant be sorted by date really, so do best I can for now
if OPT.noo == "Z to A" or OPT.noo == "Newest":
entries+= Entry.query.join(EntryDirLink).join(FileType).filter(EntryDirLink.dir_eid==dir.id).filter(FileType.name=='Directory').order_by(Entry.name.desc()).all()
# just do A to Z / Oldest by default or if no valid option
else:
entries+= Entry.query.join(EntryDirLink).join(FileType).filter(EntryDirLink.dir_eid==dir.id).filter(FileType.name=='Directory').order_by(Entry.name).all()
# add any files at the current CWD (based on dir_eid in DB)
join="Entry.query.join(File).join(EntryDirLink).filter(EntryDirLink.dir_eid==dir.id)"
file_entries= eval( f"{join}.{OPT.order}.offset(OPT.offset).limit(OPT.how_many).all()")
if OPT.offset == 0 and len(file_entries):
OPT.first_eid = file_entries[0].id
num_entries = eval( f"{join}.count()" )
last_entry = eval( f"{join}.{OPT.last_order}.limit(1).first()" )
if last_entry:
OPT.last_eid = last_entry.id
entries += file_entries;
return entries, num_entries
################################################################################
# GetEntriesInSearchView: func. to retrieve DB entries appropriate for Search view
# Defaults search is for any matching filename, contents of any matching dirname
# and any match with AI / face for that term. Explicit, only AI match via
# AI:<tag> syntax
################################################################################
def GetEntriesInSearchView( OPT ):
search_term=OPT.orig_search_term
# turn * wildcard into sql wildcard of %
search_term=search_term.replace('*', '%' )
if 'AI:' in OPT.orig_search_term:
search_term = search_term.replace('AI:','')
join=f"Entry.query.join(File).join(FaceFileLink).join(Face).join(FaceRefimgLink).join(Refimg).join(PersonRefimgLink).join(Person).filter(Person.tag == search_term)"
else:
join=f"Entry.query.join(File).join(FaceFileLink).join(Face).join(FaceRefimgLink).join(Refimg).join(PersonRefimgLink).join(Person).filter(Person.tag.ilike('%{search_term}%'))"
if 'AI:' in OPT.orig_search_term:
all_entries = eval( f"{join}.{OPT.order}.offset(OPT.offset).limit(OPT.how_many).all()")
else:
file_data=eval( f"Entry.query.join(File).filter(Entry.name.ilike('%{search_term}%')).{OPT.order}.offset({OPT.offset}).limit({OPT.how_many}).all()" )
dir_data =eval( f"Entry.query.join(File).join(EntryDirLink).join(Dir).filter(Dir.rel_path.ilike('%{search_term}%')).{OPT.order}.offset({OPT.offset}).limit({OPT.how_many}).all()" )
ai_data =eval( f"{join}.{OPT.order}.offset({OPT.offset}).limit({OPT.how_many}).all()")
# remove any duplicates from combined data
all_entries = []
for f in file_data:
all_entries.append(f)
for d in dir_data:
add_it=1
for f in file_data:
if d.name == f.name:
add_it=0
break
if add_it:
all_entries.append(d)
for a in ai_data:
add_it=1
for f in file_data:
if a.name == f.name:
add_it=0
break
if add_it:
all_entries.append(a)
# nothing found, just return now
if len(all_entries) == 0:
OPT.num_entries = 0
return []
# for all searches first_entry is worked out when first_eid not set yet & offset is 0 and we have some entries
if OPT.first_eid == 0 and OPT.offset == 0 and len(all_entries):
OPT.first_eid = all_entries[0].id
if OPT.last_eid == 0:
by_fname= f"select e.id from entry e where e.name ilike '%%{search_term}%%'"
by_dirname=f"select e.id from entry e, entry_dir_link edl where edl.entry_id = e.id and edl.dir_eid in ( select d.eid from dir d where d.rel_path ilike '%%{search_term}%%' )"
by_ai =f"select e.id from entry e, face_file_link ffl, face_refimg_link frl, person_refimg_link prl, person p where e.id = ffl.file_eid and frl.face_id = ffl.face_id and frl.refimg_id = prl.refimg_id and prl.person_id = p.id and p.tag = '{search_term}'"
if 'AI:' in OPT.orig_search_term:
sel_no_order=f"select e.*, f.* from entry e, file f where e.id=f.eid and e.id in ( {by_ai} ) "
else:
sel_no_order=f"select e.*, f.* from entry e, file f where e.id=f.eid and e.id in ( {by_fname} union {by_dirname} union {by_ai} ) "
#num_entries
num_e_sql = f"select count(1) from ( {by_fname} union {by_dirname} union {by_ai} ) as foo"
with db.engine.connect() as conn:
OPT.num_entries = conn.execute( text( num_e_sql ) ).first().count
if OPT.num_entries == 0:
return []
last_entry_sql= f"{sel_no_order} order by {OPT.last_order_raw} limit 1"
with db.engine.connect() as conn:
OPT.last_eid = conn.execute( text( last_entry_sql ) ).first().id
# store first/last eid into prefs
pref=PA_UserState.query.filter(PA_UserState.pa_user_dn==current_user.dn,PA_UserState.path_type==OPT.path_type,PA_UserState.orig_ptype==OPT.orig_ptype,PA_UserState.orig_search_term==OPT.orig_search_term).first()
UpdatePref( pref, OPT )
return all_entries
################################################################################
# set up "order strings" to use in ORM and raw queries as needed for
# GetEntries*Search*, GetEntries*Flat*, GetEntries*Fold*
################################################################################
def SetOrderStrings( OPT ):
if OPT.noo == "Newest":
OPT.order="order_by(File.year.desc(),File.month.desc(),File.day.desc(),Entry.name.desc())"
OPT.last_order="order_by(File.year,File.month,File.day,Entry.name)"
OPT.last_order_raw=f"f.year, f.month, f.day, e.name"
elif OPT.noo == "Oldest":
OPT.order="order_by(File.year,File.month,File.day,Entry.name)"
OPT.last_order="order_by(File.year.desc(),File.month.desc(),File.day.desc(),Entry.name.desc())"
OPT.last_order_raw=f"f.year desc, f.month desc, f.day desc, e.name desc"
elif OPT.noo == "Z to A":
OPT.order="order_by(Entry.name.desc())"
OPT.last_order="order_by(Entry.name)"
OPT.last_order_raw=f"e.name"
else:
# A to Z
OPT.order="order_by(Entry.name)"
OPT.last_order="order_by(Entry.name.desc())"
OPT.last_order_raw=f"e.name desc"
return
################################################################################
@@ -431,9 +264,6 @@ def process_ids():
data = request.get_json() # Parse JSON body
ids = data.get('ids', []) # Extract list of ids
# DDP: debate here, do I get query_id, do I validate whether we are asking
# for ids not in the query? OR, dont even make/store/have query?
# Query DB for matching entries
stmt = (
select(Entry)
@@ -473,23 +303,63 @@ def get_dir_entries():
# get content of dir_id
stmt=( select(Entry.id).join(EntryDirLink).filter(EntryDirLink.dir_eid==dir_id) )
stmt=stmt.order_by(*order_map.get(OPT.noo) )
ids=db.session.execute(stmt).scalars().all()
entries_schema = EntrySchema(many=True)
entries = Entry.query.filter(Entry.id.in_(ids)).all()
return jsonify(entries_schema.dump(entries))
################################################################################
# Call this ONCE on first menu choice of View files, or search box submission
# create the list of entry ids that matcht the required viewing/list
# Get all relevant Entry.ids based on search_term passed in and OPT visuals
################################################################################
def GetSearchQueryData(OPT):
query_data={}
query_data['entry_list']=None
query_data['root_eid']=0
search_term = OPT.search_term
# turn * wildcard into sql wildcard of %
search_term = search_term.replace('*', '%')
if 'AI:' in search_term:
search_term = search_term.replace('AI:', '')
# AI searches are for specific ppl/joins in the DB AND we do them for ALL types of searches, define this once
ai_query = (
select(Entry.id)
.join(File).join(FaceFileLink).join(Face).join(FaceRefimgLink).join(Refimg).join(PersonRefimgLink).join(Person)
.where(Person.tag == search_term)
.order_by(*order_map.get(OPT.noo) )
)
if 'AI:' in search_term:
all_entries = db.session.execute(ai_query).scalars().all()
else:
# match name of File
file_query = select(Entry.id).join(File).where(Entry.name.ilike(f'%{search_term}%')).order_by(*order_map.get(OPT.noo))
# match name of Dir
dir_query = select(Entry.id).join(File).join(EntryDirLink).join(Dir).where(Dir.rel_path.ilike(f'%{search_term}%')).order_by(*order_map.get(OPT.noo))
# Combine ai, file & dir matches with union() to dedup and then order them
combined_query = union( file_query, dir_query, ai_query )
all_entries = db.session.execute(combined_query).scalars().all()
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
# always get the top of the (OPT.prefix) Path's eid and keep it for OPT.folders toggling/use
dir_stmt=( select(Entry.id).join(Dir).join(PathDirLink).join(Path).
filter(Dir.rel_path == '').filter(Path.path_prefix==OPT.prefix) )
dir_stmt=(
select(Entry.id)
.join(Dir).join(PathDirLink).join(Path)
.where(Dir.rel_path == '').where(Path.path_prefix==OPT.prefix)
)
# this should return the 1 Dir (that we want to see the content of) - and with only 1, no need to worry about order
dir_arr=db.session.execute(dir_stmt).scalars().all()
dir_id=dir_arr[0]
@@ -501,75 +371,28 @@ def GetQueryData( OPT ):
stmt=( select(Entry.id).join(EntryDirLink).filter(EntryDirLink.dir_eid==dir_id) )
else:
# get every File that is in the OPT.prefix Path
stmt=( select(Entry.id).join(File).join(EntryDirLink).join(Dir).join(PathDirLink).join(Path).
filter(Path.path_prefix == OPT.prefix) )
stmt=(
select(Entry.id)
.join(File).join(EntryDirLink).join(Dir).join(PathDirLink).join(Path)
.where(Path.path_prefix == OPT.prefix)
)
stmt=stmt.order_by(*order_map.get(OPT.noo) )
query_data['entry_list']=db.session.execute(stmt).scalars().all()
return query_data
################################################################################
# /GetEntries -> helper function that Gets Entries for required files to show
# for several routes (ifles_ip, files_sp, files_rbp, search, view_list)
################################################################################
def GetEntries( OPT ):
entries=[]
SetOrderStrings( OPT )
if OPT.path_type == 'Search' or (OPT.path_type == 'View' and OPT.orig_ptype=='Search'):
return GetEntriesInSearchView( OPT )
# if we are a view, then it will be of something else, e.g. a list of
# import, storage, or bin images, reset OPT.path_type so that the paths array below works
if 'View' in OPT.path_type:
eid = OPT.url[6:]
OPT.path_type= OPT.orig_ptype
paths = []
if OPT.path_type == 'Storage':
path = SettingsSPath()
elif OPT.path_type == 'Import':
path = SettingsIPath()
elif OPT.path_type == 'Bin':
path = SettingsRBPath()
num_entries=0
path_cnt=1
# if we have not set last_eid yet, then we need to 'reset' it during the
# path loop below (if we have more than one dir in (say) Import path)
if OPT.last_eid == 0 or OPT.folders:
update_last_eid = True
else:
update_last_eid = False
prefix = SymlinkName(OPT.path_type,path,path+'/')
if OPT.folders:
tmp_ents, tmp_num_ents = GetEntriesInFolderView( OPT, prefix )
else:
tmp_ents, tmp_num_ents = GetEntriesInFlatView( OPT, prefix )
entries += tmp_ents
num_entries += tmp_num_ents
if update_last_eid:
# find pref... via path_type if we are here
OPT.num_entries=num_entries
pref=PA_UserState.query.filter(PA_UserState.pa_user_dn==current_user.dn,PA_UserState.path_type==OPT.path_type).first()
UpdatePref( pref, OPT )
return entries
################################################################################
# /change_file_opts -> allow sort order, how_many per page, etc. to change, and
# then send back the new query_data to update entryList
################################################################################
@app.route("/change_file_opts", methods=["POST"])
@login_required
def change_file_opts2():
def change_file_opts():
data = request.get_json() # Parse JSON body
# allow dot-notation for OPT
OPT = SimpleNamespace(**data)
if OPT.folders == 'True':
if hasattr(OPT, 'folders') and OPT.folders == 'True':
OPT.folders=True
else:
OPT.folders=False
@@ -581,26 +404,20 @@ def change_file_opts2():
################################################################################
# /file_list -> show detailed file list of files from import_path(s)
################################################################################
@app.route("/file_list_ip", methods=["GET", "POST"])
@app.route("/file_list_ip", methods=["GET"])
@login_required
def file_list_ip():
OPT=States( request )
# now we have reset the offset, etc. into the prefs, we can use a GET and this will be back/forward browser button safe
if request.method=='POST':
redirect("/file_list_ip")
entries=GetEntries( OPT )
return render_template("file_list.html", page_title='View File Details (Import Path)', entry_data=entries, OPT=OPT )
query_data = GetQueryData( OPT )
return render_template("file_list.html", page_title='View File Details (Import Path)', query_data=query_data, OPT=OPT )
################################################################################
# /files -> show thumbnail view of files from import_path(s)
################################################################################
@app.route("/files_ip", methods=["GET", "POST"])
@app.route("/files_ip", methods=["GET"])
@login_required
def files_ip():
OPT=States( request )
# now we have reset the offset, etc. into the prefs, we can use a GET and this will be back/forward browser button safe
if request.method=='POST':
redirect("/files_ip")
people = Person.query.all()
move_paths = MovePathDetails()
query_data = GetQueryData( OPT )
@@ -609,13 +426,10 @@ def files_ip():
################################################################################
# /files -> show thumbnail view of files from storage_path
################################################################################
@app.route("/files_sp", methods=["GET", "POST"])
@app.route("/files_sp", methods=["GET"])
@login_required
def files_sp():
OPT=States( request )
# now we have reset the offset, etc. into the prefs, we can use a GET and this will be back/forward browser button safe
if request.method=='POST':
redirect("/files_sp")
people = Person.query.all()
move_paths = MovePathDetails()
query_data = GetQueryData( OPT )
@@ -625,13 +439,10 @@ def files_sp():
################################################################################
# /files -> show thumbnail view of files from recycle_bin_path
################################################################################
@app.route("/files_rbp", methods=["GET", "POST"])
@app.route("/files_rbp", methods=["GET"])
@login_required
def files_rbp():
OPT=States( request )
# now we have reset the offset, etc. into the prefs, we can use a GET and this will be back/forward browser button safe
if request.method=='POST':
redirect("/files_rbp")
people = Person.query.all()
move_paths = MovePathDetails()
query_data = GetQueryData( OPT )
@@ -645,19 +456,13 @@ def files_rbp():
@app.route("/search/<search_term>", methods=["GET", "POST"])
@login_required
def search(search_term):
# print( f"req={request}" )
OPT=States( request )
# print( f"OPT={OPT}" )
# if we posted to get here, its a change in State, so save it to pa_user_state, and go back to the GET version or URL
if request.method=="POST":
redirect("/search/"+search_term)
OPT.search_term = search_term
# always show flat results for search to start with
OPT.folders=False
entries=GetEntries( OPT )
OPT.folders = False
query_data=GetSearchQueryData( OPT )
move_paths = MovePathDetails()
return render_template("files.html", page_title='View Files', search_term=search_term, entry_data=entries, 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, move_paths=move_paths )
################################################################################
# /files/scan_ip -> allows us to force a check for new files
@@ -786,80 +591,8 @@ def move_files():
return make_response( jsonify( job_id=job.id ) )
@login_required
@app.route("/view_list", methods=["POST"])
def view_list():
OPT=States( request )
# Get next/prev set of data - e.g. if next set, then it will use orig_url
# to go forward how_many from offset and then use viewer.html to show that
# first obj of the new list of entries
entries=GetEntries( OPT )
# this occurs when we went from the last image on a page (with how_many on
# it) and it just happened to also be the last in the DB...
if not entries:
SetFELog( message="DDP: DONT think this can happen anymore", level="danger", job=None, persistent=True, cant_close=True )
# undo the skip by how_many and getentries again
OPT.offset -= int(OPT.how_many)
entries=GetEntries( OPT )
# now flag we are at the last in db, to reset current below
objs = {}
eids=""
resp={}
resp['objs']={}
for e in entries:
if not e.file_details:
continue
eids=eids+f"{e.id},"
resp['objs'][e.id]={}
resp['objs'][e.id]['url'] = e.FullPathOnFS()
resp['objs'][e.id]['name'] = e.name
resp['objs'][e.id]['type'] = e.type.name
if e.file_details.faces:
# model is used for whole file, so set it at that level (based on first face)
resp['objs'][e.id]['face_model'] = e.file_details.faces[0].facefile_lnk.model_used
resp['objs'][e.id]['faces'] = []
# put face data back into array format (for js processing)
for face in e.file_details.faces:
fd= {}
fd['x'] = face.face_left
fd['y'] = face.face_top
fd['w'] = face.w
fd['h'] = face.h
if face.refimg:
fd['pid'] = face.refimg.person.id
fd['who'] = face.refimg.person.tag
fd['distance'] = round(face.refimg_lnk.face_distance,2)
resp['objs'][e.id]['faces'].append(fd)
eids=eids.rstrip(",")
lst = eids.split(',')
if 'next' in request.form:
OPT.current = int(lst[0])
if 'prev' in request.form:
OPT.current = int(lst[-1])
resp['current']=OPT.current
# OPT.first_eid can still be 0 IF we have gone past the first page, I could
# better set this in states rather than kludge this if... think about it
if OPT.first_eid>0:
resp['first_eid']=OPT.first_eid
resp['last_eid']=OPT.last_eid
resp['eids']=eids
resp['offset']=OPT.offset
# print( f"BUG-DEBUG: /view_list route #1 - OPT={OPT}, eids={eids} ")
# save pref to keep the new current value, first/last
pref=PA_UserState.query.filter(PA_UserState.pa_user_dn==current_user.dn,PA_UserState.orig_ptype==OPT.orig_ptype,PA_UserState.view_eid==OPT.view_eid).first()
# print( f"BUG-DEBUG: /view_list route #2 - OPT={OPT}, eids={eids} ")
UpdatePref( pref, OPT )
# print( f"BUG-DEBUG: /view_list route #3 - OPT={OPT}, eids={eids} ")
return make_response( resp )
@login_required
@app.route("/newview/", methods=["POST"])
def newview():
@app.route("/view/", methods=["POST"])
def view():
data = request.get_json() # Parse JSON body
eid = data.get('eid', 0) # Extract list of ids
@@ -882,22 +615,9 @@ def newview():
data=db.session.execute(stmt).unique().scalars().all()
return jsonify(entries_schema.dump(data))
################################################################################
# /view/id -> grabs data from DB and views it (GET)
################################################################################
@login_required
@app.route("/view/<id>", methods=["GET"])
def view(id):
OPT=States( request )
objs = {}
entries=GetEntries( OPT )
eids=""
for e in entries:
objs[e.id]=e
eids += f"{e.id},"
# if this is a dir, we wont view it with a click anyway, so move on...
if not e.file_details:
continue
####
"""
# process any overrides
for face in e.file_details.faces:
# now get any relevant override and store it in objs...
@@ -909,20 +629,6 @@ def view(id):
mo.type = FaceOverrideType.query.filter( FaceOverrideType.name== 'Manual match to existing person' ).first()
face.manual_override=mo
eids=eids.rstrip(",")
# jic, sometimes we trip this, and rather than show broken pages / destroy
if id not in eids:
# SetFELog( message=f"ERROR: viewing an id, but its not in eids OPT={OPT}, id={id}, eids={eids}", level="danger", persistent=True, cant_close=False)
# msg="Sorry, viewing data is confused, cannot view this image now"
# if os.environ['ENV'] == "production":
# msg += "Clearing out all states. This means browser back buttons will not work, please start a new tab and try again"
# PA_UserState.query.delete()
# db.session.commit()
# SetFELog( msg, "warning", persistent=True, cant_close=False )
# return redirect("/")
print( f"id={id}, eids={eids}" )
return "200"
else:
NMO_data = FaceOverrideType.query.all()
setting = Settings.query.first()
imp_path = setting.import_path
@@ -930,18 +636,7 @@ def view(id):
bin_path = setting.recycle_bin_path
# print( f"BUG-DEBUG: /view/id GET route - OPT={OPT}, eids={eids}, current={int(id)} ")
return render_template("viewer.html", current=int(id), eids=eids, objs=objs, OPT=OPT, NMO_data=NMO_data, imp_path=imp_path, st_path=st_path, bin_path=bin_path )
##################################################################################
# /view/id -> grabs data from DB and views it (POST -> set state, redirect to GET)
##################################################################################
@app.route("/view/<id>", methods=["POST"])
@login_required
def view_img_post(id):
# set pa_user_states...
OPT=States( request )
# print( f"BUG-DEBUG: /view/id POST route - OPT={OPT}, id={id} ")
# then use back-button friendly URL (and use pa_user_states to view the right image in the right list
return redirect( "/view/" + id );
"""
# route called from front/end - if multiple images are being transformed, each transorm == a separate call
# to this route (and therefore a separate transorm job. Each reponse allows the f/e to check the
@@ -1006,14 +701,6 @@ def _jinja2_filter_toplevelfolderof(path, cwd):
else:
return False
###############################################################################
# This func creates a new filter in jinja2 to test to hand back the parent path
# from a given path
################################################################################
@app.template_filter('ParentPath')
def _jinja2_filter_parentpath(path):
return os.path.dirname(path)
###############################################################################
# route to allow the Move Dialog Box to pass a date (YYYYMMDD) and returns a
# json list of existing dir names that could be near it in time. Starting

View File

@@ -351,11 +351,6 @@ function addFigure( obj, last, ecnt)
last.printed = obj.file_details.month;
}
}
/*
{% if not entry_data %}
<span class="alert alert-danger p-2 col-auto"> No matches for: '{{search_term}}'</span>
{% endif %}
*/
// Image/Video/Unknown entry
if (obj.type.name === "Image" || obj.type.name === "Video" || obj.type.name === "Unknown") {
@@ -466,9 +461,9 @@ function getDirEntries(dir_id, back)
$.ajax({
type: 'POST',
url: '/get_dir_entries',
data: JSON.stringify(data), // Stringify the data
contentType: 'application/json', // Set content type
dataType: 'json', // Expect JSON response
data: JSON.stringify(data),
contentType: 'application/json',
dataType: 'json',
success: function(res) {
document.entries=res
// rebuild entryList/pageList as each dir comes with new entries
@@ -525,6 +520,8 @@ function drawPageOfFigures()
addFigure( obj, last, ecnt )
ecnt++
}
if( document.entries.length == 0 && OPT.search_term != '' )
$('#figures').append( `<span class="alert alert-danger p-2 col-auto"> No matches for: '${OPT.search_term}'</span>` )
$('.figure').click( function(e) { DoSel(e, this ); SetButtonState(); return false; });
$('.figure').dblclick( function(e) { dblClickToViewEntry( $(this).attr('id') ) } )
// for dir, getDirEntries 2nd param is back (or "up" a dir)
@@ -532,8 +529,47 @@ function drawPageOfFigures()
$(".back").click( function(e) { getDirEntries(this.id,true) } )
}
function getPageFileList(res, viewingIdx)
{
$('#file_list_div').empty()
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)
}
// function called when we get another page from inside the files view
function getPageFigures(res, viewingIdx)
{
// add all the figures to files_div
drawPageOfFigures()
}
// 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()
}
// Function to get the 'page' of entry ids out of entryList
function getPage(pageNumber,viewing_idx=0)
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
@@ -546,29 +582,14 @@ function getPage(pageNumber,viewing_idx=0)
// set up data to send to server to get the entry data for entries in pageList
data={}
data.ids = pageList
data.query = 99999
$.ajax({
type: 'POST',
url: '/get_entries_by_ids',
data: JSON.stringify(data), // Stringify the data
contentType: 'application/json', // Set content type
dataType: 'json', // Expect JSON response
success: function(res) {
document.entries=res
// add all the figures to files_div
drawPageOfFigures()
// noting we could have been in files_div, or viewer_div, update both jic
// and fix viewer_div - update viewing, arrows and image/video too
document.viewing=document.entries[viewing_idx]
type: 'POST', url: '/get_entries_by_ids',
data: JSON.stringify(data), contentType: 'application/json',
dataType: 'json',
success: function(res) { document.entries=res; successCallback(res,viewingIdx); },
error: function(xhr, status, error) { console.error("Error:", error); } });
resetNextPrevButtons()
ViewImageOrVideo()
},
error: function(xhr, status, error) {
console.error("Error:", error);
}
});
return
}
@@ -611,7 +632,7 @@ function resetNextPrevButtons()
}
// get list of eids for the next page, also make sure next/prev buttons make sense for page we are on
function nextPage()
function nextPage(successCallback)
{
// pageList[0] is the first entry on this page
const currentPage=getPageNumberForId( pageList[0] )
@@ -621,13 +642,12 @@ function nextPage()
console.error( "WARNING: seems first on pg=" + firstEntryOnPage + " of how many=" + OPT.how_many + " gives currentPage=" + currentPage + " and we cant go next page?" )
return
}
getPage( currentPage+1 )
resetNextPrevButtons()
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()
function prevPage(successCallback)
{
// pageList[0] is the first entry on this page
const currentPage=getPageNumberForId( pageList[0] )
@@ -637,8 +657,7 @@ function prevPage()
console.error( "WARNING: seems first on pg=" + firstEntryOnPage + " of how many=" + OPT.how_many + " gives currentPage=" + currentPage + " and we cant go prev page?" )
return
}
getPage( currentPage-1 )
resetNextPrevButtons()
getPage( currentPage-1, successCallback )
return
}
@@ -646,3 +665,45 @@ function isMobile() {
try{ document.createEvent("TouchEvent"); return true; }
catch(e){ return false; }
}
function changeOPT(successCallback) {
OPT.how_many=$('#how_many').val()
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=$('#size').val()
$.ajax({
type: 'POST',
url: '/change_file_opts',
data: JSON.stringify(OPT),
contentType: 'application/json',
success: function(resp) {
entryList=resp.query_data.entry_list
// put data back into booleans, ints, etc
OPT.folders=( OPT.folders == 'True' )
OPT.how_many=parseInt(OPT.how_many)
$('.how_many_text').html( `&nbsp;${OPT.how_many} files&nbsp;` )
OPT.root_eid=parseInt(OPT.root_eid)
OPT.size=parseInt(OPT.size)
getPage(1,successCallback)
}
})
}

View File

@@ -513,42 +513,6 @@ class PA_JobManager_FE_Message(Base):
return "<id: {}, job_id: {}, level: {}, message: {}".format(self.id, self.job_id, self.level, self.message)
##############################################################################
# Class describing PA_UserState and in the DB (via sqlalchemy)
# the state for a User defines a series of remembered states for a user
# to optimise their viewing, etc. If we scan and fine new files, we need to
# invalidate these cached values, so we have this class here just for that
##############################################################################
class PA_UserState(Base):
__tablename__ = "pa_user_state"
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
##############################################################################
@@ -1131,7 +1095,6 @@ def DisconnectAllOverrides(job):
def JobForceScan(job):
JobProgressState( job, "In Progress" )
DisconnectAllOverrides(job)
session.query(PA_UserState).delete()
session.query(FaceFileLink).delete()
session.query(FaceRefimgLink).delete()
session.query(Face).delete()
@@ -1668,18 +1631,6 @@ def find_last_successful_ai_scan(job):
return ai_job.last_update.timestamp()
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
# file system and calls AddFile()/AddDir() as necessary
@@ -1788,8 +1739,6 @@ def JobImportDir(job):
if found_new_files:
job.extra.append( JobExtra( name="new_files", value=str(found_new_files) ) )
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)

View File

@@ -19,30 +19,18 @@ class PA_UserState(db.Model):
__tablename__ = "pa_user_state"
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 )
last_used = db.Column(db.DateTime(timezone=True))
path_type = db.Column(db.String, primary_key=True, unique=False, nullable=False )
noo = 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 )
st_offset = 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 )
root = 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)
search_term = db.Column(db.String, unique=False, nullable=False )
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}, search_term: {self.orig_search_term}>"
################################################################################
@@ -97,9 +85,13 @@ class States(PA):
self.default_folder_noo=u.default_storage_noo
self.default_search_noo=u.default_search_noo
self.cwd=self.root
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)
################################################################################

View File

@@ -27,7 +27,7 @@ INSERT INTO ai_model VALUES ( 2, 'CNN', 'MORE ACCURATE / MUCH SLOWER' );
CREATE TABLE settings(
id INTEGER,
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,
face_size_limit INTEGER,
scheduled_import_scan INTEGER, scheduled_storage_scan INTEGER,
@@ -46,17 +46,15 @@ CREATE TABLE pa_user(
default_grouping VARCHAR(16),
default_how_many INTEGER,
default_size INTEGER,
default_import_folders bOOLEAN,
default_storage_folders bOOLEAN,
default_import_folders BOOLEAN,
default_storage_folders BOOLEAN,
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
-- 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),
noo VARCHAR(16), grouping VARCHAR(16), how_many INTEGER, st_offset INTEGER, size INTEGER, folders bOOLEAN,
root VARCHAR, cwd VARCHAR,
orig_ptype VARCHAR, orig_search_term VARCHAR, orig_url VARCHAR,
view_eid INTEGER, current INTEGER, first_eid INTEGER, last_eid INTEGER, num_entries INTEGER, last_used TIMESTAMPTZ,
noo VARCHAR(16), grouping VARCHAR(16), how_many INTEGER, size INTEGER, folders BOOLEAN,
root VARCHAR, cwd VARCHAR, search_term VARCHAR,
CONSTRAINT fk_pa_user_dn FOREIGN KEY (pa_user_dn) REFERENCES pa_user(dn),
CONSTRAINT pk_pa_user_states_id PRIMARY KEY(id ) );

View File

@@ -1,64 +1,49 @@
{% extends "base.html" %} {% block main_content %}
<script src="{{ url_for( 'internal', filename='js/files_support.js')}}"></script>
<div class="container-fluid">
<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="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( "how_many", OPT.how_many|string, ["10", "25", "50", "75", "100", "150", "200", "500"], "", "rounded-end 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"], "changeOPT(getPageFileList); return false", "rounded-end py-1 my-1" )|safe }}
<div class="mb-1 col my-auto d-flex justify-content-center">
{% set prv_disabled="" %}
{% if OPT.offset|int == 0 %}
{% set prv_disabled="disabled" %}
{% endif %}
<button id="prev" {{prv_disabled}} name="prev" class="prev sm-txt btn btn-outline-secondary">
<button id="prev" name="prev" class="prev sm-txt btn btn-outline-secondary" onClick="prevPage(getPageFileList)">
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#prev"/></svg>
</button>
<span class="sm-txt my-auto">&nbsp;{{OPT.how_many}} files&nbsp;</span>
{% set nxt_disabled="" %}
{% if entry_data|length < OPT.how_many|int %}
{% set nxt_disabled="disabled" %}
{% endif %}
<button id="next" {{nxt_disabled}} name="next" class="next sm-txt btn btn-outline-secondary">
<span class="how_many_text sm-txt my-auto">&nbsp;{{OPT.how_many}} files&nbsp;</span>
<button 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')}}#next"/></svg>
</button>
</div class="col...">
</div class="input-group...">
</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 id="file_list_div" class="container-fluid">
</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')}}#prev"/></svg>
</button>
<span class="how_many_text sm-txt my-auto">&nbsp;{{OPT.how_many}} files&nbsp;</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')}}#next"/></svg>
</button>
</div class="col my-auto"> </div class="row">
</div class="container-fluid">
{% 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 }};
// 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 %}

View File

@@ -23,19 +23,8 @@
document.viewing_eid=null;
document.viewing=null;
var OPT={}
OPT.noo='{{OPT.noo}}'
OPT.how_many={{OPT.how_many}}
OPT.folders="{{OPT.folders}}" === "True"
OPT.grouping='{{OPT.grouping}}'
OPT.cwd='{{OPT.cwd}}'
OPT.root_eid={{query_data.root_eid}}
OPT.search_term='{{OPT.orig_search_term}}'
OPT.size={{OPT.size}}
OPT.prefix='{{OPT.prefix}}'
OPT.default_flat_noo='{{OPT.default_flat_noo}}'
OPT.default_folder_noo='{{OPT.default_folder_noo}}'
OPT.default_search_noo='{{OPT.default_search_noo}}'
var OPT = {{ OPT.to_dict()|tojson }};
OPT.root_eid = {{ query_data.root_eid }};
// this is the list of entry ids for the images for ALL matches for this query
var entryList={{query_data.entry_list}}
@@ -43,57 +32,17 @@
// 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)
getPage(1,getPageFigures)
function cFO() {
OPT.how_many=$('#how_many').val()
new_f=$('#folders').val()
new_f=( new_f == 'True' )
// if change to/from folders, also fix the noo menu
if( new_f != OPT.folders )
function changeSize()
{
if( new_f )
{
$('#noo option:lt(2)').prop('disabled', true);
$('#noo').val(OPT.default_folder_noo)
sz=$('input[name="size"]:checked').val();
$('.thumb').prop('height',sz);
}
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=$('#size').val()
$.ajax({
type: 'POST',
url: '/change_file_opts',
data: JSON.stringify(OPT),
contentType: 'application/json',
success: function(resp) {
entryList=resp.query_data.entry_list
// put data back into booleans, ints, etc
OPT.folders=( OPT.folders == 'True' )
OPT.how_many=parseInt(OPT.how_many)
$('.how_many_text').html( `&nbsp;${OPT.how_many} files&nbsp;` )
OPT.root_eid=parseInt(OPT.root_eid)
OPT.size=parseInt(OPT.size)
getPage(1)
}
})
}
</script>
<div id="files_div">
<div class="container-fluid">
<input type="hidden" name="cwd" id="cwd" value="{{OPT.cwd}}">
{% if search_term is defined %}
<input type="hidden" name="search_term" id="view_term" value="{{search_term}}">
{% endif %}
<div class="d-flex row mb-2">
{% if OPT.folders %}
<div class="my-auto col col-auto">
@@ -113,13 +62,12 @@
{% endif %}
<div class="col col-auto">
<div class="input-group">
{{CreateSelect( "noo", OPT.noo, ["Oldest", "Newest","A to Z", "Z to A"], "cFO(); return false", "rounded-start py-2")|safe }}
{{CreateSelect( "how_many", OPT.how_many|string, ["10", "25", "50", "75", "100", "150", "200", "500"], "cFO(); return false" )|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"], "changeOPT(getPageFigures); return false" )|safe }}
{% if OPT.folders %}
<input type="hidden" name="grouping" id="grouping" value="{{OPT.grouping}}">
{{CreateFoldersSelect( OPT.folders, "cFO(); return false", "rounded-end" )|safe }}
{{CreateFoldersSelect( OPT.folders, "changeOPT(getPageFigures); return false", "rounded-end" )|safe }}
{% else %}
{{CreateFoldersSelect( OPT.folders, "cFO(); return false" )|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>
{{CreateSelect( "grouping", OPT.grouping, ["None", "Day", "Week", "Month"], "OPT.grouping=$('#grouping').val();drawPageOfFigures();return false", "rounded-end")|safe }}
{% endif %}
@@ -134,11 +82,11 @@
</script>
{% endif %}
<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 disabled" onClick="prevPage()" disabled>
<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>
</button>
<span class="how_many_text sm-txt my-auto">&nbsp;{{OPT.how_many}} files&nbsp;</span>
<button aria-label="next" id="next" name="next" class="next sm-txt btn btn-outline-secondary" onClick="nextPage()">
<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;">
@@ -156,60 +104,35 @@
<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 class="col flex-grow-1">
<div class="d-flex col col-auto justify-content-end">
<div class="btn-group">
{% if OPT.size == 64 %}
{% set bt="btn-info text-white" %}
{% else %}
{% set bt="btn-outline-info" %}
{% endif %}
<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 %}
{% set bt="btn-info text-white" %}
{% else %}
{% set bt="btn-outline-info" %}
{% endif %}
<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 %}
{% set bt="btn-info text-white" %}
{% 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="btn-group" role="group" aria-label="Size radio button group">
<input type="radio" class="btn-check" name="size" id="size-xs" onCLick="changeSize()" autocomplete="off" value="64">
<label class="btn btn-outline-info btn-radio" for="size-xs">XS</label>
<input type="radio" class="btn-check" name="size" id="size-s" onCLick="changeSize()" autocomplete="off" value="96">
<label class="btn btn-outline-info btn-radio" for="size-s">S</label>
<input type="radio" class="btn-check" name="size" id="size-m" onCLick="changeSize()" autocomplete="off" value="128">
<label class="btn btn-outline-info btn-radio" for="size-m">M</label>
<input type="radio" class="btn-check" name="size" id="size-l" onCLick="changeSize()" autocomplete="off" value="192">
<label class="btn btn-outline-info btn-radio" for="size-l">L</label>
<input type="radio" class="btn-check" name="size" id="size-xl" onCLick="changeSize()" autocomplete="off" value="256">
<label class="btn btn-outline-info btn-radio" for="size-xl">XL</label>
</div>
</div class="d-flex col">
<input id="offset" type="hidden" name="offset" value="{{OPT.offset}}">
<input id="size" type="hidden" name="size" value="{{OPT.size}}">
</div class="d-flex row mb-2">
{% set eids=namespace( str="" ) %}
{# gather all the file eids and collect them in case we go gallery mode #}
{% for obj in query_data.entry_list %}
{% set eids.str = eids.str + obj|string +"," %}
{% endfor %}
<input name="eids" id="eids" type="hidden" value="{{eids.str}}">
</div container="fluid">
<div id="figures" class="row ms-2">
</div>
<div class="container-fluid">
<input type="hidden" name="cwd" id="cwd" value="{{OPT.cwd}}">
<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()" disabled>
<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>
</button>
<span class="how_many_text sm-txt my-auto">&nbsp;{{OPT.how_many}} files&nbsp;</span>
<button aria-label="next" id="next" name="next" class="next sm-txt btn btn-outline-secondary" onClick="nextPage()">
<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>
</div class="col my-auto">
@@ -219,6 +142,8 @@
<div id="viewer_div" class="d-none">
<style>
.norm-txt { font-size: 1.0rem }
.form-check-input:checked {
background-color: #39C0ED;
@@ -280,7 +205,7 @@
// pref page, load it
if( oldPageOffset != pageOffset )
// pref page is pageOffset+1 now
getPage(pageOffset+1,currentIndex)
getPage(pageOffset+1,getPageViewer,currentIndex)
else
document.viewing=document.entries[currentIndex]
}
@@ -297,7 +222,7 @@
// next page, load it
if( oldPageOffset != pageOffset )
// next page is pageOffset+1 now
getPage(pageOffset+1,currentIndex)
getPage(pageOffset+1,getPageViewer,currentIndex)
else
document.viewing=document.entries[currentIndex]
}
@@ -434,7 +359,9 @@
</div class="row">
</div id="viewer">
</div id="viewer_div">
{% endblock main_content %}
{% block script_content %}
<script>
$( document ).keydown(function(event) {
// if dbox is visible, dont process this hot-key, we are inputting text
@@ -477,12 +404,6 @@ $( document ).keydown(function(event) {
});
var fullscreen=false;
</script>
{% endblock main_content %}
{% block script_content %}
<script>
$(document).on('click', function(e) { $('.highlight').removeClass('highlight') ; SetButtonState() });
@@ -493,28 +414,6 @@ function dblClickToViewEntry(id) {
ViewImageOrVideo()
}
function CallViewRouteWrapper()
{
// CallViewRoute( document.viewing.id )
}
function CallViewRoute(id)
{
s='<form id="_fm" method="POST" action="/view/' + id + '">'
s+='<input type="hidden" name="eids" value="'+$("#eids").val() + '">'
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();
}
// different context menu on files
$.contextMenu({
selector: '.entry',
@@ -615,5 +514,7 @@ if( isMobile() )
$('#ctrl-key').css('visibility', 'visible');
}
// check the size radiobutton
$(`input[name="size"][value="${OPT.size}"]`).prop('checked', true)
</script>
{% endblock script_content %}

View File

@@ -69,24 +69,12 @@
<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.path_type}}</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>