diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f51d844..5083ccb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,6 +16,9 @@ android:supportsRtl="true" android:theme="@style/Theme.NeatCalendar" tools:targetApi="31"> + diff --git a/app/src/main/java/com/p_vacho/neat_calendar/activities/CategoriesActivity.kt b/app/src/main/java/com/p_vacho/neat_calendar/activities/CategoriesActivity.kt index b76dce8..11b8433 100644 --- a/app/src/main/java/com/p_vacho/neat_calendar/activities/CategoriesActivity.kt +++ b/app/src/main/java/com/p_vacho/neat_calendar/activities/CategoriesActivity.kt @@ -1,21 +1,133 @@ package com.p_vacho.neat_calendar.activities +import android.content.Intent 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.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat 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.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() { + 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 + + 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?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContentView(R.layout.activity_categories) + 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 } + + 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() + } } -} \ No newline at end of file + + /** + * Fetches all categories for the current user from the backend. + */ + private suspend fun fetchCategories(): List { + 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 + } +} diff --git a/app/src/main/java/com/p_vacho/neat_calendar/activities/CreateCategoryActivity.kt b/app/src/main/java/com/p_vacho/neat_calendar/activities/CreateCategoryActivity.kt new file mode 100644 index 0000000..9c55922 --- /dev/null +++ b/app/src/main/java/com/p_vacho/neat_calendar/activities/CreateCategoryActivity.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/p_vacho/neat_calendar/adapters/CategoryAdapter.kt b/app/src/main/java/com/p_vacho/neat_calendar/adapters/CategoryAdapter.kt new file mode 100644 index 0000000..6273888 --- /dev/null +++ b/app/src/main/java/com/p_vacho/neat_calendar/adapters/CategoryAdapter.kt @@ -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, + private val onDeleteClick: (CategoryResponse, Int) -> Unit +) : RecyclerView.Adapter() { + + 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) + } +} diff --git a/app/src/main/java/com/p_vacho/neat_calendar/api/services/CategoryService.kt b/app/src/main/java/com/p_vacho/neat_calendar/api/services/CategoryService.kt index 0d991b5..2b2374d 100644 --- a/app/src/main/java/com/p_vacho/neat_calendar/api/services/CategoryService.kt +++ b/app/src/main/java/com/p_vacho/neat_calendar/api/services/CategoryService.kt @@ -1,6 +1,7 @@ package com.p_vacho.neat_calendar.api.services import com.p_vacho.neat_calendar.api.models.CategoryResponse +import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.Path @@ -11,6 +12,9 @@ interface CategoryService { @GET("/events/{event_id}/categories") suspend fun eventCategories(@Path("event_id") eventId: String): List - @GET("/category/{category_id}") + @GET("/categories/{category_id}") suspend fun getCategory(@Path("category_id") categoryId: String): CategoryResponse + + @DELETE("/categories/{category_id}") + suspend fun deleteCategory(@Path("category_id") categoryId: String): Unit } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_categories.xml b/app/src/main/res/layout/activity_categories.xml index d01abd6..4a94d76 100644 --- a/app/src/main/res/layout/activity_categories.xml +++ b/app/src/main/res/layout/activity_categories.xml @@ -1,5 +1,6 @@ - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_create_category.xml b/app/src/main/res/layout/activity_create_category.xml new file mode 100644 index 0000000..f17dc48 --- /dev/null +++ b/app/src/main/res/layout/activity_create_category.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_category.xml b/app/src/main/res/layout/item_category.xml new file mode 100644 index 0000000..eb1aa79 --- /dev/null +++ b/app/src/main/res/layout/item_category.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e078925..768f773 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -81,4 +81,7 @@ View Event Swipe right on a notification to delete it. You\'re all caught up! No notifications right now. + No categories found + Categories + Delete category \ No newline at end of file