commit 5a8c661ce8e1ed3140bb7013ac7486cdf07f2a78 Author: David Date: Wed Feb 4 20:56:57 2026 +1100 HydroFlux 0.0.1 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/Motivation_App.iml b/.idea/Motivation_App.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/Motivation_App.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml new file mode 100644 index 0000000..7d60e7b --- /dev/null +++ b/.idea/caches/deviceStreaming.xml @@ -0,0 +1,1186 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..1945ce5 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..e47f084 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Hydroflux/.gitignore b/Hydroflux/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/Hydroflux/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/Hydroflux/.idea/.gitignore b/Hydroflux/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/Hydroflux/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/Hydroflux/.idea/AndroidProjectSystem.xml b/Hydroflux/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/Hydroflux/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/Hydroflux/.idea/codeStyles/Project.xml b/Hydroflux/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..7643783 --- /dev/null +++ b/Hydroflux/.idea/codeStyles/Project.xml @@ -0,0 +1,123 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Hydroflux/.idea/codeStyles/codeStyleConfig.xml b/Hydroflux/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/Hydroflux/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/Hydroflux/.idea/compiler.xml b/Hydroflux/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/Hydroflux/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Hydroflux/.idea/deploymentTargetSelector.xml b/Hydroflux/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..f34209c --- /dev/null +++ b/Hydroflux/.idea/deploymentTargetSelector.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/Hydroflux/.idea/deviceManager.xml b/Hydroflux/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/Hydroflux/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/Hydroflux/.idea/gradle.xml b/Hydroflux/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/Hydroflux/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/Hydroflux/.idea/migrations.xml b/Hydroflux/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/Hydroflux/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/Hydroflux/.idea/misc.xml b/Hydroflux/.idea/misc.xml new file mode 100644 index 0000000..b2c751a --- /dev/null +++ b/Hydroflux/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/Hydroflux/.idea/runConfigurations.xml b/Hydroflux/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/Hydroflux/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/Hydroflux/app/.gitignore b/Hydroflux/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/Hydroflux/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/Hydroflux/app/build.gradle.kts b/Hydroflux/app/build.gradle.kts new file mode 100644 index 0000000..a13be9b --- /dev/null +++ b/Hydroflux/app/build.gradle.kts @@ -0,0 +1,60 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "com.david.hydroflux" + compileSdk = 36 + + defaultConfig { + applicationId = "com.david.hydroflux.v2" + minSdk = 28 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("androidx.health.connect:connect-client:1.1.0") // Official Stable Release + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.androidx.compose.ui.test.manifest) +} \ No newline at end of file diff --git a/Hydroflux/app/proguard-rules.pro b/Hydroflux/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/Hydroflux/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/Hydroflux/app/src/androidTest/java/com/david/hydroflux/ExampleInstrumentedTest.kt b/Hydroflux/app/src/androidTest/java/com/david/hydroflux/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..475cb8e --- /dev/null +++ b/Hydroflux/app/src/androidTest/java/com/david/hydroflux/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.david.hydroflux + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.david.hydroflux", appContext.packageName) + } +} \ No newline at end of file diff --git a/Hydroflux/app/src/main/AndroidManifest.xml b/Hydroflux/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1b2072c --- /dev/null +++ b/Hydroflux/app/src/main/AndroidManifest.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Hydroflux/app/src/main/assests/css/style.css b/Hydroflux/app/src/main/assests/css/style.css new file mode 100644 index 0000000..8ef832e --- /dev/null +++ b/Hydroflux/app/src/main/assests/css/style.css @@ -0,0 +1,641 @@ +:root { + /* Futuristic Palette */ + --bg-dark: #050508; + --bg-panel: rgba(20, 20, 35, 0.4); + --primary-cyan: #00f3ff; + --secondary-purple: #bc13fe; + --text-main: #e0e0e0; + --text-muted: #8a8a9b; + --glass-border: rgba(255, 255, 255, 0.08); + --neon-shadow: 0 0 10px rgba(0, 243, 255, 0.5); + + /* Spacing */ + --spacing-sm: 8px; + --spacing-md: 16px; + --spacing-lg: 24px; + + /* Font */ + --font-heading: 'Orbitron', sans-serif; + --font-body: 'Outfit', sans-serif; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + -webkit-tap-highlight-color: transparent; + /* Remove mobile tap highlight */ +} + +body { + background-color: var(--bg-dark); + color: var(--text-main); + font-family: var(--font-body); + height: 100vh; + overflow: hidden; + background-image: + radial-gradient(circle at 10% 20%, rgba(188, 19, 254, 0.1) 0%, transparent 40%), + radial-gradient(circle at 90% 80%, rgba(0, 243, 255, 0.08) 0%, transparent 40%); +} + +#app { + display: flex; + flex-direction: column; + height: 100%; +} + +/* Typography */ +h1, +h2, +h3 { + font-family: var(--font-heading); + letter-spacing: 1px; +} + +.glow-text { + text-shadow: 0 0 8px rgba(0, 243, 255, 0.3); +} + +.highlight { + color: var(--primary-cyan); +} + +/* Glassmorphism Utilities */ +.glass-header { + background: rgba(5, 5, 8, 0.7); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + padding: var(--spacing-md); + border-bottom: 1px solid var(--glass-border); + display: flex; + justify-content: space-between; + align-items: center; + position: sticky; + top: 0; + z-index: 10; +} + +.glass-panel { + background: var(--bg-panel); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border: 1px solid var(--glass-border); + border-radius: 20px; + padding: var(--spacing-lg); + margin: var(--spacing-md); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); +} + +.glass-nav { + background: rgba(10, 10, 16, 0.8); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border-top: 1px solid var(--glass-border); + padding: var(--spacing-md) var(--spacing-lg); + display: flex; + justify-content: space-around; + position: fixed; + bottom: 0; + width: 100%; + padding-bottom: max(16px, env(safe-area-inset-bottom)); + /* Safe area for iOS */ +} + +/* Content Area */ +#main-content { + flex: 1; + overflow-y: auto; + padding-bottom: 80px; + /* Space for nav */ +} + +/* Interactive Elements */ +.nav-btn { + background: none; + border: none; + color: var(--text-muted); + padding: 10px; + border-radius: 50%; + transition: all 0.3s ease; + cursor: pointer; +} + +.nav-btn.active { + color: var(--primary-cyan); + background: rgba(0, 243, 255, 0.1); + box-shadow: 0 0 15px rgba(0, 243, 255, 0.2); +} + +.nav-btn svg { + display: block; +} + +/* Animations */ +@keyframes pulse-glow { + 0% { + box-shadow: 0 0 5px rgba(0, 243, 255, 0.5); + } + + 50% { + box-shadow: 0 0 20px rgba(0, 243, 255, 0.8); + } + + 100% { + box-shadow: 0 0 5px rgba(0, 243, 255, 0.5); + } +} + +/* --- Water Tracker Module Styles --- */ + +.water-tracker-container { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-lg); +} + +.circle-container { + position: relative; + width: 250px; + height: 250px; + border-radius: 50%; + border: 4px solid var(--bg-panel); + box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); + overflow: hidden; + /* Clips the wave */ + background: rgba(0, 0, 0, 0.3); +} + +.water-circle { + width: 100%; + height: 100%; + position: relative; +} + +/* Wave Animation */ +.wave { + position: absolute; + top: 100%; + /* Dynamic */ + left: -50%; + width: 200%; + height: 200%; + background: rgba(0, 243, 255, 0.4); + border-radius: 40%; + animation: rotate 6s linear infinite; + transition: top 1s ease-in-out; +} + +.wave::before { + content: ""; + position: absolute; + top: 5px; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 243, 255, 0.3); + /* Lighter top layer */ + border-radius: 40%; + animation: rotate 10s linear infinite reverse; +} + +@keyframes rotate { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +.circle-content { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 2; + /* Above wave */ + pointer-events: none; +} + +.water-percentage { + font-family: var(--font-heading); + font-size: 3rem; + font-weight: 700; + color: #fff; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); +} + +.water-label { + font-size: 0.8rem; + letter-spacing: 2px; + color: rgba(255, 255, 255, 0.7); + margin-top: -5px; +} + +.stats-row { + font-family: var(--font-heading); + font-size: 1.2rem; + color: var(--primary-cyan); +} + +/* Controls */ +.controls-area { + width: 100%; + display: flex; + flex-direction: column; + gap: var(--spacing-md); + align-items: center; +} + +.bottle-selector { + display: flex; + align-items: center; + gap: 10px; + font-size: 0.9rem; + color: var(--text-muted); +} + +.bottle-selector input { + background: rgba(0, 0, 0, 0.3); + border: 1px solid var(--primary-cyan); + color: var(--primary-cyan); + padding: 10px; + border-radius: 12px; + font-family: var(--font-heading); + width: 80px; + text-align: center; + font-size: 1.1rem; + outline: none; + box-shadow: 0 0 10px rgba(0, 243, 255, 0.1); +} + +.bottle-selector input:focus { + box-shadow: 0 0 15px rgba(0, 243, 255, 0.3); +} + +.action-buttons { + display: flex; + align-items: center; + gap: 20px; + margin-top: 10px; +} + +.icon-btn { + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--glass-border); + color: var(--text-muted); + width: 60px; + height: 60px; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.icon-btn.secondary { + border-color: rgba(255, 50, 50, 0.3); + color: #ff4d4d; +} + +.icon-btn:active { + transform: scale(0.9); +} + +.glow-btn { + position: relative; + background: rgba(0, 243, 255, 0.1); + border: 1px solid var(--primary-cyan); + color: var(--primary-cyan); + font-family: var(--font-heading); + font-size: 1.1rem; + font-weight: 700; + padding: 0 40px; + height: 60px; + border-radius: 30px; + cursor: pointer; + display: flex; + align-items: center; + gap: 12px; + box-shadow: 0 0 10px rgba(0, 243, 255, 0.2), inset 0 0 20px rgba(0, 243, 255, 0.1); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + text-transform: uppercase; + letter-spacing: 2px; + overflow: hidden; +} + +.glow-btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(0, 243, 255, 0.4), transparent); + transition: 0.5s; +} + +.glow-btn:hover { + background: rgba(0, 243, 255, 0.2); + box-shadow: 0 0 30px rgba(0, 243, 255, 0.6); + color: #fff; + text-shadow: 0 0 8px rgba(0, 243, 255, 0.8); +} + +.glow-btn:hover::before { + left: 100%; +} + +.glow-btn:active { + transform: scale(0.98); +} + +.glow-btn svg { + filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.8)); +} + +/* --- Streak Module Styles --- */ +.streak-container { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-lg); + text-align: center; +} + +.section-title { + font-size: 1.2rem; + color: var(--secondary-purple); + letter-spacing: 4px; + margin-bottom: 20px; +} + +.streak-counter { + background: radial-gradient(circle, rgba(188, 19, 254, 0.1) 0%, transparent 70%); + padding: 40px; + border-radius: 50%; + margin-bottom: 20px; +} + +.streak-days { + font-family: var(--font-heading); + font-size: 6rem; + color: white; + line-height: 1; + text-shadow: 0 0 20px rgba(188, 19, 254, 0.6); +} + +.streak-label { + font-size: 1.5rem; + color: var(--text-muted); + letter-spacing: 5px; +} + +.streak-detail { + font-size: 1rem; + color: var(--primary-cyan); + margin-top: 10px; +} + +.quote-card { + background: rgba(255, 255, 255, 0.05); + border-left: 3px solid var(--primary-cyan); + padding: 20px; + font-style: italic; + color: #dedede; + margin: 20px 0; + line-height: 1.6; + max-width: 300px; +} + +.danger-btn { + position: relative; + background: rgba(255, 50, 50, 0.1); + border: 1px solid #ff4d4d; + color: #ff4d4d; + font-family: var(--font-heading); + padding: 15px 40px; + border-radius: 30px; + cursor: pointer; + font-size: 1rem; + letter-spacing: 2px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + margin-top: 30px; + box-shadow: 0 0 10px rgba(255, 50, 50, 0.2), inset 0 0 20px rgba(255, 50, 50, 0.1); + text-transform: uppercase; + font-weight: 700; +} + +.danger-btn:hover { + background: rgba(255, 50, 50, 0.2); + box-shadow: 0 0 30px rgba(255, 50, 50, 0.6); + color: #fff; + text-shadow: 0 0 8px rgba(255, 50, 50, 0.8); + transform: translateY(-2px); +} + +.danger-btn:active { + transform: scale(0.95); +} + +/* --- Connect Button (Neon Cyan Variant) --- */ +.connect-glow-btn { + position: relative; + background: rgba(0, 243, 255, 0.1); + border: 1px solid var(--primary-cyan); + color: var(--primary-cyan); + font-family: var(--font-heading); + padding: 8px 24px; + border-radius: 20px; + cursor: pointer; + font-size: 0.9rem; + letter-spacing: 1px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 0 10px rgba(0, 243, 255, 0.2), inset 0 0 10px rgba(0, 243, 255, 0.1); + text-transform: uppercase; + font-weight: 700; +} + +.connect-glow-btn:hover { + background: rgba(0, 243, 255, 0.2); + box-shadow: 0 0 20px rgba(0, 243, 255, 0.6); + color: #fff; + text-shadow: 0 0 5px rgba(0, 243, 255, 0.8); + transform: translateY(-1px); +} + +.connect-glow-btn:active { + transform: scale(0.95); +} + +/* --- Concentric Rings --- */ +.rings-wrapper { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 20px; + background: radial-gradient(circle, rgba(255, 255, 255, 0.02) 0%, transparent 70%); + padding: 20px; + border-radius: 50%; +} + +.concentric-svg { + width: 200px; + height: 200px; + transform: rotate(-90deg); +} + +.ring-bg { + fill: none; + stroke: rgba(255, 255, 255, 0.05); + /* Proper track color */ +} + +.ring-progress { + fill: none; + stroke-linecap: round; + transition: stroke-dashoffset 1.5s ease-in-out; +} + +.ring-progress.cyan { + stroke: var(--primary-cyan); + filter: drop-shadow(0 0 5px var(--primary-cyan)); +} + +.ring-progress.purple { + stroke: var(--secondary-purple); + filter: drop-shadow(0 0 5px var(--secondary-purple)); +} + +.rings-legend { + display: flex; + gap: 20px; + margin-top: 15px; +} + +.legend-item { + font-size: 0.8rem; + color: var(--text-muted); + display: flex; + align-items: center; + gap: 8px; + letter-spacing: 1px; +} + +.dot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.dot.cyan { + background: var(--primary-cyan); + box-shadow: 0 0 5px var(--primary-cyan); +} + +.dot.purple { + background: var(--secondary-purple); + box-shadow: 0 0 5px var(--secondary-purple); +} + +/* --- Combined Stat Card & Charts --- */ +.stat-header { + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.stat-label { + font-size: 0.8rem; + color: var(--text-muted); + letter-spacing: 2px; + margin-bottom: 5px; + display: block; +} + +.stat-value { + font-family: var(--font-heading); + font-size: 2rem; + color: #fff; + line-height: 1; +} + +.stat-sub { + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.5); + margin-top: 5px; +} + +.icon-box { + width: 40px; + height: 40px; + border-radius: 12px; + display: flex; + justify-content: center; + align-items: center; +} + +.cyan-box { + background: rgba(0, 243, 255, 0.1); + color: var(--primary-cyan); +} + +.purple-box { + background: rgba(188, 19, 254, 0.1); + color: var(--secondary-purple); +} + +.chart-divider { + height: 1px; + background: rgba(255, 255, 255, 0.1); + margin: 20px 0; +} + +/* Re-using chart styles but refining for the box */ +.chart-container.small { + height: 100px; + margin-top: 0; + border-bottom: none; + padding-bottom: 0; + display: flex; + justify-content: space-between; + align-items: flex-end; +} + +.chart-column { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; +} + +.chart-bar { + width: 6px; + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; + transition: height 1s ease-out; +} + +.chart-bar.cyan { + background: var(--primary-cyan); + box-shadow: 0 0 5px rgba(0, 243, 255, 0.3); +} + +.chart-bar.purple { + background: var(--secondary-purple); + box-shadow: 0 0 5px rgba(188, 19, 254, 0.3); +} + +.chart-day { + margin-top: 8px; + font-size: 0.6rem; + color: var(--text-muted); +} \ No newline at end of file diff --git a/Hydroflux/app/src/main/assests/index.html b/Hydroflux/app/src/main/assests/index.html new file mode 100644 index 0000000..d6161e8 --- /dev/null +++ b/Hydroflux/app/src/main/assests/index.html @@ -0,0 +1,74 @@ + + + + + + + HydroFlux + + + + + + + + + + + + + + + + + + +
+ +
+

HYDROFLUX

+
+
+ +
+ +
+ +
Loading Core...
+
+ + + + +
+ + +
+ + + + \ No newline at end of file diff --git a/Hydroflux/app/src/main/assests/js/app.js b/Hydroflux/app/src/main/assests/js/app.js new file mode 100644 index 0000000..616e2e4 --- /dev/null +++ b/Hydroflux/app/src/main/assests/js/app.js @@ -0,0 +1,28 @@ +console.log('HydroFlux Initialized'); + +// Simple Navigation Logic +document.querySelectorAll('.nav-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + // Toggle Active State + document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active')); + e.currentTarget.classList.add('active'); + + const view = e.currentTarget.dataset.view; + + // Hide all sections + document.querySelectorAll('section').forEach(el => el.style.display = 'none'); + + // Show target section + const target = document.getElementById(`${view}-section`); + if (target) target.style.display = 'block'; + }); +}); + +// Initialize Modules +import { WaterTracker } from './modules/water.js?v=2'; +import { StreakTracker } from './modules/streak.js?v=2'; +import { FitnessDashboard } from './modules/fitness.js?v=2'; + +const waterTracker = new WaterTracker('water-section'); +const streakTracker = new StreakTracker('streak-section'); +const fitnessDashboard = new FitnessDashboard('fitness-section'); diff --git a/Hydroflux/app/src/main/assests/js/modules/fitness.js b/Hydroflux/app/src/main/assests/js/modules/fitness.js new file mode 100644 index 0000000..ee2a11d --- /dev/null +++ b/Hydroflux/app/src/main/assests/js/modules/fitness.js @@ -0,0 +1,153 @@ +export class FitnessDashboard { + constructor(containerId) { + this.container = document.getElementById(containerId); + // Mock Data + this.data = { + steps: { current: 8432, goal: 10000 }, + sleep: { current: 6.5, goal: 8 }, + history: { + steps: [4500, 7200, 10500, 8900, 6000, 11200, 8432], + sleep: [5.5, 6.0, 7.5, 8.2, 5.0, 9.0, 6.5] + } + }; + + this.render(); + // Delay animation to allow DOM paint + setTimeout(() => this.animate(), 100); + } + + render() { + const stepPercent = Math.min((this.data.steps.current / this.data.steps.goal) * 100, 100); + const sleepPercent = Math.min((this.data.sleep.current / this.data.sleep.goal) * 100, 100); + + // Ring Config + const center = 100; + const radiusOuter = 80; + const radiusInner = 55; + + const circumOuter = 2 * Math.PI * radiusOuter; + const circumInner = 2 * Math.PI * radiusInner; + + this.container.innerHTML = ` +
+

DAILY ACTIVITY

+ + +
+ + + + + + + + + + + + + + + + + +
+
+ STEPS +
+
+ SLEEP +
+
+
+ + +
+
+ TicWatch Pro 5 + Disconnected +
+ +
+ + +
+
+
+ STEPS +
${this.data.steps.current}
+
Goal: ${this.data.steps.goal}
+
+
+ +
+
+ +
+ +
+ ${this.generateBars(this.data.history.steps, 12000, 'cyan')} +
+
+ + +
+
+
+ SLEEP +
${this.data.sleep.current}h
+
Goal: ${this.data.sleep.goal}h
+
+
+ +
+
+ +
+ +
+ ${this.generateBars(this.data.history.sleep, 10, 'purple')} +
+
+
+ `; + + this.attachEvents(); + } + + generateBars(data, max, colorClass) { + const days = ['M', 'T', 'W', 'T', 'F', 'S', 'S']; + return data.map((val, index) => { + const height = Math.min((val / max) * 100, 100); + return ` +
+
+ ${days[index]} +
+ `; + }).join(''); + } + + animate() { + this.container.querySelectorAll('.ring-progress').forEach(ring => { + ring.style.strokeDashoffset = ring.dataset.offset; + }); + } + + attachEvents() { + const btn = this.container.querySelector('#connect-watch-btn'); + if (btn) { + btn.addEventListener('click', () => { + alert("Placeholder: This feature would connect to the WearOS API in the native app."); + }); + } + } +} diff --git a/Hydroflux/app/src/main/assests/js/modules/streak.js b/Hydroflux/app/src/main/assests/js/modules/streak.js new file mode 100644 index 0000000..2092ea7 --- /dev/null +++ b/Hydroflux/app/src/main/assests/js/modules/streak.js @@ -0,0 +1,102 @@ +export class StreakTracker { + constructor(containerId) { + this.container = document.getElementById(containerId); + this.STORAGE_KEY = 'hydroflux_streak'; + this.quotes = [ + "The only easy day was yesterday.", + "Discipline is doing what needs to be done, even if you don't want to.", + "Your future self is watching you right now through memories.", + "Pain is temporary. Quitting lasts forever.", + "Suffering is the currency of success.", + "Don't stop when you're tired. Stop when you're done.", + "You are stronger than your urges.", + "Focus on the goal, not the obstacle." + ]; + + this.loadState(); + this.render(); + this.startTimer(); + } + + loadState() { + const saved = localStorage.getItem(this.STORAGE_KEY); + if (saved) { + this.startDate = new Date(parseInt(saved)); + } else { + this.startDate = new Date(); + this.saveState(); + } + } + + saveState() { + localStorage.setItem(this.STORAGE_KEY, this.startDate.getTime().toString()); + } + + resetStreak() { + if (confirm("Are you sure you want to reset your streak?")) { + this.startDate = new Date(); + this.saveState(); + this.updateUI(); + + // Haptic Bad Feedback + if (navigator.vibrate) navigator.vibrate([100, 50, 100]); + } + } + + getDuration() { + const now = new Date(); + const diff = now - this.startDate; + + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); + + return { days, hours, minutes }; + } + + updateUI() { + const { days, hours, minutes } = this.getDuration(); + + const daysEl = this.container.querySelector('.streak-days'); + const detailEl = this.container.querySelector('.streak-detail'); + + if (daysEl) daysEl.textContent = days; + if (detailEl) detailEl.textContent = `${hours}h ${minutes}m`; + } + + startTimer() { + setInterval(() => this.updateUI(), 60000); // Update every minute + } + + getRandomQuote() { + return this.quotes[Math.floor(Math.random() * this.quotes.length)]; + } + + render() { + this.container.innerHTML = ` +
+

ABSTINENCE STREAK

+ +
+
0
+
DAYS
+
${0}h ${0}m
+
+ +
+ "${this.getRandomQuote()}" +
+ + +
+ `; + + this.updateUI(); + + this.container.querySelector('#reset-streak-btn').addEventListener('click', () => { + this.resetStreak(); + }); + } +} diff --git a/Hydroflux/app/src/main/assests/js/modules/water.js b/Hydroflux/app/src/main/assests/js/modules/water.js new file mode 100644 index 0000000..f21c02a --- /dev/null +++ b/Hydroflux/app/src/main/assests/js/modules/water.js @@ -0,0 +1,184 @@ + +export class WaterTracker { + constructor(containerId) { + this.container = document.getElementById(containerId); + this.state = { + current: 0, + goal: 3000, // mL + bottleSize: 500, // mL + }; + this.STORAGE_KEY = 'hydroflux_data'; + this.loadState(); + this.render(); + this.attachEvents(); + this.updateUI(); + } + + loadState() { + const saved = localStorage.getItem(this.STORAGE_KEY); + if (saved) { + const parsed = JSON.parse(saved); + this.state = { ...this.state, ...parsed }; + } + } + + saveState() { + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.state)); + this.updateUI(); + } + + addWater() { + this.state.current += this.state.bottleSize; + this.saveState(); + if (navigator.vibrate) navigator.vibrate(50); + } + + removeWater() { + this.state.current = Math.max(0, this.state.current - this.state.bottleSize); + this.saveState(); + if (navigator.vibrate) navigator.vibrate(50); + } + + setBottleSize(size) { + if (!size || size <= 0) return; + this.state.bottleSize = size; + this.saveState(); + } + + getPercentage() { + return Math.min(100, Math.max(0, (this.state.current / this.state.goal) * 100)); + } + + updateUI() { + // Update Text + const currentEl = this.container.querySelector('.water-count'); + const percentageEl = this.container.querySelector('.water-percentage'); + + if (currentEl) currentEl.textContent = `${this.state.current} / ${this.state.goal} mL`; + if (percentageEl) percentageEl.textContent = `${Math.round(this.getPercentage())}%`; + + // Update Wave Animation Height + const wave = this.container.querySelector('.wave'); + if (wave) { + wave.style.top = `${100 - this.getPercentage()}%`; + } + } + + render() { + this.container.innerHTML = ` +
+ +
+
+
+
+ 0% + HYDRATION +
+
+
+ + +
+ 0 / 3000 mL +
+ + +
+
+ + +
+ +
+ + + + + + +
+
+
+ `; + + this.checkNotificationStatus(); + } + + attachEvents() { + this.container.querySelector('#add-water-btn').addEventListener('click', () => { + this.addWater(); + }); + + this.container.querySelector('#remove-water-btn').addEventListener('click', () => { + this.removeWater(); + }); + + this.container.querySelector('#notify-btn').addEventListener('click', (e) => { + this.toggleNotifications(e.currentTarget); + }); + + const input = this.container.querySelector('#bottle-size-input'); + input.addEventListener('change', (e) => { + this.setBottleSize(parseInt(e.target.value)); + }); + } + + // --- Notification Logic --- + + toggleNotifications(btn) { + if (!("Notification" in window)) { + alert("This browser does not support desktop notifications"); + return; + } + + if (Notification.permission === "granted") { + alert("Reminders are active! We'll check every hour."); + } else if (Notification.permission !== "denied") { + Notification.requestPermission().then(permission => { + if (permission === "granted") { + this.startReminderLoop(); + btn.style.color = "var(--primary-cyan)"; + new Notification("HydroFlux", { body: "Smart Hydration Reminders Enabled!" }); + } + }); + } + } + + checkNotificationStatus() { + if (Notification.permission === "granted") { + const btn = this.container.querySelector('#notify-btn'); + if (btn) btn.style.color = "var(--primary-cyan)"; + this.startReminderLoop(); + } + } + + startReminderLoop() { + // Clear existing to avoid duplicates + if (this.reminderInterval) clearInterval(this.reminderInterval); + + // Check every minute if it's been > 1 hour since last drink + this.reminderInterval = setInterval(() => { + // Pseudo-logic check since we don't store timestamp in this simple version yet + // In a real app, you'd check this.state.lastDrinkTime + new Notification("HydroFlux Needs You", { + body: "Remember to drink water!", + icon: "/icon.png" + }); + }, 3600000); // 1 Hour + } +} diff --git a/Hydroflux/app/src/main/assests/manifest.json b/Hydroflux/app/src/main/assests/manifest.json new file mode 100644 index 0000000..4e063f7 --- /dev/null +++ b/Hydroflux/app/src/main/assests/manifest.json @@ -0,0 +1,17 @@ +{ + "name": "HydroFlux Motivation", + "short_name": "HydroFlux", + "background_color": "#050508", + "theme_color": "#050508", + "display": "standalone", + "orientation": "portrait", + "scope": "/", + "start_url": "/", + "icons": [ + { + "src": "favicon.svg", + "sizes": "192x192", + "type": "image/svg+xml" + } + ] +} diff --git a/Hydroflux/app/src/main/assets/css/fitness_stats.css b/Hydroflux/app/src/main/assets/css/fitness_stats.css new file mode 100644 index 0000000..35fa922 --- /dev/null +++ b/Hydroflux/app/src/main/assets/css/fitness_stats.css @@ -0,0 +1,40 @@ +/* --- New Fitness Stats Display --- */ +.rings-stats { + display: flex; + justify-content: center; + gap: 40px; + margin-top: 20px; + width: 100%; +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; +} + +.stat-item.cyan .stat-number { + color: var(--primary-cyan); + text-shadow: 0 0 10px rgba(0, 243, 255, 0.4); +} + +.stat-item.purple .stat-number { + color: var(--secondary-purple); + text-shadow: 0 0 10px rgba(188, 19, 254, 0.4); +} + +.stat-label-small { + font-size: 0.75rem; + color: var(--text-muted); + letter-spacing: 2px; + display: flex; + align-items: center; + gap: 6px; +} + +.stat-number { + font-family: var(--font-heading); + font-size: 1.8rem; + font-weight: 700; +} \ No newline at end of file diff --git a/Hydroflux/app/src/main/assets/css/style.css b/Hydroflux/app/src/main/assets/css/style.css new file mode 100644 index 0000000..ac54b5a --- /dev/null +++ b/Hydroflux/app/src/main/assets/css/style.css @@ -0,0 +1,955 @@ +:root { + /* Futuristic Palette */ + --bg-dark: #050508; + --bg-panel: rgba(20, 20, 35, 0.4); + --primary-cyan: #00f3ff; + --secondary-purple: #bc13fe; + --text-main: #e0e0e0; + --text-muted: #8a8a9b; + --glass-border: rgba(255, 255, 255, 0.08); + --neon-shadow: 0 0 10px rgba(0, 243, 255, 0.5); + + /* Spacing */ + --spacing-sm: 8px; + --spacing-md: 16px; + --spacing-lg: 24px; + + /* Font */ + --font-heading: 'Orbitron', sans-serif; + --font-body: 'Outfit', sans-serif; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + -webkit-tap-highlight-color: transparent; + /* Remove mobile tap highlight */ +} + +body { + background-color: var(--bg-dark); + color: var(--text-main); + font-family: var(--font-body); + height: 100vh; + overflow: hidden; + background-image: + radial-gradient(circle at 10% 20%, rgba(188, 19, 254, 0.1) 0%, transparent 40%), + radial-gradient(circle at 90% 80%, rgba(0, 243, 255, 0.08) 0%, transparent 40%); +} + +#app { + display: flex; + flex-direction: column; + height: 100%; +} + +/* Typography */ +h1, +h2, +h3 { + font-family: var(--font-heading); + letter-spacing: 1px; +} + +.glow-text { + text-shadow: 0 0 8px rgba(0, 243, 255, 0.3); +} + +.highlight { + color: var(--primary-cyan); +} + +/* Glassmorphism Utilities */ +.glass-header { + background: rgba(5, 5, 8, 0.7); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + padding: var(--spacing-md); + border-bottom: 1px solid var(--glass-border); + display: flex; + justify-content: space-between; + align-items: center; + position: sticky; + top: 0; + z-index: 10; +} + +.glass-panel { + background: var(--bg-panel); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border: 1px solid var(--glass-border); + border-radius: 20px; + padding: var(--spacing-lg); + margin: var(--spacing-md); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); +} + +.glass-nav { + background: rgba(10, 10, 16, 0.8); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border-top: 1px solid var(--glass-border); + padding: var(--spacing-md) var(--spacing-lg); + display: flex; + justify-content: space-around; + position: fixed; + bottom: 0; + width: 100%; + padding-bottom: max(16px, env(safe-area-inset-bottom)); + /* Safe area for iOS */ +} + +/* Content Area */ +#main-content { + flex: 1; + overflow-y: auto; + padding-bottom: 80px; + /* Space for nav */ +} + +/* Interactive Elements */ +.nav-btn { + background: none; + border: none; + color: var(--text-muted); + padding: 10px; + border-radius: 50%; + transition: all 0.3s ease; + cursor: pointer; +} + +.nav-btn.active { + color: var(--primary-cyan); + background: rgba(0, 243, 255, 0.1); + box-shadow: 0 0 15px rgba(0, 243, 255, 0.2); +} + +.nav-btn svg { + display: block; +} + +/* Animations */ +@keyframes pulse-glow { + 0% { + box-shadow: 0 0 5px rgba(0, 243, 255, 0.5); + } + + 50% { + box-shadow: 0 0 20px rgba(0, 243, 255, 0.8); + } + + 100% { + box-shadow: 0 0 5px rgba(0, 243, 255, 0.5); + } +} + +/* --- Water Tracker Module Styles --- */ + +.water-tracker-container { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-lg); +} + +.circle-container { + position: relative; + width: 250px; + height: 250px; + border-radius: 50%; + border: 4px solid var(--bg-panel); + box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); + overflow: hidden; + /* Clips the wave */ + background: rgba(0, 0, 0, 0.3); +} + +.water-circle { + width: 100%; + height: 100%; + position: relative; +} + +/* Wave Animation */ +.wave { + position: absolute; + top: 100%; + /* Dynamic */ + left: -50%; + width: 200%; + height: 200%; + background: rgba(0, 243, 255, 0.4); + border-radius: 40%; + animation: rotate 6s linear infinite; + transition: top 1s ease-in-out; +} + +.wave::before { + content: ""; + position: absolute; + top: 5px; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 243, 255, 0.3); + /* Lighter top layer */ + border-radius: 40%; + animation: rotate 10s linear infinite reverse; +} + +@keyframes rotate { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +.circle-content { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 2; + /* Above wave */ + pointer-events: none; +} + +.water-percentage { + font-family: var(--font-heading); + font-size: 3rem; + font-weight: 700; + color: #fff; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); +} + +.water-label { + font-size: 0.8rem; + letter-spacing: 2px; + color: rgba(255, 255, 255, 0.7); + margin-top: -5px; +} + +.stats-row { + font-family: var(--font-heading); + font-size: 1.2rem; + color: var(--primary-cyan); +} + +/* Controls */ +.controls-area { + width: 100%; + display: flex; + flex-direction: column; + gap: var(--spacing-md); + align-items: center; +} + +.bottle-selector { + display: flex; + align-items: center; + gap: 10px; + font-size: 0.9rem; + color: var(--text-muted); +} + +.bottle-selector input { + background: rgba(0, 0, 0, 0.3); + border: 1px solid var(--primary-cyan); + color: var(--primary-cyan); + padding: 10px; + border-radius: 12px; + font-family: var(--font-heading); + width: 80px; + text-align: center; + font-size: 1.1rem; + outline: none; + box-shadow: 0 0 10px rgba(0, 243, 255, 0.1); +} + +.bottle-selector input:focus { + box-shadow: 0 0 15px rgba(0, 243, 255, 0.3); +} + +.action-buttons { + display: flex; + align-items: center; + gap: 20px; + margin-top: 10px; +} + +.icon-btn { + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--glass-border); + color: var(--text-muted); + width: 60px; + height: 60px; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.icon-btn.secondary { + border-color: rgba(255, 50, 50, 0.3); + color: #ff4d4d; +} + +.icon-btn:active { + transform: scale(0.9); +} + +.glow-btn { + position: relative; + background: rgba(0, 243, 255, 0.1); + border: 1px solid var(--primary-cyan); + color: var(--primary-cyan); + font-family: var(--font-heading); + font-size: 1.1rem; + font-weight: 700; + padding: 0 40px; + height: 60px; + border-radius: 30px; + cursor: pointer; + display: flex; + align-items: center; + gap: 12px; + box-shadow: 0 0 10px rgba(0, 243, 255, 0.2), inset 0 0 20px rgba(0, 243, 255, 0.1); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + text-transform: uppercase; + letter-spacing: 2px; + overflow: hidden; +} + +.glow-btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(0, 243, 255, 0.4), transparent); + transition: 0.5s; +} + +.glow-btn:hover { + background: rgba(0, 243, 255, 0.2); + box-shadow: 0 0 30px rgba(0, 243, 255, 0.6); + color: #fff; + text-shadow: 0 0 8px rgba(0, 243, 255, 0.8); +} + +.glow-btn:hover::before { + left: 100%; +} + +.glow-btn:active { + transform: scale(0.98); +} + +.glow-btn svg { + filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.8)); +} + +/* --- Streak Module Styles --- */ +.streak-container { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-lg); + text-align: center; +} + +.section-title { + font-size: 1.2rem; + color: var(--secondary-purple); + letter-spacing: 4px; + margin-bottom: 20px; +} + +.streak-counter { + background: radial-gradient(circle, rgba(188, 19, 254, 0.1) 0%, transparent 70%); + padding: 40px; + border-radius: 50%; + margin-bottom: 20px; +} + +.streak-days { + font-family: var(--font-heading); + font-size: 6rem; + color: white; + line-height: 1; + text-shadow: 0 0 20px rgba(188, 19, 254, 0.6); +} + +.streak-label { + font-size: 1.5rem; + color: var(--text-muted); + letter-spacing: 5px; +} + +.streak-detail { + font-size: 1rem; + color: var(--primary-cyan); + margin-top: 10px; +} + +.quote-card { + background: rgba(255, 255, 255, 0.05); + border-left: 3px solid var(--primary-cyan); + padding: 20px; + font-style: italic; + color: #dedede; + margin: 20px 0; + line-height: 1.6; + max-width: 300px; +} + +.danger-btn { + position: relative; + background: rgba(255, 50, 50, 0.1); + border: 1px solid #ff4d4d; + color: #ff4d4d; + font-family: var(--font-heading); + padding: 30px 20px 15px; + /* Increased top padding for camera cutout */ + border-radius: 30px; + cursor: pointer; + font-size: 1rem; + letter-spacing: 2px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + margin-top: 30px; + box-shadow: 0 0 10px rgba(255, 50, 50, 0.2), inset 0 0 20px rgba(255, 50, 50, 0.1); + text-transform: uppercase; + font-weight: 700; +} + +.danger-btn:hover { + background: rgba(255, 50, 50, 0.2); + box-shadow: 0 0 30px rgba(255, 50, 50, 0.6); + color: #fff; + text-shadow: 0 0 8px rgba(255, 50, 50, 0.8); + transform: translateY(-2px); +} + +.danger-btn:active { + transform: scale(0.95); +} + +/* --- Connect Button (Neon Cyan Variant) --- */ +.connect-glow-btn { + position: relative; + background: rgba(0, 243, 255, 0.1); + border: 1px solid var(--primary-cyan); + color: var(--primary-cyan); + font-family: var(--font-heading); + padding: 8px 24px; + border-radius: 20px; + cursor: pointer; + font-size: 0.9rem; + letter-spacing: 1px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 0 10px rgba(0, 243, 255, 0.2), inset 0 0 10px rgba(0, 243, 255, 0.1); + text-transform: uppercase; + font-weight: 700; +} + +.connect-glow-btn:hover { + background: rgba(0, 243, 255, 0.2); + box-shadow: 0 0 20px rgba(0, 243, 255, 0.6); + color: #fff; + text-shadow: 0 0 5px rgba(0, 243, 255, 0.8); + transform: translateY(-1px); +} + +.connect-glow-btn:active { + transform: scale(0.95); +} + +/* --- Concentric Rings --- */ +.rings-wrapper { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 20px; + background: radial-gradient(circle, rgba(255, 255, 255, 0.02) 0%, transparent 70%); + padding: 20px; + border-radius: 50%; +} + +.concentric-svg { + width: 200px; + height: 200px; + transform: rotate(-90deg); +} + +.ring-bg { + fill: none; + stroke: rgba(255, 255, 255, 0.05); + /* Proper track color */ +} + +.ring-progress { + fill: none; + stroke-linecap: round; + transition: stroke-dashoffset 1.5s ease-in-out; +} + +.ring-progress.cyan { + stroke: var(--primary-cyan); + filter: drop-shadow(0 0 5px var(--primary-cyan)); +} + +.ring-progress.purple { + stroke: var(--secondary-purple); + filter: drop-shadow(0 0 5px var(--secondary-purple)); +} + +.rings-legend { + display: flex; + gap: 20px; + margin-top: 15px; +} + +.legend-item { + font-size: 0.8rem; + color: var(--text-muted); + display: flex; + align-items: center; + gap: 8px; + letter-spacing: 1px; +} + +.dot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.dot.cyan { + background: var(--primary-cyan); + box-shadow: 0 0 5px var(--primary-cyan); +} + +.dot.purple { + background: var(--secondary-purple); + box-shadow: 0 0 5px var(--secondary-purple); +} + +/* --- Combined Stat Card & Charts --- */ +.stat-header { + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.stat-label { + font-size: 0.8rem; + color: var(--text-muted); + letter-spacing: 2px; + margin-bottom: 5px; + display: block; +} + +.stat-value { + font-family: var(--font-heading); + font-size: 2rem; + color: #fff; + line-height: 1; +} + +.stat-sub { + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.5); + margin-top: 5px; +} + +.icon-box { + width: 40px; + height: 40px; + border-radius: 12px; + display: flex; + justify-content: center; + align-items: center; +} + +.cyan-box { + background: rgba(0, 243, 255, 0.1); + color: var(--primary-cyan); +} + +.purple-box { + background: rgba(188, 19, 254, 0.1); + color: var(--secondary-purple); +} + +.chart-divider { + height: 1px; + background: rgba(255, 255, 255, 0.1); + margin: 20px 0; +} + +/* Re-using chart styles but refining for the box */ +.chart-container.small { + height: 100px; + margin-top: 0; + border-bottom: none; + padding-bottom: 0; + display: flex; + justify-content: space-between; + align-items: flex-end; +} + +.chart-column { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; +} + +.chart-bar { + width: 6px; + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; + transition: height 1s ease-out; +} + +.chart-bar.cyan { + background: var(--primary-cyan); + box-shadow: 0 0 5px rgba(0, 243, 255, 0.3); +} + +.chart-bar.purple { + background: var(--secondary-purple); + box-shadow: 0 0 5px rgba(188, 19, 254, 0.3); +} + +.chart-day { + color: var(--text-muted); +} + +/* --- Calendar Module Styles --- */ +.calendar-wrapper { + background: rgba(255, 255, 255, 0.03); + border-radius: 20px; + padding: 20px; + width: 100%; + max-width: 350px; + margin-top: 20px; + border: 1px solid var(--glass-border); +} + +.calendar-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + font-family: var(--font-heading); + color: var(--text-main); +} + +.cal-nav-btn { + background: none; + border: none; + color: var(--text-muted); + font-size: 1.2rem; + cursor: pointer; + padding: 5px 10px; + transition: color 0.3s; +} + +.cal-nav-btn:hover { + color: var(--primary-cyan); +} + +.calendar-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 8px; + text-align: center; +} + +.cal-day-label { + color: var(--text-muted); + font-size: 0.7rem; + margin-bottom: 5px; + font-weight: 600; +} + +.cal-day { + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; + border-radius: 8px; + background: rgba(255, 255, 255, 0.02); + color: rgba(255, 255, 255, 0.3); + font-size: 0.9rem; + position: relative; + font-family: var(--font-body); +} + +.cal-day.active { + background: rgba(188, 19, 254, 0.2); + color: #fff; + box-shadow: 0 0 10px rgba(188, 19, 254, 0.2); + border: 1px solid rgba(188, 19, 254, 0.4); +} + +.cal-day.start-date { + background: rgba(0, 243, 255, 0.2); + border-color: var(--primary-cyan); + box-shadow: 0 0 10px rgba(0, 243, 255, 0.3); +} + +.cal-day.today::after { + content: ''; + position: absolute; + bottom: 4px; + width: 4px; + height: 4px; + background: #fff; + border-radius: 50%; +} + +/* --- Modal Styles --- */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(5px); + z-index: 100; + display: flex; + justify-content: center; + align-items: center; + animation: fadeIn 0.3s ease; +} + +.modal-content { + width: 90%; + max-width: 400px; + background: rgba(10, 10, 20, 0.95); + /* Darker for readability */ + border: 1px solid var(--primary-cyan); + box-shadow: 0 0 30px rgba(0, 243, 255, 0.2); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + border-bottom: 1px solid var(--glass-border); + padding-bottom: 10px; +} + +.icon-btn-small { + background: none; + border: none; + color: var(--text-muted); + font-size: 1.5rem; + cursor: pointer; +} + +.setting-group { + margin-bottom: 20px; +} + +.setting-group label { + display: block; + color: var(--text-muted); + margin-bottom: 8px; + font-size: 0.9rem; + letter-spacing: 1px; +} + +.glow-input { + width: 100%; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--glass-border); + color: #fff; + padding: 15px; + border-radius: 12px; + font-size: 1.1rem; + font-family: var(--font-heading); + outline: none; + transition: all 0.3s; +} + +.glow-input:focus { + border-color: var(--primary-cyan); + box-shadow: 0 0 15px rgba(0, 243, 255, 0.2); +} + +.full-width { + width: 100%; + justify-content: center; + margin-top: 10px; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +/* --- Fitness Schedule Styles --- */ +.workout-card { + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--glass-border); + border-radius: 20px; + padding: 20px; + width: 100%; + margin-bottom: 20px; + position: relative; + overflow: hidden; +} + +.glow-card { + box-shadow: 0 0 20px rgba(0, 243, 255, 0.1); + border-color: rgba(0, 243, 255, 0.3); +} + +.card-header { + margin-bottom: 20px; +} + +.day-badge { + font-size: 0.8rem; + color: var(--text-muted); + letter-spacing: 2px; + display: block; + margin-bottom: 5px; +} + +.workout-type { + font-family: var(--font-heading); + font-size: 1.8rem; + line-height: 1.2; + text-transform: uppercase; +} + +.exercise-list { + display: flex; + flex-direction: column; + gap: 15px; +} + +.exercise-item { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + padding-bottom: 10px; +} + +.exercise-item:last-child { + border-bottom: none; +} + +.ex-name { + font-size: 1rem; + color: #e0e0e0; +} + +.ex-meta { + font-family: var(--font-heading); + color: var(--primary-cyan); + font-size: 0.9rem; + text-align: right; +} + +.ex-note { + font-family: var(--font-body); + font-size: 0.8rem; + color: var(--text-muted); + font-weight: normal; +} + +.subsection-title { + font-size: 1rem; + color: var(--text-muted); + letter-spacing: 2px; + margin-bottom: 15px; + margin-top: 10px; +} + +.schedule-list { + width: 100%; +} + +.schedule-row { + display: flex; + justify-content: space-between; + padding: 12px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + font-size: 0.9rem; +} + +.sch-day { + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 1px; +} + +.sch-type { + font-weight: 600; +}/* --- New Fitness Stats Display --- */ +.rings-stats { + display: flex; + justify-content: center; + gap: 40px; + margin-top: 20px; + width: 100%; +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; +} + +.stat-item.cyan .stat-number { + color: var(--primary-cyan); + text-shadow: 0 0 10px rgba(0, 243, 255, 0.4); +} + +.stat-item.purple .stat-number { + color: var(--secondary-purple); + text-shadow: 0 0 10px rgba(188, 19, 254, 0.4); +} + +.stat-label-small { + font-size: 0.75rem; + color: var(--text-muted); + letter-spacing: 2px; + display: flex; + align-items: center; + gap: 6px; +} + +.stat-number { + font-family: var(--font-heading); + font-size: 1.8rem; + font-weight: 700; +} + \ No newline at end of file diff --git a/Hydroflux/app/src/main/assets/icon.png b/Hydroflux/app/src/main/assets/icon.png new file mode 100644 index 0000000..e69de29 diff --git a/Hydroflux/app/src/main/assets/index.html b/Hydroflux/app/src/main/assets/index.html new file mode 100644 index 0000000..98282f2 --- /dev/null +++ b/Hydroflux/app/src/main/assets/index.html @@ -0,0 +1,135 @@ + + + + + + + HydroFit + + + + + + + + + + + + + + + + + + + + + +
+ +
+

HYDROFIT

+
+
+ +
+ +
+ +
Loading Core...
+
+ + + + + + + + +
+ + + + + +
+ + + + \ No newline at end of file diff --git a/Hydroflux/app/src/main/assets/js/app.js b/Hydroflux/app/src/main/assets/js/app.js new file mode 100644 index 0000000..816dccf --- /dev/null +++ b/Hydroflux/app/src/main/assets/js/app.js @@ -0,0 +1,43 @@ +console.log('HydroFlux Initialized'); + +// Simple Navigation Logic + +// Simple Navigation Logic - REPLACED BY NEW BLOCK BELOW + +// Initialize Modules +import { WaterTracker } from './modules/water.js'; +import { StreakTracker } from './modules/streak.js'; +import { FitnessDashboard } from './modules/fitness.js'; +import { StatsDashboard } from './modules/stats.js'; +import { GoalsTracker } from './modules/goals.js'; + +const waterTracker = new WaterTracker('water-section'); +const streakTracker = new StreakTracker('streak-section'); +const fitnessDashboard = new FitnessDashboard('fitness-section'); +const statsDashboard = new StatsDashboard('stats-section'); +const goalsTracker = new GoalsTracker('goals-section'); + +// Navigation Logic with Auto-Refresh +document.querySelectorAll('.nav-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + // Toggle Active State + document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active')); + e.currentTarget.classList.add('active'); + + const view = e.currentTarget.dataset.view; + + // Hide all sections + document.querySelectorAll('section').forEach(el => el.style.display = 'none'); + + // Show target section + const target = document.getElementById(`${view}-section`); + if (target) { + target.style.display = 'block'; + + // Auto-Refresh Stats when viewing + if (view === 'stats') { + statsDashboard.update(); + } + } + }); +}); diff --git a/Hydroflux/app/src/main/assets/js/modules/fitness.js b/Hydroflux/app/src/main/assets/js/modules/fitness.js new file mode 100644 index 0000000..08a9fd9 --- /dev/null +++ b/Hydroflux/app/src/main/assets/js/modules/fitness.js @@ -0,0 +1,332 @@ +export class FitnessDashboard { + constructor(containerId) { + this.container = document.getElementById(containerId); + this.viewDate = new Date(); // Calendar View + + // 1. Activity Ring Data & History + this.data = { + steps: { current: 0, goal: 10000 }, + sleep: { current: 0, goal: 8 }, + history: {} + }; + + // Try to load existing data from storage (So we don't start at 0 on refresh) + const saved = localStorage.getItem('hydroflux_fitness_v2'); + if (saved) { + try { + const parsed = JSON.parse(saved); + this.data.steps.current = parsed.steps || 0; + this.data.sleep.current = parsed.sleep || 0; + this.data.history = parsed.history || {}; + } catch (e) { console.error("Load failed", e); } + } + + // 2. Custom Schedule Configuration + this.schedule = { + "Monday": { + type: "CHEST DAY", + focus: "Strength", + exercises: [ + { name: "Bench Press", sets: "4x8-12", note: "Bench Only" }, + { name: "Push-Ups", sets: "3xFailure", note: "Wide Grip" }, + { name: "Dips", sets: "3x10-15", note: "Use Bench/Chair" }, + { name: "Incline Push-Ups", sets: "3x12", note: "Feet on Bench" } + ], + color: "var(--primary-cyan)" + }, + "Tuesday": { + type: "REST & RECOVERY", + focus: "Recovery", + exercises: [ + { name: "Light Stretch", sets: "10 mins", note: "Full Body" }, + { name: "Hydrate", sets: "3 Liters", note: "Goal" } + ], + color: "var(--text-muted)" + }, + "Wednesday": { + type: "CARDIO", + focus: "Endurance", + exercises: [ + { name: "Running / Jogging", sets: "30 mins", note: "Steady Pace" }, + { name: "Jumping Jacks", sets: "3x1 min", note: "High Intensity" }, + { name: "Burpees", sets: "3x10", note: "Explosive" } + ], + color: "var(--secondary-purple)" + }, + "Thursday": { + type: "CORE STRENGTH", + focus: "Abs & Core", + exercises: [ + { name: "Plank", sets: "3x60s", note: "Hold Steady" }, + { name: "Crunches", sets: "3x20", note: "Slow Control" }, + { name: "Leg Raises", sets: "3x15", note: "Lower Abs focus" }, + { name: "Russian Twists", sets: "3x20", note: "Obliques" } + ], + color: "#ff0055" // Intense Red/Pink for Core + }, + "Friday": { + type: "REST & RECOVERY", + focus: "Recovery", + exercises: [ + { name: "Mobility Work", sets: "15 mins", note: "Joint Focus" }, + { name: "Walk", sets: "20 mins", note: "Light movement" } + ], + color: "var(--text-muted)" + }, + "Saturday": { + type: "FREESTYLE / ACTIVE", + focus: "Fun", + exercises: [ + { name: "Hiking / Sports", sets: "N/A", note: "Enjoy yourself" } + ], + color: "#ffd700" // Gold + }, + "Sunday": { + type: "PREP & REST", + focus: "Recharge", + exercises: [ + { name: "Meal Prep", sets: "Weekly", note: "Nutrition" }, + { name: "Sleep Early", sets: "8h+", note: "Recovery" } + ], + color: "var(--text-muted)" + } + }; + + // 3. Setup Global Listener (Once) + window.updateHealthData = (steps, sleep) => { + // Update Data Model + this.data.steps.current = steps; + this.data.sleep.current = parseFloat(sleep.toFixed(1)); + this.data.sleep.current = parseFloat(sleep.toFixed(1)); + + // Update today in history + const todayKey = new Date().toDateString(); + this.data.history[todayKey] = { + steps: steps, + sleep: this.data.sleep.current + }; + + // Persist for Stats Module (NOW INCLUDES HISTORY) + localStorage.setItem('hydroflux_fitness_v2', JSON.stringify({ + steps: this.data.steps.current, + sleep: this.data.sleep.current, + history: this.data.history + })); + + // Re-render + this.render(); + this.animate(); + this.renderCalendar(); + + const btn = this.container.querySelector('#connect-watch-btn'); + if (btn) { + btn.textContent = "UPDATE"; + btn.style.borderColor = "#00ff00"; // Green success + btn.style.color = "#00ff00"; + } + }; + + this.render(); + // Delay animation to allow DOM paint + setTimeout(() => this.animate(), 100); + + // 4. Auto-Sync on load if Native (Once) + if (window.HydroFluxNative) { + window.HydroFluxNative.requestHealthPermissions(); + } + } + + render() { + // --- Logic for Activity Rings --- + const stepPercent = Math.min((this.data.steps.current / this.data.steps.goal) * 100, 100); + const sleepPercent = Math.min((this.data.sleep.current / this.data.sleep.goal) * 100, 100); + + const center = 100; + const radiusOuter = 80; + const radiusInner = 55; + const circumOuter = 2 * Math.PI * radiusOuter; + const circumInner = 2 * Math.PI * radiusInner; + + // --- Logic for Weekly Schedule --- + const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + const todayIndex = new Date().getDay(); + const todayName = days[todayIndex]; + const plan = this.schedule[todayName]; + + this.container.innerHTML = ` +
+ +

DAILY ACTIVITY

+
+ + + + + + + + + + + +
+
+ STEPS + ${this.data.steps.current.toLocaleString()} +
+
+ SLEEP + ${this.data.sleep.current}h +
+
+
+ +
+
+ TicWatch Pro 5 + Disconnected +
+ +
+ + +

WEEKLY SCHEDULE

+ +
+
+ TODAY: ${todayName.toUpperCase()} +

${plan.type}

+
+
+ ${plan.exercises.map(ex => ` +
+ ${ex.name} + ${ex.sets} • ${ex.note} +
+ `).join('')} +
+
+ +
+

UPCOMING

+ ${days.map((d, i) => { + if (d === todayName) return ''; + const p = this.schedule[d]; + return ` +
+ ${d.substring(0, 3)} + ${p.type} +
+ `; + }).join('')} +
+ + +

HISTORY LOG

+
+
+ + Month + +
+
+
+ +
+ `; + + this.renderCalendar(); + this.attachEvents(); + } + + // --- Calendar Logic --- + changeMonth(offset) { + this.viewDate.setMonth(this.viewDate.getMonth() + offset); + this.renderCalendar(); + } + + renderCalendar() { + const grid = this.container.querySelector('#fit-calendar-grid'); + const monthLabel = this.container.querySelector('#cal-month-label-fit'); + if (!grid || !monthLabel) return; + + grid.innerHTML = ''; + + const year = this.viewDate.getFullYear(); + const month = this.viewDate.getMonth(); + const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; + monthLabel.textContent = `${months[month]} ${year}`; + + const firstDay = new Date(year, month, 1).getDay(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + + // Loop Days + const daysShort = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; + daysShort.forEach(d => grid.innerHTML += `
${d}
`); + for (let i = 0; i < firstDay; i++) grid.innerHTML += `
`; + + const todayStr = new Date().toDateString(); + + for (let i = 1; i <= daysInMonth; i++) { + const dateObj = new Date(year, month, i); + const dateKey = dateObj.toDateString(); + + let classes = 'cal-day'; + + // Check History (Prioritize Steps Goal) + if (this.data.history[dateKey]) { + const record = this.data.history[dateKey]; + // Success = Steps > Goal (10k) OR Sleep > 7h + if (record.steps >= 10000 || record.sleep >= 7) { + classes += ' start-date active'; // Cyan glow (Success) + } else { + classes += ' active'; // Just active (Purple/Normal) + } + } + + if (dateKey === todayStr) classes += ' today'; + + grid.innerHTML += `
${i}
`; + } + } + + animate() { + this.container.querySelectorAll('.ring-progress').forEach(ring => { + ring.style.strokeDashoffset = ring.dataset.offset; + }); + } + + attachEvents() { + const btn = this.container.querySelector('#connect-watch-btn'); + if (btn) { + btn.addEventListener('click', () => { + // Feature Detection for Native Android Bridge + if (window.HydroFluxNative) { + btn.textContent = "REQUESTING..."; + window.HydroFluxNative.requestHealthPermissions(); + + // Fallback reset if no response in 5s + setTimeout(() => { + if (btn.textContent === "REQUESTING...") btn.textContent = "UPDATE"; + }, 5000); + } else { + alert("This feature requires the Android App!"); + } + }); + } + + // Calendar Nav + const prevBtn = this.container.querySelector('#fit-prev-month'); + const nextBtn = this.container.querySelector('#fit-next-month'); + if (prevBtn) prevBtn.addEventListener('click', () => this.changeMonth(-1)); + if (nextBtn) nextBtn.addEventListener('click', () => this.changeMonth(1)); + } +} diff --git a/Hydroflux/app/src/main/assets/js/modules/goals.js b/Hydroflux/app/src/main/assets/js/modules/goals.js new file mode 100644 index 0000000..b8ef0bc --- /dev/null +++ b/Hydroflux/app/src/main/assets/js/modules/goals.js @@ -0,0 +1,123 @@ +export class GoalsTracker { + constructor(containerId) { + this.container = document.getElementById(containerId); + this.STORAGE_KEY = 'hydroflux_goals'; + this.goals = []; + + this.loadState(); + this.render(); + } + + loadState() { + const saved = localStorage.getItem(this.STORAGE_KEY); + if (saved) { + this.goals = JSON.parse(saved); + } else { + // Default Goal Example + this.goals = [ + { id: Date.now(), text: "Drink 3L of Water", completed: false } + ]; + this.saveState(); + } + } + + saveState() { + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.goals)); + } + + addGoal(text) { + if (!text.trim()) return; + const newGoal = { + id: Date.now(), + text: text.trim(), + completed: false + }; + this.goals.push(newGoal); + this.saveState(); + this.render(); + } + + toggleGoal(id) { + const goal = this.goals.find(g => g.id === id); + if (goal) { + goal.completed = !goal.completed; + this.saveState(); + this.render(); + + if (goal.completed && navigator.vibrate) { + navigator.vibrate(50); // Haptic feedback + } + } + } + + deleteGoal(id) { + if (confirm("Delete this goal?")) { + this.goals = this.goals.filter(g => g.id !== id); + this.saveState(); + this.render(); + } + } + + render() { + if (!this.container) return; + + this.container.innerHTML = ` +
+

GOALS

+ +
+ + +
+ +
+ ${this.goals.map(goal => ` +
+
+ ${goal.text} + +
+ `).join('')} + ${this.goals.length === 0 ? '
No active goals
' : ''} +
+
+ `; + + this.attachEvents(); + } + + attachEvents() { + const input = this.container.querySelector('#new-goal-input'); + const addBtn = this.container.querySelector('#add-goal-btn'); + + // Add Logic + const handleAdd = () => { + if (input.value) { + this.addGoal(input.value); + input.value = ''; // Reset + } + }; + + addBtn.addEventListener('click', handleAdd); + input.addEventListener('keypress', (e) => { + if (e.key === 'Enter') handleAdd(); + }); + + // Toggle Logic (Delegate) + this.container.querySelectorAll('.goal-item').forEach(item => { + item.addEventListener('click', (e) => { + // Ignore if clicked delete button + if (e.target.classList.contains('delete-goal-btn')) return; + this.toggleGoal(parseInt(item.dataset.id)); + }); + }); + + // Delete Logic + this.container.querySelectorAll('.delete-goal-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + this.deleteGoal(parseInt(btn.dataset.id)); + }); + }); + } +} diff --git a/Hydroflux/app/src/main/assets/js/modules/stats.js b/Hydroflux/app/src/main/assets/js/modules/stats.js new file mode 100644 index 0000000..1ddf40f --- /dev/null +++ b/Hydroflux/app/src/main/assets/js/modules/stats.js @@ -0,0 +1,257 @@ + +export class StatsDashboard { + constructor(containerId) { + this.container = document.getElementById(containerId); + this.render(); + } + + + getStreakData() { + const saved = localStorage.getItem('hydroflux_streak'); + if (saved) { + const start = new Date(parseInt(saved)); + const now = new Date(); + const diff = now - start; + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + return days < 0 ? 0 : days; + } + return 0; + } + + getFitnessData() { + const saved = localStorage.getItem('hydroflux_fitness_v2'); + if (saved) { + const parsed = JSON.parse(saved); + return { + steps: parsed.steps || 0, + sleep: parsed.sleep || 0, + history: parsed.history || {} + }; + } + return { steps: 0, sleep: 0, history: {} }; + } + + calculateScores() { + const waterData = this.getWaterDataRaw(); // Need raw history access + const streakDays = this.getStreakData(); + const fitness = this.getFitnessData(); + + // --- ENTROPY SCORING SYSTEM (Infinite & Decay) --- + + // 1. LIQUID LEVEL (Historical Volume - Time Decay) + // You gain +1 XP for every 100mL you drink. + // You lose -20 XP for every day since you started. + // Result: You must drink > 2000mL/day just to maintain your level. + + // Calculate Total Volume from History + let totalVolume = 0; + let firstDate = new Date(); + const history = waterData.history || {}; + const dates = Object.keys(history); + + if (dates.length > 0) { + // Find earliest date + firstDate = new Date(dates.sort((a, b) => new Date(a) - new Date(b))[0]); + + // Sum Volume + Object.values(history).forEach(day => { + totalVolume += (day.current || 0); + }); + } + + // Add Today's Volume (in case it's not in history yet) + totalVolume += waterData.current; + + // Calculate Days Since Start + const now = new Date(); + const msPerDay = 1000 * 60 * 60 * 24; + const daysSinceStart = Math.max(1, Math.floor((now - firstDate) / msPerDay)); + + // Liquid Math + const volumeXP = Math.floor(totalVolume / 100); // 1 pt per 100mL + const entropyTax = daysSinceStart * 25; // Tax: 25 pts/day (Need 2.5L to break even) + + let liquidLevel = volumeXP - entropyTax; + + // 2. WILLPOWER LEVEL (Streak * Multiplier) + // High risk, high reward. + // 1 Day = 50 Levels. + // Breaking streak = Instant loss of all these levels. + let willpowerLevel = streakDays * 50; + + // 3. FIT LEVEL (Cumulative Persistence) + // Logic: Sum of all historical performance - Daily Tax + // Good day (+XP) > Tax = Level Up + // Bad day (+0) < Tax = Level Down + + let cumulativeFitXP = 0; + const fitHistory = fitness.history || {}; + + // Sum all historical points + Object.values(fitHistory).forEach(day => { + const daySteps = day.steps || 0; + const daySleep = day.sleep || 0; + + // Formula: 1pt per 500 steps + 4pts per hour sleep + // e.g. 10k steps (20) + 8h sleep (32) = 52 pts/day + cumulativeFitXP += Math.floor(daySteps / 500) + Math.floor(daySleep * 4); + }); + + // Add current day if not in history yet + const todayKey = new Date().toDateString(); + if (!fitHistory[todayKey]) { + cumulativeFitXP += Math.floor(fitness.steps / 500) + Math.floor(fitness.sleep * 4); + } + + // Apply Entropy Tax + // You lose 30 XP per day you've been active. + const fitTax = daysSinceStart * 30; + + let fitLevel = cumulativeFitXP - fitTax; + + // 4. TOTAL HYDRO LEVEL + let hydroLevel = liquidLevel + willpowerLevel + fitLevel; + + // Prevent Negative Display (though internal math is negative) + // Actually, user wants to go down. Negative levels are valid shame indicators? + // "Go down on levels" -> usually implies dropping from 50 to 40. + // If we allow negative, it might be discouraging. Let's floor at 0. + // But the user said "harder to progress", implying you can be in a deficit. + // Let's allow negative but style it red? No, let's keep floor at 0 for MVP. + if (hydroLevel < 0) hydroLevel = 0; + if (liquidLevel < 0) liquidLevel = 0; + // Fit level can be negative (dragging you down), but we display it unsigned usually. + // Let's allow the components to sum naturally, but display 0 if total is < 0. + + return { + liquid: liquidLevel, + willpower: willpowerLevel, + fit: fitLevel, + total: hydroLevel, + details: { + daysActive: daysSinceStart, + totalLiters: (totalVolume / 1000).toFixed(1), + dailyTax: 25 + } + }; + } + + getWaterDataRaw() { + const saved = localStorage.getItem('hydroflux_data'); + if (saved) { + return JSON.parse(saved); + } + return { current: 0, goal: 3000, history: {} }; + } + + update() { + this.render(); + } + + render() { + const scores = this.calculateScores(); + + // Progress Bar Calculation Logic + const liquidPct = Math.min(100, (scores.details.waterCurrent / 2500) * 100); // 2.5L is break even + const fitPct = Math.min(100, Math.max(0, scores.fit * 5)); // Scaling fit level to bar (approx) + + this.container.innerHTML = ` +
+ +
+
+ + + + +
+ LEVEL + ${scores.total} +
+
+
+ +
+ + +
+
+
+
+ +
+
+ LIQUID LEVEL +
Daily Maintenance
+
+
+ ${scores.liquid} +
+ + +
+
+
+
+ Tax: -${scores.details.dailyTax}/day + ${Math.round(liquidPct)}% Safe +
+
+ + +
+
+
+
+ +
+
+ WILLPOWER +
Streak Intensity
+
+
+ ${scores.willpower} +
+ + +
+
+
+
+ Streak: ${scores.details.daysActive} Days + ACTIVE +
+
+ + +
+
+
+
+ +
+
+ FIT LEVEL +
Daily Activity
+
+
+ ${scores.fit} +
+ + +
+
+
+
+ Status + ${scores.fit < 0 ? 'PENALTY' : 'BOOSTING'} +
+
+ +
+
+ `; + } +} + diff --git a/Hydroflux/app/src/main/assets/js/modules/streak.js b/Hydroflux/app/src/main/assets/js/modules/streak.js new file mode 100644 index 0000000..977048c --- /dev/null +++ b/Hydroflux/app/src/main/assets/js/modules/streak.js @@ -0,0 +1,196 @@ +export class StreakTracker { + constructor(containerId) { + this.container = document.getElementById(containerId); + this.STORAGE_KEY = 'hydroflux_streak'; + this.quotes = [ + "The only easy day was yesterday.", + "Discipline is doing what needs to be done, even if you don't want to.", + "Your future self is watching you right now through memories.", + "Pain is temporary. Quitting lasts forever.", + "Suffering is the currency of success.", + "Don't stop when you're tired. Stop when you're done.", + "You are stronger than your urges.", + "Focus on the goal, not the obstacle." + ]; + + this.viewDate = new Date(); // For Calendar Navigation + this.loadState(); + this.render(); + this.startTimer(); + } + + loadState() { + const saved = localStorage.getItem(this.STORAGE_KEY); + if (saved) { + this.startDate = new Date(parseInt(saved)); + } else { + this.startDate = new Date(); + this.saveState(); + } + } + + saveState() { + localStorage.setItem(this.STORAGE_KEY, this.startDate.getTime().toString()); + } + + resetStreak() { + if (confirm("Are you sure you want to reset your streak?")) { + this.startDate = new Date(); + this.saveState(); + this.updateUI(); + this.renderCalendar(); // Refresh calendar + + // Haptic Bad Feedback + if (navigator.vibrate) navigator.vibrate([100, 50, 100]); + } + } + + getDuration() { + const now = new Date(); + const diff = now - this.startDate; + + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); + + return { days, hours, minutes }; + } + + updateUI() { + const { days, hours, minutes } = this.getDuration(); + + const daysEl = this.container.querySelector('.streak-days'); + const detailEl = this.container.querySelector('.streak-detail'); + + if (daysEl) daysEl.textContent = days; + if (detailEl) detailEl.textContent = `${hours}h ${minutes}m`; + } + + startTimer() { + setInterval(() => this.updateUI(), 60000); // Update every minute + } + + getRandomQuote() { + return this.quotes[Math.floor(Math.random() * this.quotes.length)]; + } + + changeMonth(offset) { + this.viewDate.setMonth(this.viewDate.getMonth() + offset); + this.renderCalendar(); + } + + renderCalendar() { + const grid = this.container.querySelector('.calendar-grid'); + const monthLabel = this.container.querySelector('#cal-month-label'); + if (!grid || !monthLabel) return; + + grid.innerHTML = ''; + + const year = this.viewDate.getFullYear(); + const month = this.viewDate.getMonth(); + + // Month Names + const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; + monthLabel.textContent = `${months[month]} ${year}`; + + // Date Logic + const firstDay = new Date(year, month, 1).getDay(); // 0 = Sunday + const daysInMonth = new Date(year, month + 1, 0).getDate(); + + // Adjust for Monday start if preferred, but let's stick to Sun=0 for standard + const paddingDays = firstDay; + + // Labels + const daysShort = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; + daysShort.forEach(d => { + grid.innerHTML += `
${d}
`; + }); + + // Padding + for (let i = 0; i < paddingDays; i++) { + grid.innerHTML += `
`; + } + + const now = new Date(); + const todayStr = now.toDateString(); + const startStr = this.startDate.toDateString(); + + // Days + for (let i = 1; i <= daysInMonth; i++) { + const currentObj = new Date(year, month, i); + const currentStr = currentObj.toDateString(); + + let classes = 'cal-day'; + + // Check if within streak (Active) + // Active if: current >= startDate AND current <= now + if (currentObj >= this.startDate && currentObj <= now) { + // Determine if it's the exact start date for special styling + if (currentStr === startStr) { + classes += ' start-date active'; + } else { + classes += ' active'; + } + } else if (currentStr === startStr) { + // Even if in future (impossible logic wise but for safety) or just start + classes += ' start-date'; + } + + // Today marker + if (currentStr === todayStr) { + classes += ' today'; + } + + grid.innerHTML += `
${i}
`; + } + } + + render() { + this.container.innerHTML = ` +
+

QUIT STREAK

+ +
+
0
+
DAYS
+
0h 0m
+
+ + +
+
+ + Month + +
+
+ +
+
+ +
+ "${this.getRandomQuote()}" +
+ + +
+ `; + + this.updateUI(); + this.renderCalendar(); + + this.container.querySelector('#reset-streak-btn').addEventListener('click', () => { + this.resetStreak(); + }); + + this.container.querySelector('#prev-month').addEventListener('click', () => { + this.changeMonth(-1); + }); + + this.container.querySelector('#next-month').addEventListener('click', () => { + this.changeMonth(1); + }); + } +} diff --git a/Hydroflux/app/src/main/assets/js/modules/water.js b/Hydroflux/app/src/main/assets/js/modules/water.js new file mode 100644 index 0000000..4db1bdc --- /dev/null +++ b/Hydroflux/app/src/main/assets/js/modules/water.js @@ -0,0 +1,319 @@ +export class WaterTracker { + constructor(containerId) { + this.container = document.getElementById(containerId); + this.state = { + current: 0, + goal: 3000, + bottleSize: 500, + lastActiveDate: new Date().toISOString(), + history: {} // Format: "YYYY-MM-DD": { current: 2000, goal: 3000 } + }; + this.STORAGE_KEY = 'hydroflux_data'; + this.viewDate = new Date(); // For Calendar + + this.loadState(); + this.render(); + this.attachEvents(); + this.updateUI(); + this.setupSettings(); // New Settings Logic + } + + loadState() { + const saved = localStorage.getItem(this.STORAGE_KEY); + if (saved) { + const parsed = JSON.parse(saved); + this.state = { ...this.state, ...parsed }; + if (!this.state.history) this.state.history = {}; // Safety init + + // Check for 1AM Reset + this.checkDailyReset(); + } + } + + checkDailyReset() { + const now = new Date(); + const lastDate = this.state.lastActiveDate ? new Date(this.state.lastActiveDate) : new Date(0); + + // Logic: if it is a NEW day (past 1am), reset. + if (now.toDateString() !== lastDate.toDateString()) { + if (now.getHours() >= 1) { + this.state.current = 0; + } + } + + this.state.lastActiveDate = now.toISOString(); + this.saveState(); + } + + saveState() { + // Record History for Today + const todayKey = new Date().toDateString(); // "Fri Dec 26 2025" logic + this.state.history[todayKey] = { + current: this.state.current, + goal: this.state.goal + }; + + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.state)); + this.updateUI(); // Updates UI and Calendar + this.renderCalendar(); + } + + addWater() { + this.state.current += this.state.bottleSize; + this.saveState(); + if (navigator.vibrate) navigator.vibrate(50); + } + + removeWater() { + this.state.current = Math.max(0, this.state.current - this.state.bottleSize); + this.saveState(); + if (navigator.vibrate) navigator.vibrate(50); + } + + setBottleSize(size) { + if (!size || size <= 0) return; + this.state.bottleSize = size; + this.saveState(); + // Update input field if visible + const input = document.getElementById('bottle-size-input'); + if (input) input.value = size; + } + + setGoal(goal) { + if (!goal || goal <= 0) return; + this.state.goal = goal; + this.saveState(); + } + + getPercentage() { + return Math.min(100, Math.max(0, (this.state.current / this.state.goal) * 100)); + } + + updateUI() { + // Update Text + const currentEl = this.container.querySelector('.water-count'); + const percentageEl = this.container.querySelector('.water-percentage'); + + if (currentEl) currentEl.textContent = `${this.state.current} / ${this.state.goal} mL`; + if (percentageEl) percentageEl.textContent = `${Math.round(this.getPercentage())}%`; + + // Update Wave Animation + const wave = this.container.querySelector('.wave'); + if (wave) { + wave.style.top = `${100 - this.getPercentage()}%`; + } + } + + // --- Calendar Logic --- + changeMonth(offset) { + this.viewDate.setMonth(this.viewDate.getMonth() + offset); + this.renderCalendar(); + } + + renderCalendar() { + const grid = this.container.querySelector('.calendar-grid'); + const monthLabel = this.container.querySelector('#cal-month-label-water'); + if (!grid || !monthLabel) return; + + grid.innerHTML = ''; + + const year = this.viewDate.getFullYear(); + const month = this.viewDate.getMonth(); + const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; + monthLabel.textContent = `${months[month]} ${year}`; + + const firstDay = new Date(year, month, 1).getDay(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + + // Loop Days + const daysShort = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; + daysShort.forEach(d => grid.innerHTML += `
${d}
`); + for (let i = 0; i < firstDay; i++) grid.innerHTML += `
`; + + const todayStr = new Date().toDateString(); + + for (let i = 1; i <= daysInMonth; i++) { + const dateObj = new Date(year, month, i); + const dateKey = dateObj.toDateString(); + + let classes = 'cal-day'; + + // Check History + if (this.state.history[dateKey]) { + const record = this.state.history[dateKey]; + if (record.current >= record.goal) { + classes += ' start-date active'; // Cyan glow for success + } else if (record.current > 0) { + // Partial highlight could go here, but let's keep it simple + classes += ' active'; // Purple fallback or just active + } + } + + if (dateKey === todayStr) classes += ' today'; + + grid.innerHTML += `
${i}
`; + } + } + + render() { + this.container.innerHTML = ` +
+ +
+
+
+
+ 0% + HYDRATION +
+
+
+ + +
+ 0 / 3000 mL +
+ + +
+
+ + +
+ +
+ + + + + +
+
+ + +
+
+ + Month + +
+
+
+
+ `; + + this.checkNotificationStatus(); + this.renderCalendar(); + } + + attachEvents() { + this.container.querySelector('#add-water-btn').addEventListener('click', () => this.addWater()); + this.container.querySelector('#remove-water-btn').addEventListener('click', () => this.removeWater()); + + this.container.querySelector('#notify-btn').addEventListener('click', (e) => this.toggleNotifications(e.currentTarget)); + + this.container.querySelector('#bottle-size-input').addEventListener('change', (e) => { + this.setBottleSize(parseInt(e.target.value)); + }); + + // Calendar Nav + this.container.querySelector('#water-prev-month').addEventListener('click', () => this.changeMonth(-1)); + this.container.querySelector('#water-next-month').addEventListener('click', () => this.changeMonth(1)); + } + + // --- Settings Modal Logic --- + setupSettings() { + const modal = document.getElementById('settings-modal'); + const openBtn = document.getElementById('open-settings-btn'); + const closeBtn = document.getElementById('close-settings-btn'); + const saveBtn = document.getElementById('save-settings-btn'); + + const goalInput = document.getElementById('setting-goal-input'); + const bottleInput = document.getElementById('setting-bottle-input'); + + if (openBtn) { + openBtn.addEventListener('click', () => { + // Populate inputs + goalInput.value = this.state.goal; + bottleInput.value = this.state.bottleSize; + modal.style.display = 'flex'; + }); + } + + if (closeBtn) { + closeBtn.addEventListener('click', () => { + modal.style.display = 'none'; + }); + } + + if (saveBtn) { + saveBtn.addEventListener('click', () => { + const newGoal = parseInt(goalInput.value); + const newBottle = parseInt(bottleInput.value); + + if (newGoal) this.setGoal(newGoal); + if (newBottle) this.setBottleSize(newBottle); + + modal.style.display = 'none'; + alert("Settings Saved!"); + }); + } + } + + // --- Notification Logic --- + toggleNotifications(btn) { + if (!("Notification" in window)) { + alert("Notifications coming soon to the Android App version!"); + return; + } + + if (Notification.permission === "granted") { + alert("Reminders are active! We'll check every hour."); + } else if (Notification.permission !== "denied") { + Notification.requestPermission().then(permission => { + if (permission === "granted") { + this.startReminderLoop(); + btn.style.color = "var(--primary-cyan)"; + new Notification("HydroFlux", { body: "Smart Hydration Reminders Enabled!" }); + } + }); + } + } + + checkNotificationStatus() { + if (!('Notification' in window)) return; + + if (Notification.permission === "granted") { + const btn = this.container.querySelector('#notify-btn'); + if (btn) btn.style.color = "var(--primary-cyan)"; + this.startReminderLoop(); + } + } + + startReminderLoop() { + if (!('Notification' in window)) return; + + // Clear existing to avoid duplicates + if (this.reminderInterval) clearInterval(this.reminderInterval); + + // Check every minute if it's been > 1 hour since last drink + this.reminderInterval = setInterval(() => { + // Pseudo-logic check since we don't store timestamp in this simple version yet + // In a real app, you'd check this.state.lastDrinkTime + new Notification("HydroFlux Needs You", { + body: "Remember to drink water!", + icon: "/icon.png" + }); + }, 3600000); // 1 Hour + } +} diff --git a/Hydroflux/app/src/main/assets/manifest.json b/Hydroflux/app/src/main/assets/manifest.json new file mode 100644 index 0000000..c03643e --- /dev/null +++ b/Hydroflux/app/src/main/assets/manifest.json @@ -0,0 +1,17 @@ +{ + "name": "HydroFit", + "short_name": "HydroFit", + "background_color": "#050508", + "theme_color": "#050508", + "display": "standalone", + "orientation": "portrait", + "scope": "/", + "start_url": "/", + "icons": [ + { + "src": "favicon.svg", + "sizes": "192x192", + "type": "image/svg+xml" + } + ] +} \ No newline at end of file diff --git a/Hydroflux/app/src/main/ic_launcher-playstore.png b/Hydroflux/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..264fd7d Binary files /dev/null and b/Hydroflux/app/src/main/ic_launcher-playstore.png differ diff --git a/Hydroflux/app/src/main/java/com/david/hydroflux/MainActivity.kt b/Hydroflux/app/src/main/java/com/david/hydroflux/MainActivity.kt new file mode 100644 index 0000000..ff7e8ad --- /dev/null +++ b/Hydroflux/app/src/main/java/com/david/hydroflux/MainActivity.kt @@ -0,0 +1,233 @@ +package com.david.hydroflux + +import android.annotation.SuppressLint +import android.os.Bundle +import android.webkit.JavascriptInterface +import android.webkit.WebView +import android.webkit.WebViewClient +import android.webkit.WebChromeClient +import android.widget.Toast // Added for Debugging +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope +import androidx.health.connect.client.HealthConnectClient +import androidx.health.connect.client.permission.HealthPermission +import androidx.health.connect.client.records.StepsRecord +import androidx.health.connect.client.records.SleepSessionRecord +import androidx.health.connect.client.request.ReadRecordsRequest +import androidx.health.connect.client.request.AggregateRequest +import androidx.health.connect.client.time.TimeRangeFilter +import kotlinx.coroutines.launch +import java.time.Instant +import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit + +class MainActivity : FragmentActivity() { + private lateinit var myWebView: WebView + private lateinit var healthConnectClient: HealthConnectClient + + // Define Permissions we need + private val PERMISSIONS = setOf( + HealthPermission.getReadPermission(StepsRecord::class), + HealthPermission.getReadPermission(SleepSessionRecord::class) + ) + + private val requestPermissions = registerForActivityResult( + androidx.health.connect.client.PermissionController.createRequestPermissionResultContract() + ) { granted -> + if (granted.containsAll(PERMISSIONS)) { + // Permissions Granted - Silent Sync + if (::healthConnectClient.isInitialized) { + // Use a slight delay to allow permission propogation + android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ + WebAppInterface(this@MainActivity).triggerSync() + }, 500) + } + } else { + val missing = PERMISSIONS.minus(granted) + Toast.makeText(this, "Denied. Opening Settings...", Toast.LENGTH_LONG).show() + // Fallback: Open Health Connect Settings so user can enable manually + try { + val intent = android.content.Intent("androidx.health.ACTION_HEALTH_CONNECT_SETTINGS") + startActivity(intent) + } catch (e: Exception) { + // Try generic app details + Toast.makeText(this, "Could not open Health Settings", Toast.LENGTH_SHORT).show() + val intent = android.content.Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + intent.data = android.net.Uri.parse("package:$packageName") + startActivity(intent) + } + } + } + + @SuppressLint("SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + // Setup WebView FIRST to ensure we have it + myWebView = findViewById(R.id.webview) + myWebView.settings.javaScriptEnabled = true + myWebView.settings.domStorageEnabled = true + myWebView.settings.allowFileAccess = true + myWebView.settings.allowFileAccessFromFileURLs = true + myWebView.settings.allowUniversalAccessFromFileURLs = true + myWebView.webViewClient = WebViewClient() + myWebView.webChromeClient = WebChromeClient() + myWebView.addJavascriptInterface(WebAppInterface(this), "HydroFluxNative") + myWebView.loadUrl("file:///android_asset/index.html") + + // Setup Health Connect + val availabilityStatus = HealthConnectClient.getSdkStatus(this) + + if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE) { + Toast.makeText(this, "Health Connect Unavailable on this device", Toast.LENGTH_LONG).show() + return + } + if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED) { + Toast.makeText(this, "Health Connect Update Required", Toast.LENGTH_LONG).show() + return + } + + // Grab client + try { + healthConnectClient = HealthConnectClient.getOrCreate(this) + } catch (e: Exception) { + e.printStackTrace() + } + + // Start Periodic Sleep Check (6am - 10am) + startPeriodicCheck() + } + + private fun startPeriodicCheck() { + val handler = android.os.Handler(android.os.Looper.getMainLooper()) + val runnable = object : Runnable { + override fun run() { + val cal = java.util.Calendar.getInstance() + val hour = cal.get(java.util.Calendar.HOUR_OF_DAY) + + // Check between 6:00 and 10:00 + if (hour in 6..10) { + WebAppInterface(this@MainActivity).triggerSync() + } + + // Repeat every 30 minutes (30 * 60 * 1000) + handler.postDelayed(this, 1800000) + } + } + handler.post(runnable) // Start immediately + } + + inner class WebAppInterface(private val activity: MainActivity) { + @JavascriptInterface + fun requestHealthPermissions() { + runOnUiThread { + if (!::healthConnectClient.isInitialized) { + Toast.makeText(activity, "Health Client Not Init", Toast.LENGTH_SHORT).show() + return@runOnUiThread + } + + activity.lifecycleScope.launch { + try { + val granted = healthConnectClient.permissionController.getGrantedPermissions() + + if (granted.containsAll(PERMISSIONS)) { + triggerSync() + } else { + try { + requestPermissions.launch(PERMISSIONS) + } catch (e: Exception) { + val err = e.message ?: "Unknown" + Toast.makeText(activity, "Launch Error: $err", Toast.LENGTH_LONG).show() + e.printStackTrace() + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + } + + @JavascriptInterface + fun triggerSync() { + if (!::healthConnectClient.isInitialized) return + + activity.lifecycleScope.launch { + // MOVE TO IO THREAD for stability (Prevents IllegalStateException) + kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { + + // Use Application Context for Client + try { + if (!::healthConnectClient.isInitialized) { + healthConnectClient = HealthConnectClient.getOrCreate(activity.applicationContext) + } + } catch (e: Exception) { + e.printStackTrace() + } + + val startOfDay = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS).toInstant() + val now = Instant.now() + + var totalSteps = 0L + var sleepHours = 0.0 + + // 1. Try Read Steps (AGGREGATE) + try { + val response = healthConnectClient.aggregate( + AggregateRequest( + metrics = setOf(StepsRecord.COUNT_TOTAL), + timeRangeFilter = TimeRangeFilter.between(startOfDay, now) + ) + ) + // The result may be null if no data + totalSteps = response[StepsRecord.COUNT_TOTAL] ?: 0 + } catch (e: Exception) { + e.printStackTrace() + val msg = e.message ?: "Unknown Error" + runOnUiThread { Toast.makeText(activity, "Steps Error: $msg", Toast.LENGTH_LONG).show() } + totalSteps = 0 + } + + // 2. Try Read Sleep (AGGREGATE) + try { + val sleepResponse = healthConnectClient.aggregate( + AggregateRequest( + metrics = setOf(SleepSessionRecord.SLEEP_DURATION_TOTAL), + timeRangeFilter = TimeRangeFilter.between(now.minus(24, ChronoUnit.HOURS), now) + ) + ) + // Get total duration in Seconds (Duration -> Seconds) + val duration = sleepResponse[SleepSessionRecord.SLEEP_DURATION_TOTAL] + if (duration != null) { + sleepHours = duration.seconds / 3600.0 + } + } catch (e: Exception) { + e.printStackTrace() + sleepHours = 0.0 + } + + runOnUiThread { + if (!activity.isFinishing && !activity.isDestroyed) { + try { + myWebView.evaluateJavascript("window.updateHealthData($totalSteps, $sleepHours)", null) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + } + } + } + } + + @Suppress("DEPRECATION") + @Deprecated("Deprecated in Java") + override fun onBackPressed() { + if (myWebView.canGoBack()) { + myWebView.goBack() + } else { + super.onBackPressed() + } + } +} \ No newline at end of file diff --git a/Hydroflux/app/src/main/java/com/david/hydroflux/PermissionsRationaleActivity.kt b/Hydroflux/app/src/main/java/com/david/hydroflux/PermissionsRationaleActivity.kt new file mode 100644 index 0000000..32a5500 --- /dev/null +++ b/Hydroflux/app/src/main/java/com/david/hydroflux/PermissionsRationaleActivity.kt @@ -0,0 +1,54 @@ +package com.david.hydroflux + +import android.os.Bundle +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Button +import android.view.Gravity +import android.graphics.Color +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity + +class PermissionsRationaleActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val title = TextView(this).apply { + text = "Health Permission Needed" + textSize = 24f + setTextColor(Color.CYAN) + gravity = Gravity.CENTER + setPadding(0, 0, 0, 30) + } + + val message = TextView(this).apply { + text = "HydroFit needs access to your Steps and Sleep data to populate the activity rings and history charts.\n\nWe do not share this data." + textSize = 16f + setTextColor(Color.WHITE) + gravity = Gravity.CENTER + setPadding(0, 0, 0, 50) + } + + val btn = Button(this).apply { + text = "UNDERSTOOD" + setBackgroundColor(Color.DKGRAY) + setTextColor(Color.WHITE) + setOnClickListener { + finish() // Close rationale and return to system dialog logic + } + } + + val layout = LinearLayout(this).apply { + orientation = LinearLayout.VERTICAL + gravity = Gravity.CENTER + setPadding(50, 50, 50, 50) + setBackgroundColor(Color.BLACK) + addView(title) + addView(message) + addView(btn) + } + + setContentView(layout) + } +} diff --git a/Hydroflux/app/src/main/java/com/david/hydroflux/ui/theme/Color.kt b/Hydroflux/app/src/main/java/com/david/hydroflux/ui/theme/Color.kt new file mode 100644 index 0000000..e0e2d4b --- /dev/null +++ b/Hydroflux/app/src/main/java/com/david/hydroflux/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.david.hydroflux.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/Hydroflux/app/src/main/java/com/david/hydroflux/ui/theme/Theme.kt b/Hydroflux/app/src/main/java/com/david/hydroflux/ui/theme/Theme.kt new file mode 100644 index 0000000..34f009d --- /dev/null +++ b/Hydroflux/app/src/main/java/com/david/hydroflux/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package com.david.hydroflux.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun HydrofluxTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/Hydroflux/app/src/main/java/com/david/hydroflux/ui/theme/Type.kt b/Hydroflux/app/src/main/java/com/david/hydroflux/ui/theme/Type.kt new file mode 100644 index 0000000..fb9882b --- /dev/null +++ b/Hydroflux/app/src/main/java/com/david/hydroflux/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.david.hydroflux.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/Hydroflux/app/src/main/res/drawable/ic_launcher_background.xml b/Hydroflux/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..ca3826a --- /dev/null +++ b/Hydroflux/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Hydroflux/app/src/main/res/drawable/ic_launcher_foreground.xml b/Hydroflux/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/Hydroflux/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Hydroflux/app/src/main/res/layout/activity_main.xml b/Hydroflux/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..0831055 --- /dev/null +++ b/Hydroflux/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/Hydroflux/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/Hydroflux/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..c4a603d --- /dev/null +++ b/Hydroflux/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Hydroflux/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/Hydroflux/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..c4a603d --- /dev/null +++ b/Hydroflux/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Hydroflux/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/Hydroflux/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..90d7ee5 Binary files /dev/null and b/Hydroflux/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/Hydroflux/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/Hydroflux/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..554293e Binary files /dev/null and b/Hydroflux/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/Hydroflux/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/Hydroflux/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..f644a3d Binary files /dev/null and b/Hydroflux/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/Hydroflux/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/Hydroflux/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..bc62eff Binary files /dev/null and b/Hydroflux/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/Hydroflux/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/Hydroflux/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..2ca6f3e Binary files /dev/null and b/Hydroflux/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/Hydroflux/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/Hydroflux/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..0da66a8 Binary files /dev/null and b/Hydroflux/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/Hydroflux/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/Hydroflux/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..e747d15 Binary files /dev/null and b/Hydroflux/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/Hydroflux/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/Hydroflux/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..729a626 Binary files /dev/null and b/Hydroflux/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/Hydroflux/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/Hydroflux/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..8765096 Binary files /dev/null and b/Hydroflux/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/Hydroflux/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/Hydroflux/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..2139a86 Binary files /dev/null and b/Hydroflux/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/Hydroflux/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/Hydroflux/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..7d2081a Binary files /dev/null and b/Hydroflux/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/Hydroflux/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/Hydroflux/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..c976ee4 Binary files /dev/null and b/Hydroflux/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/Hydroflux/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/Hydroflux/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..99921fc Binary files /dev/null and b/Hydroflux/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/Hydroflux/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/Hydroflux/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..1d5a2fa Binary files /dev/null and b/Hydroflux/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/Hydroflux/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/Hydroflux/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..876d7f0 Binary files /dev/null and b/Hydroflux/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/Hydroflux/app/src/main/res/values/colors.xml b/Hydroflux/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/Hydroflux/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/Hydroflux/app/src/main/res/values/strings.xml b/Hydroflux/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..440aea7 --- /dev/null +++ b/Hydroflux/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + HydroFit + \ No newline at end of file diff --git a/Hydroflux/app/src/main/res/values/themes.xml b/Hydroflux/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..bbecd3f --- /dev/null +++ b/Hydroflux/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +