Admin: enable download, surface Gemini errors in toast, AJAX file delete; style: make toast clickable

This commit is contained in:
hbrain 2026-05-29 23:21:56 +02:00
parent 3de0d498ce
commit 420ce99046
3 changed files with 99 additions and 24 deletions

View file

@ -195,24 +195,10 @@ if [ -z "$TEXT" ]; then
exit 1
fi
# Save raw Gemini JSON for reference in parent in-dir with YYYYMMDD-name.json
# Save raw Gemini JSON for reference using the subdirectory name as filename
OUT_PARENT="$(dirname "$IN_DIR")"
JOBNAME="$(basename "$IN_DIR")"
if [[ "$JOBNAME" =~ ^([0-9]{8})[_-](.+)$ ]]; then
DATE_PART="${BASH_REMATCH[1]}"
NAME_PART="${BASH_REMATCH[2]}"
else
firstfile="$(find "$IN_DIR" -maxdepth 1 -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' -o -iname '*.gif' -o -iname '*.mp4' -o -iname '*.mov' -o -iname '*.m4v' -o -iname '*.webm' -o -iname '*.mkv' \) -printf '%p\n' | sort | head -n1)"
if [ -n "$firstfile" ]; then
DATE_PART="$(date -r "$firstfile" +%Y%m%d 2>/dev/null || date +%Y%m%d)"
else
DATE_PART="$(date +%Y%m%d)"
fi
NAME_PART="$JOBNAME"
fi
# sanitize
SAFE_NAME="$(echo "$NAME_PART" | tr ' /' '__' | tr -c '[:alnum:]_.-' '_')"
OUT_JSON="$OUT_PARENT/${DATE_PART}-${SAFE_NAME}.json"
OUT_JSON="$OUT_PARENT/${JOBNAME}.json"
echo "$TEXT" | jq . > "$OUT_JSON"
chown www-data:www-data "$OUT_JSON" 2>/dev/null || true
chmod 664 "$OUT_JSON" 2>/dev/null || true

103
new.php
View file

@ -552,9 +552,41 @@ $runningJobs = active_worker_jobs($config);
<?php if($tab==='edit' && $editDir && $editRunning): ?><section><h2>Edit input dir</h2><p><code>in-dir/<?=h(basename($editDir))?></code></p><div class="err">This input directory is <?=h($editStatus)?>. Editing is disabled until the job completes.</div></section><?php endif; ?>
<?php if($tab==='edit' && $editDir && !$editRunning): ?><section><h2>Edit input dir</h2><p><code>in-dir/<?=h(basename($editDir))?></code></p><form method="post" enctype="multipart/form-data" id="edit-form"><input type="hidden" name="action" value="update"><input type="hidden" name="dir" value="<?=h(basename($editDir))?>"><label>Title<input name="title" value="<?=h($editData['title'])?>"></label><label>Date<input name="date" value="<?=h($editData['date'])?>"></label><label>Place<input name="place" value="<?=h($editData['place'])?>"></label><label>Description<textarea name="description" maxlength="5000" data-counter="description-counter-edit"><?=h($editData['description'])?></textarea><small id="description-counter-edit" class="counter"></small></label><label>Add images / videos<input type="file" name="media[]" multiple accept="image/*,video/*"></label><label>Add audio<input type="file" name="audio[]" multiple accept="audio/*"></label><h3>Files and captions</h3><div class="file-list"><?php foreach($editFiles as $f): $ext=strtolower(pathinfo($f, PATHINFO_EXTENSION)); $fileUrl='in-dir/'.rawurlencode(basename($editDir)).'/'.rawurlencode($f); ?><div class="caption-row"><div class="preview"><?php if(in_array($ext,['jpg','jpeg','png','webp','gif'])): ?><img src="<?=h($fileUrl)?>" alt=""><?php elseif(in_array($ext,['mp4','mov','m4v','webm'])): ?><video src="<?=h($fileUrl)?>" controls preload="metadata"></video><a class="download-link" href="<?=h($fileUrl)?>" download>Download</a><?php else: ?><span><?=h(strtoupper($ext ?: 'FILE'))?></span><?php endif; ?></div><div class="caption-fields"><strong><?=h($f)?></strong><?php if(in_array($ext,['jpg','jpeg','png','webp','gif','mp4','mov','m4v','webm'])): ?><input type="hidden" name="caption_files[]" value="<?=h($f)?>"><textarea name="captions[]" placeholder="Optional caption for this file"><?=h($editData['captions'][$f] ?? '')?></textarea><?php if(in_array($ext,['mp4','mov','m4v','webm'])): ?><label class="checkbox-label"><input type="checkbox" name="use_audio_files[]" value="<?=h($f)?>" <?=!empty($editData['video_audio'][$f])?'checked':''?>> <span>Use video file audio</span></label><?php endif; ?><?php else: ?><small>No caption for audio files</small><?php endif; ?></div><button type="button" onclick="deleteFile(<?=h(json_encode($f))?>)">Remove</button></div><?php endforeach; ?></div><button type="submit">Save changes</button></form><form method="post" id="delete-file-form" style="display:none"><input type="hidden" name="action" value="delete_file"><input type="hidden" name="dir" value="<?=h(basename($editDir))?>"><input type="hidden" name="file" id="delete-file-name" value=""></form><form method="post" onsubmit="return confirm(\'Delete this input directory\?\')" id="delete-dir-form""><input type="hidden" name="action" value="delete_dir"><input type="hidden" name="dir" value="<?=h(basename($editDir))?>"><label class="checkbox-label"><input type="checkbox" name="delete_output" value="1"> <span>Also delete generated video, if any</span></label><button class="danger" type="submit">Delete input dir</button></form></section><?php endif; ?>
<?php if($tab==='new'): ?><section><h2>Add new movie input</h2><form method="post" enctype="multipart/form-data" id="new-form"><input type="hidden" name="action" value="create"><label>Title*<input name="title" required></label><label>Date<input name="date" placeholder="2026-05-25"></label><label>Place<input name="place"></label><label>Description<textarea name="description" maxlength="5000" data-counter="description-counter-new"></textarea><small id="description-counter-new" class="counter"></small></label><label>Images / videos*<input type="file" name="media[]" multiple required accept="image/*,video/*"></label><label>Audio (optional)<input type="file" name="audio[]" multiple accept="audio/*"></label><button type="submit">Create input-dir</button></form></section><?php endif; ?>
<script>function deleteFile(name){if(confirm('Remove this file?')){document.getElementById('delete-file-name').value=name;document.getElementById('delete-file-form').submit();}}</script>
<script>
function showToast(message, ok){
function deleteFile(name){
if(!confirm('Remove this file?')) return;
var form = document.getElementById('delete-file-form');
if(!form){ showToast('Internal error: delete form not found', false); return; }
var dirInput = form.querySelector('input[name=dir]');
var dir = dirInput ? dirInput.value : '';
// find the caption-row for this file name
var rows = document.querySelectorAll('.caption-row');
var target = null;
for(var i=0;i<rows.length;i++){
var strong = rows[i].querySelector('.caption-fields strong') || rows[i].querySelector('strong');
if(!strong) continue;
if(strong.textContent.trim() === name){ target = rows[i]; break; }
}
var body = 'action=delete_file&dir=' + encodeURIComponent(dir) + '&file=' + encodeURIComponent(name) + '&ajax=1';
fetch('new.php', {
method: 'POST',
credentials: 'same-origin',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: body
}).then(function(r){ return r.json().catch(function(){ return { ok: false, message: 'Invalid server response' }; }); })
.then(function(json){
if(!json || !json.ok) throw new Error(json && (json.error || json.message) ? (json.error || json.message) : 'Delete failed');
if(target) target.remove();
showToast(json.message || ('Removed ' + name), true);
}).catch(function(err){
showToast(err.message || String(err), false);
});
}
</script>
<script>
function showToast(message, ok, details){
let wrap=document.getElementById('toast-wrap');
if(!wrap){
wrap=document.createElement('div');
@ -563,16 +595,62 @@ function showToast(message, ok){
document.body.appendChild(wrap);
}
const toast=document.createElement('div');
toast.className='toast '+(ok?'ok-toast':'err-toast');
toast.textContent=message;
toast.className='toast '+(ok? 'ok-toast' : 'err-toast');
toast.style.pointerEvents = 'auto';
const text=document.createElement('div');
text.style.whiteSpace='normal';
text.textContent=message;
toast.appendChild(text);
if(details){
const actions=document.createElement('div');
actions.style.marginTop='8px';
actions.style.textAlign='right';
const dbtn=document.createElement('button');
dbtn.className='button';
dbtn.textContent='Details';
dbtn.style.padding='0.35rem 0.6rem';
dbtn.style.fontSize='0.85rem';
dbtn.addEventListener('click', function(e){ e.stopPropagation(); e.preventDefault(); showLogModal('Generator log', details); });
actions.appendChild(dbtn);
toast.appendChild(actions);
}
wrap.appendChild(toast);
requestAnimationFrame(function(){toast.classList.add('show');});
const timeout = details ? 15000 : 4000;
setTimeout(function(){
toast.classList.remove('show');
setTimeout(function(){toast.remove();},250);
},3200);
}, timeout);
}
function showLogModal(title, text){
var existing=document.getElementById('mvlog-log-modal');
if(existing) existing.remove();
var overlay=document.createElement('div');
overlay.id='mvlog-log-modal';
overlay.style.cssText='position:fixed;left:0;top:0;width:100%;height:100%;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:10001;';
var box=document.createElement('div');
box.style.cssText='background:white;color:#111;padding:16px;max-width:90%;max-height:80vh;overflow:auto;border-radius:6px;font-family:inherit;';
var h=document.createElement('h3');
h.textContent=title||'Details';
var pre=document.createElement('pre');
pre.style.cssText='white-space:pre-wrap;font-family:monospace;margin-top:10px;border:1px solid #eee;padding:12px;background:#f8f8f8;border-radius:4px; max-height:60vh; overflow:auto;';
pre.textContent=text||'';
var btnBar=document.createElement('div');
btnBar.style.cssText='margin-top:12px;text-align:right;';
var copyBtn=document.createElement('button');
copyBtn.className='button'; copyBtn.textContent='Copy';
var closeBtn=document.createElement('button'); closeBtn.className='button'; closeBtn.textContent='Close'; closeBtn.style.marginLeft='8px';
btnBar.appendChild(copyBtn); btnBar.appendChild(closeBtn);
box.appendChild(h); box.appendChild(pre); box.appendChild(btnBar); overlay.appendChild(box); document.body.appendChild(overlay);
closeBtn.addEventListener('click', function(){ overlay.remove(); });
copyBtn.addEventListener('click', function(){ try{ navigator.clipboard.writeText(text).then(function(){ showToast('Copied to clipboard', true); }, function(){ showToast('Copy failed', false); }); }catch(e){ showToast('Copy failed', false); } });
}
const editForm=document.getElementById('edit-form');
if(editForm) handleAjaxFormSubmit(editForm, 'edit');
@ -846,9 +924,20 @@ function applySwitchPayload(payload){
var desc = data.description || (data.raw_json && JSON.stringify(data.raw_json)) || '';
showModal(job, desc, data.raw_json || null);
} else {
showToast('Error: ' + (data && data.message ? data.message : 'No response'), false);
var msg = (data && data.message) ? data.message : 'No response';
if (data && data.log) {
try {
var log = String(data.log);
var lines = log.split('\n').map(function(l){ return l.trim(); }).filter(function(l){ return l.length>0; });
var keywords = ['quota','rate limit','rate-limit','429','503','overloaded','high demand','busy','unavailable','error','failed','retry'];
var found = lines.find(function(l){ var ll=l.toLowerCase(); return keywords.some(function(k){ return ll.indexOf(k) !== -1; }); });
if (found) msg += ' — ' + found;
else if (lines.length) msg += ' — ' + lines[0].slice(0,200);
} catch(e){}
}
showToast('Error: ' + msg, false, (data && data.log) ? (typeof data.log === 'string' ? data.log : JSON.stringify(data.log)) : null);
}
}).catch(function(err){
}).catch(function(err){
showToast('Request failed: ' + err, false);
}).finally(function(){
el.classList.remove('disabled');

File diff suppressed because one or more lines are too long