From 86df4da9eceba144179470e66ad365c99bf3c0c5 Mon Sep 17 00:00:00 2001 From: D4vRAM369 Date: Sun, 18 Jan 2026 23:51:54 +0000 Subject: [PATCH 1/5] docs: add v1.4 design document Approved design for: - Dashboard with stats cards, chart, and patterns - CBD/THC toggle with orange/amber theme for THC - Backup system (auto + manual with AES-256) - Improved CSV with substance tracking Co-Authored-By: Claude Opus 4.5 --- docs/plans/2026-01-18-v1.4-design.md | 237 +++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 docs/plans/2026-01-18-v1.4-design.md diff --git a/docs/plans/2026-01-18-v1.4-design.md b/docs/plans/2026-01-18-v1.4-design.md new file mode 100644 index 0000000..b58706c --- /dev/null +++ b/docs/plans/2026-01-18-v1.4-design.md @@ -0,0 +1,237 @@ +# CBDcounter v1.4 - Documento de Diseno + +**Fecha:** 18 de enero de 2026 +**Estado:** Aprobado + +--- + +## Resumen Ejecutivo + +La version 1.4 incluye cuatro mejoras principales: +1. Dashboard de estadisticas con metricas avanzadas +2. Toggle CBD/THC para trackear diferentes sustancias +3. Sistema de backup automatico y manual con cifrado +4. CSV mejorado con soporte multi-sustancia + +--- + +## Feature 1: Dashboard de Estadisticas + +### Descripcion +Nueva pantalla `DashboardActivity` que reemplaza la actual `StatsActivity` como punto de entrada desde "Stats". + +### Layout + +``` ++-------------------------------------+ +| <- Toolbar: "Estadisticas" [gear icon] | +|------------------------------------| +| +---------+ +---------+ | +| | HOY | | SEMANA | | <- Cards compactas (2x2 grid) +| | 3 | | 18 | | +| +---------+ +---------+ | +| +---------+ +---------+ | +| |PROMEDIO | | RACHA | | +| | 2.5/d | | 5 dias | | +| +---------+ +---------+ | +|------------------------------------| +| [chart icon] Grafica principal | +| [7d] [14d] [30d] < 01-07 ene > | +|------------------------------------| +| [chart icon] Patrones | +| - Dia mas activo: Sabado (4.2 avg)| +| - Esta semana vs anterior: +15% | +| - Mejor dia: 12 ene (6 tomas) | +|------------------------------------| +| [ [calendar icon] Ver calendario de emojis ] | ++-------------------------------------+ +``` + +### Cards de Resumen +| Card | Descripcion | +|------|-------------| +| Hoy | Contador del dia actual | +| Semana | Suma de los ultimos 7 dias | +| Promedio | Media diaria del ultimo mes | +| Racha | Dias consecutivos con registro | + +### Archivos +- `DashboardActivity.kt` (crear) +- `activity_dashboard.xml` (crear) +- `CalendarActivity.kt` (renombrar desde StatsActivity) + +--- + +## Feature 2: Toggle CBD/THC + +### Descripcion +Selector en EmojiSettingsActivity para cambiar el modo de la app entre CBD y THC. + +### Ubicacion +Arriba de la configuracion de emojis en EmojiSettingsActivity. + +### Diseno Visual + +``` ++-------------------------------------+ +| Tipo de sustancia | +| +-----------+ +-----------+ | +| | CBD | | THC | | +| | (verde) | | (naranja) | | +| +-----------+ +-----------+ | ++-------------------------------------+ +``` + +### Cambios segun Modo + +| Elemento | Modo CBD | Modo THC | +|----------|----------|----------| +| Label principal | "CBD" | "THC" | +| Pregunta +1 | "Con que esta alinado el CBD?" | "Que estas fumando?" | +| Subtexto +1 | "Etiqueta la toma con vibra 420" | (mismo) | +| Recuadro Weed/Polen | Verde | Naranja/Ambar | +| Busqueda | "CBD" | "THC" o "CBD+THC" | +| CSV header | substance=CBD | substance=THC | + +### Colores +- **CBD:** Verde actual (#4CAF50 o similar) +- **THC:** Naranja/Ambar (#FF9800 o similar) + +### Almacenamiento +```kotlin +Prefs.substanceType: String // "CBD" o "THC" +``` + +--- + +## Feature 3: Sistema de Backup + +### Descripcion +Sistema completo de copias de seguridad con opcion automatica y manual. + +### Ubicacion +Nueva seccion en EmojiSettingsActivity (considerar renombrar a SettingsActivity). + +### Layout + +``` ++-------------------------------------+ +| COPIA DE SEGURIDAD | +| | +| Backup automatico [OFF/ON] | +| +- Frecuencia: [Diario v] | +| +- Ultima: 15 ene 2026, 10:30 | +| | +| [ [save icon] Crear backup ahora ] | +| +- Con cifrado AES-256 [ ] | +| | +| [ [download icon] Restaurar backup ] | +|-------------------------------------| +| Contenido del backup: | +| [check] Contadores diarios | +| [check] Notas de texto | +| [check] Grabaciones de voz | +| [check] Configuracion de emojis | +| [check] Preferencias | ++-------------------------------------+ +``` + +### Formato de Backup +| Tipo | Extension | Descripcion | +|------|-----------|-------------| +| Sin cifrar | .zip | ZIP estandar con JSON + audios | +| Con cifrado | .cbdbak | ZIP cifrado con AES-256 | + +### Ubicacion de Archivos +``` +Documents/CBDCounter/backups/cbdcounter_backup_2026-01-18.zip +``` + +### Contenido del Backup +- `data.json` - Contadores, notas, preferencias +- `audio/` - Carpeta con grabaciones de voz +- `config.json` - Configuracion de emojis y preferencias + +### Frecuencias de Backup Automatico +- Diario +- Semanal +- Mensual + +### Archivos +- `BackupManager.kt` (crear) +- Modificar EmojiSettingsActivity para incluir UI + +--- + +## Feature 4: CSV Mejorado + +### Estructura + +```csv +date,substance,count,notes,has_audio +15-01-2026,CBD,3,"[leaf icon] 10:30 (weed) | [chocolate icon] 14:00 (polen)",true +16-01-2026,THC,5,"[leaf icon] 09:00 (sativa) | [leaf icon] 12:30",false +``` + +### Columnas +| Columna | Tipo | Descripcion | +|---------|------|-------------| +| date | String | Fecha DD-MM-YYYY | +| substance | String | "CBD" o "THC" | +| count | Integer | Numero de tomas | +| notes | String | Notas del dia (escapadas) | +| has_audio | Boolean | Si hay grabacion de voz | + +### Compatibilidad de Importacion +El sistema detecta automaticamente el formato de fecha: +1. DD-MM-YYYY (nuevo formato) +2. YYYY-MM-DD (ISO) +3. DD/MM/YYYY (formato antiguo de la app) + +### Registros Antiguos +Registros sin campo `substance` se asumen como CBD. + +--- + +## Archivos a Crear/Modificar + +| Archivo | Accion | +|---------|--------| +| `DashboardActivity.kt` | Crear | +| `activity_dashboard.xml` | Crear | +| `CalendarActivity.kt` | Renombrar (antes StatsActivity) | +| `activity_calendar.xml` | Renombrar (antes activity_stats) | +| `BackupManager.kt` | Crear | +| `EmojiSettingsActivity.kt` | Modificar (toggle + backup UI) | +| `Prefs.kt` | Anadir preferencias nuevas | +| `MainActivity.kt` | Cambiar navegacion | +| `colors.xml` | Anadir colores naranja/ambar | +| `strings.xml` | Nuevos textos | +| CSV utils | Modificar import/export | + +--- + +## Navegacion + +``` +MainActivity + | + +-> "Stats" -> DashboardActivity + | + +-> "Ver calendario" -> CalendarActivity + | + +-> [gear icon] Ajustes -> EmojiSettingsActivity + | + +-> Toggle CBD/THC + +-> Emojis + +-> Backup +``` + +--- + +## Notas de Implementacion + +1. **Branch:** Todos los cambios en `v1.4` +2. **Commits:** No commitear hasta aprobacion final +3. **Testing:** Verificacion manual de cada feature +4. **Compatibilidad:** Mantener retrocompatibilidad con datos existentes From 5e2cf199f5f25a4af7ab38e83cdcb25c682a4eb0 Mon Sep 17 00:00:00 2001 From: D4vRAM369 Date: Mon, 19 Jan 2026 14:28:09 +0000 Subject: [PATCH 2/5] feat(v1.4): contadores duales CBD/THC, calendario y backup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Principales cambios: - Contadores separados para CBD y THC con selector de modo activo - Nueva CalendarActivity con vista mensual y leyenda de emojis - BackupManager: sistema de backup/restore en ZIP con datos y audios - EdgeToEdgeUtils para diseño inmersivo - UI renovada: chips de selección, colores diferenciados por sustancia - Migración automática de datos legacy a formato v1.4 - Mejoras en Settings: sección de backup, más opciones - Eliminada StatsActivity (reemplazada por CalendarActivity) Archivos: MainActivity, Prefs, SettingsActivity, CalendarActivity, BackupManager, EdgeToEdgeUtils, layouts y recursos visuales. Co-Authored-By: Claude Opus 4.5 --- .gitignore | 48 +-- app/build.gradle.kts | 4 +- app/release/baselineProfiles/0/app-release.dm | Bin 3247 -> 2914 bytes app/release/baselineProfiles/1/app-release.dm | Bin 3181 -> 2858 bytes app/src/main/AndroidManifest.xml | 15 +- .../com/d4vram/cbdcounter/BackupManager.kt | 176 ++++++++++ .../d4vram/cbdcounter/CBDWidgetProvider.kt | 34 +- .../{StatsActivity.kt => CalendarActivity.kt} | 4 +- .../d4vram/cbdcounter/DashboardActivity.kt | 250 ++++++++++++++ .../com/d4vram/cbdcounter/EdgeToEdgeUtils.kt | 64 ++++ .../cbdcounter/EmojiSettingsActivity.kt | 72 ++++ .../d4vram/cbdcounter/EvolutionActivity.kt | 2 + .../com/d4vram/cbdcounter/MainActivity.kt | 326 +++++++++++++----- .../main/java/com/d4vram/cbdcounter/Prefs.kt | 109 +++++- .../com/d4vram/cbdcounter/SettingsActivity.kt | 310 +++++++++++++---- app/src/main/res/drawable/bg_chip_cbd.xml | 9 + app/src/main/res/drawable/bg_chip_thc.xml | 9 + app/src/main/res/drawable/bg_counter_cbd.xml | 6 + app/src/main/res/drawable/bg_counter_thc.xml | 6 + app/src/main/res/drawable/bg_rounded_card.xml | 6 + app/src/main/res/drawable/ic_settings.xml | 11 + .../main/res/layout/activity_dashboard.xml | 312 +++++++++++++++++ .../res/layout/activity_emoji_settings.xml | 259 ++++++++++---- .../main/res/layout/activity_evolution.xml | 4 +- app/src/main/res/layout/activity_main.xml | 107 +++++- app/src/main/res/layout/activity_settings.xml | 222 +++++++++--- app/src/main/res/layout/activity_stats.xml | 4 +- app/src/main/res/layout/history_item.xml | 57 ++- app/src/main/res/menu/dashboard_menu.xml | 9 + app/src/main/res/values-night/themes.xml | 2 - app/src/main/res/values/colors.xml | 14 + app/src/main/res/values/strings.xml | 37 ++ app/src/main/res/values/themes.xml | 2 - docs/plans/2026-01-18-v1.4-design.md | 237 ------------- 34 files changed, 2151 insertions(+), 576 deletions(-) create mode 100644 app/src/main/java/com/d4vram/cbdcounter/BackupManager.kt rename app/src/main/java/com/d4vram/cbdcounter/{StatsActivity.kt => CalendarActivity.kt} (98%) create mode 100644 app/src/main/java/com/d4vram/cbdcounter/DashboardActivity.kt create mode 100644 app/src/main/java/com/d4vram/cbdcounter/EdgeToEdgeUtils.kt create mode 100644 app/src/main/res/drawable/bg_chip_cbd.xml create mode 100644 app/src/main/res/drawable/bg_chip_thc.xml create mode 100644 app/src/main/res/drawable/bg_counter_cbd.xml create mode 100644 app/src/main/res/drawable/bg_counter_thc.xml create mode 100644 app/src/main/res/drawable/bg_rounded_card.xml create mode 100644 app/src/main/res/drawable/ic_settings.xml create mode 100644 app/src/main/res/layout/activity_dashboard.xml create mode 100644 app/src/main/res/menu/dashboard_menu.xml delete mode 100644 docs/plans/2026-01-18-v1.4-design.md diff --git a/.gitignore b/.gitignore index 4c2a586..c331fbd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,41 +1,31 @@ -# Gradle -.gradle/ -build/ +# Android Studio / IntelliJ +# Gradle +# Keys / services # Local config -local.properties - +# Logs & profiling +# NDK / C++ +# OS # Output +# Sesiones PBL (Learning/Aprendizaje) +docs/ *.aab *.apk -output-metadata.json - -# Android Studio / IntelliJ -*.iml -.idea/ - -# NDK / C++ +build/ captures/ -.externalNativeBuild/ .cxx/ - -# Keys / services +.DS_Store +.externalNativeBuild/ +google-services.json +.gradle/ +*.hprof +.idea/ +*.iml *.jks *.keystore keystore.properties -google-services.json - -# Logs & profiling +local.properties *.log -*.hprof - -# OS -.DS_Store -Thumbs.db - -*.keystore - -# Sesiones PBL (Learning/Aprendizaje) markdown/ - - +output-metadata.json +Thumbs.db diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0397d2d..6e52f9b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,8 +21,8 @@ android { applicationId = "com.d4vram.cbdcounter" minSdk = 26 targetSdk = 35 - versionCode = 6 - versionName = "1.1.0" + versionCode = 7 + versionName = "1.4.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm index f2b7d686ff7ebf9429366fc24d8aea28847ed2a1..4290a861b5ef09399c07cc06557a60ff28b5a045 100644 GIT binary patch delta 2792 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 delta 3128 zcmY+`c{CJiAHeaEvc(`m*VfI}g0X}#W~fxIG(?ywTed;gF^magu9Om46ItsfyDUXx z46==~L<|OF8`)y)+c3skf4t|u?{m)Yoaa2}`RjXre|#7c<)?tR%(#z=1Niv(0DHt> zyCk6O(If%Ctt5~lL6i#sxOD^o;06Eyp1w?QKO%5l&3h#0|P8*#a zzr53hdY7Mk69UWrc9ceQD!zxeF4Vk!#VpT-zOAC8Ekt;H{)9>A1IpN&mS)~_4IR-gIfu|Duj;rO@gLLooky+EgvW{R1NSxFw&PbF7@*^tpse*kV#bqKk$kQS{f^wRH_3y1)g`%hu9| zHlam^;H`(Ey!TCNktD5~( zFj(!R!~?q`rGY%@LN)3o6T1@l$A?NEYohUWs)W(=)qX-0-+rSS>U_R3k9Vx1t~;{w zVqRW)VcS(VF_!a-dBwseRpVSM>>4*rq^FxI zll}qTBy$r=G%058DEI6{Tx#~kW0Vx%>v*XP3;Y{x17~3h4asAA!^uG$uC#zz*CM+e z0@NF;k_x;8*pLcu_59$w$&;bku9=<)%4Mhdk#E!KcVmd>WIR}0Bt-fMPaw4YR zAVZq{3LS)WHslOwq~TmEu^?T%o`D){OdG#om+5ydZCa@-^K0deSf3RV_Y|QnKJLg{ z;8oeD5Ru?X*!M_wa9StbAIs*qvp%ItY&By`C>oyb55uky(Ug7=wZ=5bRRydn)>wOq{l)y8vrH2B^l-uNbv&AC}%t}6*)PmEPAR>O~tR>as(2{|2D_kZRitoOBAm?&IMk_bNi z^Aa_(klT{?pIrWIYDTii7 zD?Bm1XMZQ`bLZW2K%Z2^3P}v!9@6e|ten!UMS;rdEtK*f-VGg_DL^%DQ_f?SXYd1~ zjzV^P>VvY<>a7IlpBEMuPIu*>D7r!r0XLKsPK^!@lZwboWPB&KXWe+&yQXv)A)2$c z5pyMd1K!LoXbw*Gl0q(YVOEY}b_OioqRbYLK#Mkn2i&E*>!n0W!mHLELwi$E zYtT+6A^}-vKE{N1?#9T2Zgek{bz>87*S-D!==VVF60m4)<+=Ua5V0Z}9jBXG-5ZWB zoep>wQl0++!|~J_vbyJeb4j57bejA~-^Vg#a^v^(tq{L22Q~R6(=pvU>Cy7Abe!(Q zoXSIx&T^3~$L#a&9WnYv&7XWa-+@Hw(0?>V)&CoI+*lU;pmy+z%`Dw`w7bm$m?KZH zzvIi}qU>+s-&W%UbX+66I$q+h*lIM*nwp_`+dvyRWsi3vK0xS*(0U}&ORMC1&J;`e zgFe(-pE>2X3(*Ohe0Ijoq=EQ;LWb7JPzehOXJ+5Yt#d6r&8+u^ zBO-PhC2a=cjd=f%rO6?qjN^d?FA0VXbFI{_WihkBn6}{O3#9UNr-KDHB}77=y+wXk z_D(f7dwgwfWZS5wetpBMNqbIMSj}Km3j_BJ-FY*fE4Soj0NuP4Fm*3y~ zG@0NIRdn3moC_x!HlWa6#3;6W`bFg5{Tla|rCD{Bw?y-c86CEk<_rWpsHo{~FJ$!zB zy5t=n^4bZ%P#h+w?T?ke~DH!>MM)XCy!u+2(|@s;7U`EZ+{z;LIrsD9qFxw zwx%;jxj`7luyLuVBlv}}_q1FUmhl1sNnXpYN+1_o7zTQdRauI$hK3`b>sJao+ZT~{ zr&{KIQcZwECUIbKkYHsipRwBJBV2evM>1}(9H-G2qY{rVg6=_^qnzs76X1)FcpY>S zQ@9A<>LrJ(x|zB6dG;U5QlD@x8?>&>w(;%|n2GUJr1wd1k;8++^_=Eipn7KCMJZ1lU^4 zibZU173m3qL5z`0T~h(xh;Jtczl~20zavPkQY-o+z((~yIzo4&B;~nfD(8z$D%b0f zK{JUCRud005}w$QGuxk^0s;4NnyJ`8jroGkvboax?>tgjR9A4@g@wV_!B8Y6@;m3K z-UN^-R>5#>)QCrlSV^%-ybN1GD_x?_kMd1^3)uXm!#O3zF4QsXE>CcBk#d)uj)-{Jh z{?KJLo#@H(;J4CkJJZyQU@sBQ&4^R*hNJ_Pg22cm3v}|`v|+4=%Z`1Zul*H5TpW)^{e zU#1m7u60R`%CMIkD8{V{4z^~xS7bzSl{eHvpS53CH9iRR4sfOP#Sp~O6m=^y-nJBB zv?68_{Z;*Z7=QE_y>xgmCZjHV5HMFNt%{}%V3^$Df^F8@DPWhr-2x)0bek%y{`Wudo>}1pt7gU-SP~O^N51BZzY?asmQ<0U8=wzhLV8 zi>pU|Q5DDKaI4O|<&sF&Mz)jbv(wkKo>KGP7FeV`%s3$$vOxbKKbf|xhr-t1s4(Ad zKzU##k<*Lo`ta$^P`UCI?$zX+#3TvOTU9NyM-1Kwp}U+gpOsbjWr-DmlUVEB*X7`k zS_axLM;?0;GK#0_^Bq6dOgf1c?%^-@b zK**yB7zs-6K|={$2zekO)X);j!+G;&eLM5coiq2`xpTfhzVDx(ttJ0aae$zpAYkc% z4jk}92_Mc6f)N4k!N03N2u6DI9Y4Ogc{l(xwKM^I-G%_bwjcly!N<-Gz{8mm?S#uZ zWV9+dw`bJ4QU#%HF52t2Snw(Kl2*o(Le4Go`z{i!H~!aJM*|%%6yY0c#_GgBImuOs z>k{pD@9J`5_H2__LJ<5H^=hSeUp-!LzkCgO200(g*>LKP8_fmbKi?=Cnlq>HTL+X|C%>u z5UdU#9MWUHS7&SwgaD7BYd`DO1IZpr_xMqdK8wDxpv0N5*1|^g!;)njJEsNA==L%q z4;@;fKYC=O)^G-5lC7ondCvFG%AwPf+1Fkb)dP*7D?-V1{tq-kkZIplj>%A5^1o{h&#zCidZXY`n9(D9CeCx?sdOH?wGCF22v(Jzsca+Y zv?wH43N!mB>*5wc#>`CN_^9V&9i?1P<@n8uAl-BvNyK1i`&v^(`3HJJd~f#v42W!o zY0YW~Q?><7E^?0I?k~^E<2E&Lnom~S(?Z@Fat%cV$dPHF{LLHko`Y9x1oIjtH{f%; z;+#+W<9P04d+GBOYElZlIQeYzb+na|N)7l_nEO_JEOL%>#lUmNoK%w*Z$r53r>tQ> zfyLfZtBd3}(=8M;+hKx*u`5t|#m>I@v38@J=kxe9prXb-)i}RDz>34tio5m0$|g;P zKGwo+H|+?x98T#NG{^FB{2^GT)rI;=FnpZ%DxKz21BzJn_Ns+x;K6v9)9_7^Iu)f)4|9?5M zWR*!`fiLqS3#R0zSuA&J;3C_{PB_umbMIHaO_4 za+!X;XH1m_Q|efQrfV@Zf7@H9YpFi+jr7a_3`&#`m@+bhwqe#-MU~SWb>|NXN%p$d zBa=za&k}F^j5M?`yUiOzmdXX^pUEVmnVNTn zO18A>EBQr6b+{+@f(7EF`JNf*?J?6hIKnRL`o@NLX3N{CN5A`YY`lHiSx`kc6{z^U zKe(yKC~dmgBROI)4CP#I{wJGv zzRkMIwRd4?^qq@NNz5|bjXQ9KxYljSsEOw-80S=AYz<_aGTafp5IsRI zi|eu+zeHtER;;$53bhYf4g^W$G>*+V9cqE(w)^C;U0hr1==ij6Rz!u>eNeD*$lCKEq zKfxWG0+~}jmo&XeL5)_gE{)LIGN=bkPTAF|mnRNhnc~~~maA8Zj@PRb-x1u4A&Zv9 zDaa2ii)^7h2+(&l0+WhhJTwZXv-oLkTNBl*JNI;GZR`V=j_RP!fe5XD(5#A|lMY?^ z#lSZ7YsOIY0bioI)qPt_0YRyPNkiaq0O0sx+x}mUI{7z_3gDCCo8NyB901VN)CTYY z+>ZbNKk%-6&{g^89n!zOfDV_k}-dEsBWTQ9VOE;~%>VLO#ktCb5k%P9?46tI?_#@(|r9 zVn-Lgc|vdBv|0^zyIRbGB7tLCltd!AmW$t#tm!Z>C8zBd_Sw<)A`CrUTt2Dey`9kF zKW00B^U~kb7%9(69uT!jc5F0xF`(O0Qn%Z2>#Oeg4BFdQD_%x%+Y@PodVM~>mAvq0 z;e5zaL8sdBdzn1U;hS7-HkH< literal 3181 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 + + + + + android:parentActivityName=".CalendarActivity"> + android:value=".CalendarActivity" /> prefsJson.put(k, v) } + backupJson.put("prefs", prefsJson) + + val emojiJson = JSONObject() + emojiPrefs.forEach { (k, v) -> emojiJson.put(k, v) } + backupJson.put("emoji_prefs", emojiJson) + + // 2. Crear archivo temporal para el JSON + val tempDir = File(context.cacheDir, "backup_temp") + if (tempDir.exists()) tempDir.deleteRecursively() + tempDir.mkdirs() + + val jsonFile = File(tempDir, JSON_FILE_NAME) + jsonFile.writeText(backupJson.toString(2)) + + // 3. Crear archivo ZIP de destino + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + val zipFileName = "cbd_backup_$timeStamp.zip" + val zipFile = File(context.getExternalFilesDir(null), "backups/$zipFileName") + zipFile.parentFile?.mkdirs() + + // 4. Escribir ZIP (incluyendo JSON y carpeta de audios) + ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile))).use { zos -> + // Agregar JSON + addToZip(zos, jsonFile, JSON_FILE_NAME) + + // Agregar Audios + val audioDir = File(context.filesDir, "audios") + if (audioDir.exists() && audioDir.isDirectory) { + audioDir.listFiles()?.forEach { audioFile -> + addToZip(zos, audioFile, "audios/${audioFile.name}") + } + } + } + + // Limpiar + tempDir.deleteRecursively() + + return zipFile + + } catch (e: Exception) { + Log.e(TAG, "Error creating backup", e) + return null + } + } + + private fun addToZip(zos: ZipOutputStream, file: File, entryName: String) { + if (!file.exists()) return + val entry = ZipEntry(entryName) + zos.putNextEntry(entry) + file.inputStream().use { it.copyTo(zos) } + zos.closeEntry() + } + + fun restoreBackup(context: Context, uri: Uri): Boolean { + val tempDir = File(context.cacheDir, "restore_temp") + if (tempDir.exists()) tempDir.deleteRecursively() + tempDir.mkdirs() + + try { + // 1. Descomprimir ZIP + context.contentResolver.openInputStream(uri)?.use { inputStream -> + java.util.zip.ZipInputStream(inputStream).use { zis -> + var entry = zis.nextEntry + while (entry != null) { + val file = File(tempDir, entry.name) + if (entry.isDirectory) { + file.mkdirs() + } else { + file.parentFile?.mkdirs() + file.outputStream().use { fos -> zis.copyTo(fos) } + } + entry = zis.nextEntry + } + } + } + + // 2. Leer y validar JSON + val jsonFile = File(tempDir, JSON_FILE_NAME) + if (!jsonFile.exists()) return false + + val jsonContent = jsonFile.readText() + val backupJson = JSONObject(jsonContent) + + // 3. Restaurar Prefs (Main) + val prefsJson = backupJson.optJSONObject("prefs") + if (prefsJson != null) { + val editor = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit() + editor.clear() // Opcional: limpiar antes de restaurar + for (key in prefsJson.keys()) { + val value = prefsJson.get(key) + when (value) { + is Boolean -> editor.putBoolean(key, value) + is Int -> editor.putInt(key, value) + is Long -> editor.putLong(key, value) + is Float -> editor.putFloat(key, value.toFloat()) + is String -> editor.putString(key, value) + } + } + editor.apply() + } + + // 4. Restaurar Prefs (Emojis) + val emojiJson = backupJson.optJSONObject("emoji_prefs") + if (emojiJson != null) { + val editor = context.getSharedPreferences(EMOJI_PREFS_NAME, Context.MODE_PRIVATE).edit() + editor.clear() + for (key in emojiJson.keys()) { + val value = emojiJson.get(key) + if (value is String) editor.putString(key, value) + } + editor.apply() + } + + // 5. Restaurar Audios + val audiosDir = File(tempDir, "audios") + if (audiosDir.exists() && audiosDir.isDirectory) { + val targetDir = File(context.filesDir, "audios") + targetDir.mkdirs() + audiosDir.listFiles()?.forEach { audioFile -> + audioFile.copyTo(File(targetDir, audioFile.name), overwrite = true) + } + } + + return true + + } catch (e: Exception) { + Log.e(TAG, "Error restoring backup", e) + return false + } finally { + tempDir.deleteRecursively() + } + } +} diff --git a/app/src/main/java/com/d4vram/cbdcounter/CBDWidgetProvider.kt b/app/src/main/java/com/d4vram/cbdcounter/CBDWidgetProvider.kt index 175ba57..665471e 100644 --- a/app/src/main/java/com/d4vram/cbdcounter/CBDWidgetProvider.kt +++ b/app/src/main/java/com/d4vram/cbdcounter/CBDWidgetProvider.kt @@ -205,35 +205,33 @@ class CBDWidgetProvider : AppWidgetProvider() { appWidgetManager.updateAppWidget(appWidgetId, views) } - private fun incrementCounter(context: Context) { + private fun incrementActiveCounter(context: Context) { val today = getCurrentDateKey() - val sharedPrefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - val currentCount = sharedPrefs.getInt("$KEY_COUNT_PREFIX$today", 0) - - sharedPrefs.edit() - .putInt("$KEY_COUNT_PREFIX$today", currentCount + 1) - .apply() + Prefs.incrementActiveCount(context, today) } private fun addStandardCBD(context: Context) { - incrementCounter(context) - val entry = "🔹 ${getCurrentTimestamp()}" + incrementActiveCounter(context) + val isThc = Prefs.getSubstanceType(context) == "THC" + val entry = if (isThc) "🟢 ${getCurrentTimestamp()}" else "🔹 ${getCurrentTimestamp()}" appendNote(context, entry) } private fun resetCBD(context: Context) { val today = getCurrentDateKey() - val sharedPrefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - - sharedPrefs.edit() - .putInt("$KEY_COUNT_PREFIX$today", 0) - .apply() + // Reset solo el contador del modo activo + val isThc = Prefs.getSubstanceType(context) == "THC" + if (isThc) { + Prefs.setThcCount(context, today, 0) + } else { + Prefs.setCbdCount(context, today, 0) + } } private fun getCurrentCount(context: Context): Int { val today = getCurrentDateKey() - val sharedPrefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - return sharedPrefs.getInt("$KEY_COUNT_PREFIX$today", 0) + // Devolver el contador del modo activo + return Prefs.getActiveCount(context, today) } private fun getCurrentDateKey(): String { @@ -247,13 +245,13 @@ class CBDWidgetProvider : AppWidgetProvider() { } private fun addWeed(context: Context) { - incrementCounter(context) + incrementActiveCounter(context) val entry = "🌿 ${getCurrentTimestamp()} (aliñado con weed)" appendNote(context, entry) } private fun addPolem(context: Context) { - incrementCounter(context) + incrementActiveCounter(context) val entry = "🍫 ${getCurrentTimestamp()} (aliñado con polen)" appendNote(context, entry) } diff --git a/app/src/main/java/com/d4vram/cbdcounter/StatsActivity.kt b/app/src/main/java/com/d4vram/cbdcounter/CalendarActivity.kt similarity index 98% rename from app/src/main/java/com/d4vram/cbdcounter/StatsActivity.kt rename to app/src/main/java/com/d4vram/cbdcounter/CalendarActivity.kt index a58e963..4cf9c9d 100644 --- a/app/src/main/java/com/d4vram/cbdcounter/StatsActivity.kt +++ b/app/src/main/java/com/d4vram/cbdcounter/CalendarActivity.kt @@ -17,7 +17,7 @@ import java.util.Calendar import java.util.Date import java.util.Locale -class StatsActivity : AppCompatActivity(), NoteBottomSheet.Listener { +class CalendarActivity : AppCompatActivity(), NoteBottomSheet.Listener { private lateinit var toolbar: MaterialToolbar private lateinit var monthLabel: TextView @@ -51,6 +51,8 @@ class StatsActivity : AppCompatActivity(), NoteBottomSheet.Listener { super.onCreate(savedInstanceState) setContentView(R.layout.activity_stats) + window.statusBarColor = getColor(R.color.gradient_start) + toolbar = findViewById(R.id.statsToolbar) monthLabel = findViewById(R.id.monthLabel) prevMonthButton = findViewById(R.id.prevMonthButton) diff --git a/app/src/main/java/com/d4vram/cbdcounter/DashboardActivity.kt b/app/src/main/java/com/d4vram/cbdcounter/DashboardActivity.kt new file mode 100644 index 0000000..8a60b5c --- /dev/null +++ b/app/src/main/java/com/d4vram/cbdcounter/DashboardActivity.kt @@ -0,0 +1,250 @@ +package com.d4vram.cbdcounter + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.button.MaterialButton +import com.google.android.material.chip.ChipGroup +import java.text.SimpleDateFormat +import java.util.* + +class DashboardActivity : AppCompatActivity() { + + private lateinit var sharedPrefs: SharedPreferences + private lateinit var lineChart: LineChartView + private lateinit var tvToday: TextView + private lateinit var tvWeek: TextView + private lateinit var tvAvg: TextView + private lateinit var tvStreak: TextView + private lateinit var tvBusiestDay: TextView + private lateinit var tvBestDay: TextView + + private val dateKeyFormat = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) + private val labelFormat = SimpleDateFormat("dd/MM", Locale.getDefault()) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_dashboard) + + window.statusBarColor = getColor(R.color.gradient_start) + + sharedPrefs = getSharedPreferences("CBDCounter", Context.MODE_PRIVATE) + + val toolbar = findViewById(R.id.dashboardToolbar) + setSupportActionBar(toolbar) + toolbar.setNavigationOnClickListener { finish() } + + // Initialize Views + tvToday = findViewById(R.id.tvTodayCount) + tvWeek = findViewById(R.id.tvWeekCount) + tvAvg = findViewById(R.id.tvAvgCount) + tvStreak = findViewById(R.id.tvStreakCount) + tvBusiestDay = findViewById(R.id.tvBusiestDay) + tvBestDay = findViewById(R.id.tvBestDay) + lineChart = findViewById(R.id.lineChart) + + val btnViewCalendar = findViewById(R.id.btnViewCalendar) + btnViewCalendar.setOnClickListener { + startActivity(Intent(this, CalendarActivity::class.java)) + } + + val rangeChipGroup = findViewById(R.id.rangeChipGroup) + rangeChipGroup.setOnCheckedChangeListener { _, checkedId -> + val days = when (checkedId) { + R.id.chip7Days -> 7 + R.id.chip14Days -> 14 + R.id.chip30Days -> 30 + else -> 7 + } + loadChartData(days) + } + + // Load Data + calculateStats() + loadChartData(7) + } + + override fun onResume() { + super.onResume() + // Refresh data on resume (in case settings changed or user returned from calendar) + calculateStats() + // Determine current selected chip range + val rangeChipGroup = findViewById(R.id.rangeChipGroup) + val days = when (rangeChipGroup.checkedChipId) { + R.id.chip14Days -> 14 + R.id.chip30Days -> 30 + else -> 7 + } + loadChartData(days) + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.dashboard_menu, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_settings -> { + startActivity(Intent(this, EmojiSettingsActivity::class.java)) + true + } + else -> super.onOptionsItemSelected(item) + } + } + + private fun calculateStats() { + // 1. Today + val todayKey = dateKeyFormat.format(Date()) + val todayCount = sharedPrefs.getInt("count_$todayKey", 0) + tvToday.text = todayCount.toString() + + // 2. Week Total & Average + val calendar = Calendar.getInstance() + var weekTotal = 0 + val counts = mutableListOf() + + // Go back 7 days (including today? usually "last 7 days" includes today) + // Let's do last 7 days excluding today if we want completed days? + // No, user expects "this week" or "rolling 7 days". Lets do rolling 7 days including today. + val tempCal = Calendar.getInstance() + for (i in 0 until 7) { + val key = dateKeyFormat.format(tempCal.time) + val c = sharedPrefs.getInt("count_$key", 0) + weekTotal += c + counts.add(c) + tempCal.add(Calendar.DAY_OF_YEAR, -1) // go back + } + tvWeek.text = weekTotal.toString() + + // Average (last 30 days for better accuracy?) + val avgCal = Calendar.getInstance() + var total30 = 0 + var daysWithData = 0 + for (i in 0 until 30) { + val key = dateKeyFormat.format(avgCal.time) + if (sharedPrefs.contains("count_$key")) { + total30 += sharedPrefs.getInt("count_$key", 0) + daysWithData++ + } + avgCal.add(Calendar.DAY_OF_YEAR, -1) + } + val avg = if (daysWithData > 0) total30.toFloat() / daysWithData else 0f + tvAvg.text = String.format("%.1f", avg) + + // 3. Streak + tvStreak.text = "${calculateCleanStreak()} días" + + // 4. Patterns + calculatePatterns() + } + + private fun calculateCleanStreak(): Int { + var streak = 0 + val calendar = Calendar.getInstance() + + // Check backwards from today + // If today has 0, streak starts from yesterday? + // Logic: "Clean Streak" usually means days WITHOUT using (count == 0). + // Let's assume standard logic: 0 means clean. + + // Check if today is clean so far? + // If currentCount > 0, streak is currently 0. + // If currentCount == 0, count today + previous days. + + // However, if the user hasn't finished the day, is it fair to count today? + // Let's just count consecutive days with 0. + + // Start from today descending + var checkingDate = Calendar.getInstance() + + // Safety Break after 365 days + for(i in 0 until 365) { + val key = dateKeyFormat.format(checkingDate.time) + // If we don't have data for a day, do we assume 0 (clean)? + // Usually yes if it's in the past. + val count = sharedPrefs.getInt("count_$key", 0) + if (count == 0) { + streak++ + } else { + break + } + checkingDate.add(Calendar.DAY_OF_YEAR, -1) + } + return streak + } + + private fun calculatePatterns() { + val allData = sharedPrefs.all + val dayCounts = IntArray(7) { 0 } // Sun=0, Mon=1... + val dayOccurrences = IntArray(7) { 0 } + + var maxCount = 0 + var bestDate = "" + + allData.forEach { (key, value) -> + if (key.startsWith("count_") && value is Int) { + val dateStr = key.removePrefix("count_") + try { + val date = dateKeyFormat.parse(dateStr) + if (date != null && value > 0) { + val cal = Calendar.getInstance() + cal.time = date + val dayOfWeek = cal.get(Calendar.DAY_OF_WEEK) - 1 // 0-indexed + dayCounts[dayOfWeek] += value + dayOccurrences[dayOfWeek]++ + + if (value > maxCount) { + maxCount = value + bestDate = dateStr + } + } + } catch (_: Exception) {} + } + } + + // Busiest Day (Highest Average) + var maxAvg = 0f + var busiestDayIndex = -1 + + for (i in 0 until 7) { + if (dayOccurrences[i] > 0) { + val avg = dayCounts[i].toFloat() / dayOccurrences[i] + if (avg > maxAvg) { + maxAvg = avg + busiestDayIndex = i + } + } + } + + val daysOfWeek = arrayOf("Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado") + + val busiestDayStr = if (busiestDayIndex != -1) "${daysOfWeek[busiestDayIndex]} (${String.format("%.1f", maxAvg)})" else "Sin datos" + tvBusiestDay.text = getString(R.string.pattern_busiest_day, busiestDayStr) + tvBestDay.text = getString(R.string.pattern_best_day, if (bestDate.isNotEmpty()) bestDate else "-", maxCount) + } + + private fun loadChartData(days: Int) { + val dataPoints = mutableListOf>() + val calendar = Calendar.getInstance() + + // We want 'days' points ending today + // Start date = Today - (days - 1) + calendar.add(Calendar.DAY_OF_YEAR, -(days - 1)) + + for (i in 0 until days) { + val dateKey = dateKeyFormat.format(calendar.time) + val label = labelFormat.format(calendar.time) + val count = sharedPrefs.getInt("count_$dateKey", 0) + dataPoints.add(Pair(label, count)) + calendar.add(Calendar.DAY_OF_YEAR, 1) + } + lineChart.setData(dataPoints) + } +} diff --git a/app/src/main/java/com/d4vram/cbdcounter/EdgeToEdgeUtils.kt b/app/src/main/java/com/d4vram/cbdcounter/EdgeToEdgeUtils.kt new file mode 100644 index 0000000..d4055ff --- /dev/null +++ b/app/src/main/java/com/d4vram/cbdcounter/EdgeToEdgeUtils.kt @@ -0,0 +1,64 @@ +package com.d4vram.cbdcounter + +import android.os.Build +import android.view.View +import android.view.WindowInsetsController +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat + +/** + * Utilidades para manejar edge-to-edge de forma consistente en toda la app + */ +object EdgeToEdgeUtils { + + /** + * Habilita edge-to-edge en una Activity. + * Usa esto cuando el layout ya tiene fitsSystemWindows="true" en el AppBarLayout/Toolbar. + */ + fun enableEdgeToEdge(activity: AppCompatActivity, lightStatusBar: Boolean = false) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + activity.window.setDecorFitsSystemWindows(false) + activity.window.insetsController?.setSystemBarsAppearance( + if (lightStatusBar) WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS else 0, + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS + ) + } else { + @Suppress("DEPRECATION") + activity.window.decorView.systemUiVisibility = ( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + ) + } + } + + /** + * Configura edge-to-edge en una Activity aplicando padding programáticamente. + * Usa esto cuando el layout NO tiene fitsSystemWindows. + * + * @param activity La activity a configurar + * @param rootView La vista raíz del layout + * @param topView La vista que debe recibir padding top (AppBarLayout o Toolbar) + * @param lightStatusBar true para iconos oscuros en status bar + */ + fun setup( + activity: AppCompatActivity, + rootView: View, + topView: View, + lightStatusBar: Boolean = false + ) { + enableEdgeToEdge(activity, lightStatusBar) + + // Aplicar insets al topView + ViewCompat.setOnApplyWindowInsetsListener(topView) { view, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + view.setPadding( + view.paddingLeft, + insets.top, + view.paddingRight, + view.paddingBottom + ) + WindowInsetsCompat.CONSUMED + } + } +} diff --git a/app/src/main/java/com/d4vram/cbdcounter/EmojiSettingsActivity.kt b/app/src/main/java/com/d4vram/cbdcounter/EmojiSettingsActivity.kt index 99154b1..c7e934f 100644 --- a/app/src/main/java/com/d4vram/cbdcounter/EmojiSettingsActivity.kt +++ b/app/src/main/java/com/d4vram/cbdcounter/EmojiSettingsActivity.kt @@ -22,6 +22,23 @@ class EmojiSettingsActivity : AppCompatActivity() { private lateinit var adapter: EmojiRangeAdapter private lateinit var resetButton: MaterialButton + private val restoreBackupLauncher = registerForActivityResult(androidx.activity.result.contract.ActivityResultContracts.OpenDocument()) { uri -> + if (uri != null) { + try { + if (BackupManager.restoreBackup(this, uri)) { + android.widget.Toast.makeText(this, "Backup restaurado correctamente", android.widget.Toast.LENGTH_LONG).show() + // Recargar datos visuales si es necesario o reiniciar app + setResult(RESULT_OK) + finish() // Cerrar para obligar a recargar MainActivity al volver + } else { + android.widget.Toast.makeText(this, "Error al restaurar backup (formato inválido)", android.widget.Toast.LENGTH_LONG).show() + } + } catch (e: Exception) { + android.widget.Toast.makeText(this, "Error: ${e.message}", android.widget.Toast.LENGTH_LONG).show() + } + } + } + // Lista de rangos con sus emojis por defecto private val emojiRanges = listOf( EmojiRange(0, "😌", R.color.green_safe, "0"), @@ -41,6 +58,9 @@ class EmojiSettingsActivity : AppCompatActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_emoji_settings) + // Configurar color de la barra de estado + window.statusBarColor = ContextCompat.getColor(this, R.color.gradient_start) + // Configurar toolbar val toolbar = findViewById(R.id.settingsToolbar) toolbar.setNavigationOnClickListener { finish() } @@ -57,11 +77,63 @@ class EmojiSettingsActivity : AppCompatActivity() { } recyclerView.adapter = adapter + // Configurar Toggle Sintonía (CBD/THC) + val substanceToggle = findViewById(R.id.substanceToggleGroup) + val currentSubstance = Prefs.getSubstanceType(this) + + if (currentSubstance == "THC") { + substanceToggle.check(R.id.btnThc) + } else { + substanceToggle.check(R.id.btnCbd) + } + + substanceToggle.addOnButtonCheckedListener { _, checkedId, isChecked -> + if (isChecked) { + val type = if (checkedId == R.id.btnThc) "THC" else "CBD" + Prefs.setSubstanceType(this, type) + // TODO: Apply Color Theme changes if necessary immediately or show toast + } + } + // Configurar botón de reset resetButton = findViewById(R.id.resetButton) resetButton.setOnClickListener { showResetConfirmationDialog() } + + // ---- BACKUP UI ---- + val btnCreateBackup = findViewById(R.id.btnCreateBackup) + val btnRestoreBackup = findViewById(R.id.btnRestoreBackup) + val checkEncrypt = findViewById(R.id.checkBackupEncrypt) + + btnCreateBackup.setOnClickListener { + // TODO: Handle encryption if checkEncrypt.isChecked + val backupFile = BackupManager.createBackup(this) + if (backupFile != null) { + shareBackupFile(backupFile) + } else { + android.widget.Toast.makeText(this, "Error al crear backup", android.widget.Toast.LENGTH_SHORT).show() + } + } + + btnRestoreBackup.setOnClickListener { + android.widget.Toast.makeText(this, "Restauración pendiente de implementar", android.widget.Toast.LENGTH_SHORT).show() + // TODO: Pick file intent -> BackupManager.restoreBackup + } + } + + private fun shareBackupFile(file: java.io.File) { + val uri = androidx.core.content.FileProvider.getUriForFile( + this, + "$packageName.fileprovider", + file + ) + val shareIntent = android.content.Intent(android.content.Intent.ACTION_SEND).apply { + type = "application/zip" + putExtra(android.content.Intent.EXTRA_STREAM, uri) + addFlags(android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + startActivity(android.content.Intent.createChooser(shareIntent, "Compartir Backup")) } /** diff --git a/app/src/main/java/com/d4vram/cbdcounter/EvolutionActivity.kt b/app/src/main/java/com/d4vram/cbdcounter/EvolutionActivity.kt index 4f115ed..142e24b 100644 --- a/app/src/main/java/com/d4vram/cbdcounter/EvolutionActivity.kt +++ b/app/src/main/java/com/d4vram/cbdcounter/EvolutionActivity.kt @@ -26,6 +26,8 @@ class EvolutionActivity : AppCompatActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_evolution) + window.statusBarColor = getColor(R.color.gradient_start) + val toolbar = findViewById(R.id.evolutionToolbar) toolbar.setNavigationOnClickListener { finish() } diff --git a/app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt b/app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt index bbd3c9f..d9fbf19 100644 --- a/app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt +++ b/app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt @@ -41,7 +41,11 @@ import kotlin.collections.ArrayList class MainActivity : AppCompatActivity() { // Views principales - private lateinit var counterText: TextView + private lateinit var counterText: TextView // Oculto, para compatibilidad + private lateinit var cbdCountText: TextView + private lateinit var thcCountText: TextView + private lateinit var cbdContainer: View + private lateinit var thcContainer: View private lateinit var dateText: TextView private lateinit var emojiText: TextView private lateinit var addButton: Button @@ -68,7 +72,9 @@ class MainActivity : AppCompatActivity() { // Data private lateinit var sharedPrefs: SharedPreferences - private var currentCount = 0 + private var cbdCount = 0 + private var thcCount = 0 + private val currentCount: Int get() = cbdCount + thcCount // Total para emoji y compatibilidad private val allHistoryData = ArrayList() private val displayedHistoryData = ArrayList() private var currentViewMode = ViewMode.WEEK @@ -121,8 +127,9 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { // 1. Aplicar tema ANTES de super.onCreate para evitar flickering initSharedPreferences() + Prefs.migrateToV14IfNeeded(this) // Migrar datos al nuevo formato si es necesario applyStoredTheme() - + super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) @@ -187,7 +194,11 @@ class MainActivity : AppCompatActivity() { private fun initViews() { // Views principales - counterText = findViewById(R.id.counterText) + counterText = findViewById(R.id.counterText) // Oculto, para compatibilidad + cbdCountText = findViewById(R.id.cbdCountText) + thcCountText = findViewById(R.id.thcCountText) + cbdContainer = findViewById(R.id.cbdContainer) + thcContainer = findViewById(R.id.thcContainer) dateText = findViewById(R.id.dateText) emojiText = findViewById(R.id.emojiText) addButton = findViewById(R.id.addButton) @@ -278,24 +289,24 @@ class MainActivity : AppCompatActivity() { private fun loadTodayData() { val today = getCurrentDateKey() - currentCount = sharedPrefs.getInt("count_$today", 0) + cbdCount = Prefs.getCbdCount(this, today) + thcCount = Prefs.getThcCount(this, today) } private fun loadAllHistoryData() { allHistoryData.clear() - val allEntries = sharedPrefs.all val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) - allEntries.forEach { (key, value) -> - if (key.startsWith("count_") && value is Int) { - val dateString = key.removePrefix("count_") - try { - val date = dateFormat.parse(dateString) - if (date != null) { - allHistoryData.add(HistoryItem(dateString, value, date)) - } - } catch (_: Exception) {} - } + val allDates = Prefs.getAllDatesWithData(this) + allDates.forEach { dateString -> + try { + val date = dateFormat.parse(dateString) + if (date != null) { + val cbd = Prefs.getCbdCount(this, dateString) + val thc = Prefs.getThcCount(this, dateString) + allHistoryData.add(HistoryItem(dateString, cbd, thc, date)) + } + } catch (_: Exception) {} } allHistoryData.sortByDescending { it.dateObject } } @@ -335,9 +346,9 @@ class MainActivity : AppCompatActivity() { streakText.text = "Racha: 0 días" return } - val average = displayedHistoryData.map { it.count }.average() + val average = displayedHistoryData.map { it.totalCount }.average() avgText.text = "Promedio: %.1f".format(average) - val total = displayedHistoryData.sumOf { it.count } + val total = displayedHistoryData.sumOf { it.totalCount } totalText.text = "Total: $total" val streak = calculateCleanStreak() streakText.text = "Racha limpia: $streak días" @@ -347,14 +358,16 @@ class MainActivity : AppCompatActivity() { var streak = 0 val sortedData = allHistoryData.sortedByDescending { it.dateObject } for (item in sortedData) { - if (item.count == 0) streak++ else break + if (item.totalCount == 0) streak++ else break } return streak } private fun saveData() { val today = getCurrentDateKey() - sharedPrefs.edit().putInt("count_$today", currentCount).apply() + Prefs.setCbdCount(this, today, cbdCount) + Prefs.setThcCount(this, today, thcCount) + loadAllHistoryData() updateHistoryView() updateStats() @@ -385,10 +398,14 @@ class MainActivity : AppCompatActivity() { } private fun updateDisplay(animate: Boolean = true) { - counterText.text = currentCount.toString() + // Actualizar contadores duales + cbdCountText.text = cbdCount.toString() + thcCountText.text = thcCount.toString() + counterText.text = currentCount.toString() // Total oculto para compatibilidad + dateText.text = getCurrentDateDisplay() val newEmoji = getEmoji(currentCount) - + if (animate && emojiText.text != newEmoji && emojiText.text.isNotEmpty()) { emojiText.animate().alpha(0f).setDuration(150).withEndAction { emojiText.text = newEmoji @@ -399,13 +416,56 @@ class MainActivity : AppCompatActivity() { emojiText.text = newEmoji } - val color = when { - currentCount == 0 -> R.color.green_safe - currentCount <= 3 -> R.color.yellow_warning - currentCount <= 6 -> R.color.orange_danger + // Destacar el modo activo + val isThc = Prefs.getSubstanceType(this) == "THC" + highlightActiveCounter(isThc) + + // Actualizar colores según cantidad + updateCounterColors() + } + + private fun highlightActiveCounter(isThcActive: Boolean) { + // El contador activo se ve más grande/destacado + val activeScale = 1.1f + val inactiveScale = 0.9f + val activeAlpha = 1.0f + val inactiveAlpha = 0.6f + + if (isThcActive) { + thcContainer.scaleX = activeScale + thcContainer.scaleY = activeScale + thcContainer.alpha = activeAlpha + cbdContainer.scaleX = inactiveScale + cbdContainer.scaleY = inactiveScale + cbdContainer.alpha = inactiveAlpha + } else { + cbdContainer.scaleX = activeScale + cbdContainer.scaleY = activeScale + cbdContainer.alpha = activeAlpha + thcContainer.scaleX = inactiveScale + thcContainer.scaleY = inactiveScale + thcContainer.alpha = inactiveAlpha + } + } + + private fun updateCounterColors() { + // Colores CBD según cantidad + val cbdColor = when { + cbdCount == 0 -> R.color.green_safe + cbdCount <= 4 -> R.color.cbd_text + cbdCount <= 6 -> R.color.orange_danger else -> R.color.red_critical } - counterText.setTextColor(ContextCompat.getColor(this, color)) + cbdCountText.setTextColor(ContextCompat.getColor(this, cbdColor)) + + // Colores THC según cantidad + val thcColor = when { + thcCount == 0 -> R.color.green_safe + thcCount <= 4 -> R.color.thc_text + thcCount <= 6 -> R.color.orange_danger + else -> R.color.red_critical + } + thcCountText.setTextColor(ContextCompat.getColor(this, thcColor)) } private fun getEmoji(count: Int): String = EmojiUtils.emojiForCount(count, this) @@ -419,10 +479,13 @@ class MainActivity : AppCompatActivity() { startActivity(Intent(this, SettingsActivity::class.java)) } subtractButton.setOnClickListener { - if (currentCount > 0) { + val isThc = Prefs.getSubstanceType(this) == "THC" + val activeCount = if (isThc) thcCount else cbdCount + + if (activeCount > 0) { // Inflar layout personalizado val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_confirm_delete, null) - + // Crear el diálogo val dialog = MaterialAlertDialogBuilder(this) .setView(dialogView) @@ -431,17 +494,18 @@ class MainActivity : AppCompatActivity() { // Configurar chips dialogView.findViewById(R.id.chip_confirm).setOnClickListener { - currentCount-- + if (isThc) thcCount-- else cbdCount-- updateDisplay() - removeLastEntryFromTodayNote() // 🎯 Borrar último timestamp + removeLastEntryFromTodayNote() // Borrar último timestamp saveData() animateCounter(0.9f) - showFeedback(getString(R.string.cbd_subtracted), true) + val msg = if (isThc) getString(R.string.thc_subtracted) else getString(R.string.cbd_subtracted) + showFeedback(msg, true) dialog.dismiss() } dialogView.findViewById(R.id.chip_keep_note).setOnClickListener { - currentCount-- + if (isThc) thcCount-- else cbdCount-- updateDisplay() // NO borramos la nota, solo restamos el contador saveData() @@ -454,7 +518,7 @@ class MainActivity : AppCompatActivity() { dialog.dismiss() } - // Mostrar con fondo transparente para que se vea bien el card (opcional, pero recomendado si el root es CardView) + // Mostrar con fondo transparente para que se vea bien el card dialog.window?.setBackgroundDrawableResource(android.R.color.transparent) dialog.show() } @@ -462,9 +526,10 @@ class MainActivity : AppCompatActivity() { resetButton.setOnClickListener { AlertDialog.Builder(this) .setTitle("Reiniciar contador") - .setMessage("¿Estás seguro de que quieres reiniciar el contador de hoy?") + .setMessage("¿Estás seguro de que quieres reiniciar el contador de hoy? (CBD y THC)") .setPositiveButton("Sí") { _, _ -> - currentCount = 0 + cbdCount = 0 + thcCount = 0 updateDisplay() saveData() showFeedback("¡Día reiniciado! 💪", true) @@ -534,32 +599,27 @@ class MainActivity : AppCompatActivity() { } private fun buildCsvContent(): String { - val prefsMap = sharedPrefs.all - if (prefsMap.isEmpty()) return "" - - val dates = mutableSetOf() - prefsMap.keys.forEach { key -> - when { - key.startsWith("count_") -> dates.add(key.removePrefix("count_")) - key.startsWith("NOTE_") -> dates.add(key.removePrefix("NOTE_")) - } - } - if (dates.isEmpty()) return "" + val allDates = Prefs.getAllDatesWithData(this) + if (allDates.isEmpty()) return "" val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) - val sortedDates = dates.mapNotNull { dateString -> + val sortedDates = allDates.mapNotNull { dateString -> runCatching { dateFormat.parse(dateString) }.getOrNull()?.let { parsed -> dateString to parsed } }.sortedBy { it.second } - val builder = StringBuilder("date,count,note\n") + val builder = StringBuilder("date,count_cbd,count_thc,note\n") sortedDates.forEach { (dateString, _) -> - val count = sharedPrefs.getInt("count_$dateString", 0) + val cbdCount = Prefs.getCbdCount(this, dateString) + val thcCount = Prefs.getThcCount(this, dateString) val note = Prefs.getNote(this, dateString) ?: "" + builder.append(dateString) .append(',') - .append(count) + .append(cbdCount) + .append(',') + .append(thcCount) .append(',') .append(escapeCsvField(note)) .append('\n') @@ -575,23 +635,51 @@ class MainActivity : AppCompatActivity() { if (lines.isEmpty()) throw IllegalArgumentException("Archivo vacío") val editor = sharedPrefs.edit() + // Limpiar datos existentes sharedPrefs.all.keys.filter { it.startsWith("count_") || it.startsWith("NOTE_") }.forEach { key -> editor.remove(key) } + // Detectar formato por cabecera + val header = lines.first().lowercase() + val isNewFormat = header.contains("count_cbd") + lines.drop(1).forEach { line -> if (line.isBlank()) return@forEach val columns = splitCsvLine(line) if (columns.size < 2) return@forEach val date = columns[0] - val count = columns[1].toIntOrNull() ?: return@forEach - editor.putInt("count_$date", count) - val rawNote = if (columns.size >= 3) columns[2] else "" - val note = unescapeCsvField(rawNote) - if (note.isNotEmpty()) { - editor.putString("NOTE_$date", note) + if (isNewFormat) { + // Nuevo formato: date,count_cbd,count_thc,note + val cbdCount = columns.getOrNull(1)?.toIntOrNull() ?: 0 + val thcCount = columns.getOrNull(2)?.toIntOrNull() ?: 0 + editor.putInt("${Prefs.KEY_COUNT_CBD_PREFIX}$date", cbdCount) + editor.putInt("${Prefs.KEY_COUNT_THC_PREFIX}$date", thcCount) + + val rawNote = columns.getOrNull(3) ?: "" + val note = unescapeCsvField(rawNote) + if (note.isNotEmpty()) { + editor.putString("${Prefs.KEY_NOTE_PREFIX}$date", note) + } + } else { + // Formato legacy: date,count,note,substance + val count = columns[1].toIntOrNull() ?: return@forEach + val substance = if (columns.size >= 4) unescapeCsvField(columns[3]) else "CBD" + + // Importar al contador correspondiente + if (substance == "THC") { + editor.putInt("${Prefs.KEY_COUNT_THC_PREFIX}$date", count) + } else { + editor.putInt("${Prefs.KEY_COUNT_CBD_PREFIX}$date", count) + } + + val rawNote = if (columns.size >= 3) columns[2] else "" + val note = unescapeCsvField(rawNote) + if (note.isNotEmpty()) { + editor.putString("${Prefs.KEY_NOTE_PREFIX}$date", note) + } } } editor.apply() @@ -678,14 +766,32 @@ class MainActivity : AppCompatActivity() { } private fun registerStandardIntake() { - val entry = "🔹 ${getCurrentTimestamp()}" - registerIntake(entry, getString(R.string.cbd_added)) + val isThc = Prefs.getSubstanceType(this) == "THC" + val entry = if (isThc) "🟢 ${getCurrentTimestamp()}" else "🔹 ${getCurrentTimestamp()}" + val feedback = if (isThc) getString(R.string.thc_added) else getString(R.string.cbd_added) + registerIntake(entry, feedback) } private fun showInfusionDialog() { val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_infusion_choice, null) val weedButton = dialogView.findViewById(R.id.weedButton) val polemButton = dialogView.findViewById(R.id.polemButton) + val title = dialogView.findViewById(R.id.infusionTitle) + val subtitle = dialogView.findViewById(R.id.infusionSubtitle) + + // Adjust text based on substance + val substanceType = Prefs.getSubstanceType(this) + if (substanceType == "THC") { + title.text = getString(R.string.infusion_question_thc) + subtitle.text = getString(R.string.infusion_subtitle_thc) + + // Adjust colors for THC mode + weedButton.setTextColor(ContextCompat.getColor(this, R.color.thc_weed_orange)) + weedButton.strokeColor = androidx.core.content.res.ResourcesCompat.getColorStateList(resources, R.color.thc_weed_outline, theme) + polemButton.setTextColor(ContextCompat.getColor(this, R.color.thc_weed_orange)) + polemButton.strokeColor = androidx.core.content.res.ResourcesCompat.getColorStateList(resources, R.color.thc_weed_outline, theme) + } + // Default CBD strings are already in layout (or we can set them explicitely) val dialog = MaterialAlertDialogBuilder(this) .setView(dialogView) @@ -715,7 +821,12 @@ class MainActivity : AppCompatActivity() { } private fun registerIntake(entry: String, feedbackMessage: String) { - currentCount++ + val isThc = Prefs.getSubstanceType(this) == "THC" + if (isThc) { + thcCount++ + } else { + cbdCount++ + } updateDisplay() appendEntryToTodayNote(entry) saveData() @@ -796,12 +907,19 @@ class MainActivity : AppCompatActivity() { } private fun openStatsCalendar() { - startActivity(Intent(this, StatsActivity::class.java)) + startActivity(Intent(this, DashboardActivity::class.java)) } } // Data class -data class HistoryItem(val date: String, val count: Int, val dateObject: Date) +data class HistoryItem( + val date: String, + val cbdCount: Int, + val thcCount: Int, + val dateObject: Date +) { + val totalCount: Int get() = cbdCount + thcCount +} // Adapter class ImprovedHistoryAdapter( @@ -839,7 +957,9 @@ class ImprovedHistoryAdapter( class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val dateText: TextView = itemView.findViewById(R.id.historyDate) - val countText: TextView = itemView.findViewById(R.id.historyCount) + val countText: TextView = itemView.findViewById(R.id.historyCount) // Oculto + val cbdChip: TextView = itemView.findViewById(R.id.cbdChip) + val thcChip: TextView = itemView.findViewById(R.id.thcChip) val emojiText: TextView = itemView.findViewById(R.id.historyEmoji) val progressBar: View = itemView.findViewById(R.id.progressBar) val noteBadge: TextView? = itemView.findViewById(R.id.noteBadge) @@ -866,48 +986,84 @@ class ImprovedHistoryAdapter( val dayFormat = SimpleDateFormat("EEEE dd", Locale("es", "ES")) holder.dateText.text = dayFormat.format(item.dateObject) .replaceFirstChar { it.uppercase() } - holder.countText.text = "${item.count} CBD" + // Mostrar chips según los datos + if (item.cbdCount > 0) { + holder.cbdChip.text = "${item.cbdCount} CBD" + holder.cbdChip.visibility = View.VISIBLE + } else { + holder.cbdChip.visibility = View.GONE + } + + if (item.thcCount > 0) { + holder.thcChip.text = "${item.thcCount} THC" + holder.thcChip.visibility = View.VISIBLE + } else { + holder.thcChip.visibility = View.GONE + } + + // Si ambos son 0, mostrar chip CBD con 0 + if (item.cbdCount == 0 && item.thcCount == 0) { + holder.cbdChip.text = "0 CBD" + holder.cbdChip.visibility = View.VISIBLE + } + + val total = item.totalCount holder.emojiText.text = when { - item.count == 0 -> "😌" - item.count <= 2 -> "🙂" - item.count <= 4 -> "😄" - item.count <= 5 -> "🫠" - item.count <= 6 -> "🤔" - item.count <= 7 -> "🙄" - item.count <= 8 -> "😶‍🌫️" - item.count <= 9 -> "🫡" - item.count <= 10 -> "🫥" - item.count <= 11 -> "⛔️" + total == 0 -> "😌" + total <= 2 -> "🙂" + total <= 4 -> "😄" + total <= 5 -> "🫠" + total <= 6 -> "🤔" + total <= 7 -> "🙄" + total <= 8 -> "😶‍🌫️" + total <= 9 -> "🫡" + total <= 10 -> "🫥" + total <= 11 -> "⛔️" else -> "💀" } - // Barra de progreso (como ya tenías) + // Barra de progreso basada en el total val maxWidth = holder.itemView.width - val progress = minOf(item.count / 10f, 1f) + val progress = minOf(total / 10f, 1f) val layoutParams = holder.progressBar.layoutParams layoutParams.width = (maxWidth * progress).toInt() holder.progressBar.layoutParams = layoutParams - val color = when { - item.count == 0 -> R.color.green_safe - item.count <= 3 -> R.color.yellow_warning - item.count <= 6 -> R.color.orange_danger - else -> R.color.red_critical + + // Color de la barra: del mayor, o verde si es 0 + val barColor = when { + total == 0 -> R.color.green_safe + item.thcCount > item.cbdCount -> { + // THC es mayor, usar escala verde + when { + total <= 4 -> R.color.thc_primary_light + total <= 6 -> R.color.orange_danger + else -> R.color.red_critical + } + } + else -> { + // CBD es mayor o igual, usar escala azul + when { + total <= 4 -> R.color.primary_light + total <= 6 -> R.color.orange_danger + else -> R.color.red_critical + } + } } holder.progressBar.setBackgroundColor( - ContextCompat.getColor(holder.itemView.context, color) + ContextCompat.getColor(holder.itemView.context, barColor) ) - // --- NUEVO: badge de nota visible si existe nota para ese día + // Badge de nota visible si existe nota para ese día val ctx = holder.itemView.context holder.noteBadge?.visibility = if (Prefs.hasNote(ctx, item.date)) View.VISIBLE else View.GONE - // --- NUEVO: badge de audio visible si existe audio para ese día + // Badge de audio visible si existe audio para ese día holder.audioBadge?.visibility = if (Prefs.hasAudio(ctx, item.date)) View.VISIBLE else View.GONE - // --- NUEVO: clicks para abrir el modal + // Clicks para abrir el modal holder.itemView.setOnClickListener { onDayClick(item.date) } holder.noteBadge?.setOnClickListener { onDayClick(item.date) } holder.audioBadge?.setOnClickListener { onDayClick(item.date) } diff --git a/app/src/main/java/com/d4vram/cbdcounter/Prefs.kt b/app/src/main/java/com/d4vram/cbdcounter/Prefs.kt index a1bdc60..1ad3352 100644 --- a/app/src/main/java/com/d4vram/cbdcounter/Prefs.kt +++ b/app/src/main/java/com/d4vram/cbdcounter/Prefs.kt @@ -6,11 +6,14 @@ import java.io.File object Prefs { private const val PREFS_NAME = "CBDCounter" - + // Constantes de claves para evitar errores tipográficos - const val KEY_COUNT_PREFIX = "count_" + const val KEY_COUNT_PREFIX = "count_" // Legacy, para migración + const val KEY_COUNT_CBD_PREFIX = "count_cbd_" + const val KEY_COUNT_THC_PREFIX = "count_thc_" const val KEY_NOTE_PREFIX = "NOTE_" const val KEY_DARK_MODE = "dark_mode" + private const val KEY_MIGRATION_V14_DONE = "migration_v1.4_done" // función privada para acceder a las SharedPreferences private fun prefs(ctx: Context): SharedPreferences = @@ -37,4 +40,106 @@ object Prefs { val audioFile = File(ctx.filesDir, "audios/audio_$date.mp3") return audioFile.exists() } + + // ---- Sustancia (CBD vs THC) - Modo activo ---- + fun getSubstanceType(ctx: Context): String = + prefs(ctx).getString("substance_type", "CBD") ?: "CBD" + + fun setSubstanceType(ctx: Context, type: String) { + prefs(ctx).edit().putString("substance_type", type).apply() + } + + // ---- Contadores separados CBD/THC ---- + fun getCbdCount(ctx: Context, date: String): Int = + prefs(ctx).getInt("${KEY_COUNT_CBD_PREFIX}$date", 0) + + fun setCbdCount(ctx: Context, date: String, count: Int) { + prefs(ctx).edit().putInt("${KEY_COUNT_CBD_PREFIX}$date", count).apply() + } + + fun getThcCount(ctx: Context, date: String): Int = + prefs(ctx).getInt("${KEY_COUNT_THC_PREFIX}$date", 0) + + fun setThcCount(ctx: Context, date: String, count: Int) { + prefs(ctx).edit().putInt("${KEY_COUNT_THC_PREFIX}$date", count).apply() + } + + fun getTotalCount(ctx: Context, date: String): Int = + getCbdCount(ctx, date) + getThcCount(ctx, date) + + /** Incrementa el contador del modo activo y devuelve el nuevo valor */ + fun incrementActiveCount(ctx: Context, date: String): Int { + val isThc = getSubstanceType(ctx) == "THC" + return if (isThc) { + val newCount = getThcCount(ctx, date) + 1 + setThcCount(ctx, date, newCount) + newCount + } else { + val newCount = getCbdCount(ctx, date) + 1 + setCbdCount(ctx, date, newCount) + newCount + } + } + + /** Decrementa el contador del modo activo (mínimo 0) y devuelve el nuevo valor */ + fun decrementActiveCount(ctx: Context, date: String): Int { + val isThc = getSubstanceType(ctx) == "THC" + return if (isThc) { + val newCount = maxOf(0, getThcCount(ctx, date) - 1) + setThcCount(ctx, date, newCount) + newCount + } else { + val newCount = maxOf(0, getCbdCount(ctx, date) - 1) + setCbdCount(ctx, date, newCount) + newCount + } + } + + /** Obtiene el conteo del modo activo */ + fun getActiveCount(ctx: Context, date: String): Int { + val isThc = getSubstanceType(ctx) == "THC" + return if (isThc) getThcCount(ctx, date) else getCbdCount(ctx, date) + } + + // ---- Migración v1.4 ---- + fun migrateToV14IfNeeded(ctx: Context) { + val prefs = prefs(ctx) + if (prefs.getBoolean(KEY_MIGRATION_V14_DONE, false)) return + + val editor = prefs.edit() + val allEntries = prefs.all + + // Migrar count_* → count_cbd_* + allEntries.keys + .filter { it.startsWith(KEY_COUNT_PREFIX) && !it.startsWith(KEY_COUNT_CBD_PREFIX) && !it.startsWith(KEY_COUNT_THC_PREFIX) } + .forEach { oldKey -> + val date = oldKey.removePrefix(KEY_COUNT_PREFIX) + val count = allEntries[oldKey] as? Int ?: 0 + editor.putInt("${KEY_COUNT_CBD_PREFIX}$date", count) + editor.remove(oldKey) + } + + // Eliminar claves substance_* obsoletas + allEntries.keys + .filter { it.startsWith("substance_") && it != "substance_type" } + .forEach { editor.remove(it) } + + editor.putBoolean(KEY_MIGRATION_V14_DONE, true) + editor.apply() + } + + /** Obtiene todas las fechas que tienen datos (para historial) */ + fun getAllDatesWithData(ctx: Context): Set { + val prefs = prefs(ctx) + val dates = mutableSetOf() + + prefs.all.keys.forEach { key -> + when { + key.startsWith(KEY_COUNT_CBD_PREFIX) -> dates.add(key.removePrefix(KEY_COUNT_CBD_PREFIX)) + key.startsWith(KEY_COUNT_THC_PREFIX) -> dates.add(key.removePrefix(KEY_COUNT_THC_PREFIX)) + key.startsWith(KEY_NOTE_PREFIX) -> dates.add(key.removePrefix(KEY_NOTE_PREFIX)) + } + } + return dates + } } diff --git a/app/src/main/java/com/d4vram/cbdcounter/SettingsActivity.kt b/app/src/main/java/com/d4vram/cbdcounter/SettingsActivity.kt index 822a19c..c0e5015 100644 --- a/app/src/main/java/com/d4vram/cbdcounter/SettingsActivity.kt +++ b/app/src/main/java/com/d4vram/cbdcounter/SettingsActivity.kt @@ -2,49 +2,130 @@ package com.d4vram.cbdcounter import android.content.Intent import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView import android.widget.Toast +import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.button.MaterialButton +import com.google.android.material.button.MaterialButtonToggleGroup import com.google.android.material.switchmaterial.SwitchMaterial import java.io.File import java.io.FileInputStream import java.io.FileOutputStream +import java.text.SimpleDateFormat +import java.util.* import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream -import javax.crypto.Cipher -import javax.crypto.CipherOutputStream -import javax.crypto.spec.SecretKeySpec class SettingsActivity : AppCompatActivity() { - private lateinit var exportAudioZipButton: MaterialButton - private lateinit var backupCsvButton: MaterialButton - private lateinit var autoBackupSwitch: SwitchMaterial + private lateinit var recyclerView: RecyclerView + private lateinit var adapter: EmojiRangeAdapter + + // Lista de rangos con sus emojis por defecto + private val emojiRanges = listOf( + EmojiRange(0, "😌", R.color.green_safe, "0"), + EmojiRange(1, "🙂", R.color.green_safe, "1-2"), + EmojiRange(3, "😄", R.color.yellow_warning, "3-4"), + EmojiRange(5, "🫠", R.color.yellow_warning, "5"), + EmojiRange(6, "🤔", R.color.orange_danger, "6"), + EmojiRange(7, "🙄", R.color.orange_danger, "7"), + EmojiRange(8, "😶‍🌫️", R.color.orange_danger, "8"), + EmojiRange(9, "🫡", R.color.red_critical, "9"), + EmojiRange(10, "🫥", R.color.red_critical, "10"), + EmojiRange(11, "⛔️", R.color.red_critical, "11"), + EmojiRange(12, "💀", R.color.primary_purple, "12+") + ) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_settings) - exportAudioZipButton = findViewById(R.id.exportAudioZipButton) - backupCsvButton = findViewById(R.id.backupCsvButton) - autoBackupSwitch = findViewById(R.id.autoBackupSwitch) + // Forzar status bar con color del toolbar + window.statusBarColor = getColor(R.color.gradient_start) - // Load auto backup preference - autoBackupSwitch.isChecked = getSharedPreferences("CBDCounter", MODE_PRIVATE) - .getBoolean("auto_backup", false) + // Toolbar + val toolbar = findViewById(R.id.settingsToolbar) + toolbar.setNavigationOnClickListener { finish() } + + setupSubstanceToggle() + setupBackupSection() + setupEmojiSection() + } - exportAudioZipButton.setOnClickListener { - exportAudiosZip() + // ======================================== + // SECCIÓN: Tipo de Sustancia (CBD/THC) + // ======================================== + private fun setupSubstanceToggle() { + val substanceToggle = findViewById(R.id.substanceToggleGroup) + val currentSubstance = Prefs.getSubstanceType(this) + + if (currentSubstance == "THC") { + substanceToggle.check(R.id.btnThc) + } else { + substanceToggle.check(R.id.btnCbd) } - backupCsvButton.setOnClickListener { - backupCsvManual() + substanceToggle.addOnButtonCheckedListener { _, checkedId, isChecked -> + if (isChecked) { + val type = if (checkedId == R.id.btnThc) "THC" else "CBD" + Prefs.setSubstanceType(this, type) + Toast.makeText(this, "Modo $type activado", Toast.LENGTH_SHORT).show() + } } + } + + // ======================================== + // SECCIÓN: Backup + // ======================================== + private fun setupBackupSection() { + val autoBackupSwitch = findViewById(R.id.switchBackupAuto) + val btnBackupCsv = findViewById(R.id.btnBackupCsv) + val btnExportAudios = findViewById(R.id.btnExportAudios) + + // Cargar preferencia de auto backup + autoBackupSwitch.isChecked = getSharedPreferences("CBDCounter", MODE_PRIVATE) + .getBoolean("auto_backup", false) autoBackupSwitch.setOnCheckedChangeListener { _, isChecked -> getSharedPreferences("CBDCounter", MODE_PRIVATE).edit() .putBoolean("auto_backup", isChecked).apply() - Toast.makeText(this, "Backup automático ${if (isChecked) "activado" else "desactivado"}", Toast.LENGTH_SHORT).show() + Toast.makeText( + this, + "Backup automático ${if (isChecked) "activado" else "desactivado"}", + Toast.LENGTH_SHORT + ).show() + } + + btnBackupCsv.setOnClickListener { exportCsvBackup() } + btnExportAudios.setOnClickListener { exportAudiosZip() } + } + + private fun exportCsvBackup() { + val csvContent = buildCsvContent() + if (csvContent.isBlank()) { + Toast.makeText(this, "No hay datos para exportar", Toast.LENGTH_SHORT).show() + return + } + + val backupDir = File(cacheDir, "backups").apply { if (!exists()) mkdirs() } + val fileName = "cbdcounter_backup_${SimpleDateFormat("yyyyMMdd_HHmm", Locale.getDefault()).format(Date())}.csv" + val file = File(backupDir, fileName) + + try { + file.writeText(csvContent, Charsets.UTF_8) + shareFile(file, "text/csv", "Compartir Backup CSV") + Toast.makeText(this, "Backup CSV exportado", Toast.LENGTH_SHORT).show() + } catch (e: Exception) { + Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_SHORT).show() } } @@ -58,7 +139,6 @@ class SettingsActivity : AppCompatActivity() { val zipFile = File(cacheDir, "audios_export.zip") try { ZipOutputStream(FileOutputStream(zipFile)).use { zos -> - // For simplicity, no encryption yet; add dialog for password later audioDir.listFiles()?.forEach { file -> FileInputStream(file).use { fis -> zos.putNextEntry(ZipEntry(file.name)) @@ -67,46 +147,21 @@ class SettingsActivity : AppCompatActivity() { } } } - - // Share the ZIP - val uri = androidx.core.content.FileProvider.getUriForFile(this, "$packageName.fileprovider", zipFile) - val shareIntent = Intent(Intent.ACTION_SEND).apply { - type = "application/zip" - putExtra(Intent.EXTRA_STREAM, uri) - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - } - startActivity(Intent.createChooser(shareIntent, "Compartir ZIP de audios")) - Toast.makeText(this, "ZIP exportado", Toast.LENGTH_SHORT).show() + shareFile(zipFile, "application/zip", "Compartir ZIP de audios") + Toast.makeText(this, "Audios exportados", Toast.LENGTH_SHORT).show() } catch (e: Exception) { - Toast.makeText(this, "Error al exportar: ${e.message}", Toast.LENGTH_SHORT).show() + Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_SHORT).show() } } - private fun backupCsvManual() { - // Reuse logic from MainActivity - val csvContent = buildCsvContent() - if (csvContent.isBlank()) { - Toast.makeText(this, "No hay datos para backup", Toast.LENGTH_SHORT).show() - return - } - - val backupDir = File(cacheDir, "backups").apply { if (!exists()) mkdirs() } - val fileName = "backup_" + java.text.SimpleDateFormat("yyyyMMdd_HHmm", java.util.Locale.getDefault()).format(java.util.Date()) + ".csv" - val file = File(backupDir, fileName) - - try { - file.writeText(csvContent, Charsets.UTF_8) - val uri = androidx.core.content.FileProvider.getUriForFile(this, "$packageName.fileprovider", file) - val shareIntent = Intent(Intent.ACTION_SEND).apply { - type = "text/csv" - putExtra(Intent.EXTRA_STREAM, uri) - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - } - startActivity(Intent.createChooser(shareIntent, "Compartir Backup CSV")) - Toast.makeText(this, "Backup CSV exportado", Toast.LENGTH_SHORT).show() - } catch (e: Exception) { - Toast.makeText(this, "Error al crear backup: ${e.message}", Toast.LENGTH_SHORT).show() + private fun shareFile(file: File, mimeType: String, title: String) { + val uri = FileProvider.getUriForFile(this, "$packageName.fileprovider", file) + val shareIntent = Intent(Intent.ACTION_SEND).apply { + type = mimeType + putExtra(Intent.EXTRA_STREAM, uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } + startActivity(Intent.createChooser(shareIntent, title)) } private fun buildCsvContent(): String { @@ -123,7 +178,7 @@ class SettingsActivity : AppCompatActivity() { } if (dates.isEmpty()) return "" - val dateFormat = java.text.SimpleDateFormat("dd/MM/yyyy", java.util.Locale.getDefault()) + val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) val sortedDates = dates.mapNotNull { dateString -> kotlin.runCatching { dateFormat.parse(dateString) }.getOrNull()?.let { parsed -> dateString to parsed @@ -146,15 +201,144 @@ class SettingsActivity : AppCompatActivity() { private fun escapeCsvField(value: String): String { if (value.isEmpty()) return "" - val builder = StringBuilder() - value.forEach { char -> - when (char) { - '\\' -> builder.append("\\\\") - '\n' -> builder.append("\\n") - ',' -> builder.append("\\,") - else -> builder.append(char) + return value.replace("\\", "\\\\").replace("\n", "\\n").replace(",", "\\,") + } + + // ======================================== + // SECCIÓN: Personalizar Emojis + // ======================================== + private fun setupEmojiSection() { + recyclerView = findViewById(R.id.emojiRangesRecycler) + recyclerView.layoutManager = LinearLayoutManager(this) + + val currentEmojis = loadCustomEmojis() + adapter = EmojiRangeAdapter(emojiRanges, currentEmojis) { range, newEmoji -> + saveCustomEmoji(range.count, newEmoji) + } + recyclerView.adapter = adapter + + findViewById(R.id.resetEmojiButton).setOnClickListener { + showResetConfirmationDialog() + } + } + + private fun loadCustomEmojis(): Map { + val prefs = getSharedPreferences("emoji_prefs", MODE_PRIVATE) + val customEmojis = mutableMapOf() + for (range in emojiRanges) { + prefs.getString("emoji_${range.count}", null)?.let { + customEmojis[range.count] = it } } - return builder.toString() + return customEmojis + } + + private fun saveCustomEmoji(count: Int, emoji: String) { + getSharedPreferences("emoji_prefs", MODE_PRIVATE) + .edit().putString("emoji_$count", emoji).apply() + } + + private fun resetToDefaults() { + getSharedPreferences("emoji_prefs", MODE_PRIVATE).edit().clear().apply() + adapter.resetToDefaults() + Toast.makeText(this, "Emojis restaurados", Toast.LENGTH_SHORT).show() + } + + private fun showResetConfirmationDialog() { + AlertDialog.Builder(this) + .setTitle("Restaurar emojis") + .setMessage("¿Restaurar todos los emojis a sus valores por defecto?") + .setPositiveButton("Sí") { _, _ -> resetToDefaults() } + .setNegativeButton("Cancelar", null) + .show() + } + + private fun showEmojiPicker(currentEmoji: String, onEmojiSelected: (String) -> Unit) { + val emojis = listOf( + "😌", "🙂", "😊", "😀", "😃", "😄", "😁", "😆", "😅", "🤣", + "😂", "🙃", "😉", "😇", "🤩", "☺️", "🥲", "😋", "😛", "😜", "🤪", "😝", + "🤔", "🤨", "😐", "😑", "😶", "🙄", "😣", "😥", "😮", "😯", "😪", "😫", "🥱", "😴", "🤤", + "🫠", "😵", "😵‍💫", "🤯", "🥴", "😲", + "🫡", "😬", "🫨", "🫥", + "😞", "😔", "😟", "😕", "🙁", "☹️", "😰", "😨", "😧", "😦", "😈", + "👿", "💀", "☠️", "👻", "👽", "👾", + "👍", "👎", "🤞", "✌️", "👌", "🤌", "🤏", "✋", "🤚", + "🌿", "🍀", "🌱", "🌾", "🪴", "🍃", + "⚠️", "🚫", "⛔️", "🔞", "📵", "🔕", "❌", "⭕️", "❗️", "❓", + "🟢", "🟡", "🟠", "🔴", "🟣", "🔵", "🟤", "⚫️", "⚪️", + "💚", "💛", "🧡", "❤️", "💜", "💙", "🖤", "🤍", "🤎", "💯", + "💥", "💫", "⭐️", "🌟", "✨", "⚡️", "🔥" + ) + + val emojiArray = emojis.toTypedArray() + var selectedIndex = emojis.indexOf(currentEmoji).takeIf { it >= 0 } ?: 0 + + AlertDialog.Builder(this) + .setTitle("Selecciona un emoji") + .setSingleChoiceItems(emojiArray, selectedIndex) { _, which -> + selectedIndex = which + } + .setPositiveButton("Aceptar") { _, _ -> + onEmojiSelected(emojis[selectedIndex]) + } + .setNegativeButton("Cancelar", null) + .show() + } + + // ======================================== + // Clases de datos y Adapter + // ======================================== + data class EmojiRange( + val count: Int, + val defaultEmoji: String, + val colorRes: Int, + val rangeText: String + ) + + inner class EmojiRangeAdapter( + private val ranges: List, + private var customEmojis: Map, + private val onEmojiChanged: (EmojiRange, String) -> Unit + ) : RecyclerView.Adapter() { + + inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val colorIndicator: View = view.findViewById(R.id.colorIndicator) + val rangeText: TextView = view.findViewById(R.id.rangeText) + val emojiText: TextView = view.findViewById(R.id.emojiText) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_emoji_range, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val range = ranges[position] + val currentEmoji = customEmojis[range.count] ?: range.defaultEmoji + + holder.rangeText.text = range.rangeText + holder.emojiText.text = currentEmoji + holder.colorIndicator.setBackgroundColor( + ContextCompat.getColor(holder.itemView.context, range.colorRes) + ) + + holder.emojiText.setOnClickListener { + showEmojiPicker(currentEmoji) { newEmoji -> + val mutableCustom = customEmojis.toMutableMap() + mutableCustom[range.count] = newEmoji + customEmojis = mutableCustom + notifyItemChanged(position) + onEmojiChanged(range, newEmoji) + } + } + } + + override fun getItemCount() = ranges.size + + fun resetToDefaults() { + customEmojis = emptyMap() + notifyDataSetChanged() + } } -} \ No newline at end of file +} diff --git a/app/src/main/res/drawable/bg_chip_cbd.xml b/app/src/main/res/drawable/bg_chip_cbd.xml new file mode 100644 index 0000000..3a64556 --- /dev/null +++ b/app/src/main/res/drawable/bg_chip_cbd.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_chip_thc.xml b/app/src/main/res/drawable/bg_chip_thc.xml new file mode 100644 index 0000000..54ca5f7 --- /dev/null +++ b/app/src/main/res/drawable/bg_chip_thc.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_counter_cbd.xml b/app/src/main/res/drawable/bg_counter_cbd.xml new file mode 100644 index 0000000..ad33198 --- /dev/null +++ b/app/src/main/res/drawable/bg_counter_cbd.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_counter_thc.xml b/app/src/main/res/drawable/bg_counter_thc.xml new file mode 100644 index 0000000..bc7438a --- /dev/null +++ b/app/src/main/res/drawable/bg_counter_thc.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_rounded_card.xml b/app/src/main/res/drawable/bg_rounded_card.xml new file mode 100644 index 0000000..3ca0a4a --- /dev/null +++ b/app/src/main/res/drawable/bg_rounded_card.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000..2ea0fbc --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/layout/activity_dashboard.xml b/app/src/main/res/layout/activity_dashboard.xml new file mode 100644 index 0000000..b175319 --- /dev/null +++ b/app/src/main/res/layout/activity_dashboard.xml @@ -0,0 +1,312 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_emoji_settings.xml b/app/src/main/res/layout/activity_emoji_settings.xml index 5190c66..360a52a 100644 --- a/app/src/main/res/layout/activity_emoji_settings.xml +++ b/app/src/main/res/layout/activity_emoji_settings.xml @@ -1,68 +1,203 @@ - + android:background="@color/background" + android:fitsSystemWindows="true"> - - - - - - - - - - - - - + android:fitsSystemWindows="true"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_evolution.xml b/app/src/main/res/layout/activity_evolution.xml index 4f031d4..b4b7518 100644 --- a/app/src/main/res/layout/activity_evolution.xml +++ b/app/src/main/res/layout/activity_evolution.xml @@ -3,7 +3,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/background"> + android:background="@color/background" + android:fitsSystemWindows="true"> + + android:gravity="center" + android:padding="12dp"> - + - + android:orientation="horizontal" + android:gravity="center"> + + + + + + + + + + + + + + + + + + + + + + + android:layout_marginTop="8dp" /> + + + diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index cf97f2b..abc33ad 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -1,50 +1,188 @@ - + android:fitsSystemWindows="true"> - - - - - - - + android:fitsSystemWindows="true"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/app/src/main/res/layout/activity_stats.xml b/app/src/main/res/layout/activity_stats.xml index d3cfda9..4df5683 100644 --- a/app/src/main/res/layout/activity_stats.xml +++ b/app/src/main/res/layout/activity_stats.xml @@ -3,7 +3,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/background"> + android:background="@color/background" + android:fitsSystemWindows="true"> + app:constraint_referenced_ids="countersContainer"/> - + + + + + + + + + + + + app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/menu/dashboard_menu.xml b/app/src/main/res/menu/dashboard_menu.xml new file mode 100644 index 0000000..fd56548 --- /dev/null +++ b/app/src/main/res/menu/dashboard_menu.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index a08dd0b..61f722c 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -15,8 +15,6 @@ @color/red_critical - @android:color/transparent @color/surface - false diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 6c290cf..3a793f8 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -58,4 +58,18 @@ #667eea #6B7280 + + #4CAF50 + #388E3C + #81C784 + #A5D6A7 + #E8F5E9 + #2E7D32 + #4CAF50 + #81C784 + + + #E3F2FD + #1565C0 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f5b58ae..cfbfbd1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -46,4 +46,41 @@ Anterior Siguiente + + Estadísticas + Hoy + Semana + Promedio + Racha + 📅 Ver calendario completo + Patrones + Día más activo: %1$s + Mejor día: %1$s (%2$d) + + + Tipo de Sustancia + THC + THC agregado + THC restado + THC con weed agregado + THC con polen agregado + ¿Qué estás fumando? + Etiqueta la toma con vibra 420. + + + Ajustes + Copia de Seguridad + Backup automático + 📄 Exportar datos (CSV) + 🎙️ Exportar audios (ZIP) + 💾 Crear backup ahora + 📥 Restaurar backup + Cifrar con contraseña (AES-256) + Backup creado con éxito + Datos restaurados correctamente + Introduce contraseña del backup + Personalizar Emojis + Toca un emoji para cambiarlo. Se actualizará en la pantalla principal y en el widget. + Restaurar emojis por defecto + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 89ad6a5..20cfb3b 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -15,8 +15,6 @@ @color/red_critical - @android:color/transparent @color/surface - false diff --git a/docs/plans/2026-01-18-v1.4-design.md b/docs/plans/2026-01-18-v1.4-design.md deleted file mode 100644 index b58706c..0000000 --- a/docs/plans/2026-01-18-v1.4-design.md +++ /dev/null @@ -1,237 +0,0 @@ -# CBDcounter v1.4 - Documento de Diseno - -**Fecha:** 18 de enero de 2026 -**Estado:** Aprobado - ---- - -## Resumen Ejecutivo - -La version 1.4 incluye cuatro mejoras principales: -1. Dashboard de estadisticas con metricas avanzadas -2. Toggle CBD/THC para trackear diferentes sustancias -3. Sistema de backup automatico y manual con cifrado -4. CSV mejorado con soporte multi-sustancia - ---- - -## Feature 1: Dashboard de Estadisticas - -### Descripcion -Nueva pantalla `DashboardActivity` que reemplaza la actual `StatsActivity` como punto de entrada desde "Stats". - -### Layout - -``` -+-------------------------------------+ -| <- Toolbar: "Estadisticas" [gear icon] | -|------------------------------------| -| +---------+ +---------+ | -| | HOY | | SEMANA | | <- Cards compactas (2x2 grid) -| | 3 | | 18 | | -| +---------+ +---------+ | -| +---------+ +---------+ | -| |PROMEDIO | | RACHA | | -| | 2.5/d | | 5 dias | | -| +---------+ +---------+ | -|------------------------------------| -| [chart icon] Grafica principal | -| [7d] [14d] [30d] < 01-07 ene > | -|------------------------------------| -| [chart icon] Patrones | -| - Dia mas activo: Sabado (4.2 avg)| -| - Esta semana vs anterior: +15% | -| - Mejor dia: 12 ene (6 tomas) | -|------------------------------------| -| [ [calendar icon] Ver calendario de emojis ] | -+-------------------------------------+ -``` - -### Cards de Resumen -| Card | Descripcion | -|------|-------------| -| Hoy | Contador del dia actual | -| Semana | Suma de los ultimos 7 dias | -| Promedio | Media diaria del ultimo mes | -| Racha | Dias consecutivos con registro | - -### Archivos -- `DashboardActivity.kt` (crear) -- `activity_dashboard.xml` (crear) -- `CalendarActivity.kt` (renombrar desde StatsActivity) - ---- - -## Feature 2: Toggle CBD/THC - -### Descripcion -Selector en EmojiSettingsActivity para cambiar el modo de la app entre CBD y THC. - -### Ubicacion -Arriba de la configuracion de emojis en EmojiSettingsActivity. - -### Diseno Visual - -``` -+-------------------------------------+ -| Tipo de sustancia | -| +-----------+ +-----------+ | -| | CBD | | THC | | -| | (verde) | | (naranja) | | -| +-----------+ +-----------+ | -+-------------------------------------+ -``` - -### Cambios segun Modo - -| Elemento | Modo CBD | Modo THC | -|----------|----------|----------| -| Label principal | "CBD" | "THC" | -| Pregunta +1 | "Con que esta alinado el CBD?" | "Que estas fumando?" | -| Subtexto +1 | "Etiqueta la toma con vibra 420" | (mismo) | -| Recuadro Weed/Polen | Verde | Naranja/Ambar | -| Busqueda | "CBD" | "THC" o "CBD+THC" | -| CSV header | substance=CBD | substance=THC | - -### Colores -- **CBD:** Verde actual (#4CAF50 o similar) -- **THC:** Naranja/Ambar (#FF9800 o similar) - -### Almacenamiento -```kotlin -Prefs.substanceType: String // "CBD" o "THC" -``` - ---- - -## Feature 3: Sistema de Backup - -### Descripcion -Sistema completo de copias de seguridad con opcion automatica y manual. - -### Ubicacion -Nueva seccion en EmojiSettingsActivity (considerar renombrar a SettingsActivity). - -### Layout - -``` -+-------------------------------------+ -| COPIA DE SEGURIDAD | -| | -| Backup automatico [OFF/ON] | -| +- Frecuencia: [Diario v] | -| +- Ultima: 15 ene 2026, 10:30 | -| | -| [ [save icon] Crear backup ahora ] | -| +- Con cifrado AES-256 [ ] | -| | -| [ [download icon] Restaurar backup ] | -|-------------------------------------| -| Contenido del backup: | -| [check] Contadores diarios | -| [check] Notas de texto | -| [check] Grabaciones de voz | -| [check] Configuracion de emojis | -| [check] Preferencias | -+-------------------------------------+ -``` - -### Formato de Backup -| Tipo | Extension | Descripcion | -|------|-----------|-------------| -| Sin cifrar | .zip | ZIP estandar con JSON + audios | -| Con cifrado | .cbdbak | ZIP cifrado con AES-256 | - -### Ubicacion de Archivos -``` -Documents/CBDCounter/backups/cbdcounter_backup_2026-01-18.zip -``` - -### Contenido del Backup -- `data.json` - Contadores, notas, preferencias -- `audio/` - Carpeta con grabaciones de voz -- `config.json` - Configuracion de emojis y preferencias - -### Frecuencias de Backup Automatico -- Diario -- Semanal -- Mensual - -### Archivos -- `BackupManager.kt` (crear) -- Modificar EmojiSettingsActivity para incluir UI - ---- - -## Feature 4: CSV Mejorado - -### Estructura - -```csv -date,substance,count,notes,has_audio -15-01-2026,CBD,3,"[leaf icon] 10:30 (weed) | [chocolate icon] 14:00 (polen)",true -16-01-2026,THC,5,"[leaf icon] 09:00 (sativa) | [leaf icon] 12:30",false -``` - -### Columnas -| Columna | Tipo | Descripcion | -|---------|------|-------------| -| date | String | Fecha DD-MM-YYYY | -| substance | String | "CBD" o "THC" | -| count | Integer | Numero de tomas | -| notes | String | Notas del dia (escapadas) | -| has_audio | Boolean | Si hay grabacion de voz | - -### Compatibilidad de Importacion -El sistema detecta automaticamente el formato de fecha: -1. DD-MM-YYYY (nuevo formato) -2. YYYY-MM-DD (ISO) -3. DD/MM/YYYY (formato antiguo de la app) - -### Registros Antiguos -Registros sin campo `substance` se asumen como CBD. - ---- - -## Archivos a Crear/Modificar - -| Archivo | Accion | -|---------|--------| -| `DashboardActivity.kt` | Crear | -| `activity_dashboard.xml` | Crear | -| `CalendarActivity.kt` | Renombrar (antes StatsActivity) | -| `activity_calendar.xml` | Renombrar (antes activity_stats) | -| `BackupManager.kt` | Crear | -| `EmojiSettingsActivity.kt` | Modificar (toggle + backup UI) | -| `Prefs.kt` | Anadir preferencias nuevas | -| `MainActivity.kt` | Cambiar navegacion | -| `colors.xml` | Anadir colores naranja/ambar | -| `strings.xml` | Nuevos textos | -| CSV utils | Modificar import/export | - ---- - -## Navegacion - -``` -MainActivity - | - +-> "Stats" -> DashboardActivity - | - +-> "Ver calendario" -> CalendarActivity - | - +-> [gear icon] Ajustes -> EmojiSettingsActivity - | - +-> Toggle CBD/THC - +-> Emojis - +-> Backup -``` - ---- - -## Notas de Implementacion - -1. **Branch:** Todos los cambios en `v1.4` -2. **Commits:** No commitear hasta aprobacion final -3. **Testing:** Verificacion manual de cada feature -4. **Compatibilidad:** Mantener retrocompatibilidad con datos existentes From 9883347fe0eda60a3c9904820772552e17ad15aa Mon Sep 17 00:00:00 2001 From: D4vRAM369 Date: Mon, 19 Jan 2026 14:30:52 +0000 Subject: [PATCH 3/5] fix(v1.4): infusiones siempre suman a contador THC Las infusiones de weed/polen ahora siempre incrementan el contador THC independientemente del modo activo seleccionado. Co-Authored-By: Claude Opus 4.5 --- .../main/java/com/d4vram/cbdcounter/MainActivity.kt | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt b/app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt index d9fbf19..fcde194 100644 --- a/app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt +++ b/app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt @@ -817,7 +817,18 @@ class MainActivity : AppCompatActivity() { val label = getString(type.labelRes) val suffix = getString(R.string.infusion_note_suffix, label) val entry = "${type.icon} ${getCurrentTimestamp()}$suffix" - registerIntake(entry, getString(type.feedbackRes)) + // Infusión (weed/polen) SIEMPRE suma a THC + registerThcIntake(entry, getString(type.feedbackRes)) + } + + /** Registra una toma que siempre va al contador THC (para infusiones) */ + private fun registerThcIntake(entry: String, feedbackMessage: String) { + thcCount++ + updateDisplay() + appendEntryToTodayNote(entry) + saveData() + animateCounter(1.1f) + showFeedback("$feedbackMessage (THC)", false) } private fun registerIntake(entry: String, feedbackMessage: String) { From ea0eeee5923497488951aeb546360a5641448cf7 Mon Sep 17 00:00:00 2001 From: D4vRAM369 Date: Mon, 19 Jan 2026 17:52:09 +0000 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20corregir=20crashes=20cr=C3=ADticos?= =?UTF-8?q?=20en=20v1.4=20(CSV=20stats=20+=20Android=2013)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PROBLEMA DETECTADO: =================== 1. App crasheaba al iniciar con ClassCastException 2. App crasheaba al abrir pantalla Stats con SecurityException 3. Estadísticas mostraban ceros después de importar CSV ROOT CAUSE ANALYSIS: ==================== Bug #1: ClassCastException en MainActivity.initViews() ------------------------------------------------------- - Error: 'Chip cannot be cast to MaterialButton' - Causa: Desajuste entre tipos declarados en Kotlin y tipos en XML - Archivos afectados: * statsButton: declarado MaterialButton, pero es Chip en XML * settingsButton: declarado MaterialButton, pero es ImageButton en XML * exportButton: declarado MaterialButton, pero es ImageButton en XML * importButton: declarado MaterialButton, pero es ImageButton en XML * searchButton: declarado MaterialButton, pero es ImageButton en XML Bug #2: SecurityException en DashboardActivity.onResume() ---------------------------------------------------------- - Error: 'RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED should be specified' - Causa: Android 13+ (API 33) requiere flag de seguridad al registrar BroadcastReceivers - Archivos afectados: * DashboardActivity.kt línea 97 * MainActivity.kt línea 182 Bug #3: Estadísticas en ceros (ya corregido en commit anterior) ---------------------------------------------------------------- - Causa: Uso de claves obsoletas count_$date en lugar de count_cbd_$date y count_thc_$date - Archivos afectados: * DashboardActivity.kt (4 funciones) * EvolutionActivity.kt (1 función) * CalendarActivity.kt (1 función) SOLUCIÓN IMPLEMENTADA: ====================== 1. Corregir tipos de variables en MainActivity.kt: - statsButton: MaterialButton → Chip - settingsButton, exportButton, importButton, searchButton: MaterialButton → ImageButton 2. Agregar flag RECEIVER_NOT_EXPORTED en registerReceiver(): - DashboardActivity: registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED) - MainActivity: registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED) Nota PBL: RECEIVER_NOT_EXPORTED indica que el receptor es PRIVADO (solo nuestra app puede enviar broadcasts). Usar RECEIVER_EXPORTED solo si otras apps necesitan comunicarse con nosotros. 3. Actualizar acceso a datos en Activities de estadísticas: - Usar Prefs.getTotalCount() en lugar de sharedPrefs.getInt("count_$date", 0) - Usar Prefs.getAllDatesWithData() en lugar de iterar sharedPrefs.all CONCEPTOS APRENDIDOS (PBL): =========================== 1. Type Safety en Android: - findViewById hace cast implícito al tipo T - Si el tipo real en XML no coincide con T → ClassCastException - Solución: Declarar variables con el tipo exacto del XML 2. BroadcastReceiver Security (Android 13+): - RECEIVER_NOT_EXPORTED: Receptor privado (comunicación interna) - RECEIVER_EXPORTED: Receptor público (otras apps pueden enviar) - Razón: Prevenir que apps maliciosas envíen broadcasts falsos 3. Data Migration Best Practices: - Centralizar acceso a datos en funciones helper (Prefs.kt) - Evita duplicación de lógica de migración - Facilita cambios futuros en estructura de datos TESTING: ======== ✅ Build exitoso ✅ App inicia sin crashes ✅ Pantalla Stats abre correctamente ⏳ Pendiente: Verificar estadísticas después de importar CSV ARCHIVOS MODIFICADOS: ===================== - MainActivity.kt: Tipos de botones + BroadcastReceiver flag - DashboardActivity.kt: BroadcastReceiver flag + Stats con Prefs helpers - EvolutionActivity.kt: Stats con Prefs helpers - CalendarActivity.kt: Stats con Prefs helpers - CBDWidgetProvider.kt: (cambios menores de baseline) --- ...ommand-_CBDcounter_v1.4_implementation.txt | 2227 +++++++++++++++++ app/release/baselineProfiles/0/app-release.dm | Bin 2914 -> 3263 bytes app/release/baselineProfiles/1/app-release.dm | Bin 2858 -> 3190 bytes .../d4vram/cbdcounter/CBDWidgetProvider.kt | 12 +- .../com/d4vram/cbdcounter/CalendarActivity.kt | 5 +- .../d4vram/cbdcounter/DashboardActivity.kt | 71 +- .../d4vram/cbdcounter/EvolutionActivity.kt | 4 +- .../com/d4vram/cbdcounter/MainActivity.kt | 63 +- 8 files changed, 2326 insertions(+), 56 deletions(-) create mode 100644 2026-01-19-command-messagesuperpowersbrainstormcommand-_CBDcounter_v1.4_implementation.txt diff --git a/2026-01-19-command-messagesuperpowersbrainstormcommand-_CBDcounter_v1.4_implementation.txt b/2026-01-19-command-messagesuperpowersbrainstormcommand-_CBDcounter_v1.4_implementation.txt new file mode 100644 index 0000000..dc9480e --- /dev/null +++ b/2026-01-19-command-messagesuperpowersbrainstormcommand-_CBDcounter_v1.4_implementation.txt @@ -0,0 +1,2227 @@ + + ▐▛███▜▌ Claude Code v2.1.12 +▝▜█████▛▘ Opus 4.5 · Claude Pro + ▘▘ ▝▝ ~/AndroidStudioProjects/CBDcounter2 + +❯ /skills + ⎿ Skills dialog dismissed + +❯ /superpowers:brainstorm + +● Skill(superpowers:brainstorming) + ⎿  Successfully loaded skill + +● Perfecto, estamos en modo brainstorming. Voy a revisar el contexto actual del + proyecto para entender dónde estamos. + +● Bash(git log --oneline -10) + ⎿  86df4da docs: add v1.4 design document + 61eaa34 feat: mejora del modal de día, controles de audio y corrección + de tema + … +31 lines (ctrl+o to expand) + +● Search(pattern: "docs/**/*.md") + ⎿  Found 2 files (ctrl+o to expand) + +● Bash(git status --short 2>/dev/null | head -30) + ⎿   M .gitignore + M app/src/main/AndroidManifest.xml + M app/src/main/java/com/d4vram/cbdcounter/EmojiSettingsActivity.kt + … +22 lines (ctrl+o to expand) + +● Search(pattern: "app/src/main/java/**/*.kt") + ⎿  Found 15 files (ctrl+o to expand) + +● Veo que estás trabajando en CBDcounter v1.4 - hay bastantes cambios en + progreso. Tienes nuevos archivos como DashboardActivity, CalendarActivity, + BackupManager, y has eliminado StatsActivity. Parece que la v1.4 trae una + reestructuración significativa. + + ¿Qué te gustaría explorar hoy? + + 1. Continuar v1.4 - Refinar o completar el diseño de los cambios actuales + (Dashboard, Calendar, etc.) + 2. Nueva feature - Diseñar algo nuevo que quieras añadir al proyecto + 3. Rediseño de algo existente - Repensar alguna parte de la app que no te + convence + 4. Otro - Cuéntame qué tienes en mente + +❯ Al activar el modo THC, si clicko en +1 debe salir THC en lugar de CBD en + la segunda foto. Si se han fumado ambos en el d + ⎿  [Image #1] + ⎿  [Image #2] + +● Entiendo la idea - tienes un toggle CBD/THC y quieres que el label cambie + según el modo activo. Veo las capturas de Ajustes pero parece que el mensaje + se cortó. + + Para asegurarme de entender bien, ¿dónde exactamente debería mostrar "THC" en + lugar de "CBD"? + + 1. En el contador principal (MainActivity) - El número grande que muestra el + conteo del día + 2. En las notas del día - Donde aparece "🌿 HH:mm" o similar + 3. En el widget - El texto que acompaña al contador + 4. Otro lugar - Descríbeme dónde + + Y mencionaste algo sobre "si se han fumado ambos en el día" pero se cortó - + ¿puedes completar esa idea? + +❯ Si se han fumado ambos en el día, entonces CBD+THC de forma que quede + bien en UI y buena UX + +● Perfecto, ahora entiendo mejor. Déjame revisar cómo funciona actualmente el + sistema de conteo para entender el contexto. + +● Read(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Read 989 lines + +● Read(app/src/main/java/com/d4vram/cbdcounter/Prefs.kt) + ⎿  Read 55 lines + +● Ahora veo el panorama completo. Encontré el problema en línea 917 del adapter: + + holder.countText.text = "${item.count} CBD" // Siempre dice "CBD" + + Y veo que actualmente Prefs.setSubstanceForDate() guarda UN solo valor por + día. Pero si quieres trackear CBD+THC, necesitamos cambiar cómo se almacena. + + Pregunta clave: ¿Cómo debe funcionar el conteo cuando se usan ambas sustancias + en el día? + + 1. Un contador único, label mixto - El número sigue siendo uno solo (ej: "5 + CBD+THC"). No sabes cuántos fueron de cada tipo, solo que usaste ambos. + 2. Contadores separados - Mostrar "3 CBD + 2 THC" con números distintos. + Requiere trackear dos conteos independientes. + 3. Un contador, pero registro en notas - El contador principal es uno, pero + las notas indican qué tipo fue cada entrada (como ya funciona con 🔹). El + label cambia según lo que aparezca en las notas del día. + + ¿Cuál encaja mejor con lo que tienes en mente? + +❯ 2 + +● Perfecto, contadores separados. Esto implica cambios en la estructura de datos + y en varias partes de la UI. + + Siguiente pregunta: ¿Cómo debe verse el contador principal en MainActivity + cuando hay ambos? + + 1. Mostrar solo el activo - Si estás en modo THC, ves "3" (THC). Cambias a + CBD, ves "2" (CBD). Simple, pero no ves el total del día de un vistazo. + 2. Mostrar ambos lado a lado - Algo como 2 CBD | 3 THC siempre visible, con el + modo activo destacado/más grande. + 3. Mostrar suma con desglose - Número grande "5" con subtexto pequeño "2 CBD + + 3 THC" debajo. + + ¿Cuál prefieres para la pantalla principal? + +❯ 2. Si sale biien en UI + +● Entendido, ambos visibles con el activo destacado. Ahora la última pregunta de + UI. + + ¿Cómo debe verse en el historial (la lista de días)? + + 1. Compacto - Una sola línea: 3 CBD · 2 THC o 3+2 CBD|THC + 2. Apilado - Dos líneas pequeñas: + 3 CBD + 2 THC + 3. Condicional - Solo mostrar lo que haya ese día: + - Si solo CBD: 3 CBD + - Si solo THC: 2 THC + - Si ambos: 3 CBD + 2 THC + +❯ Dos lineas pequeñas, siendo el color del chip de THC distinto al azul, + más verde + +● Perfecto, tengo todo lo que necesito. Déjame presentarte el diseño por partes. + + --- + Diseño: Contadores Separados CBD/THC + + 1. Estructura de Datos + + Almacenamiento actual: + - count_DD/MM/YYYY → número único + - substance_DD/MM/YYYY → "CBD" o "THC" + + Nueva estructura: + - count_cbd_DD/MM/YYYY → conteo CBD del día + - count_thc_DD/MM/YYYY → conteo THC del día + - Se elimina substance_DD/MM/YYYY (ya no necesario por día) + - Se mantiene substance_type → modo activo actual (para saber qué botón + incrementar) + + Migración: Los datos existentes se migran a count_cbd_* asumiendo que todo lo + anterior era CBD (o podemos preguntar al usuario). + + CSV export/import: Nueva columna o formato count_cbd,count_thc en lugar de + count,substance. + + --- + ¿Esto tiene sentido para la parte de datos? ¿O prefieres otro approach para la + migración? + +❯ Si, y además que si activo modo THC no se cambien los días que tienen CBD + registrados, sino que se pongan 2 chips como hemos comentado de la forma + dicha. Y si le doy al +1 verde con weed y polen, suma 1 THC + +● Exacto, entendido perfectamente: + + 1. El modo activo solo afecta qué contador incrementa el +1 (y el botón de + infusión) + 2. Los datos históricos no se tocan - si un día tiene 3 CBD y cambias a modo + THC, sigue mostrando 3 CBD. Si luego sumas +1, ese día pasa a mostrar 3 CBD / + 1 THC + 3. Infusión (weed/polen) suma al modo activo - si estás en THC, el 🌿 y 🍫 + suman a THC + + --- + 2. UI - Pantalla Principal + + Layout del contador (cuando hay ambos): + + ┌─────────────────────────┐ + │ 2 │ 3 │ ← números grandes + │ CBD │ THC │ ← labels pequeños + │ (azul) (verde) │ + └─────────────────────────┘ + ↑ activo = más grande/destacado + + - El modo activo se muestra ligeramente más grande o con fondo destacado + - Si un contador está en 0, se muestra igual pero más tenue (o 0 pequeño) + - El emoji se basa en la suma total del día (2+3=5 → emoji de 5) + + Colores: + - CBD: azul actual (cbd_primary) + - THC: verde hierba (propongo #4CAF50 o similar, definimos thc_green) + + --- + ¿Te cuadra esta distribución visual para el contador principal? + +❯ Si + +● Perfecto. Siguiente parte: + + --- + 3. UI - Historial (lista de días) + + Cada ítem del historial mostrará: + + ┌────────────────────────────────────────┐ + │ Lunes 13 😄 📝 🎤 │ + │ ┌───────┐ ┌───────┐ │ + │ │ 3 CBD │ │ 2 THC │ │ + │ └───────┘ └───────┘ │ + │ ▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░ (barra total: 5) │ + └────────────────────────────────────────┘ + + Reglas de visibilidad: + - Si solo CBD → solo chip azul 3 CBD + - Si solo THC → solo chip verde 2 THC + - Si ambos → ambos chips lado a lado + + Barra de progreso: + - Se basa en la suma total (CBD + THC) + - Color: podría ser degradado o el color del mayor, ¿prefieres algo simple + como gris neutro para cuando hay mezcla? + + --- + ¿Qué color prefieres para la barra cuando hay ambas sustancias? + +❯ Color degradado o del mayor, que el color cambie según la cantidad de + cada día y de CBD o THC. Me encanta el diseño propuesto + +● Genial, me alegra que te guste. Para la barra: + + --- + 4. Lógica de Color de la Barra + + Regla: + - Solo CBD → escala azul→naranja→rojo según cantidad (como ahora) + - Solo THC → escala verde→amarillo→rojo según cantidad + - Ambos → color del mayor. Si empate, degradado mitad-mitad + + Escala por cantidad (aplica a ambos): + 0 → verde seguro + 1-3 → color suave (azul claro / verde claro) + 4-6 → color medio (naranja / amarillo) + 7+ → rojo crítico + + --- + 5. Widget + + El widget también necesita actualizarse: + + Opciones: + 1. Mostrar solo el modo activo - Más simple, el widget muestra el contador del + modo que tengas seleccionado + 2. Mostrar ambos compactos - 2|3 con colores CBD/THC + + ¿Cuál prefieres para el widget dado el espacio limitado (2x2)? + +❯ 0 → verde seguro + + 1-4 → color suave (azul claro / verde claro) + + 5-6 → color medio (naranja / amarillo) + + 7+ → rojo crítico . El Widget la 1 + +● Perfecto, anotado. Última sección: + + --- + 6. Otros Cambios Necesarios + + Botón +1 y botón Infusión: + - Texto/color cambia según modo activo + - En modo THC: botón principal podría tener tinte verde sutil + + Notas (timestamps): + - Actualmente: 🔹 14:30 para CBD normal + - Propuesta: + - CBD: 🔹 14:30 (se mantiene) + - THC: 🟢 14:30 (o 💚) para distinguir visualmente + + Evolution/Stats: + - Los gráficos mostrarían líneas separadas CBD vs THC + - O una línea de total con opción de filtrar + + Feedback (Toast): + - Modo CBD: "+1 CBD añadido" + - Modo THC: "+1 THC añadido" + + --- + Resumen del Diseño + ┌────────────────────┬───────────────────────────────────────────────────┐ + │ Área │ Cambio │ + ├────────────────────┼───────────────────────────────────────────────────┤ + │ Datos │ Dos contadores por día: count_cbd_* y count_thc_* │ + ├────────────────────┼───────────────────────────────────────────────────┤ + │ Migración │ Datos existentes → CBD │ + ├────────────────────┼───────────────────────────────────────────────────┤ + │ Contador principal │ Dos números lado a lado, activo destacado │ + ├────────────────────┼───────────────────────────────────────────────────┤ + │ Historial │ Chips apilados, barra color del mayor │ + ├────────────────────┼───────────────────────────────────────────────────┤ + │ Widget │ Solo modo activo │ + ├────────────────────┼───────────────────────────────────────────────────┤ + │ Colores │ CBD=azul, THC=verde hierba │ + ├────────────────────┼───────────────────────────────────────────────────┤ + │ Rangos │ 0=verde, 1-4=suave, 5-6=medio, 7+=crítico │ + └────────────────────┴───────────────────────────────────────────────────┘ + --- + ¿Hay algo que quieras ajustar antes de que escriba el documento de diseño? + +❯ No, hacia adelante vamos! Y quiero luego las explicaciones PBL y también + registro para Plaud Note + +● ¡Vamos! Escribo el documento de diseño y luego te preparo el material PBL. + +● Bash(mkdir -p /home/defcon/AndroidStudioProjects/CBDcounter2/docs/plans) + ⎿  (No content) + +● Write(docs/plans/2026-01-19-dual-counters-design.md) + ⎿  Wrote 187 lines to docs/plans/2026-01-19-dual-counters-design.md + # Diseño: Contadores Separados CBD/THC + **Fecha:** 2026-01-19 + **Versión:** v1.4 + **Estado:** Aprobado + ## Resumen + Implementar contadores independientes para CBD y THC, permitiendo + trackear ambas sustancias por separado en el mismo día. La UI mostrará + ambos contadores cuando existan, con colores distintivos (azul para + CBD, verde para THC). + + … +177 lines (ctrl+o to expand) + +● Bash(git add docs/plans/2026-01-19-dual-counters-design.md && git commit -m + "docs: add dual counters CBD/THC design document…) + ⎿  Interrupted · What should Claude do instead? + +❯ Primero incluimos la carpeta docs en .gitignore + +● Read(.gitignore) + ⎿  Read 31 lines + +● Update(.gitignore) + ⎿  Added 1 line + 8 # OS + 9 # Output + 10 # Sesiones PBL (Learning/Aprendizaje) + 11 +docs/ + 12 *.aab + 13 *.apk + 14 build/ + +● Listo. Ahora docs/ está ignorado. El documento de diseño queda local para ti + pero no se sube al repo. + + --- + Explicación PBL + Registro Plaud Note + + Aquí tienes el material para estudiar y grabar: + + --- + 📦 CONCEPTO: SharedPreferences con Múltiples Claves por Entidad + + 🎯 QUÉ ES: + Patrón de almacenamiento donde una entidad (un día) tiene múltiples valores + asociados usando prefijos distintos en las claves. + + 🔧 PARA QUÉ SIRVE: + Cuando necesitas guardar datos relacionados pero independientes. En lugar de + un objeto complejo (JSON), usas claves paralelas que comparten un + identificador común. + + 💡 ANALOGÍA: + Imagina un archivador físico. Antes tenías una carpeta por día con UN papel + (el conteo). Ahora tienes DOS papeles en cada carpeta: uno azul (CBD) y uno + verde (THC). La carpeta sigue siendo la misma fecha, pero contiene información + separada. + + 📝 EJEMPLO: + // Antes: una clave por día + "count_19/01/2026" → 5 + + // Ahora: dos claves por día + "count_cbd_19/01/2026" → 3 + "count_thc_19/01/2026" → 2 + + 🔗 EN TU PROYECTO: + Prefs.kt tendrá funciones nuevas: + fun getCbdCount(ctx: Context, date: String): Int + fun getThcCount(ctx: Context, date: String): Int + fun getTotalCount(ctx: Context, date: String): Int // suma ambos + + --- + 📦 CONCEPTO: Migración de Datos + + 🎯 QUÉ ES: + Proceso de transformar datos existentes a un nuevo formato cuando cambias la + estructura de almacenamiento. + + 🔧 PARA QUÉ SIRVE: + Los usuarios que ya tienen datos no deben perderlos. La migración convierte el + formato viejo al nuevo automáticamente. + + 💡 ANALOGÍA: + Es como mudarte de casa. Tienes muebles viejos (datos) que deben caber en las + habitaciones nuevas (estructura). Empaquetas todo del formato viejo, lo + desempaquetas en el formato nuevo, y marcas "mudanza completada" para no + repetirla. + + 📝 PATRÓN: + fun migrateIfNeeded(ctx: Context) { + if (prefs.getBoolean("migration_v1.4_done", false)) return + + // Convertir count_* → count_cbd_* + // Eliminar claves viejas + + prefs.edit().putBoolean("migration_v1.4_done", true).apply() + } + + --- + 📝 PARA PLAUD NOTE (resumen de 2 min): + + "Hoy diseñamos contadores separados para CBD y THC en CBDcounter. + + El cambio principal es en SharedPreferences: pasamos de una clave count_fecha + a dos claves count_cbd_fecha y count_thc_fecha. Esto permite trackear ambas + sustancias independientemente en el mismo día. + + La UI muestra ambos contadores lado a lado en MainActivity, con el modo activo + destacado. En el historial, chips apilados con colores distintos: azul CBD, + verde THC. + + Conceptos clave aprendidos: patrón de claves paralelas en SharedPreferences y + migración de datos con flag de completado para no repetir." + + --- + ¿Quieres que empecemos la implementación ahora o prefieres dejarlo para otra + sesión? + +✻ Cogitated for 31s + +❯ Empezamos ahora, y luego al final de todo desglosa más el proceso para + Plaud Note, con explicaciones, conceptos, PBL, glosario, etc. en un + archivo + +● ¡Vamos a ello! Creo el plan de trabajo y empezamos. + +● Empezamos con Prefs.kt. Añado los nuevos getters/setters y la migración. + +● Read(app/src/main/java/com/d4vram/cbdcounter/Prefs.kt) + ⎿  Read 55 lines + +● Update(app/src/main/java/com/d4vram/cbdcounter/Prefs.kt) + ⎿  Added 5 lines, removed 2 lines + 6 + 7 object Prefs { + 8 private const val PREFS_NAME = "CBDCounter" + 9 - + 9 + + 10 // Constantes de claves para evitar errores tipográficos + 11 - const val KEY_COUNT_PREFIX = "count_" + 11 + const val KEY_COUNT_PREFIX = "count_" + + // Legacy, para migración + 12 + const val KEY_COUNT_CBD_PREFIX = "count_cbd_" + 13 + const val KEY_COUNT_THC_PREFIX = "count_thc_" + 14 const val KEY_NOTE_PREFIX = "NOTE_" + 15 const val KEY_DARK_MODE = "dark_mode" + 16 + private const val KEY_MIGRATION_V14_DONE = + +"migration_v1.4_done" + 17 + 18 // función privada para acceder a las SharedPreferences + 19 private fun prefs(ctx: Context): SharedPreferences = + +● Update(app/src/main/java/com/d4vram/cbdcounter/Prefs.kt) + ⎿  Added 93 lines, removed 5 lines + 40 val audioFile = File(ctx.filesDir, + "audios/audio_$date.mp3") + 41 return audioFile.exists() + 42 } + 43 - // ---- Sustancia (CBD vs THC) ---- + 43 + + 44 + // ---- Sustancia (CBD vs THC) - Modo activo ---- + 45 fun getSubstanceType(ctx: Context): String = + 46 prefs(ctx).getString("substance_type", "CBD") ?: "CBD" + 47 + ... + 48 prefs(ctx).edit().putString("substance_type", + type).apply() + 49 } + 50 + 51 - fun getSubstanceForDate(ctx: Context, date: String): + -String? = + 52 - prefs(ctx).getString("substance_$date", null) + 51 + // ---- Contadores separados CBD/THC ---- + 52 + fun getCbdCount(ctx: Context, date: String): Int = + 53 + prefs(ctx).getInt("${KEY_COUNT_CBD_PREFIX}$date", 0) + 54 + 55 - fun setSubstanceForDate(ctx: Context, date: String, type: + -String) { + 56 - prefs(ctx).edit().putString("substance_$date", type + -).apply() + 55 + fun setCbdCount(ctx: Context, date: String, count: Int) { + 56 + prefs(ctx).edit().putInt("${KEY_COUNT_CBD_PREFIX}$ + +date", count).apply() + 57 } + 58 + + 59 + fun getThcCount(ctx: Context, date: String): Int = + 60 + prefs(ctx).getInt("${KEY_COUNT_THC_PREFIX}$date", 0) + 61 + + 62 + fun setThcCount(ctx: Context, date: String, count: Int) { + 63 + + +prefs(ctx).edit().putInt("${KEY_COUNT_THC_PREFIX}$date", + +count).apply() + 64 + } + 65 + + 66 + fun getTotalCount(ctx: Context, date: String): Int = + 67 + getCbdCount(ctx, date) + getThcCount(ctx, date) + 68 + + 69 + /** Incrementa el contador del modo activo y devuelve el + +nuevo valor */ + 70 + fun incrementActiveCount(ctx: Context, date: String): Int + + { + 71 + val isThc = getSubstanceType(ctx) == "THC" + 72 + return if (isThc) { + 73 + val newCount = getThcCount(ctx, date) + 1 + 74 + setThcCount(ctx, date, newCount) + 75 + newCount + 76 + } else { + 77 + val newCount = getCbdCount(ctx, date) + 1 + 78 + setCbdCount(ctx, date, newCount) + 79 + newCount + 80 + } + 81 + } + 82 + + 83 + /** Decrementa el contador del modo activo (mínimo 0) y + +devuelve el nuevo valor */ + 84 + fun decrementActiveCount(ctx: Context, date: String): Int + + { + 85 + val isThc = getSubstanceType(ctx) == "THC" + 86 + return if (isThc) { + 87 + val newCount = maxOf(0, getThcCount(ctx, date) - + +1) + 88 + setThcCount(ctx, date, newCount) + 89 + newCount + 90 + } else { + 91 + val newCount = maxOf(0, getCbdCount(ctx, date) - + +1) + 92 + setCbdCount(ctx, date, newCount) + 93 + newCount + 94 + } + 95 + } + 96 + + 97 + /** Obtiene el conteo del modo activo */ + 98 + fun getActiveCount(ctx: Context, date: String): Int { + 99 + val isThc = getSubstanceType(ctx) == "THC" + 100 + return if (isThc) getThcCount(ctx, date) else + +getCbdCount(ctx, date) + 101 + } + 102 + + 103 + // ---- Migración v1.4 ---- + 104 + fun migrateToV14IfNeeded(ctx: Context) { + 105 + val prefs = prefs(ctx) + 106 + if (prefs.getBoolean(KEY_MIGRATION_V14_DONE, false)) + +return + 107 + + 108 + val editor = prefs.edit() + 109 + val allEntries = prefs.all + 110 + + 111 + // Migrar count_* → count_cbd_* + 112 + allEntries.keys + 113 + .filter { it.startsWith(KEY_COUNT_PREFIX) && + +!it.startsWith(KEY_COUNT_CBD_PREFIX) && + +!it.startsWith(KEY_COUNT_THC_PREFIX) } + 114 + .forEach { oldKey -> + 115 + val date = + +oldKey.removePrefix(KEY_COUNT_PREFIX) + 116 + val count = allEntries[oldKey] as? Int ?: 0 + 117 + editor.putInt("${KEY_COUNT_CBD_PREFIX}$date", + + count) + 118 + editor.remove(oldKey) + 119 + } + 120 + + 121 + // Eliminar claves substance_* obsoletas + 122 + allEntries.keys + 123 + .filter { it.startsWith("substance_") && it != + +"substance_type" } + 124 + .forEach { editor.remove(it) } + 125 + + 126 + editor.putBoolean(KEY_MIGRATION_V14_DONE, true) + 127 + editor.apply() + 128 + } + 129 + + 130 + /** Obtiene todas las fechas que tienen datos (para + +historial) */ + 131 + fun getAllDatesWithData(ctx: Context): Set { + 132 + val prefs = prefs(ctx) + 133 + val dates = mutableSetOf() + 134 + + 135 + prefs.all.keys.forEach { key -> + 136 + when { + 137 + key.startsWith(KEY_COUNT_CBD_PREFIX) -> + +dates.add(key.removePrefix(KEY_COUNT_CBD_PREFIX)) + 138 + key.startsWith(KEY_COUNT_THC_PREFIX) -> + +dates.add(key.removePrefix(KEY_COUNT_THC_PREFIX)) + 139 + key.startsWith(KEY_NOTE_PREFIX) -> + +dates.add(key.removePrefix(KEY_NOTE_PREFIX)) + 140 + } + 141 + } + 142 + return dates + 143 + } + 144 } + +● Ahora actualizo el CSV export/import en MainActivity. Primero la función + buildCsvContent: + +● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Added 9 lines, removed 18 lines + 559 } + 560 + 561 private fun buildCsvContent(): String { + 562 - val prefsMap = sharedPrefs.all + 563 - if (prefsMap.isEmpty()) return "" + 562 + val allDates = Prefs.getAllDatesWithData(this) + 563 + if (allDates.isEmpty()) return "" + 564 + 565 - val dates = mutableSetOf() + 566 - prefsMap.keys.forEach { key -> + 567 - when { + 568 - key.startsWith("count_") -> + -dates.add(key.removePrefix("count_")) + 569 - key.startsWith("NOTE_") -> + -dates.add(key.removePrefix("NOTE_")) + 570 - } + 571 - } + 572 - if (dates.isEmpty()) return "" + 573 - + 565 val dateFormat = SimpleDateFormat("dd/MM/yyyy", + Locale.getDefault()) + 566 - val sortedDates = dates.mapNotNull { dateString -> + 566 + val sortedDates = allDates.mapNotNull { dateString -> + 567 runCatching { dateFormat.parse(dateString) + }.getOrNull()?.let { parsed -> + 568 dateString to parsed + 569 } + 570 }.sortedBy { it.second } + 571 + 572 - val builder = StringBuilder("date,count,note,substance + -\n") + 572 + val builder = StringBuilder("date,count_cbd,count_thc, + +note\n") + 573 sortedDates.forEach { (dateString, _) -> + 574 - val count = + -sharedPrefs.getInt("count_$dateString", 0) + 574 + val cbdCount = Prefs.getCbdCount(this, + +dateString) + 575 + val thcCount = Prefs.getThcCount(this, + +dateString) + 576 val note = Prefs.getNote(this, dateString) ?: "" + 577 - val substance = Prefs.getSubstanceForDate(this, + -dateString) ?: "CBD" // Asumir CBD si no existe + 577 + 578 builder.append(dateString) + 579 .append(',') + 580 - .append(count) + 580 + .append(cbdCount) + 581 .append(',') + 582 - .append(escapeCsvField(note)) + 582 + .append(thcCount) + 583 .append(',') + 584 - .append(escapeCsvField(substance)) + 584 + .append(escapeCsvField(note)) + 585 .append('\n') + 586 } + 587 return builder.toString() + +● Ahora actualizo handleImportCsv para el nuevo formato (y compatibilidad con el + viejo): + +● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Added 34 lines, removed 9 lines + 595 if (lines.isEmpty()) throw + IllegalArgumentException("Archivo vacío") + 596 + 597 val editor = sharedPrefs.edit() + 598 + // Limpiar datos existentes + 599 sharedPrefs.all.keys.filter { + 600 it.startsWith("count_") || + it.startsWith("NOTE_") + 601 }.forEach { key -> editor.remove(key) } + 602 + 603 + // Detectar formato por cabecera + 604 + val header = lines.first().lowercase() + 605 + val isNewFormat = + +header.contains("count_cbd") + 606 + + 607 lines.drop(1).forEach { line -> + 608 if (line.isBlank()) return@forEach + 609 val columns = splitCsvLine(line) + 610 if (columns.size < 2) return@forEach + 611 + 612 val date = columns[0] + 613 - val count = columns[1].toIntOrNull() + -?: return@forEach + 614 - editor.putInt("count_$date", count) + 613 + 614 - val rawNote = if (columns.size >= 3) + -columns[2] else "" + 615 - val note = unescapeCsvField(rawNote) + 616 - if (note.isNotEmpty()) { + 617 - editor.putString("NOTE_$date", + -note) + 618 - } + 614 + if (isNewFormat) { + 615 + // Nuevo formato: + +date,count_cbd,count_thc,note + 616 + val cbdCount = + +columns.getOrNull(1)?.toIntOrNull() ?: 0 + 617 + val thcCount = + +columns.getOrNull(2)?.toIntOrNull() ?: 0 + 618 + + +editor.putInt("${Prefs.KEY_COUNT_CBD_PREFIX}$date", cbdCount) + 619 + + +editor.putInt("${Prefs.KEY_COUNT_THC_PREFIX}$date", thcCount) + 620 + 621 - val substance = if (columns.size >= + -4) unescapeCsvField(columns[3]) else "CBD" + 622 - Prefs.setSubstanceForDate(this, date, + - substance) + 621 + val rawNote = + +columns.getOrNull(3) ?: "" + 622 + val note = + +unescapeCsvField(rawNote) + 623 + if (note.isNotEmpty()) { + 624 + + +editor.putString("${Prefs.KEY_NOTE_PREFIX}$date", note) + 625 + } + 626 + } else { + 627 + // Formato legacy: + +date,count,note,substance + 628 + val count = + +columns[1].toIntOrNull() ?: return@forEach + 629 + val substance = if (columns.size + +>= 4) unescapeCsvField(columns[3]) else "CBD" + 630 + + 631 + // Importar al contador + +correspondiente + 632 + if (substance == "THC") { + 633 + + +editor.putInt("${Prefs.KEY_COUNT_THC_PREFIX}$date", count) + 634 + } else { + 635 + + +editor.putInt("${Prefs.KEY_COUNT_CBD_PREFIX}$date", count) + 636 + } + 637 + + 638 + val rawNote = if (columns.size >= + + 3) columns[2] else "" + 639 + val note = + +unescapeCsvField(rawNote) + 640 + if (note.isNotEmpty()) { + 641 + + +editor.putString("${Prefs.KEY_NOTE_PREFIX}$date", note) + 642 + } + 643 + } + 644 } + 645 editor.apply() + 646 } + +● Ahora añado la llamada a la migración en onCreate y actualizo loadTodayData: + +● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Added 2 lines, removed 1 line + 121 override fun onCreate(savedInstanceState: Bundle?) { + 122 // 1. Aplicar tema ANTES de super.onCreate para + evitar flickering + 123 initSharedPreferences() + 124 + Prefs.migrateToV14IfNeeded(this) // Migrar datos al + +nuevo formato si es necesario + 125 applyStoredTheme() + 126 - + 126 + + 127 super.onCreate(savedInstanceState) + 128 setContentView(R.layout.activity_main) + 129 + +● Ahora actualizo las variables y loadTodayData para manejar ambos contadores: + +● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Added 3 lines, removed 1 line + 68 + 69 // Data + 70 private lateinit var sharedPrefs: SharedPreferences + 71 - private var currentCount = 0 + 71 + private var cbdCount = 0 + 72 + private var thcCount = 0 + 73 + private val currentCount: Int get() = cbdCount + thcCount + + // Total para emoji y compatibilidad + 74 private val allHistoryData = ArrayList() + 75 private val displayedHistoryData = + ArrayList() + 76 private var currentViewMode = ViewMode.WEEK + +● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Added 2 lines, removed 1 line + 281 + 282 private fun loadTodayData() { + 283 val today = getCurrentDateKey() + 284 - currentCount = sharedPrefs.getInt("count_$today", 0) + 284 + cbdCount = Prefs.getCbdCount(this, today) + 285 + thcCount = Prefs.getThcCount(this, today) + 286 } + 287 + 288 private fun loadAllHistoryData() { + +● Ahora actualizo loadAllHistoryData para el nuevo formato y actualizo + HistoryItem: + +● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Added 10 lines, removed 11 lines + 287 + 288 private fun loadAllHistoryData() { + 289 allHistoryData.clear() + 290 - val allEntries = sharedPrefs.all + 290 val dateFormat = SimpleDateFormat("dd/MM/yyyy", + Locale.getDefault()) + 291 + 292 - allEntries.forEach { (key, value) -> + 293 - if (key.startsWith("count_") && value is Int) { + 294 - val dateString = key.removePrefix("count_") + 295 - try { + 296 - val date = dateFormat.parse(dateString) + 297 - if (date != null) { + 298 - + -allHistoryData.add(HistoryItem(dateString, value, date)) + 299 - } + 300 - } catch (_: Exception) {} + 301 - } + 292 + val allDates = Prefs.getAllDatesWithData(this) + 293 + allDates.forEach { dateString -> + 294 + try { + 295 + val date = dateFormat.parse(dateString) + 296 + if (date != null) { + 297 + val cbd = Prefs.getCbdCount(this, + +dateString) + 298 + val thc = Prefs.getThcCount(this, + +dateString) + 299 + + +allHistoryData.add(HistoryItem(dateString, cbd, thc, date)) + 300 + } + 301 + } catch (_: Exception) {} + 302 } + 303 allHistoryData.sortByDescending { it.dateObject } + 304 } + +● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Added 8 lines, removed 1 line + 868 } + 869 + 870 // Data class + 871 -data class HistoryItem(val date: String, val count: Int, val + -dateObject: Date) + 871 +data class HistoryItem( + 872 + val date: String, + 873 + val cbdCount: Int, + 874 + val thcCount: Int, + 875 + val dateObject: Date + 876 +) { + 877 + val totalCount: Int get() = cbdCount + thcCount + 878 +} + 879 + 880 // Adapter + 881 class ImprovedHistoryAdapter( + +● Ahora actualizo saveData: + +● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Added 2 lines, removed 5 lines + 357 + 358 private fun saveData() { + 359 val today = getCurrentDateKey() + 360 - sharedPrefs.edit().putInt("count_$today", + -currentCount).apply() + 361 - + 362 - // Guardar también el tipo de sustancia activa para + -este día + 363 - val currentSubstance = Prefs.getSubstanceType(this) + 364 - Prefs.setSubstanceForDate(this, today, + -currentSubstance) + 360 + Prefs.setCbdCount(this, today, cbdCount) + 361 + Prefs.setThcCount(this, today, thcCount) + 362 + 363 loadAllHistoryData() + 364 updateHistoryView() + +● Ahora actualizo registerIntake para incrementar el contador correcto: + +● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Added 6 lines, removed 1 line + 779 } + 780 + 781 private fun registerIntake(entry: String, + feedbackMessage: String) { + 782 - currentCount++ + 782 + val isThc = Prefs.getSubstanceType(this) == "THC" + 783 + if (isThc) { + 784 + thcCount++ + 785 + } else { + 786 + cbdCount++ + 787 + } + 788 updateDisplay() + 789 appendEntryToTodayNote(entry) + 790 saveData() + +● Ahora actualizo el botón -1 (subtract) para decrementar el contador correcto: + +● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Added 11 lines, removed 7 lines + 444 startActivity(Intent(this, + SettingsActivity::class.java)) + 445 } + 446 subtractButton.setOnClickListener { + 447 - if (currentCount > 0) { + 447 + val isThc = Prefs.getSubstanceType(this) == "THC" + 448 + val activeCount = if (isThc) thcCount else cbdCount + 449 + + 450 + if (activeCount > 0) { + 451 // Inflar layout personalizado + 452 val dialogView = LayoutInflater.from(this).inflat + e(R.layout.dialog_confirm_delete, null) + 453 - + 453 + + 454 // Crear el diálogo + 455 val dialog = MaterialAlertDialogBuilder(this) + 456 .setView(dialogView) + ... + 456 + 457 // Configurar chips + 458 dialogView.findViewById(R.id.chip_confirm). + setOnClickListener { + 459 - currentCount-- + 459 + if (isThc) thcCount-- else cbdCount-- + 460 updateDisplay() + 461 - removeLastEntryFromTodayNote() // 🎯 + -Borrar último timestamp + 461 + removeLastEntryFromTodayNote() // + +Borrar último timestamp + 462 saveData() + 463 animateCounter(0.9f) + 464 - + -showFeedback(getString(R.string.cbd_subtracted), true) + 464 + val msg = if (isThc) + +getString(R.string.thc_subtracted) else + +getString(R.string.cbd_subtracted) + 465 + showFeedback(msg, true) + 466 dialog.dismiss() + 467 } + 468 + 469 dialogView.findViewById(R.id.chip_keep_note + ).setOnClickListener { + 470 - currentCount-- + 470 + if (isThc) thcCount-- else cbdCount-- + 471 updateDisplay() + 472 // NO borramos la nota, solo restamos el + contador + 473 saveData() + ... + 479 dialog.dismiss() + 480 } + 481 + 482 - // Mostrar con fondo transparente para que se vea + -bien el card + - (opcional, pero recomendado si el root es CardView) + 482 + // Mostrar con fondo transparente para que se vea + +bien el card + 483 dialog.window?.setBackgroundDrawableResource(andr + oid.R.color.transparent) + 484 dialog.show() + 485 } + +● Actualizo el botón reset para reiniciar ambos contadores: + +● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Added 3 lines, removed 2 lines + 491 resetButton.setOnClickListener { + 492 AlertDialog.Builder(this) + 493 .setTitle("Reiniciar contador") + 494 - .setMessage("¿Estás seguro de que quieres + -reiniciar el contador de hoy?") + 494 + .setMessage("¿Estás seguro de que quieres + +reiniciar el contador de hoy? (CBD y THC)") + 495 .setPositiveButton("Sí") { _, _ -> + 496 - currentCount = 0 + 496 + cbdCount = 0 + 497 + thcCount = 0 + 498 updateDisplay() + 499 saveData() + 500 showFeedback("¡Día reiniciado! 💪", true) + +● Fase 1 completada. Ahora añado los colores THC en colors.xml: + +● Read(app/src/main/res/values/colors.xml) + ⎿  Read 71 lines + +● Veo que ya hay colores THC pero son naranjas. Tú quieres THC en verde. Los + actualizo: + +● Update(app/src/main/res/values/colors.xml) + ⎿  Added 11 lines, removed 8 lines + 58 #667eea + 59 #6B7280 + 60 + 61 - + 62 - #ED8936 + 63 - #C05621 + 64 - #FBD38D + 65 - #F6E05E + 66 - #ED8936 + 67 - #F6AD55 + 68 - #80ED8936 + 61 + + 62 + #4CAF50 + 63 + #388E3C + 64 + #81C784 + 65 + #A5D6A7 + 66 + #E8F5E9 + 67 + #2E7D32 + 68 + 69 + + 70 + #E3F2FD + 71 + #1565C0 + 72 + + 73 + +● Read(app/src/main/res/values-night/themes.xml) + ⎿  Read 21 lines + +● Search(pattern: "app/src/main/res/layout/activity_main.xml") + ⎿  Found 1 file (ctrl+o to expand) + +● Read(app/src/main/res/layout/activity_main.xml) + ⎿  Read 358 lines + +● Voy a rediseñar el card del contador para mostrar ambos contadores lado a + lado: + +● Update(app/src/main/res/layout/activity_main.xml) + ⎿  Added 91 lines, removed 14 lines + 84 + 85 + 86 + 87 - + 87 + + 88 + 104 + android:gravity="center" + 105 + android:padding="12dp"> + 106 + 107 - + 108 + + 112 + android:orientation="horizontal" + 113 + android:gravity="center"> + 114 + 115 + + 116 + + 125 + + 126 + + 134 + + 135 + + 143 + + 144 + + 145 + + 146 + + 151 + + 152 + + 153 + + 162 + + 163 + + 171 + + 172 + + 180 + + 181 + + 182 + + 183 + + 184 + 190 + android:layout_marginTop="8dp" /> + 191 + + 192 + + 193 + + 199 + 200 + 201 + +● Ahora creo los drawables para los backgrounds de los contadores: + +● Write(app/src/main/res/drawable/bg_counter_cbd.xml) + ⎿  Wrote 7 lines to app/src/main/res/drawable/bg_counter_cbd.xml + + + + + + +● Write(app/src/main/res/drawable/bg_counter_thc.xml) + ⎿  Wrote 7 lines to app/src/main/res/drawable/bg_counter_thc.xml + + + + + + +● Ahora actualizo MainActivity.kt con las nuevas views y la lógica de display + dual: + +● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Added 5 lines, removed 1 line + 41 class MainActivity : AppCompatActivity() { + 42 + 43 // Views principales + 44 - private lateinit var counterText: TextView + 44 + private lateinit var counterText: TextView + + // Oculto, para compatibilidad + 45 + private lateinit var cbdCountText: TextView + 46 + private lateinit var thcCountText: TextView + 47 + private lateinit var cbdContainer: View + 48 + private lateinit var thcContainer: View + 49 private lateinit var dateText: TextView + 50 private lateinit var emojiText: TextView + 51 private lateinit var addButton: Button + +● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Added 5 lines, removed 1 line + 194 + 195 private fun initViews() { + 196 // Views principales + 197 - counterText = findViewById(R.id.counterText) + 197 + counterText = findViewById(R.id.counterText) + + // Oculto, para compatibilidad + 198 + cbdCountText = findViewById(R.id.cbdCountText) + 199 + thcCountText = findViewById(R.id.thcCountText) + 200 + cbdContainer = findViewById(R.id.cbdContainer) + 201 + thcContainer = findViewById(R.id.thcContainer) + 202 dateText = findViewById(R.id.dateText) + 203 emojiText = findViewById(R.id.emojiText) + 204 addButton = findViewById(R.id.addButton) + +● Ahora actualizo la función updateDisplay para mostrar ambos contadores: + +● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Added 53 lines, removed 26 lines + 398 } + 399 + 400 private fun updateDisplay(animate: Boolean = true) { + 401 - counterText.text = currentCount.toString() + 401 + // Actualizar contadores duales + 402 + cbdCountText.text = cbdCount.toString() + 403 + thcCountText.text = thcCount.toString() + 404 + counterText.text = currentCount.toString() // Total + +oculto para compatibilidad + 405 + + 406 dateText.text = getCurrentDateDisplay() + 407 val newEmoji = getEmoji(currentCount) + 408 - + 408 + + 409 if (animate && emojiText.text != newEmoji && + emojiText.text.isNotEmpty()) { + 410 + emojiText.animate().alpha(0f).setDuration(150).withEndAction + { + 411 emojiText.text = newEmoji + ... + 412 emojiText.text = newEmoji + 413 } + 414 + 415 - val substanceType = Prefs.getSubstanceType(this) + 416 - val isThc = substanceType == "THC" + 415 + // Destacar el modo activo + 416 + val isThc = Prefs.getSubstanceType(this) == "THC" + 417 + highlightActiveCounter(isThc) + 418 + 419 - val color = when { + 420 - currentCount == 0 -> R.color.green_safe + 421 - currentCount <= 3 -> if (isThc) + -R.color.thc_primary_light else R.color.yellow_warning // THC + -starts lighter? Or maybe just amber + 422 - currentCount <= 6 -> if (isThc) + -R.color.thc_primary else R.color.orange_danger // THC gets + -darker + 423 - else -> if (isThc) R.color.thc_primary_dark else + -R.color.red_critical + 424 - } + 425 - // Specific logic for THC mode colors could be + -refined + 426 - val finalColor = if (isThc) { + 427 - when { + 428 - currentCount == 0 -> R.color.green_safe + 429 - currentCount <= 3 -> + -R.color.thc_primary_light + 430 - currentCount <= 6 -> R.color.thc_primary + 431 - else -> R.color.thc_primary_dark + 432 - } + 419 + // Actualizar colores según cantidad + 420 + updateCounterColors() + 421 + } + 422 + + 423 + private fun highlightActiveCounter(isThcActive: Boolean) + +{ + 424 + // El contador activo se ve más grande/destacado + 425 + val activeScale = 1.1f + 426 + val inactiveScale = 0.9f + 427 + val activeAlpha = 1.0f + 428 + val inactiveAlpha = 0.6f + 429 + + 430 + if (isThcActive) { + 431 + thcContainer.scaleX = activeScale + 432 + thcContainer.scaleY = activeScale + 433 + thcContainer.alpha = activeAlpha + 434 + cbdContainer.scaleX = inactiveScale + 435 + cbdContainer.scaleY = inactiveScale + 436 + cbdContainer.alpha = inactiveAlpha + 437 } else { + 438 - when { + 439 - currentCount == 0 -> R.color.green_safe + 440 - currentCount <= 3 -> R.color.yellow_warning + 441 - currentCount <= 6 -> R.color.orange_danger + 442 - else -> R.color.red_critical + 443 - } + 438 + cbdContainer.scaleX = activeScale + 439 + cbdContainer.scaleY = activeScale + 440 + cbdContainer.alpha = activeAlpha + 441 + thcContainer.scaleX = inactiveScale + 442 + thcContainer.scaleY = inactiveScale + 443 + thcContainer.alpha = inactiveAlpha + 444 } + 445 - + 446 - counterText.setTextColor(ContextCompat.getColor(this, + - finalColor)) + 445 } + 446 + 447 + private fun updateCounterColors() { + 448 + // Colores CBD según cantidad + 449 + val cbdColor = when { + 450 + cbdCount == 0 -> R.color.green_safe + 451 + cbdCount <= 4 -> R.color.cbd_text + 452 + cbdCount <= 6 -> R.color.orange_danger + 453 + else -> R.color.red_critical + 454 + } + 455 + + +cbdCountText.setTextColor(ContextCompat.getColor(this, + +cbdColor)) + 456 + + 457 + // Colores THC según cantidad + 458 + val thcColor = when { + 459 + thcCount == 0 -> R.color.green_safe + 460 + thcCount <= 4 -> R.color.thc_text + 461 + thcCount <= 6 -> R.color.orange_danger + 462 + else -> R.color.red_critical + 463 + } + 464 + + +thcCountText.setTextColor(ContextCompat.getColor(this, + +thcColor)) + 465 + } + 466 + + 467 private fun getEmoji(count: Int): String = + EmojiUtils.emojiForCount(count, this) + 468 + 469 private fun setupClickListeners() { + +● Primero añado el string que falta para THC: + +● Read(app/src/main/res/values/strings.xml) + ⎿  Read 83 lines + +● Update(app/src/main/res/values/strings.xml) + ⎿  Added 4 lines + 60 + 61 Tipo de + Sustancia + 62 THC + 63 + THC agregado + 64 + THC restado + 65 + THC con weed + +agregado + 66 + THC con polen + +agregado + 67 ¿Qué estás + fumando? + 68 Etiqueta la toma con + vibra 420. + 69 + +● Ahora actualizo el layout del historial. Primero veo el actual: + +● Search(pattern: "app/src/main/res/layout/history_item.xml") + ⎿  Found 1 file (ctrl+o to expand) + +● Read(app/src/main/res/layout/history_item.xml) + ⎿  Read 112 lines + +● Update(app/src/main/res/layout/history_item.xml) + ⎿  Added 45 lines, removed 10 lines + 90 + app:layout_constraintEnd_toStartOf="@id/countBarrier" + 91 app:layout_constraintBaseline_toBaselineOf="@id/h + istoryDate"/> + 92 + 93 - + 93 + + 94 + + 103 + + 104 + + 105 + + 118 + + 119 + + 120 + + 132 + + 133 + + 134 + + 135 + 142 + app:layout_constraintTop_toTopOf="parent" /> + 143 + 144 + 145 + +● Necesito actualizar el barrier y crear los drawables para los chips: + +● Update(app/src/main/res/layout/history_item.xml) + ⎿  Added 1 line, removed 1 line + 20 android:layout_width="0dp" + 21 android:layout_height="0dp" + 22 app:barrierDirection="start" + 23 - app:constraint_referenced_ids="historyCount"/> + 23 + app:constraint_referenced_ids="countersContainer"/> + 24 + 25 + 26 + + + + + + +● Write(app/src/main/res/drawable/bg_chip_thc.xml) + ⎿  Wrote 10 lines to app/src/main/res/drawable/bg_chip_thc.xml + + + + + + + +● Ahora actualizo el adapter para manejar los chips duales: + +● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Added 3 lines, removed 1 line + 955 + 956 class ItemViewHolder(itemView: View) : + RecyclerView.ViewHolder(itemView) { + 957 val dateText: TextView = + itemView.findViewById(R.id.historyDate) + 958 - val countText: TextView = + -itemView.findViewById(R.id.historyCount) + 958 + val countText: TextView = + +itemView.findViewById(R.id.historyCount) // Oculto + 959 + val cbdChip: TextView = + +itemView.findViewById(R.id.cbdChip) + 960 + val thcChip: TextView = + +itemView.findViewById(R.id.thcChip) + 961 val emojiText: TextView = + itemView.findViewById(R.id.historyEmoji) + 962 val progressBar: View = + itemView.findViewById(R.id.progressBar) + 963 val noteBadge: TextView? = + itemView.findViewById(R.id.noteBadge) + +● Ahora actualizo onBindViewHolder para mostrar los chips duales: + +● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Added 58 lines, removed 22 lines + 984 val dayFormat = SimpleDateFormat("EEEE dd", + Locale("es", "ES")) + 985 holder.dateText.text = + dayFormat.format(item.dateObject) + 986 .replaceFirstChar { it.uppercase() } + 987 - holder.countText.text = "${item.count} CBD" + 987 + 988 + // Mostrar chips según los datos + 989 + if (item.cbdCount > 0) { + 990 + holder.cbdChip.text = "${item.cbdCount} + +CBD" + 991 + holder.cbdChip.visibility = View.VISIBLE + 992 + } else { + 993 + holder.cbdChip.visibility = View.GONE + 994 + } + 995 + + 996 + if (item.thcCount > 0) { + 997 + holder.thcChip.text = "${item.thcCount} + +THC" + 998 + holder.thcChip.visibility = View.VISIBLE + 999 + } else { + 1000 + holder.thcChip.visibility = View.GONE + 1001 + } + 1002 + + 1003 + // Si ambos son 0, mostrar chip CBD con 0 + 1004 + if (item.cbdCount == 0 && item.thcCount == + +0) { + 1005 + holder.cbdChip.text = "0 CBD" + 1006 + holder.cbdChip.visibility = View.VISIBLE + 1007 + } + 1008 + + 1009 + val total = item.totalCount + 1010 holder.emojiText.text = when { + 1011 - item.count == 0 -> "😌" + 1012 - item.count <= 2 -> "🙂" + 1013 - item.count <= 4 -> "😄" + 1014 - item.count <= 5 -> "🫠" + 1015 - item.count <= 6 -> "🤔" + 1016 - item.count <= 7 -> "🙄" + 1017 - item.count <= 8 -> "😶‍🌫️" + 1018 - item.count <= 9 -> "🫡" + 1019 - item.count <= 10 -> "🫥" + 1020 - item.count <= 11 -> "⛔️" + 1011 + total == 0 -> "😌" + 1012 + total <= 2 -> "🙂" + 1013 + total <= 4 -> "😄" + 1014 + total <= 5 -> "🫠" + 1015 + total <= 6 -> "🤔" + 1016 + total <= 7 -> "🙄" + 1017 + total <= 8 -> "😶‍🌫️" + 1018 + total <= 9 -> "🫡" + 1019 + total <= 10 -> "🫥" + 1020 + total <= 11 -> "⛔️" + 1021 else -> "💀" + 1022 } + 1023 + 1024 - // Barra de progreso (como ya tenías) + 1024 + // Barra de progreso basada en el total + 1025 val maxWidth = holder.itemView.width + 1026 - val progress = minOf(item.count / 10f, 1f) + 1026 + val progress = minOf(total / 10f, 1f) + 1027 val layoutParams = + holder.progressBar.layoutParams + 1028 layoutParams.width = (maxWidth * + progress).toInt() + 1029 holder.progressBar.layoutParams = + layoutParams + 1030 - val color = when { + 1031 - item.count == 0 -> R.color.green_safe + 1032 - item.count <= 3 -> + -R.color.yellow_warning + 1033 - item.count <= 6 -> R.color.orange_danger + 1034 - else -> R.color.red_critical + 1030 + + 1031 + // Color de la barra: del mayor, o verde si + +es 0 + 1032 + val barColor = when { + 1033 + total == 0 -> R.color.green_safe + 1034 + item.thcCount > item.cbdCount -> { + 1035 + // THC es mayor, usar escala verde + 1036 + when { + 1037 + total <= 4 -> + +R.color.thc_primary_light + 1038 + total <= 6 -> + +R.color.orange_danger + 1039 + else -> R.color.red_critical + 1040 + } + 1041 + } + 1042 + else -> { + 1043 + // CBD es mayor o igual, usar escala + + azul + 1044 + when { + 1045 + total <= 4 -> + +R.color.primary_light + 1046 + total <= 6 -> + +R.color.orange_danger + 1047 + else -> R.color.red_critical + 1048 + } + 1049 + } + 1050 } + 1051 holder.progressBar.setBackgroundColor( + 1052 - + -ContextCompat.getColor(holder.itemView.context, color) + 1052 + + +ContextCompat.getColor(holder.itemView.context, barColor) + 1053 ) + 1054 + 1055 - // --- NUEVO: badge + -de nota visible si existe nota para ese día + 1055 + // Badge + +de nota visible si existe nota para ese día + 1056 val ctx = holder.itemView.context + 1057 holder.noteBadge?.visibility = + 1058 if (Prefs.hasNote(ctx, item.date)) + View.VISIBLE else View.GONE + 1059 + 1060 - // --- NUEVO: badge + -de audio visible si existe audio para ese día + 1060 + // Badge + +de audio visible si existe audio para ese día + 1061 holder.audioBadge?.visibility = + 1062 if (Prefs.hasAudio(ctx, item.date)) + View.VISIBLE else View.GONE + 1063 + 1064 - // --- NUEVO: clicks para abrir el modal + 1064 + // Clicks para abrir el modal + 1065 holder.itemView.setOnClickListener { + onDayClick(item.date) } + 1066 holder.noteBadge?.setOnClickListener { + onDayClick(item.date) } + 1067 holder.audioBadge?.setOnClickListener { + onDayClick(item.date) } + +● Read(app/src/main/java/com/d4vram/cbdcounter/CBDWidgetProvider.kt) + ⎿  Read 275 lines + +● Ahora actualizo el widget para usar los contadores duales: + +● Update(app/src/main/java/com/d4vram/cbdcounter/CBDWidgetProvider.kt) + ⎿  Added 14 lines, removed 16 lines + 205 appWidgetManager.updateAppWidget(appWidgetId, views) + 206 } + 207 + 208 - private fun incrementCounter(context: Context) { + 208 + private fun incrementActiveCounter(context: Context) { + 209 val today = getCurrentDateKey() + 210 - val sharedPrefs = + -context.getSharedPreferences(PREFS_NAME, + -Context.MODE_PRIVATE) + 211 - val currentCount = + -sharedPrefs.getInt("$KEY_COUNT_PREFIX$today", 0) + 212 - + 213 - sharedPrefs.edit() + 214 - .putInt("$KEY_COUNT_PREFIX$today", currentCount + + - 1) + 215 - .apply() + 210 + Prefs.incrementActiveCount(context, today) + 211 } + 212 + 213 private fun addStandardCBD(context: Context) { + 214 - incrementCounter(context) + 215 - val entry = "🔹 ${getCurrentTimestamp()}" + 214 + incrementActiveCounter(context) + 215 + val isThc = Prefs.getSubstanceType(context) == "THC" + 216 + val entry = if (isThc) "🟢 ${getCurrentTimestamp()}" + +else "🔹 ${getCurrentTimestamp()}" + 217 appendNote(context, entry) + 218 } + 219 + 220 private fun resetCBD(context: Context) { + 221 val today = getCurrentDateKey() + 222 - val sharedPrefs = + -context.getSharedPreferences(PREFS_NAME, + -Context.MODE_PRIVATE) + 223 - + 224 - sharedPrefs.edit() + 225 - .putInt("$KEY_COUNT_PREFIX$today", 0) + 226 - .apply() + 222 + // Reset solo el contador del modo activo + 223 + val isThc = Prefs.getSubstanceType(context) == "THC" + 224 + if (isThc) { + 225 + Prefs.setThcCount(context, today, 0) + 226 + } else { + 227 + Prefs.setCbdCount(context, today, 0) + 228 + } + 229 } + 230 + 231 private fun getCurrentCount(context: Context): Int { + 232 val today = getCurrentDateKey() + 233 - val sharedPrefs = + -context.getSharedPreferences(PREFS_NAME, + -Context.MODE_PRIVATE) + 234 - return sharedPrefs.getInt("$KEY_COUNT_PREFIX$today", + -0) + 233 + // Devolver el contador del modo activo + 234 + return Prefs.getActiveCount(context, today) + 235 } + 236 + 237 private fun getCurrentDateKey(): String { + +● Update(app/src/main/java/com/d4vram/cbdcounter/CBDWidgetProvider.kt) + ⎿  Added 2 lines, removed 2 lines + 245 } + 246 + 247 private fun addWeed(context: Context) { + 248 - incrementCounter(context) + 248 + incrementActiveCounter(context) + 249 val entry = "🌿 ${getCurrentTimestamp()} (aliñado con + weed)" + 250 appendNote(context, entry) + 251 } + 252 + 253 private fun addPolem(context: Context) { + 254 - incrementCounter(context) + 254 + incrementActiveCounter(context) + 255 val entry = "🍫 ${getCurrentTimestamp()} (aliñado con + polen)" + 256 appendNote(context, entry) + 257 } + +● Actualizo el feedback de registerStandardIntake para el modo activo: + +● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Added 4 lines, removed 2 lines + 766 } + 767 + 768 private fun registerStandardIntake() { + 769 - val entry = "🔹 ${getCurrentTimestamp()}" + 770 - registerIntake(entry, getString(R.string.cbd_added)) + 769 + val isThc = Prefs.getSubstanceType(this) == "THC" + 770 + val entry = if (isThc) "🟢 ${getCurrentTimestamp()}" + +else "🔹 ${getCurrentTimestamp()}" + 771 + val feedback = if (isThc) + +getString(R.string.thc_added) else + +getString(R.string.cbd_added) + 772 + registerIntake(entry, feedback) + 773 } + 774 + 775 private fun showInfusionDialog() { + +● Intentemos compilar para verificar que no hay errores: + +● Bash(./gradlew assembleDebug 2>&1 | tail -50) timeout: 3m 0s + ⎿  > Task :app:stripDebugDebugSymbols NO-SOURCE + > Task :app:validateSigningDebug UP-TO-DATE + > Task :app:writeDebugAppMetadata UP-TO-DATE + … +72 lines (ctrl+o to expand) + +● Hay algunos errores de compilación. Los arreglo: + +● Read(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Read 30 lines + +● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Added 3 lines, removed 3 lines + 346 streakText.text = "Racha: 0 días" + 347 return + 348 } + 349 - val average = displayedHistoryData.map { it.count + - }.average() + 349 + val average = displayedHistoryData.map { it.totalCount + + }.average() + 350 avgText.text = "Promedio: %.1f".format(average) + 351 - val total = displayedHistoryData.sumOf { it.count } + 351 + val total = displayedHistoryData.sumOf { it.totalCount + + } + 352 totalText.text = "Total: $total" + 353 val streak = calculateCleanStreak() + 354 streakText.text = "Racha limpia: $streak días" + ... + 358 var streak = 0 + 359 val sortedData = allHistoryData.sortedByDescending { + it.dateObject } + 360 for (item in sortedData) { + 361 - if (item.count == 0) streak++ else break + 361 + if (item.totalCount == 0) streak++ else break + 362 } + 363 return streak + 364 } + +● Ahora añado los colores que faltan: + +● Update(app/src/main/res/values/colors.xml) + ⎿  Added 2 lines + 65 #A5D6A7 + 66 #E8F5E9 + 67 #2E7D32 + 68 + #4CAF50 + 69 + #81C784 + 70 + 71 + 72 #E3F2FD + +● Compilo de nuevo: + +● Bash(./gradlew assembleDebug 2>&1 | tail -30) timeout: 3m 0s + ⎿  > Task :app:stripDebugDebugSymbols NO-SOURCE + > Task :app:validateSigningDebug UP-TO-DATE + > Task :app:writeDebugAppMetadata UP-TO-DATE + … +51 lines (ctrl+o to expand) + +● ¡Build exitoso! Ahora creo el archivo PBL completo para Plaud Note: + +● Write(docs/pbl/2026-01-19-dual-counters-session.md) + ⎿  Wrote 295 lines to docs/pbl/2026-01-19-dual-counters-session.md + # Sesión PBL: Contadores Duales CBD/THC + **Fecha:** 2026-01-19 + **Proyecto:** CBDcounter v1.4 + **Duración estimada de lectura para Plaud Note:** 8-10 minutos + --- + ## Resumen Ejecutivo (30 segundos) + … +285 lines (ctrl+o to expand) + +● --- + Resumen de la Sesión + + Build: ✅ SUCCESS + + Lo que implementamos: + + 1. Datos: Contadores separados count_cbd_* y count_thc_* con migración + automática + 2. UI Principal: Card dual con CBD (azul) | THC (verde), modo activo destacado + 3. Historial: Chips apilados que solo aparecen si hay datos + 4. Widget: Usa el contador del modo activo + 5. CSV: Compatible con formato nuevo y legacy + + Archivos clave modificados: + + - Prefs.kt - Nueva estructura de datos + - MainActivity.kt - Lógica dual completa + - activity_main.xml - Layout contador dual + - history_item.xml - Chips apilados + - CBDWidgetProvider.kt - Contador activo + + Documentación PBL creada: + + 📄 docs/pbl/2026-01-19-dual-counters-session.md + + Contiene: + - 6 conceptos explicados con analogías + - Glosario de términos + - Flujo de datos + - Preguntas de repaso + - Código de ejemplo + + --- + Para probar: Instala el APK (app/build/outputs/apk/debug/app-debug.apk), + cambia entre modo CBD/THC en Ajustes, y verifica que los contadores se + incrementan correctamente. + + ¿Quieres que hagamos algún ajuste o pasamos a otra cosa? + +✻ Churned for 10m 39s + +❯ Quiero que el emoji no salga algo cortado por debajo en el circulo, + solventa dandole unos px mas y dime como lo replicaría yo + ⎿  [Image #3] + ⎿  [Image #4] + +● Veo el problema - el emoji se corta por abajo del card. Aumento la altura del + card para que quepa bien. + +● Update(app/src/main/res/layout/activity_main.xml) + ⎿  Added 1 line, removed 1 line + 88 + android:layout_height="140dp" + + + android:layout_height="155dp" + + --- + Cómo replicarlo tú mismo: + + 1. Abre app/src/main/res/layout/activity_main.xml + 2. Busca el CardView con id="@+id/counterCard" (línea ~88) + 3. Cambia android:layout_height de 140dp a 155dp (o el valor que necesites) + 4. Guarda y rebuild + + Regla general: Si un elemento se corta visualmente, aumenta el height del + contenedor padre o reduce el padding/margin interno. En este caso, 15dp extra + fueron suficientes para dar espacio al emoji. + +❯ Vamos a quitarle el problema a la primera foto la screen de Configuración + que no solape arriba con la barra de tareasy que quede bien como las + otras dos. Es la pantalla que sale al entrar en Stats > Estadísticas > + Engranaje SVG esquina superior derecha > CONFIGURACIÓN + ⎿  [Image #5] + ⎿  [Image #6] + ⎿  [Image #7] + +● Veo el problema - "Configuración" se solapa con la barra de estado. Voy a + buscar qué Activity es y arreglarlo. + +● Search(pattern: "Configuración|settingsButton|engranaje", path: + "app/src/main/java/com/d4vram/cbdcounter") + ⎿  Found 1 file (ctrl+o to expand) + +● Read(app/src/main/java/com/d4vram/cbdcounter/DashboardActivity.kt) + ⎿  Read 251 lines + +● Read(app/src/main/java/com/d4vram/cbdcounter/EmojiSettingsActivity.kt) + ⎿  Read 315 lines + +● Read(app/src/main/res/layout/activity_emoji_settings.xml) + ⎿  Read 196 lines + +● El problema es que el toolbar no tiene espacio para la barra de estado. Hago + dos cambios: + +● Update(app/src/main/java/com/d4vram/cbdcounter/EmojiSettingsActivity.kt) + ⎿  Added 3 lines + 58 super.onCreate(savedInstanceState) + 59 setContentView(R.layout.activity_emoji_settings) + 60 + 61 + // Configurar color de la barra de estado + 62 + window.statusBarColor = ContextCompat.getColor(this, + +R.color.gradient_start) + 63 + + 64 // Configurar toolbar + 65 val toolbar = + findViewById(R.id.settingsToolbar) + 66 toolbar.setNavigationOnClickListener { finish() } + +● Ahora añado padding al toolbar para la barra de estado: + +● Update(app/src/main/res/layout/activity_emoji_settings.xml) + ⎿  Added 3 lines, removed 1 line + 9 &1 | tail -15) + ⎿ w: file:///home/defcon/AndroidStudioProjects/CBDcounter2/app/src/main/j + ava/com/d4vram/cbdcounter/EvolutionActivity.kt:50:24 'fun setOnCheckedC + hangeListener(p0: ChipGroup.OnCheckedChangeListener?): Unit' is depreca + … +19 lines (ctrl+o to expand) + + + + +● ✅ Build exitoso. + + --- + Cómo replicarlo tú mismo: + + 1. En el Activity (EmojiSettingsActivity.kt línea ~60): + // Añadir en onCreate(), después de setContentView() + window.statusBarColor = ContextCompat.getColor(this, R.color.gradient_start) + + 2. En el Layout (activity_emoji_settings.xml línea ~9-19): + + android:paddingTop="24dp" + android:minHeight="?attr/actionBarSize" + ... /> + + --- + Regla general: + + Cuando el header se solapa con la barra de estado: + 1. Código: window.statusBarColor = color para que el color coincida + 2. Layout: paddingTop en el toolbar (24dp es un valor común) + + layout_height="wrap_content" + minHeight="?attr/actionBarSize" + +✻ Cogitated for 1m 28s + +❯ mejor, pero no del todo, debe quedar como Ajustes en segunda foto + ⎿  [Image #8] + ⎿  [Image #9] + +● Veo, necesita más padding. Déjame ver cómo está configurado "Ajustes" + (SettingsActivity): + +● Read(app/src/main/res/layout/activity_settings.xml) + ⎿  Read 40 lines + +● Ahora veo la diferencia. "Ajustes" usa AppBarLayout con + fitsSystemWindows="true". Actualizo "Configuración" con la misma estructura: + +● Update(app/src/main/res/layout/activity_emoji_settings.xml) + ⎿  Added 19 lines, removed 13 lines + 1 + 2 - + 6 + android:background="@color/background" + 7 + android:fitsSystemWindows="true"> + 8 + 9 - + 13 + android:fitsSystemWindows="true"> + 14 + 15 + + 24 + + 25 + + 26 + 29 + android:fillViewport="true" + 30 + + +app:layout_behavior="@string/appbar_scrolling_view_behavior"> + 31 + 32 + 201 + 202 + 203 - + 203 + + +● Bash(./gradlew assembleDebug 2>&1 | tail -10) + ⎿  Interrupted · What should Claude do instead? + +❯ Ya funciona, y está genial , ¿Como hemos solventado? Dame el paso a paso + como profesor de programador y todos los tecnicismos o conceptos + definelos al final de la explicacion en GLOSARIO para Plaud Note, y + guarda el archivo como Fix_completo_toolbar_CBDcounter_edge-to-edge.md + ⎿  [Image #10] + +● Write(docs/pbl/Fix_completo_toolbar_CBDcounter_edge-to-edge.md) + ⎿  Wrote 261 lines to + docs/pbl/Fix_completo_toolbar_CBDcounter_edge-to-edge.md + # Fix Completo: Toolbar Solapado con Barra de Estado (Edge-to-Edge) + **Fecha:** 2026-01-19 + **Proyecto:** CBDcounter v1.4 + **Problema:** El header "Configuración" se solapaba con la barra de + estado del sistema + **Solución:** Implementar correctamente el patrón Edge-to-Edge de + Material Design + --- + ## El Problema + … +251 lines (ctrl+o to expand) + +● Archivo guardado en docs/pbl/Fix_completo_toolbar_CBDcounter_edge-to-edge.md + + --- + Resumen rápido del fix: + + El problema: El toolbar se dibujaba desde el borde superior, solapándose con + la barra de estado. + + La solución en 4 pasos: + + 1. Layout raíz → Cambiar LinearLayout por CoordinatorLayout + + fitsSystemWindows="true" + 2. Toolbar → Envolverlo en AppBarLayout + fitsSystemWindows="true" + 3. Contenido → Añadir + app:layout_behavior="@string/appbar_scrolling_view_behavior" al ScrollView + 4. Código → window.statusBarColor = color para que el color coincida + + Concepto clave: fitsSystemWindows="true" le dice al sistema "yo me encargo de + no solaparme con tus barras" y añade padding automáticamente. + +✻ Cooked for 1m 8s + +❯ Último detalle: al clickar en +1 superior el de "¿Con que está aliñado el + CBD?", aunque esté en modo CBD, añadir +1 a THC, sea polen o weed y con + la nota correspondiente + +● Entendido. El botón de infusión (+1 con weed/polen) siempre suma a THC, + independientemente del modo. Lo modifico: + +● Read(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Read 50 lines + +● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) + ⎿  Added 12 lines, removed 1 line + 817 val label = getString(type.labelRes) + 818 val suffix = getString(R.string.infusion_note_suffix, + label) + 819 val entry = "${type.icon} + ${getCurrentTimestamp()}$suffix" + 820 - registerIntake(entry, getString(type.feedbackRes)) + 820 + // Infusión (weed/polen) SIEMPRE suma a THC + 821 + registerThcIntake(entry, getString(type.feedbackRes)) + 822 } + 823 + 824 + /** Registra una toma que siempre va al contador THC + +(para infusiones) */ + 825 + private fun registerThcIntake(entry: String, + +feedbackMessage: String) { + 826 + thcCount++ + 827 + updateDisplay() + 828 + appendEntryToTodayNote(entry) + 829 + saveData() + 830 + animateCounter(1.1f) + 831 + showFeedback("$feedbackMessage (THC)", false) + 832 + } + 833 + + 834 private fun registerIntake(entry: String, + feedbackMessage: String) { + 835 val isThc = Prefs.getSubstanceType(this) == "THC" + 836 if (isThc) { + +● Ahora actualizo el Widget para que weed/polen también sumen a THC: + +● Update(app/src/main/java/com/d4vram/cbdcounter/CBDWidgetProvider.kt) + ⎿  Added 10 lines, removed 2 lines + 245 } + 246 + 247 private fun addWeed(context: Context) { + 248 - incrementActiveCounter(context) + 248 + // Weed SIEMPRE suma a THC + 249 + incrementThcCounter(context) + 250 val entry = "🌿 ${getCurrentTimestamp()} (aliñado con + weed)" + 251 appendNote(context, entry) + 252 } + 253 + 254 private fun addPolem(context: Context) { + 255 - incrementActiveCounter(context) + 255 + // Polen SIEMPRE suma a THC + 256 + incrementThcCounter(context) + 257 val entry = "🍫 ${getCurrentTimestamp()} (aliñado con + polen)" + 258 appendNote(context, entry) + 259 } + 260 + 261 + private fun incrementThcCounter(context: Context) { + 262 + val today = getCurrentDateKey() + 263 + val currentThc = Prefs.getThcCount(context, today) + 264 + Prefs.setThcCount(context, today, currentThc + 1) + 265 + } + 266 + + 267 private fun appendNote(context: Context, entry: String) { + 268 val today = getCurrentDateKey() + 269 val currentNote = Prefs.getNote(context, today) + +● Compilo para verificar: + +● Bash(./gradlew assembleDebug 2>&1 | tail -10) + ⎿  > Task :app:processDebugJavaRes UP-TO-DATE + > Task :app:mergeDebugJavaResource UP-TO-DATE + > Task :app:dexBuilderDebug + … +7 lines (ctrl+o to expand) + diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm index 4290a861b5ef09399c07cc06557a60ff28b5a045..f603f8430d2675ee72f9071d2e23cc05b8bf13d5 100644 GIT binary patch delta 2496 zcmV;x2|xDY7QYz`P)h>@6aWYa2mtbmkqjRMo(f_0kuNk8GYJ3yZ8`t|0RXlDI%;(n z0048bb|(RUntg0lGfsKXt&*h2r+;ia}nb-HN+Q2 zwm3(COft@G3jqhATc#sImNNZ=#AWGpS=|1B3v+{+Fmah1F*B4oHOmmlbdipR?)luF z@4XMVFP1-D&P{&z_xAn0Jil}9Jx`xrV;;NRm?{&0zv2m8-URv&7!!oyDKAlq%XC^f zTj2*Xbw2Dh7UZl6?((e^Ru_Gv7|%HIqfm>>jyflN&(3|WdSG565mq*&Z5zfkSD2Iy zI#f+akiKhUZjCwd$Rp-w-|%`c4Yu_+!QEGB|imz`H=n|o>M*++xo_sJ)ao!lLJ;|atQh$j$F;L|yQ zWu@okm0bhB8S>5zoqql7$E_x;GNG3-`Q|sjWw$w{*M4&pCA+&OJ7%|5LVbdi%C5?P zytwJzHuKh11?0!3YxqPlCVa!>y`R|qR3KB|$cVOCG8Orb6`mRX`{CT+cVRC7X+o{76xZg?Iw-1mX$A6No1ePavK^ zJb`g0@E?1uR_)i;&n)@ELw3Jtz7&{TWD9HR^P^%Sym2BX33kE`q^vvLGCU1kbttbTKGKbd=SLCl>yQk~EX^n*wwi~=- z<+;MbZZ>cHRChKs3u^rWupD)%K6ta$e{^4Dv`2yc&(_`Ot|%3L?$QX8^c%fQQ0p5n z{9>7F5;d@689cPRBKi(VV^HZkpiYexfVN;wy>9l%^0WmC|Ksf+h;n~M-DWZOClr;mrfZW3d5Yc6myIcpAg-~aA)H+aYYQh>cu zW!^bbI@H{eGX{Z`&)zGUAr{gn5}NODL8+uGMU=i{^&iwyx{e`bfI!bPhLDf@?QJ4iqCfEe%QEw?(>Zwo;`oEHg&Q+ z-<|&RJ-ZG(l(z$$$}7C)^rBl%m5>+5Y~4z}Sc)eQPavK^Jb`!u|CbYZ#-32{M<&GM z((DaJd-H8y<*kJV%KMA`TA!G)qe&8-C?L#s|)xco@zm{78pGZIRA# z7JYoj5{D{VEa5MI{MXPXAQyQcu=r#{mz{o)@d*QGeRY=PV=a>L@e8@nL^kZiz-L%u z4RH2Q*dS`ow?rPTml(;y(T8dgqnQKF9@z zIPA*3EwW+1F7k2KNFIp(b)8j9>%>RSS=OOi;>v%u^^;qFy8P;&!#ZqS1I=A{I6nH2 zk36d_F%#tkx5*M%`r%q@X6SCe__!CrDnBx`%D%BCdXWuIeYo2LI(c=^&9*ZAkQ19^ zbch_1HB)kM-zzsU>Q!7d!H^f7c(pmn@8Q@rh!}?ZiXOQ?YP8XVoh+!|vA8 zjLE5gyy9RbD`rh(!>;=i++0h#Pr%}bxL=^xEC?tGGLTLWGD9Ate4+<3RX zE4z7QXKs9Mq?1Ro)}uJ=l2s2~-)Y$72TMFU&l0$7iiLyGcPaOjY6{~%AKm#=Q?9{a6MP42gc2d&>6$4{gyRyF9!&7)ow4<2Gow?B!{NJ{iAi$TwbYY|1r}e>}VDx;5N9%)`}LH(27ZYv#HS z@Ii_xpC@r5`cb^_ZXfDReY?7@t$gENap9Gd`+B^%a-4GM8HArc-TOl^!irN%J~-=A zePOT*t+#D0h{{zHxLgi3P2MCJefMuf!005o} z002-+0|XQR2nYxO@`|&J30DCQ@`^KDisOJ~qY3~3o(hxk3M>NhijyA;90J`6lRXO` K1~d!+0001z908#K delta 2145 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 X?r@WQ3>*T036q)(9|qS700000ulG(3 diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm index e04358e9e58c6a572fb3cf165685d51790bda544..b62877cb22594a1af2ca1cafb1f67b3a7e3a693a 100644 GIT binary patch delta 2787 zcmV<93LN#S7WNnnP)h>@6aWYa2mtbmkqjSyRSK&0aB^<|FflLy0n9uA07MD^0C)lI znthBMbs5Khvpcu9cRhOBre3e~kiBw!;e9EH1Ww$eKpRrvL<}VeIZ|6Gh(U^NBShxz zS}ah~Y5)ms%vJn@L}Mx#ME{`Minfwez=$?P(yIu>SY9F+THEw|KC|<^``zAMDG7;x z(ada~ecry$%kP=lnc3N$*~F zVQ(>NG|DHwx`YeYVEPS-t=>2>#e{>wKgnoeV#BW-$oRm`H)=Kt6CDoCPV}sC`dDdX zExUC__}h#~KrZq?VDZU@E<61o;}Zsd&iW=B$;VnG7jE-=JlSMK$R4g2Yd zZ<=xPK=iNutXf(pK61`9hiZu{|4HU2w{-c{|L4qyjeVfG3lGOfAM%lBsSz`OQBH98 z7=fi9_O)h)?)Qt2V-c+KBSTBKW`-Bl<%=(Mb9Iy7>xgj51=noAs7GwVsJ>!_VGe!_etiCXB!lCB98FGaiKaYALt zE^%yNr6ZG5_sRHGL%#8HV^gk){NveG*RSE{VIIEDy1^2MT{G8lfDckk?L3JS(U0PV z_xn(9>f6_~Z{-`03$L6U*W<<2;*?9*ApG>{A0LVlR-9UY^1)e`>I;Ki7~iJPo%m zi!C6k$eAX8)mv7--)blM*nXl#SCMmKsVal6<|aFcGwoa3o5tJR!{!U@nb3TDzu4Vw z@23WV-NQB5v_|UEY0kRoif!Mqb57ts?WP93maW0gvO+Q^%?ew!Z)U(sX2g19+#&m8 zc`Q|N+6VP<6!T|plVUBf>n`znRV(0QeSW>_da=oWDhsjI{iRtl2cfE&PfjwTO)+$x zA%0O8xNmZZrCU0Ik1>IP=bS4>(f)u_ShAJ)s!+~`MH#wp4q9Wm?24CjzKa@$eN_wx z>*%d)xbkSz7_B__@rG^lGMT8ZrC@E?Gu^DqThI=!1Fl%Od`<5Pw|Chx_mf+LeoTY2 z2YCE{HfrmRk9zB`tB@~5>w2S6-tm=t?1UculPf-(T`ivfLvc=3slErBIJJ9QC9?i< zYkTc|8&}@r+(H{mXY08k<106AI_=4F6l_>~b6YVJ6kVTdSeBx&}xQ?#CRerd;@BW)g)7sYlI#X!B@|4|ScAY&W zUohn%+wE+T3*1x3pS>ECz8`x6z5Sz z{=MS6f1cD@8gA>D!h8LGr%VsxgmJFMQ?7 z{wrq`+!@;p76oqb0#~u1^;gBH-qOz7j$|um-L|>sx=F3^6SlL1J);ts62*736&L3F z%8@&xDU5-&sL}f1xu!pSU1GFXff6%veuzgEQlwWaT*PPbV!k~M`1GlVA z(uH}qiND6B+b1}Ik*C$4VA3>II)QWo=>*aV{Ff&%ICHpxXI=kmdzzztU%mI++vf(Z z(|$d)o2jGkuDW*^VnTU;c31rLap#v^X3X814?LD!c02p7f8+GCgO~mk!=9~oukRXp zsJS8MCcmfuRRV~P46U9hU(a1vv<^;R{noTq+unZli5F;gOL8X^WnE?UTRFR{9GLlZ z=ho{xioZE!QM9Jq&@#Vxa8F$>e_!YFtABnt=zr>P-F?wznRQ)%nG<@xfA@3$czpJn zy&bvumN2^il9w);RXV)!{8gEGcOE@B^k(P!?5Tad-)p_=Gp+BxdvISo(#`rE~)61kwql6G$iUe>s8w02w4z_fSg*2z>Qo z!deOd096VA08mQ`0|XQR2nYxO@`|%m30DCQ@`^KDeDz|&S_%LFRSJ{23M?Sk9w?001YukyZcz delta 2452 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 S002k{lTi#G2A>K50000<3-vDm diff --git a/app/src/main/java/com/d4vram/cbdcounter/CBDWidgetProvider.kt b/app/src/main/java/com/d4vram/cbdcounter/CBDWidgetProvider.kt index 665471e..fb1d8c4 100644 --- a/app/src/main/java/com/d4vram/cbdcounter/CBDWidgetProvider.kt +++ b/app/src/main/java/com/d4vram/cbdcounter/CBDWidgetProvider.kt @@ -245,17 +245,25 @@ class CBDWidgetProvider : AppWidgetProvider() { } private fun addWeed(context: Context) { - incrementActiveCounter(context) + // Weed SIEMPRE suma a THC + incrementThcCounter(context) val entry = "🌿 ${getCurrentTimestamp()} (aliñado con weed)" appendNote(context, entry) } private fun addPolem(context: Context) { - incrementActiveCounter(context) + // Polen SIEMPRE suma a THC + incrementThcCounter(context) val entry = "🍫 ${getCurrentTimestamp()} (aliñado con polen)" appendNote(context, entry) } + private fun incrementThcCounter(context: Context) { + val today = getCurrentDateKey() + val currentThc = Prefs.getThcCount(context, today) + Prefs.setThcCount(context, today, currentThc + 1) + } + private fun appendNote(context: Context, entry: String) { val today = getCurrentDateKey() val currentNote = Prefs.getNote(context, today) diff --git a/app/src/main/java/com/d4vram/cbdcounter/CalendarActivity.kt b/app/src/main/java/com/d4vram/cbdcounter/CalendarActivity.kt index 4cf9c9d..a63f8d1 100644 --- a/app/src/main/java/com/d4vram/cbdcounter/CalendarActivity.kt +++ b/app/src/main/java/com/d4vram/cbdcounter/CalendarActivity.kt @@ -161,9 +161,8 @@ class CalendarActivity : AppCompatActivity(), NoteBottomSheet.Listener { for (day in 1..daysInMonth) { workingCalendar.set(Calendar.DAY_OF_MONTH, day) val dateKey = dateKeyFormat.format(workingCalendar.time) - val prefKey = "count_$dateKey" - val hasData = sharedPrefs.contains(prefKey) - val count = sharedPrefs.getInt(prefKey, 0) + val count = Prefs.getTotalCount(this, dateKey) + val hasData = count > 0 val emoji = if (hasData) EmojiUtils.emojiForCount(count, this) else "" val isToday = dateKey == todayKey && todayCalendar.get(Calendar.MONTH) == currentMonth && diff --git a/app/src/main/java/com/d4vram/cbdcounter/DashboardActivity.kt b/app/src/main/java/com/d4vram/cbdcounter/DashboardActivity.kt index 8a60b5c..ab58409 100644 --- a/app/src/main/java/com/d4vram/cbdcounter/DashboardActivity.kt +++ b/app/src/main/java/com/d4vram/cbdcounter/DashboardActivity.kt @@ -1,7 +1,9 @@ package com.d4vram.cbdcounter +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.content.SharedPreferences import android.os.Bundle import android.view.Menu @@ -17,6 +19,7 @@ import java.util.* class DashboardActivity : AppCompatActivity() { private lateinit var sharedPrefs: SharedPreferences + private lateinit var dataChangeReceiver: android.content.BroadcastReceiver private lateinit var lineChart: LineChartView private lateinit var tvToday: TextView private lateinit var tvWeek: TextView @@ -36,6 +39,24 @@ class DashboardActivity : AppCompatActivity() { sharedPrefs = getSharedPreferences("CBDCounter", Context.MODE_PRIVATE) + // Initialize broadcast receiver for data changes (e.g., after CSV import) + dataChangeReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == MainActivity.ACTION_DATA_CHANGED) { + // Refresh stats when data changes + calculateStats() + // Determine current selected chip range + val rangeChipGroup = findViewById(R.id.rangeChipGroup) + val days = when (rangeChipGroup.checkedChipId) { + R.id.chip14Days -> 14 + R.id.chip30Days -> 30 + else -> 7 + } + loadChartData(days) + } + } + } + val toolbar = findViewById(R.id.dashboardToolbar) setSupportActionBar(toolbar) toolbar.setNavigationOnClickListener { finish() } @@ -72,6 +93,9 @@ class DashboardActivity : AppCompatActivity() { override fun onResume() { super.onResume() + // Register receiver for data changes + registerReceiver(dataChangeReceiver, IntentFilter(MainActivity.ACTION_DATA_CHANGED), Context.RECEIVER_NOT_EXPORTED) + // Refresh data on resume (in case settings changed or user returned from calendar) calculateStats() // Determine current selected chip range @@ -84,6 +108,16 @@ class DashboardActivity : AppCompatActivity() { loadChartData(days) } + override fun onPause() { + super.onPause() + // Unregister receiver to avoid memory leaks + try { + unregisterReceiver(dataChangeReceiver) + } catch (e: IllegalArgumentException) { + // Receiver already unregistered, ignore + } + } + override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.dashboard_menu, menu) return true @@ -102,7 +136,7 @@ class DashboardActivity : AppCompatActivity() { private fun calculateStats() { // 1. Today val todayKey = dateKeyFormat.format(Date()) - val todayCount = sharedPrefs.getInt("count_$todayKey", 0) + val todayCount = Prefs.getTotalCount(this, todayKey) tvToday.text = todayCount.toString() // 2. Week Total & Average @@ -116,7 +150,7 @@ class DashboardActivity : AppCompatActivity() { val tempCal = Calendar.getInstance() for (i in 0 until 7) { val key = dateKeyFormat.format(tempCal.time) - val c = sharedPrefs.getInt("count_$key", 0) + val c = Prefs.getTotalCount(this, key) weekTotal += c counts.add(c) tempCal.add(Calendar.DAY_OF_YEAR, -1) // go back @@ -129,8 +163,9 @@ class DashboardActivity : AppCompatActivity() { var daysWithData = 0 for (i in 0 until 30) { val key = dateKeyFormat.format(avgCal.time) - if (sharedPrefs.contains("count_$key")) { - total30 += sharedPrefs.getInt("count_$key", 0) + val dayTotal = Prefs.getTotalCount(this, key) + if (dayTotal > 0) { + total30 += dayTotal daysWithData++ } avgCal.add(Calendar.DAY_OF_YEAR, -1) @@ -169,7 +204,7 @@ class DashboardActivity : AppCompatActivity() { val key = dateKeyFormat.format(checkingDate.time) // If we don't have data for a day, do we assume 0 (clean)? // Usually yes if it's in the past. - val count = sharedPrefs.getInt("count_$key", 0) + val count = Prefs.getTotalCount(this, key) if (count == 0) { streak++ } else { @@ -181,32 +216,32 @@ class DashboardActivity : AppCompatActivity() { } private fun calculatePatterns() { - val allData = sharedPrefs.all + val allDates = Prefs.getAllDatesWithData(this) val dayCounts = IntArray(7) { 0 } // Sun=0, Mon=1... val dayOccurrences = IntArray(7) { 0 } var maxCount = 0 var bestDate = "" - allData.forEach { (key, value) -> - if (key.startsWith("count_") && value is Int) { - val dateStr = key.removePrefix("count_") - try { - val date = dateKeyFormat.parse(dateStr) - if (date != null && value > 0) { + allDates.forEach { dateStr -> + try { + val date = dateKeyFormat.parse(dateStr) + if (date != null) { + val totalCount = Prefs.getTotalCount(this, dateStr) + if (totalCount > 0) { val cal = Calendar.getInstance() cal.time = date val dayOfWeek = cal.get(Calendar.DAY_OF_WEEK) - 1 // 0-indexed - dayCounts[dayOfWeek] += value + dayCounts[dayOfWeek] += totalCount dayOccurrences[dayOfWeek]++ - if (value > maxCount) { - maxCount = value + if (totalCount > maxCount) { + maxCount = totalCount bestDate = dateStr } } - } catch (_: Exception) {} - } + } + } catch (_: Exception) {} } // Busiest Day (Highest Average) @@ -241,7 +276,7 @@ class DashboardActivity : AppCompatActivity() { for (i in 0 until days) { val dateKey = dateKeyFormat.format(calendar.time) val label = labelFormat.format(calendar.time) - val count = sharedPrefs.getInt("count_$dateKey", 0) + val count = Prefs.getTotalCount(this, dateKey) dataPoints.add(Pair(label, count)) calendar.add(Calendar.DAY_OF_YEAR, 1) } diff --git a/app/src/main/java/com/d4vram/cbdcounter/EvolutionActivity.kt b/app/src/main/java/com/d4vram/cbdcounter/EvolutionActivity.kt index 142e24b..298ec6d 100644 --- a/app/src/main/java/com/d4vram/cbdcounter/EvolutionActivity.kt +++ b/app/src/main/java/com/d4vram/cbdcounter/EvolutionActivity.kt @@ -66,7 +66,6 @@ class EvolutionActivity : AppCompatActivity() { private fun loadData(days: Int, offset: Int) { evolutionTitle.text = "Últimos $days días" - val sharedPrefs = getSharedPreferences("CBDCounter", Context.MODE_PRIVATE) val dataPoints = mutableListOf>() val calendar = Calendar.getInstance() // Move back 'offset' days from today @@ -77,8 +76,7 @@ class EvolutionActivity : AppCompatActivity() { for (i in 0 until days) { val dateKey = dateKeyFormat.format(calendar.time) val label = labelFormat.format(calendar.time) - val prefKey = "count_$dateKey" - val count = sharedPrefs.getInt(prefKey, 0) + val count = Prefs.getTotalCount(this, dateKey) dataPoints.add(Pair(label, count)) calendar.add(Calendar.DAY_OF_YEAR, 1) } diff --git a/app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt b/app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt index fcde194..baaa7cd 100644 --- a/app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt +++ b/app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt @@ -24,6 +24,7 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.annotation.StringRes import androidx.core.content.ContextCompat import androidx.core.content.FileProvider +import android.widget.LinearLayout import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.tabs.TabLayout @@ -40,52 +41,51 @@ import kotlin.collections.ArrayList class MainActivity : AppCompatActivity() { - // Views principales - private lateinit var counterText: TextView // Oculto, para compatibilidad + private lateinit var sharedPrefs: SharedPreferences + + companion object { + const val ACTION_DATA_CHANGED = "com.d4vram.cbdcounter.ACTION_DATA_CHANGED" + } + private var cbdCount = 0 + private var thcCount = 0 + private val currentCount: Int get() = cbdCount + thcCount // Total para emoji y compatibilidad + private val allHistoryData = ArrayList() + private val displayedHistoryData = ArrayList() + private var currentViewMode = ViewMode.WEEK + + private val importMimeTypes = arrayOf( + "text/csv", + "text/comma-separated-values", + "application/csv", + "application/vnd.ms-excel", + "text/plain" + ) + + // View variables + private lateinit var counterText: TextView private lateinit var cbdCountText: TextView private lateinit var thcCountText: TextView private lateinit var cbdContainer: View private lateinit var thcContainer: View private lateinit var dateText: TextView private lateinit var emojiText: TextView - private lateinit var addButton: Button + private lateinit var addButton: MaterialButton private lateinit var addInfusedButton: MaterialButton - private lateinit var statsButton: Chip - private lateinit var subtractButton: Button - private lateinit var resetButton: Button + private lateinit var statsButton: Chip // Changed from MaterialButton to Chip + private lateinit var subtractButton: MaterialButton + private lateinit var resetButton: MaterialButton private lateinit var exportButton: ImageButton private lateinit var importButton: ImageButton private lateinit var settingsButton: ImageButton - - // Botón switch para cambiar el tema private lateinit var themeSwitch: SwitchMaterial - - // Views del historial mejorado private lateinit var historyRecyclerView: RecyclerView - private lateinit var historyAdapter: ImprovedHistoryAdapter private lateinit var tabLayout: TabLayout - private lateinit var statsContainer: View + private lateinit var statsContainer: LinearLayout private lateinit var avgText: TextView private lateinit var totalText: TextView private lateinit var streakText: TextView private lateinit var searchButton: ImageButton - - // Data - private lateinit var sharedPrefs: SharedPreferences - private var cbdCount = 0 - private var thcCount = 0 - private val currentCount: Int get() = cbdCount + thcCount // Total para emoji y compatibilidad - private val allHistoryData = ArrayList() - private val displayedHistoryData = ArrayList() - private var currentViewMode = ViewMode.WEEK - - private val importMimeTypes = arrayOf( - "text/csv", - "text/comma-separated-values", - "application/csv", - "application/vnd.ms-excel", - "text/plain" - ) + private lateinit var historyAdapter: ImprovedHistoryAdapter // Receptor para detectar cambio de día/hora mientras la app está abierta private val dateChangeReceiver = object : BroadcastReceiver() { @@ -179,7 +179,7 @@ class MainActivity : AppCompatActivity() { addAction(Intent.ACTION_TIME_CHANGED) addAction(Intent.ACTION_TIMEZONE_CHANGED) } - registerReceiver(dateChangeReceiver, filter) + registerReceiver(dateChangeReceiver, filter, Context.RECEIVER_NOT_EXPORTED) } override fun onPause() { @@ -694,6 +694,9 @@ class MainActivity : AppCompatActivity() { updateDisplay() updateHistoryView() updateStats() + + // Notify other activities (like DashboardActivity) that data has changed + sendBroadcast(Intent(ACTION_DATA_CHANGED)) } showFeedback("Importación completada", false) }.onFailure { e -> From 43b8beb18a31ceca1de7f35e15a7706277c5765d Mon Sep 17 00:00:00 2001 From: D4vRAM369 Date: Tue, 20 Jan 2026 01:34:09 +0000 Subject: [PATCH 5/5] fix: corregir formato CSV export de notas (RFC 4180) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PROBLEMA: Las notas con espacios o comas se exportaban mal en CSV, dividiéndose en múltiples columnas en lugar de una sola. Ejemplo: Antes: 23/08/2025,5,0,Nueva feature de notas Ahora: 23/08/2025,5,0,"Nueva feature de notas" CAUSA: escapeCsvField() usaba escape con backslash (\,) en lugar del estándar RFC 4180 que requiere comillas dobles. SOLUCIÓN: - Actualizar escapeCsvField() para usar RFC 4180: * Campos con comas/saltos/comillas → entre comillas dobles * Comillas internas se duplican: " → "" - Actualizar unescapeCsvField() para: * Parsear nuevo formato RFC 4180 (comillas dobles) * Mantener compatibilidad con formato antiguo (backslash) IMPACTO: ✅ Export CSV ahora genera archivos estándar ✅ Import CSV sigue funcionando con ambos formatos ✅ Compatible con Excel, LibreOffice, Google Sheets ARCHIVOS: - MainActivity.kt: escapeCsvField() y unescapeCsvField() - docs/: Exportados 10 artifacts .md con fecha 2026-01-19 --- ...ommand-_CBDcounter_v1.4_implementation.txt | 2227 ----------------- app/release/baselineProfiles/0/app-release.dm | Bin 3263 -> 3261 bytes app/release/baselineProfiles/1/app-release.dm | Bin 3190 -> 3195 bytes .../com/d4vram/cbdcounter/MainActivity.kt | 26 +- docs/index.md | 86 - docs/privacy-policy.md | 229 -- 6 files changed, 17 insertions(+), 2551 deletions(-) delete mode 100644 2026-01-19-command-messagesuperpowersbrainstormcommand-_CBDcounter_v1.4_implementation.txt delete mode 100644 docs/index.md delete mode 100644 docs/privacy-policy.md diff --git a/2026-01-19-command-messagesuperpowersbrainstormcommand-_CBDcounter_v1.4_implementation.txt b/2026-01-19-command-messagesuperpowersbrainstormcommand-_CBDcounter_v1.4_implementation.txt deleted file mode 100644 index dc9480e..0000000 --- a/2026-01-19-command-messagesuperpowersbrainstormcommand-_CBDcounter_v1.4_implementation.txt +++ /dev/null @@ -1,2227 +0,0 @@ - - ▐▛███▜▌ Claude Code v2.1.12 -▝▜█████▛▘ Opus 4.5 · Claude Pro - ▘▘ ▝▝ ~/AndroidStudioProjects/CBDcounter2 - -❯ /skills - ⎿ Skills dialog dismissed - -❯ /superpowers:brainstorm - -● Skill(superpowers:brainstorming) - ⎿  Successfully loaded skill - -● Perfecto, estamos en modo brainstorming. Voy a revisar el contexto actual del - proyecto para entender dónde estamos. - -● Bash(git log --oneline -10) - ⎿  86df4da docs: add v1.4 design document - 61eaa34 feat: mejora del modal de día, controles de audio y corrección - de tema - … +31 lines (ctrl+o to expand) - -● Search(pattern: "docs/**/*.md") - ⎿  Found 2 files (ctrl+o to expand) - -● Bash(git status --short 2>/dev/null | head -30) - ⎿   M .gitignore - M app/src/main/AndroidManifest.xml - M app/src/main/java/com/d4vram/cbdcounter/EmojiSettingsActivity.kt - … +22 lines (ctrl+o to expand) - -● Search(pattern: "app/src/main/java/**/*.kt") - ⎿  Found 15 files (ctrl+o to expand) - -● Veo que estás trabajando en CBDcounter v1.4 - hay bastantes cambios en - progreso. Tienes nuevos archivos como DashboardActivity, CalendarActivity, - BackupManager, y has eliminado StatsActivity. Parece que la v1.4 trae una - reestructuración significativa. - - ¿Qué te gustaría explorar hoy? - - 1. Continuar v1.4 - Refinar o completar el diseño de los cambios actuales - (Dashboard, Calendar, etc.) - 2. Nueva feature - Diseñar algo nuevo que quieras añadir al proyecto - 3. Rediseño de algo existente - Repensar alguna parte de la app que no te - convence - 4. Otro - Cuéntame qué tienes en mente - -❯ Al activar el modo THC, si clicko en +1 debe salir THC en lugar de CBD en - la segunda foto. Si se han fumado ambos en el d - ⎿  [Image #1] - ⎿  [Image #2] - -● Entiendo la idea - tienes un toggle CBD/THC y quieres que el label cambie - según el modo activo. Veo las capturas de Ajustes pero parece que el mensaje - se cortó. - - Para asegurarme de entender bien, ¿dónde exactamente debería mostrar "THC" en - lugar de "CBD"? - - 1. En el contador principal (MainActivity) - El número grande que muestra el - conteo del día - 2. En las notas del día - Donde aparece "🌿 HH:mm" o similar - 3. En el widget - El texto que acompaña al contador - 4. Otro lugar - Descríbeme dónde - - Y mencionaste algo sobre "si se han fumado ambos en el día" pero se cortó - - ¿puedes completar esa idea? - -❯ Si se han fumado ambos en el día, entonces CBD+THC de forma que quede - bien en UI y buena UX - -● Perfecto, ahora entiendo mejor. Déjame revisar cómo funciona actualmente el - sistema de conteo para entender el contexto. - -● Read(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Read 989 lines - -● Read(app/src/main/java/com/d4vram/cbdcounter/Prefs.kt) - ⎿  Read 55 lines - -● Ahora veo el panorama completo. Encontré el problema en línea 917 del adapter: - - holder.countText.text = "${item.count} CBD" // Siempre dice "CBD" - - Y veo que actualmente Prefs.setSubstanceForDate() guarda UN solo valor por - día. Pero si quieres trackear CBD+THC, necesitamos cambiar cómo se almacena. - - Pregunta clave: ¿Cómo debe funcionar el conteo cuando se usan ambas sustancias - en el día? - - 1. Un contador único, label mixto - El número sigue siendo uno solo (ej: "5 - CBD+THC"). No sabes cuántos fueron de cada tipo, solo que usaste ambos. - 2. Contadores separados - Mostrar "3 CBD + 2 THC" con números distintos. - Requiere trackear dos conteos independientes. - 3. Un contador, pero registro en notas - El contador principal es uno, pero - las notas indican qué tipo fue cada entrada (como ya funciona con 🔹). El - label cambia según lo que aparezca en las notas del día. - - ¿Cuál encaja mejor con lo que tienes en mente? - -❯ 2 - -● Perfecto, contadores separados. Esto implica cambios en la estructura de datos - y en varias partes de la UI. - - Siguiente pregunta: ¿Cómo debe verse el contador principal en MainActivity - cuando hay ambos? - - 1. Mostrar solo el activo - Si estás en modo THC, ves "3" (THC). Cambias a - CBD, ves "2" (CBD). Simple, pero no ves el total del día de un vistazo. - 2. Mostrar ambos lado a lado - Algo como 2 CBD | 3 THC siempre visible, con el - modo activo destacado/más grande. - 3. Mostrar suma con desglose - Número grande "5" con subtexto pequeño "2 CBD + - 3 THC" debajo. - - ¿Cuál prefieres para la pantalla principal? - -❯ 2. Si sale biien en UI - -● Entendido, ambos visibles con el activo destacado. Ahora la última pregunta de - UI. - - ¿Cómo debe verse en el historial (la lista de días)? - - 1. Compacto - Una sola línea: 3 CBD · 2 THC o 3+2 CBD|THC - 2. Apilado - Dos líneas pequeñas: - 3 CBD - 2 THC - 3. Condicional - Solo mostrar lo que haya ese día: - - Si solo CBD: 3 CBD - - Si solo THC: 2 THC - - Si ambos: 3 CBD + 2 THC - -❯ Dos lineas pequeñas, siendo el color del chip de THC distinto al azul, - más verde - -● Perfecto, tengo todo lo que necesito. Déjame presentarte el diseño por partes. - - --- - Diseño: Contadores Separados CBD/THC - - 1. Estructura de Datos - - Almacenamiento actual: - - count_DD/MM/YYYY → número único - - substance_DD/MM/YYYY → "CBD" o "THC" - - Nueva estructura: - - count_cbd_DD/MM/YYYY → conteo CBD del día - - count_thc_DD/MM/YYYY → conteo THC del día - - Se elimina substance_DD/MM/YYYY (ya no necesario por día) - - Se mantiene substance_type → modo activo actual (para saber qué botón - incrementar) - - Migración: Los datos existentes se migran a count_cbd_* asumiendo que todo lo - anterior era CBD (o podemos preguntar al usuario). - - CSV export/import: Nueva columna o formato count_cbd,count_thc en lugar de - count,substance. - - --- - ¿Esto tiene sentido para la parte de datos? ¿O prefieres otro approach para la - migración? - -❯ Si, y además que si activo modo THC no se cambien los días que tienen CBD - registrados, sino que se pongan 2 chips como hemos comentado de la forma - dicha. Y si le doy al +1 verde con weed y polen, suma 1 THC - -● Exacto, entendido perfectamente: - - 1. El modo activo solo afecta qué contador incrementa el +1 (y el botón de - infusión) - 2. Los datos históricos no se tocan - si un día tiene 3 CBD y cambias a modo - THC, sigue mostrando 3 CBD. Si luego sumas +1, ese día pasa a mostrar 3 CBD / - 1 THC - 3. Infusión (weed/polen) suma al modo activo - si estás en THC, el 🌿 y 🍫 - suman a THC - - --- - 2. UI - Pantalla Principal - - Layout del contador (cuando hay ambos): - - ┌─────────────────────────┐ - │ 2 │ 3 │ ← números grandes - │ CBD │ THC │ ← labels pequeños - │ (azul) (verde) │ - └─────────────────────────┘ - ↑ activo = más grande/destacado - - - El modo activo se muestra ligeramente más grande o con fondo destacado - - Si un contador está en 0, se muestra igual pero más tenue (o 0 pequeño) - - El emoji se basa en la suma total del día (2+3=5 → emoji de 5) - - Colores: - - CBD: azul actual (cbd_primary) - - THC: verde hierba (propongo #4CAF50 o similar, definimos thc_green) - - --- - ¿Te cuadra esta distribución visual para el contador principal? - -❯ Si - -● Perfecto. Siguiente parte: - - --- - 3. UI - Historial (lista de días) - - Cada ítem del historial mostrará: - - ┌────────────────────────────────────────┐ - │ Lunes 13 😄 📝 🎤 │ - │ ┌───────┐ ┌───────┐ │ - │ │ 3 CBD │ │ 2 THC │ │ - │ └───────┘ └───────┘ │ - │ ▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░ (barra total: 5) │ - └────────────────────────────────────────┘ - - Reglas de visibilidad: - - Si solo CBD → solo chip azul 3 CBD - - Si solo THC → solo chip verde 2 THC - - Si ambos → ambos chips lado a lado - - Barra de progreso: - - Se basa en la suma total (CBD + THC) - - Color: podría ser degradado o el color del mayor, ¿prefieres algo simple - como gris neutro para cuando hay mezcla? - - --- - ¿Qué color prefieres para la barra cuando hay ambas sustancias? - -❯ Color degradado o del mayor, que el color cambie según la cantidad de - cada día y de CBD o THC. Me encanta el diseño propuesto - -● Genial, me alegra que te guste. Para la barra: - - --- - 4. Lógica de Color de la Barra - - Regla: - - Solo CBD → escala azul→naranja→rojo según cantidad (como ahora) - - Solo THC → escala verde→amarillo→rojo según cantidad - - Ambos → color del mayor. Si empate, degradado mitad-mitad - - Escala por cantidad (aplica a ambos): - 0 → verde seguro - 1-3 → color suave (azul claro / verde claro) - 4-6 → color medio (naranja / amarillo) - 7+ → rojo crítico - - --- - 5. Widget - - El widget también necesita actualizarse: - - Opciones: - 1. Mostrar solo el modo activo - Más simple, el widget muestra el contador del - modo que tengas seleccionado - 2. Mostrar ambos compactos - 2|3 con colores CBD/THC - - ¿Cuál prefieres para el widget dado el espacio limitado (2x2)? - -❯ 0 → verde seguro - - 1-4 → color suave (azul claro / verde claro) - - 5-6 → color medio (naranja / amarillo) - - 7+ → rojo crítico . El Widget la 1 - -● Perfecto, anotado. Última sección: - - --- - 6. Otros Cambios Necesarios - - Botón +1 y botón Infusión: - - Texto/color cambia según modo activo - - En modo THC: botón principal podría tener tinte verde sutil - - Notas (timestamps): - - Actualmente: 🔹 14:30 para CBD normal - - Propuesta: - - CBD: 🔹 14:30 (se mantiene) - - THC: 🟢 14:30 (o 💚) para distinguir visualmente - - Evolution/Stats: - - Los gráficos mostrarían líneas separadas CBD vs THC - - O una línea de total con opción de filtrar - - Feedback (Toast): - - Modo CBD: "+1 CBD añadido" - - Modo THC: "+1 THC añadido" - - --- - Resumen del Diseño - ┌────────────────────┬───────────────────────────────────────────────────┐ - │ Área │ Cambio │ - ├────────────────────┼───────────────────────────────────────────────────┤ - │ Datos │ Dos contadores por día: count_cbd_* y count_thc_* │ - ├────────────────────┼───────────────────────────────────────────────────┤ - │ Migración │ Datos existentes → CBD │ - ├────────────────────┼───────────────────────────────────────────────────┤ - │ Contador principal │ Dos números lado a lado, activo destacado │ - ├────────────────────┼───────────────────────────────────────────────────┤ - │ Historial │ Chips apilados, barra color del mayor │ - ├────────────────────┼───────────────────────────────────────────────────┤ - │ Widget │ Solo modo activo │ - ├────────────────────┼───────────────────────────────────────────────────┤ - │ Colores │ CBD=azul, THC=verde hierba │ - ├────────────────────┼───────────────────────────────────────────────────┤ - │ Rangos │ 0=verde, 1-4=suave, 5-6=medio, 7+=crítico │ - └────────────────────┴───────────────────────────────────────────────────┘ - --- - ¿Hay algo que quieras ajustar antes de que escriba el documento de diseño? - -❯ No, hacia adelante vamos! Y quiero luego las explicaciones PBL y también - registro para Plaud Note - -● ¡Vamos! Escribo el documento de diseño y luego te preparo el material PBL. - -● Bash(mkdir -p /home/defcon/AndroidStudioProjects/CBDcounter2/docs/plans) - ⎿  (No content) - -● Write(docs/plans/2026-01-19-dual-counters-design.md) - ⎿  Wrote 187 lines to docs/plans/2026-01-19-dual-counters-design.md - # Diseño: Contadores Separados CBD/THC - **Fecha:** 2026-01-19 - **Versión:** v1.4 - **Estado:** Aprobado - ## Resumen - Implementar contadores independientes para CBD y THC, permitiendo - trackear ambas sustancias por separado en el mismo día. La UI mostrará - ambos contadores cuando existan, con colores distintivos (azul para - CBD, verde para THC). - - … +177 lines (ctrl+o to expand) - -● Bash(git add docs/plans/2026-01-19-dual-counters-design.md && git commit -m - "docs: add dual counters CBD/THC design document…) - ⎿  Interrupted · What should Claude do instead? - -❯ Primero incluimos la carpeta docs en .gitignore - -● Read(.gitignore) - ⎿  Read 31 lines - -● Update(.gitignore) - ⎿  Added 1 line - 8 # OS - 9 # Output - 10 # Sesiones PBL (Learning/Aprendizaje) - 11 +docs/ - 12 *.aab - 13 *.apk - 14 build/ - -● Listo. Ahora docs/ está ignorado. El documento de diseño queda local para ti - pero no se sube al repo. - - --- - Explicación PBL + Registro Plaud Note - - Aquí tienes el material para estudiar y grabar: - - --- - 📦 CONCEPTO: SharedPreferences con Múltiples Claves por Entidad - - 🎯 QUÉ ES: - Patrón de almacenamiento donde una entidad (un día) tiene múltiples valores - asociados usando prefijos distintos en las claves. - - 🔧 PARA QUÉ SIRVE: - Cuando necesitas guardar datos relacionados pero independientes. En lugar de - un objeto complejo (JSON), usas claves paralelas que comparten un - identificador común. - - 💡 ANALOGÍA: - Imagina un archivador físico. Antes tenías una carpeta por día con UN papel - (el conteo). Ahora tienes DOS papeles en cada carpeta: uno azul (CBD) y uno - verde (THC). La carpeta sigue siendo la misma fecha, pero contiene información - separada. - - 📝 EJEMPLO: - // Antes: una clave por día - "count_19/01/2026" → 5 - - // Ahora: dos claves por día - "count_cbd_19/01/2026" → 3 - "count_thc_19/01/2026" → 2 - - 🔗 EN TU PROYECTO: - Prefs.kt tendrá funciones nuevas: - fun getCbdCount(ctx: Context, date: String): Int - fun getThcCount(ctx: Context, date: String): Int - fun getTotalCount(ctx: Context, date: String): Int // suma ambos - - --- - 📦 CONCEPTO: Migración de Datos - - 🎯 QUÉ ES: - Proceso de transformar datos existentes a un nuevo formato cuando cambias la - estructura de almacenamiento. - - 🔧 PARA QUÉ SIRVE: - Los usuarios que ya tienen datos no deben perderlos. La migración convierte el - formato viejo al nuevo automáticamente. - - 💡 ANALOGÍA: - Es como mudarte de casa. Tienes muebles viejos (datos) que deben caber en las - habitaciones nuevas (estructura). Empaquetas todo del formato viejo, lo - desempaquetas en el formato nuevo, y marcas "mudanza completada" para no - repetirla. - - 📝 PATRÓN: - fun migrateIfNeeded(ctx: Context) { - if (prefs.getBoolean("migration_v1.4_done", false)) return - - // Convertir count_* → count_cbd_* - // Eliminar claves viejas - - prefs.edit().putBoolean("migration_v1.4_done", true).apply() - } - - --- - 📝 PARA PLAUD NOTE (resumen de 2 min): - - "Hoy diseñamos contadores separados para CBD y THC en CBDcounter. - - El cambio principal es en SharedPreferences: pasamos de una clave count_fecha - a dos claves count_cbd_fecha y count_thc_fecha. Esto permite trackear ambas - sustancias independientemente en el mismo día. - - La UI muestra ambos contadores lado a lado en MainActivity, con el modo activo - destacado. En el historial, chips apilados con colores distintos: azul CBD, - verde THC. - - Conceptos clave aprendidos: patrón de claves paralelas en SharedPreferences y - migración de datos con flag de completado para no repetir." - - --- - ¿Quieres que empecemos la implementación ahora o prefieres dejarlo para otra - sesión? - -✻ Cogitated for 31s - -❯ Empezamos ahora, y luego al final de todo desglosa más el proceso para - Plaud Note, con explicaciones, conceptos, PBL, glosario, etc. en un - archivo - -● ¡Vamos a ello! Creo el plan de trabajo y empezamos. - -● Empezamos con Prefs.kt. Añado los nuevos getters/setters y la migración. - -● Read(app/src/main/java/com/d4vram/cbdcounter/Prefs.kt) - ⎿  Read 55 lines - -● Update(app/src/main/java/com/d4vram/cbdcounter/Prefs.kt) - ⎿  Added 5 lines, removed 2 lines - 6 - 7 object Prefs { - 8 private const val PREFS_NAME = "CBDCounter" - 9 - - 9 + - 10 // Constantes de claves para evitar errores tipográficos - 11 - const val KEY_COUNT_PREFIX = "count_" - 11 + const val KEY_COUNT_PREFIX = "count_" - + // Legacy, para migración - 12 + const val KEY_COUNT_CBD_PREFIX = "count_cbd_" - 13 + const val KEY_COUNT_THC_PREFIX = "count_thc_" - 14 const val KEY_NOTE_PREFIX = "NOTE_" - 15 const val KEY_DARK_MODE = "dark_mode" - 16 + private const val KEY_MIGRATION_V14_DONE = - +"migration_v1.4_done" - 17 - 18 // función privada para acceder a las SharedPreferences - 19 private fun prefs(ctx: Context): SharedPreferences = - -● Update(app/src/main/java/com/d4vram/cbdcounter/Prefs.kt) - ⎿  Added 93 lines, removed 5 lines - 40 val audioFile = File(ctx.filesDir, - "audios/audio_$date.mp3") - 41 return audioFile.exists() - 42 } - 43 - // ---- Sustancia (CBD vs THC) ---- - 43 + - 44 + // ---- Sustancia (CBD vs THC) - Modo activo ---- - 45 fun getSubstanceType(ctx: Context): String = - 46 prefs(ctx).getString("substance_type", "CBD") ?: "CBD" - 47 - ... - 48 prefs(ctx).edit().putString("substance_type", - type).apply() - 49 } - 50 - 51 - fun getSubstanceForDate(ctx: Context, date: String): - -String? = - 52 - prefs(ctx).getString("substance_$date", null) - 51 + // ---- Contadores separados CBD/THC ---- - 52 + fun getCbdCount(ctx: Context, date: String): Int = - 53 + prefs(ctx).getInt("${KEY_COUNT_CBD_PREFIX}$date", 0) - 54 - 55 - fun setSubstanceForDate(ctx: Context, date: String, type: - -String) { - 56 - prefs(ctx).edit().putString("substance_$date", type - -).apply() - 55 + fun setCbdCount(ctx: Context, date: String, count: Int) { - 56 + prefs(ctx).edit().putInt("${KEY_COUNT_CBD_PREFIX}$ - +date", count).apply() - 57 } - 58 + - 59 + fun getThcCount(ctx: Context, date: String): Int = - 60 + prefs(ctx).getInt("${KEY_COUNT_THC_PREFIX}$date", 0) - 61 + - 62 + fun setThcCount(ctx: Context, date: String, count: Int) { - 63 + - +prefs(ctx).edit().putInt("${KEY_COUNT_THC_PREFIX}$date", - +count).apply() - 64 + } - 65 + - 66 + fun getTotalCount(ctx: Context, date: String): Int = - 67 + getCbdCount(ctx, date) + getThcCount(ctx, date) - 68 + - 69 + /** Incrementa el contador del modo activo y devuelve el - +nuevo valor */ - 70 + fun incrementActiveCount(ctx: Context, date: String): Int - + { - 71 + val isThc = getSubstanceType(ctx) == "THC" - 72 + return if (isThc) { - 73 + val newCount = getThcCount(ctx, date) + 1 - 74 + setThcCount(ctx, date, newCount) - 75 + newCount - 76 + } else { - 77 + val newCount = getCbdCount(ctx, date) + 1 - 78 + setCbdCount(ctx, date, newCount) - 79 + newCount - 80 + } - 81 + } - 82 + - 83 + /** Decrementa el contador del modo activo (mínimo 0) y - +devuelve el nuevo valor */ - 84 + fun decrementActiveCount(ctx: Context, date: String): Int - + { - 85 + val isThc = getSubstanceType(ctx) == "THC" - 86 + return if (isThc) { - 87 + val newCount = maxOf(0, getThcCount(ctx, date) - - +1) - 88 + setThcCount(ctx, date, newCount) - 89 + newCount - 90 + } else { - 91 + val newCount = maxOf(0, getCbdCount(ctx, date) - - +1) - 92 + setCbdCount(ctx, date, newCount) - 93 + newCount - 94 + } - 95 + } - 96 + - 97 + /** Obtiene el conteo del modo activo */ - 98 + fun getActiveCount(ctx: Context, date: String): Int { - 99 + val isThc = getSubstanceType(ctx) == "THC" - 100 + return if (isThc) getThcCount(ctx, date) else - +getCbdCount(ctx, date) - 101 + } - 102 + - 103 + // ---- Migración v1.4 ---- - 104 + fun migrateToV14IfNeeded(ctx: Context) { - 105 + val prefs = prefs(ctx) - 106 + if (prefs.getBoolean(KEY_MIGRATION_V14_DONE, false)) - +return - 107 + - 108 + val editor = prefs.edit() - 109 + val allEntries = prefs.all - 110 + - 111 + // Migrar count_* → count_cbd_* - 112 + allEntries.keys - 113 + .filter { it.startsWith(KEY_COUNT_PREFIX) && - +!it.startsWith(KEY_COUNT_CBD_PREFIX) && - +!it.startsWith(KEY_COUNT_THC_PREFIX) } - 114 + .forEach { oldKey -> - 115 + val date = - +oldKey.removePrefix(KEY_COUNT_PREFIX) - 116 + val count = allEntries[oldKey] as? Int ?: 0 - 117 + editor.putInt("${KEY_COUNT_CBD_PREFIX}$date", - + count) - 118 + editor.remove(oldKey) - 119 + } - 120 + - 121 + // Eliminar claves substance_* obsoletas - 122 + allEntries.keys - 123 + .filter { it.startsWith("substance_") && it != - +"substance_type" } - 124 + .forEach { editor.remove(it) } - 125 + - 126 + editor.putBoolean(KEY_MIGRATION_V14_DONE, true) - 127 + editor.apply() - 128 + } - 129 + - 130 + /** Obtiene todas las fechas que tienen datos (para - +historial) */ - 131 + fun getAllDatesWithData(ctx: Context): Set { - 132 + val prefs = prefs(ctx) - 133 + val dates = mutableSetOf() - 134 + - 135 + prefs.all.keys.forEach { key -> - 136 + when { - 137 + key.startsWith(KEY_COUNT_CBD_PREFIX) -> - +dates.add(key.removePrefix(KEY_COUNT_CBD_PREFIX)) - 138 + key.startsWith(KEY_COUNT_THC_PREFIX) -> - +dates.add(key.removePrefix(KEY_COUNT_THC_PREFIX)) - 139 + key.startsWith(KEY_NOTE_PREFIX) -> - +dates.add(key.removePrefix(KEY_NOTE_PREFIX)) - 140 + } - 141 + } - 142 + return dates - 143 + } - 144 } - -● Ahora actualizo el CSV export/import en MainActivity. Primero la función - buildCsvContent: - -● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Added 9 lines, removed 18 lines - 559 } - 560 - 561 private fun buildCsvContent(): String { - 562 - val prefsMap = sharedPrefs.all - 563 - if (prefsMap.isEmpty()) return "" - 562 + val allDates = Prefs.getAllDatesWithData(this) - 563 + if (allDates.isEmpty()) return "" - 564 - 565 - val dates = mutableSetOf() - 566 - prefsMap.keys.forEach { key -> - 567 - when { - 568 - key.startsWith("count_") -> - -dates.add(key.removePrefix("count_")) - 569 - key.startsWith("NOTE_") -> - -dates.add(key.removePrefix("NOTE_")) - 570 - } - 571 - } - 572 - if (dates.isEmpty()) return "" - 573 - - 565 val dateFormat = SimpleDateFormat("dd/MM/yyyy", - Locale.getDefault()) - 566 - val sortedDates = dates.mapNotNull { dateString -> - 566 + val sortedDates = allDates.mapNotNull { dateString -> - 567 runCatching { dateFormat.parse(dateString) - }.getOrNull()?.let { parsed -> - 568 dateString to parsed - 569 } - 570 }.sortedBy { it.second } - 571 - 572 - val builder = StringBuilder("date,count,note,substance - -\n") - 572 + val builder = StringBuilder("date,count_cbd,count_thc, - +note\n") - 573 sortedDates.forEach { (dateString, _) -> - 574 - val count = - -sharedPrefs.getInt("count_$dateString", 0) - 574 + val cbdCount = Prefs.getCbdCount(this, - +dateString) - 575 + val thcCount = Prefs.getThcCount(this, - +dateString) - 576 val note = Prefs.getNote(this, dateString) ?: "" - 577 - val substance = Prefs.getSubstanceForDate(this, - -dateString) ?: "CBD" // Asumir CBD si no existe - 577 - 578 builder.append(dateString) - 579 .append(',') - 580 - .append(count) - 580 + .append(cbdCount) - 581 .append(',') - 582 - .append(escapeCsvField(note)) - 582 + .append(thcCount) - 583 .append(',') - 584 - .append(escapeCsvField(substance)) - 584 + .append(escapeCsvField(note)) - 585 .append('\n') - 586 } - 587 return builder.toString() - -● Ahora actualizo handleImportCsv para el nuevo formato (y compatibilidad con el - viejo): - -● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Added 34 lines, removed 9 lines - 595 if (lines.isEmpty()) throw - IllegalArgumentException("Archivo vacío") - 596 - 597 val editor = sharedPrefs.edit() - 598 + // Limpiar datos existentes - 599 sharedPrefs.all.keys.filter { - 600 it.startsWith("count_") || - it.startsWith("NOTE_") - 601 }.forEach { key -> editor.remove(key) } - 602 - 603 + // Detectar formato por cabecera - 604 + val header = lines.first().lowercase() - 605 + val isNewFormat = - +header.contains("count_cbd") - 606 + - 607 lines.drop(1).forEach { line -> - 608 if (line.isBlank()) return@forEach - 609 val columns = splitCsvLine(line) - 610 if (columns.size < 2) return@forEach - 611 - 612 val date = columns[0] - 613 - val count = columns[1].toIntOrNull() - -?: return@forEach - 614 - editor.putInt("count_$date", count) - 613 - 614 - val rawNote = if (columns.size >= 3) - -columns[2] else "" - 615 - val note = unescapeCsvField(rawNote) - 616 - if (note.isNotEmpty()) { - 617 - editor.putString("NOTE_$date", - -note) - 618 - } - 614 + if (isNewFormat) { - 615 + // Nuevo formato: - +date,count_cbd,count_thc,note - 616 + val cbdCount = - +columns.getOrNull(1)?.toIntOrNull() ?: 0 - 617 + val thcCount = - +columns.getOrNull(2)?.toIntOrNull() ?: 0 - 618 + - +editor.putInt("${Prefs.KEY_COUNT_CBD_PREFIX}$date", cbdCount) - 619 + - +editor.putInt("${Prefs.KEY_COUNT_THC_PREFIX}$date", thcCount) - 620 - 621 - val substance = if (columns.size >= - -4) unescapeCsvField(columns[3]) else "CBD" - 622 - Prefs.setSubstanceForDate(this, date, - - substance) - 621 + val rawNote = - +columns.getOrNull(3) ?: "" - 622 + val note = - +unescapeCsvField(rawNote) - 623 + if (note.isNotEmpty()) { - 624 + - +editor.putString("${Prefs.KEY_NOTE_PREFIX}$date", note) - 625 + } - 626 + } else { - 627 + // Formato legacy: - +date,count,note,substance - 628 + val count = - +columns[1].toIntOrNull() ?: return@forEach - 629 + val substance = if (columns.size - +>= 4) unescapeCsvField(columns[3]) else "CBD" - 630 + - 631 + // Importar al contador - +correspondiente - 632 + if (substance == "THC") { - 633 + - +editor.putInt("${Prefs.KEY_COUNT_THC_PREFIX}$date", count) - 634 + } else { - 635 + - +editor.putInt("${Prefs.KEY_COUNT_CBD_PREFIX}$date", count) - 636 + } - 637 + - 638 + val rawNote = if (columns.size >= - + 3) columns[2] else "" - 639 + val note = - +unescapeCsvField(rawNote) - 640 + if (note.isNotEmpty()) { - 641 + - +editor.putString("${Prefs.KEY_NOTE_PREFIX}$date", note) - 642 + } - 643 + } - 644 } - 645 editor.apply() - 646 } - -● Ahora añado la llamada a la migración en onCreate y actualizo loadTodayData: - -● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Added 2 lines, removed 1 line - 121 override fun onCreate(savedInstanceState: Bundle?) { - 122 // 1. Aplicar tema ANTES de super.onCreate para - evitar flickering - 123 initSharedPreferences() - 124 + Prefs.migrateToV14IfNeeded(this) // Migrar datos al - +nuevo formato si es necesario - 125 applyStoredTheme() - 126 - - 126 + - 127 super.onCreate(savedInstanceState) - 128 setContentView(R.layout.activity_main) - 129 - -● Ahora actualizo las variables y loadTodayData para manejar ambos contadores: - -● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Added 3 lines, removed 1 line - 68 - 69 // Data - 70 private lateinit var sharedPrefs: SharedPreferences - 71 - private var currentCount = 0 - 71 + private var cbdCount = 0 - 72 + private var thcCount = 0 - 73 + private val currentCount: Int get() = cbdCount + thcCount - + // Total para emoji y compatibilidad - 74 private val allHistoryData = ArrayList() - 75 private val displayedHistoryData = - ArrayList() - 76 private var currentViewMode = ViewMode.WEEK - -● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Added 2 lines, removed 1 line - 281 - 282 private fun loadTodayData() { - 283 val today = getCurrentDateKey() - 284 - currentCount = sharedPrefs.getInt("count_$today", 0) - 284 + cbdCount = Prefs.getCbdCount(this, today) - 285 + thcCount = Prefs.getThcCount(this, today) - 286 } - 287 - 288 private fun loadAllHistoryData() { - -● Ahora actualizo loadAllHistoryData para el nuevo formato y actualizo - HistoryItem: - -● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Added 10 lines, removed 11 lines - 287 - 288 private fun loadAllHistoryData() { - 289 allHistoryData.clear() - 290 - val allEntries = sharedPrefs.all - 290 val dateFormat = SimpleDateFormat("dd/MM/yyyy", - Locale.getDefault()) - 291 - 292 - allEntries.forEach { (key, value) -> - 293 - if (key.startsWith("count_") && value is Int) { - 294 - val dateString = key.removePrefix("count_") - 295 - try { - 296 - val date = dateFormat.parse(dateString) - 297 - if (date != null) { - 298 - - -allHistoryData.add(HistoryItem(dateString, value, date)) - 299 - } - 300 - } catch (_: Exception) {} - 301 - } - 292 + val allDates = Prefs.getAllDatesWithData(this) - 293 + allDates.forEach { dateString -> - 294 + try { - 295 + val date = dateFormat.parse(dateString) - 296 + if (date != null) { - 297 + val cbd = Prefs.getCbdCount(this, - +dateString) - 298 + val thc = Prefs.getThcCount(this, - +dateString) - 299 + - +allHistoryData.add(HistoryItem(dateString, cbd, thc, date)) - 300 + } - 301 + } catch (_: Exception) {} - 302 } - 303 allHistoryData.sortByDescending { it.dateObject } - 304 } - -● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Added 8 lines, removed 1 line - 868 } - 869 - 870 // Data class - 871 -data class HistoryItem(val date: String, val count: Int, val - -dateObject: Date) - 871 +data class HistoryItem( - 872 + val date: String, - 873 + val cbdCount: Int, - 874 + val thcCount: Int, - 875 + val dateObject: Date - 876 +) { - 877 + val totalCount: Int get() = cbdCount + thcCount - 878 +} - 879 - 880 // Adapter - 881 class ImprovedHistoryAdapter( - -● Ahora actualizo saveData: - -● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Added 2 lines, removed 5 lines - 357 - 358 private fun saveData() { - 359 val today = getCurrentDateKey() - 360 - sharedPrefs.edit().putInt("count_$today", - -currentCount).apply() - 361 - - 362 - // Guardar también el tipo de sustancia activa para - -este día - 363 - val currentSubstance = Prefs.getSubstanceType(this) - 364 - Prefs.setSubstanceForDate(this, today, - -currentSubstance) - 360 + Prefs.setCbdCount(this, today, cbdCount) - 361 + Prefs.setThcCount(this, today, thcCount) - 362 - 363 loadAllHistoryData() - 364 updateHistoryView() - -● Ahora actualizo registerIntake para incrementar el contador correcto: - -● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Added 6 lines, removed 1 line - 779 } - 780 - 781 private fun registerIntake(entry: String, - feedbackMessage: String) { - 782 - currentCount++ - 782 + val isThc = Prefs.getSubstanceType(this) == "THC" - 783 + if (isThc) { - 784 + thcCount++ - 785 + } else { - 786 + cbdCount++ - 787 + } - 788 updateDisplay() - 789 appendEntryToTodayNote(entry) - 790 saveData() - -● Ahora actualizo el botón -1 (subtract) para decrementar el contador correcto: - -● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Added 11 lines, removed 7 lines - 444 startActivity(Intent(this, - SettingsActivity::class.java)) - 445 } - 446 subtractButton.setOnClickListener { - 447 - if (currentCount > 0) { - 447 + val isThc = Prefs.getSubstanceType(this) == "THC" - 448 + val activeCount = if (isThc) thcCount else cbdCount - 449 + - 450 + if (activeCount > 0) { - 451 // Inflar layout personalizado - 452 val dialogView = LayoutInflater.from(this).inflat - e(R.layout.dialog_confirm_delete, null) - 453 - - 453 + - 454 // Crear el diálogo - 455 val dialog = MaterialAlertDialogBuilder(this) - 456 .setView(dialogView) - ... - 456 - 457 // Configurar chips - 458 dialogView.findViewById(R.id.chip_confirm). - setOnClickListener { - 459 - currentCount-- - 459 + if (isThc) thcCount-- else cbdCount-- - 460 updateDisplay() - 461 - removeLastEntryFromTodayNote() // 🎯 - -Borrar último timestamp - 461 + removeLastEntryFromTodayNote() // - +Borrar último timestamp - 462 saveData() - 463 animateCounter(0.9f) - 464 - - -showFeedback(getString(R.string.cbd_subtracted), true) - 464 + val msg = if (isThc) - +getString(R.string.thc_subtracted) else - +getString(R.string.cbd_subtracted) - 465 + showFeedback(msg, true) - 466 dialog.dismiss() - 467 } - 468 - 469 dialogView.findViewById(R.id.chip_keep_note - ).setOnClickListener { - 470 - currentCount-- - 470 + if (isThc) thcCount-- else cbdCount-- - 471 updateDisplay() - 472 // NO borramos la nota, solo restamos el - contador - 473 saveData() - ... - 479 dialog.dismiss() - 480 } - 481 - 482 - // Mostrar con fondo transparente para que se vea - -bien el card - - (opcional, pero recomendado si el root es CardView) - 482 + // Mostrar con fondo transparente para que se vea - +bien el card - 483 dialog.window?.setBackgroundDrawableResource(andr - oid.R.color.transparent) - 484 dialog.show() - 485 } - -● Actualizo el botón reset para reiniciar ambos contadores: - -● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Added 3 lines, removed 2 lines - 491 resetButton.setOnClickListener { - 492 AlertDialog.Builder(this) - 493 .setTitle("Reiniciar contador") - 494 - .setMessage("¿Estás seguro de que quieres - -reiniciar el contador de hoy?") - 494 + .setMessage("¿Estás seguro de que quieres - +reiniciar el contador de hoy? (CBD y THC)") - 495 .setPositiveButton("Sí") { _, _ -> - 496 - currentCount = 0 - 496 + cbdCount = 0 - 497 + thcCount = 0 - 498 updateDisplay() - 499 saveData() - 500 showFeedback("¡Día reiniciado! 💪", true) - -● Fase 1 completada. Ahora añado los colores THC en colors.xml: - -● Read(app/src/main/res/values/colors.xml) - ⎿  Read 71 lines - -● Veo que ya hay colores THC pero son naranjas. Tú quieres THC en verde. Los - actualizo: - -● Update(app/src/main/res/values/colors.xml) - ⎿  Added 11 lines, removed 8 lines - 58 #667eea - 59 #6B7280 - 60 - 61 - - 62 - #ED8936 - 63 - #C05621 - 64 - #FBD38D - 65 - #F6E05E - 66 - #ED8936 - 67 - #F6AD55 - 68 - #80ED8936 - 61 + - 62 + #4CAF50 - 63 + #388E3C - 64 + #81C784 - 65 + #A5D6A7 - 66 + #E8F5E9 - 67 + #2E7D32 - 68 - 69 + - 70 + #E3F2FD - 71 + #1565C0 - 72 + - 73 - -● Read(app/src/main/res/values-night/themes.xml) - ⎿  Read 21 lines - -● Search(pattern: "app/src/main/res/layout/activity_main.xml") - ⎿  Found 1 file (ctrl+o to expand) - -● Read(app/src/main/res/layout/activity_main.xml) - ⎿  Read 358 lines - -● Voy a rediseñar el card del contador para mostrar ambos contadores lado a - lado: - -● Update(app/src/main/res/layout/activity_main.xml) - ⎿  Added 91 lines, removed 14 lines - 84 - 85 - 86 - 87 - - 87 + - 88 - 104 + android:gravity="center" - 105 + android:padding="12dp"> - 106 - 107 - - 108 + - 112 + android:orientation="horizontal" - 113 + android:gravity="center"> - 114 - 115 + - 116 + - 125 + - 126 + - 134 + - 135 + - 143 + - 144 + - 145 + - 146 + - 151 + - 152 + - 153 + - 162 + - 163 + - 171 + - 172 + - 180 + - 181 + - 182 + - 183 + - 184 - 190 + android:layout_marginTop="8dp" /> - 191 + - 192 + - 193 + - 199 - 200 - 201 - -● Ahora creo los drawables para los backgrounds de los contadores: - -● Write(app/src/main/res/drawable/bg_counter_cbd.xml) - ⎿  Wrote 7 lines to app/src/main/res/drawable/bg_counter_cbd.xml - - - - - - -● Write(app/src/main/res/drawable/bg_counter_thc.xml) - ⎿  Wrote 7 lines to app/src/main/res/drawable/bg_counter_thc.xml - - - - - - -● Ahora actualizo MainActivity.kt con las nuevas views y la lógica de display - dual: - -● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Added 5 lines, removed 1 line - 41 class MainActivity : AppCompatActivity() { - 42 - 43 // Views principales - 44 - private lateinit var counterText: TextView - 44 + private lateinit var counterText: TextView - + // Oculto, para compatibilidad - 45 + private lateinit var cbdCountText: TextView - 46 + private lateinit var thcCountText: TextView - 47 + private lateinit var cbdContainer: View - 48 + private lateinit var thcContainer: View - 49 private lateinit var dateText: TextView - 50 private lateinit var emojiText: TextView - 51 private lateinit var addButton: Button - -● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Added 5 lines, removed 1 line - 194 - 195 private fun initViews() { - 196 // Views principales - 197 - counterText = findViewById(R.id.counterText) - 197 + counterText = findViewById(R.id.counterText) - + // Oculto, para compatibilidad - 198 + cbdCountText = findViewById(R.id.cbdCountText) - 199 + thcCountText = findViewById(R.id.thcCountText) - 200 + cbdContainer = findViewById(R.id.cbdContainer) - 201 + thcContainer = findViewById(R.id.thcContainer) - 202 dateText = findViewById(R.id.dateText) - 203 emojiText = findViewById(R.id.emojiText) - 204 addButton = findViewById(R.id.addButton) - -● Ahora actualizo la función updateDisplay para mostrar ambos contadores: - -● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Added 53 lines, removed 26 lines - 398 } - 399 - 400 private fun updateDisplay(animate: Boolean = true) { - 401 - counterText.text = currentCount.toString() - 401 + // Actualizar contadores duales - 402 + cbdCountText.text = cbdCount.toString() - 403 + thcCountText.text = thcCount.toString() - 404 + counterText.text = currentCount.toString() // Total - +oculto para compatibilidad - 405 + - 406 dateText.text = getCurrentDateDisplay() - 407 val newEmoji = getEmoji(currentCount) - 408 - - 408 + - 409 if (animate && emojiText.text != newEmoji && - emojiText.text.isNotEmpty()) { - 410 - emojiText.animate().alpha(0f).setDuration(150).withEndAction - { - 411 emojiText.text = newEmoji - ... - 412 emojiText.text = newEmoji - 413 } - 414 - 415 - val substanceType = Prefs.getSubstanceType(this) - 416 - val isThc = substanceType == "THC" - 415 + // Destacar el modo activo - 416 + val isThc = Prefs.getSubstanceType(this) == "THC" - 417 + highlightActiveCounter(isThc) - 418 - 419 - val color = when { - 420 - currentCount == 0 -> R.color.green_safe - 421 - currentCount <= 3 -> if (isThc) - -R.color.thc_primary_light else R.color.yellow_warning // THC - -starts lighter? Or maybe just amber - 422 - currentCount <= 6 -> if (isThc) - -R.color.thc_primary else R.color.orange_danger // THC gets - -darker - 423 - else -> if (isThc) R.color.thc_primary_dark else - -R.color.red_critical - 424 - } - 425 - // Specific logic for THC mode colors could be - -refined - 426 - val finalColor = if (isThc) { - 427 - when { - 428 - currentCount == 0 -> R.color.green_safe - 429 - currentCount <= 3 -> - -R.color.thc_primary_light - 430 - currentCount <= 6 -> R.color.thc_primary - 431 - else -> R.color.thc_primary_dark - 432 - } - 419 + // Actualizar colores según cantidad - 420 + updateCounterColors() - 421 + } - 422 + - 423 + private fun highlightActiveCounter(isThcActive: Boolean) - +{ - 424 + // El contador activo se ve más grande/destacado - 425 + val activeScale = 1.1f - 426 + val inactiveScale = 0.9f - 427 + val activeAlpha = 1.0f - 428 + val inactiveAlpha = 0.6f - 429 + - 430 + if (isThcActive) { - 431 + thcContainer.scaleX = activeScale - 432 + thcContainer.scaleY = activeScale - 433 + thcContainer.alpha = activeAlpha - 434 + cbdContainer.scaleX = inactiveScale - 435 + cbdContainer.scaleY = inactiveScale - 436 + cbdContainer.alpha = inactiveAlpha - 437 } else { - 438 - when { - 439 - currentCount == 0 -> R.color.green_safe - 440 - currentCount <= 3 -> R.color.yellow_warning - 441 - currentCount <= 6 -> R.color.orange_danger - 442 - else -> R.color.red_critical - 443 - } - 438 + cbdContainer.scaleX = activeScale - 439 + cbdContainer.scaleY = activeScale - 440 + cbdContainer.alpha = activeAlpha - 441 + thcContainer.scaleX = inactiveScale - 442 + thcContainer.scaleY = inactiveScale - 443 + thcContainer.alpha = inactiveAlpha - 444 } - 445 - - 446 - counterText.setTextColor(ContextCompat.getColor(this, - - finalColor)) - 445 } - 446 - 447 + private fun updateCounterColors() { - 448 + // Colores CBD según cantidad - 449 + val cbdColor = when { - 450 + cbdCount == 0 -> R.color.green_safe - 451 + cbdCount <= 4 -> R.color.cbd_text - 452 + cbdCount <= 6 -> R.color.orange_danger - 453 + else -> R.color.red_critical - 454 + } - 455 + - +cbdCountText.setTextColor(ContextCompat.getColor(this, - +cbdColor)) - 456 + - 457 + // Colores THC según cantidad - 458 + val thcColor = when { - 459 + thcCount == 0 -> R.color.green_safe - 460 + thcCount <= 4 -> R.color.thc_text - 461 + thcCount <= 6 -> R.color.orange_danger - 462 + else -> R.color.red_critical - 463 + } - 464 + - +thcCountText.setTextColor(ContextCompat.getColor(this, - +thcColor)) - 465 + } - 466 + - 467 private fun getEmoji(count: Int): String = - EmojiUtils.emojiForCount(count, this) - 468 - 469 private fun setupClickListeners() { - -● Primero añado el string que falta para THC: - -● Read(app/src/main/res/values/strings.xml) - ⎿  Read 83 lines - -● Update(app/src/main/res/values/strings.xml) - ⎿  Added 4 lines - 60 - 61 Tipo de - Sustancia - 62 THC - 63 + THC agregado - 64 + THC restado - 65 + THC con weed - +agregado - 66 + THC con polen - +agregado - 67 ¿Qué estás - fumando? - 68 Etiqueta la toma con - vibra 420. - 69 - -● Ahora actualizo el layout del historial. Primero veo el actual: - -● Search(pattern: "app/src/main/res/layout/history_item.xml") - ⎿  Found 1 file (ctrl+o to expand) - -● Read(app/src/main/res/layout/history_item.xml) - ⎿  Read 112 lines - -● Update(app/src/main/res/layout/history_item.xml) - ⎿  Added 45 lines, removed 10 lines - 90 - app:layout_constraintEnd_toStartOf="@id/countBarrier" - 91 app:layout_constraintBaseline_toBaselineOf="@id/h - istoryDate"/> - 92 - 93 - - 93 + - 94 + - 103 + - 104 + - 105 + - 118 + - 119 + - 120 + - 132 + - 133 + - 134 + - 135 - 142 + app:layout_constraintTop_toTopOf="parent" /> - 143 - 144 - 145 - -● Necesito actualizar el barrier y crear los drawables para los chips: - -● Update(app/src/main/res/layout/history_item.xml) - ⎿  Added 1 line, removed 1 line - 20 android:layout_width="0dp" - 21 android:layout_height="0dp" - 22 app:barrierDirection="start" - 23 - app:constraint_referenced_ids="historyCount"/> - 23 + app:constraint_referenced_ids="countersContainer"/> - 24 - 25 - 26 - - - - - - -● Write(app/src/main/res/drawable/bg_chip_thc.xml) - ⎿  Wrote 10 lines to app/src/main/res/drawable/bg_chip_thc.xml - - - - - - - -● Ahora actualizo el adapter para manejar los chips duales: - -● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Added 3 lines, removed 1 line - 955 - 956 class ItemViewHolder(itemView: View) : - RecyclerView.ViewHolder(itemView) { - 957 val dateText: TextView = - itemView.findViewById(R.id.historyDate) - 958 - val countText: TextView = - -itemView.findViewById(R.id.historyCount) - 958 + val countText: TextView = - +itemView.findViewById(R.id.historyCount) // Oculto - 959 + val cbdChip: TextView = - +itemView.findViewById(R.id.cbdChip) - 960 + val thcChip: TextView = - +itemView.findViewById(R.id.thcChip) - 961 val emojiText: TextView = - itemView.findViewById(R.id.historyEmoji) - 962 val progressBar: View = - itemView.findViewById(R.id.progressBar) - 963 val noteBadge: TextView? = - itemView.findViewById(R.id.noteBadge) - -● Ahora actualizo onBindViewHolder para mostrar los chips duales: - -● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Added 58 lines, removed 22 lines - 984 val dayFormat = SimpleDateFormat("EEEE dd", - Locale("es", "ES")) - 985 holder.dateText.text = - dayFormat.format(item.dateObject) - 986 .replaceFirstChar { it.uppercase() } - 987 - holder.countText.text = "${item.count} CBD" - 987 - 988 + // Mostrar chips según los datos - 989 + if (item.cbdCount > 0) { - 990 + holder.cbdChip.text = "${item.cbdCount} - +CBD" - 991 + holder.cbdChip.visibility = View.VISIBLE - 992 + } else { - 993 + holder.cbdChip.visibility = View.GONE - 994 + } - 995 + - 996 + if (item.thcCount > 0) { - 997 + holder.thcChip.text = "${item.thcCount} - +THC" - 998 + holder.thcChip.visibility = View.VISIBLE - 999 + } else { - 1000 + holder.thcChip.visibility = View.GONE - 1001 + } - 1002 + - 1003 + // Si ambos son 0, mostrar chip CBD con 0 - 1004 + if (item.cbdCount == 0 && item.thcCount == - +0) { - 1005 + holder.cbdChip.text = "0 CBD" - 1006 + holder.cbdChip.visibility = View.VISIBLE - 1007 + } - 1008 + - 1009 + val total = item.totalCount - 1010 holder.emojiText.text = when { - 1011 - item.count == 0 -> "😌" - 1012 - item.count <= 2 -> "🙂" - 1013 - item.count <= 4 -> "😄" - 1014 - item.count <= 5 -> "🫠" - 1015 - item.count <= 6 -> "🤔" - 1016 - item.count <= 7 -> "🙄" - 1017 - item.count <= 8 -> "😶‍🌫️" - 1018 - item.count <= 9 -> "🫡" - 1019 - item.count <= 10 -> "🫥" - 1020 - item.count <= 11 -> "⛔️" - 1011 + total == 0 -> "😌" - 1012 + total <= 2 -> "🙂" - 1013 + total <= 4 -> "😄" - 1014 + total <= 5 -> "🫠" - 1015 + total <= 6 -> "🤔" - 1016 + total <= 7 -> "🙄" - 1017 + total <= 8 -> "😶‍🌫️" - 1018 + total <= 9 -> "🫡" - 1019 + total <= 10 -> "🫥" - 1020 + total <= 11 -> "⛔️" - 1021 else -> "💀" - 1022 } - 1023 - 1024 - // Barra de progreso (como ya tenías) - 1024 + // Barra de progreso basada en el total - 1025 val maxWidth = holder.itemView.width - 1026 - val progress = minOf(item.count / 10f, 1f) - 1026 + val progress = minOf(total / 10f, 1f) - 1027 val layoutParams = - holder.progressBar.layoutParams - 1028 layoutParams.width = (maxWidth * - progress).toInt() - 1029 holder.progressBar.layoutParams = - layoutParams - 1030 - val color = when { - 1031 - item.count == 0 -> R.color.green_safe - 1032 - item.count <= 3 -> - -R.color.yellow_warning - 1033 - item.count <= 6 -> R.color.orange_danger - 1034 - else -> R.color.red_critical - 1030 + - 1031 + // Color de la barra: del mayor, o verde si - +es 0 - 1032 + val barColor = when { - 1033 + total == 0 -> R.color.green_safe - 1034 + item.thcCount > item.cbdCount -> { - 1035 + // THC es mayor, usar escala verde - 1036 + when { - 1037 + total <= 4 -> - +R.color.thc_primary_light - 1038 + total <= 6 -> - +R.color.orange_danger - 1039 + else -> R.color.red_critical - 1040 + } - 1041 + } - 1042 + else -> { - 1043 + // CBD es mayor o igual, usar escala - + azul - 1044 + when { - 1045 + total <= 4 -> - +R.color.primary_light - 1046 + total <= 6 -> - +R.color.orange_danger - 1047 + else -> R.color.red_critical - 1048 + } - 1049 + } - 1050 } - 1051 holder.progressBar.setBackgroundColor( - 1052 - - -ContextCompat.getColor(holder.itemView.context, color) - 1052 + - +ContextCompat.getColor(holder.itemView.context, barColor) - 1053 ) - 1054 - 1055 - // --- NUEVO: badge - -de nota visible si existe nota para ese día - 1055 + // Badge - +de nota visible si existe nota para ese día - 1056 val ctx = holder.itemView.context - 1057 holder.noteBadge?.visibility = - 1058 if (Prefs.hasNote(ctx, item.date)) - View.VISIBLE else View.GONE - 1059 - 1060 - // --- NUEVO: badge - -de audio visible si existe audio para ese día - 1060 + // Badge - +de audio visible si existe audio para ese día - 1061 holder.audioBadge?.visibility = - 1062 if (Prefs.hasAudio(ctx, item.date)) - View.VISIBLE else View.GONE - 1063 - 1064 - // --- NUEVO: clicks para abrir el modal - 1064 + // Clicks para abrir el modal - 1065 holder.itemView.setOnClickListener { - onDayClick(item.date) } - 1066 holder.noteBadge?.setOnClickListener { - onDayClick(item.date) } - 1067 holder.audioBadge?.setOnClickListener { - onDayClick(item.date) } - -● Read(app/src/main/java/com/d4vram/cbdcounter/CBDWidgetProvider.kt) - ⎿  Read 275 lines - -● Ahora actualizo el widget para usar los contadores duales: - -● Update(app/src/main/java/com/d4vram/cbdcounter/CBDWidgetProvider.kt) - ⎿  Added 14 lines, removed 16 lines - 205 appWidgetManager.updateAppWidget(appWidgetId, views) - 206 } - 207 - 208 - private fun incrementCounter(context: Context) { - 208 + private fun incrementActiveCounter(context: Context) { - 209 val today = getCurrentDateKey() - 210 - val sharedPrefs = - -context.getSharedPreferences(PREFS_NAME, - -Context.MODE_PRIVATE) - 211 - val currentCount = - -sharedPrefs.getInt("$KEY_COUNT_PREFIX$today", 0) - 212 - - 213 - sharedPrefs.edit() - 214 - .putInt("$KEY_COUNT_PREFIX$today", currentCount + - - 1) - 215 - .apply() - 210 + Prefs.incrementActiveCount(context, today) - 211 } - 212 - 213 private fun addStandardCBD(context: Context) { - 214 - incrementCounter(context) - 215 - val entry = "🔹 ${getCurrentTimestamp()}" - 214 + incrementActiveCounter(context) - 215 + val isThc = Prefs.getSubstanceType(context) == "THC" - 216 + val entry = if (isThc) "🟢 ${getCurrentTimestamp()}" - +else "🔹 ${getCurrentTimestamp()}" - 217 appendNote(context, entry) - 218 } - 219 - 220 private fun resetCBD(context: Context) { - 221 val today = getCurrentDateKey() - 222 - val sharedPrefs = - -context.getSharedPreferences(PREFS_NAME, - -Context.MODE_PRIVATE) - 223 - - 224 - sharedPrefs.edit() - 225 - .putInt("$KEY_COUNT_PREFIX$today", 0) - 226 - .apply() - 222 + // Reset solo el contador del modo activo - 223 + val isThc = Prefs.getSubstanceType(context) == "THC" - 224 + if (isThc) { - 225 + Prefs.setThcCount(context, today, 0) - 226 + } else { - 227 + Prefs.setCbdCount(context, today, 0) - 228 + } - 229 } - 230 - 231 private fun getCurrentCount(context: Context): Int { - 232 val today = getCurrentDateKey() - 233 - val sharedPrefs = - -context.getSharedPreferences(PREFS_NAME, - -Context.MODE_PRIVATE) - 234 - return sharedPrefs.getInt("$KEY_COUNT_PREFIX$today", - -0) - 233 + // Devolver el contador del modo activo - 234 + return Prefs.getActiveCount(context, today) - 235 } - 236 - 237 private fun getCurrentDateKey(): String { - -● Update(app/src/main/java/com/d4vram/cbdcounter/CBDWidgetProvider.kt) - ⎿  Added 2 lines, removed 2 lines - 245 } - 246 - 247 private fun addWeed(context: Context) { - 248 - incrementCounter(context) - 248 + incrementActiveCounter(context) - 249 val entry = "🌿 ${getCurrentTimestamp()} (aliñado con - weed)" - 250 appendNote(context, entry) - 251 } - 252 - 253 private fun addPolem(context: Context) { - 254 - incrementCounter(context) - 254 + incrementActiveCounter(context) - 255 val entry = "🍫 ${getCurrentTimestamp()} (aliñado con - polen)" - 256 appendNote(context, entry) - 257 } - -● Actualizo el feedback de registerStandardIntake para el modo activo: - -● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Added 4 lines, removed 2 lines - 766 } - 767 - 768 private fun registerStandardIntake() { - 769 - val entry = "🔹 ${getCurrentTimestamp()}" - 770 - registerIntake(entry, getString(R.string.cbd_added)) - 769 + val isThc = Prefs.getSubstanceType(this) == "THC" - 770 + val entry = if (isThc) "🟢 ${getCurrentTimestamp()}" - +else "🔹 ${getCurrentTimestamp()}" - 771 + val feedback = if (isThc) - +getString(R.string.thc_added) else - +getString(R.string.cbd_added) - 772 + registerIntake(entry, feedback) - 773 } - 774 - 775 private fun showInfusionDialog() { - -● Intentemos compilar para verificar que no hay errores: - -● Bash(./gradlew assembleDebug 2>&1 | tail -50) timeout: 3m 0s - ⎿  > Task :app:stripDebugDebugSymbols NO-SOURCE - > Task :app:validateSigningDebug UP-TO-DATE - > Task :app:writeDebugAppMetadata UP-TO-DATE - … +72 lines (ctrl+o to expand) - -● Hay algunos errores de compilación. Los arreglo: - -● Read(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Read 30 lines - -● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Added 3 lines, removed 3 lines - 346 streakText.text = "Racha: 0 días" - 347 return - 348 } - 349 - val average = displayedHistoryData.map { it.count - - }.average() - 349 + val average = displayedHistoryData.map { it.totalCount - + }.average() - 350 avgText.text = "Promedio: %.1f".format(average) - 351 - val total = displayedHistoryData.sumOf { it.count } - 351 + val total = displayedHistoryData.sumOf { it.totalCount - + } - 352 totalText.text = "Total: $total" - 353 val streak = calculateCleanStreak() - 354 streakText.text = "Racha limpia: $streak días" - ... - 358 var streak = 0 - 359 val sortedData = allHistoryData.sortedByDescending { - it.dateObject } - 360 for (item in sortedData) { - 361 - if (item.count == 0) streak++ else break - 361 + if (item.totalCount == 0) streak++ else break - 362 } - 363 return streak - 364 } - -● Ahora añado los colores que faltan: - -● Update(app/src/main/res/values/colors.xml) - ⎿  Added 2 lines - 65 #A5D6A7 - 66 #E8F5E9 - 67 #2E7D32 - 68 + #4CAF50 - 69 + #81C784 - 70 - 71 - 72 #E3F2FD - -● Compilo de nuevo: - -● Bash(./gradlew assembleDebug 2>&1 | tail -30) timeout: 3m 0s - ⎿  > Task :app:stripDebugDebugSymbols NO-SOURCE - > Task :app:validateSigningDebug UP-TO-DATE - > Task :app:writeDebugAppMetadata UP-TO-DATE - … +51 lines (ctrl+o to expand) - -● ¡Build exitoso! Ahora creo el archivo PBL completo para Plaud Note: - -● Write(docs/pbl/2026-01-19-dual-counters-session.md) - ⎿  Wrote 295 lines to docs/pbl/2026-01-19-dual-counters-session.md - # Sesión PBL: Contadores Duales CBD/THC - **Fecha:** 2026-01-19 - **Proyecto:** CBDcounter v1.4 - **Duración estimada de lectura para Plaud Note:** 8-10 minutos - --- - ## Resumen Ejecutivo (30 segundos) - … +285 lines (ctrl+o to expand) - -● --- - Resumen de la Sesión - - Build: ✅ SUCCESS - - Lo que implementamos: - - 1. Datos: Contadores separados count_cbd_* y count_thc_* con migración - automática - 2. UI Principal: Card dual con CBD (azul) | THC (verde), modo activo destacado - 3. Historial: Chips apilados que solo aparecen si hay datos - 4. Widget: Usa el contador del modo activo - 5. CSV: Compatible con formato nuevo y legacy - - Archivos clave modificados: - - - Prefs.kt - Nueva estructura de datos - - MainActivity.kt - Lógica dual completa - - activity_main.xml - Layout contador dual - - history_item.xml - Chips apilados - - CBDWidgetProvider.kt - Contador activo - - Documentación PBL creada: - - 📄 docs/pbl/2026-01-19-dual-counters-session.md - - Contiene: - - 6 conceptos explicados con analogías - - Glosario de términos - - Flujo de datos - - Preguntas de repaso - - Código de ejemplo - - --- - Para probar: Instala el APK (app/build/outputs/apk/debug/app-debug.apk), - cambia entre modo CBD/THC en Ajustes, y verifica que los contadores se - incrementan correctamente. - - ¿Quieres que hagamos algún ajuste o pasamos a otra cosa? - -✻ Churned for 10m 39s - -❯ Quiero que el emoji no salga algo cortado por debajo en el circulo, - solventa dandole unos px mas y dime como lo replicaría yo - ⎿  [Image #3] - ⎿  [Image #4] - -● Veo el problema - el emoji se corta por abajo del card. Aumento la altura del - card para que quepa bien. - -● Update(app/src/main/res/layout/activity_main.xml) - ⎿  Added 1 line, removed 1 line - 88 - android:layout_height="140dp" - - - android:layout_height="155dp" - - --- - Cómo replicarlo tú mismo: - - 1. Abre app/src/main/res/layout/activity_main.xml - 2. Busca el CardView con id="@+id/counterCard" (línea ~88) - 3. Cambia android:layout_height de 140dp a 155dp (o el valor que necesites) - 4. Guarda y rebuild - - Regla general: Si un elemento se corta visualmente, aumenta el height del - contenedor padre o reduce el padding/margin interno. En este caso, 15dp extra - fueron suficientes para dar espacio al emoji. - -❯ Vamos a quitarle el problema a la primera foto la screen de Configuración - que no solape arriba con la barra de tareasy que quede bien como las - otras dos. Es la pantalla que sale al entrar en Stats > Estadísticas > - Engranaje SVG esquina superior derecha > CONFIGURACIÓN - ⎿  [Image #5] - ⎿  [Image #6] - ⎿  [Image #7] - -● Veo el problema - "Configuración" se solapa con la barra de estado. Voy a - buscar qué Activity es y arreglarlo. - -● Search(pattern: "Configuración|settingsButton|engranaje", path: - "app/src/main/java/com/d4vram/cbdcounter") - ⎿  Found 1 file (ctrl+o to expand) - -● Read(app/src/main/java/com/d4vram/cbdcounter/DashboardActivity.kt) - ⎿  Read 251 lines - -● Read(app/src/main/java/com/d4vram/cbdcounter/EmojiSettingsActivity.kt) - ⎿  Read 315 lines - -● Read(app/src/main/res/layout/activity_emoji_settings.xml) - ⎿  Read 196 lines - -● El problema es que el toolbar no tiene espacio para la barra de estado. Hago - dos cambios: - -● Update(app/src/main/java/com/d4vram/cbdcounter/EmojiSettingsActivity.kt) - ⎿  Added 3 lines - 58 super.onCreate(savedInstanceState) - 59 setContentView(R.layout.activity_emoji_settings) - 60 - 61 + // Configurar color de la barra de estado - 62 + window.statusBarColor = ContextCompat.getColor(this, - +R.color.gradient_start) - 63 + - 64 // Configurar toolbar - 65 val toolbar = - findViewById(R.id.settingsToolbar) - 66 toolbar.setNavigationOnClickListener { finish() } - -● Ahora añado padding al toolbar para la barra de estado: - -● Update(app/src/main/res/layout/activity_emoji_settings.xml) - ⎿  Added 3 lines, removed 1 line - 9 &1 | tail -15) - ⎿ w: file:///home/defcon/AndroidStudioProjects/CBDcounter2/app/src/main/j - ava/com/d4vram/cbdcounter/EvolutionActivity.kt:50:24 'fun setOnCheckedC - hangeListener(p0: ChipGroup.OnCheckedChangeListener?): Unit' is depreca - … +19 lines (ctrl+o to expand) - - - - -● ✅ Build exitoso. - - --- - Cómo replicarlo tú mismo: - - 1. En el Activity (EmojiSettingsActivity.kt línea ~60): - // Añadir en onCreate(), después de setContentView() - window.statusBarColor = ContextCompat.getColor(this, R.color.gradient_start) - - 2. En el Layout (activity_emoji_settings.xml línea ~9-19): - - android:paddingTop="24dp" - android:minHeight="?attr/actionBarSize" - ... /> - - --- - Regla general: - - Cuando el header se solapa con la barra de estado: - 1. Código: window.statusBarColor = color para que el color coincida - 2. Layout: paddingTop en el toolbar (24dp es un valor común) + - layout_height="wrap_content" + minHeight="?attr/actionBarSize" - -✻ Cogitated for 1m 28s - -❯ mejor, pero no del todo, debe quedar como Ajustes en segunda foto - ⎿  [Image #8] - ⎿  [Image #9] - -● Veo, necesita más padding. Déjame ver cómo está configurado "Ajustes" - (SettingsActivity): - -● Read(app/src/main/res/layout/activity_settings.xml) - ⎿  Read 40 lines - -● Ahora veo la diferencia. "Ajustes" usa AppBarLayout con - fitsSystemWindows="true". Actualizo "Configuración" con la misma estructura: - -● Update(app/src/main/res/layout/activity_emoji_settings.xml) - ⎿  Added 19 lines, removed 13 lines - 1 - 2 - - 6 + android:background="@color/background" - 7 + android:fitsSystemWindows="true"> - 8 - 9 - - 13 + android:fitsSystemWindows="true"> - 14 - 15 + - 24 + - 25 + - 26 - 29 + android:fillViewport="true" - 30 + - +app:layout_behavior="@string/appbar_scrolling_view_behavior"> - 31 - 32 - 201 - 202 - 203 - - 203 + - -● Bash(./gradlew assembleDebug 2>&1 | tail -10) - ⎿  Interrupted · What should Claude do instead? - -❯ Ya funciona, y está genial , ¿Como hemos solventado? Dame el paso a paso - como profesor de programador y todos los tecnicismos o conceptos - definelos al final de la explicacion en GLOSARIO para Plaud Note, y - guarda el archivo como Fix_completo_toolbar_CBDcounter_edge-to-edge.md - ⎿  [Image #10] - -● Write(docs/pbl/Fix_completo_toolbar_CBDcounter_edge-to-edge.md) - ⎿  Wrote 261 lines to - docs/pbl/Fix_completo_toolbar_CBDcounter_edge-to-edge.md - # Fix Completo: Toolbar Solapado con Barra de Estado (Edge-to-Edge) - **Fecha:** 2026-01-19 - **Proyecto:** CBDcounter v1.4 - **Problema:** El header "Configuración" se solapaba con la barra de - estado del sistema - **Solución:** Implementar correctamente el patrón Edge-to-Edge de - Material Design - --- - ## El Problema - … +251 lines (ctrl+o to expand) - -● Archivo guardado en docs/pbl/Fix_completo_toolbar_CBDcounter_edge-to-edge.md - - --- - Resumen rápido del fix: - - El problema: El toolbar se dibujaba desde el borde superior, solapándose con - la barra de estado. - - La solución en 4 pasos: - - 1. Layout raíz → Cambiar LinearLayout por CoordinatorLayout + - fitsSystemWindows="true" - 2. Toolbar → Envolverlo en AppBarLayout + fitsSystemWindows="true" - 3. Contenido → Añadir - app:layout_behavior="@string/appbar_scrolling_view_behavior" al ScrollView - 4. Código → window.statusBarColor = color para que el color coincida - - Concepto clave: fitsSystemWindows="true" le dice al sistema "yo me encargo de - no solaparme con tus barras" y añade padding automáticamente. - -✻ Cooked for 1m 8s - -❯ Último detalle: al clickar en +1 superior el de "¿Con que está aliñado el - CBD?", aunque esté en modo CBD, añadir +1 a THC, sea polen o weed y con - la nota correspondiente - -● Entendido. El botón de infusión (+1 con weed/polen) siempre suma a THC, - independientemente del modo. Lo modifico: - -● Read(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Read 50 lines - -● Update(app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt) - ⎿  Added 12 lines, removed 1 line - 817 val label = getString(type.labelRes) - 818 val suffix = getString(R.string.infusion_note_suffix, - label) - 819 val entry = "${type.icon} - ${getCurrentTimestamp()}$suffix" - 820 - registerIntake(entry, getString(type.feedbackRes)) - 820 + // Infusión (weed/polen) SIEMPRE suma a THC - 821 + registerThcIntake(entry, getString(type.feedbackRes)) - 822 } - 823 - 824 + /** Registra una toma que siempre va al contador THC - +(para infusiones) */ - 825 + private fun registerThcIntake(entry: String, - +feedbackMessage: String) { - 826 + thcCount++ - 827 + updateDisplay() - 828 + appendEntryToTodayNote(entry) - 829 + saveData() - 830 + animateCounter(1.1f) - 831 + showFeedback("$feedbackMessage (THC)", false) - 832 + } - 833 + - 834 private fun registerIntake(entry: String, - feedbackMessage: String) { - 835 val isThc = Prefs.getSubstanceType(this) == "THC" - 836 if (isThc) { - -● Ahora actualizo el Widget para que weed/polen también sumen a THC: - -● Update(app/src/main/java/com/d4vram/cbdcounter/CBDWidgetProvider.kt) - ⎿  Added 10 lines, removed 2 lines - 245 } - 246 - 247 private fun addWeed(context: Context) { - 248 - incrementActiveCounter(context) - 248 + // Weed SIEMPRE suma a THC - 249 + incrementThcCounter(context) - 250 val entry = "🌿 ${getCurrentTimestamp()} (aliñado con - weed)" - 251 appendNote(context, entry) - 252 } - 253 - 254 private fun addPolem(context: Context) { - 255 - incrementActiveCounter(context) - 255 + // Polen SIEMPRE suma a THC - 256 + incrementThcCounter(context) - 257 val entry = "🍫 ${getCurrentTimestamp()} (aliñado con - polen)" - 258 appendNote(context, entry) - 259 } - 260 - 261 + private fun incrementThcCounter(context: Context) { - 262 + val today = getCurrentDateKey() - 263 + val currentThc = Prefs.getThcCount(context, today) - 264 + Prefs.setThcCount(context, today, currentThc + 1) - 265 + } - 266 + - 267 private fun appendNote(context: Context, entry: String) { - 268 val today = getCurrentDateKey() - 269 val currentNote = Prefs.getNote(context, today) - -● Compilo para verificar: - -● Bash(./gradlew assembleDebug 2>&1 | tail -10) - ⎿  > Task :app:processDebugJavaRes UP-TO-DATE - > Task :app:mergeDebugJavaResource UP-TO-DATE - > Task :app:dexBuilderDebug - … +7 lines (ctrl+o to expand) - diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm index f603f8430d2675ee72f9071d2e23cc05b8bf13d5..0475c27051766d3d561d2c222316e175794c18fe 100644 GIT binary patch delta 2493 zcmV;u2}1V28NC?|P)h>@6aWYa2mtyEG?5J+1DpzDkuEh8F$n+wZ8`t|0RZoKaX@t! z004Bcb|?XVe{5A}8ONVn$sUl_RFDhhOBO@x<7F0tP)M;;<%t;WKkG13Q-f7nF}$DAJYhELw!H({ocO4 z+;-znmvfSr=Xsy+_xbTY=e_s5oLjRyKW0{K$(3|};LseykqsN{$9D%^n2ycqord(U)_|F-Z%HPdiMMvdJ~)cLrOK`0?V18wVfNeC zvRC+j++cg9KQ+t8uD+`74)<^0C4A08*OnXVKOEA&sAl{8olzV-+P$tJn+melX*DUU zux#5~BXhagidQ?1%^GfLu4#AOj(nWWtyz){9+r>^VExJ72QuH8*H40f^%p<=rpn$2G}=`nZPxnft`&)}pij@37QB9Pj14##x4kR5&I*@cA=|Iwf zG9CEGc(b~6ed9b2>*9Ud7I!Aqfp$t`Jfw!UemO6}cBF@2h2fYPK3_8B;>^c?TQSAJ zR(jp{Tu)Wc!mO=s3mmP&^7p!fa%h>S28pS4-G|Rr$7}v{-Ja*C=6HQ(Yg12N6mJSZ z9HcX0hXt$a(}P58qhI+u9X9W$1%f+hMpw=FyuYH1-B#jVo6e=$qWZKu06$OyjWLa~ z?nPzSHOsaImbPc@hYzj@8}coG_4|U^8A~;Nqu0H=bVusL?<)<_8_q~K&K|Zdd;7|c zakc5xAEtzLuck9C!^p+j?Pjaunr#kfH`k5orK#@tTR|qwI7{xF`>()7S7>=>Te|nM zi;xk9w#E_Du_jTFt?wIHJpIMWXyIhD1p*@{aYgMx)r&ndZTGcA9+#AzV2PRn7qD6OX zUb?F#dboMJor&uf+#a3o8DE>J-Tb?!?>bviwc+ge+Tf9}rYoGDzxwQ!!yozN>Ap|Y zx+90XF5B9B|MIqf&%EbE*!o8QnStML&Qz}qE9P8t{O;VFN6+kko|buOb6h!V=QE98 z{dF9aI~Way8TrqRN4|d7-omNHA^j?NBrBOE9Y{KmbRg+K(t-DG2cDQT^q}Wq&m++v z*rvL{Z;iZ>0pl;Yd=7t9#(NfQymG=PTaehm-Rmi9F4({o104+BLN|C0>va>7F~<#X z9_`feB{y8jw$%-P@WEdV9f4Tz#08TLU)iY#9ZVb~>*E!UFxDa+jK7fMEfqEfKFB&Z zSObzDhJ}-Jz8l2RdhwAi9(5=eKB}4X7#z)|Yc1FrnsY1^N0_ASx`)(xMS%|{oLF%1 z!=~8V3O4Ll7BG%y#9>gs?z3WPonXYg+yP+zQ2ZIrPHfG8#ZmnqFZgi})OYcb@To%> zan`#*PZSg07B}Eh5BFLgo=RZ=><`jTw*AP*2X<#?f5CLbR^^90!CidCyX3gw`|lz z{?EFBjXA!53~_v^yx_zG^L6SYhmYZ-81fefUwp)&7J7m|*c%0jKkJu29O?Mu$G%h^ zeIAJeuHJ)V(>jTRt{TY6fal+fY{BODPc_5+o*UGM&L4#+zMmJ2*yr7lj?`G`6k<}l z@=zDJpG*8wNe^U0myhh4`|+^pb4P9D#l~|)JjFtPsyA>{BQ|lAU%ukd2j2%A%MaXgFoN(=nQeOGbe^$yK-Ld?CcGmUGjlX4G+2x=fAEV zSx;*L)4K818f8O{rYnaylKRfm99fPln`-p=(#6qSax|Y(JN^v%DPN?Xv#OPzVfXW? z$HY{BTuHdn<+HJ1!>;`aZ>}5KCvd?T>=(u>j)W8kOnXAH<%5)s*c!r1G4ZJf9lkH6 zFDQwnxJaE@=!b8YF1uoBU#Wla^gKYj;2YBh; zP#!R?4I6VGlkV55nEt)QPxn(XkiMVt2v=->)r`H=N1Reg4`uUnVADAVU7rDQk$!$( z_v6S;-}u~U?)z&!@`EE?`IzfF4V!Sd_%r8Of|O0ZNI3c~Wlt%mIR5j|?>{+}3yv`P zNorl*6EzTqPeIKeqDue8p0|tXnbg$A=hy z;LNpWwI11tqw}F(n_q)+`T13!Z!7g5Z7yHspe8?#YL!2H25Y*mz||ZMeD%$bBY$l8 zGSn}>2H&Tgj-O&lVuPzWIx%%l23HPYo zyEwj0pF7D#4ixT7^-M7&sgHb`D-OOZ-SA_KrvDEB#6Vb3O9u$+PE<9a3IG6{3IG65 zO9KQH00;;O0Qw6wv+4>~0S)>KG+gUWR5hUr005kmO$;pq`U^CZeGD4`+mo3LAO@6aWYa2mtbmGm#A*1D*CMw!o0-^<~azx7~sWF@PO&5#uy9#1}@k zI7fg?GR|!a0SBR5rXxa@GW~MahV%2Gn6?s%Mi$Pk&cG$`P`oG zy$`o9mOow2O@8d1o{sc6NKR@FHwribXqxE z;RiBxKI}CXaisIwyS3&V8Z}< zrRU|9T?4-v^3DyNe*NsnttPB8p_eiF<~P4(w>hQPesdHhySpYkX17*CeS(zAuFAZB zxar+C^VU@b;*T@AQ+eZ+P%!zkbi1qe(JfNqu*JiT~)x8^2#+7XR^@k(13;-wqaMI%o9!Jh}SJ z)fe}8r}k8>N_YQv%@6)Mtua$4J0(g>6E8@)_W>l-ip zVwr0aHLznDJhZ#zRbza!GQ4+xQCp&}tF`tWGd*Rz8B03TW|c9ywPVgNJG<-r`su~Q zVaw1nc|Ymr{#lc#e%w#B7Cqw#H`#-{Y~`KSbDOF4dn;PJh1oxtRD$!z#$k7r3Qm|V zE4G{}9P-LImS^nW15O2VDdvu0Q=CBQw0epGF)W@yJb`!u@dQ2v6BwLTZ1NS;^Y*d&@Z@8g zzjtu1XWH$55A9~^;@S0^ix4Yh+d2xTkB7f*5@U92E^snAYYunc|L%1+c*p-zfW1;> z-Z@e_)ZCFYb>}#Y(>5c293I&)QNFHqcQ+1Rx#9hp>jyq~>-pDcc5k#33M)*0!~03Q ztL&fka{IpfTZ7+SyENRGt!}s}IDf1%ncCX^=smx@;Pt$8p>k`uII*Q8aYfgUpE~fb z=h`-&XiXNjgyHsu$M2Y(xv=Zj^@({;UOYeYUi-F+&vxg2*tqV0^Nk;#J%6$`b+SF* zo&NJZyAC{*w*#BXE4=3PqFYXtkQc{n-AcY#iYE|HAf7-xfp`M{mlJr#o>1^dCdA{@ zC2S3rcocK;fRA5rxfsqH9*Ar_M~Mj{4i5h`OG_dfe&s;M2hN^&7|tgANQXmhk{e|7|FuXhiVa{o;gVYBVDrAf*q1>e`{96%O|c_T|@eu6XD%H$OVQt z?8?0@vSGh2@^RKk9*F*RomETg#7E9q)}dPB%73->lUurf{OX^>I&546&0TmnKKhW4 zJgY4+6XgWA$r4!l;aY2E=x)FGxEH}HKQgq+zOg2Hkqu6LxZ49dd3Ddtwle*Y6Psjo zh#ZnNQ*vpIfRa`Z}kQbeJ?jhDlUUI00*2Xnt?Zm0BWH|J6>uckALK_mCyYaM4ODkdxk3 zhraN;wS+I{%s@6|#mFw*&4*2&J9?uoHl8EGDHmLSvjL+Xu?eI4iWP=AxH0(gx$}_> zj_+u4Vk18`Vq6(NIn)ze^|;2w5YKl#G9)Z^baJ@8t0q4Y=HB4hr5Nz^u+@GzKf3wktSv73S*$+>KKm`ITFLJ!3DA5vH8$U=6!l16}(ZWPJwQc(=YQ zyLn`1ZhUT}lSi`Fqd4r6RS#X?Y1rfkOFTNy61Z%Ng@e&|Dfg6W3gbQ>-T6~fwZO=y zIB~5-SnSyNo<^o7vFbr_aP^{C?~yRz_bJy)p*#?7sIE?&9RFY8tg;)x-D z2Yz(jvs#brrrL z7Sg=j9^9DmWa5-d92;2a$mG;M8NX`CH(qXR$~BRHJiF?;HQYSR!_`?gSmLm2=DH8? zL5eA#CvhVBQM~YOAL>ngySlEeeB*Im;gys7dc3%DoO0@6aWYa2mtyEG?5J+e_9Hq^>A`;05CBy00GQA002n}004La z?V5j#R96+p@0*$3on4kPY=JJYz<|q-wt!Qt1!J|tj~2vQ5lO@}kVSr28ZjVf!J>J~ zE-n3yrZuFib+!EitJYPbrZ$!V4JD0*Qlk(xftpH)p+Kt<&<*SP%)IaJ-C=i2{mMRPbbF`6fhQ^!Ul zE7@%~!rov+0Adjb0*g&L=F(FSGB#o0f2{8+BiUGsWNagihDSQ|_`vIoSOdJ$2pvSu z>x_t__2MI0IOCj&v z*@hV>4n+Mr&dQ~AVk72gGboq1;*T*qu{D=n^?$@{=r{)IyYO&q)FB&jRvOV0f5im1 z)d(#0aIDoc=6=1{*o$Bl9~oL|Z>))0q=S1E*q6$q&m(cLtM}N^ zX`RGDRt@BY!1>1_8|nQ1sb;X>Gon7^QAUdI=fy_si$;>+qQxcK12R1;kJIL7$k&$%8M5*9skV)(Ty=Z8(t-r(9LAMn(0pM5z0IlXs1iz1uWjjz@y z9lSqTIfN0{IZtzVUs&l>qmP#?jOOC~`IPJNhv=t#;kwSMR(gis&!-*}Q*p(?N|w*s zNQYkg6WnAY?Gv!rA@&ROf3kt#!eG;$P;B|Yr6aaT_Hvu>)PoG(=aQ$E#Zp|j?pf%E zua_*nVrgHgf8caIK-NB!zh5)=TJw#3opA4#QxEi#5x#ys%*lbhd~YZZHmwaEbKfS} zuT?SqV~L-Rr((c;Kjo2Ku~jqravx#JIX#rl&w)<&Imr492n+Y~fBUi@M|%3k=SFki zU+a+{7|F`VT<0`&vV+B+IoA@nbn=CR(Ycg8rJTa}*GIqqk<|{I?mI` zhpEwfByZ=<*OXj3e{68prToI67sl7=b0LZ`#ii7WBBR@ue@+*m?;q*#cyjqJ_ZJMOLuIjd=OoDjfkua*Z_hB~s zY@}19q;)Up>P^clRm3KWx{9Jt%4MQhN$c@7CRdoXvWP8Z9Aa{*iiB};^}MJeQLF)J zm{CPh1EPc+e{E8gMfK|~Rgy01N11mCIjJa>WYDG9L>t(w_I-TijX3EWs$7c^V6YCFpcNuSg!bfvoK0V^3R<{?oTX#aUq2UHtVbWx&Sz{CuVLf1;CC8hlH~OTDBILX{$qiy|@` zYUm0>{IVi&UuKKNBksVv=s^36&Sk^!^?*}YG;(}Z$R|@-DHg2=>WgyTWpCqr7gl%M zN*Fe4?~ylLzPIYKS$^`9)g4n3iLhcs#>%i~x=EKdr+r)pTsCvt=H|8T*y`2pM|TG; zs7_35f9LUAud%4O>#e@onRF)H-W>L__C48ZC-m@buJ{bQTKx94?4;79^d9Ww)b5Q! zX!TXLHrsm#SKcceDQnBk^&d#lzI@Hj>vreE;NjL44cSDHb=zDGAMI?@o0mmf_-gZs z@jWfgHSVq3RS2`WWwW!vBd(ca(BH^cx_rg%e~?R2RoEP^&o<`MQ$`j~iO#j~G@^Xu zi(ibhjW-uNS#Ir_pL_><57I6Q+k*c*_ViEhbd9qESNQ(HZ4WHZ4QpuqO(HY?6Yo2i zN^ZA@htHmGbYYbXgPbcg{NamyZ|xqt-9hC1uA2O1cWd3CcM{V1rMVw&{o6$MCw`l9 z?!0U2Jl5?L9&v@>DZ8J_O0~1AOSf54b@PYs2wdl1&wP7-%`Y16U2@SLMu-21jO(4d zaON+3vEH=uTa#~mDD}+dRCQ*^lznq5e^Yy(FSyw|UpoE3S8i!qG-7Wd)3KmEx1c>Z z{iVbAEZdVB(l)OYB=@QGSLdhJbR9Wxhr9LAxUS<3RU5Lm<`&hpJXg8$%$Q4U!Ru{R zOEcU4z4jYN2i51g%NkHP*RpO$C697TqtGQO=5Yt&4#XXZI}mpu?m*muxC4E4f8g)- zkBYi<{n#m0(RO=wX}8oz`R7Ql(ViNU9y_5&HTUi7 zo2zTniQf-T)xDa|Ea?|IYM-tz%w`5;^0(KTz-3l;ep_Sd`^ zv$H>%Bwgq6H!pQVOb8clf8*b+uJw7>^Fy)^xDq*ZtrdG-{><`3`;Uv4?)5|5fyek$ zduh}Ub`2*!WODb-)Wxo|GM4kgsmF7L_n$d*?dhgJ|9t)vJh+?1|6dvQ#PsZ;!7l6a zGuLdHvu8p6!TCGgTVdVQRrzymgK9IiTYmS{UFVaPtIrRr4IWC>f3&1V6{erxdh~-I zKiBcGTK=u?+|}FL@0r*1uW{>6rxv}@`Bv8}TQXIPQ^|=poVq)A^7vbOM`m8!5>|}g z{oS!&`*j%fbz@Wv)BEj~WB1>+Ke}r%OQ#C%jf!V+2jULI9f&&+ci_F-f&T(%WK=0o zO9u#4%4&vR3IG6F5DEYQP)h>@6aWYa2mtyEG_#=!RsjwA3p89*%4&vR3IG6FlLQPc uANmV4T<$*zyc_`l02cuO01W^D00000000000001)lTZvG2Jj020002)Hm`jE delta 2765 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 TISd;Cl#^8qAO`CT00000>cEgY diff --git a/app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt b/app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt index baaa7cd..ddad134 100644 --- a/app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt +++ b/app/src/main/java/com/d4vram/cbdcounter/MainActivity.kt @@ -731,20 +731,28 @@ class MainActivity : AppCompatActivity() { private fun escapeCsvField(value: String): String { if (value.isEmpty()) return "" - val builder = StringBuilder() - value.forEach { char -> - when (char) { - '\\' -> builder.append("\\\\") - '\n' -> builder.append("\\n") - ',' -> builder.append("\\,") - else -> builder.append(char) - } + + // RFC 4180: Si el campo contiene comas, saltos de línea o comillas, + // debe ir entre comillas dobles. Las comillas dobles se duplican. + val needsQuotes = value.contains(',') || value.contains('\n') || value.contains('"') || value.contains('\r') + + return if (needsQuotes) { + // Duplicar comillas dobles y envolver todo en comillas + "\"${value.replace("\"", "\"\"")}\"" + } else { + value } - return builder.toString() } private fun unescapeCsvField(value: String): String { if (value.isEmpty()) return "" + + // RFC 4180: Si el campo está entre comillas, removerlas y desduplicar comillas internas + if (value.startsWith("\"") && value.endsWith("\"") && value.length >= 2) { + return value.substring(1, value.length - 1).replace("\"\"", "\"") + } + + // Backward compatibility: Manejar formato antiguo con backslash val builder = StringBuilder() var escape = false value.forEach { char -> diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 84b7ab4..0000000 --- a/docs/index.md +++ /dev/null @@ -1,86 +0,0 @@ -# CBDCounter - Documentación Oficial - -
- -🌿 **Tracking Personal de CBD con Total Privacidad** - -[![Versión](https://img.shields.io/badge/versión-1.1-green.svg)](https://github.com/tu-usuario/CBDcounter2/releases) -[![Android](https://img.shields.io/badge/Android-7.0%2B-blue.svg)](https://developer.android.com) -[![Licencia](https://img.shields.io/badge/licencia-GPL--3.0-orange.svg)](LICENSE) - -
- ---- - -## 📱 Acerca de CBDCounter - -CBDCounter es una aplicación Android intuitiva y privada diseñada para el seguimiento personal de consumo de CBD. Con un enfoque en la privacidad absoluta, todos tus datos permanecen en tu dispositivo. - -**Características principales:** -- 📊 Contador diario con estadísticas detalladas -- 🏠 Widget de pantalla principal -- 📅 Calendario visual con emojis personalizables -- 📝 Sistema de notas con timestamps -- 💾 Exportación/Importación CSV -- 🔒 100% privado - cero recopilación de datos -- 🌙 Tema oscuro/claro - ---- - -## 📋 Documentación - -### Información Legal - -- **[Política de Privacidad](privacy-policy.md)** - Cómo manejamos (o mejor dicho, NO manejamos) tus datos - -### Soporte - -- **Email de contacto:** d4vram369@gmail.com -- **Tiempo de respuesta:** ~7 días hábiles - ---- - -## 🔒 Privacidad - -**Tu privacidad es nuestra prioridad #1:** - -✅ CERO recopilación de datos -✅ TODO se guarda localmente -✅ NO hay servidores externos -✅ NO usamos analytics -✅ NO compartimos nada con terceros - ---- - -## ⚠️ Descargo de Responsabilidad - -CBDCounter es una herramienta de tracking personal y **NO** constituye: -- Dispositivo médico -- Consejo médico profesional -- Promoción de consumo de sustancias -- Facilitación de compra/venta - -Consulta siempre con un profesional de la salud. - ---- - -## 🆓 Licencia - -Este proyecto está licenciado bajo GPL-3.0. Ver el archivo [LICENSE](../LICENSE) para más detalles. - ---- - -## 👨‍💻 Desarrollador - -**D4vRAM** -🇮🇨 Gran Canaria, España - ---- - -
- -© 2025 D4vRAM. Todos los derechos reservados. - -Desarrollado con ❤️ desde Islas Canarias 🇮🇨 (España🇪 🇪🇸) - -
diff --git a/docs/privacy-policy.md b/docs/privacy-policy.md deleted file mode 100644 index 383e701..0000000 --- a/docs/privacy-policy.md +++ /dev/null @@ -1,229 +0,0 @@ -# Política de Privacidad de CBDCounter - -**Última actualización:** 10 de noviembre de 2025 - -**Desarrollador:** D4vRAM -**Contacto:** d4vram369@gmail.com - ---- - -## 1. Introducción - -CBDCounter ("la Aplicación") es una herramienta de seguimiento personal desarrollada por D4vRAM. Esta Política de Privacidad describe cómo manejamos la información en relación con el uso de la Aplicación. - -**En resumen:** Esta aplicación NO recopila, transmite, almacena en servidores ni comparte ningún dato personal. - ---- - -## 2. Información que Recopilamos - -**CBDCounter NO recopila ningún dato personal de los usuarios.** - -La Aplicación no: -- ❌ Recopila nombres, correos electrónicos o información de contacto -- ❌ Recopila datos de ubicación -- ❌ Accede a tu lista de contactos, fotos o archivos -- ❌ Rastrea tu actividad fuera de la app -- ❌ Utiliza cookies o tecnologías de seguimiento -- ❌ Recopila identificadores de dispositivo únicos -- ❌ Transmite datos a servidores externos - ---- - -## 3. Datos Almacenados Localmente - -Todos los datos que generes usando CBDCounter se almacenan **exclusivamente en tu dispositivo** mediante la tecnología SharedPreferences de Android: - -### Datos almacenados localmente: -- **Contadores diarios:** Número de consumos registrados por fecha -- **Notas personales:** Texto que escribas para cada día -- **Emojis personalizados:** Configuraciones visuales que hayas modificado -- **Preferencias de tema:** Modo oscuro/claro -- **Estado del disclaimer:** Indicador de que aceptaste el aviso legal - -### Características importantes: -✅ Estos datos **NUNCA** salen de tu dispositivo -✅ NO se sincronizan con ningún servidor -✅ NO son accesibles por el desarrollador -✅ Son eliminados automáticamente si desinstalas la app - ---- - -## 4. Función de Exportación CSV - -La Aplicación permite exportar tus datos a un archivo CSV para crear copias de seguridad personales. - -**Importante:** -- Tú controlas este archivo exportado -- El archivo se guarda en tu dispositivo -- Puedes compartirlo manualmente mediante las opciones estándar de Android -- El desarrollador NO tiene acceso a este archivo -- Tú eres responsable de cómo compartes o almacenas este archivo - ---- - -## 5. Permisos de Android - -La Aplicación solicita los siguientes permisos: - -### `RECEIVE_BOOT_COMPLETED` -**Propósito:** Restaurar el widget de pantalla principal después de reiniciar el dispositivo. -**Uso:** Solo se utiliza para actualizar el widget, no para rastrear ni monitorear actividad. - -**Ningún otro permiso es solicitado o utilizado.** - ---- - -## 6. Servicios de Terceros - -CBDCounter **NO utiliza**: -- ❌ Servicios de analytics (Google Analytics, Firebase, etc.) -- ❌ Redes publicitarias -- ❌ Sistemas de tracking o métricas -- ❌ SDKs de terceros (excepto AndroidX y Material Design de Google, que son librerías estándar de UI sin capacidad de recopilación de datos) - ---- - -## 7. Seguridad de los Datos - -Dado que todos los datos se almacenan localmente en tu dispositivo: - -- La seguridad depende de la protección de tu dispositivo (PIN, huella, etc.) -- En dispositivos rooteados, aplicaciones con permisos de root podrían acceder a los datos -- Recomendamos usar las funciones de seguridad nativas de Android - -**Nota:** Si tu dispositivo es compartido con otras personas, ellas podrían acceder a la Aplicación y ver tus datos. - ---- - -## 8. Privacidad de Menores - -CBDCounter no está dirigida a menores de 13 años. Dado que no recopilamos ningún dato, no recopilamos intencionalmente información de niños menores de 13 años. - ---- - -## 9. Cambios a esta Política - -Podemos actualizar esta Política de Privacidad ocasionalmente. Te notificaremos cualquier cambio publicando la nueva Política en esta página y actualizando la fecha de "Última actualización". - -**Es tu responsabilidad revisar esta Política periódicamente.** El uso continuado de la Aplicación después de cambios constituye aceptación de dichos cambios. - ---- - -## 10. Limitaciones y Descargo de Responsabilidad - -### CBDCounter NO es un dispositivo médico - -- Esta Aplicación NO proporciona consejo médico, diagnóstico o tratamiento -- NO está destinada a sustituir la consulta con un profesional de la salud -- El contenido es solo para fines informativos y de seguimiento personal -- Siempre consulta con un médico u otro profesional de la salud calificado sobre cualquier pregunta relacionada con tu salud - -### Uso de la Aplicación - -- El uso de CBDCounter es bajo tu propio riesgo -- La Aplicación se proporciona "tal cual" sin garantías de ningún tipo -- El desarrollador no se hace responsable del uso que hagas de los datos rastreados -- No promovemos, facilitamos ni apoyamos actividades ilegales - -### Legalidad del CBD - -- Es responsabilidad del usuario verificar la legalidad del CBD en su jurisdicción -- El desarrollador no asume responsabilidad por el uso de la Aplicación en jurisdicciones donde el CBD es ilegal -- Esta Aplicación NO facilita la compra, venta ni distribución de CBD u otras sustancias - ---- - -## 11. Tus Derechos - -Dado que no recopilamos ni almacenamos datos fuera de tu dispositivo: - -### Derecho de acceso -✅ Todos tus datos están siempre accesibles en la Aplicación - -### Derecho de rectificación -✅ Puedes editar tus notas y datos en cualquier momento - -### Derecho de eliminación -✅ Simplemente desinstala la Aplicación o usa la función de "reset" en ajustes - -### Derecho de portabilidad -✅ Usa la función "Exportar CSV" para obtener tus datos en formato portable - -### Derecho de oposición -✅ No aplica, ya que no procesamos tus datos - ---- - -## 12. Cumplimiento Legal - -Esta Política de Privacidad cumple con: -- **RGPD** (Reglamento General de Protección de Datos de la UE) -- **LOPD** (Ley Orgánica de Protección de Datos de España) -- **Políticas de Privacidad de Google Play** - ---- - -## 13. Transferencias Internacionales - -No aplicable. Tus datos nunca abandonan tu dispositivo. - ---- - -## 14. Retención de Datos - -Los datos se retienen mientras: -- Mantengas la Aplicación instalada en tu dispositivo -- No uses la función de reset o borrado manual - -Los datos se eliminan automáticamente cuando: -- Desinstalas la Aplicación -- Borras manualmente los datos desde configuración de Android -- Reseteas tu dispositivo a valores de fábrica - ---- - -## 15. Base Legal para el Procesamiento (RGPD) - -No aplicable, ya que no procesamos datos personales fuera de tu dispositivo. - -El almacenamiento local en tu dispositivo está bajo tu control exclusivo. - ---- - -## 16. Contacto - -Si tienes preguntas, comentarios o inquietudes sobre esta Política de Privacidad: - -**Email:** d4vram369@gmail.com -**Método de contacto preferido:** Email -**Tiempo de respuesta esperado:** 7 días hábiles - ---- - -## 17. Jurisdicción - -Esta Política de Privacidad se rige por las leyes de España. - ---- - -## 18. Idioma - -Esta Política está disponible en español. En caso de discrepancia entre traducciones, la versión en español prevalecerá. - ---- - -## Resumen Ejecutivo (TL;DR) - -✅ **NO recopilamos datos** -✅ **Todo se guarda en tu dispositivo** -✅ **NO compartimos nada con nadie** -✅ **NO usamos analytics ni publicidad** -✅ **Tú controlas 100% de tus datos** -✅ **Desinstalar = eliminar todo** - ---- - -**Fecha de entrada en vigor:** 10 de noviembre de 2025 - -© 2025 D4vRAM. Todos los derechos reservados.