Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion feature/task-details/impl/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
plugins {
id("todozy.android.library")
id("todozy.android.library.compose")
}

android {
Expand All @@ -26,6 +26,10 @@ dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.material)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.runtime.livedata)
implementation(libs.androidx.compose.material.icons.extended)

testImplementation(libs.junit4)
testImplementation(libs.mockk.core)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,24 @@ package br.com.sailboat.todozy.feature.task.details.impl.presentation
import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.TooltipCompat
import androidx.core.view.MenuProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import br.com.sailboat.todozy.domain.model.TaskMetrics
import br.com.sailboat.todozy.domain.model.TaskProgressRange
import br.com.sailboat.todozy.feature.navigation.android.TaskFormNavigator
import br.com.sailboat.todozy.feature.task.details.impl.databinding.FrgTaskDetailsBinding
import br.com.sailboat.todozy.feature.task.details.impl.presentation.viewmodel.TaskDetailsViewAction
import br.com.sailboat.todozy.feature.task.details.impl.presentation.viewmodel.TaskDetailsViewIntent
import br.com.sailboat.todozy.feature.task.details.impl.presentation.viewmodel.TaskDetailsViewIntent.OnClickConfirmDeleteTask
import br.com.sailboat.todozy.feature.task.details.impl.presentation.viewmodel.TaskDetailsViewModel
import br.com.sailboat.todozy.utility.android.fragment.hapticHandled
import br.com.sailboat.todozy.utility.android.view.gone
import br.com.sailboat.todozy.utility.android.view.setSafeClickListener
import br.com.sailboat.todozy.utility.android.view.visible
Expand All @@ -33,6 +29,7 @@ import br.com.sailboat.uicomponent.impl.dialog.twooptions.TwoOptionsDialog
import br.com.sailboat.uicomponent.impl.helper.getTaskId
import br.com.sailboat.uicomponent.impl.helper.putTaskId
import br.com.sailboat.uicomponent.impl.progress.TaskProgressHeaderAdapter
import br.com.sailboat.uicomponent.impl.theme.TodozyTheme
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import br.com.sailboat.todozy.feature.task.details.impl.R as TaskDetailsR
Expand Down Expand Up @@ -96,7 +93,6 @@ internal class TaskDetailsFragment : Fragment() {
}

