/* global React, ReactDOM */
const { useState, useEffect } = React;
// Componentes y utilidades compartidas (definidos en common.jsx).
const {
Icon, useReveal, Navbar, Footer, useSiteTweaks, SiteTweaks,
SubscribeModal, Galeria
} = window;
// Software gratis: la lista sale de api/list.php (tabla softwares). Cada ficha
// trae en details las características, los requisitos y las capturas con
// título. Los programas no tienen enlace directo: al aceptar el aviso legal se
// pide un enlace temporal a api/sw_token.php (como MADeCa Mini), que cuenta la
// descarga. El manual se abre desde api/sw_manual.php, que también lo cuenta.
const SW_ERRORES = {
demasiadas: "Se han pedido demasiadas descargas desde su conexión. Inténtelo dentro de una hora.",
no_disponible: "La descarga no está disponible en este momento. Inténtelo más tarde.",
robot: "Su navegador no permite esta descarga. Pruebe con otro navegador."
};
const conV = v => (v ? (/^v/i.test(v) ? v : "v" + v) : "");
// Capturas de la ficha: las de details (con título y descripción) o, en fichas
// antiguas, la principal y las extra sin texto.
const capturasDe = item => {
const det = item.details && Array.isArray(item.details.capturas) ? item.details.capturas : [];
if (det.length) return det.filter(c => c && c.src);
const urls = [item.screenshot_url, ...(item.screenshots_extra_urls || [])].filter(Boolean);
return urls.map((src, i) => ({ src, t: `Captura ${i + 1}`, d: "" }));
};
/* ---------- Descarga: aviso legal + enlace temporal ---------- */
const SwDescarga = ({ item, onClose }) => {
const [accept, setAccept] = useState(false);
const [preparando, setPreparando] = useState(false);
const [error, setError] = useState(null);
const [iniciada, setIniciada] = useState(false);
const open = !!item;
useEffect(() => {
if (!open) return undefined;
setAccept(false); setError(null); setIniciada(false);
document.body.style.overflow = "hidden";
const esc = e => { if (e.key === "Escape") onClose(); };
window.addEventListener("keydown", esc);
return () => { window.removeEventListener("keydown", esc); document.body.style.overflow = ""; };
}, [item]);
const descargar = async () => {
if (!accept || preparando) return;
setPreparando(true); setError(null); setIniciada(false);
try {
const r = await fetch("api/sw_token.php", {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: item.slug, acepto: true })
});
const d = await r.json().catch(() => ({}));
if (!r.ok || !d.ok || !d.url) throw new Error(d.error || "servidor");
window.location.href = d.url;
setIniciada(true);
} catch (e) {
setError(SW_ERRORES[e.message] || "No se ha podido iniciar la descarga. Inténtelo de nuevo en unos minutos.");
} finally {
setPreparando(false);
}
};
return (
{item.title} es gratuito y se entrega tal cual, sin garantía de ningún tipo.
Antes de descargarlo, lea el aviso legal y las limitaciones de su manual
{item.manual_url
? <> (abrir el aviso legal).>
: "."}
Para contar las descargas se guarda la dirección IP y el país aproximado.
{error &&
{error}
}
{iniciada &&
La descarga ha comenzado. El enlace es personal y caduca en una hora.
);
};
// Análisis del programa en VirusTotal, buscado por su huella SHA-256.
const vtUrl = sha => `https://www.virustotal.com/gui/file/${encodeURIComponent(sha)}`;
// Programas que también se publican en GitHub (repositorio público con las descargas en Releases).
const GITHUB = {
maclipboard: "MAClipBoard", macolor: "MAColor", madisk: "MADisk",
maprocess: "MAProcess", mascrshot: "MAScrShot", maserv: "MAServ",
};
const githubUrl = item => GITHUB[item.slug] ? `https://github.com/MaSoft/${GITHUB[item.slug]}/releases/latest` : null;
// Resumen de una línea para el índice: la descripción hasta los dos puntos o
// el primer punto (todas empiezan diciendo qué hace el programa).
const resumenDe = item => {
const d = (item.description || "").trim();
const corte = d.search(/[:.]/);
return corte > 0 ? d.slice(0, corte) : d;
};
/* ---------- Índice: una tarjeta por programa ---------- */
const Indice = ({ items, onIr }) => (
);
/* ---------- Barra fija con los programas (marca en cuál estás) ----------
Misma subcabecera que las páginas de producto (MADeCa, MASys, MAPerit). */
const BarraProgramas = ({ items, activo, onIr }) => {
const caja = React.useRef(null);
// En pantallas estrechas la barra se desliza: se centra el programa activo.
useEffect(() => {
const nav = caja.current;
const el = nav && nav.querySelector(".is-active");
if (!el || nav.scrollWidth <= nav.clientWidth) return;
nav.scrollTo({ left: el.offsetLeft - (nav.clientWidth - el.offsetWidth) / 2, behavior: "smooth" });
}, [activo]);
return (
);
};
/* ---------- Sección de un programa ---------- */
const Programa = ({ item, num, total, onDownload }) => {
const det = item.details || {};
const caracteristicas = Array.isArray(det.caracteristicas) ? det.caracteristicas : [];
const requisitos = Array.isArray(det.requisitos) ? det.requisitos : [];
const capturas = capturasDe(item);
return (
);
};
/* ---------- Página Software gratis ---------- */
const SoftwareGratisPage = () => {
const [t, setTweak] = useSiteTweaks();
const [items, setItems] = useState(null); // null = cargando
const [fallo, setFallo] = useState(false);
const [subOpen, setSubOpen] = useState(false);
const [dlItem, setDlItem] = useState(null);
const [contactInfo, setContactInfo] = useState({});
const [activo, setActivo] = useState(null);
useReveal(items ? items.length : 0);
const cargar = () => {
setFallo(false); setItems(null);
fetch("api/list.php")
.then(r => r.json())
.then(d => { if (d && d.ok && Array.isArray(d.items)) setItems(d.items); else throw new Error(); })
.catch(() => { setItems([]); setFallo(true); });
};
useEffect(() => {
cargar();
fetch("api/settings.php?action=get_public")
.then(r => r.json())
.then(d => { if (d.ok && d.settings) setContactInfo(d.settings); })
.catch(() => {});
}, []);
// Programa activo en la barra: el último cuya sección ya ha pasado bajo la
// barra fija. Ninguno mientras se está en la cabecera o en el índice.
useEffect(() => {
if (!items || !items.length) return undefined;
const calcular = () => {
let actual = null;
document.querySelectorAll("[data-sg]").forEach(s => {
if (s.getBoundingClientRect().top <= 160) actual = s.dataset.sg;
});
setActivo(actual);
};
calcular();
window.addEventListener("scroll", calcular, { passive: true });
window.addEventListener("resize", calcular);
return () => { window.removeEventListener("scroll", calcular); window.removeEventListener("resize", calcular); };
}, [items]);
// Al llegar con #programa en la dirección, ir a su sección cuando ya existe.
useEffect(() => {
const h = decodeURIComponent((window.location.hash || "").slice(1));
if (items && h && document.getElementById(h)) irA(h, "instant");
}, [items]);
// 128 px = menú principal + barra de programas. Si al terminar el desplazamiento
// algo de encima ha cambiado de alto (imágenes que acaban de cargar), se reajusta.
const irA = (slug, behavior = "smooth") => {
const el = document.getElementById(slug);
if (!el) return;
const ir = b => window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - 128, behavior: b });
ir(behavior);
[700, 1400].forEach(ms => setTimeout(() => {
if (Math.abs(el.getBoundingClientRect().top - 128) > 4) ir("instant");
}, ms));
if (window.history.replaceState) window.history.replaceState(null, "", "#" + slug);
};
const onScroll = (id) => {
if (id === "top") { window.scrollTo({ top: 0, behavior: "smooth" }); return; }
irA(id);
};
const adminEmail = contactInfo.contact_email || "hola@masoft.es";
const lista = items || [];
return (
<>
setTweak("theme", t.theme === "dark" ? "light" : "dark")} onScroll={onScroll}/>
{lista.length > 0 && }
Software gratis
Utilidades gratuitas para Windows.
Programas desarrollados por Masoft.es que usamos en nuestro trabajo diario.
Gratis y sin registro: un solo archivo que se copia y se abre, con su manual.
{/* Suscripción arriba, a la vista nada más entrar (antes estaba al final de la página) */}
¿Quieres estar al día?
Recibe un aviso cuando publiquemos nuevo software o actualizaciones. Sin spam.
{items === null &&
Cargando el software…
}
{fallo && (
No se ha podido cargar la lista de programas.{" "}
)}
{items && !fallo && items.length === 0 &&
Todavía no hay software publicado.
}
{lista.length > 0 && (
Importante: al abrir el programa por primera vez
Los programas no están firmados digitalmente, así que Windows puede mostrar el aviso
«Windows protegió su PC» (SmartScreen). Para abrirlo, pulse «Más información» y después
«Ejecutar de todas formas». Si quiere comprobar que el archivo es el original, compare su
huella SHA-256 con la publicada en la ficha. En PowerShell: Get-FileHash .\nombre_del_archivo.exe
Para mayor seguridad, cada ficha enlaza al análisis del programa en{" "}
VirusTotal, que lo revisa
con decenas de antivirus. También puede subir allí el archivo descargado y comprobar que su huella coincide.
En VirusTotal puede ver uno o dos avisos de antivirus poco conocidos que marcan cualquier programa sin firma
digital, por su forma de empaquetarlo y no por lo que hace. Ninguno de los antivirus de uso habitual
(Microsoft Defender, ESET, Kaspersky, Bitdefender…) detecta nada. Si un antivirus importante lo marcara,
retiraríamos la descarga hasta aclararlo.