Skip to content

Commit

Permalink
[Feature] Add a simple date chooser.
Browse files Browse the repository at this point in the history
[UI] Tab switch.
[Code] Other optimization and features.
  • Loading branch information
Konyaco committed Feb 21, 2022
1 parent 58936bd commit bd0b0fc
Show file tree
Hide file tree
Showing 11 changed files with 201 additions and 74 deletions.
1 change: 1 addition & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- [ ] 标签的颜色
- [ ] 记录顶部的统计条
- [ ] 修改记录数据
- 筛查页面
- 统计页面
- [ ] 环形统计图
- 设置页面
Expand Down
7 changes: 4 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ kapt {
}

dependencies {
testImplementation("org.junit.jupiter:junit-jupiter")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")

val hiltVersion = "2.40.5"
Expand All @@ -68,7 +69,7 @@ dependencies {

implementation("androidx.core:core-ktx:1.7.0")

implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.1")

implementation("androidx.activity:activity-compose:1.4.0")

Expand All @@ -79,11 +80,11 @@ dependencies {
implementation("androidx.compose.animation:animation:$composeVersion")
implementation("androidx.compose.material:material:$composeVersion")
implementation("androidx.compose.material:material-icons-extended:$composeVersion")
implementation("androidx.compose.material3:material3:1.0.0-alpha04")
implementation("androidx.compose.material3:material3:1.0.0-alpha05")
androidTestImplementation("androidx.compose.ui:ui-test-junit4:$composeVersion")
debugImplementation("androidx.compose.ui:ui-tooling:$composeVersion")

val accompanistVersion = "0.22.1-rc"
val accompanistVersion = "0.23.0"
implementation("com.google.accompanist:accompanist-pager:$accompanistVersion")
implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package me.konyaco.keeptally.database

internal class AppDatabaseTest
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ class MainViewModel @Inject constructor(
)

data class Date(
val timestamp: Long,
val dateString: String,
val daysOffset: Int
)
Expand Down Expand Up @@ -135,15 +134,15 @@ class MainViewModel @Inject constructor(
}
}
statistics.emit(Statistics(expenditure, income, 0)) // TODO: Budget
val dailyRecords = separateToDays(result)
val dailyRecords = groupToDailyRecord(result)
records.emit(dailyRecords)
}

private suspend fun EntityRecord.mapToRecord(): Record {
val instant = Instant.ofEpochSecond(this.timestamp)
val zoned = instant.atZone(localZoneId)
val time = hhmFormatter.format(zoned)
val date = Date(this.timestamp, dateFormatter.format(zoned), calculateDayOffset(zoned))
val date = Date(dateFormatter.format(zoned), calculateDayOffset(zoned))
val label = recordTypeDao.loadAllByIds(this.typeId).first().mapToRecordType()

return Record(
Expand Down Expand Up @@ -253,9 +252,9 @@ class MainViewModel @Inject constructor(
}
}

private fun separateToDays(records: List<Record>): List<DailyRecord> {
private fun groupToDailyRecord(records: List<Record>): List<DailyRecord> {
return records.groupBy {
it.date
it.date.dateString
}.map { (date, records) ->
var expenditure = 0
var income = 0
Expand All @@ -266,7 +265,7 @@ class MainViewModel @Inject constructor(
income += it.money
}
}
DailyRecord(date = date, expenditure = expenditure, income = income, records = records)
DailyRecord(date = records.first().date, expenditure = expenditure, income = income, records = records)
}
}

Expand Down
65 changes: 50 additions & 15 deletions app/src/main/kotlin/me/konyaco/keeptally/ui/App.kt
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
package me.konyaco.keeptally.ui

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.*
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import kotlinx.coroutines.launch
import me.konyaco.keeptally.component.MainViewModel
import me.konyaco.keeptally.ui.component.HomeTopBar
import me.konyaco.keeptally.ui.component.HomeTopBarState
import me.konyaco.keeptally.ui.detail.AddRecord
import me.konyaco.keeptally.ui.detail.DetailScreen
import me.konyaco.keeptally.ui.theme.AndroidKeepTallyTheme

@OptIn(ExperimentalMaterialApi::class)
@OptIn(ExperimentalMaterialApi::class, ExperimentalPagerApi::class)
@Composable
fun App(viewModel: MainViewModel) {
AndroidKeepTallyTheme {
Expand All @@ -46,16 +48,49 @@ fun App(viewModel: MainViewModel) {
sheetShape = RectangleShape
) {
Column(Modifier.fillMaxSize()) {
HomeTopBar(Modifier.fillMaxWidth())
DetailScreen(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
onAddClick = { scope.launch { sheet.show() } },
viewModel = viewModel
)
val homeTopBarState = remember {
HomeTopBarState { year, month ->
viewModel.setDateRange(MainViewModel.DateRange.Month(year, month))
}
}
val pagerState = rememberPagerState()

LaunchedEffect(homeTopBarState.selectedTab.value) {
val index = HomeTopBarState.TabItem.values()
.indexOf(homeTopBarState.selectedTab.value)
pagerState.animateScrollToPage(index)
}

LaunchedEffect(pagerState.currentPage) {
val tab = HomeTopBarState.TabItem.values()[pagerState.currentPage]
homeTopBarState.selectTab(tab)
}

HomeTopBar(Modifier.fillMaxWidth(), homeTopBarState)
HorizontalPager(
count = remember { HomeTopBarState.TabItem.values().size },
state = pagerState
) {
when (it) {
0 -> DetailScreen(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
onAddClick = { scope.launch { sheet.show() } },
viewModel = viewModel
)
1, 2, 3 -> TodoScreen()
}
}
}
}
}
}
}

@Composable
private fun TodoScreen() {
Box(Modifier.fillMaxSize(), Alignment.Center) {
Text("TODO")
}
}
144 changes: 104 additions & 40 deletions app/src/main/kotlin/me/konyaco/keeptally/ui/component/HomeTopBar.kt
Original file line number Diff line number Diff line change
@@ -1,79 +1,143 @@
package me.konyaco.keeptally.ui.component

import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material.Divider
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.sharp.*
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import me.konyaco.keeptally.R
import me.konyaco.keeptally.ui.theme.KeepTallyTheme
import java.time.LocalDate

class HomeTopBarState(val onDateChosen: (year: Int, month: Int) -> Unit) {
val dateChooser = DateChooserState(onDateChosen)
val selectedTab = mutableStateOf<TabItem>(TabItem.Detail)

enum class TabItem(@StringRes val labelRes: Int, val icon: ImageVector) {
Detail(R.string.detail, Icons.Sharp.Article),
Filter(R.string.filter, Icons.Sharp.FilterAlt),
Statistics(R.string.statistics, Icons.Sharp.Leaderboard),
Other(R.string.other, Icons.Sharp.Widgets);

val label: String
@Composable get() = stringResource(id = labelRes)
}

fun selectTab(tabItem: TabItem) {
selectedTab.value = tabItem
}
}

@Composable
fun HomeTopBar(modifier: Modifier = Modifier) {
fun HomeTopBar(modifier: Modifier = Modifier, state: HomeTopBarState) {
Row(
modifier = modifier.height(64.dp),
verticalAlignment = Alignment.CenterVertically
) {
DateChooser(state.dateChooser)
Divider(Modifier.size(1.dp, 36.dp))
Row(
modifier = Modifier
.clip(RectangleShape)
.weight(1f)
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
HomeTopBarState.TabItem.values().forEach {
CustomTab(
selected = state.selectedTab.value == it,
text = it.label,
icon = it.icon,
onClick = { state.selectTab(it) }
)
}
}
}
}

class DateChooserState(val onChosen: (year: Int, month: Int) -> Unit) {
private val now = LocalDate.now()
private fun calcAvailableDate(): List<Triple<Int, Int, String>> {
val availableDate = mutableListOf<Triple<Int, Int, String>>()

for (y in now.year downTo now.year - 2) {
val maxMonth = if (y == now.year) now.monthValue else 12
for (m in maxMonth downTo 1) {
availableDate.add(Triple(y, m, buildDateString(y, m)))
}
}

return availableDate
}

private fun buildDateString(year: Int, month: Int): String = when {
year == now.year && month == now.monthValue -> "本月"
year == now.year -> "$month"
else -> "$year$month"
}

var showDateChooser by mutableStateOf(false)
var currentDate by mutableStateOf<Triple<Int, Int, String>>(
Triple(
now.year,
now.monthValue,
buildDateString(now.year, now.monthValue)
)
)

var availableDate = calcAvailableDate()

fun selectDate(year: Int, month: Int) {
currentDate = Triple(year, month, buildDateString(year, month))
onChosen(year, month)
}
}

@Composable
private fun DateChooser(state: DateChooserState) {
Box {
Row(
Modifier
.fillMaxHeight()
.clickable(remember { MutableInteractionSource() }, null) {
/* TODO */
state.showDateChooser = true
}
.padding(start = 16.dp, end = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "本月", style = MaterialTheme.typography.titleLarge)
Text(text = state.currentDate.third, style = MaterialTheme.typography.titleLarge)
Spacer(Modifier.width(4.dp))
Icon(Icons.Sharp.ArrowDropDown, contentDescription = "Dropdown")
}
Divider(Modifier.size(1.dp, 36.dp))
var selected by remember { mutableStateOf(0) }
Row(
modifier = Modifier
.clip(RectangleShape)
.weight(1f)
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
DropdownMenu(
expanded = state.showDateChooser, onDismissRequest = { state.showDateChooser = false },
offset = DpOffset(16.dp, 0.dp)
) {
CustomTab(
selected = selected == 0,
text = "明细",
icon = Icons.Sharp.Article,
onClick = { selected = 0 }
)
CustomTab(
selected = selected == 1,
text = "筛查",
icon = Icons.Sharp.FilterAlt,
onClick = { selected = 1 }
)
CustomTab(
selected = selected == 2,
text = "统计",
icon = Icons.Sharp.Leaderboard,
onClick = { selected = 2 }
)
CustomTab(
selected = selected == 3,
text = "杂项",
icon = Icons.Sharp.Widgets,
onClick = { selected = 3 }
)
state.availableDate.forEach { (year, month, str) ->
DropdownMenuItem(
text = { Text(str) },
onClick = {
state.selectDate(year, month)
state.showDateChooser = false
}
)
}
}
}
}
Expand Down Expand Up @@ -124,6 +188,6 @@ private fun CustomTabPreview() {
@Composable
private fun HomeTopBarPreview() {
KeepTallyTheme {
HomeTopBar(Modifier.fillMaxWidth())
HomeTopBar(Modifier.fillMaxWidth(), remember { HomeTopBarState { _, _ -> } })
}
}
Loading

0 comments on commit bd0b0fc

Please sign in to comment.