Compare commits

...

10 Commits

Author SHA1 Message Date
Sockenklaus
84c40317a5 Merge HEAD with 6e0eb1c8f4 2022-11-19 15:53:35 +01:00
Sockenklaus
cd1ce2ccda Several changes to implement more streamlined UX. 2022-11-19 00:03:02 +01:00
Sockenklaus
a7c92b020b LiveData to Flow 2022-11-17 10:04:35 +01:00
Sockenklaus
c20d5d55e5 Lots of work on charges list... lots of work to do... 2022-11-17 02:47:33 +01:00
Sockenklaus
49eabce142 Updated dependencies 2022-11-16 14:51:43 +01:00
sockenklaus
6e0eb1c8f4 Implemented some keyboardActions.
Worked on #17
2022-07-25 21:43:23 +02:00
Sockenklaus
0f99fb589c Some work on #17, listeners still missing. 2022-07-23 23:03:17 +02:00
Pascal König
8ca5db7c52 Merge branch 'auto-capitalize' of socrates/batterytracker into master 2022-07-23 16:42:51 +02:00
sockenklaus
3b2e09c134 Implemented auto-capitalization toggle in AddBattery.kt.
Fixed case-sensitive sorting when retrieving batteries
Fixed behaviour of MyOutlinedTextFieldWithSuffix that always used up all the space.
2022-07-23 16:39:45 +02:00
sockenklaus
ebbee65330 Fixed #13 unified outer padding on all components.
Fixed not setting appTitle when navigating via FAB.
Unified "currentScreen" / "appTitle" behaviour.
2022-07-23 15:24:20 +02:00
19 changed files with 498 additions and 350 deletions

17
.idea/deploymentTargetDropDown.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="C:\Android\.android\avd\Pixel_3a_API_30_x86.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2022-11-16T20:26:11.531085900Z" />
</component>
</project>

View File

