Implement improved task creation flow

This commit is contained in:
Phillip Thelen 2022-06-23 16:11:01 +02:00
parent 253210d0c2
commit cf2e46d199
14 changed files with 238 additions and 82 deletions

View file

@ -6,13 +6,22 @@ import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.activity.ComponentActivity
import androidx.core.view.children
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import androidx.wear.activity.ConfirmationActivity
import com.google.android.gms.tasks.Tasks
import com.google.android.gms.wearable.CapabilityClient
import com.google.android.gms.wearable.MessageClient
import com.google.android.gms.wearable.Wearable
import com.habitrpg.android.habitica.databinding.ActivityWrapperBinding
import com.habitrpg.wearos.habitica.ui.viewmodels.BaseViewModel
import com.habitrpg.wearos.habitica.ui.views.IndeterminateProgressView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
abstract class BaseActivity<B: ViewBinding, VM: BaseViewModel> : ComponentActivity() {
val messageClient: MessageClient by lazy { Wearable.getMessageClient(this) }
val capabilityClient: CapabilityClient by lazy { Wearable.getCapabilityClient(this) }
companion object {
var currentActivityClassName: String? = null
}
@ -64,4 +73,37 @@ abstract class BaseActivity<B: ViewBinding, VM: BaseViewModel> : ComponentActivi
}
}
}
internal fun openRemoteActivity(url: String) {
sendMessage("open_activity", url, null)
}
internal fun sendMessage(
permission: String,
url: String,
data: ByteArray?,
function: ((Boolean) -> Unit)? = null
) {
lifecycleScope.launch(Dispatchers.IO) {
val info = Tasks.await(
capabilityClient.getCapability(
permission,
CapabilityClient.FILTER_REACHABLE
)
)
val nodeID = info.nodes.firstOrNull { it.isNearby }
if (nodeID != null) {
function?.invoke(true)
Tasks.await(
messageClient.sendMessage(
nodeID.id,
url,
data
)
)
} else {
function?.invoke(false)
}
}
}
}

View file