private fun initViews() {
binding.toolbar.setTitle(UiR.string.task_details)
val fabLabel = getString(TaskDetailsR.string.fab_edit_task)
binding.fab.root.text = fabLabel
binding.fab.root.setIconResource(UiR.drawable.ic_edit_white_24dp)
Expand Down Expand Up @@ -124,12 +120,17 @@ internal class TaskDetailsFragment : Fragment() {
layoutManager = LinearLayoutManager(activity)
}

(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
binding.toolbar.setNavigationIcon(UiR.drawable.ic_arrow_back_white_24dp)
binding.toolbar.setNavigationOnClickListener {
requireActivity().onBackPressedDispatcher.onBackPressed()
binding.taskDetailsTopBar.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
binding.taskDetailsTopBar.setContent {
TodozyTheme {
val taskMetrics by viewModel.viewState.taskMetrics.observeAsState()
TaskDetailsTopBar(
taskMetrics = taskMetrics,
onNavigateBack = { requireActivity().onBackPressedDispatcher.onBackPressed() },
onClickDelete = { viewModel.dispatchViewIntent(TaskDetailsViewIntent.OnClickMenuDelete) },
)
}
}
addMenuProvider()
}

private fun observeViewModel() {
Expand All @@ -146,9 +147,6 @@ internal class TaskDetailsFragment : Fragment() {
viewModel.viewState.taskDetails.observe(viewLifecycleOwner) { items ->
taskDetailsAdapter?.submitList(items)
}
viewModel.viewState.taskMetrics.observe(viewLifecycleOwner) { taskMetrics ->
taskMetrics?.run { showMetrics(this) } ?: hideMetrics()
}
viewModel.viewState.taskProgressDays.observe(viewLifecycleOwner) { renderProgress() }
viewModel.viewState.taskProgressDayOrder.observe(viewLifecycleOwner) { renderProgress() }
viewModel.viewState.taskProgressRange.observe(viewLifecycleOwner) { renderProgress() }
Expand Down Expand Up @@ -195,52 +193,6 @@ internal class TaskDetailsFragment : Fragment() {
Toast.makeText(activity, UiR.string.msg_error, Toast.LENGTH_SHORT).show()
}

private fun addMenuProvider() {
requireActivity().addMenuProvider(
object : MenuProvider {
override fun onCreateMenu(
menu: Menu,
menuInflater: MenuInflater,
) {
menuInflater.inflate(TaskDetailsR.menu.menu_task_details, menu)
}

override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
TaskDetailsR.id.menu_delete -> {
return hapticHandled {
viewModel.dispatchViewIntent(TaskDetailsViewIntent.OnClickMenuDelete)
}
}
else -> false
}
}
},
viewLifecycleOwner,
)
}

private fun showMetrics(taskMetrics: TaskMetrics) {
binding.taskMetrics.tvMetricsDone.text = taskMetrics.doneTasks.toString()
binding.taskMetrics.tvMetricsNotDone.text = taskMetrics.notDoneTasks.toString()
binding.taskMetrics.tvMetricsFire.text = taskMetrics.consecutiveDone.toString()

if (taskMetrics.consecutiveDone == 0) {
binding.taskMetrics.taskMetricsLlFire.gone()
} else {
binding.taskMetrics.taskMetricsLlFire.visible()
}

binding.appbar.setExpanded(true, true)
binding.appbarTaskDetailsFlMetrics.visible()
binding.taskMetrics.root.visible()
}

private fun hideMetrics() {
binding.taskMetrics.root.gone()
binding.appbarTaskDetailsFlMetrics.gone()
}

private fun renderProgress() {
val progressDays = viewModel.viewState.taskProgressDays.value.orEmpty()
val range = viewModel.viewState.taskProgressRange.value ?: TaskProgressRange.LAST_YEAR
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package br.com.sailboat.todozy.feature.task.details.impl.presentation

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import br.com.sailboat.todozy.domain.model.TaskMetrics
import br.com.sailboat.uicomponent.impl.metrics.TaskMetricsRow
import br.com.sailboat.uicomponent.impl.theme.LocalTodozySpacing
import br.com.sailboat.uicomponent.impl.R as UiR

@Composable
internal fun TaskDetailsTopBar(
taskMetrics: TaskMetrics?,
onNavigateBack: () -> Unit,
onClickDelete: () -> Unit,
modifier: Modifier = Modifier,
) {
val spacing = LocalTodozySpacing.current
var isMenuExpanded by remember { mutableStateOf(false) }
Surface(
color = colorResource(id = UiR.color.md_blue_500),
elevation = 4.dp,
modifier = modifier,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.statusBarsPadding()
.padding(vertical = spacing.small),
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(onClick = onNavigateBack) {
Icon(
painter = painterResource(id = UiR.drawable.ic_arrow_back_white_24dp),
contentDescription = null,
tint = Color.White,
)
}
if (taskMetrics == null) {
Text(
text = stringResource(id = UiR.string.task_details),
style = MaterialTheme.typography.h6,
color = Color.White,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f),
)
} else {
TaskMetricsRow(
taskMetrics = taskMetrics,
modifier =
Modifier
.weight(1f)
.padding(horizontal = spacing.small),
)
}
Box {
IconButton(onClick = { isMenuExpanded = true }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = null,
tint = Color.White,
)
}
DropdownMenu(
expanded = isMenuExpanded,
onDismissRequest = { isMenuExpanded = false },
) {
DropdownMenuItem(
onClick = {
isMenuExpanded = false
onClickDelete()
},
) {
Text(text = stringResource(id = UiR.string.delete))
}
}
}
}
}
}
24 changes: 3 additions & 21 deletions feature/task-details/impl/src/main/res/layout/frg_task_details.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,11 @@
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
<androidx.compose.ui.platform.ComposeView
android:id="@+id/taskDetailsTopBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
app:popupTheme="@style/Theme.AppCompat.Light"
app:titleTextAppearance="@style/ToolbarTitle">

<FrameLayout
android:id="@+id/appbar_task_details__fl__metrics"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginTop="10dp"
android:visibility="gone">

<include
android:id="@+id/task_metrics"
layout="@layout/task_metrics" />

</FrameLayout>
</androidx.appcompat.widget.Toolbar>
android:minHeight="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>

<androidx.recyclerview.widget.RecyclerView
Expand Down
1 change: 1 addition & 0 deletions feature/task-details/impl/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fab_edit_task">Edit Task</string>
<string name="metrics_title">Metrics</string>
</resources>
1 change: 1 addition & 0 deletions feature/task-list/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.lifecycle.livedata)
implementation(libs.androidx.compose.lifecycle.runtime.compose)
implementation(libs.koin.android)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.recyclerview)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import br.com.sailboat.todozy.domain.model.TaskProgressRange
import br.com.sailboat.todozy.feature.navigation.android.AboutNavigator
import br.com.sailboat.todozy.feature.navigation.android.HomeDestination
Expand All @@ -25,6 +28,7 @@ import br.com.sailboat.todozy.feature.task.list.impl.presentation.viewmodel.Task
import br.com.sailboat.todozy.feature.task.list.impl.presentation.viewmodel.TaskListViewIntent
import br.com.sailboat.todozy.feature.task.list.impl.presentation.viewmodel.TaskListViewModel
import br.com.sailboat.uicomponent.impl.helper.NotificationHelper
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import br.com.sailboat.uicomponent.impl.R as UiR
Expand Down Expand Up @@ -56,14 +60,14 @@ internal class TaskListFragment : Fragment() {
)
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
val tasksLoading by viewModel.viewState.tasksLoading.observeAsState(false)
val items by viewModel.viewState.itemsView.observeAsState(mutableListOf())
val taskMetrics by viewModel.viewState.taskMetrics.observeAsState()
val taskProgressDays by viewModel.viewState.taskProgressDays.observeAsState(emptyList())
val taskProgressRange by viewModel.viewState.taskProgressRange.observeAsState(
val tasksLoading by viewModel.viewState.tasksLoading.collectAsStateWithLifecycle(false)
val items by viewModel.viewState.itemsView.collectAsStateWithLifecycle(emptyList())
val taskMetrics by viewModel.viewState.taskMetrics.collectAsStateWithLifecycle()
val taskProgressDays by viewModel.viewState.taskProgressDays.collectAsStateWithLifecycle(emptyList())
val taskProgressRange by viewModel.viewState.taskProgressRange.collectAsStateWithLifecycle(
TaskProgressRange.LAST_YEAR,
)
val taskProgressLoading by viewModel.viewState.taskProgressLoading.observeAsState(false)
val taskProgressLoading by viewModel.viewState.taskProgressLoading.collectAsStateWithLifecycle(false)
val haptics = LocalHapticFeedback.current

TaskListScreen(
Expand Down Expand Up @@ -129,16 +133,21 @@ internal class TaskListFragment : Fragment() {
}

private fun observeActions() {
viewModel.viewState.viewAction.observe(viewLifecycleOwner) { viewAction ->
when (viewAction) {
is TaskListViewAction.CloseNotifications -> closeNotifications()
is TaskListViewAction.NavigateToAbout -> navigateToAbout()
is TaskListViewAction.NavigateToHistory -> navigateToHistory()
is TaskListViewAction.NavigateToSettings -> navigateToSettings()
is TaskListViewAction.NavigateToTaskForm -> navigateToTaskForm()
is TaskListViewAction.NavigateToTaskDetails -> navigateToTaskDetails(viewAction.taskId)
is TaskListViewAction.ShowErrorCompletingTask -> showErrorCompletingTask()
is TaskListViewAction.ShowErrorLoadingTasks -> showErrorLoadingTasks()
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.viewState.viewAction.collect { viewAction ->
when (viewAction) {
is TaskListViewAction.CloseNotifications -> closeNotifications()
is TaskListViewAction.NavigateToAbout -> navigateToAbout()
is TaskListViewAction.NavigateToHistory -> navigateToHistory()
is TaskListViewAction.NavigateToSettings -> navigateToSettings()
is TaskListViewAction.NavigateToTaskForm -> navigateToTaskForm()
is TaskListViewAction.NavigateToTaskDetails -> navigateToTaskDetails(viewAction.taskId)
is TaskListViewAction.ShowErrorCompletingTask -> showErrorCompletingTask()
is TaskListViewAction.ShowErrorLoadingTasks -> showErrorLoadingTasks()
}
viewModel.viewState.viewAction.resetReplayCache()
}
}
}
}
Expand Down
Loading