Admin: add Describe UI, synchronous Gemini generator endpoint; update generate_data.sh (no-append)
This commit is contained in:
parent
c8a51488c0
commit
c2d64cb00a
6 changed files with 634 additions and 0 deletions
141
new.php
141
new.php
|
|
@ -417,6 +417,16 @@ try {
|
|||
$canShow = $output !== '' && is_file($config['videos_dir'].'/'.$output) && !input_dir_preview($dir);
|
||||
if ($visibleNow && !$canShow) throw new RuntimeException('Only finished full-quality videos can be shown.');
|
||||
set_input_dir_visible($dir, $visibleNow && $canShow);
|
||||
// Attempt to send "Show enabled" notification (server-side) on enable. Non-fatal.
|
||||
if ($visibleNow && $canShow) {
|
||||
if (is_file(__DIR__ . '/lib/send_push.php')) {
|
||||
include_once __DIR__ . '/lib/send_push.php';
|
||||
try_send_show_notification(basename($dir), $dir, __DIR__, '/var/log/mvlog_notify.log');
|
||||
} else {
|
||||
error_log("[mvlog] send_push helper missing, cannot send notification for " . basename($dir) . "\n", 3, '/var/log/mvlog_notify.log');
|
||||
}
|
||||
}
|
||||
|
||||
$message = ($visibleNow ? 'Shown: in-dir/' : 'Hidden: in-dir/') . basename($dir);
|
||||
$currentVisible = input_dir_visible($dir) && $canShow;
|
||||
if (!empty($_POST['ajax'])) {
|
||||
|
|
@ -746,4 +756,135 @@ function applySwitchPayload(payload){
|
|||
<footer class="site-footer admin-footer"><div class="admin-left"><nav class="tabs"><a class="<?= $tab==='new'?'active':'' ?>" href="new.php?tab=new">New</a><a class="<?= $tab==='edit'?'active':'' ?>" href="new.php?tab=edit">Edit</a><a class="<?= $tab==='videos'?'active':'' ?>" href="new.php?tab=videos">Videos</a></nav></div><div id="job-status" class="job-status"<?= empty($runningJobs) ? ' hidden' : '' ?>><?php foreach($runningJobs as $job): ?><span class="job-dot"></span><span><?=h(str_replace('_', ' ', $job['name']))?></span><?php if(!empty($job['started_at'])): ?><span class="job-age">(<?=h(format_job_age($job['started_at']))?>)</span><?php endif; ?><?php endforeach; ?></div><div class="admin-right"><a class="logout-link" href="logout.php">Log off</a></div></footer>
|
||||
<script>document.querySelectorAll('textarea[data-counter]').forEach(function(t){var c=document.getElementById(t.dataset.counter);function u(){c.textContent=t.value.length+' / '+t.maxLength+' characters';}t.addEventListener('input',u);u();});</script>
|
||||
<script>(function(){const box=document.getElementById('job-status');if(!box)return;function fmt(job){const name=(job.name||'job').replace(/_/g,' ');const started=job.started_at ? new Date(job.started_at) : null;const ageText=started && !Number.isNaN(started.getTime()) ? ' <span class="job-age">(' + formatAge(started) + ')</span>' : '';return '<span class="job-dot"></span><span>'+name+'</span>'+ageText;}function formatAge(started){const seconds=Math.max(0,Math.floor((Date.now()-started.getTime())/1000));const h=Math.floor(seconds/3600);const m=Math.floor((seconds%3600)/60);const s=seconds%60;if(h>0) return h+'h '+m+'m';if(m>0) return m+'m '+s+'s';return s+'s';}function syncEditButtons(runningJobs){document.querySelectorAll('.admin-video-row[data-job]').forEach(function(row){const job=row.dataset.job || '';const edit=row.querySelector('[data-edit-action="1"]');if(!edit) return;const running=runningJobs.has(job);if(edit.tagName==='A'){if(running){if(!edit.dataset.editHref) edit.dataset.editHref = edit.getAttribute('href') || '';edit.removeAttribute('href');edit.classList.add('disabled');edit.setAttribute('aria-disabled','true');edit.title='Rendering now';}else{const href=edit.dataset.editHref || ('?edit=' + encodeURIComponent(job));edit.setAttribute('href', href);edit.classList.remove('disabled');edit.removeAttribute('aria-disabled');edit.title='Edit';}}else{edit.classList.toggle('disabled', running);edit.title=running ? 'Rendering now' : '';}});}async function updateJobs(){try{const r=await fetch('job_status.php',{cache:'no-store'});if(!r.ok) throw new Error('status failed');const data=await r.json();const jobs=(data.jobs||[]).filter(function(job){ return job && job.name; });const runningJobs=new Set(jobs.map(function(job){ return String(job.name); }));syncEditButtons(runningJobs);if(!jobs.length){box.hidden=true;box.innerHTML='';return;}box.hidden=false;box.innerHTML=jobs.map(fmt).join('');}catch(e){if(box.innerHTML.trim()!=='') box.hidden=false;}}updateJobs();setInterval(updateJobs,30000);})();</script>
|
||||
<script>
|
||||
(function(){
|
||||
function attachDescribeButtons(){
|
||||
document.querySelectorAll('.admin-video-row').forEach(function(row){
|
||||
if (row.querySelector('.describe-button')) return;
|
||||
var actions = row.querySelector('.admin-actions');
|
||||
if (!actions) return;
|
||||
var job = row.dataset.job;
|
||||
if (!job) return;
|
||||
var describe = document.createElement('a');
|
||||
describe.className = 'button describe-button';
|
||||
describe.href = '#';
|
||||
describe.dataset.job = job;
|
||||
describe.textContent = 'Describe';
|
||||
var first = actions.querySelector('.button');
|
||||
if (first) actions.insertBefore(describe, first);
|
||||
else actions.appendChild(describe);
|
||||
});
|
||||
}
|
||||
|
||||
function showModal(job, description, rawJson){
|
||||
var existing = document.getElementById('mvlog-gemini-modal');
|
||||
if (existing) existing.remove();
|
||||
var overlay = document.createElement('div');
|
||||
overlay.id = 'mvlog-gemini-modal';
|
||||
overlay.style.cssText = 'position:fixed;left:0;top:0;width:100%;height:100%;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999;';
|
||||
var box = document.createElement('div');
|
||||
box.style.cssText = 'background:white;color:#111;padding:20px;max-width:900px;max-height:80vh;overflow:auto;border-radius:6px;font-family:inherit;';
|
||||
var h = document.createElement('h3');
|
||||
h.textContent = 'Generated description';
|
||||
var pre = document.createElement('div');
|
||||
pre.style.cssText = 'white-space:pre-wrap;font-family:inherit;margin-top:10px;border:1px solid #eee;padding:12px;background:#f8f8f8;border-radius:4px;';
|
||||
pre.textContent = description || '';
|
||||
var btnBar = document.createElement('div');
|
||||
btnBar.style.cssText = 'margin-top:12px;text-align:right;';
|
||||
var useBtn = document.createElement('button');
|
||||
useBtn.className = 'button';
|
||||
useBtn.textContent = 'Use in form';
|
||||
var cancelBtn = document.createElement('button');
|
||||
cancelBtn.className = 'button';
|
||||
cancelBtn.textContent = 'Close';
|
||||
cancelBtn.style.marginLeft = '8px';
|
||||
btnBar.appendChild(useBtn);
|
||||
btnBar.appendChild(cancelBtn);
|
||||
box.appendChild(h);
|
||||
box.appendChild(pre);
|
||||
box.appendChild(btnBar);
|
||||
overlay.appendChild(box);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
cancelBtn.addEventListener('click', function(){ overlay.remove(); });
|
||||
useBtn.addEventListener('click', function(){
|
||||
var editForm = document.getElementById('edit-form');
|
||||
if (editForm){
|
||||
var dirInput = editForm.querySelector('input[name="dir"]');
|
||||
if (dirInput && dirInput.value === job){
|
||||
var textarea = editForm.querySelector('textarea[name="description"]');
|
||||
if (textarea){
|
||||
textarea.value = description || '';
|
||||
showToast('Inserted generated description into form. Click Save changes to apply.', true);
|
||||
overlay.remove();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
try { localStorage.setItem('mvlog_gemini_description_' + job, description || ''); } catch(e){}
|
||||
window.location.href = 'new.php?tab=edit&edit=' + encodeURIComponent(job);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e){
|
||||
var el = (e.target && e.target.closest && e.target.closest('.describe-button')) || (e.target && e.target.classList && e.target.classList.contains && e.target.classList.contains('describe-button') ? e.target : null);
|
||||
if (!el) return;
|
||||
e.preventDefault();
|
||||
var job = el.dataset.job;
|
||||
if (!job) return;
|
||||
el.classList.add('disabled');
|
||||
var oldText = el.textContent;
|
||||
el.textContent = 'Generating...';
|
||||
fetch('lib/generate_data_sync.php', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {'Content-Type':'application/x-www-form-urlencoded'},
|
||||
body: 'job=' + encodeURIComponent(job) + '&max_frames=16'
|
||||
}).then(function(r){ return r.json().catch(()=>({})); })
|
||||
.then(function(data){
|
||||
if (data && data.status === 'ok'){
|
||||
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);
|
||||
}
|
||||
}).catch(function(err){
|
||||
showToast('Request failed: ' + err, false);
|
||||
}).finally(function(){
|
||||
el.classList.remove('disabled');
|
||||
el.textContent = oldText;
|
||||
});
|
||||
});
|
||||
|
||||
attachDescribeButtons();
|
||||
var list = document.querySelector('.admin-list.video-list');
|
||||
if (list) new MutationObserver(function(){ attachDescribeButtons(); }).observe(list, {childList:true, subtree:true});
|
||||
|
||||
// On edit page load: if a generated description is in localStorage, insert it into the form.
|
||||
(function(){
|
||||
var editForm = document.getElementById('edit-form');
|
||||
if (!editForm) return;
|
||||
var dirInput = editForm.querySelector('input[name="dir"]');
|
||||
if (!dirInput) return;
|
||||
var job = dirInput.value;
|
||||
try {
|
||||
var key = 'mvlog_gemini_description_' + job;
|
||||
var desc = localStorage.getItem(key);
|
||||
if (desc) {
|
||||
var textarea = editForm.querySelector('textarea[name="description"]');
|
||||
if (textarea) {
|
||||
textarea.value = desc;
|
||||
showToast('Inserted generated description into form. Click Save changes to apply.', true);
|
||||
}
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
} catch(e){}
|
||||
})();
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
</body></html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue