@@ -38,10 +38,11 @@ function enhanceGallery(){
const cards = [ ... grid . querySelectorAll ( '.image-card' ) ] ; $ ( '#gallery-search' ) . oninput = ( ) => { state . galleryQuery = $ ( '#gallery-search' ) . value ; const query = state . galleryQuery . toLowerCase ( ) ; cards . forEach ( card => card . hidden = ! card . textContent . toLowerCase ( ) . includes ( query ) ) ; } ; $ ( '#gallery-sort' ) . onchange = ( ) => { const order = $ ( '#gallery-sort' ) . value ; state . galleryOrder = order ; const indexes = cards . map ( ( card , index ) => ( { card , item : state . gallery [ index ] } ) ) ; indexes . sort ( ( a , b ) => order === 'name' ? a . item . name . localeCompare ( b . item . name ) : order === 'oldest' ? String ( a . item . created _at ) . localeCompare ( String ( b . item . created _at ) ) : String ( b . item . created _at ) . localeCompare ( String ( a . item . created _at ) ) ) ; indexes . forEach ( x => grid . append ( x . card ) ) ; } ;
cards . forEach ( ( card , i ) => { const item = state . gallery [ i ] ; if ( state . tab === 'local' ) { const star = document . createElement ( 'button' ) ; star . textContent = t ( item . starred ? 'unstar' : 'star' ) ; star . setAttribute ( 'aria-pressed' , String ( item . starred ) ) ; star . disabled = Boolean ( item . excluded ) ; star . title = item . excluded ? t ( 'clearExclusionFirst' ) : '' ; card . querySelector ( '.actions' ) . append ( star ) ; const exclude = document . createElement ( 'button' ) ; exclude . textContent = t ( item . excluded ? 'includePhoto' : 'excludePhoto' ) ; exclude . setAttribute ( 'aria-pressed' , String ( Boolean ( item . excluded ) ) ) ; exclude . disabled = Boolean ( item . starred ) ; exclude . title = item . starred ? t ( 'clearStarFirst' ) : '' ; card . querySelector ( '.actions' ) . append ( exclude ) ; exclude . onclick = busy ( async ( ) => { await api ( '/api/v1/assets/' + item . asset _id + '/exclusion' , { method : 'PUT' , body : { excluded : ! item . excluded } } ) ; await navigate ( ) ; } ) ; if ( item . starred ) { const kept = document . createElement ( 'small' ) ; kept . className = 'photo-mark' ; kept . textContent = t ( 'photoStarred' ) ; card . querySelector ( 'h3' ) . after ( kept ) ; } if ( item . excluded ) { const badge = document . createElement ( 'small' ) ; badge . className = 'excluded-photo' ; badge . textContent = t ( 'photoExcluded' ) ; card . querySelector ( 'h3' ) . after ( badge ) ; } star . onclick = busy ( async ( ) => { await api ( '/api/v1/assets/' + item . asset _id + '/star' , { method : 'PUT' , body : { starred : ! item . starred } } ) ; await navigate ( ) ; } ) ; const label = document . createElement ( 'small' ) ; label . textContent = item . origin === 'task' ? t ( 'taskOrigin' ) + ' · ' + ( item . job _name || item . job _id ) : t ( 'origin_' + item . origin ) ; card . querySelector ( 'h3' ) . after ( label ) ; const select = document . createElement ( 'input' ) ; select . type = 'checkbox' ; select . className = 'asset-select' ; select . dataset . asset = item . asset _id ; select . setAttribute ( 'aria-label' , t ( 'selectImage' ) ) ; card . prepend ( select ) ; } if ( item . width && item . height ) { const dimensions = document . createElement ( 'small' ) ; dimensions . textContent = item . width + ' × ' + item . height ; card . querySelector ( 'h3' ) . after ( dimensions ) ; } const download = document . createElement ( 'button' ) ; download . textContent = t ( state . tab === 'local' ? 'downloadShort' : 'download' ) ; card . querySelector ( '.actions' ) . append ( download ) ; download . onclick = busy ( ( ) => downloadImage ( item . url , item . name ) ) ; if ( state . tab === 'local' ) card . querySelector ( '.actions' ) . append ( card . querySelector ( '[data-delete]' ) ) ; } ) ;
if ( state . tab === 'local' ) {
const updateMarks = ( ) => { const selected = [ ... grid . querySelectorAll ( '.asset-select:checked' ) ] . map ( box => state . gallery . find ( item => item . asset _id === box . dataset . asset ) ) ; const starred = selected . some ( item => item ? . starred ) , excluded = selected . some ( item => item ? . excluded ) ; $ ( '#asset-batch-star' ) . disabled = ! selected . length || excluded ; $ ( '#asset-batch-star' ) . title = excluded ? t ( 'clearExclusionFirst' ) : '' ; $ ( '#asset-batch-exclude' ) . disabled = ! selected . length || starred ; $ ( '#asset-batch-exclude' ) . title = starred ? t ( 'clearStarFirst' ) : '' ; } ;
const updateMarks = ( ) => { const selected = [ ... grid . querySelectorAll ( '.asset-select:checked' ) ] . map ( box => state . gallery . find ( item => item . asset _id === box . dataset . asset ) ) ; const starred = selected . some ( item => item ? . starred ) , excluded = selected . some ( item => item ? . excluded ) ; $ ( '#asset-batch-star' ) . disabled = ! selected . length || excluded ; $ ( '#asset-batch-star' ) . title = ! selected . length ? t ( 'selectImagesFirst' ) : excluded ? t ( 'clearExclusionFirst' ) : '' ; $ ( '#asset-batch-exclude' ) . disabled = ! selected . length || starred ; $ ( '#asset-batch-exclude' ) . title = ! selected . length ? t ( 'selectImagesFirst' ) : starred ? t ( 'clearStarFirst' ) : '' ; disabledMarkHints ( grid . parentElement ) ; } ;
grid . querySelectorAll ( '.asset-select' ) . forEach ( box => box . onchange = updateMarks ) ;
for ( const id of [ 'asset-all' , 'asset-invert' ] ) { const previous = $ ( '#' + id ) . onclick ; $ ( '#' + id ) . onclick = event => { previous ( event ) ; updateMarks ( ) ; } ; } updateMarks ( ) ;
}
disabledMarkHints ( grid ) ;
$ ( '#gallery-search' ) . value = state . galleryQuery || '' ; $ ( '#gallery-sort' ) . value = state . galleryOrder || 'newest' ; $ ( '#gallery-search' ) . oninput ( ) ; $ ( '#gallery-sort' ) . onchange ( ) ;
}
async function weatherSettings ( ) {
@@ -164,6 +165,7 @@ async function browseSource(id,offset=0,refresh=false){
const result = await api ( '/api/v1/photo-sources/' + id + '/images?device_id=' + encodeURIComponent ( state . device ) + '&offset=' + offset + '&limit=50&refresh=' + refresh ) ; const box = $ ( '#source-browser' ) ; if ( ! box ) return ;
box . innerHTML = ` <h4> ${ t ( 'sourceImages' ) } · ${ result . total } ${ result . total >= result . catalog _limit ? ' (limit)' : '' } </h4><p class="hint"> ${ t ( 'sourceBrowseHint' ) } ${ t ( 'excludePhotoHelp' ) } </p><div class="source-items"> ${ result . items . map ( ( x , i ) => ` <article class="source-photo"><button data-source-item=" ${ i } "> ${ esc ( x . name ) } ${ x . width ? ` <small> ${ x . width } × ${ x . height } </small> ` : '' } ${ x . caption ? ` <small> ${ esc ( [ x . caption . date , x . caption . people , x . caption . title , x . caption . place , x . caption . filename ] . filter ( Boolean ) . join ( ' · ' ) ) } </small> ` : '' } </button><div class="actions"><button data-source-star=" ${ i } " aria-pressed=" ${ Boolean ( x . starred ) } " ${ x . excluded ? 'disabled' : '' } title=" ${ x . excluded ? t ( 'clearExclusionFirst' ) : '' } "> ${ t ( x . starred ? 'unstar' : 'star' ) } </button><button data-source-exclude=" ${ i } " aria-pressed=" ${ Boolean ( x . excluded ) } " ${ x . starred ? 'disabled' : '' } title=" ${ x . starred ? t ( 'clearStarFirst' ) : '' } "> ${ t ( x . excluded ? 'includePhoto' : 'excludePhoto' ) } </button></div> ${ x . starred ? ` <small class="photo-mark"> ${ t ( 'photoStarred' ) } </small> ` : '' } ${ x . excluded ? ` <small class="excluded-photo"> ${ t ( 'photoExcluded' ) } </small> ` : '' } </article> ` ) . join ( '' ) || t ( 'noSourcePhotos' ) } </div><div class="actions"><button id="source-prev" ${ offset ? '' : 'disabled' } > ${ t ( 'previousPage' ) } </button><button id="source-next" ${ offset + result . items . length < result . total ? '' : 'disabled' } > ${ t ( 'nextPage' ) } </button> ${ buttons ( 'source-refresh' , 'refresh' ) } </div> ` ;
for ( const key of [ 'star' , 'exclude' ] ) box . querySelectorAll ( '[data-source-' + key + ']' ) . forEach ( button => button . onclick = busy ( async ( ) => { const item = result . items [ Number ( button . dataset [ key === 'star' ? 'sourceStar' : 'sourceExclude' ] ) ] , field = key === 'star' ? 'starred' : 'excluded' ; await api ( '/api/v1/photo-sources/' + id + '/preferences?device_id=' + encodeURIComponent ( state . device ) , { method : 'PUT' , body : { item _id : item . id , [ field ] : ! item [ field ] } } ) ; await browseSource ( id , offset ) ; } ) ) ;
disabledMarkHints ( box ) ;
box . querySelectorAll ( '[data-source-item]' ) . forEach ( b => b . onclick = busy ( ( ) => previewSource ( id , result . items [ Number ( b . dataset . sourceItem ) ] . id ) ) ) ; bind ( 'source-prev' , ( ) => browseSource ( id , Math . max ( 0 , offset - 50 ) ) ) ; bind ( 'source-next' , ( ) => browseSource ( id , offset + 50 ) ) ; bind ( 'source-refresh' , ( ) => browseSource ( id , 0 , true ) ) ;
}
async function previewSource ( id , item _id = null ) {
@@ -195,3 +197,23 @@ function historyControls(){
bind ( 'history-all' , ( ) => { const boxes = checkboxes ( ) , checked = boxes . length > 0 && boxes . every ( x => x . checked ) ; boxes . forEach ( x => x . checked = ! checked ) ; update ( ) ; } ) ; bind ( 'history-invert' , ( ) => { checkboxes ( ) . forEach ( x => x . checked = ! x . checked ) ; update ( ) ; } ) ; bind ( 'history-delete' , ( ) => deleteHistory ( checkboxes ( ) . filter ( x => x . checked ) . map ( x => x . value ) ) ) ; update ( ) ;
}
async function deleteHistory ( ids ) { await confirmDialog ( t ( 'deleteHistoryConfirm' ) . replace ( '{count}' , ids . length ) , async ( ) => { await api ( '/api/v1/operation-history' , { method : 'DELETE' , body : { operation _ids : ids } } ) ; await navigate ( ) ; } ) ; }
function hideDisabledMarkTooltip ( ) { const tooltip = document . getElementById ( 'disabled-mark-tooltip' ) ; if ( tooltip ) tooltip . hidden = true ; }
function disabledMarkHints ( root ) {
hideDisabledMarkTooltip ( ) ;
for ( const button of root . querySelectorAll ( '.image-card .actions button[aria-pressed],#asset-batch-star,#asset-batch-exclude,[data-source-star],[data-source-exclude]' ) ) {
let wrapper = button . parentElement . classList . contains ( 'disabled-mark-hint' ) ? button . parentElement : null ;
const reason = button . disabled ? ( button . title || wrapper ? . dataset . reason || '' ) : '' ;
if ( ! reason ) { if ( wrapper ) wrapper . replaceWith ( button ) ; continue ; }
if ( ! wrapper ) { wrapper = document . createElement ( 'span' ) ; wrapper . className = 'disabled-mark-hint' ; button . before ( wrapper ) ; wrapper . append ( button ) ; }
button . removeAttribute ( 'title' ) ; wrapper . dataset . reason = reason ; wrapper . tabIndex = 0 ; wrapper . setAttribute ( 'role' , 'group' ) ; wrapper . setAttribute ( 'aria-label' , button . textContent + ': ' + reason ) ;
wrapper . onmouseenter = wrapper . onfocus = ( ) => {
let tooltip = document . getElementById ( 'disabled-mark-tooltip' ) ; if ( ! tooltip ) { tooltip = document . createElement ( 'div' ) ; tooltip . id = 'disabled-mark-tooltip' ; tooltip . setAttribute ( 'role' , 'tooltip' ) ; document . body . append ( tooltip ) ; }
tooltip . textContent = wrapper . dataset . reason ; tooltip . hidden = false ; const anchor = wrapper . getBoundingClientRect ( ) , rect = tooltip . getBoundingClientRect ( ) ; tooltip . style . left = Math . max ( 12 , Math . min ( anchor . left , innerWidth - rect . width - 12 ) ) + 'px' ; tooltip . style . top = Math . max ( 12 , Math . min ( anchor . top >= rect . height + 20 ? anchor . top - rect . height - 8 : anchor . bottom + 8 , innerHeight - rect . height - 12 ) ) + 'px' ;
} ;
wrapper . onmouseleave = wrapper . onblur = hideDisabledMarkTooltip ;
wrapper . onkeydown = event => { if ( event . key === 'Escape' ) hideDisabledMarkTooltip ( ) ; } ;
}
}
window . addEventListener ( 'resize' , hideDisabledMarkTooltip ) ;