@@ -11,12 +11,12 @@ android {
release {
}
}
compileSdk 32
compileSdk 33
defaultConfig {
applicationId "com.sockenklaus.batterytracker"
minSdk 29
targetSdk 32
targetSdk 33
versionCode 1
versionName "1.0"
@@ -41,6 +41,8 @@ android {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
applicationIdSuffix '.release'
versionNameSuffix 'release'
}
}
compileOptions {
@@ -63,50 +65,46 @@ android {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
namespace 'com.sockenklaus.batterytracker'
flavorDimensions
}
dependencies {
implementation 'androidx.room:room-runtime:2.4.2'
implementation 'androidx.room:room-rxjava3:2.4.2'
implementation "androidx.room:room-paging:2.4.2"
implementation "androidx.room:room-ktx:2.4.2"
kapt 'androidx.room:room-compiler:2.4.2'
implementation 'androidx.room:room-runtime:2.4.3'
implementation 'androidx.room:room-rxjava3:2.4.3'
implementation "androidx.room:room-paging:2.4.3"
implementation "androidx.room:room-ktx:2.4.3"
kapt 'androidx.room:room-compiler:2.4.3'
implementation "androidx.compose.ui:ui:1.3.0-alpha01"
implementation "androidx.compose.ui:ui-tooling-preview:1.3.0-alpha01"
implementation 'androidx.compose.material:material-icons-extended:1.3.0-alpha01'
implementation 'androidx.compose.material:material:1.3.0-alpha01'
implementation 'androidx.compose.runtime:runtime-livedata:1.1.1'
implementation 'androidx.compose.animation:animation:1.3.0-alpha01'
implementation 'androidx.compose.ui:ui-tooling:1.3.0-alpha01'
androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.3.0-alpha01'
debugImplementation "androidx.compose.ui:ui-test-manifest:1.1.1"
implementation "androidx.compose.ui:ui:1.4.0-alpha02"
implementation "androidx.compose.ui:ui-tooling-preview:1.4.0-alpha02"
implementation 'androidx.compose.material:material-icons-extended:1.4.0-alpha02'
implementation 'androidx.compose.material:material:1.4.0-alpha02'
implementation 'androidx.compose.runtime:runtime-livedata:1.3.1'
implementation 'androidx.compose.animation:animation:1.4.0-alpha02'
implementation 'androidx.compose.ui:ui-tooling:1.4.0-alpha02'
androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.4.0-alpha02'
debugImplementation "androidx.compose.ui:ui-test-manifest:1.3.1"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'
implementation 'com.google.android.material:material:1.7.0-alpha03'
implementation 'com.google.android.material:compose-theme-adapter:1.1.14'
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.0'
implementation "androidx.navigation:navigation-compose:2.5.0"
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
implementation "androidx.navigation:navigation-compose:2.5.3"
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.activity:activity-compose:1.5.0'
implementation 'androidx.activity:activity-compose:1.6.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
}

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.sockenklaus.batterytracker">
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"

View File

@@ -16,10 +16,10 @@ interface BatteryDao {
@Update
fun updateBattery(battery: Battery): Int
@Query("Select * FROM batteries ORDER BY name ASC")
@Query("Select * FROM batteries ORDER BY name COLLATE NOCASE ASC")
fun getBatteries(): Flow<List<Battery>>
@Query("Select * FROM batteries WHERE id = :id")
@Query("SELECT * FROM batteries WHERE id = :id")
fun getBatteryById(id: Int): Flow<Battery>
@Transaction

View File

@@ -25,4 +25,7 @@ interface ChargeDao {
@Transaction
@Query("SELECT * FROM batteries WHERE id = :id")
fun getBatteryAndCharges(id: Int): Flow<BatteryAndCharges>
@Query("SELECT * FROM charges WHERE battery_id = :id ORDER BY created_at DESC")
fun getChargesByBatteryId(id: Int): Flow<List<Charge>>
}

View File

@@ -1,9 +1,9 @@
package com.sockenklaus.batterytracker.ui
import android.annotation.SuppressLint
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
@@ -24,36 +24,44 @@ import com.sockenklaus.batterytracker.ui.models.MainViewModel
import kotlinx.coroutines.launch
object Routes {
const val HOME = "home"
const val ADD_BATTERY = "add_battery"
const val ADD_CHARGE = "add_charge"
const val BATTERY_DETAILS = "battery_details"
const val HOME = "Home"
const val ADD_BATTERY = "Add Battery"
const val ADD_CHARGE = "Add Charge"
const val BATTERY_DETAILS = "Battery Details"
}
@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
@Composable
fun BatteryTracker() {
MaterialTheme {
val state: MainViewModel = viewModel()
state.init()
/**
* TODO Is there a smarter way to work with Scaffold? Is it possible to change the
* components of the Scaffold from within the composables?
*/
Scaffold(
scaffoldState = state.scaffoldState,
topBar = {
when(state.currentScreen){
when {
state.currentScreen.startsWith(Routes.BATTERY_DETAILS) -> {
Routes.BATTERY_DETAILS -> DetailsTopAppBar(
navController = state.navController,
changeCurrentScreen = { state.currentScreen = it }
)
DetailsTopAppBar(
navController = state.navController,
drawerState = state.scaffoldState.drawerState,
appTitle = state.currentScreen
)
}
else -> MainTopAppBar(
drawerState = state.scaffoldState.drawerState,
appTitle = state.appTitle
appTitle = state.currentScreen,
)
}
}
) { innerPadding ->
) {
ModalDrawer(
drawerState = state.scaffoldState.drawerState,
drawerContent = {
@@ -63,38 +71,48 @@ fun BatteryTracker() {
route = Routes.HOME,
state = state,
)
NavListItem(
icon = Icons.Default.BatteryChargingFull,
textId = R.string.nav_add_charge,
route = Routes.ADD_CHARGE,
state = state,
)
NavListItem(
icon = Icons.Default.BatteryFull,
textId = R.string.nav_add_battery,
route = Routes.ADD_BATTERY,
state = state,
)
}
) {
NavHost(
navController = state.navController,
startDestination = Routes.HOME,
modifier = Modifier.padding(innerPadding))
)
{
composable(Routes.HOME) { Home(state.navController, state) }
composable(Routes.ADD_CHARGE) { AddCharge(state.navController) }
composable(Routes.ADD_BATTERY) { AddBattery(state.navController) }
composable(Routes.HOME) {
state.currentScreen = Routes.HOME
Home(state.navController)
}
composable(
route = Routes.ADD_CHARGE
){
AddCharge(
navController = state.navController
)
}
composable(
route = "${Routes.ADD_CHARGE}/{batteryId}",
arguments = listOf(navArgument("batteryId"){ type = NavType.IntType })
) {
val id = it.arguments?.getInt("batteryId")
state.currentScreen = "${Routes.ADD_CHARGE} for Battery ${state.getBatteryName(id = id)}"
AddCharge(
batteryId = id,
navController = state.navController
)
}
composable(Routes.ADD_BATTERY) {
state.currentScreen = Routes.ADD_BATTERY
AddBattery(state.navController)
}
composable(
route = "${Routes.BATTERY_DETAILS}/{batteryId}",
arguments = listOf(navArgument("batteryId"){ type = NavType.IntType })
) { navBackStackEntry ->
val id = navBackStackEntry.arguments?.getInt("batteryId")
state.currentScreen = Routes.BATTERY_DETAILS + ": " + state.getBatteryName(id = id)
BatteryDetails(
navController = state.navController,
batteryId = navBackStackEntry.arguments?.getInt("batteryId"),
appState = state
batteryId = id ,
navController = state.navController
)
}
}
@@ -162,9 +180,7 @@ fun NavListItem(
modifier = Modifier
.clickable {
scope.launch {
state.appTitle = text
state.navController.navigate(route)
state.currentScreen = route
state.scaffoldState.drawerState.close()
}
}

View File

@@ -1,6 +1,7 @@
package com.sockenklaus.batterytracker.ui.composables
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
@@ -10,11 +11,14 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.sockenklaus.batterytracker.R
import com.sockenklaus.batterytracker.ui.Routes
import com.sockenklaus.batterytracker.ui.composables.util.MyOutlinedTextFieldWithSuffix
import com.sockenklaus.batterytracker.ui.models.AddBatteryViewModel
import com.sockenklaus.batterytracker.util.validateDecimal
@@ -24,29 +28,55 @@ fun AddBattery(navController: NavController){
val model: AddBatteryViewModel = viewModel()
val batteries by model.batteries.observeAsState(emptyList())
val outerPadding = 24.dp
val outerPadding = 16.dp
val innerPadding = 16.dp
Column(
Modifier.padding(outerPadding)
) {
MyOutlinedTextFieldWithSuffix(
value = model.batteryName,
onValueChange = { value ->
model.batteryName = value
model.batteryHasError = false
model.batteryHelperId = R.string.helper_required
if(batteries.any{ it.name.equals(value, ignoreCase = true) }){
model.batteryHasError = true
model.batteryHelperId = R.string.helper_battery_not_unique
}
},
labelId = R.string.hint_enter_battery_name,
leadingIcon = { Icon(Icons.Default.Tag, "Icon Tag") },
isError = model.batteryHasError,
helperTextId = model.batteryHelperId
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
){
MyOutlinedTextFieldWithSuffix(
value = model.batteryName,
onValueChange = { value ->
model.batteryName = value
model.batteryHasError = false
model.batteryHelperId = R.string.helper_required
if(batteries.any{ it.name.equals(value, ignoreCase = true) }){
model.batteryHasError = true
model.batteryHelperId = R.string.helper_battery_not_unique
}
},
singleLine = true,
labelId = R.string.hint_enter_battery_name,
leadingIcon = { Icon(Icons.Default.Tag, "Icon Tag") },
isError = model.batteryHasError,
helperTextId = model.batteryHelperId,
keyboardOptions = KeyboardOptions(
capitalization = if(model.switchAutoCap) KeyboardCapitalization.Characters else KeyboardCapitalization.None,
autoCorrect = false,
keyboardType = KeyboardType.Ascii,
imeAction = ImeAction.Next
)
)
Column() {
Switch(
checked = model.switchAutoCap,
onCheckedChange = { model.switchAutoCap = !model.switchAutoCap }
)
Text(
text = "Auto-Cap.",
style = MaterialTheme.typography.caption
)
}
}
Spacer(Modifier.size(innerPadding))
@@ -56,24 +86,38 @@ fun AddBattery(navController: NavController){
model.declaredCapacity = validateDecimal(it, model.declaredCapacity)
},
labelId = R.string.hint_enter_declared_capacity,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Decimal,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = {
saveBattery(model = model, navController = navController)
}
),
leadingIcon = { Icon(Icons.Default.BatteryFull, "Icon Battery Full") },
suffix = "Ah"
suffix = "Ah",
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.size(outerPadding))
ExtendedFloatingActionButton(
onClick = {
if(model.batteryName.isBlank()) model.batteryHasError = true
if(!model.batteryHasError && model.saveBattery(model.batteryName, model.declaredCapacity)){
navController.navigate("home")
}
},
onClick = { saveBattery(model = model, navController = navController) },
icon = { Icon(Icons.Default.Save, "Icon Save") },
text = { Text(stringResource(R.string.button_save_battery)) },
modifier = Modifier.align(Alignment.End)
)
}
}
private fun saveBattery(
model: AddBatteryViewModel,
navController: NavController
){
if(model.batteryName.isBlank()) model.batteryHasError = true
if(!model.batteryHasError && model.saveBattery(model.batteryName, model.declaredCapacity)) {
navController.navigate(Routes.HOME)
}
}

View File

@@ -2,6 +2,7 @@ package com.sockenklaus.batterytracker.ui.composables
import android.app.DatePickerDialog
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
@@ -16,12 +17,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.sockenklaus.batterytracker.R
import com.sockenklaus.batterytracker.room.entities.Battery
import com.sockenklaus.batterytracker.ui.Routes
import com.sockenklaus.batterytracker.ui.composables.util.MyOutlinedTextFieldWithSuffix
import com.sockenklaus.batterytracker.ui.models.AddChargeViewModel
import com.sockenklaus.batterytracker.ui.theme.Gray500
@@ -33,14 +37,23 @@ import java.util.*
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun AddCharge(navController: NavController){
val outerPadding = 24.dp
fun AddCharge(
batteryId: Int? = null,
navController: NavController
){
val outerPadding = 16.dp
val innerPadding = 16.dp
var bIdExpanded by remember { mutableStateOf(false)}
val model: AddChargeViewModel = viewModel()
val batteries by model.batteries.observeAsState(emptyList())
if(batteryId != null && batteries.any { it.id == batteryId }) {
model.batteryName = TextFieldValue(
text = batteries.find { it.id == batteryId }!!.name
)
}
Column(
Modifier.padding(outerPadding)
) {
@@ -49,11 +62,12 @@ fun AddCharge(navController: NavController){
onExpandedChange = { bIdExpanded = !bIdExpanded}
) {
val filteringOptions = batteries.filter { it.name.contains(model.batteryId.text, ignoreCase = true)}
val filteringOptions = batteries.filter { it.name.contains(model.batteryName.text, ignoreCase = true)}
OutlinedTextField(
value = model.batteryId,
enabled = batteryId == null,
value = model.batteryName,
onValueChange = {
model.batteryId = it
model.batteryName = it
model.batteryHasError = false
model.batteryHelper = R.string.helper_required
bIdExpanded = filteringOptions.size > 1
@@ -65,7 +79,17 @@ fun AddCharge(navController: NavController){
},
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
modifier = Modifier.fillMaxWidth(),
isError = model.batteryHasError
isError = model.batteryHasError,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next
),
keyboardActions = KeyboardActions(
onNext = {
bIdExpanded = false
defaultKeyboardAction(ImeAction.Next)
}
),
singleLine = true
)
if(filteringOptions.isNotEmpty()) {
@@ -76,7 +100,7 @@ fun AddCharge(navController: NavController){
for (filteringOption in filteringOptions) {
DropdownMenuItem(
onClick = {
model.batteryId = TextFieldValue(
model.batteryName = TextFieldValue(
text = filteringOption.name,
selection = TextRange(filteringOption.name.length)
)
@@ -104,12 +128,21 @@ fun AddCharge(navController: NavController){
model.charge = validateDecimal(it, model.charge)
model.chargeHasError = false
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Decimal,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = {
saveCharge(batteries, model, navController)
}
),
labelId = R.string.hint_charge,
leadingIcon = { Icon(Icons.Default.BatteryChargingFull, "Icon Battery Charging Full") },
isError = model.chargeHasError,
helperTextId = R.string.helper_required,
suffix = "Ah"
suffix = "Ah",
modifier = Modifier.fillMaxWidth(),
)
Spacer(Modifier.size(innerPadding))
@@ -124,21 +157,18 @@ fun AddCharge(navController: NavController){
ExtendedFloatingActionButton(
text = { Text(stringResource(R.string.button_save_charge)) },
onClick = {
if(!batteries.any{ it.name == model.batteryId.text }){
if(!batteries.any{ it.name == model.batteryName.text }){
model.batteryHasError = true
model.batteryHelper = R.string.helper_battery_not_found
}
if(model.batteryId.text.isBlank()){
if(model.batteryName.text.isBlank()){
model.batteryHasError = true
model.batteryHelper = R.string.helper_required
}
if(model.charge.isBlank()){
model.chargeHasError = true
}
if(!model.batteryHasError && !model.chargeHasError && model.saveCharge(batteries, model.batteryId.text, model.charge, model.date)){
navController.navigate("home")
}
saveCharge(batteries, model, navController)
},
icon = { Icon(Icons.Default.Save, "Icon Save") },
modifier = Modifier.align(Alignment.End)
@@ -147,6 +177,31 @@ fun AddCharge(navController: NavController){
}
}
fun saveCharge(
batteries: List<Battery>,
model: AddChargeViewModel,
navController: NavController
) {
if(!batteries.any{ it.name == model.batteryName.text }){
model.batteryHasError = true
model.batteryHelper = R.string.helper_battery_not_found
}
if(model.batteryName.text.isBlank()){
model.batteryHasError = true
model.batteryHelper = R.string.helper_required
}
if(model.charge.isBlank()){
model.chargeHasError = true
}
if(!model.batteryHasError && !model.chargeHasError && model.saveCharge(batteries, model.batteryName.text, model.charge, model.date)){
val id = model.getBatId(
batteries,
model.batteryName.text
)
navController.navigate("${Routes.BATTERY_DETAILS}/${id}")
}
}
@Composable
fun ChargeDatePicker(

View File

@@ -1,77 +1,94 @@
package com.sockenklaus.batterytracker.ui.composables
import androidx.activity.OnBackPressedCallback
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.sockenklaus.batterytracker.R
import com.sockenklaus.batterytracker.room.entities.Battery
import com.sockenklaus.batterytracker.room.entities.Charge
import com.sockenklaus.batterytracker.ui.AppBarTitle
import com.sockenklaus.batterytracker.ui.Routes
import com.sockenklaus.batterytracker.ui.models.MainViewModel
import com.sockenklaus.batterytracker.ui.models.BatteryDetailsViewModel
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.*
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun BatteryDetails(
navController: NavController,
batteryId: Int? = null,
appState: MainViewModel
batteryId: Int?,
navController: NavController
){
var onBack = {
val backTarget = navController.previousBackStackEntry?.destination?.route ?: Routes.HOME
appState.currentScreen = backTarget
navController.navigate(backTarget)
val model: BatteryDetailsViewModel = viewModel()
val battery by model.battery.collectAsState(Battery(name = ""))
val charges: List<Charge> by model.charges.collectAsState(emptyList())
Box(
modifier = Modifier
.fillMaxSize()
){
val outputFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.GERMANY)
LazyColumn(
state = LazyListState()
) {
items(charges){ charge ->
ListItem(
text = { Text("Charge: " + charge.charge.toString() + " Ah") },
secondaryText = { Text("Date: " + charge.date.format(outputFormat)) }
)
Divider(Modifier.padding(horizontal = 16.dp))
}
}
ExtendedFloatingActionButton(
text = { Text(stringResource(R.string.add_charge)) },
onClick = {
navController.navigate("${Routes.ADD_CHARGE}/${battery.id}")
},
icon = { Icon(Icons.Default.Add, "Icon Add") },
modifier = Modifier.align(Alignment.BottomEnd)
.padding(16.dp)
)
}
BackPressHandler(onBackPressed = onBack)
Text(batteryId.toString())
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun DetailsTopAppBar(
navController: NavController,
changeCurrentScreen: (String) -> Unit
appTitle: String,
drawerState: DrawerState
){
TopAppBar(
title = { Text("Details") },
title = {
AppBarTitle(
drawerTarget = drawerState.targetValue,
appTitle = appTitle
)
},
navigationIcon = {
IconButton(
onClick = {
navController.navigate("home")
changeCurrentScreen(Routes.HOME)
navController.navigate(Routes.HOME)
}
) {
Icon(Icons.Default.ArrowBack, null)
}
}
},
)
}
@Composable
fun BackPressHandler(
backPressedDispatcher: OnBackPressedDispatcher? = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher,
onBackPressed: () -> Unit
) {
val currentOnBackPressed by rememberUpdatedState(newValue = onBackPressed)
val backCallback = remember {
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
currentOnBackPressed()
}
}
}
DisposableEffect(key1 = backPressedDispatcher) {
backPressedDispatcher?.addCallback(backCallback)
onDispose {
backCallback.remove()
}
}
}

View File

@@ -1,17 +1,22 @@
package com.sockenklaus.batterytracker.ui.composables
import android.telecom.Call
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
@@ -20,57 +25,80 @@ import com.sockenklaus.batterytracker.room.entities.Battery
import com.sockenklaus.batterytracker.ui.Routes
import com.sockenklaus.batterytracker.ui.composables.util.MyOutlinedTextFieldWithSuffix
import com.sockenklaus.batterytracker.ui.models.HomeViewModel
import com.sockenklaus.batterytracker.ui.models.MainViewModel
import java.lang.reflect.GenericDeclaration
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun Home(
navController: NavController,
appState: MainViewModel
navController: NavController
) {
val model: HomeViewModel = viewModel()
val batteries by model.batteries.observeAsState(emptyList<Battery>())
var filterText by remember { mutableStateOf("")}
val filteredList = batteries.filter { it.name.contains(filterText, ignoreCase = true) }
val modHorizontalPadding = Modifier.padding(horizontal = 16.dp)
Column {
MyOutlinedTextFieldWithSuffix(
value = filterText,
onValueChange = { filterText = it },
labelId = R.string.hint_filter_batteries,
modifier = Modifier.padding(
start = 16.dp,
end = 16.dp,
top = 16.dp
)
)
LazyColumn(
state = LazyListState()
Box(
modifier = Modifier.fillMaxSize()
){
Column(
modifier = Modifier
.padding(bottom = 16.dp)
.fillMaxHeight()
) {
items(filteredList){ battery ->
ListItem(
text = {
ListPrimaryText(battery)
},
secondaryText = {
ListSecondaryText(
min = model.getMinChargeById(battery.id),
avg = model.getAvgChargeById(battery.id),
max = model.getMaxChargeById(battery.id)
)
},
modifier = Modifier.clickable {
navController.navigate("${Routes.BATTERY_DETAILS}/${battery.id}")
appState.currentScreen = Routes.BATTERY_DETAILS
MyOutlinedTextFieldWithSuffix(
value = filterText,
onValueChange = { filterText = it },
labelId = R.string.hint_filter_batteries,
modifier = Modifier
.padding(
top = 16.dp,
start = 16.dp,
end = 16.dp
)
.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Search
),
keyboardActions = KeyboardActions(
onSearch = {
}
)
Divider(modHorizontalPadding)
)
LazyColumn(
state = LazyListState(),
) {
items(filteredList){ battery ->
ListItem(
text = {
ListPrimaryText(battery)
},
secondaryText = {
ListSecondaryText(
min = model.getMinChargeById(battery.id),
avg = model.getAvgChargeById(battery.id),
max = model.getMaxChargeById(battery.id)
)
},
modifier = Modifier.clickable {
navController.navigate("${Routes.BATTERY_DETAILS}/${battery.id}")
}
)
Divider(Modifier.padding(horizontal = 16.dp))
}
}
}
ExtendedFloatingActionButton(
text = { Text(stringResource(R.string.nav_add_battery)) },
icon = { Icon(Icons.Default.Add, contentDescription = "Add Battery") },
onClick = {
navController.navigate(Routes.ADD_BATTERY)
},
modifier = Modifier.padding(16.dp)
.align(Alignment.BottomEnd)
)
}
}

View File

@@ -21,90 +21,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.sockenklaus.batterytracker.ui.theme.Gray500
/*@Composable
fun TopAppBar(
title: @Composable () -> Unit,
modifier: Modifier = Modifier,
navigationIcon: @Composable (() -> Unit)? = null,
actions: @Composable RowScope.() -> Unit = {},
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor),
elevation: Dp = AppBarDefaults.TopAppBarElevation
) {
AppBar(
backgroundColor,
contentColor,
elevation,
AppBarDefaults.ContentPadding,
RectangleShape,
modifier
) {
if (navigationIcon == null) {
Spacer(Modifier.width(16.dp - 4.dp))
} else {
Row(Modifier.fillMaxHeight().width(72.dp - 4.dp), verticalAlignment = Alignment.CenterVertically) {
CompositionLocalProvider(
LocalContentAlpha provides ContentAlpha.high,
content = navigationIcon
)
}
}
Row(
Modifier.fillMaxHeight().weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
ProvideTextStyle(value = MaterialTheme.typography.h6) {
CompositionLocalProvider(
LocalContentAlpha provides ContentAlpha.high,
content = title
)
}
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Row(
Modifier.fillMaxHeight(),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
content = actions
)
}
}
}
@Composable
private fun AppBar(
backgroundColor: Color,
contentColor: Color,
elevation: Dp,
contentPadding: PaddingValues,
shape: Shape,
modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit
) {
Surface(
color = backgroundColor,
contentColor = contentColor,
elevation = elevation,
shape = shape,
modifier = modifier
) {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Row(
Modifier.fillMaxWidth()
.padding(contentPadding)
.height(56.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
content = content
)
}
}
}*/
@Composable
@OptIn(ExperimentalMaterialApi::class)
fun MyOutlinedTextFieldWithSuffix(
@@ -142,69 +58,72 @@ fun MyOutlinedTextFieldWithSuffix(
Gray500
}
BasicTextField(
value = value,
modifier = modifier
Column() {
BasicTextField(
value = value,
modifier = modifier
.padding(top = 8.dp)
.background(colors.backgroundColor(enabled).value, shape)
.defaultMinSize(
minWidth = TextFieldDefaults.MinWidth,
minHeight = TextFieldDefaults.MinHeight
),
onValueChange = onValueChange,
enabled = enabled,
readOnly = readOnly,
textStyle = mergedTextStyle,
cursorBrush = SolidColor(colors.cursorColor(isError).value),
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
interactionSource = interactionSource,
singleLine = singleLine,
maxLines = maxLines,
decorationBox = @Composable { innerTextField ->
OutlinedTextFieldDecorationBox(
value = value,
visualTransformation = visualTransformation,
innerTextField = {
Row {
val alignModifier = Modifier.alignByBaseline()
Box(alignModifier.weight(1f)){
innerTextField()
onValueChange = onValueChange,
enabled = enabled,
readOnly = readOnly,
textStyle = mergedTextStyle,
cursorBrush = SolidColor(colors.cursorColor(isError).value),
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
interactionSource = interactionSource,
singleLine = singleLine,
maxLines = maxLines,
decorationBox = @Composable { innerTextField ->
OutlinedTextFieldDecorationBox(
value = value,
visualTransformation = visualTransformation,
innerTextField = {
Row(
horizontalArrangement = Arrangement.SpaceBetween
) {
val alignModifier = Modifier.alignByBaseline()
Box(alignModifier){
innerTextField()
}
Text(
suffix,
alignModifier,
colors.trailingIconColor(enabled = enabled, isError = isError).value
)
}
Text(
suffix,
alignModifier,
colors.trailingIconColor(enabled = enabled, isError = isError).value
},
placeholder = placeholder,
label = { Text(stringResource(labelId)) },
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
singleLine = singleLine,
enabled = enabled,
isError = isError,
interactionSource = interactionSource,
colors = colors,
border = {
TextFieldDefaults.BorderBox(
enabled,
isError,
interactionSource,
colors,
shape
)
}
},
placeholder = placeholder,
label = { Text(stringResource(labelId)) },
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
singleLine = singleLine,
enabled = enabled,
isError = isError,
interactionSource = interactionSource,
colors = colors,
border = {
TextFieldDefaults.BorderBox(
enabled,
isError,
interactionSource,
colors,
shape
)
}
)
}
)
Text(
text = if(helperTextId != null) stringResource(helperTextId) else "",
style = MaterialTheme.typography.caption,
color = helperTextColor,
modifier = Modifier.padding(start = 16.dp)
)
)
}
)
Text(
text = if(helperTextId != null) stringResource(helperTextId) else "",
style = MaterialTheme.typography.caption,
color = helperTextColor,
modifier = Modifier.padding(start = 16.dp)
)
}
}

View File

@@ -22,6 +22,7 @@ class AddBatteryViewModel(application: Application): AndroidViewModel(applicatio
var batteryName by mutableStateOf("")
var batteryHasError by mutableStateOf(false)
var batteryHelperId by mutableStateOf(R.string.helper_required)
var switchAutoCap by mutableStateOf(true)
var declaredCapacity by mutableStateOf("")

View File

@@ -1,7 +1,6 @@
package com.sockenklaus.batterytracker.ui.models
import android.app.Application
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -23,7 +22,7 @@ class AddChargeViewModel(application: Application) : AndroidViewModel(applicatio
private val db = BatteryTrackerDB.getInstance(application)
var batteries: LiveData<List<Battery>> = db.batteryDao().getBatteries().asLiveData()
var batteryId by mutableStateOf(TextFieldValue(""))
var batteryName by mutableStateOf(TextFieldValue(""))
var date: LocalDate by mutableStateOf(LocalDate.now())
var charge by mutableStateOf("")
@@ -32,6 +31,14 @@ class AddChargeViewModel(application: Application) : AndroidViewModel(applicatio
var chargeHasError by mutableStateOf(false)
fun getBatId(
batteries: List<Battery>,
name: String
) : Int? {
return batteries.find { it.name == name }?.id
}
fun saveCharge(
batteryList: List<Battery>,
batteryName: String,

View File

@@ -0,0 +1,29 @@
package com.sockenklaus.batterytracker.ui.models
import android.app.Application
import androidx.lifecycle.*
import com.sockenklaus.batterytracker.room.BatteryTrackerDB
import com.sockenklaus.batterytracker.room.entities.Battery
import com.sockenklaus.batterytracker.room.entities.Charge
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class BatteryDetailsViewModel(
application: Application,
state: SavedStateHandle
) : AndroidViewModel(application) {
private val db = BatteryTrackerDB.getInstance(application)
val battery: Flow<Battery> = if(state.get<Int?>("batteryId") !== null) {
db.batteryDao().getBatteryById(state["batteryId"]!!)
} else {
MutableStateFlow(Battery(name=""))
}
val charges: Flow<List<Charge>> = if(state.get<Int?>("batteryId") !== null){
db.chargeDao().getChargesByBatteryId(state["batteryId"]!!)
} else {
MutableStateFlow(emptyList())
}
}

View File

@@ -21,7 +21,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
val batteryAndCharges = batteriesAndCharges?.find{ it.battery.id == batteryId }
if((batteryAndCharges != null) && batteryAndCharges.charges.isNotEmpty()){
return batteryAndCharges.charges.map { it.charge }.min()
return batteryAndCharges.charges.minOfOrNull { it.charge }
}
return null
}
@@ -32,7 +32,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
val batteryAndCharges = batteriesAndCharges?.find { it.battery.id == batteryId }
if((batteryAndCharges != null) && batteryAndCharges.charges.isNotEmpty()) {
return batteryAndCharges.charges.map { it.charge }.max()
return batteryAndCharges.charges.maxOfOrNull { it.charge }
}
return null
}

View File

@@ -2,17 +2,16 @@ package com.sockenklaus.batterytracker.ui.models
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.BlendMode.Companion.Screen
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.ViewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.sockenklaus.batterytracker.ui.AppBarTitle
import com.sockenklaus.batterytracker.room.BatteryTrackerDB
import com.sockenklaus.batterytracker.room.entities.Battery
import com.sockenklaus.batterytracker.ui.Routes
import com.sockenklaus.batterytracker.ui.ToggleDrawerButton
class MainViewModel : ViewModel() {
var appTitle by mutableStateOf("Home")
var currentScreen by mutableStateOf(Routes.HOME)
lateinit var navController: NavHostController
@@ -23,4 +22,20 @@ class MainViewModel : ViewModel() {
navController = rememberNavController()
scaffoldState = rememberScaffoldState()
}
@Composable
fun getBatteryName(
id: Int?
) : String {
val db = BatteryTrackerDB.getInstance(LocalContext.current)
val battery = id?.let { db.batteryDao().getBatteryById(it).collectAsState(initial = Battery(name="")) }
var name = ""
if (battery != null){
name = battery.value.name
}
return name
}
}

View File

@@ -11,7 +11,7 @@
<string name="nav_header_title">Navigation</string>
<string name="nav_home">Home</string>
<string name="nav_add_charge">Add Charge</string>
<string name="add_charge">Add Charge</string>
<string name="nav_add_battery">Add Battery</string>
<string name="battery_id">Battery ID</string>

View File

@@ -5,8 +5,8 @@ ext {
}
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.2.1' apply false
id 'com.android.library' version '7.2.1' apply false
id 'com.android.application' version '7.3.1' apply false
id 'com.android.library' version '7.3.1' apply false
id 'org.jetbrains.kotlin.android' version '1.7.0' apply false
id 'org.jetbrains.kotlin.kapt' version '1.7.0' apply false
}

View File

@@ -1,6 +1,6 @@
#Sun Jul 10 23:29:55 CEST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME