Compare commits

..

2 Commits

Author SHA1 Message Date
rafa b3a2ae682b fix: style author archives in dark mode
Co-authored-by: rafa <rcalvotorrejon@gmail.com>
Signed-off-by: rafa <rcalvotorrejon@gmail.com>
2026-08-12 07:05:57 -04:00
rafa 825434a0b7 fix: preserve dark menu background in articles
Co-authored-by: rafa <rcalvotorrejon@gmail.com>
Signed-off-by: rafa <rcalvotorrejon@gmail.com>
2026-08-12 06:11:06 -04:00
3 changed files with 39 additions and 73 deletions
+23
View File
@@ -98,6 +98,13 @@ add_action('wp_head', function () {
background-color: #0d0d0d !important;
color: #ececec !important;
}
/* En artículos, fea-ui.php fija --wp--preset--color--base en el body
para el modo claro. El contenedor desplegable del menú hereda esa
variable, así que hay que restaurarla aquí en oscuro. */
html[data-fea-theme="dark"] body.single-post {
--wp--preset--color--base: #0d0d0d;
--wp--preset--color--contrast: #ececec;
}
html[data-fea-theme="dark"] a { color: <?php echo esc_html($accent_dark); ?>; }
html[data-fea-theme="dark"] a:focus-visible,
html[data-fea-theme="dark"] button:focus-visible {
@@ -161,6 +168,22 @@ add_action('wp_head', function () {
}
html[data-fea-theme="dark"] .fea-card-title a { color: #ececec; }
/* Archivos (categoría, búsqueda y autor) — fea-homepage.php fija aquí
fondos y texto claros que no heredan del tema. */
html[data-fea-theme="dark"] .wp-block-query-title {
color: #ececec !important;
}
html[data-fea-theme="dark"] .fea-archive-card {
background: #161311;
border-color: #2a2320;
}
html[data-fea-theme="dark"] .fea-archive-title a {
color: #ececec;
}
html[data-fea-theme="dark"] .fea-archive-excerpt {
color: #b8afa8;
}
/* Selector de idioma (fea-homepage.php) */
html[data-fea-theme="dark"] #fea-lang-btn { border-color: rgba(255,255,255,0.3); }
html[data-fea-theme="dark"] #fea-lang-btn:hover,
+12 -62
View File
@@ -13,23 +13,11 @@ function fea_audio_player_html(): string {
$url = get_post_meta($id, 'fea_audio_url', true);
if (!$url) return '';
$voice = get_post_meta($id, 'fea_audio_voice', true) ?: 'NicoFeadulta2026';
$sha256 = get_post_meta($id, 'fea_audio_sha256', true);
$label = '<span class="fea-audio-label">'
return '<div class="fea-audio">'
. '<span class="fea-audio-label">'
. '<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false">'
. '<path fill="currentColor" d="M3 10v4h4l5 5V5L7 10H3zm13.5 2a4.5 4.5 0 0 0-2.5-4.03v8.06A4.5 4.5 0 0 0 16.5 12zM14 3.23v2.06a7 7 0 0 1 0 13.42v2.06a9 9 0 0 0 0-17.54z"/>'
. '</svg> Escucha</span>';
// Para un asset con hash registrado, descarga y verifica el fichero completo
// antes de delegar playback al navegador. Evita que un stream Range mezclado
// por una caché reproduzca fragmentos de otro MP3.
if (preg_match('/^[a-f0-9]{64}$/i', $sha256)) {
return '<div class="fea-audio">' . $label
. '<button type="button" class="fea-audio-verified-play" data-fea-audio-verified-play'
. ' data-src="' . esc_url($url) . '" data-sha256="' . esc_attr(strtolower($sha256)) . '"'
. ' data-post-id="' . esc_attr($id) . '" data-voice="' . esc_attr($voice) . '">Reproducir audio</button>'
. '<audio controls preload="none" hidden data-fea-audio-track data-post-id="' . esc_attr($id) . '" data-voice="' . esc_attr($voice) . '"></audio>'
. '<span class="fea-audio-status" role="status" aria-live="polite"></span></div>';
}
return '<div class="fea-audio">' . $label
. '</svg> Escucha</span>'
. '<audio controls preload="none" src="' . esc_url($url) . '"'
. ' data-fea-audio-track data-post-id="' . esc_attr($id) . '" data-voice="' . esc_attr($voice) . '"></audio>'
. '</div>';
@@ -68,9 +56,6 @@ add_action('wp_head', function () {
.fea-audio-label{display:inline-flex;align-items:center;gap:.35rem;font-size:.78rem;
font-weight:600;color:#8b1a2e;white-space:nowrap;line-height:1}
.fea-audio audio{height:32px;width:230px;max-width:44vw}
.fea-audio-verified-play{border:1px solid #8b1a2e;border-radius:6px;background:#fff;color:#8b1a2e;padding:.35rem .6rem;font:inherit;font-size:.82rem;font-weight:600;cursor:pointer}
.fea-audio-verified-play:disabled{opacity:.65;cursor:wait}
.fea-audio-status{font-size:.76rem;color:#6b5c60}
@media(max-width:600px){
.fea-audio{margin-left:0;width:100%;margin-top:.5rem}
.fea-audio audio{flex:1 1 auto;width:auto;max-width:none}
@@ -88,52 +73,17 @@ add_action('wp_footer', function () {
if (!get_post_meta(get_queried_object_id(), 'fea_audio_url', true)) return;
?>
<script>
(function () {
function hex(buffer) {
return Array.from(new Uint8Array(buffer)).map(function (n) {
return n.toString(16).padStart(2, '0');
}).join('');
}
document.querySelectorAll('[data-fea-audio-verified-play]').forEach(function (button) {
button.addEventListener('click', async function () {
var wrap = button.closest('.fea-audio');
var audio = wrap.querySelector('audio[data-fea-audio-track]');
var status = wrap.querySelector('.fea-audio-status');
button.disabled = true;
status.textContent = 'Verificando audio…';
try {
// Se descarga el objeto entero una vez y se valida su hash. Sólo
// entonces se crea un blob local: el <audio> no hará Range contra
// CDN y jamás podrá mezclar fragmentos de otro MP3.
var response = await fetch(button.dataset.src, {cache: 'no-store'});
if (!response.ok) throw new Error('HTTP ' + response.status);
var bytes = await response.arrayBuffer();
var digest = hex(await crypto.subtle.digest('SHA-256', bytes));
if (digest !== button.dataset.sha256) throw new Error('La copia recibida no coincide con el audio verificado');
audio.src = URL.createObjectURL(new Blob([bytes], {type: 'audio/mpeg'}));
audio.hidden = false;
button.hidden = true;
status.textContent = '';
await audio.play();
} catch (error) {
status.textContent = 'No se pudo verificar el audio. Inténtalo de nuevo.';
button.disabled = false;
console.error('Fe Adulta audio verification failed:', error);
}
document.querySelectorAll('audio[data-fea-audio-track]').forEach(function (audio) {
var fired = false;
audio.addEventListener('play', function () {
if (fired || typeof gtag !== 'function') return;
fired = true;
gtag('event', 'audio_play', {
post_id: audio.dataset.postId,
voice: audio.dataset.voice
});
});
document.querySelectorAll('audio[data-fea-audio-track]').forEach(function (audio) {
var fired = false;
audio.addEventListener('play', function () {
if (fired || typeof gtag !== 'function') return;
fired = true;
gtag('event', 'audio_play', {
post_id: audio.dataset.postId,
voice: audio.dataset.voice
});
});
});
}());
});
</script>
<?php
});
+4 -11
View File
@@ -19,7 +19,6 @@ Rollback (despublica en prod lo que este script publicó):
from __future__ import annotations
import argparse
import hashlib
import json
import os
import subprocess
@@ -145,21 +144,16 @@ def prod_helper(subcmd: str, *args: str) -> str:
return _ssh_text(_remote_wrap(inner), timeout=60)
def prod_upload_mp3(post_id: int) -> str:
def prod_upload_mp3(post_id: int) -> None:
src = LOCAL_TTS_DIR / f"{post_id}.mp3"
data = src.read_bytes()
local_sha256 = hashlib.sha256(data).hexdigest()
remote_path = f"{PROD_UPLOADS_TTS}/{post_id}.mp3"
_ssh_upload_bytes(data, remote_path)
# Verificación de tamaño+contenido: un rc=0 de ssh no acredita que el asset
# sea el mismo. El hash se persiste como meta y permite playback verificado.
# Verificación de tamaño: no fiarse ciegamente del rc=0 de ssh. `wc -c` debe
# correr (y resolver la redirección) DENTRO del contenedor — ver _remote_wrap.
remote_size = int(_ssh_text(_remote_wrap(f"wc -c < {remote_path}")).strip())
if remote_size != len(data):
raise RuntimeError(f"tamaño no coincide tras subir #{post_id}: local={len(data)} remoto={remote_size}")
remote_sha256 = _ssh_text(_remote_wrap(f"sha256sum {remote_path} | cut -d' ' -f1")).strip()
if remote_sha256 != local_sha256:
raise RuntimeError(f"SHA-256 no coincide tras subir #{post_id}: local={local_sha256} remoto={remote_sha256}")
return local_sha256
def prod_remove_mp3(post_id: int) -> None:
@@ -191,9 +185,8 @@ def sync_one(post_id: int, state: dict, *, dry_run: bool) -> str:
return "PLAN: subiría mp3 + setaudio"
voice = local_meta(post_id, "fea_audio_voice") or "NicoFeadulta2026"
sha256 = prod_upload_mp3(post_id)
prod_upload_mp3(post_id)
prod_helper("setaudio", str(post_id), f"/wp-content/uploads/tts/{post_id}.mp3", voice)
prod_helper("setflag", str(post_id), "fea_audio_sha256", sha256)
if post_id not in state["synced"]:
state["synced"].append(post_id)
save_state(state)