Compare commits
3 commits
01f1b003c9
...
354e00487a
Author | SHA1 | Date | |
---|---|---|---|
Peter Vacho | 354e00487a | ||
Peter Vacho | 5ba14f2aba | ||
Peter Vacho | bf3b296136 |
|
@ -16,6 +16,9 @@
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.NeatCalendar"
|
android:theme="@style/Theme.NeatCalendar"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
|
<activity
|
||||||
|
android:name=".activities.CreateCategoryActivity"
|
||||||
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.CategoriesActivity"
|
android:name=".activities.CategoriesActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
|
@ -1,21 +1,133 @@
|
||||||
package com.p_vacho.neat_calendar.activities
|
package com.p_vacho.neat_calendar.activities
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.p_vacho.neat_calendar.MyApplication
|
||||||
import com.p_vacho.neat_calendar.R
|
import com.p_vacho.neat_calendar.R
|
||||||
|
import com.p_vacho.neat_calendar.adapters.CategoryAdapter
|
||||||
|
import com.p_vacho.neat_calendar.api.RetrofitClient
|
||||||
|
import com.p_vacho.neat_calendar.api.models.CategoryResponse
|
||||||
|
import com.p_vacho.neat_calendar.api.models.EventResponse
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class CategoriesActivity : AppCompatActivity() {
|
class CategoriesActivity : AppCompatActivity() {
|
||||||
|
private lateinit var rvCategories: RecyclerView
|
||||||
|
private lateinit var btnBack: ImageButton
|
||||||
|
private lateinit var btnAddCategory: ImageButton
|
||||||
|
private lateinit var tvEmptyState: TextView
|
||||||
|
|
||||||
|
private lateinit var categories: MutableList<CategoryResponse>
|
||||||
|
|
||||||
|
private val createActivityLauncher = registerForActivityResult(
|
||||||
|
androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode == RESULT_OK) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val newCategory: CategoryResponse? = result.data?.getParcelableExtra("newCategory")
|
||||||
|
|
||||||
|
newCategory?.let { categoryCreateReply(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContentView(R.layout.activity_categories)
|
setContentView(R.layout.activity_categories)
|
||||||
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
||||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||||
insets
|
insets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rvCategories = findViewById(R.id.rvCategories)
|
||||||
|
btnBack = findViewById(R.id.btnBack)
|
||||||
|
btnAddCategory = findViewById(R.id.btnAddCategory)
|
||||||
|
tvEmptyState = findViewById(R.id.tvEmptyState)
|
||||||
|
|
||||||
|
btnBack.setOnClickListener { finish() }
|
||||||
|
btnAddCategory.setOnClickListener { navigateToCreateCategory() }
|
||||||
|
|
||||||
|
rvCategories.layoutManager = LinearLayoutManager(this)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
categories = fetchCategories().toMutableList()
|
||||||
|
|
||||||
|
val adapter = CategoryAdapter(categories, ::handleDeleteCategory)
|
||||||
|
|
||||||
|
rvCategories.adapter = adapter
|
||||||
|
updateEmptyState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all categories for the current user from the backend.
|
||||||
|
*/
|
||||||
|
private suspend fun fetchCategories(): List<CategoryResponse> {
|
||||||
|
val userId = (application as MyApplication).tokenManager.userId
|
||||||
|
?: run {
|
||||||
|
finish()
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
RetrofitClient.categoryService.userCategories(userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback function triggered by the Category Adapter, when the user
|
||||||
|
* clicks on the delete button.
|
||||||
|
*/
|
||||||
|
private fun handleDeleteCategory(category: CategoryResponse, position: Int) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
RetrofitClient.categoryService.deleteCategory(category.id)
|
||||||
|
}
|
||||||
|
// Remove category from the adapter and update the UI
|
||||||
|
(rvCategories.adapter as CategoryAdapter).removeCategoryAt(position)
|
||||||
|
Toast.makeText(this@CategoriesActivity, "Category deleted", Toast.LENGTH_SHORT).show()
|
||||||
|
updateEmptyState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates to the activity for adding a new category.
|
||||||
|
*/
|
||||||
|
private fun navigateToCreateCategory() {
|
||||||
|
val intent = Intent(this, CreateCategoryActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used as a callback, triggered when the CreateCategory Activity returns a result.
|
||||||
|
*
|
||||||
|
* The returned value (the new category data) is passed over as a parameter.
|
||||||
|
*/
|
||||||
|
private fun categoryCreateReply(category: CategoryResponse) {
|
||||||
|
(rvCategories.adapter as CategoryAdapter).addCategory(category)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a helper function to toggle the visibility of the empty state message
|
||||||
|
* when no categories are available.
|
||||||
|
*
|
||||||
|
* This should be called whenever a category was removed or added.
|
||||||
|
*/
|
||||||
|
private fun updateEmptyState() {
|
||||||
|
tvEmptyState.visibility = if (categories.isEmpty()) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.p_vacho.neat_calendar.activities
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import com.p_vacho.neat_calendar.R
|
||||||
|
|
||||||
|
class CreateCategoryActivity : AppCompatActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
setContentView(R.layout.activity_create_category)
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
||||||
|
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -127,6 +127,10 @@ class NotificationsActivity : AppCompatActivity() {
|
||||||
return fetchedEvents.associateBy { it.id }
|
return fetchedEvents.associateBy { it.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch both the incoming & outgoing (owned) invitations for the currently
|
||||||
|
* logged in user.
|
||||||
|
*/
|
||||||
private suspend fun fetchInvitations(): Map<String, InvitationResponse> {
|
private suspend fun fetchInvitations(): Map<String, InvitationResponse> {
|
||||||
val userId = (application as MyApplication).tokenManager.userId
|
val userId = (application as MyApplication).tokenManager.userId
|
||||||
if (userId == null) {
|
if (userId == null) {
|
||||||
|
@ -146,6 +150,13 @@ class NotificationsActivity : AppCompatActivity() {
|
||||||
return fetchedInvitations.associateBy { it.id }
|
return fetchedInvitations.associateBy { it.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback function passed to the Notification Adapter, to allow it to request
|
||||||
|
* a specific invitation by ID.
|
||||||
|
*
|
||||||
|
* This function only obtains the invitation from the pre-fetched invitations list,
|
||||||
|
* it will not make any new requests. Unknown IDs will result in null.
|
||||||
|
*/
|
||||||
private fun getInvitationData(invitationId: String, rvPosition: Int): InvitationResponse? {
|
private fun getInvitationData(invitationId: String, rvPosition: Int): InvitationResponse? {
|
||||||
val ret = invitations[invitationId]
|
val ret = invitations[invitationId]
|
||||||
if (ret == null) {
|
if (ret == null) {
|
||||||
|
@ -155,6 +166,13 @@ class NotificationsActivity : AppCompatActivity() {
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback function passed to the Notification Adapter, to allow it to request
|
||||||
|
* a specific event by ID.
|
||||||
|
*
|
||||||
|
* This function only obtains the events from the pre-fetched events list,
|
||||||
|
* it will not make any new requests. Unknown IDs will result in null.
|
||||||
|
*/
|
||||||
private fun getEventData(eventId: String, rvPosition: Int): EventResponse? {
|
private fun getEventData(eventId: String, rvPosition: Int): EventResponse? {
|
||||||
val ret = events[eventId]
|
val ret = events[eventId]
|
||||||
if (ret == null) {
|
if (ret == null) {
|
||||||
|
@ -164,6 +182,21 @@ class NotificationsActivity : AppCompatActivity() {
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback function passed to the Notification Adapter, to allow it to request
|
||||||
|
* a specific user by ID.
|
||||||
|
*
|
||||||
|
* This function obtains the users lazily, making a new API request whenever it
|
||||||
|
* encounters an unknown ID. After obtaining the user data, it will be cached and
|
||||||
|
* another request for the same user will not be made.
|
||||||
|
*
|
||||||
|
* Since the API call needs to be async, instead of making this function block, it
|
||||||
|
* will immediately return null at first, while the leaving the coroutine for the
|
||||||
|
* API fetching runs in the background. Once this request finished, if successful,
|
||||||
|
* the adapter will be notified about a change of this item, which will make it
|
||||||
|
* call this function again. This time though, it will return immediately from cache,
|
||||||
|
* without any making further API calls.
|
||||||
|
*/
|
||||||
private fun getUserData(userId: String, rvPosition: Int?): UserResponse? {
|
private fun getUserData(userId: String, rvPosition: Int?): UserResponse? {
|
||||||
return users.getOrPut(userId) {
|
return users.getOrPut(userId) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
@ -198,6 +231,15 @@ class NotificationsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback function passed to the Notification Adapter, triggered by clicking on one of the
|
||||||
|
* action buttons of the notification. (Currently, the only actions notifications support are
|
||||||
|
* invite related).
|
||||||
|
*
|
||||||
|
* The notification adapter should only call this function if the action is performable, so
|
||||||
|
* we shouldn't need to perform any additional checks for whether the requested action makes
|
||||||
|
* sense for given notification.
|
||||||
|
*/
|
||||||
private fun handleNotificationAction(notification: NotificationResponse, action: NotificationAdapter.Action, position: Int) {
|
private fun handleNotificationAction(notification: NotificationResponse, action: NotificationAdapter.Action, position: Int) {
|
||||||
when (action) {
|
when (action) {
|
||||||
NotificationAdapter.Action.ACCEPT -> {
|
NotificationAdapter.Action.ACCEPT -> {
|
||||||
|
@ -247,6 +289,12 @@ class NotificationsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback function passed to the Notification Adapter, triggered by clicking on the notification
|
||||||
|
* itself. This should mark the notification as read.
|
||||||
|
*
|
||||||
|
* The adapter should only call this function if the notification isn't already marked as read.
|
||||||
|
*/
|
||||||
private fun handleNotificationClick(notification: NotificationResponse, position: Int, sendToast: Boolean = true) {
|
private fun handleNotificationClick(notification: NotificationResponse, position: Int, sendToast: Boolean = true) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val updatedNotification =
|
val updatedNotification =
|
||||||
|
@ -262,6 +310,11 @@ class NotificationsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach an ItemTouchHelper to the recycler view adapter, to allow swiping
|
||||||
|
* of it's items. We only enable swiping to the right, which will trigger
|
||||||
|
* a deletion of that notification.
|
||||||
|
*/
|
||||||
private fun setupSwipeToDelete(adapter: NotificationAdapter) {
|
private fun setupSwipeToDelete(adapter: NotificationAdapter) {
|
||||||
val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) {
|
val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) {
|
||||||
override fun onMove(
|
override fun onMove(
|
||||||
|
@ -280,15 +333,9 @@ class NotificationsActivity : AppCompatActivity() {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
RetrofitClient.notificationsService.deleteNotification(notification.id)
|
RetrofitClient.notificationsService.deleteNotification(notification.id)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
// Remove the notification & notify the adapter about it
|
// This both notifies the adapter & removes the notification from the
|
||||||
notifications.removeAt(position)
|
// notifications list
|
||||||
adapter.notifyItemRemoved(position)
|
(rvNotifications.adapter as NotificationAdapter).removeNotificationAt(position)
|
||||||
|
|
||||||
// Annoyingly, we can't just use notifyItemRemoved for the single removed item,
|
|
||||||
// as all the items below it would now be using the wrong position that was
|
|
||||||
// already bounded to the callbacks from the click listeners, so we need to refresh
|
|
||||||
// all of the notifications below this one as well.
|
|
||||||
adapter.notifyItemRangeChanged(position, notifications.size - position)
|
|
||||||
|
|
||||||
Toast.makeText(this@NotificationsActivity, "Notification deleted", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@NotificationsActivity, "Notification deleted", Toast.LENGTH_SHORT).show()
|
||||||
updateEmptyState()
|
updateEmptyState()
|
||||||
|
@ -301,6 +348,12 @@ class NotificationsActivity : AppCompatActivity() {
|
||||||
itemTouchHelper.attachToRecyclerView(rvNotifications)
|
itemTouchHelper.attachToRecyclerView(rvNotifications)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a helper function to toggle the visibility of the empty state message
|
||||||
|
* when no notifications are available.
|
||||||
|
*
|
||||||
|
* This should be called whenever a notification was removed or added.
|
||||||
|
*/
|
||||||
private fun updateEmptyState() {
|
private fun updateEmptyState() {
|
||||||
val isEmpty = notifications.isEmpty()
|
val isEmpty = notifications.isEmpty()
|
||||||
tvEmptyState.visibility = if (isEmpty) View.VISIBLE else View.GONE
|
tvEmptyState.visibility = if (isEmpty) View.VISIBLE else View.GONE
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
package com.p_vacho.neat_calendar.adapters
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.p_vacho.neat_calendar.R
|
||||||
|
import com.p_vacho.neat_calendar.api.models.CategoryResponse
|
||||||
|
|
||||||
|
class CategoryAdapter(
|
||||||
|
private val categories: MutableList<CategoryResponse>,
|
||||||
|
private val onDeleteClick: (CategoryResponse, Int) -> Unit
|
||||||
|
) : RecyclerView.Adapter<CategoryAdapter.CategoryViewHolder>() {
|
||||||
|
|
||||||
|
inner class CategoryViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
val colorIndicator: View = view.findViewById(R.id.colorIndicator)
|
||||||
|
val categoryName: TextView = view.findViewById(R.id.tvCategoryName)
|
||||||
|
val deleteButton: ImageButton = view.findViewById(R.id.btnDeleteCategory)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CategoryViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_category, parent, false)
|
||||||
|
return CategoryViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: CategoryViewHolder, position: Int) {
|
||||||
|
val category = categories[position]
|
||||||
|
|
||||||
|
// Bind category data to the views
|
||||||
|
holder.colorIndicator.setBackgroundColor(category.color.toArgb())
|
||||||
|
holder.categoryName.text = category.name
|
||||||
|
|
||||||
|
// Set click listener for the delete button
|
||||||
|
holder.deleteButton.setOnClickListener {
|
||||||
|
onDeleteClick(category, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = categories.size
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a category from the list and notify the adapter.
|
||||||
|
*
|
||||||
|
* Call this after the onDeleteClick callback deletes the category from the backend API.
|
||||||
|
*/
|
||||||
|
fun removeCategoryAt(position: Int) {
|
||||||
|
categories.removeAt(position)
|
||||||
|
notifyItemRemoved(position)
|
||||||
|
|
||||||
|
// Annoyingly, we can't just use notifyItemRemoved for the single removed item,
|
||||||
|
// as all the items below it would now be using the wrong position that was
|
||||||
|
// already bound to the callbacks from the click listeners, so we need to refresh
|
||||||
|
// all of the categories below this one as well.
|
||||||
|
notifyItemRangeChanged(position, categories.size - position)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new category to the end of the list and notify the adapter.
|
||||||
|
*/
|
||||||
|
fun addCategory(category: CategoryResponse) {
|
||||||
|
categories.add(category)
|
||||||
|
notifyItemInserted(categories.size)
|
||||||
|
}
|
||||||
|
}
|
|
@ -189,4 +189,20 @@ class NotificationAdapter(
|
||||||
else -> DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").format(createdAt)
|
else -> DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").format(createdAt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a notification from the list and notify the adapter.
|
||||||
|
*
|
||||||
|
* Call this after the callback deletes the notification from the backend API.
|
||||||
|
*/
|
||||||
|
fun removeNotificationAt(position: Int) {
|
||||||
|
notifications.removeAt(position)
|
||||||
|
notifyItemRemoved(position)
|
||||||
|
|
||||||
|
// Annoyingly, we can't just use notifyItemRemoved for the single removed item,
|
||||||
|
// as all the items below it would now be using the wrong position that was
|
||||||
|
// already bound to the callbacks from the click listeners, so we need to refresh
|
||||||
|
// all of the notifications below this one as well.
|
||||||
|
notifyItemRangeChanged(position, notifications.size - position)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.p_vacho.neat_calendar.api.services
|
package com.p_vacho.neat_calendar.api.services
|
||||||
|
|
||||||
import com.p_vacho.neat_calendar.api.models.CategoryResponse
|
import com.p_vacho.neat_calendar.api.models.CategoryResponse
|
||||||
|
import retrofit2.http.DELETE
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
|
|
||||||
|
@ -11,6 +12,9 @@ interface CategoryService {
|
||||||
@GET("/events/{event_id}/categories")
|
@GET("/events/{event_id}/categories")
|
||||||
suspend fun eventCategories(@Path("event_id") eventId: String): List<CategoryResponse>
|
suspend fun eventCategories(@Path("event_id") eventId: String): List<CategoryResponse>
|
||||||
|
|
||||||
@GET("/category/{category_id}")
|
@GET("/categories/{category_id}")
|
||||||
suspend fun getCategory(@Path("category_id") categoryId: String): CategoryResponse
|
suspend fun getCategory(@Path("category_id") categoryId: String): CategoryResponse
|
||||||
|
|
||||||
|
@DELETE("/categories/{category_id}")
|
||||||
|
suspend fun deleteCategory(@Path("category_id") categoryId: String): Unit
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/main"
|
android:id="@+id/main"
|
||||||
|
@ -7,4 +8,84 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".activities.CategoriesActivity">
|
tools:context=".activities.CategoriesActivity">
|
||||||
|
|
||||||
|
<!-- Title Bar -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/titleBar"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:background="?android:attr/dividerHorizontal"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
|
<!-- Back Button -->
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnBack"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@drawable/ic_arrow_back"
|
||||||
|
android:contentDescription="@string/back"
|
||||||
|
app:tint="?android:attr/textColorPrimary" />
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTitle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/categories"
|
||||||
|
tools:text="Categories" />
|
||||||
|
|
||||||
|
<!-- Add Button -->
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnAddCategory"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@drawable/ic_add"
|
||||||
|
android:contentDescription="@string/add_category"
|
||||||
|
app:tint="?android:attr/textColorPrimary" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- RecyclerView for displaying categories -->
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rvCategories"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/titleBar"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
tools:listitem="@layout/item_category"
|
||||||
|
tools:itemCount="5"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:paddingBottom="24dp"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:layout_marginTop="8dp" />
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvEmptyState"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/no_categories"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:gravity="center"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/titleBar"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
10
app/src/main/res/layout/activity_create_category.xml
Normal file
10
app/src/main/res/layout/activity_create_category.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/main"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".activities.CreateCategoryActivity">
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
50
app/src/main/res/layout/item_category.xml
Normal file
50
app/src/main/res/layout/item_category.xml
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
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="wrap_content"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<!-- Color Indicator -->
|
||||||
|
<View
|
||||||
|
android:id="@+id/colorIndicator"
|
||||||
|
android:layout_width="8dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@android:color/holo_blue_dark"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<!-- Category Name -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvCategoryName"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
tools:text="Work"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/colorIndicator"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/btnDeleteCategory" />
|
||||||
|
|
||||||
|
<!-- Delete Button -->
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnDeleteCategory"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@drawable/ic_trashbin"
|
||||||
|
android:contentDescription="@string/delete_category"
|
||||||
|
app:tint="?android:attr/textColorSecondary"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -81,4 +81,7 @@
|
||||||
<string name="view_event">View Event</string>
|
<string name="view_event">View Event</string>
|
||||||
<string name="swipe_to_delete_hint">Swipe right on a notification to delete it.</string>
|
<string name="swipe_to_delete_hint">Swipe right on a notification to delete it.</string>
|
||||||
<string name="no_notifications">You\'re all caught up! No notifications right now.</string>
|
<string name="no_notifications">You\'re all caught up! No notifications right now.</string>
|
||||||
|
<string name="no_categories">No categories found</string>
|
||||||
|
<string name="categories">Categories</string>
|
||||||
|
<string name="delete_category">Delete category</string>
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in a new issue