Add HF model auto-download with configurable AI env, and share link visibility toggles
This commit is contained in:
+17
-3
@@ -10,6 +10,15 @@ let searchTimer = null;
|
||||
let view = localStorage.getItem('journalView') === 'cards' ? 'cards' : 'list';
|
||||
let aiCorrecting = false;
|
||||
let editDeletePhoto = false;
|
||||
let linkSettings = null;
|
||||
|
||||
async function loadLinkSettings() {
|
||||
if (!linkSettings) {
|
||||
const res = await fetch(`${API}/api/settings`, { headers: hdr() });
|
||||
linkSettings = await res.json().catch(() => ({}));
|
||||
}
|
||||
return linkSettings;
|
||||
}
|
||||
|
||||
function isImageFile(name) {
|
||||
return /\.(jpe?g|png|gif|webp|bmp|avif|svg|ico)$/i.test(name || '');
|
||||
@@ -531,10 +540,15 @@ async function openLinkCreate() {
|
||||
document.getElementById('linkNameInput').value = `${g ? g.name : ''} ${dfl}`;
|
||||
document.getElementById('linkStudent').value = currentStudent;
|
||||
document.getElementById('linkExpiresDays').value = '7';
|
||||
document.getElementById('linkAnonymizeNames').checked = false;
|
||||
document.getElementById('linkShowNames').checked = false;
|
||||
document.getElementById('linkPassword').value = '';
|
||||
document.getElementById('linkMessage').value = '';
|
||||
document.getElementById('linkUrl').value = '';
|
||||
const s = await loadLinkSettings();
|
||||
document.getElementById('linkShowNames').checked = s.share_show_student_names !== 'false';
|
||||
document.getElementById('linkShowMessage').checked = s.share_show_student_message === 'true';
|
||||
document.getElementById('linkShowDate').checked = s.share_show_entry_date === 'true';
|
||||
document.getElementById('linkShowPhotos').checked = s.share_show_group_photos !== 'false';
|
||||
document.getElementById('linkCreateView').style.display = '';
|
||||
document.getElementById('linkResultView').style.display = 'none';
|
||||
document.getElementById('linkModal').classList.add('open');
|
||||
@@ -551,7 +565,7 @@ async function createLink() {
|
||||
const date_from = document.getElementById('dateFrom').value;
|
||||
const date_to = document.getElementById('dateTo').value;
|
||||
const expires_days = parseInt(document.getElementById('linkExpiresDays').value, 10);
|
||||
const anonymize_names = document.getElementById('linkAnonymizeNames').checked;
|
||||
const show_student_names = document.getElementById('linkShowNames').checked;
|
||||
const access_password = document.getElementById('linkPassword').value;
|
||||
const message = document.getElementById('linkMessage').value.trim();
|
||||
const link_url = document.getElementById('linkUrl').value.trim();
|
||||
@@ -561,7 +575,7 @@ async function createLink() {
|
||||
const res = await fetch(`${API}/api/links`, {
|
||||
method: 'POST',
|
||||
headers: hdrJson(),
|
||||
body: JSON.stringify({ name, group_id, student_name: student_name || null, date_from: date_from || null, date_to: date_to || null, anonymize_names, expires_at, access_password: access_password || null, message: message || null, link_url: link_url || null })
|
||||
body: JSON.stringify({ name, group_id, student_name: student_name || null, date_from: date_from || null, date_to: date_to || null, show_student_names, expires_at, access_password: access_password || null, message: message || null, link_url: link_url || null, show_student_message: document.getElementById('linkShowMessage').checked, show_entry_date: document.getElementById('linkShowDate').checked, show_group_photos: document.getElementById('linkShowPhotos').checked })
|
||||
});
|
||||
if (!res.ok) { const err = await res.json(); alert(err.error || 'Ошибка'); return; }
|
||||
const link = await res.json();
|
||||
|
||||
+29
-7
@@ -2,15 +2,27 @@ let links = [];
|
||||
let groups = [];
|
||||
let editingId = null;
|
||||
let studentsLoaded = false;
|
||||
let linkSettings = null;
|
||||
|
||||
async function loadLinkSettings() {
|
||||
if (!linkSettings) {
|
||||
const res = await fetch(`${API}/api/settings`, { headers: hdr() });
|
||||
linkSettings = await res.json().catch(() => ({}));
|
||||
}
|
||||
return linkSettings;
|
||||
}
|
||||
|
||||
function linkUrl(token) { return `${location.origin}/s/${token}`; }
|
||||
|
||||
async function loadLinks() {
|
||||
await loadLinkSettings();
|
||||
const res = await fetch(`${API}/api/links`, { headers: hdr() });
|
||||
links = await res.json();
|
||||
const el = document.getElementById('linksList');
|
||||
if (!links.length) { el.innerHTML = '<div class="empty">Ссылок пока нет. Создайте её в Журнале кнопкой «Ссылка».</div>'; return; }
|
||||
el.innerHTML = links.map(l => `
|
||||
el.innerHTML = links.map(l => {
|
||||
const namesHidden = l.show_student_names != null ? !l.show_student_names : linkSettings.share_show_student_names === 'false';
|
||||
return `
|
||||
<div class="item">
|
||||
<div style="min-width:0">
|
||||
<div class="name">${esc(l.name)}</div>
|
||||
@@ -19,7 +31,7 @@ async function loadLinks() {
|
||||
${l.student_name ? `${l.group_name ? ' · ' : ''}Ученик: <b>${esc(l.student_name)}</b>` : ''}
|
||||
${l.date_from || l.date_to ? ` · период: <b>${l.date_from ? l.date_from.split('-').reverse().join('.') : '…'}</b> — <b>${l.date_to ? l.date_to.split('-').reverse().join('.') : '…'}</b>` : ' · весь период'}
|
||||
${l.expires_at ? ` · истекает ${new Date(l.expires_at).toLocaleDateString('ru')}` : ''}
|
||||
${l.anonymize_names ? ` · <span style="color:#f59e0b">анонимизировано</span>` : ''}
|
||||
${namesHidden ? ` · <span style="color:#f59e0b">имена скрыты</span>` : ''}
|
||||
${l.access_password_hash ? ` · <span style="color:#2563eb">🔒 пароль</span>` : ''}
|
||||
· создана ${new Date(l.created_at).toLocaleString('ru')}
|
||||
</div>
|
||||
@@ -31,7 +43,7 @@ async function loadLinks() {
|
||||
<button class="btn-link" style="color:var(--muted)" data-del-link="${l.id}" title="Удалить">×</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`;}).join('');
|
||||
}
|
||||
|
||||
function fmtDate(d) { const [y, m, day] = (d || '').split('-'); return d ? `${day}.${m}.${y}` : ''; }
|
||||
@@ -43,10 +55,15 @@ async function openLinkModal() {
|
||||
document.getElementById('linkDateFrom').value = '';
|
||||
document.getElementById('linkDateTo').value = '';
|
||||
document.getElementById('linkExpiresDays').value = '7';
|
||||
document.getElementById('linkAnonymizeNames').checked = false;
|
||||
document.getElementById('linkShowNames').checked = false;
|
||||
document.getElementById('linkPassword').value = '';
|
||||
document.getElementById('linkMessage').value = '';
|
||||
document.getElementById('linkUrl').value = '';
|
||||
const s = await loadLinkSettings();
|
||||
document.getElementById('linkShowNames').checked = s.share_show_student_names !== 'false';
|
||||
document.getElementById('linkShowMessage').checked = s.share_show_student_message === 'true';
|
||||
document.getElementById('linkShowDate').checked = s.share_show_entry_date === 'true';
|
||||
document.getElementById('linkShowPhotos').checked = s.share_show_group_photos !== 'false';
|
||||
if (!groups.length) {
|
||||
const res = await fetch(`${API}/api/groups`, { headers: hdr() });
|
||||
groups = await res.json();
|
||||
@@ -72,10 +89,15 @@ async function openLinkEdit(id) {
|
||||
document.getElementById('linkDateFrom').value = l.date_from || '';
|
||||
document.getElementById('linkDateTo').value = l.date_to || '';
|
||||
document.getElementById('linkExpiresDays').value = l.expires_at ? Math.ceil((new Date(l.expires_at) - new Date()) / (1000 * 60 * 60 * 24)) : '7';
|
||||
document.getElementById('linkAnonymizeNames').checked = l.anonymize_names || false;
|
||||
document.getElementById('linkShowNames').checked = l.show_student_names != null ? !!l.show_student_names : true;
|
||||
document.getElementById('linkPassword').value = '';
|
||||
document.getElementById('linkMessage').value = l.message || '';
|
||||
document.getElementById('linkUrl').value = l.link_url || '';
|
||||
const s = await loadLinkSettings();
|
||||
document.getElementById('linkShowNames').checked = l.show_student_names != null ? !!l.show_student_names : s.share_show_student_names !== 'false';
|
||||
document.getElementById('linkShowMessage').checked = l.show_student_message != null ? !!l.show_student_message : s.share_show_student_message === 'true';
|
||||
document.getElementById('linkShowDate').checked = l.show_entry_date != null ? !!l.show_entry_date : s.share_show_entry_date === 'true';
|
||||
document.getElementById('linkShowPhotos').checked = l.show_group_photos != null ? !!l.show_group_photos : s.share_show_group_photos !== 'false';
|
||||
if (!groups.length) {
|
||||
const res = await fetch(`${API}/api/groups`, { headers: hdr() });
|
||||
groups = await res.json();
|
||||
@@ -106,14 +128,14 @@ async function saveLink() {
|
||||
const date_from = document.getElementById('linkDateFrom').value;
|
||||
const date_to = document.getElementById('linkDateTo').value;
|
||||
const expires_days = parseInt(document.getElementById('linkExpiresDays').value, 10);
|
||||
const anonymize_names = document.getElementById('linkAnonymizeNames').checked;
|
||||
const show_student_names = document.getElementById('linkShowNames').checked;
|
||||
const access_password = document.getElementById('linkPassword').value;
|
||||
const message = document.getElementById('linkMessage').value.trim();
|
||||
const link_url = document.getElementById('linkUrl').value.trim();
|
||||
if (!name) { alert('Введите название'); return; }
|
||||
if (expires_days < 1 || expires_days > 365) { alert('Дней истечения: 1-365'); return; }
|
||||
const expires_at = new Date(Date.now() + expires_days * 24 * 60 * 60 * 1000).toISOString();
|
||||
const body = JSON.stringify({ name, group_id: group_id || null, student_name: student_name || null, date_from: date_from || null, date_to: date_to || null, anonymize_names, expires_at, access_password: access_password || null, message: message || null, link_url: link_url || null });
|
||||
const body = JSON.stringify({ name, group_id: group_id || null, student_name: student_name || null, date_from: date_from || null, date_to: date_to || null, show_student_names, expires_at, access_password: access_password || null, message: message || null, link_url: link_url || null, show_student_message: document.getElementById('linkShowMessage').checked, show_entry_date: document.getElementById('linkShowDate').checked, show_group_photos: document.getElementById('linkShowPhotos').checked });
|
||||
const res = await fetch(editingId ? `${API}/api/links/${editingId}` : `${API}/api/links`, {
|
||||
method: editingId ? 'PUT' : 'POST',
|
||||
headers: hdrJson(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const DIRTY_FIELDS = ['spamInterval', 'footerLeft', 'footerRight', 'aiPrompt', 'aiAutoCheck'];
|
||||
const DIRTY_FIELDS = ['spamInterval', 'footerLeft', 'footerRight', 'aiPrompt', 'aiAutoCheck', 'shareShowMessage', 'shareShowDate', 'shareShowNames', 'shareShowPhotos'];
|
||||
|
||||
function fieldValue(el) {
|
||||
return el.type === 'checkbox' ? String(el.checked) : el.value;
|
||||
@@ -47,6 +47,10 @@ async function loadSettings() {
|
||||
document.getElementById('footerRight').value = s.footer_right || '';
|
||||
document.getElementById('aiPrompt').value = s.ai_prompt || '';
|
||||
document.getElementById('aiAutoCheck').checked = s.ai_autocheck_enabled !== 'false';
|
||||
document.getElementById('shareShowMessage').checked = s.share_show_student_message === 'true';
|
||||
document.getElementById('shareShowDate').checked = s.share_show_entry_date === 'true';
|
||||
document.getElementById('shareShowNames').checked = s.share_show_student_names !== 'false';
|
||||
document.getElementById('shareShowPhotos').checked = s.share_show_group_photos !== 'false';
|
||||
snapshotDirty();
|
||||
loadAiQueue();
|
||||
if (stRes.ok) {
|
||||
@@ -158,6 +162,10 @@ async function saveSettings() {
|
||||
footer_right: document.getElementById('footerRight').value.trim(),
|
||||
ai_prompt: document.getElementById('aiPrompt').value.trim(),
|
||||
ai_autocheck_enabled: document.getElementById('aiAutoCheck').checked ? 'true' : 'false',
|
||||
share_show_student_message: document.getElementById('shareShowMessage').checked ? 'true' : 'false',
|
||||
share_show_entry_date: document.getElementById('shareShowDate').checked ? 'true' : 'false',
|
||||
share_show_student_names: document.getElementById('shareShowNames').checked ? 'true' : 'false',
|
||||
share_show_group_photos: document.getElementById('shareShowPhotos').checked ? 'true' : 'false',
|
||||
} })
|
||||
});
|
||||
if (res.ok) {
|
||||
|
||||
+87
-5
@@ -2,6 +2,81 @@ function esc(s){return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').
|
||||
function fmt(d){return d?d.split('-').reverse().join('.'):'весь период'}
|
||||
function showImg(src){document.getElementById('lightboxImg').src=src;document.getElementById('lightbox').classList.add('open')}
|
||||
function isImageFile(name){return /\.(jpe?g|png|gif|webp|bmp|avif|svg|ico)$/i.test(name||'')}
|
||||
|
||||
let countdownInterval = null;
|
||||
let countdownTarget = null;
|
||||
let countdownScrollHandler = null;
|
||||
|
||||
function formatRemaining(ms) {
|
||||
if (ms <= 0) return null;
|
||||
const totalSec = Math.floor(ms / 1000);
|
||||
const days = Math.floor(totalSec / 86400);
|
||||
const hours = Math.floor((totalSec % 86400) / 3600);
|
||||
const minutes = Math.floor((totalSec % 3600) / 60);
|
||||
const seconds = totalSec % 60;
|
||||
|
||||
const parts = [];
|
||||
if (days > 0) parts.push(`${days} ${declension(days, 'день', 'дня', 'дней')}`);
|
||||
if (hours > 0) parts.push(`${hours} ${declension(hours, 'час', 'часа', 'часов')}`);
|
||||
if (minutes > 0) parts.push(`${minutes} ${declension(minutes, 'минута', 'минуты', 'минут')}`);
|
||||
if (days === 0) parts.push(`${seconds} ${declension(seconds, 'секунда', 'секунды', 'секунд')}`);
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
function declension(n, one, few, many) {
|
||||
const mod10 = n % 10;
|
||||
const mod100 = n % 100;
|
||||
if (mod100 >= 11 && mod100 <= 19) return many;
|
||||
if (mod10 === 1) return one;
|
||||
if (mod10 >= 2 && mod10 <= 4) return few;
|
||||
return many;
|
||||
}
|
||||
|
||||
function shouldShowCountdown() {
|
||||
const hero = document.querySelector('.hero');
|
||||
if (!hero) return true;
|
||||
return window.scrollY > hero.offsetHeight - 80;
|
||||
}
|
||||
|
||||
function syncCountdownVisibility() {
|
||||
const el = document.getElementById('countdown');
|
||||
if (!countdownTarget) { el.classList.remove('visible'); return; }
|
||||
if (countdownTarget - Date.now() <= 0) { el.classList.remove('visible'); return; }
|
||||
if (shouldShowCountdown()) el.classList.add('visible');
|
||||
else el.classList.remove('visible');
|
||||
}
|
||||
|
||||
function updateCountdown() {
|
||||
const el = document.getElementById('countdown');
|
||||
if (!countdownTarget) { el.classList.remove('visible'); return; }
|
||||
const now = Date.now();
|
||||
const remaining = countdownTarget - now;
|
||||
if (remaining <= 0) {
|
||||
el.classList.remove('visible');
|
||||
if (countdownInterval) { clearInterval(countdownInterval); countdownInterval = null; }
|
||||
return;
|
||||
}
|
||||
const text = formatRemaining(remaining);
|
||||
if (!text) { el.classList.remove('visible'); return; }
|
||||
el.classList.remove('warn', 'urgent');
|
||||
if (remaining < 3600000) el.classList.add('urgent');
|
||||
else if (remaining < 86400000) el.classList.add('warn');
|
||||
el.querySelector('.cd-text').innerHTML = `Ссылка доступна ещё <strong>${text}</strong>`;
|
||||
syncCountdownVisibility();
|
||||
}
|
||||
|
||||
function startCountdown(expiresAt) {
|
||||
if (countdownInterval) { clearInterval(countdownInterval); countdownInterval = null; }
|
||||
if (countdownScrollHandler) { window.removeEventListener('scroll', countdownScrollHandler); countdownScrollHandler = null; }
|
||||
const el = document.getElementById('countdown');
|
||||
if (!expiresAt) { el.classList.remove('visible'); return; }
|
||||
countdownTarget = new Date(expiresAt).getTime();
|
||||
countdownScrollHandler = syncCountdownVisibility;
|
||||
window.addEventListener('scroll', countdownScrollHandler, { passive: true });
|
||||
updateCountdown();
|
||||
countdownInterval = setInterval(updateCountdown, 1000);
|
||||
}
|
||||
|
||||
function fileType(name){
|
||||
if(/\.(jpe?g|png|gif|webp|bmp|avif|svg|ico)$/i.test(name||'')) return 'image';
|
||||
if(/\.(mp3|wav|ogg|flac|m4a|aac|opus)$/i.test(name||'')) return 'music';
|
||||
@@ -98,6 +173,9 @@ function showError(msg) {
|
||||
document.getElementById('photosBlock').style.display = 'none';
|
||||
document.getElementById('shareMessage').style.display = 'none';
|
||||
document.getElementById('shareLinkBlock').style.display = 'none';
|
||||
document.getElementById('countdown').classList.remove('visible');
|
||||
if (countdownInterval) { clearInterval(countdownInterval); countdownInterval = null; }
|
||||
if (countdownScrollHandler) { window.removeEventListener('scroll', countdownScrollHandler); countdownScrollHandler = null; }
|
||||
}
|
||||
|
||||
function renderShare(data, password) {
|
||||
@@ -109,6 +187,8 @@ function renderShare(data, password) {
|
||||
if (data.date_from || data.date_to) chips.push(`Период: <b>${fmt(data.date_from)}</b> — <b>${fmt(data.date_to)}</b>`);
|
||||
document.getElementById('chips').innerHTML = chips.map(c => `<span class="chip">${c}</span>`).join('');
|
||||
|
||||
startCountdown(data.expires_at);
|
||||
|
||||
const msgEl = document.getElementById('shareMessage');
|
||||
if (data.message) {
|
||||
msgEl.textContent = data.message;
|
||||
@@ -133,7 +213,7 @@ function renderShare(data, password) {
|
||||
}
|
||||
|
||||
const pb = document.getElementById('photosBlock');
|
||||
if (data.photos && data.photos.length) {
|
||||
if (data.show_group_photos && data.photos && data.photos.length) {
|
||||
document.getElementById('photosGrid').innerHTML = data.photos.map(p => `
|
||||
<a class="gphoto" href="#" data-img="${esc(p.photo_path)}">
|
||||
<img src="${p.photo_path}" alt="${esc(p.caption || '')}" loading="lazy">
|
||||
@@ -150,7 +230,9 @@ function renderShare(data, password) {
|
||||
grid.innerHTML = '<div class="empty"><div class="big">🎈</div>Записей пока нет</div>';
|
||||
return;
|
||||
}
|
||||
const isAnonymized = data.anonymize_names;
|
||||
const isAnonymized = !data.show_student_names;
|
||||
const showStudentMessage = !!data.show_student_message;
|
||||
const showEntryDate = !!data.show_entry_date;
|
||||
grid.innerHTML = data.entries.map(e => `
|
||||
<div class="card">
|
||||
<div class="photo">
|
||||
@@ -161,10 +243,10 @@ function renderShare(data, password) {
|
||||
<div class="body">
|
||||
${!isAnonymized ? `<div class="name">${esc(e.student_name)}</div>` : ''}
|
||||
${!isAnonymized && e.group_name ? `<div><span class="group">${esc(e.group_name)}</span></div>` : ''}
|
||||
<div class="desc">${esc(e.description)}</div>
|
||||
${showStudentMessage && e.description ? `<div class="desc">${esc(e.description)}</div>` : ''}
|
||||
${filesHTML(e.files, password, currentToken)}
|
||||
</div>
|
||||
<div class="foot">${new Date(e.created_at).toLocaleString('ru',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'})}</div>
|
||||
${showEntryDate ? `<div class="foot">${new Date(e.created_at).toLocaleString('ru',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'})}</div>` : ''}
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
@@ -179,4 +261,4 @@ document.addEventListener('click', e => {
|
||||
|
||||
(async()=>{
|
||||
await loadShare();
|
||||
})();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user