[{"data":1,"prerenderedAt":249},["ShallowReactive",2],{"\u002Fprojects\u002Fabreader":3,"posts":211},{"id":4,"title":5,"body":6,"description":152,"extension":153,"featured":154,"images":155,"links":172,"meta":176,"navigation":154,"order":177,"path":178,"seo":179,"stack":180,"stats":194,"stem":207,"tag":208,"url":209,"__hash__":210},"projects\u002Fprojects\u002Fabreader.md","ABReader",{"type":7,"value":8,"toc":138},"minimark",[9,14,67,71,76,80,84,87,91,94,98,101,105,108,112],[10,11,13],"h2",{"id":12},"qué-hace","Qué hace",[15,16,17,25,31,37,43,49,55,61],"ul",{},[18,19,20,24],"li",{},[21,22,23],"strong",{},"Biblioteca local."," Colecciones, búsqueda, estados de lectura y estadísticas con rachas y mapa de actividad.",[18,26,27,30],{},[21,28,29],{},"Once formatos."," ePub, PDF, MOBI, FB2, DOCX, CBZ, CBR, DjVu, HTML, Markdown y TXT.",[18,32,33,36],{},[21,34,35],{},"Lectura cómoda."," Temas, tipografías del sistema, modo paginado o continuo y atajos configurables.",[18,38,39,42],{},[21,40,41],{},"Subrayados y notas."," Resaltados con notas y citas que se pueden exportar.",[18,44,45,48],{},[21,46,47],{},"Aprender idiomas."," Diccionario y traducción al seleccionar una palabra, y tarjetas de vocabulario con repaso espaciado.",[18,50,51,54],{},[21,52,53],{},"Escuchar el libro."," Lectura en voz alta frase a frase, con resaltado, también sin conexión.",[18,56,57,60],{},[21,58,59],{},"Sincronización opcional."," Los datos se guardan en el Google Drive del usuario, no en un servidor mío.",[18,62,63,66],{},[21,64,65],{},"Web y escritorio."," Funciona en el navegador como PWA y en Windows, macOS y Linux como app nativa. La interfaz está en 9 idiomas.",[10,68,70],{"id":69},"cómo-está-hecho","Cómo está hecho",[72,73,75],"h3",{"id":74},"sin-backend-local-first","Sin backend, local-first",[77,78,79],"p",{},"No hay servidor propio. Libros, progreso, marcadores y estadísticas viven en IndexedDB y en OPFS, el sistema de archivos privado del navegador, o en disco cuando corre como app de escritorio. Eso no elimina los problemas de backend, los traslada al cliente: merge entre dispositivos, borrados que se propagan, archivos de cientos de megas, cuotas de almacenamiento y OAuth con Google Drive.",[72,81,83],{"id":82},"capas-con-dependencias-en-un-solo-sentido","Capas con dependencias en un solo sentido",[77,85,86],{},"La interfaz usa composables, los composables usan repositorios y handlers de formato, y solo dos ficheros tocan IndexedDB. La lógica que no necesita reactividad (merge, calendario de repasos, validaciones) son funciones puras, testeables sin montar componentes e importables desde un Web Worker.",[72,88,90],{"id":89},"un-contrato-por-formato","Un contrato por formato",[77,92,93],{},"Cada formato implementa la misma interfaz (patrón Strategy) y declara qué sabe hacer: índice, zoom, modo continuo. El lector nunca pregunta qué formato tiene delante, y la librería de cada uno (pdf.js, el parser de MOBI, el de DjVu) solo se descarga al abrir un libro de ese tipo.",[72,95,97],{"id":96},"voces-neuronales-en-el-navegador","Voces neuronales en el navegador",[77,99,100],{},"La síntesis de voz local encadena modelos ONNX dentro de Web Workers, usa WebGPU cuando hay GPU y WASM cuando no, y guarda los modelos en OPFS con una descarga atómica. Si el modelo aún no está listo, la lectura usa la voz del navegador y vuelve sola a la local cuando lo está.",[72,102,104],{"id":103},"web-y-escritorio-con-el-mismo-código","Web y escritorio con el mismo código",[77,106,107],{},"La versión de escritorio usa Tauri en lugar de Electron: el WebView del sistema en vez de empaquetar Chromium, así que el instalador es pequeño y consume menos memoria. Las diferencias de plataforma están aisladas en dos plugins.",[10,109,111],{"id":110},"problemas-que-resolví","Problemas que resolví",[15,113,114,120,126,132],{},[18,115,116,119],{},[21,117,118],{},"Libros borrados que «resucitaban» al sincronizar."," El borrado no dejaba rastro, así que otro dispositivo volvía a añadir el libro. Lo resolví con marcas de borrado explícitas que viajan en la sincronización, partiendo de una regla: que algo falte en el remoto no significa que se haya borrado.",[18,121,122,125],{},[21,123,124],{},"Exportar cientos de MB sin quedarse sin memoria."," La exportación construía todo en memoria. Ahora se genera en streaming y la subida a Drive es reanudable.",[18,127,128,131],{},[21,129,130],{},"La posición de lectura se perdía al redimensionar."," El resaltado de la lectura en voz alta alteraba las posiciones que guarda epub.js. Ahora se guarda la posición del bloque que contiene la frase, que no cambia.",[18,133,134,137],{},[21,135,136],{},"Un fallo de build que ningún check detectaba."," Un cambio en el worker de voz pasaba lint, tests y typecheck pero rompía el build de producción. Además de arreglarlo, añadí el build de producción al CI en cada push.",{"title":139,"searchDepth":140,"depth":140,"links":141},"",2,[142,143,151],{"id":12,"depth":140,"text":13},{"id":69,"depth":140,"text":70,"children":144},[145,147,148,149,150],{"id":74,"depth":146,"text":75},3,{"id":82,"depth":146,"text":83},{"id":89,"depth":146,"text":90},{"id":96,"depth":146,"text":97},{"id":103,"depth":146,"text":104},{"id":110,"depth":140,"text":111},"Lector de libros electrónicos para web y escritorio. Once formatos, anotaciones, vocabulario con repaso y lectura en voz alta con voces neuronales que se ejecutan en tu dispositivo. Sin cuentas ni servidores.","md",true,[156,161,164,167],{"src":157,"alt":158,"width":159,"height":160},"\u002Fimages\u002Fposts\u002Fabreader-tu-biblioteca-en-el-navegador\u002Fbiblioteca.webp","La biblioteca con portadas y progreso de lectura",1440,900,{"src":162,"alt":163,"width":159,"height":160},"\u002Fimages\u002Fposts\u002Fabreader-tu-biblioteca-en-el-navegador\u002Flector.webp","El lector con un libro abierto",{"src":165,"alt":166,"width":159,"height":160},"\u002Fimages\u002Fposts\u002Fabreader-tu-biblioteca-en-el-navegador\u002Fvocabulario.webp","El vocabulario con tarjetas de repaso",{"src":168,"alt":169,"width":170,"height":171},"\u002Fimages\u002Fposts\u002Fabreader-tu-biblioteca-en-el-navegador\u002Fbiblioteca-movil.webp","La biblioteca en el móvil",780,1688,[173],{"label":174,"url":175},"Cómo se usa","\u002Fposts\u002Fabreader-tu-biblioteca-en-el-navegador",{},1,"\u002Fprojects\u002Fabreader",{"title":5,"description":152},[181,182,183,184,185,186,187,188,189,190,191,192,193],"Nuxt 4","Vue 3","TypeScript","Tailwind v4","epub.js","pdf.js","IndexedDB \u002F OPFS","Tauri v2","onnxruntime-web","Google Drive API","Vitest","Playwright","GitHub Actions",[195,198,201,204],{"value":196,"label":197},"11","formatos",{"value":199,"label":200},"~2.500","tests unitarios",{"value":202,"label":203},"23","specs end-to-end",{"value":205,"label":206},"~900","commits desde feb. 2026","projects\u002Fabreader","abreader","https:\u002F\u002Fabdell1999.github.io\u002Febook-reader-public\u002F","De2B2Pdybu9Gc3fKX37dMsOFTFldi1M4T7JYbVn87uw",[212,222,230,236,241],{"title":213,"description":214,"date":215,"path":216,"tags":217},"ABR Cipher: cifrado seguro, completamente en tu navegador","Una suite de cifrado que no envía nada a ningún servidor. Cifrar textos y archivos, compartir secretos con un enlace que caduca, guardar contraseñas y destruir archivos. Qué hace y cómo se usa.","2026-10-18","\u002Fposts\u002Fabr-cipher-cifrado-en-tu-navegador",[218,219,220,221],"cipher","seguridad","privacidad","proyectos",{"title":223,"description":224,"date":215,"path":225,"tags":226},"ABR Cipher por dentro: criptografía sin servidor, de la URL al disco","Por qué ABR Cipher solo usa la Web Crypto API, cómo viaja un secreto en el fragmento de una URL, cómo se cifra un archivo de varios GB en el navegador y qué hace falta para destruir un archivo de verdad, con un compañero en Rust que lee NTFS.","\u002Fposts\u002Fabr-cipher-por-dentro",[218,219,227,228,229],"arquitectura","typescript","rust",{"title":231,"description":232,"date":215,"path":233,"tags":234},"ABReader por dentro: el lector que no encontré, construido a mi medida","Voces neuronales que se ejecutan en el navegador, sincronización entre dispositivos sin servidor y once formatos detrás de un mismo contrato. Las decisiones técnicas de ABReader y por qué las tomé.","\u002Fposts\u002Fabreader-por-dentro",[208,227,228,235],"nuxt",{"title":237,"description":238,"date":215,"path":175,"tags":239},"ABReader: tu biblioteca de libros electrónicos en el navegador","Un lector de libros electrónicos que funciona en el navegador, sin cuentas ni servidores. Qué hace, cómo se usa y cómo probarlo en un minuto.",[208,240,221],"lectura",{"title":242,"description":243,"date":215,"path":244,"tags":245},"Cómo construí este blog: estático, sin servidor y sin coste de hosting","Por qué elegí Nuxt y Markdown en lugar de WordPress, cómo separo el código privado del sitio publicado y qué se rompió en el primer despliegue.","\u002Fposts\u002Farquitectura-de-este-blog",[235,246,247,248],"vue","devops","meta",1792430530364]