[{"data":1,"prerenderedAt":321},["ShallowReactive",2],{"\u002Fprojects\u002Fbluedrop\u002F":3,"posts":207},{"id":4,"title":5,"body":6,"description":143,"extension":144,"featured":145,"images":146,"links":161,"meta":171,"navigation":145,"order":172,"path":173,"seo":174,"stack":175,"stats":190,"stem":203,"tag":204,"url":205,"__hash__":206},"projects\u002Fprojects\u002Fbluedrop.md","BlueDrop",{"type":7,"value":8,"toc":131},"minimark",[9,14,67,71,76,89,93,96,100,103,107],[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",{},"Sin pulsar nada."," Cuando el móvil se desconecta del Bluetooth del coche, guarda la ubicación y la dirección y te avisa con una notificación. No hace falta abrir la app.",[18,26,27,30],{},[21,28,29],{},"Volver al coche."," El último aparcamiento en el mapa, con un botón para que el navegador te lleve andando hasta él y otro para compartir la ubicación.",[18,32,33,36],{},[21,34,35],{},"Cuánto tiempo estuviste."," Al volver a conectarte al coche apunta la hora de llegada y te dice cuánto tiempo ha estado aparcado. Si no llega a reconectar, la sesión se cierra a mano.",[18,38,39,42],{},[21,40,41],{},"Sin GPS también se guarda."," Si no hay señal, el aparcamiento se guarda igual y luego puedes arrastrar el pin a su sitio.",[18,44,45,48],{},[21,46,47],{},"Historial."," Todos los aparcamientos con fecha, duración, una nota (\"planta -2, plaza 47\") y una foto.",[18,50,51,54],{},[21,52,53],{},"Estadísticas y widget."," Tiempo aparcado en el mes, media, récord y zonas más frecuentes. En la pantalla de inicio, un widget con la dirección y el botón de navegar.",[18,56,57,60],{},[21,58,59],{},"Varios coches."," Puedes vigilar más de un dispositivo Bluetooth, el del coche y el de la moto, por ejemplo.",[18,62,63,66],{},[21,64,65],{},"Privada."," Sin anuncios, sin cuentas y sin servidor propio: los aparcamientos, las notas y las fotos se guardan en el móvil. Tema claro u oscuro y seis idiomas.",[10,68,70],{"id":69},"cómo-está-hecho","Cómo está hecho",[72,73,75],"h3",{"id":74},"del-bluetooth-al-guardado-en-un-foreground-service","Del Bluetooth al guardado en un foreground service",[77,78,79,80,84,85,88],"p",{},"El aviso de desconexión llega a un ",[81,82,83],"code",{},"BroadcastReceiver",", y Android le da muy poco tiempo antes de poder matar el proceso. Por eso el receptor solo comprueba si el dispositivo es uno de los vigilados y arranca un foreground service. El servicio pide la ubicación con Fused Location, saca la dirección con el ",[81,86,87],{},"Geocoder",", guarda el registro en Room y lanza la notificación.",[72,90,92],{"id":91},"clean-architecture-en-10-módulos","Clean Architecture en 10 módulos",[77,94,95],{},"Un módulo de dominio en Kotlin puro, sin dependencias de Android, con los modelos, las interfaces de repositorio y nueve casos de uso. Alrededor, módulos de infraestructura (datos, Bluetooth, ubicación, tema) y uno por pantalla (mapa, historial, estadísticas, ajustes), todos dependiendo hacia el dominio. La interfaz es Jetpack Compose con MVVM, y Hilt lo conecta todo.",[72,97,99],{"id":98},"openstreetmap-en-lugar-de-google-maps","OpenStreetMap en lugar de Google Maps",[77,101,102],{},"El mapa empezó con Google Maps Compose. Lo cambié por osmdroid, que pinta OpenStreetMap dentro de Compose sin API key ni cuenta de facturación.",[10,104,106],{"id":105},"problemas-que-resolví","Problemas que resolví",[15,108,109,115,125],{},[18,110,111,114],{},[21,112,113],{},"Desconexiones duplicadas."," Algunos equipos de Bluetooth envían dos desconexiones seguidas, lo que creaba aparcamientos fantasma. Ahora se ignora cualquier desconexión del mismo dispositivo en los 30 segundos siguientes.",[18,116,117,120,121,124],{},[21,118,119],{},"Un aparcamiento en el océano."," Sin GPS, el registro se guarda con coordenadas ",[81,122,123],{},"(0, 0)",", y al editarlo el pin aparecía frente a la costa de África. Ahora el modo edición coloca el pin en la última posición buena conocida.",[18,126,127,130],{},[21,128,129],{},"Una dirección que no se actualizaba."," Al mover el pin a mano se guardaba la dirección antigua. Si no la has tocado, ahora se vuelve a calcular a partir de las nuevas coordenadas.",{"title":132,"searchDepth":133,"depth":133,"links":134},"",2,[135,136,142],{"id":12,"depth":133,"text":13},{"id":69,"depth":133,"text":70,"children":137},[138,140,141],{"id":74,"depth":139,"text":75},3,{"id":91,"depth":139,"text":92},{"id":98,"depth":139,"text":99},{"id":105,"depth":133,"text":106},"App nativa de Android que guarda dónde has aparcado en cuanto el móvil se desconecta del Bluetooth del coche, sin pulsar nada ni abrir la app. Mapa con OpenStreetMap, aviso al volver con el tiempo aparcado, historial con notas y fotos, estadísticas mensuales y widget de inicio. Sin anuncios, sin cuentas y sin servidor propio.","md",true,[147,152,155,158],{"src":148,"alt":149,"width":150,"height":151},"\u002Fimages\u002Fposts\u002Fbluedrop\u002Fmapa.webp","El último aparcamiento en el mapa de OpenStreetMap, con la dirección y los botones Compartir y Navegar al coche",540,1200,{"src":153,"alt":154,"width":150,"height":151},"\u002Fimages\u002Fposts\u002Fbluedrop\u002Fhistorial.webp","El historial de aparcamientos con fechas, duración y notas como la planta y la plaza del parking",{"src":156,"alt":157,"width":150,"height":151},"\u002Fimages\u002Fposts\u002Fbluedrop\u002Festadisticas.webp","Las estadísticas del mes, con el tiempo total aparcado, sesiones, media, récord y zonas más frecuentes",{"src":159,"alt":160,"width":150,"height":151},"\u002Fimages\u002Fposts\u002Fbluedrop\u002Fwidget.webp","El widget de inicio con la dirección del coche y el botón Navegar al coche",[162,165,168],{"label":163,"url":164},"Cómo se usa","\u002Fposts\u002Fbluedrop-donde-aparque-el-coche",{"label":166,"url":167},"Por dentro","\u002Fposts\u002Fbluedrop-por-dentro",{"label":169,"url":170},"Web","https:\u002F\u002Fabdell1999.github.io\u002Fbluedrop-public\u002F",{},12,"\u002Fprojects\u002Fbluedrop",{"title":5,"description":143},[176,177,178,179,180,181,182,183,184,185,186,187,188,189],"Kotlin 2.0","Jetpack Compose","Material 3","Hilt","Coroutines","Flow","Room","DataStore","Fused Location","CameraX","Glance","osmdroid","JUnit","MockK",[191,194,197,200],{"value":192,"label":193},"0","toques para guardar",{"value":195,"label":196},"10","módulos Gradle",{"value":198,"label":199},"6","idiomas",{"value":201,"label":202},"47","tests unitarios","projects\u002Fbluedrop","bluedrop","https:\u002F\u002Fplay.google.com\u002Fstore\u002Fapps\u002Fdetails?id=com.abr.bluedrop","FuD4F-j3kS4C8MC5yCpKlD7Ud8a3cWC82BkhrC6oD0o",[208,219,228,235,241,249,254,263,271,276,283,288,296,302,308,314],{"title":209,"description":210,"date":211,"path":212,"tags":213,"type":218},"ABR Offline Converter: convierte imágenes, vídeo, PDF y datos sin subir nada a ningún servidor","Un conversor de archivos que funciona entero en tu navegador. Imágenes, vídeo a GIF, vídeo a audio, CSV a JSON, PDF, fuentes y subtítulos, y un Taller del libro que convierte tus capítulos en Markdown en un EPUB con portada e índice. Qué hace y cómo se usa.","2026-10-24","\u002Fposts\u002Foffline-converter-convierte-sin-subir-nada",[214,215,216,217],"offline-converter","privacidad","productividad","proyectos","guia",{"title":220,"description":221,"date":211,"path":222,"tags":223,"type":227},"ABR Offline Converter por dentro: un conversor sin servidor, de ffmpeg.wasm a un EPUB hecho a mano","Cómo funciona un conversor de imágenes, vídeo, PDF, fuentes, subtítulos, texto y datos que no tiene backend. Workers, ffmpeg compilado a WebAssembly, codificadores BMP, ICO y TIFF escritos a mano, 811 páginas prerenderizadas y un Taller del libro que genera EPUB3 sin pandoc.","\u002Fposts\u002Foffline-converter-por-dentro",[214,224,225,226],"arquitectura","typescript","nuxt","tecnico",{"title":229,"description":230,"date":231,"path":232,"tags":233,"type":218},"Simple TTS: escucha cualquier artículo de la web en voz alta, sin salir de tu navegador","Una extensión gratuita para Chrome y Edge que lee en voz alta lo que selecciones o el artículo entero con las voces del propio navegador. Sin cuentas, sin servidores y sin APIs externas. Qué hace y cómo se usa.","2026-10-23","\u002Fposts\u002Fsimple-tts-escucha-cualquier-web",[234,215,216,217],"simple-tts",{"title":236,"description":237,"date":231,"path":238,"tags":239,"type":227},"Simple TTS por dentro: mi primera extensión, de JavaScript a WXT con Vue","Cómo pasó Simple TTS de tres archivos de JavaScript a una extensión Manifest V3 con WXT, Vue 3 y TypeScript. Qué me hizo cambiar en cada etapa, cómo se mantiene vivo un service worker durante una lectura larga, por qué leía la frase del día anterior y cómo se consigue que el resaltado vaya a la par de la voz.","\u002Fposts\u002Fsimple-tts-por-dentro",[234,224,225,240],"vue",{"title":242,"description":243,"date":244,"path":245,"tags":246,"type":227},"TranscribeLocal por dentro: Whisper en el navegador y cómo dejó de inventarse frases","Cómo ejecuta TranscribeLocal Whisper, la separación de hablantes y un modelo de lenguaje dentro del navegador con Transformers.js, por qué Whisper se inventaba frases en los silencios, qué hace falta para transcribir audios largos y por qué un service worker cortaba las descargas a los cinco minutos.","2026-10-22","\u002Fposts\u002Ftranscribe-local-por-dentro",[247,248,224,225,226],"transcribe-local","ia",{"title":250,"description":251,"date":244,"path":252,"tags":253,"type":218},"TranscribeLocal: transcribe y saca subtítulos de cualquier vídeo, sin salir de tu navegador","Una app web que convierte audio y vídeo en texto y subtítulos con Whisper, distingue quién habla y hace resúmenes, todo en tu ordenador. Sin subir nada a ningún servidor, sin límites de duración y gratis. Qué hace y cómo se usa.","\u002Fposts\u002Ftranscribe-local-subtitulos-en-tu-navegador",[247,248,215,217],{"title":255,"description":256,"date":257,"path":258,"tags":259,"type":218},"PokeStore: un tema de WordPress a medida para tiendas de cartas coleccionables","Un tema de WordPress y WooCommerce hecho desde cero para tiendas de Pokémon y One Piece TCG. Un recorrido por la tienda como la ve quien compra, de la portada a la lista de deseos, y por lo que puede ajustar quien la lleva.","2026-10-21","\u002Fposts\u002Fpokestore-tema-wordpress-para-tiendas-tcg",[260,261,262,217],"pokestore","wordpress","woocommerce",{"title":264,"description":265,"date":266,"path":267,"tags":268,"type":218},"YouTube Like Shortcut: da like con una tecla, como en TikTok","Una extensión de Chrome que da like al vídeo o Short de YouTube que estás viendo al pulsar la L. Cómo se instala, qué opciones tiene y por qué no se dispara mientras escribes.","2026-10-20","\u002Fposts\u002Fyoutube-like-shortcut-like-con-una-tecla",[269,270,216,217],"youtube-like-shortcut","youtube",{"title":272,"description":273,"date":266,"path":274,"tags":275,"type":227},"YouTube Like Shortcut por dentro: pulsar un botón en una web que cambia sin avisar","Cómo una extensión de una sola función llega antes que el reproductor de YouTube, encuentra el botón de like entre variantes ocultas y layouts que cambian con el ancho de la ventana, y se asegura de que el like ha entrado aunque YouTube esté reconstruyendo la página.","\u002Fposts\u002Fyoutube-like-shortcut-por-dentro",[269,270,224,225],{"title":277,"description":278,"date":279,"path":280,"tags":281,"type":218},"AI Upscaler: amplía imágenes y quita fondos con IA, sin salir de tu navegador","Una extensión de Chrome que amplía imágenes ×2 o ×4 y elimina fondos con inteligencia artificial, todo en tu equipo. Sin subir nada a ningún servidor, sin cuentas y gratis. Qué hace y cómo se usa.","2026-10-19","\u002Fposts\u002Fai-upscaler-imagenes-con-ia-en-tu-navegador",[282,248,215,217],"ai-upscaler",{"title":284,"description":285,"date":279,"path":286,"tags":287,"type":227},"AI Upscaler por dentro: dos redes neuronales en una extensión de Chrome","Cómo ejecuta AI Upscaler Real-ESRGAN y U²-Net dentro del navegador con ONNX Runtime Web, por qué la imagen se procesa en mosaicos con margen, qué hubo que parchear en el modelo de quitar fondos y qué obstáculos pone una extensión Manifest V3 a los workers y al WebAssembly.","\u002Fposts\u002Fai-upscaler-por-dentro",[282,248,224,225,240],{"title":289,"description":290,"date":291,"path":292,"tags":293,"type":218},"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",[294,295,215,217],"cipher","seguridad",{"title":297,"description":298,"date":291,"path":299,"tags":300,"type":227},"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",[294,295,224,225,301],"rust",{"title":303,"description":304,"date":291,"path":305,"tags":306,"type":227},"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",[307,224,225,226],"abreader",{"title":309,"description":310,"date":291,"path":311,"tags":312,"type":218},"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.","\u002Fposts\u002Fabreader-tu-biblioteca-en-el-navegador",[307,313,217],"lectura",{"title":315,"description":316,"date":291,"path":317,"tags":318,"type":227},"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",[226,240,319,320],"devops","meta",1792878937109]