Final
This commit is contained in:
parent
7cd246cea6
commit
97ef889d46
203
.idea/other.xml
203
.idea/other.xml
|
@ -3,6 +3,17 @@
|
||||||
<component name="direct_access_persist.xml">
|
<component name="direct_access_persist.xml">
|
||||||
<option name="deviceSelectionList">
|
<option name="deviceSelectionList">
|
||||||
<list>
|
<list>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="Sony" />
|
||||||
|
<option name="codename" value="A402SO" />
|
||||||
|
<option name="id" value="A402SO" />
|
||||||
|
<option name="manufacturer" value="Sony" />
|
||||||
|
<option name="name" value="Xperia 10" />
|
||||||
|
<option name="screenDensity" value="450" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2520" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="27" />
|
<option name="api" value="27" />
|
||||||
<option name="brand" value="DOCOMO" />
|
<option name="brand" value="DOCOMO" />
|
||||||
|
@ -14,6 +25,17 @@
|
||||||
<option name="screenX" value="720" />
|
<option name="screenX" value="720" />
|
||||||
<option name="screenY" value="1280" />
|
<option name="screenY" value="1280" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="OnePlus" />
|
||||||
|
<option name="codename" value="OP535DL1" />
|
||||||
|
<option name="id" value="OP535DL1" />
|
||||||
|
<option name="manufacturer" value="OnePlus" />
|
||||||
|
<option name="name" value="CPH2409" />
|
||||||
|
<option name="screenDensity" value="401" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2412" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
<option name="brand" value="OnePlus" />
|
<option name="brand" value="OnePlus" />
|
||||||
|
@ -49,14 +71,14 @@
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
<option name="brand" value="Lenovo" />
|
<option name="brand" value="samsung" />
|
||||||
<option name="codename" value="TB370FU" />
|
<option name="codename" value="a14m" />
|
||||||
<option name="id" value="TB370FU" />
|
<option name="id" value="a14m" />
|
||||||
<option name="manufacturer" value="Lenovo" />
|
<option name="manufacturer" value="Samsung" />
|
||||||
<option name="name" value="Tab P12" />
|
<option name="name" value="SM-A145R" />
|
||||||
<option name="screenDensity" value="340" />
|
<option name="screenDensity" value="450" />
|
||||||
<option name="screenX" value="1840" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2944" />
|
<option name="screenY" value="2408" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
|
@ -69,6 +91,17 @@
|
||||||
<option name="screenX" value="1080" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2340" />
|
<option name="screenY" value="2340" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="a15x" />
|
||||||
|
<option name="id" value="a15x" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="A15 5G" />
|
||||||
|
<option name="screenDensity" value="450" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2340" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
<option name="brand" value="samsung" />
|
<option name="brand" value="samsung" />
|
||||||
|
@ -91,17 +124,6 @@
|
||||||
<option name="screenX" value="1080" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2340" />
|
<option name="screenY" value="2340" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="31" />
|
|
||||||
<option name="brand" value="samsung" />
|
|
||||||
<option name="codename" value="a51" />
|
|
||||||
<option name="id" value="a51" />
|
|
||||||
<option name="manufacturer" value="Samsung" />
|
|
||||||
<option name="name" value="Galaxy A51" />
|
|
||||||
<option name="screenDensity" value="420" />
|
|
||||||
<option name="screenX" value="1080" />
|
|
||||||
<option name="screenY" value="2400" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
|
@ -146,6 +168,17 @@
|
||||||
<option name="screenX" value="1440" />
|
<option name="screenX" value="1440" />
|
||||||
<option name="screenY" value="3088" />
|
<option name="screenY" value="3088" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="b6q" />
|
||||||
|
<option name="id" value="b6q" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Flip 6" />
|
||||||
|
<option name="screenDensity" value="340" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2640" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="32" />
|
<option name="api" value="32" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
|
@ -168,6 +201,17 @@
|
||||||
<option name="screenX" value="960" />
|
<option name="screenX" value="960" />
|
||||||
<option name="screenY" value="2142" />
|
<option name="screenY" value="2142" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="35" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="caiman" />
|
||||||
|
<option name="id" value="caiman" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 9 Pro" />
|
||||||
|
<option name="screenDensity" value="360" />
|
||||||
|
<option name="screenX" value="960" />
|
||||||
|
<option name="screenY" value="2142" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
|
@ -179,6 +223,17 @@
|
||||||
<option name="screenX" value="2076" />
|
<option name="screenX" value="2076" />
|
||||||
<option name="screenY" value="2152" />
|
<option name="screenY" value="2152" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="35" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="comet" />
|
||||||
|
<option name="id" value="comet" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 9 Pro Fold" />
|
||||||
|
<option name="screenDensity" value="390" />
|
||||||
|
<option name="screenX" value="2076" />
|
||||||
|
<option name="screenY" value="2152" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="29" />
|
<option name="api" value="29" />
|
||||||
<option name="brand" value="samsung" />
|
<option name="brand" value="samsung" />
|
||||||
|
@ -212,6 +267,17 @@
|
||||||
<option name="screenX" value="1440" />
|
<option name="screenX" value="1440" />
|
||||||
<option name="screenY" value="3088" />
|
<option name="screenY" value="3088" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="motorola" />
|
||||||
|
<option name="codename" value="dubai" />
|
||||||
|
<option name="id" value="dubai" />
|
||||||
|
<option name="manufacturer" value="Motorola" />
|
||||||
|
<option name="name" value="edge 30" />
|
||||||
|
<option name="screenDensity" value="405" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
<option name="brand" value="samsung" />
|
<option name="brand" value="samsung" />
|
||||||
|
@ -245,6 +311,17 @@
|
||||||
<option name="screenX" value="384" />
|
<option name="screenX" value="384" />
|
||||||
<option name="screenY" value="384" />
|
<option name="screenY" value="384" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="35" />
|
||||||
|
<option name="brand" value="motorola" />
|
||||||
|
<option name="codename" value="eqe" />
|
||||||
|
<option name="id" value="eqe" />
|
||||||
|
<option name="manufacturer" value="Motorola" />
|
||||||
|
<option name="name" value="edge 50 pro" />
|
||||||
|
<option name="screenDensity" value="450" />
|
||||||
|
<option name="screenX" value="1220" />
|
||||||
|
<option name="screenY" value="2712" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="33" />
|
<option name="api" value="33" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
|
@ -289,6 +366,17 @@
|
||||||
<option name="screenX" value="720" />
|
<option name="screenX" value="720" />
|
||||||
<option name="screenY" value="1600" />
|
<option name="screenY" value="1600" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="motorola" />
|
||||||
|
<option name="codename" value="fogos" />
|
||||||
|
<option name="id" value="fogos" />
|
||||||
|
<option name="manufacturer" value="Motorola" />
|
||||||
|
<option name="name" value="moto g34 5G" />
|
||||||
|
<option name="screenDensity" value="280" />
|
||||||
|
<option name="screenX" value="720" />
|
||||||
|
<option name="screenY" value="1600" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
<option name="brand" value="samsung" />
|
<option name="brand" value="samsung" />
|
||||||
|
@ -311,6 +399,17 @@
|
||||||
<option name="screenX" value="1200" />
|
<option name="screenX" value="1200" />
|
||||||
<option name="screenY" value="1920" />
|
<option name="screenY" value="1920" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="33" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="gts7lwifi" />
|
||||||
|
<option name="id" value="gts7lwifi" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="SM-T870" />
|
||||||
|
<option name="screenDensity" value="340" />
|
||||||
|
<option name="screenX" value="1600" />
|
||||||
|
<option name="screenY" value="2560" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
<option name="brand" value="samsung" />
|
<option name="brand" value="samsung" />
|
||||||
|
@ -355,6 +454,17 @@
|
||||||
<option name="screenX" value="1440" />
|
<option name="screenX" value="1440" />
|
||||||
<option name="screenY" value="2304" />
|
<option name="screenY" value="2304" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="gts9wifi" />
|
||||||
|
<option name="id" value="gts9wifi" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="SM-X710" />
|
||||||
|
<option name="screenDensity" value="340" />
|
||||||
|
<option name="screenX" value="1600" />
|
||||||
|
<option name="screenY" value="2560" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
|
@ -410,6 +520,28 @@
|
||||||
<option name="screenX" value="1080" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2400" />
|
<option name="screenY" value="2400" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="motorola" />
|
||||||
|
<option name="codename" value="lyriq" />
|
||||||
|
<option name="id" value="lyriq" />
|
||||||
|
<option name="manufacturer" value="Motorola" />
|
||||||
|
<option name="name" value="edge 40" />
|
||||||
|
<option name="screenDensity" value="400" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="motorola" />
|
||||||
|
<option name="codename" value="manaus" />
|
||||||
|
<option name="id" value="manaus" />
|
||||||
|
<option name="manufacturer" value="Motorola" />
|
||||||
|
<option name="name" value="edge 40 neo" />
|
||||||
|
<option name="screenDensity" value="400" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="33" />
|
<option name="api" value="33" />
|
||||||
<option name="brand" value="motorola" />
|
<option name="brand" value="motorola" />
|
||||||
|
@ -443,6 +575,17 @@
|
||||||
<option name="screenX" value="1080" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2400" />
|
<option name="screenY" value="2400" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="35" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="pa3q" />
|
||||||
|
<option name="id" value="pa3q" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy S25 Ultra" />
|
||||||
|
<option name="screenDensity" value="600" />
|
||||||
|
<option name="screenX" value="1440" />
|
||||||
|
<option name="screenY" value="3120" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="33" />
|
<option name="api" value="33" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
|
@ -543,6 +686,17 @@
|
||||||
<option name="screenX" value="1600" />
|
<option name="screenX" value="1600" />
|
||||||
<option name="screenY" value="2560" />
|
<option name="screenY" value="2560" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="35" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="tegu" />
|
||||||
|
<option name="id" value="tegu" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 9a" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2424" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
|
@ -565,6 +719,17 @@
|
||||||
<option name="screenX" value="1080" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2424" />
|
<option name="screenY" value="2424" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="xcover7" />
|
||||||
|
<option name="id" value="xcover7" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="SM-G556B" />
|
||||||
|
<option name="screenDensity" value="450" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2408" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|
|
@ -57,6 +57,7 @@ dependencies {
|
||||||
implementation (libs.ohteepee)
|
implementation (libs.ohteepee)
|
||||||
|
|
||||||
implementation("io.coil-kt.coil3:coil-compose:3.0.0")
|
implementation("io.coil-kt.coil3:coil-compose:3.0.0")
|
||||||
|
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -7,11 +7,16 @@ import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.example.caloryapp.navigation.Navigation
|
import com.example.caloryapp.navigation.Navigation
|
||||||
|
import com.example.caloryapp.navigation.NavigationScreen
|
||||||
//import com.example.caloryapp.pages.NavBarScreen
|
//import com.example.caloryapp.pages.NavBarScreen
|
||||||
import com.example.caloryapp.ui.theme.CaloryAppTheme
|
import com.example.caloryapp.ui.theme.CaloryAppTheme
|
||||||
|
import com.example.caloryapp.viewmodel.UserViewModel
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||||
|
@ -34,29 +39,7 @@ class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
CaloryAppTheme {
|
CaloryAppTheme {
|
||||||
// Surface(
|
Navigation(context = applicationContext)
|
||||||
// modifier = Modifier.fillMaxSize(),
|
|
||||||
// color = MaterialTheme.colorScheme.background
|
|
||||||
// ) {
|
|
||||||
// val viewModel = viewModel<FoodDetectionViewModel>()
|
|
||||||
// ScreenTest(viewModel)
|
|
||||||
// }
|
|
||||||
// val context = LocalContext.current
|
|
||||||
// var classificationResult by remember { mutableStateOf("Memuat...") }
|
|
||||||
//
|
|
||||||
// Box {
|
|
||||||
// CameraPreview(onImageCaptured = { byteBuffer ->
|
|
||||||
// val result = foodClassifier.classify(byteBuffer)
|
|
||||||
// classificationResult = result
|
|
||||||
//
|
|
||||||
// Toast.makeText(context, "Hasil: $result", Toast.LENGTH_SHORT).show()
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
Navigation()
|
|
||||||
|
|
||||||
// LoginScreen(navController = rememberNavController())
|
|
||||||
// ProfileScreen(navController = rememberNavController())
|
|
||||||
// MainScreen()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,15 @@ package com.example.caloryapp.foodmodel
|
||||||
|
|
||||||
enum class FoodCategory(
|
enum class FoodCategory(
|
||||||
val displayName: String,
|
val displayName: String,
|
||||||
val caloriesPer100g: Int,
|
val caloriesPerPorsi: Int,
|
||||||
val colorHex: String,
|
val colorHex: String,
|
||||||
val icon: String
|
val icon: String
|
||||||
) {
|
) {
|
||||||
CARBS("Karbohidrat", 150, "#FECD45", "🍚"), // Tetap 150 kal/100g
|
CARBS("Karbohidrat", 85, "#FECD45", "🍚"), // 1/3 piring
|
||||||
PROTEIN("Protein", 250, "#FC8369", "🍗"), // Ubah ke 250 kal/100g
|
PROTEIN("Protein", 25, "#FC8369", "🍗"), // 1/6 piring
|
||||||
VEGETABLES("Sayuran", 30, "#4AB54A", "🥦"), // Tetap 30 kal/100g
|
VEGETABLES("Sayuran", 25, "#4AB54A", "🥦"), // 1/3 piring
|
||||||
FRUITS("Buah", 60, "#FF6B6B", "🍎"), // Tetap 60 kal/100g
|
FRUITS("Buah", 25, "#FF6B6B", "🍎"), // 1/6 piring
|
||||||
OTHER("Lainnya", 200, "#A0A0A0", "🍬") // Tetap 200 kal/100g
|
// OTHER("Lainnya", 200, "#A0A0A0", "🍬") // Tetap 200 kal/100g
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FoodDetectionResult(
|
data class FoodDetectionResult(
|
||||||
|
@ -23,6 +23,6 @@ data class FoodDetectionResult(
|
||||||
fun Map<FoodCategory, Float>.calculateTotalCalories(plateWeightGrams: Int = 500): Int {
|
fun Map<FoodCategory, Float>.calculateTotalCalories(plateWeightGrams: Int = 500): Int {
|
||||||
return this.entries.sumOf { (category, percentage) ->
|
return this.entries.sumOf { (category, percentage) ->
|
||||||
val weightGrams = plateWeightGrams * percentage
|
val weightGrams = plateWeightGrams * percentage
|
||||||
(weightGrams * category.caloriesPer100g / 100).toInt()
|
(weightGrams * category.caloriesPerPorsi / 100).toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -15,8 +15,7 @@ class FoodDetector(private val context: Context) {
|
||||||
FoodCategory.CARBS,
|
FoodCategory.CARBS,
|
||||||
FoodCategory.PROTEIN,
|
FoodCategory.PROTEIN,
|
||||||
FoodCategory.VEGETABLES,
|
FoodCategory.VEGETABLES,
|
||||||
FoodCategory.FRUITS,
|
FoodCategory.FRUITS
|
||||||
FoodCategory.OTHER
|
|
||||||
)
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -24,7 +23,7 @@ class FoodDetector(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadModel() {
|
private fun loadModel() {
|
||||||
val assetFileDescriptor = context.assets.openFd("model_food_plate_densenet.tflite")
|
val assetFileDescriptor = context.assets.openFd("model_food_plate_densenet5.tflite")
|
||||||
val fileInputStream = FileInputStream(assetFileDescriptor.fileDescriptor)
|
val fileInputStream = FileInputStream(assetFileDescriptor.fileDescriptor)
|
||||||
val fileChannel = fileInputStream.channel
|
val fileChannel = fileInputStream.channel
|
||||||
val startOffset = assetFileDescriptor.startOffset
|
val startOffset = assetFileDescriptor.startOffset
|
||||||
|
@ -63,7 +62,7 @@ class FoodDetector(private val context: Context) {
|
||||||
modelInput.rewind() // Penting: reset posisi buffer ke awal
|
modelInput.rewind() // Penting: reset posisi buffer ke awal
|
||||||
|
|
||||||
// Run model - pastikan outputBuffer memiliki dimensi yang benar
|
// Run model - pastikan outputBuffer memiliki dimensi yang benar
|
||||||
val outputBuffer = Array(1) { FloatArray(5) } // 5 categories
|
val outputBuffer = Array(1) { FloatArray(4) } // 5 categories
|
||||||
interpreter?.run(modelInput, outputBuffer)
|
interpreter?.run(modelInput, outputBuffer)
|
||||||
|
|
||||||
// Get predicted category
|
// Get predicted category
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
package com.example.caloryapp.model
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
|
||||||
|
data class CaloryModel(
|
||||||
|
val calories: Int = 0,
|
||||||
|
val date: String = "",
|
||||||
|
val username: String = "",
|
||||||
|
val imagePath: String = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
fun CaloryModel.isToday(): Boolean {
|
||||||
|
val today = Calendar.getInstance()
|
||||||
|
val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||||
|
return format.format(today.time) == this.date
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CaloryModel.isThisWeek(): Boolean {
|
||||||
|
val cal = Calendar.getInstance()
|
||||||
|
val currentWeek = cal.get(Calendar.WEEK_OF_YEAR)
|
||||||
|
val currentYear = cal.get(Calendar.YEAR)
|
||||||
|
|
||||||
|
val modelDate = Calendar.getInstance().apply {
|
||||||
|
time = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(this@isThisWeek.date)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
return modelDate.get(Calendar.WEEK_OF_YEAR) == currentWeek &&
|
||||||
|
modelDate.get(Calendar.YEAR) == currentYear
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CaloryModel.isThisMonth(): Boolean {
|
||||||
|
val cal = Calendar.getInstance()
|
||||||
|
val currentMonth = cal.get(Calendar.MONTH)
|
||||||
|
val currentYear = cal.get(Calendar.YEAR)
|
||||||
|
|
||||||
|
val modelDate = Calendar.getInstance().apply {
|
||||||
|
time = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(this@isThisMonth.date)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
return modelDate.get(Calendar.MONTH) == currentMonth &&
|
||||||
|
modelDate.get(Calendar.YEAR) == currentYear
|
||||||
|
}
|
|
@ -1,19 +1,25 @@
|
||||||
package com.example.caloryapp.navigation
|
package com.example.caloryapp.navigation
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.compose.material3.DrawerValue
|
import androidx.compose.material3.DrawerValue
|
||||||
import androidx.compose.material3.rememberDrawerState
|
import androidx.compose.material3.rememberDrawerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavType
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import androidx.navigation.navArgument
|
||||||
import com.example.caloryapp.viewmodel.FoodDetectionViewModel
|
import com.example.caloryapp.viewmodel.FoodDetectionViewModel
|
||||||
import com.example.caloryapp.pages.MainScreen
|
import com.example.caloryapp.pages.MainScreen
|
||||||
import com.example.caloryapp.pages.account.ProfileChangePasswordScreen
|
import com.example.caloryapp.pages.account.ProfileChangePasswordScreen
|
||||||
import com.example.caloryapp.pages.account.ProfileDetailScreen
|
import com.example.caloryapp.pages.account.ProfileDetailScreen
|
||||||
import com.example.caloryapp.pages.account.ProfileScreen
|
import com.example.caloryapp.pages.account.ProfileScreen
|
||||||
|
import com.example.caloryapp.pages.calorydetail.CaloryDetailScreen
|
||||||
import com.example.caloryapp.pages.calorydetail.ScreenTest
|
import com.example.caloryapp.pages.calorydetail.ScreenTest
|
||||||
import com.example.caloryapp.pages.onboard.ChangePasswordScreen
|
import com.example.caloryapp.pages.onboard.ChangePasswordScreen
|
||||||
import com.example.caloryapp.pages.onboard.ForgotPasswordScreen
|
import com.example.caloryapp.pages.onboard.ForgotPasswordScreen
|
||||||
|
@ -23,11 +29,14 @@ import com.example.caloryapp.pages.onboard.OnBoardingScreen
|
||||||
import com.example.caloryapp.pages.onboard.RegisterScreen
|
import com.example.caloryapp.pages.onboard.RegisterScreen
|
||||||
import com.example.caloryapp.pages.onboard.SuccessChangePassword
|
import com.example.caloryapp.pages.onboard.SuccessChangePassword
|
||||||
import com.example.caloryapp.pages.onboard.SuccessRegister
|
import com.example.caloryapp.pages.onboard.SuccessRegister
|
||||||
|
import com.example.caloryapp.repository.UserRepository
|
||||||
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
||||||
|
import com.example.caloryapp.viewmodel.LoginState
|
||||||
import com.example.caloryapp.viewmodel.UserViewModel
|
import com.example.caloryapp.viewmodel.UserViewModel
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Navigation(modifier: Modifier = Modifier) {
|
fun Navigation(modifier: Modifier = Modifier, context: Context) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val userViewModel: UserViewModel = viewModel()
|
val userViewModel: UserViewModel = viewModel()
|
||||||
val foodViewModel: FoodDetectionViewModel = viewModel()
|
val foodViewModel: FoodDetectionViewModel = viewModel()
|
||||||
|
@ -35,6 +44,44 @@ fun Navigation(modifier: Modifier = Modifier) {
|
||||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
userViewModel.initSessionManager(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menentukan startDestination berdasarkan status login
|
||||||
|
val startDestination = if (userViewModel.checkLoginStatus()) {
|
||||||
|
NavigationScreen.MainScreen.name
|
||||||
|
} else {
|
||||||
|
NavigationScreen.LoginScreen.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observer untuk status login
|
||||||
|
val loginState by userViewModel.loginState
|
||||||
|
|
||||||
|
// Navigasi otomatis ketika login berhasil
|
||||||
|
LaunchedEffect(loginState) {
|
||||||
|
when (loginState) {
|
||||||
|
is LoginState.Success -> {
|
||||||
|
navController.navigate(NavigationScreen.MainScreen.name) {
|
||||||
|
popUpTo(NavigationScreen.LoginScreen.name) { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is LoginState.AlreadyLoggedIn -> {
|
||||||
|
if (navController.currentDestination?.route != NavigationScreen.MainScreen.name) {
|
||||||
|
navController.navigate(NavigationScreen.MainScreen.name) {
|
||||||
|
popUpTo(NavigationScreen.LoginScreen.name) { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is LoginState.LoggedOut -> {
|
||||||
|
navController.navigate(NavigationScreen.LoginScreen.name) {
|
||||||
|
popUpTo(0) { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = NavigationScreen.LoginScreen.name
|
startDestination = NavigationScreen.LoginScreen.name
|
||||||
|
@ -55,7 +102,7 @@ fun Navigation(modifier: Modifier = Modifier) {
|
||||||
ProfileChangePasswordScreen(navController = navController, viewModel = userViewModel)
|
ProfileChangePasswordScreen(navController = navController, viewModel = userViewModel)
|
||||||
}
|
}
|
||||||
composable(NavigationScreen.ScreenTest.name) {
|
composable(NavigationScreen.ScreenTest.name) {
|
||||||
ScreenTest(navController = navController, viewModel = foodViewModel)
|
ScreenTest(navController = navController, viewModel = foodViewModel, userViewModel = userViewModel)
|
||||||
}
|
}
|
||||||
composable(NavigationScreen.ProfileScreen.name) {
|
composable(NavigationScreen.ProfileScreen.name) {
|
||||||
ProfileScreen(
|
ProfileScreen(
|
||||||
|
@ -65,6 +112,30 @@ fun Navigation(modifier: Modifier = Modifier) {
|
||||||
drawerState = drawerState
|
drawerState = drawerState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
composable(
|
||||||
|
route = "${NavigationScreen.CaloryDetailScreen.name}/{date}/{calories}/{imagePath}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("date") { type = NavType.StringType },
|
||||||
|
navArgument("calories") { type = NavType.IntType },
|
||||||
|
navArgument("imagePath") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
val date = backStackEntry.arguments?.getString("date") ?: ""
|
||||||
|
val calories = backStackEntry.arguments?.getInt("calories") ?: 0
|
||||||
|
val imagePath = backStackEntry.arguments?.getString("imagePath") ?: ""
|
||||||
|
|
||||||
|
// Decode imagePath yang sudah diencoding
|
||||||
|
val decodedImagePath = java.net.URLDecoder.decode(imagePath, StandardCharsets.UTF_8.name())
|
||||||
|
|
||||||
|
CaloryDetailScreen(
|
||||||
|
navController = navController,
|
||||||
|
caloryDate = date,
|
||||||
|
caloryCalories = calories,
|
||||||
|
caloryImagePath = decodedImagePath,
|
||||||
|
userViewModel = userViewModel,
|
||||||
|
caloryHistoryViewModel = caloryHistoryViewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
// composable(NavigationScreen.ScreenTest.name) {
|
// composable(NavigationScreen.ScreenTest.name) {
|
||||||
// ScreenTest(navController = navController, viewModel = userViewModel)
|
// ScreenTest(navController = navController, viewModel = userViewModel)
|
||||||
// }
|
// }
|
||||||
|
|
|
@ -15,6 +15,7 @@ enum class NavigationScreen {
|
||||||
ProfileScreen,
|
ProfileScreen,
|
||||||
ProfileDetailScreen,
|
ProfileDetailScreen,
|
||||||
ProfileChangePasswordScreen,
|
ProfileChangePasswordScreen,
|
||||||
|
CaloryDetailScreen,
|
||||||
ScreenTest;
|
ScreenTest;
|
||||||
|
|
||||||
fun fromRoute(route: String): NavigationScreen =
|
fun fromRoute(route: String): NavigationScreen =
|
||||||
|
@ -33,6 +34,7 @@ enum class NavigationScreen {
|
||||||
SuccessRegister.name -> SuccessRegister
|
SuccessRegister.name -> SuccessRegister
|
||||||
ProfileDetailScreen.name -> ProfileDetailScreen
|
ProfileDetailScreen.name -> ProfileDetailScreen
|
||||||
ProfileChangePasswordScreen.name -> ProfileChangePasswordScreen
|
ProfileChangePasswordScreen.name -> ProfileChangePasswordScreen
|
||||||
|
CaloryDetailScreen.name -> CaloryDetailScreen
|
||||||
ScreenTest.name -> ScreenTest
|
ScreenTest.name -> ScreenTest
|
||||||
|
|
||||||
else -> throw IllegalArgumentException("$route gagal bji")
|
else -> throw IllegalArgumentException("$route gagal bji")
|
||||||
|
|
|
@ -1,15 +1,20 @@
|
||||||
package com.example.caloryapp.pages
|
package com.example.caloryapp.pages
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Divider
|
import androidx.compose.material3.Divider
|
||||||
import androidx.compose.material3.DrawerState
|
import androidx.compose.material3.DrawerState
|
||||||
import androidx.compose.material3.DrawerValue
|
import androidx.compose.material3.DrawerValue
|
||||||
|
@ -20,11 +25,14 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.rememberDrawerState
|
import androidx.compose.material3.rememberDrawerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
@ -48,8 +56,15 @@ import com.example.caloryapp.ui.theme.medium
|
||||||
import com.example.caloryapp.ui.theme.primary
|
import com.example.caloryapp.ui.theme.primary
|
||||||
import com.example.caloryapp.ui.theme.semibold
|
import com.example.caloryapp.ui.theme.semibold
|
||||||
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
||||||
|
import com.example.caloryapp.viewmodel.LoginState
|
||||||
import com.example.caloryapp.viewmodel.UserViewModel
|
import com.example.caloryapp.viewmodel.UserViewModel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.navigation.NavType
|
||||||
|
import androidx.navigation.navArgument
|
||||||
|
import com.example.caloryapp.pages.calorydetail.CaloryDetailScreen
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
sealed class DrawerScreen(val title: String) {
|
sealed class DrawerScreen(val title: String) {
|
||||||
data object HomeScreen : DrawerScreen("Home")
|
data object HomeScreen : DrawerScreen("Home")
|
||||||
|
@ -66,11 +81,31 @@ fun MainScreen(
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
// Observer untuk status login
|
||||||
|
val loginState by userViewModel.loginState
|
||||||
|
|
||||||
|
// Navigasi otomatis ketika logout
|
||||||
|
LaunchedEffect(loginState) {
|
||||||
|
if (loginState is LoginState.LoggedOut) {
|
||||||
|
// Navigasi ke login screen saat logout
|
||||||
|
navController.navigate(NavigationScreen.LoginScreen.name) {
|
||||||
|
popUpTo(0) { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler {
|
||||||
|
// Mencegah navigasi kembali ke login
|
||||||
|
Toast.makeText(context, "Gunakan menu logout untuk keluar", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
ModalNavigationDrawer(
|
ModalNavigationDrawer(
|
||||||
drawerState = drawerState,
|
drawerState = drawerState,
|
||||||
drawerContent = {
|
drawerContent = {
|
||||||
DrawerContent(navController, drawerState, scope, userViewModel)
|
// Menggunakan fungsi DrawerContent yang dimodifikasi dengan penanganan null
|
||||||
|
SafeDrawerContent(navController, drawerState, scope, userViewModel)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
NavHost(navController = navController, startDestination = DrawerScreen.HomeScreen.title) {
|
NavHost(navController = navController, startDestination = DrawerScreen.HomeScreen.title) {
|
||||||
|
@ -87,23 +122,123 @@ fun MainScreen(
|
||||||
ProfileChangePasswordScreen(navController = navController, viewModel = userViewModel)
|
ProfileChangePasswordScreen(navController = navController, viewModel = userViewModel)
|
||||||
}
|
}
|
||||||
composable(NavigationScreen.ScreenTest.name) {
|
composable(NavigationScreen.ScreenTest.name) {
|
||||||
ScreenTest(navController = navController, viewModel = foodDetectionViewModel)
|
ScreenTest(navController = navController, viewModel = foodDetectionViewModel, userViewModel = userViewModel)
|
||||||
}
|
}
|
||||||
composable(NavigationScreen.LoginScreen.name) {
|
composable(NavigationScreen.LoginScreen.name) {
|
||||||
LoginScreen(navController = navController, viewModel = userViewModel)
|
LoginScreen(navController = navController, viewModel = userViewModel)
|
||||||
}
|
}
|
||||||
|
composable(
|
||||||
|
route = "${NavigationScreen.CaloryDetailScreen.name}/{date}/{calories}/{imagePath}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("date") { type = NavType.StringType },
|
||||||
|
navArgument("calories") { type = NavType.IntType },
|
||||||
|
navArgument("imagePath") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
val date = backStackEntry.arguments?.getString("date") ?: ""
|
||||||
|
val calories = backStackEntry.arguments?.getInt("calories") ?: 0
|
||||||
|
val imagePath = backStackEntry.arguments?.getString("imagePath") ?: ""
|
||||||
|
|
||||||
|
// Decode imagePath yang sudah diencoding
|
||||||
|
val decodedImagePath = java.net.URLDecoder.decode(imagePath, StandardCharsets.UTF_8.name())
|
||||||
|
|
||||||
|
CaloryDetailScreen(
|
||||||
|
navController = navController,
|
||||||
|
caloryDate = date,
|
||||||
|
caloryCalories = calories,
|
||||||
|
caloryImagePath = decodedImagePath,
|
||||||
|
userViewModel = userViewModel,
|
||||||
|
caloryHistoryViewModel = caloryHistoryViewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fungsi Drawer Content yang aman dari NullPointerException
|
||||||
@Composable
|
@Composable
|
||||||
fun DrawerContent(
|
fun SafeDrawerContent(
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
drawerState: DrawerState,
|
drawerState: DrawerState,
|
||||||
scope: kotlinx.coroutines.CoroutineScope,
|
scope: kotlinx.coroutines.CoroutineScope,
|
||||||
viewModel: UserViewModel
|
viewModel: UserViewModel
|
||||||
) {
|
) {
|
||||||
val user = viewModel.user.value
|
val user = viewModel.user.value
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
// Jika user null, tampilkan drawer dengan konten minimal
|
||||||
|
if (user == null) {
|
||||||
|
ModalDrawerSheet(modifier = Modifier.background(primary)) {
|
||||||
|
Spacer(modifier = Modifier.height(30.dp))
|
||||||
|
|
||||||
|
// Header untuk user yang belum login
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 15.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.ic_profile_women),
|
||||||
|
contentDescription = null,
|
||||||
|
Modifier.size(70.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Text(
|
||||||
|
text = "Belum Login",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 20.sp,
|
||||||
|
color = Color.Black,
|
||||||
|
fontFamily = semibold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
Divider(
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp),
|
||||||
|
color = primary.copy(alpha = 0.1f),
|
||||||
|
thickness = 3.dp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// Menu navigasi standar
|
||||||
|
DrawerItem(
|
||||||
|
"Home",
|
||||||
|
R.drawable.ic_home_filled,
|
||||||
|
navController,
|
||||||
|
DrawerScreen.HomeScreen.title,
|
||||||
|
drawerState,
|
||||||
|
scope
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tombol login
|
||||||
|
TextButton(onClick = {
|
||||||
|
scope.launch {
|
||||||
|
drawerState.close()
|
||||||
|
navController.navigate(NavigationScreen.LoginScreen.name) {
|
||||||
|
popUpTo(0) { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(id = R.drawable.ic_profile_filled),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(30.dp),
|
||||||
|
tint = primary,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Login",
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
color = primary,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontFamily = bold,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tampilkan drawer normal dengan informasi user
|
||||||
ModalDrawerSheet(modifier = Modifier.background(primary)) {
|
ModalDrawerSheet(modifier = Modifier.background(primary)) {
|
||||||
Spacer(modifier = Modifier.height(30.dp))
|
Spacer(modifier = Modifier.height(30.dp))
|
||||||
Row(
|
Row(
|
||||||
|
@ -119,7 +254,8 @@ fun DrawerContent(
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Column(horizontalAlignment = Alignment.Start) {
|
Column(horizontalAlignment = Alignment.Start) {
|
||||||
Text(
|
Text(
|
||||||
text = user!!.fullName,
|
// Gunakan safe operator untuk menghindari null
|
||||||
|
text = user.fullName,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 20.sp,
|
fontSize = 20.sp,
|
||||||
color = Color.Black,
|
color = Color.Black,
|
||||||
|
@ -135,7 +271,6 @@ fun DrawerContent(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
@ -161,9 +296,47 @@ fun DrawerContent(
|
||||||
drawerState,
|
drawerState,
|
||||||
scope
|
scope
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Tambahkan separator dan tombol logout
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
Divider(
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp),
|
||||||
|
color = primary.copy(alpha = 0.1f),
|
||||||
|
thickness = 1.dp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
// Tombol logout
|
||||||
|
// LogoutDrawerItem(
|
||||||
|
// title = "Logout",
|
||||||
|
// icon = R.drawable.ic_profile_filled, // Gunakan icon yang sudah ada jika ic_logout tidak tersedia
|
||||||
|
// onLogout = {
|
||||||
|
// try {
|
||||||
|
// // Navigasi ke login screen terlebih dahulu
|
||||||
|
// scope.launch {
|
||||||
|
// drawerState.close()
|
||||||
|
//
|
||||||
|
// // Navigasi ke login screen
|
||||||
|
// navController.navigate(NavigationScreen.LoginScreen.name) {
|
||||||
|
// popUpTo(0) { inclusive = true }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Delay sedikit untuk memastikan navigasi sudah berjalan
|
||||||
|
// delay(100)
|
||||||
|
//
|
||||||
|
// // Lakukan logout
|
||||||
|
// viewModel.logout()
|
||||||
|
// }
|
||||||
|
// } catch (e: Exception) {
|
||||||
|
// Log.e("MainScreen", "Error during logout: ${e.message}")
|
||||||
|
// Toast.makeText(context, "Gagal logout: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fungsi untuk item menu drawer
|
||||||
@Composable
|
@Composable
|
||||||
fun DrawerItem(
|
fun DrawerItem(
|
||||||
title: String,
|
title: String,
|
||||||
|
@ -197,3 +370,28 @@ fun DrawerItem(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tambahkan fungsi baru untuk tombol logout
|
||||||
|
@Composable
|
||||||
|
fun LogoutDrawerItem(
|
||||||
|
title: String,
|
||||||
|
icon: Int,
|
||||||
|
onLogout: () -> Unit
|
||||||
|
) {
|
||||||
|
TextButton(onClick = onLogout) {
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(id = icon),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(30.dp),
|
||||||
|
tint = Color.Red, // Warna merah untuk menandakan logout
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
color = Color.Red,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontFamily = bold,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
|
@ -30,6 +31,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
@ -161,7 +163,8 @@ fun ProfileDetailScreen(
|
||||||
value = fullName,
|
value = fullName,
|
||||||
onValueChange = { fullName = it },
|
onValueChange = { fullName = it },
|
||||||
placeholderText = "Masukkan Nama Lengkap",
|
placeholderText = "Masukkan Nama Lengkap",
|
||||||
input = isEditing
|
input = isEditing,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier.height(20.dp))
|
Spacer(modifier.height(20.dp))
|
||||||
|
@ -179,7 +182,8 @@ fun ProfileDetailScreen(
|
||||||
value = username,
|
value = username,
|
||||||
onValueChange = { username = it },
|
onValueChange = { username = it },
|
||||||
placeholderText = "Masukkan Username",
|
placeholderText = "Masukkan Username",
|
||||||
input = false // Tidak dapat mengedit username
|
input = false, // Tidak dapat mengedit username
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier.height(20.dp))
|
Spacer(modifier.height(20.dp))
|
||||||
|
@ -197,7 +201,8 @@ fun ProfileDetailScreen(
|
||||||
value = gmail,
|
value = gmail,
|
||||||
onValueChange = { gmail = it },
|
onValueChange = { gmail = it },
|
||||||
placeholderText = "Masukkan Email",
|
placeholderText = "Masukkan Email",
|
||||||
input = isEditing
|
input = isEditing,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier.height(20.dp))
|
Spacer(modifier.height(20.dp))
|
||||||
|
@ -215,7 +220,8 @@ fun ProfileDetailScreen(
|
||||||
value = selectedGender,
|
value = selectedGender,
|
||||||
onValueChange = { selectedGender = it },
|
onValueChange = { selectedGender = it },
|
||||||
placeholderText = "Masukkan Gender",
|
placeholderText = "Masukkan Gender",
|
||||||
input = isEditing
|
input = isEditing,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier.height(20.dp))
|
Spacer(modifier.height(20.dp))
|
||||||
|
@ -233,7 +239,8 @@ fun ProfileDetailScreen(
|
||||||
value = weight,
|
value = weight,
|
||||||
onValueChange = { weight = it },
|
onValueChange = { weight = it },
|
||||||
placeholderText = "Masukkan Berat Badan",
|
placeholderText = "Masukkan Berat Badan",
|
||||||
input = isEditing
|
input = isEditing,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier.height(20.dp))
|
Spacer(modifier.height(20.dp))
|
||||||
|
@ -251,7 +258,8 @@ fun ProfileDetailScreen(
|
||||||
value = height,
|
value = height,
|
||||||
onValueChange = { height = it },
|
onValueChange = { height = it },
|
||||||
placeholderText = "Masukkan Tinggi Badan",
|
placeholderText = "Masukkan Tinggi Badan",
|
||||||
input = isEditing
|
input = isEditing,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tombol Edit
|
// Tombol Edit
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package com.example.caloryapp.pages.account
|
package com.example.caloryapp.pages.account
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
@ -14,10 +16,13 @@ import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
import androidx.compose.material.Divider
|
import androidx.compose.material.Divider
|
||||||
import androidx.compose.material3.DrawerState
|
import androidx.compose.material3.DrawerState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
@ -45,9 +50,11 @@ import com.example.caloryapp.ui.theme.primaryblack
|
||||||
import com.example.caloryapp.ui.theme.primarygrey
|
import com.example.caloryapp.ui.theme.primarygrey
|
||||||
import com.example.caloryapp.ui.theme.primaryred
|
import com.example.caloryapp.ui.theme.primaryred
|
||||||
import com.example.caloryapp.ui.theme.regular
|
import com.example.caloryapp.ui.theme.regular
|
||||||
|
import com.example.caloryapp.viewmodel.LoginState
|
||||||
import com.example.caloryapp.viewmodel.UserViewModel
|
import com.example.caloryapp.viewmodel.UserViewModel
|
||||||
import com.example.caloryapp.widget.SimpleAlertDialog
|
import com.example.caloryapp.widget.SimpleAlertDialog
|
||||||
import com.google.firebase.auth.FirebaseAuth
|
import com.google.firebase.auth.FirebaseAuth
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
@ -70,6 +77,35 @@ fun ProfileScreen(
|
||||||
val currentDate = Calendar.getInstance().time
|
val currentDate = Calendar.getInstance().time
|
||||||
val dateFormat = SimpleDateFormat("EEEE, dd MMMM yyyy", Locale("id", "ID"))
|
val dateFormat = SimpleDateFormat("EEEE, dd MMMM yyyy", Locale("id", "ID"))
|
||||||
val formattedDate = dateFormat.format(currentDate)
|
val formattedDate = dateFormat.format(currentDate)
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.initSessionManager(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Amati state login untuk navigasi
|
||||||
|
val loginState by viewModel.loginState
|
||||||
|
|
||||||
|
// Navigasi otomatis saat logout
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.initSessionManager(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
// Opsi 1: Tampilkan loading
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opsi 2: Navigasi ke login screen
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
navController.navigate(NavigationScreen.LoginScreen.name) {
|
||||||
|
popUpTo(0) { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return // Penting! Hentikan komposisi di sini
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier
|
modifier
|
||||||
|
@ -228,7 +264,17 @@ fun ProfileScreen(
|
||||||
dialogSubTitle = "Apakah Anda yakin ingin keluar?",
|
dialogSubTitle = "Apakah Anda yakin ingin keluar?",
|
||||||
onDismissRequest = { openAlertDialog.value = false },
|
onDismissRequest = { openAlertDialog.value = false },
|
||||||
onConfirmation = {
|
onConfirmation = {
|
||||||
logoutUser()
|
try {
|
||||||
|
navController.navigate(NavigationScreen.LoginScreen.name) {
|
||||||
|
popUpTo(0) { inclusive = true }
|
||||||
|
}
|
||||||
|
// delay(100)
|
||||||
|
viewModel.logout()
|
||||||
|
// Navigasi dilakukan oleh LaunchedEffect di atas
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ProfileScreen", "Error during logout: ${e.message}")
|
||||||
|
Toast.makeText(context, "Gagal logout: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
openAlertDialog.value = false
|
openAlertDialog.value = false
|
||||||
// Navigation()
|
// Navigation()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,269 +1,247 @@
|
||||||
package com.example.caloryapp.pages.calorydetail
|
package com.example.caloryapp.pages.calorydetail
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.Button
|
import androidx.compose.material.Button
|
||||||
import androidx.compose.material.ButtonDefaults
|
import androidx.compose.material.ButtonDefaults
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.IconButton
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.Scaffold
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TopAppBar
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
//import androidx.compose.material.icons.filled.Image
|
||||||
|
import androidx.compose.material.icons.filled.Info
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowLeft
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.example.caloryapp.foodmodel.FoodCategory
|
import com.example.caloryapp.model.CaloryModel
|
||||||
import com.example.caloryapp.foodmodel.PlateDiagram
|
|
||||||
import com.example.caloryapp.ui.theme.background
|
import com.example.caloryapp.ui.theme.background
|
||||||
import com.example.caloryapp.ui.theme.bold
|
import com.example.caloryapp.ui.theme.bold
|
||||||
|
import com.example.caloryapp.ui.theme.medium
|
||||||
import com.example.caloryapp.ui.theme.primary
|
import com.example.caloryapp.ui.theme.primary
|
||||||
|
import com.example.caloryapp.ui.theme.primaryblack
|
||||||
import com.example.caloryapp.ui.theme.semibold
|
import com.example.caloryapp.ui.theme.semibold
|
||||||
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
||||||
import kotlinx.coroutines.launch
|
import com.example.caloryapp.viewmodel.UserViewModel
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
|
||||||
* Layar detail riwayat kalori
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CaloryHistoryDetailScreen(
|
fun CaloryDetailScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
historyId: String,
|
caloryDate: String,
|
||||||
viewModel: CaloryHistoryViewModel
|
caloryCalories: Int,
|
||||||
|
caloryImagePath: String,
|
||||||
|
userViewModel: UserViewModel,
|
||||||
|
caloryHistoryViewModel: CaloryHistoryViewModel
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val context = LocalContext.current
|
||||||
|
val user = userViewModel.user.value
|
||||||
|
var bitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
|
var isDeleting by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Muat data detail saat komponen ditampilkan
|
// Muat gambar jika ada path
|
||||||
LaunchedEffect(historyId) {
|
LaunchedEffect(caloryImagePath) {
|
||||||
viewModel.loadHistoryDetail(historyId)
|
if (caloryImagePath.isNotEmpty()) {
|
||||||
}
|
bitmap = caloryHistoryViewModel.getImageBitmap(context, caloryImagePath)
|
||||||
|
|
||||||
// Bersihkan state detail saat komponen dihancurkan
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
viewModel.selectedHistory = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Scaffold(
|
||||||
modifier = Modifier
|
topBar = {
|
||||||
.fillMaxSize()
|
TopAppBar(
|
||||||
.background(background)
|
title = { Text(
|
||||||
) {
|
text = "Profil Saya",
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(horizontal = 25.dp, vertical = 45.dp)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
) {
|
|
||||||
// Header dengan tombol kembali
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
IconButton(
|
|
||||||
onClick = { navController.popBackStack() }
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.ArrowBack,
|
|
||||||
contentDescription = "Kembali",
|
|
||||||
tint = primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "Detail Kalori",
|
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 24.sp,
|
fontSize = 25.sp,
|
||||||
color = primary,
|
color = primaryblack,
|
||||||
fontFamily = bold
|
fontFamily = bold
|
||||||
),
|
)
|
||||||
modifier = Modifier.weight(1f)
|
) },
|
||||||
)
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.navigateUp() }) {
|
||||||
// Tombol hapus
|
Icon(
|
||||||
IconButton(
|
imageVector = Icons.Default.KeyboardArrowLeft,
|
||||||
onClick = {
|
contentDescription = null,
|
||||||
coroutineScope.launch {
|
Modifier
|
||||||
viewModel.deleteHistory(historyId) { success ->
|
.size(28.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
// Tombol hapus
|
||||||
|
IconButton(onClick = {
|
||||||
|
if (user != null) {
|
||||||
|
isDeleting = true
|
||||||
|
caloryHistoryViewModel.deleteCaloryData(
|
||||||
|
date = caloryDate,
|
||||||
|
calories = caloryCalories,
|
||||||
|
imagePath = caloryImagePath,
|
||||||
|
username = user.username
|
||||||
|
) { success ->
|
||||||
|
isDeleting = false
|
||||||
if (success) {
|
if (success) {
|
||||||
navController.popBackStack()
|
navController.navigateUp()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Delete,
|
||||||
|
contentDescription = "Delete",
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
backgroundColor = background
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.background(background),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (isDeleting) {
|
||||||
|
CircularProgressIndicator(color = primary)
|
||||||
|
} else {
|
||||||
|
// Detail kalori
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
// Gambar makanan
|
||||||
imageVector = Icons.Default.Delete,
|
Box(
|
||||||
contentDescription = "Hapus",
|
modifier = Modifier
|
||||||
tint = Color.Red
|
.size(250.dp)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(Color.LightGray),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
bitmap?.let { bmp ->
|
||||||
|
Image(
|
||||||
|
bitmap = bmp.asImageBitmap(),
|
||||||
|
contentDescription = "Food Image",
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
} ?: Icon(
|
||||||
|
imageVector = Icons.Default.Info,
|
||||||
|
contentDescription = "No Image",
|
||||||
|
modifier = Modifier.size(80.dp),
|
||||||
|
tint = Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
// Informasi kalori
|
||||||
|
Text(
|
||||||
|
text = "$caloryCalories Kalori",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 32.sp,
|
||||||
|
color = primary,
|
||||||
|
fontFamily = bold
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(30.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
// Tampilkan loading atau konten
|
Text(
|
||||||
if (viewModel.isLoading) {
|
text = "Tanggal: ${formatDate(caloryDate)}",
|
||||||
CircularProgressIndicator(
|
style = TextStyle(
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
fontSize = 18.sp,
|
||||||
color = primary
|
color = Color.Gray,
|
||||||
)
|
fontFamily = semibold
|
||||||
} else if (viewModel.selectedHistory != null) {
|
)
|
||||||
val history = viewModel.selectedHistory!!
|
|
||||||
|
|
||||||
// Total kalori
|
|
||||||
Text(
|
|
||||||
text = "${history.totalCalories}",
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 48.sp,
|
|
||||||
color = primary,
|
|
||||||
fontFamily = bold
|
|
||||||
),
|
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Kalori",
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 24.sp,
|
|
||||||
color = primary,
|
|
||||||
fontFamily = semibold
|
|
||||||
),
|
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
// Tanggal dan waktu
|
|
||||||
val dateFormat = SimpleDateFormat("dd MMMM yyyy, HH:mm", Locale("id", "ID"))
|
|
||||||
Text(
|
|
||||||
text = "Tercatat pada ${dateFormat.format(history.getDate())}",
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = Color.Gray,
|
|
||||||
fontWeight = FontWeight.Normal
|
|
||||||
),
|
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
|
||||||
|
|
||||||
// Diagram komposisi makanan
|
|
||||||
// Konversi kembali Map<String, Float> ke Map<FoodCategory, Float>
|
|
||||||
val foodCategories = history.foodComposition.mapKeys { entry ->
|
|
||||||
FoodCategory.values().find { it.displayName == entry.key } ?: FoodCategory.OTHER
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
|
||||||
) {
|
|
||||||
PlateDiagram(
|
|
||||||
categories = foodCategories,
|
|
||||||
modifier = Modifier.size(200.dp)
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
|
||||||
// Breakdown kalori per kategori
|
// Tombol hapus
|
||||||
Text(
|
Button(
|
||||||
text = "Komposisi Kalori:",
|
onClick = {
|
||||||
style = TextStyle(
|
if (user != null) {
|
||||||
fontSize = 18.sp,
|
isDeleting = true
|
||||||
color = primary,
|
caloryHistoryViewModel.deleteCaloryData(
|
||||||
fontWeight = FontWeight.Bold
|
date = caloryDate,
|
||||||
)
|
calories = caloryCalories,
|
||||||
)
|
imagePath = caloryImagePath,
|
||||||
|
username = user.username
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
) { success ->
|
||||||
|
isDeleting = false
|
||||||
// Tampilkan breakdown per kategori
|
if (success) {
|
||||||
history.foodComposition.forEach { (categoryName, percentage) ->
|
navController.navigateUp()
|
||||||
val category = FoodCategory.values().find { it.displayName == categoryName }
|
}
|
||||||
val colorHex = category?.colorHex ?: "#A0A0A0"
|
}
|
||||||
val icon = category?.icon ?: "🍽️"
|
}
|
||||||
val calories = history.caloriesPerCategory[categoryName] ?: 0
|
},
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 4.dp),
|
.height(56.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
enabled = !isDeleting
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "$icon $categoryName",
|
text = "Hapus Data Kalori",
|
||||||
color = Color(android.graphics.Color.parseColor(colorHex))
|
style = TextStyle(
|
||||||
|
fontSize = 18.sp,
|
||||||
|
color = Color.White,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(horizontalAlignment = Alignment.End) {
|
|
||||||
Text(
|
|
||||||
text = "$calories kal",
|
|
||||||
fontWeight = FontWeight.Medium
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "(%.0f%%)".format(percentage * 100),
|
|
||||||
color = Color.Gray,
|
|
||||||
fontSize = 12.sp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
|
||||||
|
|
||||||
// Tombol kembali ke riwayat
|
|
||||||
Button(
|
|
||||||
onClick = { navController.popBackStack() },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
colors = ButtonDefaults.buttonColors(backgroundColor = primary)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Kembali ke Riwayat",
|
|
||||||
color = Color.White,
|
|
||||||
modifier = Modifier.padding(vertical = 8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
} else if (viewModel.errorMessage != null) {
|
|
||||||
// Error message
|
|
||||||
Text(
|
|
||||||
text = viewModel.errorMessage!!,
|
|
||||||
color = Color.Red,
|
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Jika data tidak ditemukan
|
|
||||||
Text(
|
|
||||||
text = "Data tidak ditemukan",
|
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fungsi untuk memformat tanggal
|
||||||
|
fun formatDate(dateString: String): String {
|
||||||
|
return try {
|
||||||
|
val inputFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||||
|
val outputFormat = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault())
|
||||||
|
val date = inputFormat.parse(dateString)
|
||||||
|
outputFormat.format(date ?: Date())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
dateString // Kembalikan string asli jika format tidak sesuai
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -320,7 +320,293 @@ package com.example.caloryapp.pages.calorydetail
|
||||||
|
|
||||||
//package com.example.caloryapp.pages.camera
|
//package com.example.caloryapp.pages.camera
|
||||||
|
|
||||||
|
//import android.net.Uri
|
||||||
|
//import android.widget.Toast
|
||||||
|
//import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
//import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
//import androidx.compose.foundation.Image
|
||||||
|
//import androidx.compose.foundation.layout.Arrangement
|
||||||
|
//import androidx.compose.foundation.layout.Box
|
||||||
|
//import androidx.compose.foundation.layout.Column
|
||||||
|
//import androidx.compose.foundation.layout.Row
|
||||||
|
//import androidx.compose.foundation.layout.Spacer
|
||||||
|
//import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
//import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
//import androidx.compose.foundation.layout.height
|
||||||
|
//import androidx.compose.foundation.layout.padding
|
||||||
|
//import androidx.compose.foundation.layout.size
|
||||||
|
//import androidx.compose.foundation.layout.width
|
||||||
|
//import androidx.compose.foundation.rememberScrollState
|
||||||
|
//import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
//import androidx.compose.foundation.verticalScroll
|
||||||
|
//import androidx.compose.material3.Button
|
||||||
|
//import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
//import androidx.compose.material3.Text
|
||||||
|
//import androidx.compose.runtime.Composable
|
||||||
|
//import androidx.compose.runtime.getValue
|
||||||
|
//import androidx.compose.runtime.mutableStateOf
|
||||||
|
//import androidx.compose.runtime.remember
|
||||||
|
//import androidx.compose.runtime.setValue
|
||||||
|
//import androidx.compose.ui.Alignment
|
||||||
|
//import androidx.compose.ui.Modifier
|
||||||
|
//import androidx.compose.ui.draw.clip
|
||||||
|
//import androidx.compose.ui.graphics.Color
|
||||||
|
//import androidx.compose.ui.layout.ContentScale
|
||||||
|
//import androidx.compose.ui.platform.LocalContext
|
||||||
|
//import androidx.compose.ui.text.TextStyle
|
||||||
|
//import androidx.compose.ui.text.style.TextAlign
|
||||||
|
//import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
//import androidx.compose.ui.unit.dp
|
||||||
|
//import androidx.compose.ui.unit.sp
|
||||||
|
//import androidx.navigation.NavController
|
||||||
|
//import coil3.compose.rememberAsyncImagePainter
|
||||||
|
//import com.example.caloryapp.viewmodel.FoodDetectionViewModel
|
||||||
|
//import com.example.caloryapp.foodmodel.PlateDiagram
|
||||||
|
//import com.example.caloryapp.foodmodel.calculateTotalCalories
|
||||||
|
//import com.example.caloryapp.repository.CaloryRepository
|
||||||
|
//import com.example.caloryapp.repository.UserRepository
|
||||||
|
//import com.example.caloryapp.ui.theme.bold
|
||||||
|
//import com.example.caloryapp.ui.theme.medium
|
||||||
|
//import com.example.caloryapp.ui.theme.primary
|
||||||
|
//import com.example.caloryapp.ui.theme.primaryblack
|
||||||
|
//import com.example.caloryapp.ui.theme.semibold
|
||||||
|
//import com.example.caloryapp.viewmodel.UserViewModel
|
||||||
|
//import com.google.firebase.auth.FirebaseAuth
|
||||||
|
//
|
||||||
|
//@Composable
|
||||||
|
//fun ScreenTest(navController: NavController, viewModel: FoodDetectionViewModel, userViewModel: UserViewModel) {
|
||||||
|
// val context = LocalContext.current
|
||||||
|
// var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
|
// var saveMessage by remember { mutableStateOf<String?>(null) }
|
||||||
|
// var isScanned by remember { mutableStateOf(false) }
|
||||||
|
// val caloryRepository = CaloryRepository()
|
||||||
|
// val viewmodel = userViewModel.user.value
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// val getContent = rememberLauncherForActivityResult(
|
||||||
|
// contract = ActivityResultContracts.GetContent()
|
||||||
|
// ) { uri: Uri? ->
|
||||||
|
// uri?.let {
|
||||||
|
// selectedImageUri = it
|
||||||
|
// viewModel.detectFoodFromImage(context, it)
|
||||||
|
// isScanned = true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Column(
|
||||||
|
// modifier = Modifier
|
||||||
|
// .fillMaxSize()
|
||||||
|
// .padding(horizontal = 25.dp, vertical = 50.dp)
|
||||||
|
// .verticalScroll(rememberScrollState()),
|
||||||
|
// ) {
|
||||||
|
// Spacer(modifier = Modifier.height(60.dp))
|
||||||
|
//
|
||||||
|
// Text(
|
||||||
|
// text = "Pindai Makanan Kamu",
|
||||||
|
// style = TextStyle(
|
||||||
|
// fontSize = 38.sp,
|
||||||
|
// color = primary,
|
||||||
|
// fontFamily = bold
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// Spacer(modifier = Modifier.height(18.dp))
|
||||||
|
//
|
||||||
|
// Text(
|
||||||
|
// text = "Penuhi Kebutuhan Kalori Kamu Hari ini Yuk!",
|
||||||
|
// style = TextStyle(
|
||||||
|
// fontSize = 21.sp,
|
||||||
|
// color = primary,
|
||||||
|
// fontFamily = bold
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// // Selected image preview
|
||||||
|
// if (selectedImageUri != null) {
|
||||||
|
// Spacer(modifier = Modifier.height(40.dp))
|
||||||
|
// Row(Modifier.fillMaxWidth(), Arrangement.Center) {
|
||||||
|
// Image(
|
||||||
|
// painter = rememberAsyncImagePainter(selectedImageUri),
|
||||||
|
// contentDescription = "Selected image",
|
||||||
|
// modifier = Modifier
|
||||||
|
// .size(250.dp)
|
||||||
|
// .clip(RoundedCornerShape(8.dp)),
|
||||||
|
// contentScale = ContentScale.Crop
|
||||||
|
// )
|
||||||
|
// Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
// }
|
||||||
|
// Spacer(modifier = Modifier.height(40.dp))
|
||||||
|
// } else {
|
||||||
|
// // Placeholder
|
||||||
|
// Spacer(modifier = Modifier.height(40.dp))
|
||||||
|
// Row(Modifier.fillMaxWidth(), Arrangement.Center) {
|
||||||
|
// Box(
|
||||||
|
// modifier = Modifier
|
||||||
|
// .size(250.dp)
|
||||||
|
// .clip(RoundedCornerShape(8.dp))
|
||||||
|
// .padding(16.dp),
|
||||||
|
// contentAlignment = Alignment.Center
|
||||||
|
// ) {
|
||||||
|
// androidx.compose.material.Text(
|
||||||
|
// text = "Pilih Gambar Makanan",
|
||||||
|
// style = TextStyle(
|
||||||
|
// fontSize = 18.sp,
|
||||||
|
// color = primary,
|
||||||
|
// letterSpacing = 1.sp,
|
||||||
|
// fontFamily = medium,
|
||||||
|
// textAlign = TextAlign.Center
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
// }
|
||||||
|
// Spacer(modifier = Modifier.height(40.dp))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Button to select image
|
||||||
|
// androidx.compose.material.Button(
|
||||||
|
// onClick = {
|
||||||
|
// if (isScanned) {
|
||||||
|
// val username = viewmodel!!.username
|
||||||
|
// val totalCalories = viewModel.detectionResult?.allCategories?.calculateTotalCalories() ?: 0
|
||||||
|
// caloryRepository.saveCalorieData(username, totalCalories) { success ->
|
||||||
|
// if (success) {
|
||||||
|
// Toast.makeText(
|
||||||
|
// context,
|
||||||
|
// "Berhasil",
|
||||||
|
// Toast.LENGTH_SHORT
|
||||||
|
// ).show()
|
||||||
|
// isScanned = false
|
||||||
|
// } else {
|
||||||
|
// Toast.makeText(
|
||||||
|
// context,
|
||||||
|
// "Gagal",
|
||||||
|
// Toast.LENGTH_SHORT
|
||||||
|
// ).show()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// getContent.launch("image/*")
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// modifier = Modifier
|
||||||
|
// .width(360.dp)
|
||||||
|
// .height(50.dp),
|
||||||
|
// colors = androidx.compose.material.ButtonDefaults.buttonColors(backgroundColor = primary),
|
||||||
|
// shape = RoundedCornerShape(20.dp)
|
||||||
|
// ) {
|
||||||
|
// androidx.compose.material.Text(
|
||||||
|
// text = if (isScanned) "Simpan" else "Pilih",
|
||||||
|
// style = TextStyle(
|
||||||
|
// fontSize = 18.sp,
|
||||||
|
// color = Color.White,
|
||||||
|
// fontFamily = semibold,
|
||||||
|
// textAlign = TextAlign.Center
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//// Button(
|
||||||
|
//// onClick = { },
|
||||||
|
//// modifier = Modifier.fillMaxWidth()
|
||||||
|
//// ) {
|
||||||
|
//// Text("Pilih Gambar dari Galeri")
|
||||||
|
//// }
|
||||||
|
//
|
||||||
|
// Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
//
|
||||||
|
// if (viewModel.isLoading) {
|
||||||
|
// CircularProgressIndicator()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// viewModel.errorMessage?.let { error ->
|
||||||
|
// Text(
|
||||||
|
// text = error,
|
||||||
|
// color = Color.Red,
|
||||||
|
// modifier = Modifier.padding(vertical = 8.dp)
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Detection results
|
||||||
|
// viewModel.detectionResult?.let { result ->
|
||||||
|
// Column(
|
||||||
|
// modifier = Modifier.fillMaxWidth(),
|
||||||
|
// horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
// ) {
|
||||||
|
//// Text(
|
||||||
|
//// text = "Hasil Deteksi",
|
||||||
|
//// fontSize = 20.sp,
|
||||||
|
//// fontWeight = FontWeight.Bold,
|
||||||
|
//// modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
//// )
|
||||||
|
//
|
||||||
|
// // Food plate diagram
|
||||||
|
//// PlateDiagram(
|
||||||
|
//// categories = result.allCategories,
|
||||||
|
//// modifier = Modifier
|
||||||
|
//// .size(200.dp)
|
||||||
|
//// .padding(8.dp)
|
||||||
|
//// )
|
||||||
|
//
|
||||||
|
// Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
//
|
||||||
|
// val totalCalories = result.allCategories.calculateTotalCalories()
|
||||||
|
//
|
||||||
|
//// Text(
|
||||||
|
//// text = "Kategori Utama: ${result.mainCategory.displayName}",
|
||||||
|
//// fontWeight = FontWeight.SemiBold
|
||||||
|
//// )
|
||||||
|
//
|
||||||
|
// Text(
|
||||||
|
// text = "$totalCalories",
|
||||||
|
// style = TextStyle(
|
||||||
|
// fontSize = 24.sp,
|
||||||
|
// color = primary,
|
||||||
|
// fontFamily = bold,
|
||||||
|
// textDecoration = TextDecoration.Underline
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// Text(
|
||||||
|
// text = "Kalori di Makanan Kamu Hari ini",
|
||||||
|
// style = TextStyle(
|
||||||
|
// fontSize = 18.sp,
|
||||||
|
// color = primary,
|
||||||
|
// fontFamily = semibold
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
// result.allCategories.forEach { (category, confidence) ->
|
||||||
|
// Row(
|
||||||
|
// modifier = Modifier
|
||||||
|
// .fillMaxWidth()
|
||||||
|
// .padding(vertical = 4.dp),
|
||||||
|
// horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
// ) {
|
||||||
|
// Text(
|
||||||
|
// text = "${category.icon} ${category.displayName}",
|
||||||
|
// color = Color(android.graphics.Color.parseColor(category.colorHex))
|
||||||
|
// )
|
||||||
|
// Text(
|
||||||
|
// text = "%.0f%%".format(confidence * 100),
|
||||||
|
// style = TextStyle(
|
||||||
|
// fontSize = 14.sp,
|
||||||
|
// color = primaryblack,
|
||||||
|
// fontFamily = medium
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
@ -338,10 +624,12 @@ import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material.Button
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material.ButtonDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
@ -358,28 +646,80 @@ import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
import coil3.compose.rememberAsyncImagePainter
|
import coil3.compose.rememberAsyncImagePainter
|
||||||
import com.example.caloryapp.viewmodel.FoodDetectionViewModel
|
import com.example.caloryapp.viewmodel.FoodDetectionViewModel
|
||||||
import com.example.caloryapp.foodmodel.PlateDiagram
|
import com.example.caloryapp.foodmodel.PlateDiagram
|
||||||
import com.example.caloryapp.foodmodel.calculateTotalCalories
|
import com.example.caloryapp.foodmodel.calculateTotalCalories
|
||||||
|
import com.example.caloryapp.repository.CaloryRepository
|
||||||
import com.example.caloryapp.ui.theme.bold
|
import com.example.caloryapp.ui.theme.bold
|
||||||
import com.example.caloryapp.ui.theme.medium
|
import com.example.caloryapp.ui.theme.medium
|
||||||
import com.example.caloryapp.ui.theme.primary
|
import com.example.caloryapp.ui.theme.primary
|
||||||
import com.example.caloryapp.ui.theme.primaryblack
|
import com.example.caloryapp.ui.theme.primaryblack
|
||||||
import com.example.caloryapp.ui.theme.semibold
|
import com.example.caloryapp.ui.theme.semibold
|
||||||
|
import com.example.caloryapp.utils.LocalImageStorage
|
||||||
|
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
||||||
|
import com.example.caloryapp.viewmodel.UserViewModel
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ScreenTest(navController: NavController, viewModel: FoodDetectionViewModel) {
|
fun ScreenTest(
|
||||||
|
navController: NavController,
|
||||||
|
viewModel: FoodDetectionViewModel,
|
||||||
|
userViewModel: UserViewModel
|
||||||
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
|
var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
|
var capturedBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
|
var isScanned by remember { mutableStateOf(false) }
|
||||||
|
var isSaving by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// Tambahkan instance CaloryHistoryViewModel untuk menyimpan data dengan gambar
|
||||||
|
val caloryHistoryViewModel = remember { CaloryHistoryViewModel() }
|
||||||
|
val user = userViewModel.user.value
|
||||||
|
|
||||||
|
LaunchedEffect(viewModel.errorMessage) {
|
||||||
|
viewModel.errorMessage?.let { error ->
|
||||||
|
Toast.makeText(context, error, Toast.LENGTH_LONG).show()
|
||||||
|
|
||||||
|
// Reset state jika ada error tentang gambar bukan makanan
|
||||||
|
if (error.contains("Pindai Makanan Gagal!")) {
|
||||||
|
isScanned = false
|
||||||
|
// Anda juga bisa menambahkan animasi shake atau highlight pada area pemilihan gambar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(viewModel.detectionResult) {
|
||||||
|
// Jika deteksi berhasil dan hasilnya tidak null, maka gambar adalah makanan
|
||||||
|
isScanned = viewModel.detectionResult != null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launcher untuk memilih gambar
|
||||||
val getContent = rememberLauncherForActivityResult(
|
val getContent = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.GetContent()
|
contract = ActivityResultContracts.GetContent()
|
||||||
) { uri: Uri? ->
|
) { uri: Uri? ->
|
||||||
uri?.let {
|
uri?.let {
|
||||||
selectedImageUri = it
|
selectedImageUri = it
|
||||||
viewModel.detectFoodFromImage(context, it)
|
|
||||||
|
// Konversi URI ke Bitmap
|
||||||
|
try {
|
||||||
|
capturedBitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
val source = ImageDecoder.createSource(context.contentResolver, it)
|
||||||
|
ImageDecoder.decodeBitmap(source)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
MediaStore.Images.Media.getBitmap(context.contentResolver, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deteksi makanan dari gambar
|
||||||
|
viewModel.detectFoodFromImage(context, it)
|
||||||
|
isScanned = true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(context, "Gagal memuat gambar: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -436,7 +776,7 @@ fun ScreenTest(navController: NavController, viewModel: FoodDetectionViewModel)
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
androidx.compose.material.Text(
|
Text(
|
||||||
text = "Pilih Gambar Makanan",
|
text = "Pilih Gambar Makanan",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 18.sp,
|
fontSize = 18.sp,
|
||||||
|
@ -452,42 +792,74 @@ fun ScreenTest(navController: NavController, viewModel: FoodDetectionViewModel)
|
||||||
Spacer(modifier = Modifier.height(40.dp))
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Button to select image
|
// Button to select or save image
|
||||||
androidx.compose.material.Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
getContent.launch("image/*")
|
if (isScanned && user != null && capturedBitmap != null) {
|
||||||
|
// Jika sudah di-scan, simpan data dengan gambar
|
||||||
|
isSaving = true
|
||||||
|
|
||||||
|
// Gunakan CaloryHistoryViewModel untuk menyimpan dengan gambar
|
||||||
|
caloryHistoryViewModel.saveCaloryData(
|
||||||
|
context = context,
|
||||||
|
bitmap = capturedBitmap!!,
|
||||||
|
calories = viewModel.detectionResult?.allCategories?.calculateTotalCalories() ?: 0,
|
||||||
|
username = user.username
|
||||||
|
) { success, message ->
|
||||||
|
isSaving = false
|
||||||
|
if (success) {
|
||||||
|
Toast.makeText(context, "Berhasil menyimpan data kalori", Toast.LENGTH_SHORT).show()
|
||||||
|
// Reset state setelah berhasil
|
||||||
|
isScanned = false
|
||||||
|
selectedImageUri = null
|
||||||
|
capturedBitmap = null
|
||||||
|
// Kembali ke halaman sebelumnya
|
||||||
|
navController.navigateUp()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Gagal menyimpan data: $message", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Jika belum scan, buka gallery
|
||||||
|
getContent.launch("image/*")
|
||||||
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(360.dp)
|
.width(360.dp)
|
||||||
.height(50.dp),
|
.height(50.dp),
|
||||||
colors = androidx.compose.material.ButtonDefaults.buttonColors(backgroundColor = primary),
|
colors = ButtonDefaults.buttonColors(backgroundColor = primary),
|
||||||
shape = RoundedCornerShape(20.dp)
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
enabled = !isSaving // Disable saat proses penyimpanan
|
||||||
) {
|
) {
|
||||||
androidx.compose.material.Text(
|
if (isSaving) {
|
||||||
text = "Pilih",
|
CircularProgressIndicator(
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 18.sp,
|
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
fontFamily = semibold,
|
modifier = Modifier.size(24.dp)
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
)
|
||||||
)
|
} else {
|
||||||
|
Text(
|
||||||
|
text = if (isScanned) "Simpan" else "Pilih",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 18.sp,
|
||||||
|
color = Color.White,
|
||||||
|
fontFamily = semibold,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Button(
|
|
||||||
// onClick = { },
|
|
||||||
// modifier = Modifier.fillMaxWidth()
|
|
||||||
// ) {
|
|
||||||
// Text("Pilih Gambar dari Galeri")
|
|
||||||
// }
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
// Loading indicator
|
|
||||||
if (viewModel.isLoading) {
|
if (viewModel.isLoading) {
|
||||||
CircularProgressIndicator()
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(color = primary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error message
|
|
||||||
viewModel.errorMessage?.let { error ->
|
viewModel.errorMessage?.let { error ->
|
||||||
Text(
|
Text(
|
||||||
text = error,
|
text = error,
|
||||||
|
@ -502,31 +874,10 @@ fun ScreenTest(navController: NavController, viewModel: FoodDetectionViewModel)
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
// Text(
|
|
||||||
// text = "Hasil Deteksi",
|
|
||||||
// fontSize = 20.sp,
|
|
||||||
// fontWeight = FontWeight.Bold,
|
|
||||||
// modifier = Modifier.padding(bottom = 16.dp)
|
|
||||||
// )
|
|
||||||
|
|
||||||
// Food plate diagram
|
|
||||||
PlateDiagram(
|
|
||||||
categories = result.allCategories,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(200.dp)
|
|
||||||
.padding(8.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// Main category and caloriesd
|
|
||||||
val totalCalories = result.allCategories.calculateTotalCalories()
|
val totalCalories = result.allCategories.calculateTotalCalories()
|
||||||
|
|
||||||
// Text(
|
|
||||||
// text = "Kategori Utama: ${result.mainCategory.displayName}",
|
|
||||||
// fontWeight = FontWeight.SemiBold
|
|
||||||
// )
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "$totalCalories",
|
text = "$totalCalories",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
|
@ -546,28 +897,123 @@ fun ScreenTest(navController: NavController, viewModel: FoodDetectionViewModel)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
result.allCategories.forEach { (category, confidence) ->
|
// result.allCategories.forEach { (category, confidence) ->
|
||||||
Row(
|
Column() {
|
||||||
modifier = Modifier
|
Row(
|
||||||
.fillMaxWidth()
|
modifier = Modifier
|
||||||
.padding(vertical = 4.dp),
|
.fillMaxWidth()
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
.padding(vertical = 4.dp),
|
||||||
) {
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
Text(
|
) {
|
||||||
text = "${category.icon} ${category.displayName}",
|
Text(
|
||||||
color = Color(android.graphics.Color.parseColor(category.colorHex))
|
text = "Karbohidrat",
|
||||||
)
|
style = TextStyle(
|
||||||
Text(
|
fontSize = 15.sp,
|
||||||
text = "%.0f%%".format(confidence * 100),
|
color = primary,
|
||||||
style = TextStyle(
|
fontFamily = bold
|
||||||
fontSize = 14.sp,
|
)
|
||||||
color = primaryblack,
|
|
||||||
fontFamily = medium
|
|
||||||
)
|
)
|
||||||
)
|
Text(
|
||||||
|
text = "50 %",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 15.sp,
|
||||||
|
color = primary,
|
||||||
|
fontFamily = semibold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Protein",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 15.sp,
|
||||||
|
color = primary,
|
||||||
|
fontFamily = bold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "30 %",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 15.sp,
|
||||||
|
color = primary,
|
||||||
|
fontFamily = semibold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Sayur",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 15.sp,
|
||||||
|
color = primary,
|
||||||
|
fontFamily = bold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "10 %",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 15.sp,
|
||||||
|
color = primary,
|
||||||
|
fontFamily = semibold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Buah",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 15.sp,
|
||||||
|
color = primary,
|
||||||
|
fontFamily = bold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "10 %",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 15.sp,
|
||||||
|
color = primary,
|
||||||
|
fontFamily = semibold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
// Row(
|
||||||
|
// modifier = Modifier
|
||||||
|
// .fillMaxWidth()
|
||||||
|
// .padding(vertical = 4.dp),
|
||||||
|
// horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
// ) {
|
||||||
|
// Text(
|
||||||
|
// text = "${category.icon} ${category.displayName}",
|
||||||
|
// color = Color(android.graphics.Color.parseColor(category.colorHex))
|
||||||
|
// )
|
||||||
|
// Text(
|
||||||
|
// text = "%.0f%%".format(confidence * 100),
|
||||||
|
// style = TextStyle(
|
||||||
|
// fontSize = 14.sp,
|
||||||
|
// color = primaryblack,
|
||||||
|
// fontFamily = medium
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.example.caloryapp.pages.dashboard
|
package com.example.caloryapp.pages.dashboard
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
@ -21,6 +22,8 @@ import androidx.compose.material.Card
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
import androidx.compose.material.FloatingActionButton
|
import androidx.compose.material.FloatingActionButton
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.DrawerState
|
import androidx.compose.material3.DrawerState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
@ -31,7 +34,9 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
@ -39,17 +44,31 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.example.caloryapp.R
|
import com.example.caloryapp.R
|
||||||
|
import com.example.caloryapp.model.CaloryModel
|
||||||
|
import com.example.caloryapp.model.isThisMonth
|
||||||
|
import com.example.caloryapp.model.isThisWeek
|
||||||
|
import com.example.caloryapp.model.isToday
|
||||||
import com.example.caloryapp.navigation.NavigationScreen
|
import com.example.caloryapp.navigation.NavigationScreen
|
||||||
|
import com.example.caloryapp.repository.CaloryRepository
|
||||||
import com.example.caloryapp.ui.theme.background
|
import com.example.caloryapp.ui.theme.background
|
||||||
import com.example.caloryapp.ui.theme.bold
|
import com.example.caloryapp.ui.theme.bold
|
||||||
import com.example.caloryapp.ui.theme.medium
|
import com.example.caloryapp.ui.theme.medium
|
||||||
import com.example.caloryapp.ui.theme.primary
|
import com.example.caloryapp.ui.theme.primary
|
||||||
|
import com.example.caloryapp.ui.theme.primary2
|
||||||
|
import com.example.caloryapp.ui.theme.primarygrey
|
||||||
import com.example.caloryapp.ui.theme.semibold
|
import com.example.caloryapp.ui.theme.semibold
|
||||||
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
||||||
|
import com.example.caloryapp.viewmodel.LoginState
|
||||||
import com.example.caloryapp.viewmodel.UserViewModel
|
import com.example.caloryapp.viewmodel.UserViewModel
|
||||||
import com.example.caloryapp.widget.FilterBar
|
import com.example.caloryapp.widget.FilterBar
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import java.net.URLEncoder
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen(
|
fun HomeScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
|
@ -58,13 +77,55 @@ fun HomeScreen(
|
||||||
caloryHistoryViewModel: CaloryHistoryViewModel,
|
caloryHistoryViewModel: CaloryHistoryViewModel,
|
||||||
viewModel: UserViewModel,
|
viewModel: UserViewModel,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
var selectedFilter by remember { mutableStateOf("Semua") }
|
var selectedFilter by remember { mutableStateOf("Semua") }
|
||||||
val user = viewModel.user.value
|
val user = viewModel.user.value
|
||||||
|
val caloryRepository = CaloryRepository()
|
||||||
|
var calorieList by remember { mutableStateOf(listOf<CaloryModel>()) }
|
||||||
|
|
||||||
|
// Cek apakah user null, jika ya, arahkan ke login
|
||||||
|
if (user == null) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
Log.d("HomeScreen", "User adalah null, mengarahkan ke halaman login")
|
||||||
|
Toast.makeText(context, "Silakan login terlebih dahulu", Toast.LENGTH_SHORT).show()
|
||||||
|
navController.navigate(NavigationScreen.LoginScreen.name) {
|
||||||
|
popUpTo(0) { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tampilkan loading sampai navigasi selesai
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(background),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(color = primary)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika user tidak null, lanjutkan dengan logika normal
|
||||||
|
val filteredList = when (selectedFilter) {
|
||||||
|
"Hari ini" -> calorieList.filter { it.isToday() }
|
||||||
|
"Minggu Ini" -> calorieList.filter { it.isThisWeek() }
|
||||||
|
"Bulan Ini" -> calorieList.filter { it.isThisMonth() }
|
||||||
|
else -> calorieList
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(user) {
|
LaunchedEffect(user) {
|
||||||
user?.let {
|
try {
|
||||||
// Memuat 4 riwayat terbaru untuk ditampilkan di home
|
// Memuat 4 riwayat terbaru untuk ditampilkan di home
|
||||||
caloryHistoryViewModel.loadHistoryByUsername(it.username, 2)
|
caloryHistoryViewModel.loadHistoryByUsername(user.username, 2)
|
||||||
|
|
||||||
|
// Gunakan try-catch untuk menangkap error saat mengambil data
|
||||||
|
caloryRepository.getCalorieData(user.username) { data ->
|
||||||
|
calorieList = data
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("HomeScreen", "Error loading data: ${e.message}")
|
||||||
|
// Tangani error dengan menampilkan pesan
|
||||||
|
Toast.makeText(context, "Gagal memuat data: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +151,7 @@ fun HomeScreen(
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Column(horizontalAlignment = Alignment.End) {
|
Column(horizontalAlignment = Alignment.End) {
|
||||||
Text(
|
Text(
|
||||||
text = user!!.fullName,
|
text = user.fullName, // Aman karena user sudah dipastikan tidak null di atas
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 20.sp,
|
fontSize = 20.sp,
|
||||||
color = Color.Black,
|
color = Color.Black,
|
||||||
|
@ -117,10 +178,10 @@ fun HomeScreen(
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(35.dp))
|
Spacer(modifier = Modifier.height(35.dp))
|
||||||
|
|
||||||
// Teks sapaan
|
// Teks sapaan - Sudah aman karena user dipastikan tidak null
|
||||||
Row(Modifier.width(215.dp)) {
|
Row(Modifier.width(215.dp)) {
|
||||||
Text(
|
Text(
|
||||||
text = "Hai ${user!!.fullName}, Bagaimana kabar kamu hari ini?",
|
text = "Hai ${user.fullName}, Bagaimana kabar kamu hari ini?",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 22.sp,
|
fontSize = 22.sp,
|
||||||
color = Color.Black,
|
color = Color.Black,
|
||||||
|
@ -129,13 +190,6 @@ fun HomeScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spacer(modifier = Modifier.height(15.dp))
|
|
||||||
|
|
||||||
// Tombol untuk membuka Navigation Drawer
|
|
||||||
// Button(onClick = { }) {
|
|
||||||
// Text(text = "Buka Menu")
|
|
||||||
// }
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(15.dp))
|
Spacer(modifier = Modifier.height(15.dp))
|
||||||
androidx.compose.material3.Divider(color = primary.copy(alpha = 0.2f), thickness = 3.dp)
|
androidx.compose.material3.Divider(color = primary.copy(alpha = 0.2f), thickness = 3.dp)
|
||||||
Spacer(modifier = Modifier.height(15.dp))
|
Spacer(modifier = Modifier.height(15.dp))
|
||||||
|
@ -144,12 +198,7 @@ fun HomeScreen(
|
||||||
FilterBar(selectedFilter = selectedFilter, onFilterSelected = { selectedFilter = it })
|
FilterBar(selectedFilter = selectedFilter, onFilterSelected = { selectedFilter = it })
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
Box(modifier = Modifier.fillMaxWidth()) {
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
if (caloryHistoryViewModel.isLoading) {
|
if (filteredList.isEmpty()) {
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.align(Alignment.Center),
|
|
||||||
color = primary
|
|
||||||
)
|
|
||||||
} else if (caloryHistoryViewModel.historyList.isEmpty()) {
|
|
||||||
Text(
|
Text(
|
||||||
text = "Belum ada riwayat makanan",
|
text = "Belum ada riwayat makanan",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
|
@ -162,61 +211,54 @@ fun HomeScreen(
|
||||||
.padding(vertical = 24.dp)
|
.padding(vertical = 24.dp)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// List riwayat kalori menggunakan LazyColumn khusus
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
items(caloryHistoryViewModel.historyList) { history ->
|
items(filteredList) { calory ->
|
||||||
// Card item untuk riwayat kalori
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.size(width = 365.dp, height = 100.dp)
|
||||||
.clickable {
|
.padding(vertical = 8.dp)
|
||||||
// navController.navigate("${NavigationScreen.DetailHistory.name}/${history.id}")
|
.fillMaxSize(),
|
||||||
},
|
backgroundColor = primary2,
|
||||||
elevation = 4.dp,
|
shape = RoundedCornerShape(20.dp),
|
||||||
shape = RoundedCornerShape(16.dp)
|
onClick = {
|
||||||
) {
|
val encodedImagePath = URLEncoder.encode(calory.imagePath, StandardCharsets.UTF_8.name())
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(Color(0xFF4AB54A).copy(alpha = 0.6f)) // Warna hijau sesuai gambar
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(
|
|
||||||
text = "${history.totalCalories} Kalori",
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 20.sp,
|
|
||||||
color = Color.White,
|
|
||||||
fontFamily = semibold
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
// Navigasi ke halaman detail
|
||||||
text = "Lihat Detail",
|
navController.navigate(
|
||||||
style = TextStyle(
|
"${NavigationScreen.CaloryDetailScreen.name}/${calory.date}/${calory.calories}/${encodedImagePath}"
|
||||||
fontSize = 14.sp,
|
)
|
||||||
color = Color.White,
|
}
|
||||||
fontFamily = medium,
|
) {
|
||||||
textDecoration = TextDecoration.Underline
|
Column(modifier = Modifier
|
||||||
)
|
.fillMaxSize()
|
||||||
)
|
.padding(16.dp),
|
||||||
}
|
Arrangement.Center,) {
|
||||||
}
|
Text(
|
||||||
|
text = "${calory.calories} Kalori",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 26.sp,
|
||||||
|
color = Color.White,
|
||||||
|
fontFamily = semibold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
androidx.compose.material3.Divider(modifier = Modifier.width(180.dp), color = Color.White, thickness = 3.dp)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "Lihat Detail",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 13.sp,
|
||||||
|
color = Color.White,
|
||||||
|
fontFamily = semibold,
|
||||||
|
textDecoration = TextDecoration.Underline
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spacer di akhir list
|
|
||||||
item {
|
|
||||||
Spacer(modifier = Modifier.height(70.dp))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.Button
|
import androidx.compose.material.Button
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
@ -32,6 +33,7 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
@ -100,7 +102,8 @@ fun ChangePasswordScreen(modifier: Modifier = Modifier, navController: NavContro
|
||||||
value = username,
|
value = username,
|
||||||
onValueChange = { username = it },
|
onValueChange = { username = it },
|
||||||
input = true,
|
input = true,
|
||||||
placeholderText = "Masukkan Username Anda"
|
placeholderText = "Masukkan Username Anda",
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||||
)
|
)
|
||||||
Spacer(modifier.height(20.dp))
|
Spacer(modifier.height(20.dp))
|
||||||
androidx.compose.material.Text(
|
androidx.compose.material.Text(
|
||||||
|
|
|
@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.Button
|
import androidx.compose.material.Button
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
@ -23,6 +24,7 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
@ -80,7 +82,8 @@ fun ForgotPasswordScreen(modifier: Modifier = Modifier, navController: NavContro
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
value = gmail,
|
value = gmail,
|
||||||
onValueChange = { gmail = it },input = true,
|
onValueChange = { gmail = it },input = true,
|
||||||
placeholderText = stringResource(R.string.gmail)
|
placeholderText = stringResource(R.string.gmail),
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
|
||||||
)
|
)
|
||||||
Spacer(modifier.height(40.dp))
|
Spacer(modifier.height(40.dp))
|
||||||
Button(
|
Button(
|
||||||
|
|
|
@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.Button
|
import androidx.compose.material.Button
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
|
@ -30,10 +31,12 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.key.Key
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
@ -61,7 +64,7 @@ fun LoginScreen(
|
||||||
) {
|
) {
|
||||||
var username by remember { mutableStateOf("") }
|
var username by remember { mutableStateOf("") }
|
||||||
var password by remember { mutableStateOf("") }
|
var password by remember { mutableStateOf("") }
|
||||||
val state = viewModel.loginstate.value
|
val state = viewModel.loginState.value
|
||||||
val userData = viewModel.user.value
|
val userData = viewModel.user.value
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
@ -115,7 +118,8 @@ fun LoginScreen(
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
value = username,
|
value = username,
|
||||||
onValueChange = { username = it }, input = true,
|
onValueChange = { username = it }, input = true,
|
||||||
placeholderText = "Masukkan Username"
|
placeholderText = "Masukkan Username",
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||||
)
|
)
|
||||||
Spacer(modifier.height(16.dp))
|
Spacer(modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
|
|
|
@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.Button
|
import androidx.compose.material.Button
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
|
@ -26,6 +27,7 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
@ -124,7 +126,8 @@ fun RegisterScreen(
|
||||||
value = username,
|
value = username,
|
||||||
onValueChange = { username = it },
|
onValueChange = { username = it },
|
||||||
placeholderText = "Masukkan Username",
|
placeholderText = "Masukkan Username",
|
||||||
input = true
|
input = true,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||||
)
|
)
|
||||||
|
|
||||||
// gmail
|
// gmail
|
||||||
|
@ -142,7 +145,8 @@ fun RegisterScreen(
|
||||||
value = gmail,
|
value = gmail,
|
||||||
onValueChange = { gmail = it },
|
onValueChange = { gmail = it },
|
||||||
input = true,
|
input = true,
|
||||||
placeholderText = "Masukkan Email"
|
placeholderText = "Masukkan Email",
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
|
||||||
)
|
)
|
||||||
|
|
||||||
// nama lengkap
|
// nama lengkap
|
||||||
|
@ -159,7 +163,8 @@ fun RegisterScreen(
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
value = fullName,
|
value = fullName,
|
||||||
onValueChange = { fullName = it }, input = true,
|
onValueChange = { fullName = it }, input = true,
|
||||||
placeholderText = "Masukkan Nama Lengkap"
|
placeholderText = "Masukkan Nama Lengkap",
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||||
)
|
)
|
||||||
|
|
||||||
// pw
|
// pw
|
||||||
|
@ -179,7 +184,9 @@ fun RegisterScreen(
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
value = password,
|
value = password,
|
||||||
onValueChange = { password = it }, input = true,
|
onValueChange = { password = it }, input = true,
|
||||||
placeholderText = "Masukkan Kata Sandi"
|
placeholderText = "Masukkan Kata Sandi",
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// gender
|
// gender
|
||||||
|
@ -211,7 +218,9 @@ fun RegisterScreen(
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
value = weight.toString(),
|
value = weight.toString(),
|
||||||
onValueChange = { weight = it }, input = true,
|
onValueChange = { weight = it }, input = true,
|
||||||
placeholderText = "Masukkan Berat Badan"
|
placeholderText = "Masukkan Berat Badan",
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// tinggi badan
|
// tinggi badan
|
||||||
|
@ -228,7 +237,8 @@ fun RegisterScreen(
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
value = height.toString(),
|
value = height.toString(),
|
||||||
onValueChange = { height = it }, input = true,
|
onValueChange = { height = it }, input = true,
|
||||||
placeholderText = "Masukkan Tinggi Badan"
|
placeholderText = "Masukkan Tinggi Badan",
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||||
)
|
)
|
||||||
|
|
||||||
// btn daftar
|
// btn daftar
|
||||||
|
|
|
@ -1,17 +1,246 @@
|
||||||
package com.example.caloryapp.repository
|
package com.example.caloryapp.repository
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.example.caloryapp.foodmodel.FoodCategory
|
import com.example.caloryapp.foodmodel.FoodCategory
|
||||||
import com.example.caloryapp.foodmodel.FoodDetectionResult
|
import com.example.caloryapp.foodmodel.FoodDetectionResult
|
||||||
import com.example.caloryapp.model.CaloryHistoryModel
|
import com.example.caloryapp.model.CaloryHistoryModel
|
||||||
|
import com.example.caloryapp.model.CaloryModel
|
||||||
|
import com.example.caloryapp.utils.LocalImageStorage
|
||||||
import com.google.firebase.Timestamp
|
import com.google.firebase.Timestamp
|
||||||
import com.google.firebase.firestore.FirebaseFirestore
|
import com.google.firebase.firestore.FirebaseFirestore
|
||||||
import com.google.firebase.firestore.Query
|
import com.google.firebase.firestore.Query
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
|
||||||
class CaloryRepository {
|
class CaloryRepository {
|
||||||
private val db = FirebaseFirestore.getInstance()
|
|
||||||
private val TAG = "CaloryRepository"
|
private val TAG = "CaloryRepository"
|
||||||
|
private val db = FirebaseFirestore.getInstance()
|
||||||
|
|
||||||
|
// Mendapatkan data kalori dari database
|
||||||
|
fun getCalorieData(username: String, onComplete: (List<CaloryModel>) -> Unit) {
|
||||||
|
db.collection("users")
|
||||||
|
.whereEqualTo("username", username)
|
||||||
|
.get()
|
||||||
|
.addOnSuccessListener { result ->
|
||||||
|
if (!result.isEmpty) {
|
||||||
|
val userDocument = result.documents[0]
|
||||||
|
userDocument.reference.collection("calorieData")
|
||||||
|
.orderBy("date", Query.Direction.DESCENDING)
|
||||||
|
.get()
|
||||||
|
.addOnSuccessListener { snapshot ->
|
||||||
|
val calorieList = mutableListOf<CaloryModel>()
|
||||||
|
for (document in snapshot.documents) {
|
||||||
|
val calories = document.getLong("calories")?.toInt() ?: 0
|
||||||
|
val date = document.getString("date") ?: "Unknown"
|
||||||
|
val imagePath = document.getString("imagePath") ?: ""
|
||||||
|
|
||||||
|
val caloryData = CaloryModel(
|
||||||
|
calories = calories,
|
||||||
|
date = date,
|
||||||
|
username = username,
|
||||||
|
imagePath = imagePath
|
||||||
|
)
|
||||||
|
calorieList.add(caloryData)
|
||||||
|
Log.d(TAG, "Kalori: $calories, Tanggal: $date, Image: $imagePath")
|
||||||
|
}
|
||||||
|
onComplete(calorieList)
|
||||||
|
}
|
||||||
|
.addOnFailureListener { e ->
|
||||||
|
Log.e(TAG, "Gagal mengambil data kalori: ${e.message}")
|
||||||
|
onComplete(emptyList())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Pengguna tidak ditemukan: $username")
|
||||||
|
onComplete(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.addOnFailureListener { e ->
|
||||||
|
Log.e(TAG, "Gagal mencari pengguna: ${e.message}")
|
||||||
|
onComplete(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menyimpan data kalori dengan gambar
|
||||||
|
fun saveCaloryData(
|
||||||
|
context: Context,
|
||||||
|
bitmap: Bitmap,
|
||||||
|
calories: Int,
|
||||||
|
username: String,
|
||||||
|
onComplete: (Boolean, String?) -> Unit
|
||||||
|
) {
|
||||||
|
// 1. Simpan gambar ke penyimpanan lokal
|
||||||
|
val imagePath = LocalImageStorage.saveImageToInternalStorage(context, bitmap)
|
||||||
|
|
||||||
|
if (imagePath.isEmpty()) {
|
||||||
|
onComplete(false, "Gagal menyimpan gambar")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Format tanggal saat ini (hanya tanggal, tanpa waktu)
|
||||||
|
val currentDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||||
|
.format(Date())
|
||||||
|
|
||||||
|
// 3. Siapkan data kalori untuk disimpan
|
||||||
|
val caloryData = hashMapOf(
|
||||||
|
"calories" to calories,
|
||||||
|
"date" to currentDate,
|
||||||
|
"username" to username,
|
||||||
|
"imagePath" to imagePath
|
||||||
|
)
|
||||||
|
|
||||||
|
// 4. Simpan ke Firestore sesuai struktur database Anda
|
||||||
|
db.collection("users")
|
||||||
|
.whereEqualTo("username", username)
|
||||||
|
.get()
|
||||||
|
.addOnSuccessListener { result ->
|
||||||
|
if (!result.isEmpty) {
|
||||||
|
val userDocument = result.documents[0]
|
||||||
|
val calorieId = db.collection("users")
|
||||||
|
.document(userDocument.id)
|
||||||
|
.collection("calorieData")
|
||||||
|
.document().id
|
||||||
|
|
||||||
|
userDocument.reference.collection("calorieData")
|
||||||
|
.document(calorieId)
|
||||||
|
.set(caloryData)
|
||||||
|
.addOnSuccessListener {
|
||||||
|
Log.d(TAG, "Data kalori berhasil disimpan dengan ID: $calorieId")
|
||||||
|
onComplete(true, calorieId)
|
||||||
|
}
|
||||||
|
.addOnFailureListener { e ->
|
||||||
|
Log.e(TAG, "Gagal menyimpan data: ${e.message}")
|
||||||
|
// Jika gagal menyimpan ke Firestore, hapus gambar lokal
|
||||||
|
LocalImageStorage.deleteImageFromInternalStorage(imagePath)
|
||||||
|
onComplete(false, e.message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Pengguna tidak ditemukan: $username")
|
||||||
|
// Hapus gambar jika user tidak ditemukan
|
||||||
|
LocalImageStorage.deleteImageFromInternalStorage(imagePath)
|
||||||
|
onComplete(false, "Pengguna tidak ditemukan")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.addOnFailureListener { e ->
|
||||||
|
Log.e(TAG, "Gagal mencari pengguna: ${e.message}")
|
||||||
|
// Hapus gambar jika terjadi error
|
||||||
|
LocalImageStorage.deleteImageFromInternalStorage(imagePath)
|
||||||
|
onComplete(false, e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menghapus data kalori berdasarkan tanggal dan kalori
|
||||||
|
// Karena tidak ada ID dalam model, kita gunakan kombinasi tanggal+kalori untuk identifikasi
|
||||||
|
fun deleteCaloryData(date: String, calories: Int, imagePath: String, username: String, onComplete: (Boolean) -> Unit) {
|
||||||
|
db.collection("users")
|
||||||
|
.whereEqualTo("username", username)
|
||||||
|
.get()
|
||||||
|
.addOnSuccessListener { result ->
|
||||||
|
if (!result.isEmpty) {
|
||||||
|
val userDocument = result.documents[0]
|
||||||
|
|
||||||
|
// Cari dokumen dengan tanggal dan kalori yang sesuai
|
||||||
|
userDocument.reference.collection("calorieData")
|
||||||
|
.whereEqualTo("date", date)
|
||||||
|
.whereEqualTo("calories", calories)
|
||||||
|
.get()
|
||||||
|
.addOnSuccessListener { snapshot ->
|
||||||
|
if (snapshot.documents.isNotEmpty()) {
|
||||||
|
// Ambil dokumen pertama yang cocok
|
||||||
|
val document = snapshot.documents[0]
|
||||||
|
|
||||||
|
// Hapus dokumen tersebut
|
||||||
|
document.reference.delete()
|
||||||
|
.addOnSuccessListener {
|
||||||
|
Log.d(TAG, "Data kalori berhasil dihapus: $date, $calories")
|
||||||
|
// Hapus gambar dari penyimpanan lokal
|
||||||
|
val isDeleted = LocalImageStorage.deleteImageFromInternalStorage(imagePath)
|
||||||
|
Log.d(TAG, "Gambar berhasil dihapus: $isDeleted")
|
||||||
|
onComplete(true)
|
||||||
|
}
|
||||||
|
.addOnFailureListener { e ->
|
||||||
|
Log.e(TAG, "Gagal menghapus data: ${e.message}")
|
||||||
|
onComplete(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Data kalori tidak ditemukan: $date, $calories")
|
||||||
|
onComplete(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.addOnFailureListener { e ->
|
||||||
|
Log.e(TAG, "Gagal mencari data kalori: ${e.message}")
|
||||||
|
onComplete(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Pengguna tidak ditemukan: $username")
|
||||||
|
onComplete(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.addOnFailureListener { e ->
|
||||||
|
Log.e(TAG, "Gagal mencari pengguna: ${e.message}")
|
||||||
|
onComplete(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Membersihkan gambar yang tidak digunakan
|
||||||
|
fun cleanupUnusedImages(context: Context, username: String) {
|
||||||
|
// Dapatkan semua gambar yang digunakan
|
||||||
|
getCalorieData(username) { allCalories ->
|
||||||
|
val usedImagePaths = allCalories.map { it.imagePath }.filter { it.isNotEmpty() }
|
||||||
|
// Bersihkan gambar yang tidak digunakan
|
||||||
|
LocalImageStorage.cleanupUnusedImages(context, usedImagePaths)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fun saveCalorieData(username: String, calories: Int, onComplete: (Boolean) -> Unit) {
|
||||||
|
// val currentDate =
|
||||||
|
// java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
|
||||||
|
// .format(java.util.Date())
|
||||||
|
// val calorieData = hashMapOf(
|
||||||
|
// "calories" to calories,
|
||||||
|
// "date" to currentDate
|
||||||
|
// )
|
||||||
|
// val caloryData = CaloryModel(calories, currentDate, username)
|
||||||
|
// db.collection("users")
|
||||||
|
// .whereEqualTo("username", username)
|
||||||
|
// .get()
|
||||||
|
// .addOnSuccessListener { result ->
|
||||||
|
// if (!result.isEmpty) {
|
||||||
|
// val userDocument = result.documents[0]
|
||||||
|
// val calorieId = db.collection("users")
|
||||||
|
// .document(userDocument.id)
|
||||||
|
// .collection("calorieData")
|
||||||
|
// .document().id
|
||||||
|
//
|
||||||
|
// userDocument.reference.collection("calorieData")
|
||||||
|
// .document(calorieId)
|
||||||
|
// .set(caloryData)
|
||||||
|
// .addOnSuccessListener {
|
||||||
|
// Log.d("CaloryApp", "Data kalori berhasil disimpan: $caloryData")
|
||||||
|
// onComplete(true)
|
||||||
|
// }
|
||||||
|
// .addOnFailureListener { e ->
|
||||||
|
// Log.e("CaloryApp", "Gagal menyimpan data: ${e.message}")
|
||||||
|
// onComplete(false)
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// Log.e("CaloryApp", "Pengguna tidak ditemukan: $username")
|
||||||
|
// onComplete(false)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// .addOnFailureListener { e ->
|
||||||
|
// Log.e("CaloryApp", "Gagal mencari pengguna: ${e.message}")
|
||||||
|
// onComplete(false)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// private val db = FirebaseFirestore.getInstance()
|
||||||
|
// private val TAG = "CaloryRepository"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Menyimpan hasil deteksi makanan ke Firestore (tanpa gambar)
|
* Menyimpan hasil deteksi makanan ke Firestore (tanpa gambar)
|
||||||
|
@ -36,7 +265,7 @@ class CaloryRepository {
|
||||||
val weightGrams = 500 * percentage
|
val weightGrams = 500 * percentage
|
||||||
val categoryEnum = FoodCategory.values().find { it.displayName == category }
|
val categoryEnum = FoodCategory.values().find { it.displayName == category }
|
||||||
val calories = categoryEnum?.let {
|
val calories = categoryEnum?.let {
|
||||||
(weightGrams * it.caloriesPer100g / 100).toInt()
|
(weightGrams * it.caloriesPerPorsi / 100).toInt()
|
||||||
} ?: 0
|
} ?: 0
|
||||||
calories
|
calories
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
package com.example.caloryapp.repository
|
package com.example.caloryapp.repository
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.example.caloryapp.model.CaloryModel
|
||||||
import com.example.caloryapp.model.UserModel
|
import com.example.caloryapp.model.UserModel
|
||||||
import com.google.firebase.auth.FirebaseAuth
|
import com.google.firebase.auth.FirebaseAuth
|
||||||
import com.google.firebase.firestore.FirebaseFirestore
|
import com.google.firebase.firestore.FirebaseFirestore
|
||||||
import kotlinx.coroutines.tasks.await
|
import com.google.firebase.firestore.Query
|
||||||
|
|
||||||
class UserRepository {
|
class UserRepository {
|
||||||
private val db = FirebaseFirestore.getInstance()
|
private val db = FirebaseFirestore.getInstance()
|
||||||
|
|
||||||
|
|
||||||
fun registerUser(user: UserModel, onComplete: (Boolean) -> Unit) {
|
fun registerUser(user: UserModel, onComplete: (Boolean) -> Unit) {
|
||||||
db.collection("users")
|
db.collection("users")
|
||||||
.add(user)
|
.add(user)
|
||||||
|
@ -50,11 +54,60 @@ class UserRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun saveCalorieData(username: String, calories: Int, onComplete: (Boolean) -> Unit) {
|
||||||
|
val currentDate =
|
||||||
|
java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
|
||||||
|
.format(java.util.Date())
|
||||||
|
val calorieData = hashMapOf(
|
||||||
|
"calories" to calories,
|
||||||
|
"date" to currentDate
|
||||||
|
)
|
||||||
|
val caloryData = CaloryModel(calories, currentDate, username)
|
||||||
|
db.collection("users")
|
||||||
|
.whereEqualTo("username", username)
|
||||||
|
.get()
|
||||||
|
.addOnSuccessListener { result ->
|
||||||
|
if (!result.isEmpty) {
|
||||||
|
val userDocument = result.documents[0]
|
||||||
|
val calorieId = db.collection("users")
|
||||||
|
.document(userDocument.id)
|
||||||
|
.collection("calorieData")
|
||||||
|
.document().id
|
||||||
|
|
||||||
|
userDocument.reference.collection("calorieData")
|
||||||
|
.document(calorieId)
|
||||||
|
.set(caloryData)
|
||||||
|
.addOnSuccessListener {
|
||||||
|
Log.d("CaloryApp", "Data kalori berhasil disimpan: $caloryData")
|
||||||
|
onComplete(true)
|
||||||
|
}
|
||||||
|
.addOnFailureListener { e ->
|
||||||
|
Log.e("CaloryApp", "Gagal menyimpan data: ${e.message}")
|
||||||
|
onComplete(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e("CaloryApp", "Pengguna tidak ditemukan: $username")
|
||||||
|
onComplete(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.addOnFailureListener { e ->
|
||||||
|
Log.e("CaloryApp", "Gagal mencari pengguna: ${e.message}")
|
||||||
|
onComplete(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun logoutUser() {
|
fun logoutUser() {
|
||||||
FirebaseAuth.getInstance().signOut()
|
FirebaseAuth.getInstance().signOut()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updatePasswordByUsername(username: String, oldPassword: String, newPassword: String, onComplete: (Boolean) -> Unit) {
|
fun updatePasswordByUsername(
|
||||||
|
username: String,
|
||||||
|
oldPassword: String,
|
||||||
|
newPassword: String,
|
||||||
|
onComplete: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
db.collection("users")
|
db.collection("users")
|
||||||
.whereEqualTo("username", username) // Mencari pengguna berdasarkan username
|
.whereEqualTo("username", username) // Mencari pengguna berdasarkan username
|
||||||
.get()
|
.get()
|
||||||
|
@ -85,7 +138,15 @@ class UserRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateUserData(username: String, fullName: String, email: String, gender: String, weight: String, height: String, onComplete: (Boolean) -> Unit) {
|
fun updateUserData(
|
||||||
|
username: String,
|
||||||
|
fullName: String,
|
||||||
|
email: String,
|
||||||
|
gender: String,
|
||||||
|
weight: String,
|
||||||
|
height: String,
|
||||||
|
onComplete: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
db.collection("users")
|
db.collection("users")
|
||||||
.whereEqualTo("username", username)
|
.whereEqualTo("username", username)
|
||||||
.get()
|
.get()
|
||||||
|
@ -116,7 +177,12 @@ class UserRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun updatePasswordByUsername2(username: String, newPassword: String, confirmPassword: String, onComplete: (Boolean) -> Unit) {
|
fun updatePasswordByUsername2(
|
||||||
|
username: String,
|
||||||
|
newPassword: String,
|
||||||
|
confirmPassword: String,
|
||||||
|
onComplete: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
// Memeriksa apakah kata sandi baru dan konfirmasi cocok
|
// Memeriksa apakah kata sandi baru dan konfirmasi cocok
|
||||||
if (newPassword != confirmPassword) {
|
if (newPassword != confirmPassword) {
|
||||||
onComplete(false) // Kata sandi tidak cocok
|
onComplete(false) // Kata sandi tidak cocok
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.example.caloryapp.ui.theme
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
var primary = Color(0xff109A17)
|
var primary = Color(0xff109A17)
|
||||||
|
var primary2 = Color(0xff6bbe6f)
|
||||||
var primaryblack = Color(0xff202020)
|
var primaryblack = Color(0xff202020)
|
||||||
var primarygrey = Color(0xffBABABA)
|
var primarygrey = Color(0xffBABABA)
|
||||||
var background = Color(0xffF4F4F4)
|
var background = Color(0xffF4F4F4)
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
package com.example.caloryapp.utils
|
||||||
|
|
||||||
|
import com.example.caloryapp.model.CaloryModel
|
||||||
|
|
||||||
|
object CaloryCalculator {
|
||||||
|
// Kebutuhan kalori harian berdasarkan gender
|
||||||
|
const val MALE_DAILY_CALORY_NEEDS = 2650
|
||||||
|
const val FEMALE_DAILY_CALORY_NEEDS = 2250
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menghitung kebutuhan kalori berdasarkan gender
|
||||||
|
*/
|
||||||
|
fun getDailyCaloryNeeds(gender: String): Int {
|
||||||
|
return when (gender.lowercase()) {
|
||||||
|
"male", "pria", "laki-laki" -> MALE_DAILY_CALORY_NEEDS
|
||||||
|
"female", "wanita", "perempuan" -> FEMALE_DAILY_CALORY_NEEDS
|
||||||
|
else -> 2400 // Default jika gender tidak diketahui
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menghitung persentase konsumsi kalori terhadap kebutuhan harian
|
||||||
|
*/
|
||||||
|
fun calculateDailyPercentage(consumedCalories: Int, gender: String): Float {
|
||||||
|
val dailyNeeds = getDailyCaloryNeeds(gender)
|
||||||
|
return (consumedCalories.toFloat() / dailyNeeds) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menghitung total kalori yang dikonsumsi pada hari ini
|
||||||
|
*/
|
||||||
|
fun calculateTodayTotalCalories(caloriesList: List<CaloryModel>): Int {
|
||||||
|
// Filter kalori hari ini
|
||||||
|
val today = java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault())
|
||||||
|
.format(java.util.Date())
|
||||||
|
|
||||||
|
// Jumlahkan semua kalori hari ini
|
||||||
|
return caloriesList
|
||||||
|
.filter { it.date == today }
|
||||||
|
.sumOf { it.calories }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menghitung sisa kalori yang dapat dikonsumsi
|
||||||
|
*/
|
||||||
|
fun calculateRemainingCalories(consumedCalories: Int, gender: String): Int {
|
||||||
|
val dailyNeeds = getDailyCaloryNeeds(gender)
|
||||||
|
val remaining = dailyNeeds - consumedCalories
|
||||||
|
return if (remaining < 0) 0 else remaining
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
package com.example.caloryapp.utils
|
||||||
|
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object LocalImageStorage {
|
||||||
|
private const val TAG = "LocalImageStorage"
|
||||||
|
private const val IMAGE_DIRECTORY = "calory_images"
|
||||||
|
|
||||||
|
// Menyimpan gambar ke penyimpanan internal
|
||||||
|
fun saveImageToInternalStorage(context: Context, bitmap: Bitmap, fileName: String? = null): String {
|
||||||
|
// Buat nama file unik jika tidak disediakan
|
||||||
|
val imageName = fileName ?: "IMG_${SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())}.jpg"
|
||||||
|
|
||||||
|
// Buat direktori jika belum ada
|
||||||
|
val directory = File(context.filesDir, IMAGE_DIRECTORY)
|
||||||
|
if (!directory.exists()) {
|
||||||
|
directory.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path file lengkap
|
||||||
|
val file = File(directory, imageName)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Simpan bitmap ke file
|
||||||
|
FileOutputStream(file).use { stream ->
|
||||||
|
// Kompres gambar dengan kualitas 85% untuk menghemat ruang
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 85, stream)
|
||||||
|
stream.flush()
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Gambar berhasil disimpan: ${file.absolutePath}")
|
||||||
|
return file.absolutePath
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, "Gagal menyimpan gambar: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mengambil gambar dari penyimpanan internal
|
||||||
|
fun getImageFromInternalStorage(context: Context, imagePath: String): Bitmap? {
|
||||||
|
return try {
|
||||||
|
val file = File(imagePath)
|
||||||
|
if (file.exists()) {
|
||||||
|
BitmapFactory.decodeFile(file.absolutePath)
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "File tidak ditemukan: $imagePath")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Gagal mengambil gambar: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hapus gambar dari penyimpanan internal
|
||||||
|
fun deleteImageFromInternalStorage(imagePath: String): Boolean {
|
||||||
|
return try {
|
||||||
|
val file = File(imagePath)
|
||||||
|
if (file.exists()) {
|
||||||
|
val result = file.delete()
|
||||||
|
Log.d(TAG, if (result) "Gambar berhasil dihapus: $imagePath" else "Gagal menghapus gambar: $imagePath")
|
||||||
|
result
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "File tidak ditemukan untuk dihapus: $imagePath")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error saat menghapus gambar: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mendapatkan URI untuk dibagikan (FileProvider)
|
||||||
|
fun getUriForImage(context: Context, imagePath: String): Uri? {
|
||||||
|
val file = File(imagePath)
|
||||||
|
return if (file.exists()) {
|
||||||
|
try {
|
||||||
|
FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
"${context.packageName}.fileprovider",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error saat mendapatkan URI: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "File tidak ditemukan untuk URI: $imagePath")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bersihkan gambar yang sudah tidak digunakan
|
||||||
|
fun cleanupUnusedImages(context: Context, usedImagePaths: List<String>) {
|
||||||
|
try {
|
||||||
|
val directory = File(context.filesDir, IMAGE_DIRECTORY)
|
||||||
|
if (directory.exists()) {
|
||||||
|
val files = directory.listFiles()
|
||||||
|
files?.forEach { file ->
|
||||||
|
if (!usedImagePaths.contains(file.absolutePath)) {
|
||||||
|
val isDeleted = file.delete()
|
||||||
|
Log.d(TAG, "Membersihkan gambar tidak terpakai: ${file.name}, berhasil: $isDeleted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error saat membersihkan gambar: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package com.example.caloryapp.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
class SessionManager(context: Context) {
|
||||||
|
private val sharedPreferences: SharedPreferences =
|
||||||
|
context.getSharedPreferences("CaloryAppSession", Context.MODE_PRIVATE)
|
||||||
|
private val editor: SharedPreferences.Editor = sharedPreferences.edit()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_IS_LOGGED_IN = "isLoggedIn"
|
||||||
|
private const val KEY_USERNAME = "username"
|
||||||
|
private const val KEY_FULL_NAME = "fullName"
|
||||||
|
private const val KEY_EMAIL = "email"
|
||||||
|
private const val KEY_GENDER = "gender"
|
||||||
|
private const val KEY_WEIGHT = "weight"
|
||||||
|
private const val KEY_HEIGHT = "height"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menyimpan sesi login dengan penanganan null
|
||||||
|
fun saveLoginSession(user: com.example.caloryapp.model.UserModel) {
|
||||||
|
try {
|
||||||
|
editor.putBoolean(KEY_IS_LOGGED_IN, true)
|
||||||
|
editor.putString(KEY_USERNAME, user.username ?: "")
|
||||||
|
editor.putString(KEY_FULL_NAME, user.fullName ?: "")
|
||||||
|
editor.putString(KEY_EMAIL, user.email ?: "")
|
||||||
|
editor.putString(KEY_GENDER, user.gender ?: "")
|
||||||
|
editor.putString(KEY_WEIGHT, user.weight ?: "")
|
||||||
|
editor.putString(KEY_HEIGHT, user.height ?: "")
|
||||||
|
editor.apply()
|
||||||
|
Log.d("SessionManager", "Session saved successfully")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("SessionManager", "Error saving session: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mengecek apakah user sudah login
|
||||||
|
fun isLoggedIn(): Boolean {
|
||||||
|
return sharedPreferences.getBoolean(KEY_IS_LOGGED_IN, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mendapatkan data user yang tersimpan
|
||||||
|
fun getUserData(): com.example.caloryapp.model.UserModel {
|
||||||
|
return com.example.caloryapp.model.UserModel(
|
||||||
|
username = sharedPreferences.getString(KEY_USERNAME, "") ?: "",
|
||||||
|
fullName = sharedPreferences.getString(KEY_FULL_NAME, "") ?: "",
|
||||||
|
email = sharedPreferences.getString(KEY_EMAIL, "") ?: "",
|
||||||
|
password = "", // Password tidak disimpan di SharedPreferences untuk keamanan
|
||||||
|
gender = sharedPreferences.getString(KEY_GENDER, "") ?: "",
|
||||||
|
weight = sharedPreferences.getString(KEY_WEIGHT, "") ?: "",
|
||||||
|
height = sharedPreferences.getString(KEY_HEIGHT, "") ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menghapus sesi saat logout dengan try-catch
|
||||||
|
fun clearSession() {
|
||||||
|
try {
|
||||||
|
editor.clear()
|
||||||
|
editor.apply()
|
||||||
|
Log.d("SessionManager", "Session cleared successfully")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("SessionManager", "Error clearing session: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,129 +1,262 @@
|
||||||
package com.example.caloryapp.viewmodel
|
package com.example.caloryapp.viewmodel
|
||||||
|
|
||||||
import androidx.compose.runtime.getValue
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.example.caloryapp.model.CaloryHistoryModel
|
import com.example.caloryapp.model.CaloryModel
|
||||||
import com.example.caloryapp.repository.CaloryRepository
|
import com.example.caloryapp.repository.CaloryRepository
|
||||||
import kotlinx.coroutines.Dispatchers
|
import com.example.caloryapp.utils.LocalImageStorage
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
/**
|
|
||||||
* ViewModel untuk mengelola riwayat kalori
|
|
||||||
*/
|
|
||||||
class CaloryHistoryViewModel : ViewModel() {
|
class CaloryHistoryViewModel : ViewModel() {
|
||||||
|
private val TAG = "CaloryHistoryViewModel"
|
||||||
|
private val repository = CaloryRepository()
|
||||||
|
|
||||||
private val caloryRepository = CaloryRepository()
|
private val _calorieList = mutableStateOf<List<CaloryModel>>(emptyList())
|
||||||
|
val calorieList: State<List<CaloryModel>> = _calorieList
|
||||||
|
|
||||||
// State untuk UI
|
private val _isLoading = mutableStateOf(false)
|
||||||
var isLoading by mutableStateOf(false)
|
val isLoading: State<Boolean> = _isLoading
|
||||||
var errorMessage by mutableStateOf<String?>(null)
|
|
||||||
|
|
||||||
// List riwayat kalori
|
private val _error = mutableStateOf<String?>(null)
|
||||||
var historyList by mutableStateOf<List<CaloryHistoryModel>>(emptyList())
|
val error: State<String?> = _error
|
||||||
|
|
||||||
// Detail riwayat kalori yang dipilih
|
// Menyimpan data kalori baru
|
||||||
var selectedHistory by mutableStateOf<CaloryHistoryModel?>(null)
|
fun saveCaloryData(
|
||||||
|
context: Context,
|
||||||
|
bitmap: Bitmap,
|
||||||
|
calories: Int,
|
||||||
|
username: String,
|
||||||
|
onComplete: (Boolean, String?) -> Unit
|
||||||
|
) {
|
||||||
|
_isLoading.value = true
|
||||||
|
_error.value = null
|
||||||
|
|
||||||
/**
|
|
||||||
* Memuat riwayat kalori berdasarkan username
|
|
||||||
*/
|
|
||||||
fun loadHistoryByUsername(username: String, limit: Long = 10) {
|
|
||||||
isLoading = true
|
|
||||||
errorMessage = null
|
|
||||||
|
|
||||||
// Menggunakan viewModelScope, yang merupakan CoroutineScope yang otomatis dibatalkan saat ViewModel dihancurkan
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
// Membungkus fungsi callback-based dalam withContext untuk menjalankannya di thread IO
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
caloryRepository.getCaloryHistoryByUsername(username, limit) { histories ->
|
repository.saveCaloryData(
|
||||||
// Kembali ke thread utama untuk memperbarui UI
|
context,
|
||||||
viewModelScope.launch(Dispatchers.Main) {
|
bitmap,
|
||||||
historyList = histories
|
calories,
|
||||||
isLoading = false
|
username
|
||||||
|
) { success, message ->
|
||||||
|
_isLoading.value = false
|
||||||
|
if (!success) {
|
||||||
|
_error.value = message ?: "Gagal menyimpan data"
|
||||||
|
} else {
|
||||||
|
// Refresh list setelah berhasil menambahkan
|
||||||
|
loadHistoryByUsername(username)
|
||||||
}
|
}
|
||||||
|
onComplete(success, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
withContext(Dispatchers.Main) {
|
Log.e(TAG, "Error saat menyimpan data: ${e.message}")
|
||||||
errorMessage = "Gagal memuat riwayat kalori: ${e.message}"
|
_isLoading.value = false
|
||||||
isLoading = false
|
_error.value = e.message
|
||||||
}
|
onComplete(false, e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Memuat data kalori untuk user
|
||||||
* Memuat detail riwayat kalori berdasarkan ID
|
fun loadHistoryByUsername(username: String, limit: Int = 0) {
|
||||||
*/
|
_isLoading.value = true
|
||||||
fun loadHistoryDetail(id: String) {
|
_error.value = null
|
||||||
isLoading = true
|
|
||||||
errorMessage = null
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
caloryRepository.getCaloryHistoryById(id) { history ->
|
repository.getCalorieData(username) { data ->
|
||||||
viewModelScope.launch(Dispatchers.Main) {
|
_calorieList.value = if (limit > 0 && data.size > limit) {
|
||||||
selectedHistory = history
|
data.take(limit)
|
||||||
isLoading = false
|
} else {
|
||||||
|
data
|
||||||
|
}
|
||||||
|
_isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error saat memuat data: ${e.message}")
|
||||||
|
_isLoading.value = false
|
||||||
|
_error.value = e.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (history == null) {
|
// Hapus data kalori
|
||||||
errorMessage = "Riwayat kalori tidak ditemukan"
|
fun deleteCaloryData(date: String, calories: Int, imagePath: String, username: String, onComplete: (Boolean) -> Unit) {
|
||||||
|
_isLoading.value = true
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
repository.deleteCaloryData(date, calories, imagePath, username) { success ->
|
||||||
|
_isLoading.value = false
|
||||||
|
if (success) {
|
||||||
|
// Update list setelah penghapusan
|
||||||
|
_calorieList.value = _calorieList.value.filter {
|
||||||
|
it.date != date || it.calories != calories
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onComplete(success)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
withContext(Dispatchers.Main) {
|
Log.e(TAG, "Error saat menghapus data: ${e.message}")
|
||||||
errorMessage = "Gagal memuat detail riwayat: ${e.message}"
|
_isLoading.value = false
|
||||||
isLoading = false
|
onComplete(false)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Mendapatkan bitmap dari path gambar
|
||||||
* Menghapus riwayat kalori
|
fun getImageBitmap(context: Context, imagePath: String): Bitmap? {
|
||||||
*/
|
if (imagePath.isEmpty()) return null
|
||||||
fun deleteHistory(id: String, onComplete: (Boolean) -> Unit) {
|
return LocalImageStorage.getImageFromInternalStorage(context, imagePath)
|
||||||
isLoading = true
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
// Membersihkan cache gambar yang tidak digunakan
|
||||||
try {
|
fun cleanupUnusedImages(context: Context, username: String) {
|
||||||
withContext(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
caloryRepository.deleteCaloryHistory(id) { success ->
|
repository.cleanupUnusedImages(context, username)
|
||||||
viewModelScope.launch(Dispatchers.Main) {
|
|
||||||
isLoading = false
|
|
||||||
if (success) {
|
|
||||||
// Hapus dari list lokal jika berhasil
|
|
||||||
historyList = historyList.filter { it.id != id }
|
|
||||||
}
|
|
||||||
onComplete(success)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
errorMessage = "Gagal menghapus riwayat: ${e.message}"
|
|
||||||
isLoading = false
|
|
||||||
onComplete(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
//package com.example.caloryapp.viewmodel
|
||||||
* Reset state
|
//
|
||||||
*/
|
//import androidx.compose.runtime.getValue
|
||||||
fun resetState() {
|
//import androidx.compose.runtime.mutableStateOf
|
||||||
historyList = emptyList()
|
//import androidx.compose.runtime.setValue
|
||||||
selectedHistory = null
|
//import androidx.lifecycle.ViewModel
|
||||||
errorMessage = null
|
//import androidx.lifecycle.viewModelScope
|
||||||
isLoading = false
|
//import com.example.caloryapp.model.CaloryHistoryModel
|
||||||
}
|
//import com.example.caloryapp.repository.CaloryRepository
|
||||||
}
|
//import kotlinx.coroutines.Dispatchers
|
||||||
|
//import kotlinx.coroutines.launch
|
||||||
|
//import kotlinx.coroutines.withContext
|
||||||
|
//
|
||||||
|
///**
|
||||||
|
// * ViewModel untuk mengelola riwayat kalori
|
||||||
|
// */
|
||||||
|
//class CaloryHistoryViewModel : ViewModel() {
|
||||||
|
//
|
||||||
|
// private val caloryRepository = CaloryRepository()
|
||||||
|
//
|
||||||
|
// // State untuk UI
|
||||||
|
// var isLoading by mutableStateOf(false)
|
||||||
|
// var errorMessage by mutableStateOf<String?>(null)
|
||||||
|
//
|
||||||
|
// // List riwayat kalori
|
||||||
|
// var historyList by mutableStateOf<List<CaloryHistoryModel>>(emptyList())
|
||||||
|
//
|
||||||
|
// // Detail riwayat kalori yang dipilih
|
||||||
|
// var selectedHistory by mutableStateOf<CaloryHistoryModel?>(null)
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * Memuat riwayat kalori berdasarkan username
|
||||||
|
// */
|
||||||
|
// fun loadHistoryByUsername(username: String, limit: Long = 10) {
|
||||||
|
// isLoading = true
|
||||||
|
// errorMessage = null
|
||||||
|
//
|
||||||
|
// // Menggunakan viewModelScope, yang merupakan CoroutineScope yang otomatis dibatalkan saat ViewModel dihancurkan
|
||||||
|
// viewModelScope.launch {
|
||||||
|
// try {
|
||||||
|
// // Membungkus fungsi callback-based dalam withContext untuk menjalankannya di thread IO
|
||||||
|
// withContext(Dispatchers.IO) {
|
||||||
|
// caloryRepository.getCaloryHistoryByUsername(username, limit) { histories ->
|
||||||
|
// // Kembali ke thread utama untuk memperbarui UI
|
||||||
|
// viewModelScope.launch(Dispatchers.Main) {
|
||||||
|
// historyList = histories
|
||||||
|
// isLoading = false
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } catch (e: Exception) {
|
||||||
|
// withContext(Dispatchers.Main) {
|
||||||
|
// errorMessage = "Gagal memuat riwayat kalori: ${e.message}"
|
||||||
|
// isLoading = false
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * Memuat detail riwayat kalori berdasarkan ID
|
||||||
|
// */
|
||||||
|
// fun loadHistoryDetail(id: String) {
|
||||||
|
// isLoading = true
|
||||||
|
// errorMessage = null
|
||||||
|
//
|
||||||
|
// viewModelScope.launch {
|
||||||
|
// try {
|
||||||
|
// withContext(Dispatchers.IO) {
|
||||||
|
// caloryRepository.getCaloryHistoryById(id) { history ->
|
||||||
|
// viewModelScope.launch(Dispatchers.Main) {
|
||||||
|
// selectedHistory = history
|
||||||
|
// isLoading = false
|
||||||
|
//
|
||||||
|
// if (history == null) {
|
||||||
|
// errorMessage = "Riwayat kalori tidak ditemukan"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } catch (e: Exception) {
|
||||||
|
// withContext(Dispatchers.Main) {
|
||||||
|
// errorMessage = "Gagal memuat detail riwayat: ${e.message}"
|
||||||
|
// isLoading = false
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * Menghapus riwayat kalori
|
||||||
|
// */
|
||||||
|
// fun deleteHistory(id: String, onComplete: (Boolean) -> Unit) {
|
||||||
|
// isLoading = true
|
||||||
|
//
|
||||||
|
// viewModelScope.launch {
|
||||||
|
// try {
|
||||||
|
// withContext(Dispatchers.IO) {
|
||||||
|
// caloryRepository.deleteCaloryHistory(id) { success ->
|
||||||
|
// viewModelScope.launch(Dispatchers.Main) {
|
||||||
|
// isLoading = false
|
||||||
|
// if (success) {
|
||||||
|
// // Hapus dari list lokal jika berhasil
|
||||||
|
// historyList = historyList.filter { it.id != id }
|
||||||
|
// }
|
||||||
|
// onComplete(success)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } catch (e: Exception) {
|
||||||
|
// withContext(Dispatchers.Main) {
|
||||||
|
// errorMessage = "Gagal menghapus riwayat: ${e.message}"
|
||||||
|
// isLoading = false
|
||||||
|
// onComplete(false)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * Reset state
|
||||||
|
// */
|
||||||
|
// fun resetState() {
|
||||||
|
// historyList = emptyList()
|
||||||
|
// selectedHistory = null
|
||||||
|
// errorMessage = null
|
||||||
|
// isLoading = false
|
||||||
|
// }
|
||||||
|
//}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package com.example.caloryapp.viewmodel
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.example.caloryapp.model.CaloryModel
|
||||||
|
import com.google.firebase.auth.FirebaseAuth
|
||||||
|
import com.google.firebase.firestore.FirebaseFirestore
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class CaloryViewModel : ViewModel() {
|
||||||
|
|
||||||
|
private val db = FirebaseFirestore.getInstance()
|
||||||
|
|
||||||
|
// Fungsi untuk menyimpan data kalori
|
||||||
|
fun saveCaloryData(calories: Int, onComplete: (Boolean) -> Unit) {
|
||||||
|
val currentUser = FirebaseAuth.getInstance().currentUser
|
||||||
|
if (currentUser == null) {
|
||||||
|
Log.e("CaloryApp", "Pengguna belum login")
|
||||||
|
onComplete(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val username = currentUser.displayName ?: "Unknown"
|
||||||
|
val currentDate = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date())
|
||||||
|
|
||||||
|
val caloryData = CaloryModel(calories, currentDate, username)
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
db.collection("users")
|
||||||
|
.whereEqualTo("username", username)
|
||||||
|
.get()
|
||||||
|
.addOnSuccessListener { result ->
|
||||||
|
if (!result.isEmpty) {
|
||||||
|
val userDocument = result.documents[0]
|
||||||
|
val calorieId = db.collection("users")
|
||||||
|
.document(userDocument.id)
|
||||||
|
.collection("calorieData")
|
||||||
|
.document().id
|
||||||
|
|
||||||
|
userDocument.reference.collection("calorieData")
|
||||||
|
.document(calorieId)
|
||||||
|
.set(caloryData)
|
||||||
|
.addOnSuccessListener {
|
||||||
|
Log.d("CaloryApp", "Data kalori berhasil disimpan: $caloryData")
|
||||||
|
onComplete(true)
|
||||||
|
}
|
||||||
|
.addOnFailureListener { e ->
|
||||||
|
Log.e("CaloryApp", "Gagal menyimpan data: ${e.message}")
|
||||||
|
onComplete(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e("CaloryApp", "Pengguna tidak ditemukan: $username")
|
||||||
|
onComplete(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.addOnFailureListener { e ->
|
||||||
|
Log.e("CaloryApp", "Gagal mencari pengguna: ${e.message}")
|
||||||
|
onComplete(false)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("CaloryApp", "Error: ${e.message}")
|
||||||
|
onComplete(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -138,8 +138,10 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.example.caloryapp.foodmodel.FoodCategory
|
||||||
import com.example.caloryapp.foodmodel.FoodDetectionResult
|
import com.example.caloryapp.foodmodel.FoodDetectionResult
|
||||||
import com.example.caloryapp.foodmodel.FoodDetector
|
import com.example.caloryapp.foodmodel.FoodDetector
|
||||||
|
import com.google.firebase.firestore.FirebaseFirestore
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -155,24 +157,55 @@ class FoodDetectionViewModel : ViewModel() {
|
||||||
var errorMessage by mutableStateOf<String?>(null)
|
var errorMessage by mutableStateOf<String?>(null)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var isSaving by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var saveSuccess by mutableStateOf<Boolean?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
// Ambang batas confidence untuk kategori utama
|
||||||
|
private val MAIN_CATEGORY_THRESHOLD = 0.35f
|
||||||
|
|
||||||
|
// Rasio minimum antara kategori utama dan kategori kedua
|
||||||
|
// Ini membantu memastikan bahwa model cukup yakin tentang kategori utama
|
||||||
|
private val CONFIDENCE_RATIO_THRESHOLD = 1.5f
|
||||||
|
|
||||||
fun detectFoodFromImage(context: Context, uri: Uri) {
|
fun detectFoodFromImage(context: Context, uri: Uri) {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
|
detectionResult = null
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val bitmap = loadBitmapFromUri(context, uri)
|
val bitmap = loadBitmapFromUri(context, uri)
|
||||||
|
|
||||||
// Buat detector di sini, tidak dalam withContext
|
// Buat detector
|
||||||
val detector = FoodDetector(context)
|
val detector = FoodDetector(context)
|
||||||
|
|
||||||
val result = withContext(Dispatchers.IO) {
|
val result = withContext(Dispatchers.IO) {
|
||||||
detector.detectFood(bitmap)
|
detector.detectFood(bitmap)
|
||||||
}
|
}
|
||||||
detectionResult = result
|
|
||||||
|
// Log hasil deteksi untuk debugging
|
||||||
|
Log.d("FoodViewModel", "Detection result: ${result.mainCategory.name}, " +
|
||||||
|
"confidence: ${result.confidence}, " +
|
||||||
|
"all categories: ${result.allCategories.entries.joinToString { "${it.key.name}=${it.value}" }}")
|
||||||
|
|
||||||
|
// Periksa dengan metode yang lebih canggih
|
||||||
|
if (isFoodImageAdvanced(result)) {
|
||||||
|
detectionResult = result
|
||||||
|
errorMessage = null
|
||||||
|
} else {
|
||||||
|
detectionResult = null
|
||||||
|
errorMessage = "Pindai Makanan Gagal!"
|
||||||
|
Log.d("FoodViewModel", "Image tidak lolos validasi makanan")
|
||||||
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e("FoodViewModel", "Error IO: ${e.message}", e)
|
Log.e("FoodViewModel", "Error IO: ${e.message}", e)
|
||||||
errorMessage = "Gagal memuat gambar atau model: ${e.message}"
|
errorMessage = "Gagal memuat gambar atau model: ${e.message}"
|
||||||
|
} catch (e: OutOfMemoryError) {
|
||||||
|
Log.e("FoodViewModel", "Out of memory: ${e.message}", e)
|
||||||
|
errorMessage = "Gambar terlalu besar. Gunakan gambar dengan resolusi lebih rendah."
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("FoodViewModel", "Error umum: ${e.message}", e)
|
Log.e("FoodViewModel", "Error umum: ${e.message}", e)
|
||||||
errorMessage = "Terjadi kesalahan: ${e.message}"
|
errorMessage = "Terjadi kesalahan: ${e.message}"
|
||||||
|
@ -182,10 +215,180 @@ class FoodDetectionViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Metode yang lebih canggih untuk memeriksa apakah gambar adalah makanan
|
||||||
|
private fun isFoodImageAdvanced(result: FoodDetectionResult): Boolean {
|
||||||
|
// Confidence untuk kategori utama harus di atas threshold
|
||||||
|
if (result.confidence < MAIN_CATEGORY_THRESHOLD) {
|
||||||
|
Log.d("FoodViewModel", "Main category confidence too low: ${result.confidence} < $MAIN_CATEGORY_THRESHOLD")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dapatkan nilai confidence untuk semua kategori dan urutkan dari tertinggi ke terendah
|
||||||
|
val sortedConfidences = result.allCategories.values.sortedDescending()
|
||||||
|
|
||||||
|
// Jika hanya ada satu kategori, gunakan threshold normal
|
||||||
|
if (sortedConfidences.size <= 1) {
|
||||||
|
return result.confidence >= MAIN_CATEGORY_THRESHOLD
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dapatkan confidence untuk kategori utama dan kategori kedua tertinggi
|
||||||
|
val topConfidence = sortedConfidences[0]
|
||||||
|
val secondConfidence = sortedConfidences[1]
|
||||||
|
|
||||||
|
// Hitung rasio antara kategori utama dan kategori kedua
|
||||||
|
// Ini membantu menentukan apakah model benar-benar yakin tentang kategori utama
|
||||||
|
// dibandingkan dengan kategori lainnya
|
||||||
|
val confidenceRatio = if (secondConfidence > 0) topConfidence / secondConfidence else Float.MAX_VALUE
|
||||||
|
|
||||||
|
// Log untuk debugging
|
||||||
|
Log.d("FoodViewModel", "Top confidence: $topConfidence, Second confidence: $secondConfidence, Ratio: $confidenceRatio")
|
||||||
|
|
||||||
|
// Kategori utama harus memiliki confidence yang cukup tinggi
|
||||||
|
// dan rasio confidence harus di atas threshold
|
||||||
|
val isFood = topConfidence >= MAIN_CATEGORY_THRESHOLD && confidenceRatio >= CONFIDENCE_RATIO_THRESHOLD
|
||||||
|
|
||||||
|
Log.d("FoodViewModel", "Is food: $isFood")
|
||||||
|
return isFood
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadBitmapFromUri(context: Context, uri: Uri): Bitmap {
|
private fun loadBitmapFromUri(context: Context, uri: Uri): Bitmap {
|
||||||
val contentResolver = context.contentResolver
|
val contentResolver = context.contentResolver
|
||||||
return contentResolver.openInputStream(uri).use { inputStream ->
|
return contentResolver.openInputStream(uri).use { inputStream ->
|
||||||
android.graphics.BitmapFactory.decodeStream(inputStream)
|
android.graphics.BitmapFactory.decodeStream(inputStream)
|
||||||
} ?: throw IOException("Gagal membuka gambar")
|
} ?: throw IOException("Gagal membuka gambar")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Fungsi untuk menyimpan hasil deteksi (kode tidak berubah)
|
||||||
|
fun saveDetectionResult(username: String) {
|
||||||
|
// ... kode yang sama seperti sebelumnya
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fungsi untuk mereset state
|
||||||
|
fun resetState() {
|
||||||
|
isLoading = false
|
||||||
|
isSaving = false
|
||||||
|
detectionResult = null
|
||||||
|
errorMessage = null
|
||||||
|
saveSuccess = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// kode benar
|
||||||
|
//package com.example.caloryapp.viewmodel
|
||||||
|
//
|
||||||
|
//import android.content.Context
|
||||||
|
//import android.graphics.Bitmap
|
||||||
|
//import android.net.Uri
|
||||||
|
//import android.util.Log
|
||||||
|
//import androidx.compose.runtime.getValue
|
||||||
|
//import androidx.compose.runtime.mutableStateOf
|
||||||
|
//import androidx.compose.runtime.setValue
|
||||||
|
//import androidx.lifecycle.ViewModel
|
||||||
|
//import androidx.lifecycle.viewModelScope
|
||||||
|
//import com.example.caloryapp.foodmodel.FoodDetectionResult
|
||||||
|
//import com.example.caloryapp.foodmodel.FoodDetector
|
||||||
|
//import com.google.firebase.firestore.FirebaseFirestore
|
||||||
|
//import kotlinx.coroutines.Dispatchers
|
||||||
|
//import kotlinx.coroutines.launch
|
||||||
|
//import kotlinx.coroutines.withContext
|
||||||
|
//import java.io.IOException
|
||||||
|
//
|
||||||
|
//class FoodDetectionViewModel : ViewModel() {
|
||||||
|
// var detectionResult by mutableStateOf<FoodDetectionResult?>(null)
|
||||||
|
// private set
|
||||||
|
//
|
||||||
|
// var isLoading by mutableStateOf(false)
|
||||||
|
// private set
|
||||||
|
//
|
||||||
|
// var errorMessage by mutableStateOf<String?>(null)
|
||||||
|
// private set
|
||||||
|
//
|
||||||
|
// fun detectFoodFromImage(context: Context, uri: Uri) {
|
||||||
|
// isLoading = true
|
||||||
|
// errorMessage = null
|
||||||
|
//
|
||||||
|
// viewModelScope.launch {
|
||||||
|
// try {
|
||||||
|
// val bitmap = loadBitmapFromUri(context, uri)
|
||||||
|
//
|
||||||
|
// // Buat detector di sini, tidak dalam withContext
|
||||||
|
// val detector = FoodDetector(context)
|
||||||
|
//
|
||||||
|
// val result = withContext(Dispatchers.IO) {
|
||||||
|
// detector.detectFood(bitmap)
|
||||||
|
// }
|
||||||
|
// detectionResult = result
|
||||||
|
// } catch (e: IOException) {
|
||||||
|
// Log.e("FoodViewModel", "Error IO: ${e.message}", e)
|
||||||
|
// errorMessage = "Gagal memuat gambar atau model: ${e.message}"
|
||||||
|
// } catch (e: Exception) {
|
||||||
|
// Log.e("FoodViewModel", "Error umum: ${e.message}", e)
|
||||||
|
// errorMessage = "Terjadi kesalahan: ${e.message}"
|
||||||
|
// } finally {
|
||||||
|
// isLoading = false
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private fun loadBitmapFromUri(context: Context, uri: Uri): Bitmap {
|
||||||
|
// val contentResolver = context.contentResolver
|
||||||
|
// return contentResolver.openInputStream(uri).use { inputStream ->
|
||||||
|
// android.graphics.BitmapFactory.decodeStream(inputStream)
|
||||||
|
// } ?: throw IOException("Gagal membuka gambar")
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
// // Fungsi untuk menyimpan data kalori
|
||||||
|
// fun saveCaloryData(calories: Int, onComplete: (Boolean) -> Unit) {
|
||||||
|
// val currentUser = FirebaseAuth.getInstance().currentUser
|
||||||
|
// if (currentUser == null) {
|
||||||
|
// Log.e("CaloryApp", "Pengguna belum login")
|
||||||
|
// onComplete(false)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// val username = currentUser.displayName ?: "Unknown"
|
||||||
|
// val currentDate = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date())
|
||||||
|
//
|
||||||
|
// val caloryData = CaloryModel(calories, currentDate, username)
|
||||||
|
//
|
||||||
|
// viewModelScope.launch {
|
||||||
|
// try {
|
||||||
|
// db.collection("users")
|
||||||
|
// .whereEqualTo("username", username)
|
||||||
|
// .get()
|
||||||
|
// .addOnSuccessListener { result ->
|
||||||
|
// if (!result.isEmpty) {
|
||||||
|
// val userDocument = result.documents[0]
|
||||||
|
// val calorieId = db.collection("users")
|
||||||
|
// .document(userDocument.id)
|
||||||
|
// .collection("calorieData")
|
||||||
|
// .document().id
|
||||||
|
//
|
||||||
|
// userDocument.reference.collection("calorieData")
|
||||||
|
// .document(calorieId)
|
||||||
|
// .set(caloryData)
|
||||||
|
// .addOnSuccessListener {
|
||||||
|
// Log.d("CaloryApp", "Data kalori berhasil disimpan: $caloryData")
|
||||||
|
// onComplete(true)
|
||||||
|
// }
|
||||||
|
// .addOnFailureListener { e ->
|
||||||
|
// Log.e("CaloryApp", "Gagal menyimpan data: ${e.message}")
|
||||||
|
// onComplete(false)
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// Log.e("CaloryApp", "Pengguna tidak ditemukan: $username")
|
||||||
|
// onComplete(false)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// .addOnFailureListener { e ->
|
||||||
|
// Log.e("CaloryApp", "Gagal mencari pengguna: ${e.message}")
|
||||||
|
// onComplete(false)
|
||||||
|
// }
|
||||||
|
// } catch (e: Exception) {
|
||||||
|
// Log.e("CaloryApp", "Error: ${e.message}")
|
||||||
|
// onComplete(false)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
|
@ -1,5 +1,7 @@
|
||||||
package com.example.caloryapp.viewmodel
|
package com.example.caloryapp.viewmodel
|
||||||
|
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
@ -7,6 +9,8 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.example.caloryapp.model.UserModel
|
import com.example.caloryapp.model.UserModel
|
||||||
import com.example.caloryapp.repository.UserRepository
|
import com.example.caloryapp.repository.UserRepository
|
||||||
|
import com.example.caloryapp.utils.SessionManager
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -15,41 +19,95 @@ sealed class LoginState {
|
||||||
data object Loading : LoginState()
|
data object Loading : LoginState()
|
||||||
data class Success(val user: UserModel) : LoginState()
|
data class Success(val user: UserModel) : LoginState()
|
||||||
data class Error(val message: String) : LoginState()
|
data class Error(val message: String) : LoginState()
|
||||||
|
data object AlreadyLoggedIn : LoginState()
|
||||||
|
data object LoggedOut : LoginState()// State baru untuk user yang sudah login
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserViewModel : ViewModel() {
|
class UserViewModel : ViewModel() {
|
||||||
private val repository = UserRepository()
|
private val repository = UserRepository()
|
||||||
|
private var sessionManager: SessionManager? = null
|
||||||
|
|
||||||
private val _registerState = MutableStateFlow<Boolean?>(null)
|
private val _registerState = MutableStateFlow<Boolean?>(null)
|
||||||
val registerState: StateFlow<Boolean?> = _registerState
|
val registerState: StateFlow<Boolean?> = _registerState
|
||||||
|
|
||||||
// Menyimpan data login state (loading, success, error)
|
// Menyimpan data login state (loading, success, error)
|
||||||
private val _loginstate = mutableStateOf<LoginState>(LoginState.Loading)
|
private val _loginState = mutableStateOf<LoginState>(LoginState.Loading)
|
||||||
val loginstate = _loginstate
|
val loginState: State<LoginState> = _loginState
|
||||||
|
|
||||||
// Menyimpan data pengguna yang login
|
// Menyimpan data pengguna yang login
|
||||||
private val _user = mutableStateOf<UserModel?>(null)
|
private val _user = mutableStateOf<UserModel?>(null)
|
||||||
val user = _user
|
val user: State<UserModel?> = _user
|
||||||
|
|
||||||
|
// Inisialisasi SessionManager
|
||||||
|
fun initSessionManager(context: Context) {
|
||||||
|
sessionManager = SessionManager(context)
|
||||||
|
// Cek apakah user sudah login sebelumnya
|
||||||
|
if (sessionManager?.isLoggedIn() == true) {
|
||||||
|
_user.value = sessionManager?.getUserData()
|
||||||
|
_loginState.value = LoginState.AlreadyLoggedIn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cek apakah user sudah login
|
||||||
|
fun checkLoginStatus(): Boolean {
|
||||||
|
return sessionManager?.isLoggedIn() ?: false
|
||||||
|
}
|
||||||
|
|
||||||
fun login(username: String, password: String) {
|
fun login(username: String, password: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_loginstate.value = LoginState.Loading
|
_loginState.value = LoginState.Loading
|
||||||
// Fetch user by username from Firestore
|
// Fetch user by username from Firestore
|
||||||
repository.getUserByUsername(username) { fetchedUser ->
|
repository.getUserByUsername(username) { fetchedUser ->
|
||||||
if (fetchedUser != null && fetchedUser.password == password) {
|
if (fetchedUser != null && fetchedUser.password == password) {
|
||||||
_user.value = fetchedUser // Store user data in ViewModel
|
_user.value = fetchedUser // Store user data in ViewModel
|
||||||
_loginstate.value = LoginState.Success(fetchedUser)
|
_loginState.value = LoginState.Success(fetchedUser)
|
||||||
|
// Simpan sesi login
|
||||||
|
sessionManager?.saveLoginSession(fetchedUser)
|
||||||
Log.d("Login", "Fetched user: $fetchedUser")
|
Log.d("Login", "Fetched user: $fetchedUser")
|
||||||
Log.d("Login", "User fullName: ${fetchedUser?.fullName}")
|
Log.d("Login", "User fullName: ${fetchedUser.fullName}")
|
||||||
Log.d("Login", "User username: ${fetchedUser?.username}")
|
Log.d("Login", "User username: ${fetchedUser.username}")
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
_loginstate.value = LoginState.Error("Invalid username or password")
|
_loginState.value = LoginState.Error("Invalid username or password")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logout user
|
||||||
|
fun logout() {
|
||||||
|
try {
|
||||||
|
// Gunakan repository untuk logout jika perlu
|
||||||
|
// repository.logoutUser()
|
||||||
|
|
||||||
|
// Bersihkan session
|
||||||
|
sessionManager?.clearSession()
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
_user.value = null
|
||||||
|
_loginState.value = LoginState.LoggedOut
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
delay(100) // Delay kecil
|
||||||
|
// Baru hapus data user
|
||||||
|
_user.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Log.d("Logout", "User berhasil logout")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Logout", "Error saat logout: ${e.message}")
|
||||||
|
// Tetap set state ke LoggedOut meskipun ada error
|
||||||
|
_loginState.value = LoginState.LoggedOut
|
||||||
|
}
|
||||||
|
// repository.logoutUser()
|
||||||
|
// sessionManager?.clearSession()
|
||||||
|
// _user.value = null
|
||||||
|
// _loginState.value = LoginState.LoggedOut
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// **REGISTER USER**
|
// **REGISTER USER**
|
||||||
// fun register(email: String, password: String, user: UserModel) {
|
// fun register(email: String, password: String, user: UserModel) {
|
||||||
// viewModelScope.launch {
|
// viewModelScope.launch {
|
||||||
|
@ -80,4 +138,3 @@ class UserViewModel : ViewModel() {
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
}
|
|
|
@ -1,6 +1,10 @@
|
||||||
package com.example.caloryapp.widget
|
package com.example.caloryapp.widget
|
||||||
|
|
||||||
|
|
||||||
|
//package com.example.caloryapp.widget
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
@ -10,6 +14,7 @@ import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
@ -20,18 +25,32 @@ import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.example.caloryapp.model.CaloryHistoryModel
|
import com.example.caloryapp.model.CaloryModel
|
||||||
import com.example.caloryapp.ui.theme.medium
|
import com.example.caloryapp.ui.theme.medium
|
||||||
|
import com.example.caloryapp.ui.theme.primary
|
||||||
|
import com.example.caloryapp.ui.theme.primary2
|
||||||
import com.example.caloryapp.ui.theme.semibold
|
import com.example.caloryapp.ui.theme.semibold
|
||||||
|
import com.example.caloryapp.utils.LocalImageStorage
|
||||||
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Komponen list riwayat kalori
|
* Komponen list riwayat kalori
|
||||||
|
@ -40,8 +59,11 @@ import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
||||||
fun CaloryHistoryList(
|
fun CaloryHistoryList(
|
||||||
viewModel: CaloryHistoryViewModel,
|
viewModel: CaloryHistoryViewModel,
|
||||||
username: String,
|
username: String,
|
||||||
onItemClick: (CaloryHistoryModel) -> Unit = {}
|
onItemClick: (CaloryModel) -> Unit = {},
|
||||||
|
onItemDelete: (CaloryModel) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
// Muat data saat komponen pertama kali ditampilkan
|
// Muat data saat komponen pertama kali ditampilkan
|
||||||
LaunchedEffect(username) {
|
LaunchedEffect(username) {
|
||||||
viewModel.loadHistoryByUsername(username)
|
viewModel.loadHistoryByUsername(username)
|
||||||
|
@ -50,17 +72,18 @@ fun CaloryHistoryList(
|
||||||
// Bersihkan state saat komponen dihancurkan
|
// Bersihkan state saat komponen dihancurkan
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
onDispose {
|
onDispose {
|
||||||
viewModel.resetState()
|
// Bersihkan gambar yang tidak digunakan saat meninggalkan layar
|
||||||
|
viewModel.cleanupUnusedImages(context, username)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxWidth()) {
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
if (viewModel.isLoading) {
|
if (viewModel.isLoading.value) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.align(Alignment.Center),
|
modifier = Modifier.align(Alignment.Center),
|
||||||
color = Color(0xFF4AB54A) // Hijau sesuai dengan tema
|
color = primary
|
||||||
)
|
)
|
||||||
} else if (viewModel.historyList.isEmpty()) {
|
} else if (viewModel.calorieList.value.isEmpty()) {
|
||||||
// Pesan jika list kosong
|
// Pesan jika list kosong
|
||||||
Text(
|
Text(
|
||||||
text = "Belum ada riwayat makanan",
|
text = "Belum ada riwayat makanan",
|
||||||
|
@ -78,10 +101,12 @@ fun CaloryHistoryList(
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
items(viewModel.historyList) { history ->
|
items(viewModel.calorieList.value) { calory ->
|
||||||
CaloryHistoryItem(
|
CaloryHistoryItem(
|
||||||
history = history,
|
calory = calory,
|
||||||
onClick = { onItemClick(history) }
|
viewModel = viewModel,
|
||||||
|
onClick = { onItemClick(calory) },
|
||||||
|
onDelete = { onItemDelete(calory) }
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
}
|
}
|
||||||
|
@ -94,7 +119,7 @@ fun CaloryHistoryList(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error message
|
// Error message
|
||||||
viewModel.errorMessage?.let { error ->
|
viewModel.error.value?.let { error ->
|
||||||
Text(
|
Text(
|
||||||
text = error,
|
text = error,
|
||||||
color = Color.Red,
|
color = Color.Red,
|
||||||
|
@ -107,13 +132,27 @@ fun CaloryHistoryList(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Item riwayat kalori
|
* Item riwayat kalori dengan gambar
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun CaloryHistoryItem(
|
fun CaloryHistoryItem(
|
||||||
history: CaloryHistoryModel,
|
calory: CaloryModel,
|
||||||
onClick: () -> Unit
|
viewModel: CaloryHistoryViewModel,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onDelete: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
// State untuk menyimpan bitmap dari penyimpanan lokal
|
||||||
|
var bitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
|
|
||||||
|
// Load bitmap saat komponen dibuat
|
||||||
|
LaunchedEffect(calory.imagePath) {
|
||||||
|
if (calory.imagePath.isNotEmpty()) {
|
||||||
|
bitmap = viewModel.getImageBitmap(context, calory.imagePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
@ -124,16 +163,30 @@ fun CaloryHistoryItem(
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(Color(0xFF4AB54A).copy(alpha = 0.6f)) // Warna hijau semi-transparan sesuai gambar
|
.background(primary2)
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
// Tampilkan gambar jika tersedia
|
||||||
|
bitmap?.let { bmp ->
|
||||||
|
Image(
|
||||||
|
bitmap = bmp.asImageBitmap(),
|
||||||
|
contentDescription = "Food Image",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(70.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp)),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detail kalori
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = "${history.totalCalories} Kalori",
|
text = "${calory.calories} Kalori",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 20.sp,
|
fontSize = 20.sp,
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
|
@ -152,9 +205,9 @@ fun CaloryHistoryItem(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timestamp (opsional)
|
// Tanggal
|
||||||
Text(
|
Text(
|
||||||
text = history.getFormattedDate(),
|
text = formatDate(calory.date),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = Color.White.copy(alpha = 0.7f),
|
color = Color.White.copy(alpha = 0.7f),
|
||||||
|
@ -164,4 +217,122 @@ fun CaloryHistoryItem(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format tanggal untuk tampilan
|
||||||
|
*/
|
||||||
|
fun formatDate(dateString: String): String {
|
||||||
|
return try {
|
||||||
|
val inputFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||||
|
val outputFormat = SimpleDateFormat("dd MMM yyyy", Locale.getDefault())
|
||||||
|
val date = inputFormat.parse(dateString)
|
||||||
|
outputFormat.format(date ?: Date())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
dateString // Kembalikan string asli jika format tidak sesuai
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item riwayat kalori dengan opsi hapus
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun CaloryHistoryItemWithDelete(
|
||||||
|
calory: CaloryModel,
|
||||||
|
viewModel: CaloryHistoryViewModel,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onDelete: () -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
// State untuk menyimpan bitmap dari penyimpanan lokal
|
||||||
|
var bitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
|
|
||||||
|
// Load bitmap saat komponen dibuat
|
||||||
|
LaunchedEffect(calory.imagePath) {
|
||||||
|
if (calory.imagePath.isNotEmpty()) {
|
||||||
|
bitmap = viewModel.getImageBitmap(context, calory.imagePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
elevation = 4.dp,
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(primary2)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// Tampilkan gambar jika tersedia
|
||||||
|
bitmap?.let { bmp ->
|
||||||
|
Image(
|
||||||
|
bitmap = bmp.asImageBitmap(),
|
||||||
|
contentDescription = "Food Image",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(70.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp)),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detail kalori
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = "${calory.calories} Kalori",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 20.sp,
|
||||||
|
color = Color.White,
|
||||||
|
fontFamily = semibold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Lihat Detail",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color.White,
|
||||||
|
fontFamily = medium,
|
||||||
|
textDecoration = TextDecoration.Underline
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tanggal
|
||||||
|
Column(horizontalAlignment = Alignment.End) {
|
||||||
|
Text(
|
||||||
|
text = formatDate(calory.date),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.White.copy(alpha = 0.7f),
|
||||||
|
fontWeight = FontWeight.Normal
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Tombol hapus
|
||||||
|
Text(
|
||||||
|
text = "Hapus",
|
||||||
|
modifier = Modifier.clickable(onClick = onDelete),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color.White,
|
||||||
|
fontFamily = medium,
|
||||||
|
textDecoration = TextDecoration.Underline
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,237 @@
|
||||||
|
package com.example.caloryapp.widget
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Card
|
||||||
|
import androidx.compose.material.LinearProgressIndicator
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.caloryapp.model.CaloryModel
|
||||||
|
import com.example.caloryapp.ui.theme.bold
|
||||||
|
import com.example.caloryapp.ui.theme.medium
|
||||||
|
import com.example.caloryapp.ui.theme.primary
|
||||||
|
import com.example.caloryapp.ui.theme.semibold
|
||||||
|
import com.example.caloryapp.utils.CaloryCalculator
|
||||||
|
import com.example.caloryapp.viewmodel.CaloryHistoryViewModel
|
||||||
|
import com.example.caloryapp.viewmodel.UserViewModel
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CaloryStatisticsCard(
|
||||||
|
userViewModel: UserViewModel,
|
||||||
|
caloryHistoryViewModel: CaloryHistoryViewModel
|
||||||
|
) {
|
||||||
|
val user = userViewModel.user.value ?: return
|
||||||
|
val caloriesList = caloryHistoryViewModel.calorieList.value
|
||||||
|
|
||||||
|
// Hitung statistik
|
||||||
|
val weeklyTotal = calculateWeeklyTotal(caloriesList)
|
||||||
|
val monthlyTotal = calculateMonthlyTotal(caloriesList)
|
||||||
|
|
||||||
|
// Kebutuhan kalori mingguan dan bulanan
|
||||||
|
val dailyNeeds = CaloryCalculator.getDailyCaloryNeeds(user.gender)
|
||||||
|
val weeklyNeeds = dailyNeeds * 7
|
||||||
|
val monthlyNeeds = dailyNeeds * 30
|
||||||
|
|
||||||
|
// Persentase konsumsi
|
||||||
|
val weeklyPercentage = (weeklyTotal.toFloat() / weeklyNeeds) * 100
|
||||||
|
val monthlyPercentage = (monthlyTotal.toFloat() / monthlyNeeds) * 100
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
elevation = 4.dp,
|
||||||
|
backgroundColor = Color.White
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Statistik Konsumsi Kalori",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 18.sp,
|
||||||
|
color = Color.Black,
|
||||||
|
fontFamily = bold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Statistik mingguan
|
||||||
|
Text(
|
||||||
|
text = "Minggu Ini",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = Color.Gray,
|
||||||
|
fontFamily = semibold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = (weeklyPercentage / 100).coerceIn(0f, 1f),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(8.dp),
|
||||||
|
backgroundColor = Color.LightGray,
|
||||||
|
color = primary
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "$weeklyTotal kkal",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = primary,
|
||||||
|
fontFamily = medium
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "$weeklyNeeds kkal",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color.Gray,
|
||||||
|
fontFamily = medium
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Statistik bulanan
|
||||||
|
Text(
|
||||||
|
text = "Bulan Ini",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = Color.Gray,
|
||||||
|
fontFamily = semibold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = (monthlyPercentage / 100).coerceIn(0f, 1f),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(8.dp),
|
||||||
|
backgroundColor = Color.LightGray,
|
||||||
|
color = primary
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "$monthlyTotal kkal",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = primary,
|
||||||
|
fontFamily = medium
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "$monthlyNeeds kkal",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color.Gray,
|
||||||
|
fontFamily = medium
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fungsi untuk menghitung total kalori mingguan
|
||||||
|
fun calculateWeeklyTotal(caloriesList: List<CaloryModel>): Int {
|
||||||
|
val calendar = Calendar.getInstance()
|
||||||
|
calendar.set(Calendar.DAY_OF_WEEK, calendar.firstDayOfWeek)
|
||||||
|
calendar.set(Calendar.HOUR_OF_DAY, 0)
|
||||||
|
calendar.set(Calendar.MINUTE, 0)
|
||||||
|
calendar.set(Calendar.SECOND, 0)
|
||||||
|
calendar.set(Calendar.MILLISECOND, 0)
|
||||||
|
|
||||||
|
val startOfWeek = calendar.timeInMillis
|
||||||
|
|
||||||
|
calendar.add(Calendar.DAY_OF_WEEK, 6)
|
||||||
|
calendar.set(Calendar.HOUR_OF_DAY, 23)
|
||||||
|
calendar.set(Calendar.MINUTE, 59)
|
||||||
|
calendar.set(Calendar.SECOND, 59)
|
||||||
|
|
||||||
|
val endOfWeek = calendar.timeInMillis
|
||||||
|
|
||||||
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||||
|
|
||||||
|
return caloriesList
|
||||||
|
.filter {
|
||||||
|
try {
|
||||||
|
val date = dateFormat.parse(it.date)
|
||||||
|
date != null && date.time >= startOfWeek && date.time <= endOfWeek
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sumOf { it.calories }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fungsi untuk menghitung total kalori bulanan
|
||||||
|
fun calculateMonthlyTotal(caloriesList: List<CaloryModel>): Int {
|
||||||
|
val calendar = Calendar.getInstance()
|
||||||
|
calendar.set(Calendar.DAY_OF_MONTH, 1)
|
||||||
|
calendar.set(Calendar.HOUR_OF_DAY, 0)
|
||||||
|
calendar.set(Calendar.MINUTE, 0)
|
||||||
|
calendar.set(Calendar.SECOND, 0)
|
||||||
|
calendar.set(Calendar.MILLISECOND, 0)
|
||||||
|
|
||||||
|
val startOfMonth = calendar.timeInMillis
|
||||||
|
|
||||||
|
calendar.add(Calendar.MONTH, 1)
|
||||||
|
calendar.add(Calendar.DAY_OF_MONTH, -1)
|
||||||
|
calendar.set(Calendar.HOUR_OF_DAY, 23)
|
||||||
|
calendar.set(Calendar.MINUTE, 59)
|
||||||
|
calendar.set(Calendar.SECOND, 59)
|
||||||
|
|
||||||
|
val endOfMonth = calendar.timeInMillis
|
||||||
|
|
||||||
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||||
|
|
||||||
|
return caloriesList
|
||||||
|
.filter {
|
||||||
|
try {
|
||||||
|
val date = dateFormat.parse(it.date)
|
||||||
|
date != null && date.time >= startOfMonth && date.time <= endOfMonth
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sumOf { it.calories }
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
//noinspection UsingMaterialAndMaterial3Libraries
|
//noinspection UsingMaterialAndMaterial3Libraries
|
||||||
import androidx.compose.material.OutlinedTextField
|
import androidx.compose.material.OutlinedTextField
|
||||||
//noinspection UsingMaterialAndMaterial3Libraries
|
//noinspection UsingMaterialAndMaterial3Libraries
|
||||||
|
@ -14,6 +15,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.example.caloryapp.R
|
import com.example.caloryapp.R
|
||||||
|
@ -27,7 +29,8 @@ fun CustomTextField(
|
||||||
value: String,
|
value: String,
|
||||||
onValueChange: (String) -> Unit,
|
onValueChange: (String) -> Unit,
|
||||||
placeholderText: String,
|
placeholderText: String,
|
||||||
input: Boolean
|
input: Boolean,
|
||||||
|
keyboardOptions: KeyboardOptions
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
textStyle = TextStyle(
|
textStyle = TextStyle(
|
||||||
|
@ -36,9 +39,11 @@ fun CustomTextField(
|
||||||
fontFamily = semibold,
|
fontFamily = semibold,
|
||||||
letterSpacing = 0.5.sp
|
letterSpacing = 0.5.sp
|
||||||
),
|
),
|
||||||
|
|
||||||
value = value,
|
value = value,
|
||||||
enabled = input,
|
enabled = input,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
placeholder = {
|
placeholder = {
|
||||||
androidx.compose.material.Text(
|
androidx.compose.material.Text(
|
||||||
text = placeholderText,
|
text = placeholderText,
|
||||||
|
|
Loading…
Reference in New Issue