@ -81,7 +81,7 @@ class LoginActivity: BaseActivity<ActivityLoginBinding, LoginViewModel>() {
}
private fun openRegisterOnPhone() {
openRemoteActivity("/show/register")
}
private fun loginLocal() {

View file

@ -4,9 +4,6 @@ import android.os.Bundle
import androidx.activity.viewModels
import androidx.core.view.children
import androidx.core.view.isVisible
import com.google.android.gms.wearable.CapabilityClient
import com.google.android.gms.wearable.MessageClient
import com.google.android.gms.wearable.Wearable
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.databinding.ActivityRyaBinding
import com.habitrpg.android.habitica.databinding.RowDailyBinding
@ -18,8 +15,6 @@ import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class RYAActivity : BaseActivity<ActivityRyaBinding, RYAViewModel>() {
val messageClient: MessageClient by lazy { Wearable.getMessageClient(this) }
val capabilityClient: CapabilityClient by lazy { Wearable.getCapabilityClient(this) }
override val viewModel: RYAViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
@ -40,7 +35,7 @@ class RYAActivity : BaseActivity<ActivityRyaBinding, RYAViewModel>() {
}
binding.phoneButton.setOnClickListener {
openRemoteActivity("/show/rya")
}
binding.startDayButton.setOnClickListener {

View file

@ -5,10 +5,6 @@ import android.os.Bundle
import androidx.activity.viewModels
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.tasks.Tasks
import com.google.android.gms.wearable.CapabilityClient
import com.google.android.gms.wearable.MessageClient
import com.google.android.gms.wearable.Wearable
import com.habitrpg.android.habitica.databinding.ActivitySplashBinding
import com.habitrpg.wearos.habitica.ui.viewmodels.SplashViewModel
import dagger.hilt.android.AndroidEntryPoint
@ -18,8 +14,6 @@ import kotlinx.coroutines.launch
@AndroidEntryPoint
class SplashActivity: BaseActivity<ActivitySplashBinding, SplashViewModel>() {
override val viewModel: SplashViewModel by viewModels()
private val messageClient: MessageClient by lazy { Wearable.getMessageClient(this) }
private val capabilityClient: CapabilityClient by lazy { Wearable.getCapabilityClient(this) }
override fun onCreate(savedInstanceState: Bundle?) {
binding = ActivitySplashBinding.inflate(layoutInflater)
@ -38,17 +32,14 @@ class SplashActivity: BaseActivity<ActivitySplashBinding, SplashViewModel>() {
}
messageClient.addListener(viewModel)
lifecycleScope.launch(Dispatchers.IO) {
val info = Tasks.await(capabilityClient.getCapability("provide_auth", CapabilityClient.FILTER_REACHABLE))
val nodeID = info.nodes.firstOrNull { it.isNearby }
if (nodeID != null) {
showAccountLoader(true)
Tasks.await(messageClient.sendMessage(nodeID.id, "/request/auth", null))
} else {
showAccountLoader(false)
startLoginActivity()
sendMessage("provide_auth", "/request/auth", null) {
if (it) {
showAccountLoader(true)
} else {
showAccountLoader(false)
startLoginActivity()
}
}
}
}
private fun startMainActivity() {

View file

@ -3,22 +3,13 @@ package com.habitrpg.wearos.habitica.ui.activities
import android.os.Bundle
import androidx.activity.viewModels
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.tasks.Tasks
import com.google.android.gms.wearable.CapabilityClient
import com.google.android.gms.wearable.MessageClient
import com.google.android.gms.wearable.Wearable
import com.habitrpg.android.habitica.databinding.ActivityTaskDetailBinding
import com.habitrpg.wearos.habitica.ui.viewmodels.TaskDetailViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.Locale
@AndroidEntryPoint
class TaskDetailActivity: BaseActivity<ActivityTaskDetailBinding, TaskDetailViewModel>() {
val messageClient: MessageClient by lazy { Wearable.getMessageClient(this) }
val capabilityClient: CapabilityClient by lazy { Wearable.getCapabilityClient(this) }
override val viewModel: TaskDetailViewModel by viewModels()
@ -34,13 +25,7 @@ class TaskDetailActivity: BaseActivity<ActivityTaskDetailBinding, TaskDetailView
}
private fun openEditFormOnPhone() {
lifecycleScope.launch(Dispatchers.IO) {
val info = Tasks.await(capabilityClient.getCapability("edit_task", CapabilityClient.FILTER_REACHABLE))
val nodeID = info.nodes.firstOrNull { it.isNearby }
if (nodeID != null) {
Tasks.await(messageClient.sendMessage(nodeID.id, "/tasks/edit", viewModel.taskID?.toByteArray()))
}
}
sendMessage("edit_task", "/tasks/edit", viewModel.taskID?.toByteArray())
}
private fun subscribeUI() {

View file

@ -1,68 +1,95 @@
package com.habitrpg.wearos.habitica.ui.activities
import android.app.AlertDialog
import android.content.Intent
import android.content.res.ColorStateList
import android.os.Bundle
import android.widget.ArrayAdapter
import android.view.inputmethod.EditorInfo
import android.widget.TextView
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.lifecycleScope
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.databinding.ActivityTaskFormBinding
import com.habitrpg.common.habitica.models.tasks.TaskType
import com.habitrpg.wearos.habitica.ui.viewmodels.TaskFormViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.launch
@AndroidEntryPoint
class TaskFormActivity: BaseActivity<ActivityTaskFormBinding, TaskFormViewModel>() {
class TaskFormActivity : BaseActivity<ActivityTaskFormBinding, TaskFormViewModel>() {
var taskType: TaskType? = null
set(value) {
field = value
binding.taskTypeButton.text = value?.value
}
set(value) {
field = value
updateTaskTypeButton(binding.todoButton, TaskType.TODO)
updateTaskTypeButton(binding.dailyButton, TaskType.DAILY)
updateTaskTypeButton(binding.habitButton, TaskType.HABIT)
binding.confirmationTitle.text = getString(R.string.create_task, value?.value)
}
override val viewModel: TaskFormViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
binding = ActivityTaskFormBinding.inflate(layoutInflater)
super.onCreate(savedInstanceState)
binding.editText.setOnEditorActionListener { _, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == EditorInfo.IME_ACTION_DONE) {
if (binding.editText.text?.isNotEmpty() == true) {
binding.editTaskWrapper.isVisible = false
binding.taskConfirmationWrapper.isVisible = true
binding.confirmationText.text = binding.editText.text
binding.editText.clearFocus()
}
}
false
}
binding.editButton.setOnClickListener {
binding.editTaskWrapper.isVisible = true
binding.taskConfirmationWrapper.isVisible = false
}
binding.todoButton.setOnClickListener { taskType = TaskType.TODO }
binding.dailyButton.setOnClickListener { taskType = TaskType.DAILY }
binding.habitButton.setOnClickListener { taskType = TaskType.HABIT }
binding.editText.doOnTextChanged { text, _, _, _ ->
binding.saveButton.isEnabled = text?.isNotBlank() == true
}
binding.taskTypeButton.setOnClickListener {
showTaskTypeSelector()
}
binding.saveButton.setOnClickListener {
binding.saveButton.isEnabled = false
lifecycleScope.launch {
lifecycleScope.launch(CoroutineExceptionHandler { _, _ ->
binding.saveButton.isEnabled = true
binding.editTaskWrapper.isVisible = true
binding.taskConfirmationWrapper.isVisible = false
}) {
viewModel.saveTask(binding.editText.text, taskType)
finish()
parent.startActivity(Intent(parent, TaskListActivity::class.java).apply {
putExtra("task_type", taskType?.value)
})
}
}
if (intent.extras?.containsKey("task_type") == true) {
taskType = TaskType.from(intent.getStringExtra("task_type"))
binding.taskTypeButton.isVisible = false
binding.taskTypeHeader.isVisible = false
binding.taskTypeWrapper.isVisible = false
binding.header.textView.text = getString(R.string.create_task, taskType?.value)
binding.confirmationTitle.text = getString(R.string.create_task, taskType?.value)
} else {
taskType = TaskType.TODO
binding.header.textView.text = getString(R.string.create_task_title)
}
}
private fun showTaskTypeSelector() {
val taskTypes = arrayOf(
TaskType.HABIT,
TaskType.DAILY,
TaskType.TODO,
TaskType.REWARD
)
val adapter = ArrayAdapter(this, R.layout.spinner_item, taskTypes)
val alert = AlertDialog.Builder(this).setAdapter(adapter) { _, which ->
taskType = taskTypes[which]
binding.taskTypeButton.text = taskType?.value
private fun updateTaskTypeButton(button: TextView, thisType: TaskType) {
if (taskType == thisType) {
button.backgroundTintList =
ColorStateList.valueOf(ContextCompat.getColor(this, R.color.watch_purple_10))
button.background.alpha = 100
button.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.radio_checked, 0)
} else {
button.backgroundTintList =
ColorStateList.valueOf(ContextCompat.getColor(this, R.color.watch_purple_5))
button.background.alpha = 255
button.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.radio_unchecked, 0)
}
alert.show()
}
}

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:width="10dp" android:height="10dp" android:gravity="center">
<shape android:shape="oval">
<solid android:color="@color/watch_purple_200" />
</shape>
</item>
<item android:width="18dp" android:height="18dp">
<shape android:shape="oval">
<stroke android:width="2dp" android:color="@color/watch_purple_200" />
<padding android:top="4dp" android:right="4dp" android:left="4dp" android:bottom="4dp" />
</shape>
</item>
</layer-list>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<size android:width="18dp" android:height="18dp" />
<stroke android:width="2dp" android:color="@color/gray_200" />
</shape>

View file

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#FFFFFF" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
</vector>

View file

@ -1,14 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">
<com.habitrpg.wearos.habitica.ui.views.HabiticaScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_boxedEdges="all">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/task_confirmation_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible"
android:gravity="center">
<TextView
android:id="@+id/confirmation_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Text.Subheader1"
android:textSize="14sp"
android:textColor="@color/gray_200"
/>
<TextView
android:id="@+id/confirmation_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Text.Body1"
android:textSize="14sp"
android:textColor="@color/white"
/>
<Button
android:id="@+id/save_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/ChipButton"
android:text="@string/save"
android:gravity="start|center_vertical"
android:layout_marginTop="20dp"
android:layout_marginBottom="4dp"
android:drawableStart="@drawable/save"/>
<Button
android:id="@+id/edit_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/ChipButton"
android:gravity="start|center_vertical"
android:text="@string/action_edit"
android:drawableStart="@drawable/edit"/>
</LinearLayout>
<LinearLayout
android:id="@+id/edit_task_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="38dp"
@ -21,20 +68,52 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAutofill="no"
android:minHeight="52dp"
android:paddingHorizontal="18dp"
android:hint="@string/task_title_hint"
android:background="@drawable/row_background_outline"
android:inputType="textCapWords" />
<Button
android:id="@+id/task_type_button"
<TextView
android:id="@+id/task_type_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Chip"
tools:text="Task Type"
android:textAllCaps="false"
android:textColor="@color/watch_purple_500"/>
android:gravity="center"
android:layout_marginTop="20dp"
android:layout_marginBottom="12dp"
android:textSize="14sp"
android:textColor="@color/watch_purple_200"
android:text="@string/task_type" />
<LinearLayout
android:id="@+id/task_type_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/todo_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Chip"
android:gravity="start|center_vertical"
android:paddingHorizontal="20dp"
android:text="@string/todo" />
<TextView
android:id="@+id/daily_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Chip"
android:gravity="start|center_vertical"
android:paddingHorizontal="20dp"
android:text="@string/daily" />
<TextView
android:id="@+id/habit_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Chip"
android:gravity="start|center_vertical"
android:paddingHorizontal="20dp"
android:text="@string/habit" />
</LinearLayout>
</LinearLayout>
</FrameLayout>
</com.habitrpg.wearos.habitica.ui.views.HabiticaScrollView>
<com.habitrpg.wearos.habitica.ui.views.AddTaskButton
android:id="@+id/save_button"
android:layout_width="match_parent"
android:layout_height="38dp"
android:layout_gravity="bottom" />
</FrameLayout>

View file

@ -48,11 +48,12 @@
<color name="watch_gray_100">#A5A1AC</color>
<color name="watch_gray_10">#878190</color>
<color name="watch_gray_5">#686274</color>
<color name="watch_gray_1">#34313A</color>
<color name="ic_launcher_background">@color/brand_300</color>
<color name="surface">#23202A</color>
<color name="surface">@color/watch_purple_5</color>
<color name="bar_background_color">#34313a</color>
<color name="bar_background_color">@color/gray_1</color>
<color name="exp_bar_color">@color/watch_yellow_100</color>
<color name="mp_bar_color">@color/watch_blue_100</color>
<color name="hp_bar_color">@color/watch_red_100</color>

View file

@ -24,4 +24,7 @@
<string name="checklist_disclaimer">Task checklists are available on the phone</string>
<string name="start_new_day">Start new day</string>
<string name="check_off_yesterday">Check off any you did yesterday:</string>
<string name="task_type">Task Type</string>
<string name="task_title_hint">Task title...</string>
<string name="save">Save</string>
</resources>

View file

@ -53,6 +53,10 @@
<item name="android:textSize">16sp</item>
</style>
<style name="Text.Body1">
<item name="android:fontFamily">sans-serif-medium</item>
<item name="android:textSize">14sp</item>
</style>
<style name="Text.Body2">
<item name="android:textSize">14sp</item>
</style>