diff --git a/desktop/latex.js b/desktop/latex.js index f7a376c..885198e 100644 --- a/desktop/latex.js +++ b/desktop/latex.js @@ -33,15 +33,46 @@ export const PACCHETTI_BASE = [ 'tex4ht', 'make4ht', 'l3packages', 'l3kernel', ]; +// Collezioni tlmgr installate dopo PACCHETTI_BASE per avere una distribuzione +// "completa" fin da subito: coprono quasi tutti i pacchetti comuni (LaTeX +// extra, font, matematica/scienze, grafica, bibliografia, lingue europee...) +// senza scaricare anche le collezioni di lingue rare o la documentazione +// sorgente, che pesano molto e servono a pochissimi utenti. +export const COLLEZIONI_COMPLETE = [ + 'collection-basic', 'collection-latex', 'collection-latexrecommended', + 'collection-latexextra', 'collection-fontsrecommended', 'collection-fontsextra', + 'collection-mathscience', 'collection-pictures', 'collection-plaingeneric', + 'collection-humanities', 'collection-publishers', 'collection-formatsextra', + 'collection-bibtexextra', 'collection-binextra', 'collection-metapost', + 'collection-luatex', 'collection-xetex', + 'collection-langitalian', 'collection-langeuropean', +]; + const PIATTAFORMA = process.platform; // 'win32' | 'darwin' | 'linux' // URL ufficiali di TinyTeX (distribuzione TeX Live minima e installabile -// senza privilegi). Sovrascrivibili con TEXFORGE_TINYTEX_URL per usare un -// mirror o una copia locale. +// senza privilegi). Ne teniamo più di uno per piattaforma perché sia il +// redirect di yihui.org sia i nomi dei file sulle release GitHub sono +// cambiati più volte nel tempo: se il primo risponde 404 si passa al +// successivo invece di far fallire subito l'installazione. +// Sovrascrivibile con TEXFORGE_TINYTEX_URL per usare un mirror o una copia +// locale (in tal caso viene provato solo quell'URL). const TINYTEX_URL = { - win32: 'https://yihui.org/tinytex/TinyTeX-1.zip', - darwin: 'https://yihui.org/tinytex/TinyTeX-1.tgz', - linux: 'https://yihui.org/tinytex/TinyTeX-1.tar.gz', + win32: [ + 'https://github.com/rstudio/tinytex-releases/releases/download/daily/TinyTeX-1.zip', + 'https://github.com/rstudio/tinytex-releases/releases/latest/download/TinyTeX-1.zip', + 'https://yihui.org/tinytex/TinyTeX-1.zip', + ], + darwin: [ + 'https://github.com/rstudio/tinytex-releases/releases/download/daily/TinyTeX-1.tgz', + 'https://github.com/rstudio/tinytex-releases/releases/latest/download/TinyTeX-1.tgz', + 'https://yihui.org/tinytex/TinyTeX-1.tgz', + ], + linux: [ + 'https://github.com/rstudio/tinytex-releases/releases/download/daily/TinyTeX-1.tar.gz', + 'https://github.com/rstudio/tinytex-releases/releases/latest/download/TinyTeX-1.tar.gz', + 'https://yihui.org/tinytex/TinyTeX-1.tar.gz', + ], }; // --------------------------------------------------------------------------- @@ -212,18 +243,37 @@ async function estrai(archivio, destinazione, onLog) { } } +// Scarica la distribuzione provando gli URL candidati in ordine: se uno +// risponde 404 (es. un redirect diventato stale, o un file rinominato in +// una nuova release) si passa al successivo invece di arrendersi subito. +async function scaricaConFallback(candidati, archivio, onLog, onProgress) { + let ultimoErrore; + for (const url of candidati) { + try { + onLog?.(`Scarico la distribuzione TeX da ${url}\n`); + await scarica(url, archivio, onProgress); + return; + } catch (e) { + ultimoErrore = e; + onLog?.(`Fonte non disponibile (${e.message}), provo un'altra fonte...\n`); + } + } + throw ultimoErrore; +} + // Scarica e installa TinyTeX nella cartella dati dell'utente. export async function installaDistribuzione(userDataDir, { onLog, onProgress } = {}) { - const url = process.env.TEXFORGE_TINYTEX_URL || TINYTEX_URL[PIATTAFORMA]; - if (!url) throw new Error(`piattaforma non supportata: ${PIATTAFORMA}`); + const candidati = process.env.TEXFORGE_TINYTEX_URL + ? [process.env.TEXFORGE_TINYTEX_URL] + : TINYTEX_URL[PIATTAFORMA]; + if (!candidati) throw new Error(`piattaforma non supportata: ${PIATTAFORMA}`); const radice = radiceGestita(userDataDir); const tmp = await fsp.mkdtemp(path.join(os.tmpdir(), 'texforge-tex-')); - const archivio = path.join(tmp, path.basename(new URL(url).pathname)); + const archivio = path.join(tmp, path.basename(new URL(candidati[0]).pathname)); try { - onLog?.(`Scarico la distribuzione TeX da ${url}\n`); - await scarica(url, archivio, onProgress); + await scaricaConFallback(candidati, archivio, onLog, onProgress); onLog?.('\nEstrazione in corso...\n'); const staging = path.join(tmp, 'estratto'); @@ -323,11 +373,19 @@ export async function aggiornaPacchetti(tex, onLog) { await tlmgr(tex, ['update', '--self', '--all'], { onLog, timeout: 900000 }); } -// Installa i pacchetti di base subito dopo la prima installazione. +// Installa i pacchetti di base, poi le collezioni che rendono la +// distribuzione "completa", subito dopo la prima installazione. Il +// download di TinyTeX resta piccolo e affidabile: è tlmgr, dopo, che porta +// la dotazione di pacchetti a un livello quasi completo. export async function installaPacchettiBase(tex, onLog) { onLog?.('\nInstallo i pacchetti di base...\n'); // Un unico comando: tlmgr risolve le dipendenze e salta quelli già presenti. await tlmgr(tex, ['install', ...PACCHETTI_BASE], { onLog, timeout: 1800000 }); + + onLog?.('\nInstallo il resto dei pacchetti (può richiedere diversi minuti)...\n'); + await tlmgr(tex, ['install', ...COLLEZIONI_COMPLETE], { onLog, timeout: 3600000 }).catch((e) => { + onLog?.(`\nAvviso: alcune collezioni di pacchetti non sono state installate: ${e.message}\n`); + }); } // --------------------------------------------------------------------------- diff --git a/desktop/main.js b/desktop/main.js index 1ac9736..4fb5968 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -14,6 +14,7 @@ import os from 'node:os'; import { fileURLToPath } from 'node:url'; import * as latex from './latex.js'; +import { creaZip } from './zip.js'; import { generateHtmlFromQuiz, generatePdfBytes, rendiHtmlAutonomo } from './vendor/compiler.js'; import * as pdfLib from 'pdf-lib'; import fontkit from '@pdf-lib/fontkit'; @@ -318,6 +319,20 @@ ipcMain.handle('salva-file', async (_e, { nome, dati }) => { return res.filePath; }); +// `file` è [{ nome, dati }] con i byte in base64: li impacchetta in un unico +// ZIP, invece di far salvare ogni output uno per uno. +ipcMain.handle('salva-zip', async (_e, { nome, file }) => { + const res = await dialog.showSaveDialog(finestra, { + title: 'Salva lo ZIP', + defaultPath: nome, + filters: [{ name: 'Archivio ZIP', extensions: ['zip'] }], + }); + if (res.canceled || !res.filePath) return null; + const zip = creaZip(file.map((f) => ({ nome: f.nome, dati: Buffer.from(f.dati, 'base64') }))); + await fsp.writeFile(res.filePath, zip); + return res.filePath; +}); + ipcMain.handle('apri-cartella-dati', () => shell.openPath(app.getPath('userData'))); // --------------------------------------------------------------------------- diff --git a/desktop/preload.cjs b/desktop/preload.cjs index fd9b2a8..5a4be1a 100644 --- a/desktop/preload.cjs +++ b/desktop/preload.cjs @@ -25,6 +25,7 @@ contextBridge.exposeInMainWorld('texforge', { apriTex: () => ipcRenderer.invoke('apri-tex'), compila: (documenti) => ipcRenderer.invoke('compila', documenti), salvaFile: (file) => ipcRenderer.invoke('salva-file', file), + salvaZip: (archivio) => ipcRenderer.invoke('salva-zip', archivio), anteprima: (file) => ipcRenderer.invoke('anteprima', file), chiudiAnteprime: () => ipcRenderer.invoke('chiudi-anteprime'), apriCartellaDati: () => ipcRenderer.invoke('apri-cartella-dati'), diff --git a/desktop/renderer/app.css b/desktop/renderer/app.css index 4063586..e31a0e7 100644 --- a/desktop/renderer/app.css +++ b/desktop/renderer/app.css @@ -141,6 +141,11 @@ body { } .output-card h3 { font-family: var(--mono); font-size: 1.1rem; margin-bottom: 0.35rem; } .output-card p { font-size: 0.85rem; color: var(--ink-soft); margin-bottom: 1rem; } +.output-file-list { + list-style: none; text-align: left; font-size: 0.85rem; color: var(--ink-soft); + margin: 0 0 1rem; padding: 0; max-height: 8rem; overflow-y: auto; +} +.output-file-list li { padding: 0.15rem 0; } .output-btn { display: block; width: 100%; background: var(--accent-bg); color: var(--accent); border: none; padding: 0.7rem 1rem; border-radius: var(--r); diff --git a/desktop/renderer/app.js b/desktop/renderer/app.js index 999040c..896bc3d 100644 --- a/desktop/renderer/app.js +++ b/desktop/renderer/app.js @@ -107,7 +107,6 @@ api.suProgresso((p) => { // ═══════════════ COMPILATORE ═══════════════ let documenti = []; -const urlCreati = []; function aggiungi(nome, contenuto) { if (!documenti.some((d) => d.nome === nome)) documenti.push({ nome, contenuto }); @@ -162,43 +161,51 @@ $('compileBtn').addEventListener('click', async () => { $('preview').hidden = true; $('logStatus').textContent = ''; $('log').hidden = false; - while (urlCreati.length) URL.revokeObjectURL(urlCreati.pop()); try { const res = await api.compila(documenti); $('logStatus').textContent = res.log + '\n' + $('logStatus').textContent; - for (const f of res.file) { - const bytes = Uint8Array.from(atob(f.dati), (c) => c.charCodeAt(0)); - const blob = new Blob([bytes], { - type: f.tipo === 'pdf' ? 'application/pdf' : 'text/html', - }); - const url = URL.createObjectURL(blob); - urlCreati.push(url); + // Un unico ZIP per tipo (PDF / HTML), invece di un pulsante di salvataggio + // per ogni singolo file: con molti documenti compilati insieme era una + // lista infinita di "Salva con nome…" da cliccare uno per uno. + for (const tipo of ['pdf', 'html']) { + const file = res.file.filter((f) => f.tipo === tipo); + if (!file.length) continue; const card = document.createElement('div'); card.className = 'output-card'; card.innerHTML = `
${esc(f.nome)}
${esc(f.motore)}