'use strict';
/* Fotos de familia — cliente. Habla con /api (Cloudflare Pages Functions). */
/* ───────────────── Estado y sesión ───────────────── */
var state = {
role: null, token: null,
view: 'recientes', album: null,
favorites: new Set(),
selectMode: false, selected: new Set(),
viewerList: [], viewerIndex: 0
};
var PHOTOS = [];
var ALBUM_NAMES = [];
function loadSession(){
var t = localStorage.getItem('ff_token') || sessionStorage.getItem('ff_token');
var r = localStorage.getItem('ff_role') || sessionStorage.getItem('ff_role');
if(t && r){ state.token = t; state.role = r; }
try{ state.favorites = new Set(JSON.parse(localStorage.getItem('ff_favs') || '[]')); }catch(e){}
}
function saveSession(remember){
var store = remember ? localStorage : sessionStorage;
store.setItem('ff_token', state.token);
store.setItem('ff_role', state.role);
}
function clearSession(){
['ff_token','ff_role'].forEach(function(k){ localStorage.removeItem(k); sessionStorage.removeItem(k); });
state.token = null; state.role = null;
}
function saveFavs(){ localStorage.setItem('ff_favs', JSON.stringify(Array.from(state.favorites))); }
/* ───────────────── API ───────────────── */
function api(path, opts){
opts = opts || {};
opts.headers = Object.assign({}, opts.headers, { authorization: 'Bearer ' + state.token });
return fetch('/api/' + path, opts).then(function(r){
if(r.status === 401){ doLogout(); throw new Error('Sesión caducada'); }
return r.json().then(function(j){
if(!r.ok) throw new Error(j.error || 'Error de la aplicación');
return j;
});
});
}
function mediaUrl(kind, id, extra){
return '/api/' + kind + '/' + id + '?t=' + encodeURIComponent(state.token) + (extra || '');
}
function applyIndex(index){
ALBUM_NAMES = index.albums || [];
PHOTOS = (index.items || []).map(function(m){
return {
id: m.id, name: m.name, album: m.album, video: m.video,
date: new Date(m.date),
dur: m.dur ? fmtDur(m.dur) : null,
src: mediaUrl('thumb', m.id, '&s=480'),
big: mediaUrl('thumb', m.id, '&s=1600')
};
});
}
function fmtDur(s){ var m = Math.floor(s/60); return m + ':' + String(s % 60).padStart(2, '0'); }
/* ───────────────── Utilidades ───────────────── */
var MESES = ['enero','febrero','marzo','abril','mayo','junio','julio','agosto','septiembre','octubre','noviembre','diciembre'];
function $(s){ return document.querySelector(s); }
function dayLabel(date){
var hoy = new Date(); hoy.setHours(0,0,0,0);
var dd = new Date(date); dd.setHours(0,0,0,0);
var diff = Math.round((hoy - dd)/864e5);
if(diff === 0) return 'Hoy';
if(diff === 1) return 'Ayer';
return dd.getDate() + ' de ' + MESES[dd.getMonth()] + (dd.getFullYear() !== hoy.getFullYear() ? ' de ' + dd.getFullYear() : '');
}
var toastTimer = null;
function toast(msg){
var t = $('#toast'); t.textContent = msg; t.classList.remove('hidden');
if(toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(function(){ t.classList.add('hidden'); }, 3000);
}
function svgHeart(filled){
return '';
}
/* ───────────────── Cuadrículas ───────────────── */
function photoTile(p){
var b = document.createElement('button');
b.className = 'ph' + (state.selected.has(p.id) ? ' selected' : '');
b.setAttribute('aria-label', (p.video?'Vídeo':'Foto') + ' del ' + dayLabel(p.date));
var inner = '
';
if(state.favorites.has(p.id)) inner += ''+svgHeart(true)+'';
if(p.video) inner += ''+(p.dur||'')+'';
inner += '';
b.innerHTML = inner;
b.addEventListener('click', function(){ onTileClick(p); });
return b;
}
function renderGroupedGrid(container, photos){
var lastLabel = null, grid = null;
photos.forEach(function(p){
var lbl = dayLabel(p.date);
if(lbl !== lastLabel){
var h = document.createElement('div'); h.className='day-label'; h.textContent = lbl;
container.appendChild(h);
grid = document.createElement('div'); grid.className='grid';
container.appendChild(grid);
lastLabel = lbl;
}
grid.appendChild(photoTile(p));
});
}
/* ───────────────── Vistas ───────────────── */
function setTitle(t, hint){ $('#page-title').textContent = t; $('#page-hint').textContent = hint || ''; }
function viewRecientes(c){
var hoyCount = PHOTOS.filter(function(p){ return dayLabel(p.date)==='Hoy'; }).length;
setTitle('Hola, familia', hoyCount ? hoyCount + ' fotos nuevas hoy' : 'Al día con el álbum');
if(!PHOTOS.length){
var e = document.createElement('div'); e.className='empty';
e.innerHTML = '
El álbum está vacío.
Toca + para añadir las primeras fotos.
';
c.appendChild(e); return;
}
renderGroupedGrid(c, PHOTOS);
}
function viewAlbumes(c){
if(state.album){
var fotos = PHOTOS.filter(function(p){ return p.album === state.album; });
setTitle(state.album, fotos.length + ' elementos');
var back = document.createElement('button'); back.className='back-row';
back.innerHTML = ' Todos los álbumes';
back.addEventListener('click', function(){ state.album = null; render(); });
c.appendChild(back);
renderGroupedGrid(c, fotos);
return;
}
setTitle('Álbumes', 'Tal como están organizados en Drive');
var wrap = document.createElement('div'); wrap.className='album-grid';
ALBUM_NAMES.forEach(function(name){
var fotos = PHOTOS.filter(function(p){ return p.album===name; });
if(!fotos.length) return;
var card = document.createElement('button'); card.className='album-card';
card.innerHTML = '
';
card.querySelector('.name').textContent = name;
card.addEventListener('click', function(){ state.album = name; render(); });
wrap.appendChild(card);
});
c.appendChild(wrap);
}
function viewVideos(c){
var vids = PHOTOS.filter(function(p){ return p.video; });
setTitle('Vídeos', vids.length + ' vídeos · solo cargan al pulsar play');
renderGroupedGrid(c, vids);
}
function viewFavoritos(c){
var favs = PHOTOS.filter(function(p){ return state.favorites.has(p.id); });
setTitle('Favoritos', favs.length ? favs.length + ' marcados en este dispositivo' : '');
if(!favs.length){
var e = document.createElement('div'); e.className='empty';
e.innerHTML = '♡
Aún no hay favoritos.
Abre una foto y toca el corazón
para guardarla aquí.
';
c.appendChild(e);
return;
}
renderGroupedGrid(c, favs);
}
function render(){
var c = $('#content'); c.innerHTML='';
var views = { recientes: viewRecientes, albumes: viewAlbumes, videos: viewVideos, favoritos: viewFavoritos };
views[state.view](c);
document.querySelectorAll('#mainnav button').forEach(function(b){
b.classList.toggle('active', b.getAttribute('data-view')===state.view);
});
$('#btn-select').classList.toggle('active', state.selectMode);
}
/* ───────────────── Interacción ───────────────── */
function currentList(){
if(state.view==='videos') return PHOTOS.filter(function(p){ return p.video; });
if(state.view==='favoritos') return PHOTOS.filter(function(p){ return state.favorites.has(p.id); });
if(state.view==='albumes' && state.album) return PHOTOS.filter(function(p){ return p.album===state.album; });
return PHOTOS;
}
function onTileClick(p){
if(state.selectMode){
if(state.selected.has(p.id)) state.selected.delete(p.id); else state.selected.add(p.id);
updateSelBar(); render();
return;
}
openViewer(currentList(), p);
}
/* ───────────────── Visor ───────────────── */
function openViewer(list, photo){
state.viewerList = list.slice();
state.viewerIndex = state.viewerList.indexOf(photo);
$('#viewer').classList.remove('hidden');
document.body.style.overflow='hidden';
updateViewer();
}
function closeViewer(){
$('#viewer').classList.add('hidden');
var v = $('#v-video'); v.pause(); v.removeAttribute('src'); v.load();
document.body.style.overflow='';
render();
}
function updateViewer(){
var p = state.viewerList[state.viewerIndex];
if(!p) return closeViewer();
var img = $('#v-img'), vid = $('#v-video');
vid.pause(); vid.removeAttribute('src'); vid.load();
if(p.video){
img.classList.add('hidden'); vid.classList.remove('hidden');
vid.poster = p.big;
vid.src = mediaUrl('play', p.id);
$('#v-playwrap').classList.add('hidden');
} else {
vid.classList.add('hidden'); img.classList.remove('hidden');
img.src = p.big;
img.alt = 'Foto del ' + dayLabel(p.date);
$('#v-playwrap').classList.add('hidden');
}
$('#v-date').textContent = dayLabel(p.date);
$('#v-album').textContent = p.album;
var on = state.favorites.has(p.id);
$('#v-fav').classList.toggle('fav-on', on);
$('#v-fav').setAttribute('aria-pressed', String(on));
$('#v-fav-ic').setAttribute('fill', on ? 'currentColor' : 'none');
$('#v-fav-txt').textContent = on ? 'Favorito ✓' : 'Favorito';
$('#v-prev').style.visibility = state.viewerIndex>0 ? 'visible':'hidden';
$('#v-next').style.visibility = state.viewerIndex=0 && n= state.viewerList.length) state.viewerIndex = state.viewerList.length - 1;
updateViewer();
});
});
var selDeleteArmed = false;
$('#sel-delete').addEventListener('click', function(){
if(state.role !== 'admin') return;
var n = state.selected.size;
if(!n){ toast('No hay nada seleccionado todavía'); return; }
if(!selDeleteArmed){
selDeleteArmed = true;
$('#sel-delete').classList.add('arm');
$('#sel-delete').textContent = '¿Eliminar ' + n + '?';
return;
}
apiDelete(Array.from(state.selected), function(res){
toast(res.borradas + (res.borradas===1?' elemento eliminado':' elementos eliminados'));
exitSelect();
});
});
/* ───────────────── Descarga múltiple ───────────────── */
$('#sel-download').addEventListener('click', function(){
var ids = Array.from(state.selected);
if(!ids.length){ toast('No hay nada seleccionado todavía'); return; }
toast('Descargando ' + ids.length + ' originales…');
ids.forEach(function(id, i){
setTimeout(function(){
var p = PHOTOS.find(function(x){ return x.id===id; });
var a = document.createElement('a');
a.href = mediaUrl('file', id);
a.download = (p && p.name) || 'foto';
document.body.appendChild(a); a.click(); a.remove();
}, i * 600);
});
exitSelect();
});
/* ───────────────── Login y sesión ───────────────── */
function showApp(){
$('#login').classList.add('hidden');
$('#app').classList.remove('hidden');
setTitle('Hola, familia', 'Cargando el álbum…');
api('index').then(function(index){
applyIndex(index);
render();
}).catch(function(e){ toast(e.message); });
}
function doLogin(){
var v = $('#key-input').value.trim();
if(!v){ $('#login-error').textContent = 'Escribe la clave de la familia.'; return; }
var btn = $('#btn-login'); btn.disabled = true; btn.textContent = 'Entrando…';
fetch('/api/login', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({ key: v }) })
.then(function(r){ return r.json().then(function(j){ return { ok:r.ok, j:j }; }); })
.then(function(res){
if(!res.ok){ $('#login-error').textContent = 'Esa clave no es. Pregunta a quien lleva el álbum.'; return; }
state.token = res.j.token; state.role = res.j.role;
saveSession($('#remember').checked);
$('#login-error').textContent='';
showApp();
toast(state.role==='admin' ? 'Hola de nuevo, administrador' : 'Clave guardada en este dispositivo');
})
.catch(function(){ $('#login-error').textContent = 'No hay conexión con la aplicación. Inténtalo de nuevo.'; })
.finally(function(){ btn.disabled = false; btn.textContent = 'Entrar'; });
}
function doLogout(){
clearSession();
state.album = null; state.view='recientes';
state.selectMode = false; state.selected.clear();
$('#settings').classList.add('hidden');
$('#viewer').classList.add('hidden');
$('#selbar').classList.add('hidden');
$('#app').classList.add('hidden');
$('#login').classList.remove('hidden');
$('#key-input').value='';
document.body.style.overflow='';
}
/* ───────────────── Eventos generales ───────────────── */
$('#btn-login').addEventListener('click', doLogin);
$('#key-input').addEventListener('keydown', function(e){ if(e.key==='Enter') doLogin(); });
document.querySelectorAll('#mainnav button').forEach(function(b){
b.addEventListener('click', function(){
state.view = b.getAttribute('data-view');
state.album = null;
exitSelect();
});
});
$('#btn-select').addEventListener('click', function(){
state.selectMode = !state.selectMode;
if(!state.selectMode) state.selected.clear();
updateSelBar(); render();
if(state.selectMode) toast('Toca las fotos que quieras descargar juntas');
});
$('#sel-cancel').addEventListener('click', exitSelect);
$('#v-close').addEventListener('click', closeViewer);
$('#v-prev').addEventListener('click', function(){ viewerStep(-1); });
$('#v-next').addEventListener('click', function(){ viewerStep(1); });
$('#v-fav').addEventListener('click', toggleFav);
$('#v-download').addEventListener('click', downloadCurrent);
document.addEventListener('keydown', function(e){
if($('#viewer').classList.contains('hidden')) return;
if(e.key==='ArrowLeft') viewerStep(-1);
if(e.key==='ArrowRight') viewerStep(1);
if(e.key==='Escape') closeViewer();
});
$('#btn-settings').addEventListener('click', function(){
$('#row-key').classList.toggle('hidden', state.role !== 'admin');
$('#row-key-locked').classList.toggle('hidden', state.role === 'admin');
$('#settings').classList.remove('hidden');
});
$('#settings-close').addEventListener('click', function(){ $('#settings').classList.add('hidden'); });
$('#settings').addEventListener('click', function(e){ if(e.target === $('#settings')) $('#settings').classList.add('hidden'); });
$('#save-key').addEventListener('click', function(){
var v = $('#new-key').value.trim();
if(v.length < 4){ toast('La clave necesita al menos 4 caracteres'); return; }
api('change-key', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({ newKey: v }) })
.then(function(){ $('#new-key').value=''; toast('Clave cambiada. Avisa a la familia de la nueva.'); })
.catch(function(e){ toast(e.message); });
});
$('#clear-favs').addEventListener('click', function(){
state.favorites.clear(); saveFavs(); toast('Favoritos vaciados en este dispositivo'); render();
});
$('#install-app').addEventListener('click', function(){
toast('Usa el menú del navegador: «Añadir a pantalla de inicio» o «Instalar aplicación»');
});
$('#logout').addEventListener('click', doLogout);
/* ───────────────── Arranque ───────────────── */
loadSession();
if(state.token){ showApp(); }