diff --git a/app/build.gradle b/app/build.gradle
index 9a1a5aa..2ad6f2d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,7 +1,7 @@
plugins {
id 'com.android.application' version '9.2.1'
- id 'org.jetbrains.kotlin.plugin.serialization' version '2.3.21'
- id 'org.jetbrains.kotlin.plugin.compose' version '2.3.21'
+ id 'org.jetbrains.kotlin.plugin.serialization' version '2.4.0'
+ id 'org.jetbrains.kotlin.plugin.compose' version '2.4.0'
}
android {
@@ -14,7 +14,7 @@ android {
applicationId "net.helcel.cowspent"
minSdk = 26
targetSdk = 37
- versionName "1.0"
+ versionName "1.0a"
versionCode project.hasProperty('VERSION_CODE') ? project.property('VERSION_CODE').toInteger() : 1
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 67a0b67..778d94d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -20,11 +20,14 @@
android:supportsRtl="true"
android:theme="@style/AppTheme">
-
+
@@ -45,7 +48,7 @@
android:name="net.helcel.cowspent.android.account.AccountActivity"
android:label="@string/settings_server_settings"
android:parentActivityName="net.helcel.cowspent.android.settings.PreferencesActivity"
- android:windowSoftInputMode="stateVisible"
+ android:windowSoftInputMode="adjustResize|stateVisible"
android:launchMode="standard"
/>
@@ -66,7 +69,7 @@
android:name="net.helcel.cowspent.android.project.create.NewProjectActivity"
android:label="@string/simple_add_project"
android:parentActivityName="net.helcel.cowspent.android.main.BillsListViewActivity"
- android:windowSoftInputMode="stateHidden"
+ android:windowSoftInputMode="adjustResize"
android:launchMode="standard"
android:exported="true">
@@ -93,7 +96,7 @@
android:name="net.helcel.cowspent.android.project.edit.EditProjectActivity"
android:label="@string/simple_edit_project"
android:parentActivityName="net.helcel.cowspent.android.main.BillsListViewActivity"
- android:windowSoftInputMode="stateHidden"
+ android:windowSoftInputMode="adjustResize"
android:launchMode="standard">
@@ -101,7 +104,7 @@
android:name="net.helcel.cowspent.android.bill_edit.EditBillActivity"
android:label="@string/simple_edit_bill"
android:parentActivityName="net.helcel.cowspent.android.main.BillsListViewActivity"
- android:windowSoftInputMode="stateHidden"
+ android:windowSoftInputMode="adjustResize"
android:launchMode="standard">
diff --git a/app/src/main/java/net/helcel/cowspent/android/about/AboutActivity.kt b/app/src/main/java/net/helcel/cowspent/android/about/AboutActivity.kt
index a0606b1..a8c09d6 100644
--- a/app/src/main/java/net/helcel/cowspent/android/about/AboutActivity.kt
+++ b/app/src/main/java/net/helcel/cowspent/android/about/AboutActivity.kt
@@ -1,6 +1,7 @@
package net.helcel.cowspent.android.about
import android.os.Bundle
+import androidx.activity.enableEdgeToEdge
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import net.helcel.cowspent.theme.ThemeUtils
@@ -8,6 +9,7 @@ import net.helcel.cowspent.theme.ThemeUtils
class AboutActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
diff --git a/app/src/main/java/net/helcel/cowspent/android/account/AccountActivity.kt b/app/src/main/java/net/helcel/cowspent/android/account/AccountActivity.kt
index efe64de..3250acf 100644
--- a/app/src/main/java/net/helcel/cowspent/android/account/AccountActivity.kt
+++ b/app/src/main/java/net/helcel/cowspent/android/account/AccountActivity.kt
@@ -13,6 +13,7 @@ import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
+import androidx.activity.enableEdgeToEdge
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
@@ -73,6 +74,7 @@ class AccountActivity : AppCompatActivity() {
private var showLoginDialog by mutableStateOf(false)
override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
super.onCreate(savedInstanceState)
preferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt
index 4681bfc..8f3898e 100644
--- a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt
+++ b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt
@@ -5,6 +5,7 @@ import android.app.TimePickerDialog
import android.content.Intent
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
+import androidx.activity.enableEdgeToEdge
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
@@ -39,6 +40,7 @@ class EditBillActivity : AppCompatActivity() {
private val calendar = Calendar.getInstance()
override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
super.onCreate(savedInstanceState)
db = CowspentSQLiteOpenHelper.getInstance(this)
diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsActivity.kt b/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsActivity.kt
index 6345092..07545c1 100644
--- a/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsActivity.kt
+++ b/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsActivity.kt
@@ -3,6 +3,7 @@ package net.helcel.cowspent.android.bill_label
import android.content.Context
import android.content.Intent
import android.os.Bundle
+import androidx.activity.enableEdgeToEdge
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
@@ -20,6 +21,7 @@ class LabelBillsActivity : AppCompatActivity() {
private val viewModel: LabelBillsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
super.onCreate(savedInstanceState)
val projectId = intent.getLongExtra(EXTRA_PROJECT_ID, -1L)
diff --git a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt
index a0c711b..be736c5 100644
--- a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt
+++ b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt
@@ -3,6 +3,7 @@ package net.helcel.cowspent.android.currencies
import android.os.Bundle
import android.util.Log
import android.widget.Toast
+import androidx.activity.enableEdgeToEdge
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
@@ -40,6 +41,7 @@ class ManageCurrenciesActivity : AppCompatActivity() {
}
override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
super.onCreate(savedInstanceState)
intent.extras?.let {
diff --git a/app/src/main/java/net/helcel/cowspent/android/helper/QrCodeScannerActivity.kt b/app/src/main/java/net/helcel/cowspent/android/helper/QrCodeScannerActivity.kt
index 2c87df7..f5ba6fd 100644
--- a/app/src/main/java/net/helcel/cowspent/android/helper/QrCodeScannerActivity.kt
+++ b/app/src/main/java/net/helcel/cowspent/android/helper/QrCodeScannerActivity.kt
@@ -7,6 +7,7 @@ import android.os.Bundle
import android.os.VibrationEffect
import android.os.Vibrator
import android.util.Log
+import androidx.activity.enableEdgeToEdge
import androidx.activity.compose.setContent
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
@@ -42,6 +43,7 @@ class QrCodeScannerActivity : AppCompatActivity() {
private var isScanning = true
override fun onCreate(state: Bundle?) {
+ enableEdgeToEdge()
super.onCreate(state)
cameraExecutor = Executors.newSingleThreadExecutor()
diff --git a/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt b/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt
index 06df8f1..e479865 100644
--- a/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt
+++ b/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt
@@ -17,6 +17,7 @@ import android.util.Log
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
@@ -193,6 +194,7 @@ class BillsListViewActivity :
}
override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
super.onCreate(savedInstanceState)
isActivityVisible = true
if (savedInstanceState != null) {
diff --git a/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectActivity.kt b/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectActivity.kt
index 92f58a6..7bdf5d5 100644
--- a/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectActivity.kt
+++ b/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectActivity.kt
@@ -5,6 +5,7 @@ import android.net.Uri
import android.os.Bundle
import android.util.Patterns
import android.widget.Toast
+import androidx.activity.enableEdgeToEdge
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
@@ -33,6 +34,7 @@ class NewProjectActivity : AppCompatActivity() {
private lateinit var db: CowspentSQLiteOpenHelper
override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
super.onCreate(savedInstanceState)
db = CowspentSQLiteOpenHelper.getInstance(this)
diff --git a/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectActivity.kt b/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectActivity.kt
index a105eb8..4e39ac1 100644
--- a/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectActivity.kt
+++ b/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectActivity.kt
@@ -3,6 +3,7 @@ package net.helcel.cowspent.android.project.edit
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
+import androidx.activity.enableEdgeToEdge
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
@@ -27,6 +28,7 @@ class EditProjectActivity : AppCompatActivity() {
private lateinit var project: DBProject
override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
super.onCreate(savedInstanceState)
db = CowspentSQLiteOpenHelper.getInstance(this)
diff --git a/app/src/main/java/net/helcel/cowspent/android/settings/PreferencesActivity.kt b/app/src/main/java/net/helcel/cowspent/android/settings/PreferencesActivity.kt
index 854f23f..623c2fd 100644
--- a/app/src/main/java/net/helcel/cowspent/android/settings/PreferencesActivity.kt
+++ b/app/src/main/java/net/helcel/cowspent/android/settings/PreferencesActivity.kt
@@ -3,6 +3,7 @@ package net.helcel.cowspent.android.settings
import android.content.Intent
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
+import androidx.activity.enableEdgeToEdge
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.getValue
@@ -25,6 +26,7 @@ import net.helcel.cowspent.util.ColorUtils
class PreferencesActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
super.onCreate(savedInstanceState)
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
diff --git a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSankeyDiagram.kt b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSankeyDiagram.kt
index 19410a1..416a5cc 100644
--- a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSankeyDiagram.kt
+++ b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSankeyDiagram.kt
@@ -1,34 +1,86 @@
package net.helcel.cowspent.android.statistics
import android.annotation.SuppressLint
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.layout.*
-import androidx.compose.material.*
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberDraggableState
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.DropdownMenuItem
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.LocalContentColor
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Group
-import androidx.compose.runtime.*
+import androidx.compose.material.icons.filled.ViewColumn
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableLongStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.CornerRadius
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import androidx.compose.ui.res.stringResource
+import androidx.core.graphics.toColorInt
+import kotlinx.coroutines.launch
import net.helcel.cowspent.R
-import net.helcel.cowspent.android.helper.*
+import net.helcel.cowspent.android.helper.EditableExposedDropdownMenu
+import net.helcel.cowspent.android.helper.UserAvatar
+import net.helcel.cowspent.android.helper.formatShortValue
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCategory
import net.helcel.cowspent.model.DBMember
-import androidx.core.graphics.toColorInt
+import kotlin.math.abs
+import kotlin.math.roundToInt
+
+private object SankeyDimens {
+ val NodeHeight = 76.dp
+ val TotalNodeHeight = 42.dp
+ val NormalGap = 8.dp
+ val FocusGap = 16.dp
+ val ActiveMinWidth = 160.dp
+}
@OptIn(ExperimentalMaterialApi::class)
@SuppressLint("UseKtx")
@@ -41,15 +93,11 @@ fun ProjectSankeyDiagram(
onShareReady: (String) -> Unit
) {
val shareStatsIntro = stringResource(R.string.share_stats_intro, projectName)
-
var selectedMemberId by remember { mutableLongStateOf(-1L) }
var expanded by remember { mutableStateOf(false) }
val activeBills = remember(allBills) {
- allBills.filter {
- it.state != DBBill.STATE_DELETED &&
- it.categoryRemoteId != DBBill.CATEGORY_REIMBURSEMENT
- }
+ allBills.filter { it.state != DBBill.STATE_DELETED && it.categoryRemoteId != DBBill.CATEGORY_REIMBURSEMENT }
}
val membersMap = remember(allMembers) { allMembers.associateBy { it.id } }
@@ -62,408 +110,293 @@ fun ProjectSankeyDiagram(
return
}
- // Consolidated spending calculation
val spendings = remember(activeBills, selectedMemberId, membersMap) {
- val spentMap = mutableMapOf()
+ val spentMap = mutableMapOf().apply { membersMap.keys.forEach { put(it, 0.0) } }
val catMap = mutableMapOf()
- membersMap.keys.forEach { spentMap[it] = 0.0 }
activeBills.forEach { bill ->
- val totalWeight = bill.billOwers.sumOf { bo ->
- membersMap[bo.memberId]?.weight ?: 1.0
- }
+ val totalWeight = bill.billOwers.sumOf { membersMap[it.memberId]?.weight ?: 1.0 }
if (totalWeight > 0) {
if (selectedMemberId == -1L) {
catMap[bill.categoryRemoteId] = (catMap[bill.categoryRemoteId] ?: 0.0) + bill.amount
bill.billOwers.forEach { bo ->
val weight = membersMap[bo.memberId]?.weight ?: 1.0
- val share = (bill.amount / totalWeight) * weight
- spentMap[bo.memberId] = (spentMap[bo.memberId] ?: 0.0) + share
+ spentMap[bo.memberId] = (spentMap[bo.memberId] ?: 0.0) + (bill.amount / totalWeight) * weight
}
} else {
bill.billOwers.find { it.memberId == selectedMemberId }?.let { bo ->
val weight = membersMap[bo.memberId]?.weight ?: 1.0
- val share = (bill.amount / totalWeight) * weight
- catMap[bill.categoryRemoteId] = (catMap[bill.categoryRemoteId] ?: 0.0) + share
- spentMap[selectedMemberId] = (spentMap[selectedMemberId] ?: 0.0) + share
+ catMap[bill.categoryRemoteId] = (catMap[bill.categoryRemoteId] ?: 0.0) + (bill.amount / totalWeight) * weight
+ spentMap[selectedMemberId] = (spentMap[selectedMemberId] ?: 0.0) + (bill.amount / totalWeight) * weight
}
}
}
}
-
- val memberList = spentMap.toList().filter { it.second > 0 }.sortedByDescending { it.second }
- val categoryList = catMap.toList().sortedByDescending { it.second }
- memberList to categoryList
+ spentMap.toList().filter { it.second > 0 }.sortedByDescending { it.second } to catMap.toList().sortedByDescending { it.second }
}
val displayMemberSpendings = spendings.first
val displayCategorySpendings = spendings.second
val totalAmount = remember(displayMemberSpendings) { displayMemberSpendings.sumOf { it.second } }
+ var isSpecialMode by remember { mutableStateOf(false) }
LaunchedEffect(displayMemberSpendings, displayCategorySpendings, totalAmount, projectName) {
- val statsText = StringBuilder()
- statsText.append("// ").append(shareStatsIntro.replace("\n", "\n// ")).append("\n\n")
-
- val middleNode = if (selectedMemberId == -1L) "Total" else "Spent"
-
- displayMemberSpendings.forEach { (memberId, amount) ->
- val name = membersMap[memberId]?.name ?: "???"
- statsText.append("$name [${formatShortValue(amount)}] $middleNode\n")
- }
-
- statsText.append("\n")
-
- displayCategorySpendings.forEach { (catRemoteId, amount) ->
- val name = categoriesMap[catRemoteId]?.name ?: "Other"
- statsText.append("$middleNode [${formatShortValue(amount)}] $name\n")
- }
-
- statsText.append("\n")
-
- displayMemberSpendings.forEach { (memberId, _) ->
- val member = membersMap[memberId]
- if (member != null) {
- val hexColor = String.format("#%02x%02x%02x", member.r ?: 128, member.g ?: 128, member.b ?: 128)
- statsText.append(":${member.name} $hexColor\n")
+ val statsText = StringBuilder().apply {
+ append("// ").append(shareStatsIntro.replace("\n", "\n// ")).append("\n\n")
+ val middleNode = if (selectedMemberId == -1L) "Total" else "Spent"
+ displayMemberSpendings.forEach { (id, amount) -> append("${membersMap[id]?.name ?: "???"} [${formatShortValue(amount)}] $middleNode\n") }
+ append("\n")
+ displayCategorySpendings.forEach { (id, amount) -> append("$middleNode [${formatShortValue(amount)}] ${categoriesMap[id]?.name ?: "Other"}\n") }
+ append("\n")
+ displayMemberSpendings.forEach { (id, _) ->
+ membersMap[id]?.let { append(":${it.name} ${String.format("#%02x%02x%02x", it.r ?: 128, it.g ?: 128, it.b ?: 128)}\n") }
}
+ displayCategorySpendings.forEach { (id, _) -> categoriesMap[id]?.let { append(":${it.name ?: "Other"} ${it.color}\n") } }
}
-
- displayCategorySpendings.forEach { (catRemoteId, _) ->
- val category = categoriesMap[catRemoteId]
- if (category != null) {
- statsText.append(":${category.name ?: "Other"} ${category.color}\n")
- }
- }
-
onShareReady(statsText.toString())
}
- Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
- val selectedMember = membersMap[selectedMemberId]
- EditableExposedDropdownMenu(
- value = selectedMember?.name ?: "All Members",
- placeholder = "Filter by member",
- expanded = expanded,
- onExpandedChange = { expanded = it },
- onDismissRequest = { expanded = false },
- modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
- leadingIcon = {
- Box(modifier = Modifier.padding(start = 12.dp)) {
- if (selectedMember != null) {
- UserAvatar(
- name = selectedMember.name,
- r = selectedMember.r,
- g = selectedMember.g,
- b = selectedMember.b,
- disabled = !selectedMember.isActivated,
- size = 24.dp
- )
- } else {
- Icon(Icons.Default.Group, contentDescription = null)
- }
- }
- },
- content = {
- DropdownMenuItem(onClick = {
- selectedMemberId = -1L
- expanded = false
- }) {
- Icon(Icons.Default.Group, contentDescription = null)
- Spacer(modifier = Modifier.width(12.dp))
- Text("All Members")
- }
- allMembers.forEach { member ->
- DropdownMenuItem(onClick = {
- selectedMemberId = member.id
- expanded = false
- }) {
- UserAvatar(
- name = member.name,
- r = member.r,
- g = member.g,
- b = member.b,
- disabled = !member.isActivated,
- size = 24.dp
- )
- Spacer(modifier = Modifier.width(12.dp))
- Text(member.name)
+ Column(modifier = Modifier.fillMaxSize()) {
+ Box(modifier = Modifier.padding(16.dp)) {
+ val selectedMember = membersMap[selectedMemberId]
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ EditableExposedDropdownMenu(
+ value = selectedMember?.name ?: "All Members",
+ placeholder = "Filter by member",
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
+ onDismissRequest = { expanded = false },
+ modifier = Modifier.weight(1f).padding(bottom = 16.dp),
+ leadingIcon = {
+ Box(modifier = Modifier.padding(start = 12.dp)) {
+ if (selectedMember != null) {
+ UserAvatar(name = selectedMember.name, r = selectedMember.r, g = selectedMember.g, b = selectedMember.b, disabled = !selectedMember.isActivated, size = 24.dp)
+ } else Icon(Icons.Default.Group, contentDescription = null)
+ }
+ },
+ content = {
+ DropdownMenuItem(onClick = { selectedMemberId = -1L; expanded = false }) {
+ Icon(Icons.Default.Group, contentDescription = null)
+ Spacer(modifier = Modifier.width(12.dp))
+ Text("All Members")
+ }
+ allMembers.forEach { member ->
+ DropdownMenuItem(onClick = { selectedMemberId = member.id; expanded = false }) {
+ UserAvatar(name = member.name, r = member.r, g = member.g, b = member.b, disabled = !member.isActivated, size = 24.dp)
+ Spacer(modifier = Modifier.width(12.dp))
+ Text(member.name)
+ }
+ }
}
+ )
+ IconButton(onClick = { isSpecialMode = !isSpecialMode }, modifier = Modifier.padding(bottom = 16.dp)) {
+ Icon(Icons.Default.ViewColumn, contentDescription = "Focus Mode", tint = if (isSpecialMode) MaterialTheme.colors.primary else LocalContentColor.current)
}
}
- )
+ }
if (totalAmount <= 0) {
Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {
Text("No spending data for selected filter", style = MaterialTheme.typography.body1)
}
} else {
- BoxWithConstraints(modifier = Modifier.weight(1f).fillMaxWidth()) {
- val nodeHeightMember = 76.dp
- val nodeHeightCategory = 76.dp
- val nodeHeightTotal = 42.dp
- val horizontalGap = 8.dp
-
- val memberCount = displayMemberSpendings.size
- val categoryCount = displayCategorySpendings.size
-
- val maxGaps = maxOf(memberCount - 1, categoryCount - 1, 0)
- val usableWidth = (maxWidth.value - horizontalGap.value * maxGaps).coerceAtLeast(0f)
- val moneyScale = if (totalAmount > 0) usableWidth / totalAmount.toFloat() else 0f
-
- Canvas(modifier = Modifier.fillMaxSize()) {
- val nodeHeightMemberPx = nodeHeightMember.toPx()
- val nodeHeightCategoryPx = nodeHeightCategory.toPx()
- val nodeHeightTotalPx = nodeHeightTotal.toPx()
- val gapPx = horizontalGap.toPx()
-
- val totalWidthPx = size.width
- val usableWidthPx = totalWidthPx - gapPx * maxGaps
- val moneyScalePx = if (totalAmount > 0) usableWidthPx / totalAmount else 0.0
-
- val totalNodeWidthPx = (totalAmount * moneyScalePx).toFloat()
- val totalNodeXPx = (totalWidthPx - totalNodeWidthPx) / 2
-
- val topY = 0f
- val middleY = (size.height - nodeHeightTotalPx) / 2
- val bottomY = size.height - nodeHeightCategoryPx
-
- val totalNodeColor = Color(0xFF333333)
-
- // Flows
- var currentXTop = (totalWidthPx - (totalAmount * moneyScalePx + (memberCount - 1).coerceAtLeast(0) * gapPx).toFloat()) / 2
- var currentXInTotalTop = totalNodeXPx
- displayMemberSpendings.forEach { (memberId, amount) ->
- val member = membersMap[memberId]
- val boxWidth = (amount * moneyScalePx).toFloat()
- val color = member?.let {
- Color(it.r ?: 128, it.g ?: 128, it.b ?: 128)
- } ?: Color.Gray
-
- if (boxWidth > 0.5f) {
- drawSankeyFlow(
- startX = currentXTop,
- startY = topY + nodeHeightMemberPx * 0.5f,
- startWidth = boxWidth,
- endX = currentXInTotalTop,
- endY = middleY + nodeHeightTotalPx * 0.5f,
- endWidth = boxWidth,
- startColor = color.copy(alpha = 0.5f),
- endColor = totalNodeColor.copy(alpha = 0.35f)
- )
- }
- currentXTop += boxWidth + gapPx
- currentXInTotalTop += boxWidth
- }
-
- var currentXBottom = (totalWidthPx - (totalAmount * moneyScalePx + (categoryCount - 1).coerceAtLeast(0) * gapPx).toFloat()) / 2
- var currentXInTotalBottom = totalNodeXPx
- displayCategorySpendings.forEach { (catRemoteId, amount) ->
- val category = categoriesMap[catRemoteId]
- val boxWidth = (amount * moneyScalePx).toFloat()
- val color = category?.color?.let {
- try { Color(it.toColorInt()) } catch (_: Exception) { Color(0xFF999999) }
- } ?: Color(0xFF999999)
-
- if (boxWidth > 0.5f) {
- drawSankeyFlow(
- startX = currentXInTotalBottom,
- startY = middleY + nodeHeightTotalPx * 0.5f,
- startWidth = boxWidth,
- endX = currentXBottom,
- endY = bottomY + nodeHeightCategoryPx * 0.5f,
- endWidth = boxWidth,
- startColor = totalNodeColor.copy(alpha = 0.35f),
- endColor = color.copy(alpha = 0.5f)
- )
- }
- currentXInTotalBottom += boxWidth
- currentXBottom += boxWidth + gapPx
- }
-
- // Nodes
- currentXTop = (totalWidthPx - (totalAmount * moneyScalePx + (memberCount - 1).coerceAtLeast(0) * gapPx).toFloat()) / 2
- displayMemberSpendings.forEach { (memberId, amount) ->
- val member = membersMap[memberId]
- val width = (amount * moneyScalePx).toFloat()
- val color = member?.let {
- Color(it.r ?: 128, it.g ?: 128, it.b ?: 128)
- } ?: Color.Gray
-
- if (width > 0.5f) {
- drawRoundRect(
- color = color,
- topLeft = Offset(currentXTop, topY),
- size = Size(width, nodeHeightMemberPx),
- cornerRadius = CornerRadius(10.dp.toPx())
- )
- }
- currentXTop += width + gapPx
- }
-
- drawRect(
- color = totalNodeColor,
- topLeft = Offset(totalNodeXPx, middleY),
- size = Size(totalNodeWidthPx, nodeHeightTotalPx)
- )
-
- currentXBottom = (totalWidthPx - (totalAmount * moneyScalePx + (categoryCount - 1).coerceAtLeast(0) * gapPx).toFloat()) / 2
- displayCategorySpendings.forEach { (catRemoteId, amount) ->
- val category = categoriesMap[catRemoteId]
- val width = (amount * moneyScalePx).toFloat()
- val color = category?.color?.let {
- try { Color(it.toColorInt()) } catch (_: Exception) { Color(0xFF999999) }
- } ?: Color(0xFF999999)
-
- if (width > 0.5f) {
- drawRoundRect(
- color = color,
- topLeft = Offset(currentXBottom, bottomY),
- size = Size(width, nodeHeightCategoryPx),
- cornerRadius = CornerRadius(10.dp.toPx())
- )
- }
- currentXBottom += width + gapPx
- }
- }
-
- // Member labels
- var currentXTopLabel = (maxWidth.value - (totalAmount.toFloat() * moneyScale + (memberCount - 1).coerceAtLeast(0) * horizontalGap.value)) / 2
- displayMemberSpendings.forEach { (memberId, amount) ->
- val member = membersMap[memberId]
- val widthValue = amount.toFloat() * moneyScale
- val width = widthValue.dp
- if (widthValue > 12f) {
- Box(
- modifier = Modifier
- .offset(x = currentXTopLabel.dp, y = 0.dp)
- .size(width, nodeHeightMember),
- contentAlignment = Alignment.Center
- ) {
- if (width >= 36.dp) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(
- text = member?.name ?: "???",
- fontSize = 16.sp,
- color = Color.White,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- fontWeight = FontWeight.Bold
- )
- Text(
- text = formatShortValue(amount),
- fontSize = 14.sp,
- color = Color.White.copy(alpha = 0.9f)
- )
- }
- }
- }
- }
- currentXTopLabel += widthValue + horizontalGap.value
- }
-
- // Total label
- Box(
- modifier = Modifier
- .align(Alignment.Center)
- .fillMaxWidth()
- .height(nodeHeightTotal),
- contentAlignment = Alignment.Center
- ) {
- Text(
- text = if (selectedMemberId == -1L) formatShortValue(totalAmount) else "SPENT: ${formatShortValue(totalAmount)}",
- fontSize = 16.sp,
- color = Color.White,
- textAlign = TextAlign.Center,
- fontWeight = FontWeight.ExtraBold
- )
- }
-
- // Category labels
- var currentXBottomLabel = (maxWidth.value - (totalAmount.toFloat() * moneyScale + (categoryCount - 1).coerceAtLeast(0) * horizontalGap.value)) / 2
- displayCategorySpendings.forEach { (catRemoteId, amount) ->
- val category = categoriesMap[catRemoteId]
- val widthValue = amount.toFloat() * moneyScale
- val width = widthValue.dp
- if (widthValue > 12f) {
- Box(
- modifier = Modifier
- .align(Alignment.BottomStart)
- .offset(x = currentXBottomLabel.dp)
- .size(width, nodeHeightCategory),
- contentAlignment = Alignment.Center
- ) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(
- text = category?.icon ?: "❔",
- fontSize = 20.sp
- )
- if (width >= 32.dp) {
- Text(
- text = formatShortValue(amount),
- fontSize = 14.sp,
- color = Color.White.copy(alpha = 0.9f)
- )
- }
- }
- }
- }
- currentXBottomLabel += widthValue + horizontalGap.value
- }
- }
+ SankeyContent(isSpecialMode, selectedMemberId, totalAmount, displayMemberSpendings, displayCategorySpendings, membersMap, categoriesMap)
}
Spacer(Modifier.fillMaxWidth().height(32.dp))
}
}
-private fun DrawScope.drawSankeyFlow(
- startX: Float,
- startY: Float,
- startWidth: Float,
- endX: Float,
- endY: Float,
- endWidth: Float,
- startColor: Color,
- endColor: Color
+private data class NodeLayout(val centerX: Float, val visualWidth: Float)
+
+@SuppressLint("FrequentlyChangingValue")
+@Composable
+private fun SankeyContent(
+ isSpecialMode: Boolean,
+ selectedMemberId: Long,
+ totalAmount: Double,
+ displayMemberSpendings: List>,
+ displayCategorySpendings: List>,
+ membersMap: Map,
+ categoriesMap: Map
) {
+ val density = LocalDensity.current
+ val scope = rememberCoroutineScope()
+
+ val topFocalIndex = remember { Animatable(0f) }
+ val bottomFocalIndex = remember { Animatable(0f) }
+
+ var diagramAreaCoordinates by remember { mutableStateOf(null) }
+ var totalNodeCoordinates by remember { mutableStateOf(null) }
+
+ LaunchedEffect(isSpecialMode) {
+ if (!isSpecialMode) {
+ topFocalIndex.snapTo(0f)
+ bottomFocalIndex.snapTo(0f)
+ }
+ }
+
+ BoxWithConstraints(modifier = Modifier.fillMaxSize().onGloballyPositioned { diagramAreaCoordinates = it }) {
+ val totalMaxWidthPx = with(density) { maxWidth.toPx() }
+ val normalGapPx = with(density) { SankeyDimens.NormalGap.toPx() }
+ val focusGapPx = with(density) { SankeyDimens.FocusGap.toPx() }
+ val activeMinPx = with(density) { SankeyDimens.ActiveMinWidth.toPx() }
+
+ val moneyScalePx = if (totalAmount > 0) (totalMaxWidthPx - (maxOf(displayMemberSpendings.size, displayCategorySpendings.size) - 1) * normalGapPx).coerceAtLeast(0f) / totalAmount else 0.0
+
+ fun calculateRowLayout(spendings: List>, focalIndex: Float): List {
+ val visualWidths = spendings.mapIndexed { i, (_, amount) ->
+ val propWidthPx = (amount * moneyScalePx).toFloat()
+ if (!isSpecialMode) propWidthPx
+ else {
+ val focusAmount = (1f - abs(i - focalIndex)).coerceIn(0f, 1f)
+ maxOf(propWidthPx, activeMinPx * focusAmount)
+ }
+ }
+
+ return if (!isSpecialMode) {
+ val totalWidthPx = visualWidths.sum() + (spendings.size - 1).coerceAtLeast(0) * normalGapPx
+ var currentX = (totalMaxWidthPx - totalWidthPx) / 2
+ visualWidths.map { w ->
+ NodeLayout(currentX + w / 2, w).also { currentX += w + normalGapPx }
+ }
+ } else {
+ val centers = mutableListOf()
+ var currentX = 0f
+ visualWidths.forEachIndexed { i, w ->
+ if (i > 0) currentX += visualWidths[i - 1] / 2 + focusGapPx + w / 2
+ else currentX = w / 2
+ centers.add(currentX)
+ }
+
+ val focalPointX = if (spendings.isEmpty()) 0f else {
+ val idx = focalIndex.coerceIn(0f, (spendings.size - 1).toFloat())
+ val low = idx.toInt()
+ val high = (low + 1).coerceAtMost(spendings.size - 1)
+ val fract = idx - low
+ centers[low] * (1 - fract) + centers[high] * fract
+ }
+
+ centers.mapIndexed { i, c ->
+ NodeLayout(totalMaxWidthPx / 2 + (c - focalPointX), visualWidths[i])
+ }
+ }
+ }
+
+ val topLayout = calculateRowLayout(displayMemberSpendings, topFocalIndex.value)
+ val bottomLayout = calculateRowLayout(displayCategorySpendings, bottomFocalIndex.value)
+
+ Canvas(modifier = Modifier.fillMaxSize()) {
+ val root = diagramAreaCoordinates ?: return@Canvas
+ val totalRect = totalNodeCoordinates?.let {
+ val pos = it.positionInRoot() - root.positionInRoot()
+ Rect(pos.x, pos.y, pos.x + it.size.width, pos.y + it.size.height)
+ } ?: return@Canvas
+ val flowMoneyScalePx = totalRect.width / totalAmount
+ val totalNodeColor = Color(0xFF333333)
+
+ displayMemberSpendings.forEachIndexed { index, (_, amount) ->
+ val flowWidth = (amount * flowMoneyScalePx).toFloat()
+ val layout = topLayout[index]
+ if (flowWidth > 0.5f) {
+ val color = membersMap[displayMemberSpendings[index].first]?.let { Color(it.r ?: 128, it.g ?: 128, it.b ?: 128) } ?: Color.Gray
+ drawSankeyFlow(layout.centerX - layout.visualWidth / 2, SankeyDimens.NodeHeight.toPx() / 2, layout.visualWidth, totalRect.left + displayMemberSpendings.take(index).sumOf { it.second * flowMoneyScalePx }.toFloat(), totalRect.top + totalRect.height / 2, flowWidth, color.copy(alpha = 0.5f), totalNodeColor.copy(alpha = 0.35f))
+ }
+ }
+ displayCategorySpendings.forEachIndexed { index, (_, amount) ->
+ val flowWidth = (amount * flowMoneyScalePx).toFloat()
+ val layout = bottomLayout[index]
+ if (flowWidth > 0.5f) {
+ val color = categoriesMap[displayCategorySpendings[index].first]?.color?.let { try { Color(it.toColorInt()) } catch (_: Exception) { Color(0xFF999999) } } ?: Color(0xFF999999)
+ drawSankeyFlow(totalRect.left + displayCategorySpendings.take(index).sumOf { it.second * flowMoneyScalePx }.toFloat(), totalRect.top + totalRect.height / 2, flowWidth, layout.centerX - layout.visualWidth / 2, size.height - SankeyDimens.NodeHeight.toPx() / 2, layout.visualWidth, totalNodeColor.copy(alpha = 0.35f), color.copy(alpha = 0.5f))
+ }
+ }
+ }
+
+ Column(modifier = Modifier.fillMaxSize()) {
+ // TOP Row
+ Box(modifier = Modifier.fillMaxWidth().height(SankeyDimens.NodeHeight).then(if (isSpecialMode) Modifier.draggable(
+ orientation = Orientation.Horizontal,
+ state = rememberDraggableState { delta ->
+ scope.launch {
+ val currentIdx = topFocalIndex.value.roundToInt().coerceIn(displayMemberSpendings.indices)
+ val step = topLayout[currentIdx].visualWidth + focusGapPx
+ topFocalIndex.snapTo((topFocalIndex.value - delta / step).coerceIn(0f, (displayMemberSpendings.size - 1).toFloat()))
+ }
+ },
+ onDragStopped = {
+ topFocalIndex.animateTo(topFocalIndex.value.roundToInt().toFloat(), spring(Spring.DampingRatioLowBouncy, Spring.StiffnessLow))
+ }
+ ) else Modifier)) {
+ displayMemberSpendings.forEachIndexed { index, (id, amount) ->
+ val member = membersMap[id]
+ val layout = topLayout[index]
+ val wDp = with(density) { layout.visualWidth.toDp() }
+ val xDp = with(density) { (layout.centerX - layout.visualWidth / 2).toDp() }
+
+ Box(modifier = Modifier.offset(x = xDp).width(wDp).fillMaxHeight()
+ .then(if (isSpecialMode) Modifier.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) {
+ scope.launch { topFocalIndex.animateTo(index.toFloat(), spring(Spring.DampingRatioLowBouncy, Spring.StiffnessLow)) }
+ } else Modifier), contentAlignment = Alignment.Center) {
+ Column(modifier = Modifier.fillMaxSize().background(color = member?.let { Color(it.r ?: 128, it.g ?: 128, it.b ?: 128) } ?: Color.Gray, shape = RoundedCornerShape(10.dp)), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
+ Text(text = member?.name ?: "???", color = Color.White, fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, modifier = Modifier.wrapContentWidth(unbounded = true))
+ if (wDp >= 40.dp) Text(text = formatShortValue(amount), fontSize = 12.sp, color = Color.White.copy(alpha = 0.9f), textAlign = TextAlign.Center)
+ }
+ }
+ }
+ }
+
+ Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {
+ Box(modifier = Modifier.width(with(density) { (totalAmount * moneyScalePx).toFloat().toDp() }).height(SankeyDimens.TotalNodeHeight).background(Color(0xFF333333)).onGloballyPositioned { totalNodeCoordinates = it }, contentAlignment = Alignment.Center) {
+ Text(text = if (selectedMemberId == -1L) formatShortValue(totalAmount) else "SPENT: ${formatShortValue(totalAmount)}", color = Color.White, fontWeight = FontWeight.ExtraBold, fontSize = 16.sp, modifier = Modifier.padding(horizontal = 8.dp), maxLines = 1)
+ }
+ }
+
+ // BOTTOM Row
+ Box(modifier = Modifier.fillMaxWidth().height(SankeyDimens.NodeHeight).then(if (isSpecialMode) Modifier.draggable(
+ orientation = Orientation.Horizontal,
+ state = rememberDraggableState { delta ->
+ scope.launch {
+ val currentIdx = bottomFocalIndex.value.roundToInt().coerceIn(displayCategorySpendings.indices)
+ val step = bottomLayout[currentIdx].visualWidth + focusGapPx
+ bottomFocalIndex.snapTo((bottomFocalIndex.value - delta / step).coerceIn(0f, (displayCategorySpendings.size - 1).toFloat()))
+ }
+ },
+ onDragStopped = {
+ bottomFocalIndex.animateTo(bottomFocalIndex.value.roundToInt().toFloat(), spring(Spring.DampingRatioLowBouncy, Spring.StiffnessLow))
+ }
+ ) else Modifier)) {
+ displayCategorySpendings.forEachIndexed { index, (id, amount) ->
+ val category = categoriesMap[id]
+ val layout = bottomLayout[index]
+ val wDp = with(density) { layout.visualWidth.toDp() }
+ val xDp = with(density) { (layout.centerX - layout.visualWidth / 2).toDp() }
+
+ Box(modifier = Modifier.offset(x = xDp).width(wDp).fillMaxHeight()
+ .then(if (isSpecialMode) Modifier.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) {
+ scope.launch { bottomFocalIndex.animateTo(index.toFloat(), spring(Spring.DampingRatioLowBouncy, Spring.StiffnessLow)) }
+ } else Modifier), contentAlignment = Alignment.Center) {
+ Column(modifier = Modifier.fillMaxSize().background(color = category?.color?.let { try { Color(it.toColorInt()) } catch (_: Exception) { Color(0xFF999999) } } ?: Color(0xFF999999), shape = RoundedCornerShape(10.dp)), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
+ Text(text = category?.icon ?: "❔", fontSize = 20.sp, textAlign = TextAlign.Center, modifier = Modifier.wrapContentWidth(unbounded = true))
+ if (wDp >= 40.dp) Text(text = formatShortValue(amount), fontSize = 12.sp, color = Color.White.copy(alpha = 0.9f), textAlign = TextAlign.Center)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+private fun DrawScope.drawSankeyFlow(startX: Float, startY: Float, startWidth: Float, endX: Float, endY: Float, endWidth: Float, startColor: Color, endColor: Color) {
val path = Path().apply {
moveTo(startX, startY)
- cubicTo(
- startX, startY + (endY - startY) * 0.5f,
- endX, endY - (endY - startY) * 0.5f,
- endX, endY
- )
+ cubicTo(startX, startY + (endY - startY) * 0.5f, endX, endY - (endY - startY) * 0.5f, endX, endY)
lineTo(endX + endWidth, endY)
- cubicTo(
- endX + endWidth, endY - (endY - startY) * 0.5f,
- startX + startWidth, startY + (endY - startY) * 0.5f,
- startX + startWidth, startY
- )
+ cubicTo(endX + endWidth, endY - (endY - startY) * 0.5f, startX + startWidth, startY + (endY - startY) * 0.5f, startX + startWidth, startY)
close()
}
- drawPath(
- path = path,
- brush = Brush.verticalGradient(
- colors = listOf(startColor, endColor),
- startY = startY,
- endY = endY
- )
- )
+ drawPath(path = path, brush = Brush.verticalGradient(colors = listOf(startColor, endColor), startY = startY, endY = endY))
}
@Preview(showBackground = true, widthDp = 360, heightDp = 640)
@Composable
-fun ProjectSankeyDiagramPreview() {
- MaterialTheme {
- ProjectSankeyDiagram(
- projectName = "Test Project",
- allMembers = StatisticsMockData.members,
- allBills = StatisticsMockData.bills,
- customCategories = emptyList(),
- onShareReady = {}
- )
- }
-}
+fun ProjectSankeyDiagramPreview() = MaterialTheme { ProjectSankeyDiagram("Test Project", StatisticsMockData.members, StatisticsMockData.bills, emptyList()) {} }
diff --git a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsActivity.kt b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsActivity.kt
index 8ff5c6a..5677935 100644
--- a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsActivity.kt
+++ b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsActivity.kt
@@ -3,6 +3,7 @@ package net.helcel.cowspent.android.statistics
import android.content.Context
import android.content.Intent
import android.os.Bundle
+import androidx.activity.enableEdgeToEdge
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
@@ -15,6 +16,7 @@ import net.helcel.cowspent.theme.ThemeUtils
class ProjectStatisticsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
super.onCreate(savedInstanceState)
val projectId = intent.getLongExtra(EXTRA_PROJECT_ID, -1L)
diff --git a/app/src/main/java/net/helcel/cowspent/theme/ThemeUtils.kt b/app/src/main/java/net/helcel/cowspent/theme/ThemeUtils.kt
index dfdcb6c..2b371fa 100644
--- a/app/src/main/java/net/helcel/cowspent/theme/ThemeUtils.kt
+++ b/app/src/main/java/net/helcel/cowspent/theme/ThemeUtils.kt
@@ -100,14 +100,18 @@ object ThemeUtils {
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
- Column {
+ Column(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.primary)
.statusBarsPadding()
)
- Box(modifier = Modifier.fillMaxSize()) {
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .navigationBarsPadding()
+ ) {
content()
}
}