Compare commits
6 commits
cc38fe5484
...
ae2efe89f2
Author | SHA1 | Date | |
---|---|---|---|
Peter Vacho | ae2efe89f2 | ||
Peter Vacho | df18f13972 | ||
Peter Vacho | c900c7093f | ||
Peter Vacho | 6d6e1945bb | ||
Peter Vacho | 224f8642bb | ||
Peter Vacho | 025234a93b |
|
@ -18,7 +18,6 @@ import com.p_vacho.neat_calendar.R
|
||||||
import com.p_vacho.neat_calendar.adapters.CategoryAdapter
|
import com.p_vacho.neat_calendar.adapters.CategoryAdapter
|
||||||
import com.p_vacho.neat_calendar.api.RetrofitClient
|
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.CategoryResponse
|
||||||
import com.p_vacho.neat_calendar.api.models.EventResponse
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -38,7 +37,12 @@ class CategoriesActivity : AppCompatActivity() {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
val newCategory: CategoryResponse? = result.data?.getParcelableExtra("newCategory")
|
val newCategory: CategoryResponse? = result.data?.getParcelableExtra("newCategory")
|
||||||
|
|
||||||
newCategory?.let { categoryCreateReply(it) }
|
@Suppress("DEPRECATION")
|
||||||
|
val editedCategory: CategoryResponse? = result.data?.getParcelableExtra("editedCategory")
|
||||||
|
|
||||||
|
if (newCategory != null && editedCategory != null) throw IllegalStateException("Got both edit & new response")
|
||||||
|
if (newCategory != null) categoryCreateReply(newCategory, CategoryMode.CREATE)
|
||||||
|
if (editedCategory != null) categoryCreateReply(editedCategory, CategoryMode.EDIT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,14 +63,14 @@ class CategoriesActivity : AppCompatActivity() {
|
||||||
tvEmptyState = findViewById(R.id.tvEmptyState)
|
tvEmptyState = findViewById(R.id.tvEmptyState)
|
||||||
|
|
||||||
btnBack.setOnClickListener { finish() }
|
btnBack.setOnClickListener { finish() }
|
||||||
btnAddCategory.setOnClickListener { navigateToCreateCategory() }
|
btnAddCategory.setOnClickListener { handleCreateCategory() }
|
||||||
|
|
||||||
rvCategories.layoutManager = LinearLayoutManager(this)
|
rvCategories.layoutManager = LinearLayoutManager(this)
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
categories = fetchCategories().toMutableList()
|
categories = fetchCategories().toMutableList()
|
||||||
|
|
||||||
val adapter = CategoryAdapter(categories, ::handleDeleteCategory)
|
val adapter = CategoryAdapter(categories, ::handleDeleteCategory, ::handleEditCategory)
|
||||||
|
|
||||||
rvCategories.adapter = adapter
|
rvCategories.adapter = adapter
|
||||||
updateEmptyState()
|
updateEmptyState()
|
||||||
|
@ -105,20 +109,44 @@ class CategoriesActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigates to the activity for adding a new category.
|
* Handle create category button getting pressed.
|
||||||
|
*
|
||||||
|
* Navigates to the create category activity in create mode.
|
||||||
*/
|
*/
|
||||||
private fun navigateToCreateCategory() {
|
private fun handleCreateCategory() {
|
||||||
val intent = Intent(this, CreateCategoryActivity::class.java)
|
val intent = Intent(this, CreateCategoryActivity::class.java).apply {
|
||||||
startActivity(intent)
|
putExtra("mode", CategoryMode.CREATE.name)
|
||||||
|
}
|
||||||
|
createActivityLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle edit category button being pressed.
|
||||||
|
*
|
||||||
|
* Navigates to the create category activity in edit mode.
|
||||||
|
*/
|
||||||
|
private fun handleEditCategory(category: CategoryResponse, position: Int) {
|
||||||
|
val intent = Intent(this, CreateCategoryActivity::class.java).apply {
|
||||||
|
putExtra("mode", CategoryMode.EDIT.name)
|
||||||
|
putExtra("category", category)
|
||||||
|
}
|
||||||
|
createActivityLauncher.launch(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used as a callback, triggered when the CreateCategory Activity returns a result.
|
* 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.
|
* The returned value (the new / edited category data) is passed over as a parameter.
|
||||||
*/
|
*/
|
||||||
private fun categoryCreateReply(category: CategoryResponse) {
|
private fun categoryCreateReply(category: CategoryResponse, mode: CategoryMode) {
|
||||||
(rvCategories.adapter as CategoryAdapter).addCategory(category)
|
val adapter = (rvCategories.adapter as CategoryAdapter)
|
||||||
|
when (mode) {
|
||||||
|
CategoryMode.CREATE -> {
|
||||||
|
adapter.addCategory(category)
|
||||||
|
updateEmptyState()
|
||||||
|
}
|
||||||
|
CategoryMode.EDIT -> { adapter.editCategory(category) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,13 +1,48 @@
|
||||||
package com.p_vacho.neat_calendar.activities
|
package com.p_vacho.neat_calendar.activities
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
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.content.ContextCompat
|
||||||
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 com.github.dhaval2404.colorpicker.ColorPickerDialog
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
import com.google.gson.Gson
|
||||||
import com.p_vacho.neat_calendar.R
|
import com.p_vacho.neat_calendar.R
|
||||||
|
import com.p_vacho.neat_calendar.api.RetrofitClient
|
||||||
|
import com.p_vacho.neat_calendar.api.models.CategoryRequest
|
||||||
|
import com.p_vacho.neat_calendar.api.models.CategoryResponse
|
||||||
|
import com.p_vacho.neat_calendar.api.models.PartialCategoryRequest
|
||||||
|
import com.p_vacho.neat_calendar.api.models.ValidationError
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import retrofit2.HttpException
|
||||||
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
|
enum class CategoryMode {
|
||||||
|
CREATE, EDIT
|
||||||
|
}
|
||||||
|
|
||||||
class CreateCategoryActivity : AppCompatActivity() {
|
class CreateCategoryActivity : AppCompatActivity() {
|
||||||
|
private var selectedColor by Delegates.notNull<Int>()
|
||||||
|
private var existingCategory: CategoryResponse? = null
|
||||||
|
private lateinit var mode: CategoryMode
|
||||||
|
|
||||||
|
// UI components
|
||||||
|
private lateinit var etCategoryName: TextInputEditText
|
||||||
|
private lateinit var btnColorPicker: MaterialButton
|
||||||
|
private lateinit var btnSaveCategory: MaterialButton
|
||||||
|
private lateinit var btnCancel: MaterialButton
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
@ -17,5 +52,107 @@ class CreateCategoryActivity : AppCompatActivity() {
|
||||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||||
insets
|
insets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initializeViews()
|
||||||
|
mode = intent.getStringExtra("mode")!!.let { CategoryMode.valueOf(it) }
|
||||||
|
|
||||||
|
if (mode == CategoryMode.EDIT) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
existingCategory = intent.getParcelableExtra("category")!!
|
||||||
|
|
||||||
|
etCategoryName.setText(existingCategory?.name)
|
||||||
|
selectedColor = existingCategory?.color?.toArgb() ?: ContextCompat.getColor(this, R.color.category_indicator_color)
|
||||||
|
btnSaveCategory.setText(R.string.update_category)
|
||||||
|
} else {
|
||||||
|
selectedColor = ContextCompat.getColor(this, R.color.category_indicator_color)
|
||||||
|
}
|
||||||
|
|
||||||
|
btnColorPicker.iconTint = ColorStateList.valueOf(selectedColor)
|
||||||
|
setupListeners()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private fun initializeViews() {
|
||||||
|
etCategoryName = findViewById(R.id.etCategoryName)
|
||||||
|
btnColorPicker = findViewById(R.id.btnColorPicker)
|
||||||
|
btnSaveCategory = findViewById(R.id.btnSaveCategory)
|
||||||
|
btnCancel = findViewById(R.id.btnCancel)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupListeners() {
|
||||||
|
btnColorPicker.setOnClickListener { openColorPickerDialog() }
|
||||||
|
btnSaveCategory.setOnClickListener { saveCategory() }
|
||||||
|
btnCancel.setOnClickListener { finish() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveCategory() {
|
||||||
|
val categoryName = etCategoryName.text.toString().trim()
|
||||||
|
if (categoryName.isEmpty()) {
|
||||||
|
Toast.makeText(this, getString(R.string.please_enter_a_category_name), Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val resultCategory = if (mode == CategoryMode.CREATE) {
|
||||||
|
val request =
|
||||||
|
CategoryRequest(name = categoryName, color = Color.valueOf(selectedColor))
|
||||||
|
RetrofitClient.categoryService.createCategory(request)
|
||||||
|
} else {
|
||||||
|
val request = PartialCategoryRequest(
|
||||||
|
name = categoryName.takeIf { it != existingCategory?.name },
|
||||||
|
color = Color.valueOf(selectedColor)
|
||||||
|
.takeIf { it != existingCategory?.color }
|
||||||
|
)
|
||||||
|
RetrofitClient.categoryService.updateCategory(existingCategory!!.id, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) { handleCategorySaved(resultCategory) }
|
||||||
|
} catch (e: HttpException) {
|
||||||
|
if (e.code() != 422) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
|
val errorBody = e.response()?.errorBody()?.string()
|
||||||
|
val validationError = Gson().fromJson(errorBody, ValidationError::class.java)
|
||||||
|
val errMsg = validationError.detail.joinToString("\n")
|
||||||
|
|
||||||
|
Log.e("CategoryCreate", "Got HTTP 422: $validationError")
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Toast.makeText(
|
||||||
|
this@CreateCategoryActivity,
|
||||||
|
getString(R.string.failed_to_save_category, errMsg),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCategorySaved(category: CategoryResponse) {
|
||||||
|
Log.w("CreateCategory", "Ending")
|
||||||
|
val intent = Intent().apply {
|
||||||
|
if (mode == CategoryMode.CREATE) {
|
||||||
|
putExtra("newCategory", category)
|
||||||
|
Log.w("CreateCategory", "New")
|
||||||
|
} else {
|
||||||
|
putExtra("editedCategory", category)
|
||||||
|
Log.w("CreateCategory", "Edit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setResult(RESULT_OK, intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openColorPickerDialog() {
|
||||||
|
ColorPickerDialog
|
||||||
|
.Builder(this)
|
||||||
|
.setTitle(getString(R.string.select_color))
|
||||||
|
.setDefaultColor(selectedColor)
|
||||||
|
.setColorListener { color, _ ->
|
||||||
|
selectedColor = color
|
||||||
|
btnColorPicker.iconTint = ColorStateList.valueOf(color)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,15 +1,51 @@
|
||||||
package com.p_vacho.neat_calendar.activities
|
package com.p_vacho.neat_calendar.activities
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TextView
|
||||||
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.google.android.material.chip.Chip
|
||||||
|
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.CategoryChipAdapter
|
||||||
|
import com.p_vacho.neat_calendar.adapters.UserChipAdapter
|
||||||
|
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 com.p_vacho.neat_calendar.api.models.EventResponse
|
||||||
|
import com.p_vacho.neat_calendar.api.models.UserResponse
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
class EventDetailsActivity : AppCompatActivity() {
|
class EventDetailsActivity : AppCompatActivity() {
|
||||||
private lateinit var event: EventResponse
|
private lateinit var event: EventResponse
|
||||||
|
private lateinit var categories: Map<String, CategoryResponse>
|
||||||
|
private lateinit var users: Map<String, UserResponse>
|
||||||
|
|
||||||
|
// UI components
|
||||||
|
private lateinit var btnBack: ImageButton
|
||||||
|
private lateinit var tvEventTitle: TextView
|
||||||
|
private lateinit var tvEventStartTime: TextView
|
||||||
|
private lateinit var tvEventEndTime: TextView
|
||||||
|
private lateinit var tvEventDescription: TextView
|
||||||
|
private lateinit var rvEventCategories: RecyclerView
|
||||||
|
private lateinit var rvEventAttendees: RecyclerView
|
||||||
|
private lateinit var tvEventCreatedAt: TextView
|
||||||
|
private lateinit var tvNoCategories: TextView
|
||||||
|
private lateinit var tvNoAttendees: TextView
|
||||||
|
private lateinit var tvEventOwner: TextView
|
||||||
|
private lateinit var chipEventOwner: Chip
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -21,7 +57,205 @@ class EventDetailsActivity : AppCompatActivity() {
|
||||||
insets
|
insets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initializeViews()
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
event = intent.getParcelableExtra<EventResponse>("event")!!
|
event = intent.getParcelableExtra("event")!!
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
categories = intent.getParcelableArrayListExtra<CategoryResponse>("categories")?.associateBy { it.id } ?: emptyMap()
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
users = intent.getParcelableArrayListExtra<UserResponse>("users")?.associateBy { it.user_id } ?: emptyMap()
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
// Collect tasks for missing data
|
||||||
|
// This will also start the fetching coroutines
|
||||||
|
val fetchCategoriesTask = determineCategoryFetchTask()
|
||||||
|
val fetchUsersTask = determineUserFetchTask()
|
||||||
|
|
||||||
|
// Await results
|
||||||
|
val fetchedCategories = fetchCategoriesTask?.await()
|
||||||
|
val fetchedUsers = fetchUsersTask?.await()
|
||||||
|
|
||||||
|
// Update data maps
|
||||||
|
if (fetchedCategories != null) categories = categories + fetchedCategories
|
||||||
|
if (fetchedUsers != null) users = users + fetchedUsers
|
||||||
|
|
||||||
|
setEventDetails()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private fun initializeViews() {
|
||||||
|
btnBack = findViewById(R.id.btnBack)
|
||||||
|
tvEventTitle = findViewById(R.id.tvEventTitle)
|
||||||
|
tvEventStartTime = findViewById(R.id.tvEventStartTime)
|
||||||
|
tvEventEndTime = findViewById(R.id.tvEventEndTime)
|
||||||
|
tvEventDescription = findViewById(R.id.tvEventDescription)
|
||||||
|
rvEventCategories = findViewById(R.id.rvEventCategories)
|
||||||
|
rvEventAttendees = findViewById(R.id.rvEventAttendees)
|
||||||
|
tvEventCreatedAt = findViewById(R.id.tvEventCreatedAt)
|
||||||
|
tvNoCategories = findViewById(R.id.tvNoCategories)
|
||||||
|
tvNoAttendees = findViewById(R.id.tvNoAttendees)
|
||||||
|
tvEventOwner = findViewById(R.id.tvEventOwner)
|
||||||
|
chipEventOwner = findViewById(R.id.chipEventOwner)
|
||||||
|
|
||||||
|
rvEventCategories.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
rvEventCategories.adapter = CategoryChipAdapter(emptyList(), isRemovable = false)
|
||||||
|
|
||||||
|
rvEventAttendees.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
rvEventAttendees.adapter = UserChipAdapter(emptyList())
|
||||||
|
|
||||||
|
btnBack.setOnClickListener { finish() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populate the individual UI items with actual content about the event.
|
||||||
|
*
|
||||||
|
* This can only be called once all of the necessary data is fetched.
|
||||||
|
*/
|
||||||
|
private fun setEventDetails() {
|
||||||
|
// Set basic event details
|
||||||
|
tvEventTitle.text = event.title
|
||||||
|
tvEventStartTime.text = getString(R.string.event_start_time, formatDateTime(event.start_time.toLocalDateTime()))
|
||||||
|
tvEventEndTime.text = getString(R.string.event_end_time, formatDateTime(event.end_time.toLocalDateTime()))
|
||||||
|
tvEventDescription.text = event.description
|
||||||
|
tvEventCreatedAt.text = getString(R.string.event_created_at, formatDateTime(event.created_at.toLocalDateTime()))
|
||||||
|
|
||||||
|
val userId = (application as MyApplication).tokenManager.userId
|
||||||
|
|
||||||
|
// Set categories or show the placeholder
|
||||||
|
val categoryList = event.category_ids.mapNotNull { categories[it] }
|
||||||
|
rvEventCategories.adapter = CategoryChipAdapter(categoryList, isRemovable = false)
|
||||||
|
tvNoCategories.visibility = if (categoryList.isEmpty()) View.VISIBLE else View.GONE
|
||||||
|
if (event.owner_user_id == userId) {
|
||||||
|
tvNoCategories.text = getString(R.string.no_categories_placeholder)
|
||||||
|
} else {
|
||||||
|
tvNoCategories.text = getString(R.string.no_invited_categories_placeholder)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Set attendees or show the placeholder
|
||||||
|
val attendeeList = event.attendee_ids.mapNotNull { users[it] }
|
||||||
|
rvEventAttendees.adapter = UserChipAdapter(attendeeList)
|
||||||
|
tvNoAttendees.visibility = if (attendeeList.isEmpty()) View.VISIBLE else View.GONE
|
||||||
|
tvNoAttendees.text = getString(R.string.no_attendees_placeholder)
|
||||||
|
|
||||||
|
// Show the owner section only if this event isn't owned by the logged user
|
||||||
|
if (event.owner_user_id != userId) {
|
||||||
|
val owner = users[event.owner_user_id]
|
||||||
|
if (owner == null) throw IllegalStateException("Event owner wasn't fetched")
|
||||||
|
tvEventOwner.visibility = View.VISIBLE
|
||||||
|
chipEventOwner.visibility = View.VISIBLE
|
||||||
|
chipEventOwner.text = owner.username
|
||||||
|
} else {
|
||||||
|
tvEventOwner.visibility = View.GONE
|
||||||
|
chipEventOwner.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether event categories need to be fetched.
|
||||||
|
*
|
||||||
|
* Categories will be fetched if:
|
||||||
|
* - They weren't already set
|
||||||
|
* - One (or more) of the event categories wasn't found in the list of existing categories.
|
||||||
|
* Note that this case will also produce a warning.
|
||||||
|
*
|
||||||
|
* Categories won't be fetched if the currently logged in user isn't also the event owner,
|
||||||
|
* as they don't have the rights to access the invitor's categories.
|
||||||
|
*
|
||||||
|
* This will return back a Deferred object, being obtained in an already started coroutine.
|
||||||
|
*/
|
||||||
|
private fun determineCategoryFetchTask(): Deferred<Map<String, CategoryResponse>>? {
|
||||||
|
val userId = (application as MyApplication).tokenManager.userId
|
||||||
|
|
||||||
|
if (userId != event.owner_user_id) return null
|
||||||
|
|
||||||
|
return if (categories.isEmpty()) {
|
||||||
|
lifecycleScope.async { fetchEventCategories(event.id) }
|
||||||
|
} else if (categories.keys.intersect(event.category_ids).size != event.category_ids.size) {
|
||||||
|
Log.w(
|
||||||
|
"EventDetailsActivity",
|
||||||
|
"One or more of the event categories wasn't found in the list of categories. Categories will be re-fetched."
|
||||||
|
)
|
||||||
|
lifecycleScope.async { fetchEventCategories(event.id) }
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines which users need to be fetched and creates a fetch task if necessary.
|
||||||
|
*
|
||||||
|
* This will fetch all the users that are attending the event and the event's user id,
|
||||||
|
* skipping any users that are already fetched. Note that if any of the event attendees
|
||||||
|
* aren't already in the users list, yet the list isn't empty, a warning will be produced.
|
||||||
|
*
|
||||||
|
* This will return back a Deferred object, being obtained in an already started coroutine.
|
||||||
|
*/
|
||||||
|
private fun determineUserFetchTask(): Deferred<Map<String, UserResponse>>? {
|
||||||
|
val userIdsToFetch = mutableSetOf<String>().apply {
|
||||||
|
if (users.isEmpty()) {
|
||||||
|
addAll(event.attendee_ids)
|
||||||
|
add(event.owner_user_id)
|
||||||
|
} else {
|
||||||
|
if (!users.containsKey(event.owner_user_id)) add(event.owner_user_id)
|
||||||
|
|
||||||
|
// Check for missing attendees and log a warning for each
|
||||||
|
val missingAttendees = event.attendee_ids.filter { !users.containsKey(it) }
|
||||||
|
if (missingAttendees.isNotEmpty()) {
|
||||||
|
missingAttendees.forEach { attendeeId ->
|
||||||
|
Log.w(
|
||||||
|
"EventDetailsActivity",
|
||||||
|
"Missing attendee ID: $attendeeId in the provided users list. Will fetch it additionally."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
addAll(missingAttendees)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (userIdsToFetch.isNotEmpty()) {
|
||||||
|
lifecycleScope.async { fetchUsers(userIdsToFetch.toList()) }
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all the categories of given event.
|
||||||
|
*
|
||||||
|
* This will return a hash map, with the category IDs as keys.
|
||||||
|
*/
|
||||||
|
private suspend fun fetchEventCategories(eventId: String): Map<String, CategoryResponse> {
|
||||||
|
val fetchedCategories = withContext(Dispatchers.IO) {
|
||||||
|
RetrofitClient.categoryService.eventCategories(eventId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchedCategories.associateBy { it.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all the requested users (by IDs).
|
||||||
|
*
|
||||||
|
* This will return a hash map, with the user IDs as keys.
|
||||||
|
*
|
||||||
|
* Note that this will make an API request for each requested user ID.
|
||||||
|
* These requests will be made in parallel.
|
||||||
|
*/
|
||||||
|
private suspend fun fetchUsers(userIds: List<String>): Map<String, UserResponse> {
|
||||||
|
val fetchedUsers = withContext(Dispatchers.IO) {
|
||||||
|
val usersDeferred = userIds.map { async { RetrofitClient.usersService.getUser(it) }}
|
||||||
|
usersDeferred.map { it.await() }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchedUsers.associateBy { it.user_id }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a date time object into a string.
|
||||||
|
*
|
||||||
|
* This is used to format the datetime shown for the event start, end & creation times.
|
||||||
|
*/
|
||||||
|
private fun formatDateTime(dateTime: java.time.LocalDateTime): String {
|
||||||
|
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -11,12 +11,14 @@ import com.p_vacho.neat_calendar.api.models.CategoryResponse
|
||||||
|
|
||||||
class CategoryAdapter(
|
class CategoryAdapter(
|
||||||
private val categories: MutableList<CategoryResponse>,
|
private val categories: MutableList<CategoryResponse>,
|
||||||
private val onDeleteClick: (CategoryResponse, Int) -> Unit
|
private val onDeleteClick: (CategoryResponse, Int) -> Unit,
|
||||||
|
private val onEditClick: (CategoryResponse, Int) -> Unit
|
||||||
) : RecyclerView.Adapter<CategoryAdapter.CategoryViewHolder>() {
|
) : RecyclerView.Adapter<CategoryAdapter.CategoryViewHolder>() {
|
||||||
|
|
||||||
inner class CategoryViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
inner class CategoryViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
val colorIndicator: View = view.findViewById(R.id.colorIndicator)
|
val colorIndicator: View = view.findViewById(R.id.colorIndicator)
|
||||||
val categoryName: TextView = view.findViewById(R.id.tvCategoryName)
|
val categoryName: TextView = view.findViewById(R.id.tvCategoryName)
|
||||||
|
val editButton: ImageButton = view.findViewById(R.id.btnEditCategory)
|
||||||
val deleteButton: ImageButton = view.findViewById(R.id.btnDeleteCategory)
|
val deleteButton: ImageButton = view.findViewById(R.id.btnDeleteCategory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,10 +35,9 @@ class CategoryAdapter(
|
||||||
holder.colorIndicator.setBackgroundColor(category.color.toArgb())
|
holder.colorIndicator.setBackgroundColor(category.color.toArgb())
|
||||||
holder.categoryName.text = category.name
|
holder.categoryName.text = category.name
|
||||||
|
|
||||||
// Set click listener for the delete button
|
// Set click listeners for the buttons
|
||||||
holder.deleteButton.setOnClickListener {
|
holder.deleteButton.setOnClickListener { onDeleteClick(category, position) }
|
||||||
onDeleteClick(category, position)
|
holder.editButton.setOnClickListener { onEditClick(category, position) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = categories.size
|
override fun getItemCount(): Int = categories.size
|
||||||
|
@ -64,4 +65,15 @@ class CategoryAdapter(
|
||||||
categories.add(category)
|
categories.add(category)
|
||||||
notifyItemInserted(categories.size)
|
notifyItemInserted(categories.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit an existing category, updating it in the UI.
|
||||||
|
*
|
||||||
|
* Call this after the onEditClick callback edits the category from the backend API.
|
||||||
|
*/
|
||||||
|
fun editCategory(category: CategoryResponse) {
|
||||||
|
val position = categories.indexOfFirst { it.id == category.id }
|
||||||
|
categories[position] = category
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,9 @@ package com.p_vacho.neat_calendar.adapters
|
||||||
|
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageButton
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.chip.Chip
|
||||||
import com.p_vacho.neat_calendar.R
|
import com.p_vacho.neat_calendar.R
|
||||||
import com.p_vacho.neat_calendar.api.models.CategoryResponse
|
import com.p_vacho.neat_calendar.api.models.CategoryResponse
|
||||||
|
|
||||||
|
@ -14,47 +12,37 @@ class CategoryChipAdapter(
|
||||||
private val categories: List<CategoryResponse>,
|
private val categories: List<CategoryResponse>,
|
||||||
private val isRemovable: Boolean = false,
|
private val isRemovable: Boolean = false,
|
||||||
private val onRemoveCategory: ((CategoryResponse, Int) -> Unit)? = null
|
private val onRemoveCategory: ((CategoryResponse, Int) -> Unit)? = null
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
) : RecyclerView.Adapter<CategoryChipAdapter.CategoryChipViewHolder>() {
|
||||||
|
|
||||||
inner class CategoryViewHolder(val textView: TextView) : RecyclerView.ViewHolder(textView)
|
inner class CategoryChipViewHolder(val chip: Chip) : RecyclerView.ViewHolder(chip)
|
||||||
|
|
||||||
inner class RemovableCategoryViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|
||||||
val textView: TextView = view.findViewById(R.id.chipText)
|
|
||||||
val removeButton: ImageButton = view.findViewById(R.id.removeButton)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
return if (isRemovable) 1 else 0
|
return if (isRemovable) 1 else 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CategoryChipViewHolder {
|
||||||
return if (viewType == 1) {
|
val layoutResId = if (viewType == 1) {
|
||||||
val view = LayoutInflater.from(parent.context)
|
R.layout.item_removable_category_chip
|
||||||
.inflate(R.layout.item_removable_category_chip, parent, false)
|
|
||||||
RemovableCategoryViewHolder(view)
|
|
||||||
} else {
|
} else {
|
||||||
val view = LayoutInflater.from(parent.context)
|
R.layout.item_category_chip
|
||||||
.inflate(R.layout.item_category_chip, parent, false) as TextView
|
|
||||||
CategoryViewHolder(view)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val chip = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(layoutResId, parent, false) as Chip
|
||||||
|
return CategoryChipViewHolder(chip)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: CategoryChipViewHolder, position: Int) {
|
||||||
val category = categories[position]
|
val category = categories[position]
|
||||||
|
val chip = holder.chip
|
||||||
|
|
||||||
if (holder is CategoryViewHolder) {
|
// Set the chip's text and background color
|
||||||
// Regular category chip
|
chip.text = category.name
|
||||||
holder.textView.text = category.name
|
chip.chipStrokeColor = ColorStateList.valueOf(category.color.toArgb())
|
||||||
holder.textView.backgroundTintList = ColorStateList.valueOf(category.color.toArgb())
|
|
||||||
} else if (holder is RemovableCategoryViewHolder) {
|
|
||||||
// Removable category chip
|
|
||||||
holder.textView.text = category.name
|
|
||||||
|
|
||||||
// Apply background tint to the LinearLayout (container)
|
if (isRemovable) {
|
||||||
holder.itemView.backgroundTintList = ColorStateList.valueOf(category.color.toArgb())
|
// Set close icon click listener for removable chips
|
||||||
|
chip.setOnCloseIconClickListener {
|
||||||
// Set up the remove button's click listener
|
|
||||||
holder.removeButton.setOnClickListener {
|
|
||||||
onRemoveCategory?.invoke(category, position)
|
onRemoveCategory?.invoke(category, position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ class EventCardAdapter(
|
||||||
// Initialize empty state for categories
|
// Initialize empty state for categories
|
||||||
holder.rvCategories.layoutManager =
|
holder.rvCategories.layoutManager =
|
||||||
LinearLayoutManager(holder.itemView.context, LinearLayoutManager.HORIZONTAL, false)
|
LinearLayoutManager(holder.itemView.context, LinearLayoutManager.HORIZONTAL, false)
|
||||||
holder.rvCategories.adapter = CategoryChipAdapter(emptyList())
|
holder.rvCategories.adapter = CategoryChipAdapter(emptyList(), isRemovable = false)
|
||||||
|
|
||||||
// Events list might contain events that are owned by others (invited events)
|
// Events list might contain events that are owned by others (invited events)
|
||||||
// we can't show the categories & edit / delete buttons for these, as we don't
|
// we can't show the categories & edit / delete buttons for these, as we don't
|
||||||
|
@ -138,7 +138,7 @@ class EventCardAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the RecyclerView adapter on the main thread
|
// Update the RecyclerView adapter on the main thread
|
||||||
holder.rvCategories.adapter = CategoryChipAdapter(categories)
|
holder.rvCategories.adapter = CategoryChipAdapter(categories, isRemovable = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.p_vacho.neat_calendar.adapters
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.chip.Chip
|
||||||
|
import com.p_vacho.neat_calendar.R
|
||||||
|
import com.p_vacho.neat_calendar.api.models.UserResponse
|
||||||
|
|
||||||
|
class UserChipAdapter(
|
||||||
|
private val users: List<UserResponse>
|
||||||
|
) : RecyclerView.Adapter<UserChipAdapter.UserChipViewHolder>() {
|
||||||
|
|
||||||
|
inner class UserChipViewHolder(val chip: Chip) : RecyclerView.ViewHolder(chip)
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserChipViewHolder {
|
||||||
|
val chip = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_user_chip, parent, false) as Chip
|
||||||
|
return UserChipViewHolder(chip)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: UserChipViewHolder, position: Int) {
|
||||||
|
val user = users[position]
|
||||||
|
val chip = holder.chip
|
||||||
|
|
||||||
|
chip.text = user.username
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = users.size
|
||||||
|
}
|
|
@ -1,13 +1,16 @@
|
||||||
package com.p_vacho.neat_calendar.api.models
|
package com.p_vacho.neat_calendar.api.models
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
data class UserResponse(
|
data class UserResponse(
|
||||||
val user_id: String,
|
val user_id: String,
|
||||||
val username: String,
|
val username: String,
|
||||||
val email: String,
|
val email: String,
|
||||||
val created_at: OffsetDateTime,
|
val created_at: OffsetDateTime,
|
||||||
)
|
) : Parcelable
|
||||||
|
|
||||||
data class PartialUserRequest(
|
data class PartialUserRequest(
|
||||||
val username: String?,
|
val username: String?,
|
||||||
|
|
|
@ -24,7 +24,7 @@ interface CategoryService {
|
||||||
suspend fun createCategory(@Body categoryData: CategoryRequest): CategoryResponse
|
suspend fun createCategory(@Body categoryData: CategoryRequest): CategoryResponse
|
||||||
|
|
||||||
@PATCH("/categories/{category_id}")
|
@PATCH("/categories/{category_id}")
|
||||||
suspend fun createCategory(@Path("category_id") categoryId: String, @Body categoryData: PartialCategoryRequest): CategoryResponse
|
suspend fun updateCategory(@Path("category_id") categoryId: String, @Body categoryData: PartialCategoryRequest): CategoryResponse
|
||||||
|
|
||||||
@DELETE("/categories/{category_id}")
|
@DELETE("/categories/{category_id}")
|
||||||
suspend fun deleteCategory(@Path("category_id") categoryId: String): Unit
|
suspend fun deleteCategory(@Path("category_id") categoryId: String): Unit
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:text="@string/select_color"
|
android:text="@string/select_color"
|
||||||
app:icon="@drawable/ic_circle"
|
app:icon="@drawable/ic_circle"
|
||||||
app:iconTint="@android:color/holo_blue_dark"
|
app:iconTint="@color/category_indicator_color"
|
||||||
app:cornerRadius="8dp"
|
app:cornerRadius="8dp"
|
||||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
|
||||||
|
|
||||||
|
|
|
@ -121,7 +121,7 @@
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:textColor="?android:attr/textColorPrimary"
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
tools:text="Categories:" />
|
android:text="@string/categories_section" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/rvEventCategories"
|
android:id="@+id/rvEventCategories"
|
||||||
|
@ -133,6 +133,18 @@
|
||||||
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
tools:orientation="horizontal" />
|
tools:orientation="horizontal" />
|
||||||
|
|
||||||
|
<!-- Placeholder for empty categories -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvNoCategories"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/no_categories_placeholder"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
<!-- Attendees Section -->
|
<!-- Attendees Section -->
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvEventAttendees"
|
android:id="@+id/tvEventAttendees"
|
||||||
|
@ -142,18 +154,48 @@
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:textColor="?android:attr/textColorPrimary"
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
tools:text="Attendees:" />
|
android:text="@string/attendees_section" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/rvEventAttendees"
|
android:id="@+id/rvEventAttendees"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
tools:listitem="@layout/item_attendee_chip"
|
tools:listitem="@layout/item_user_chip"
|
||||||
tools:itemCount="3"
|
tools:itemCount="3"
|
||||||
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
tools:orientation="horizontal" />
|
tools:orientation="horizontal" />
|
||||||
|
|
||||||
|
<!-- Placeholder for empty attendees -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvNoAttendees"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/no_attendees_placeholder"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<!-- Event Owner Section -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvEventOwner"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:text="@string/event_owner_section" />
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/chipEventOwner"
|
||||||
|
layout="@layout/item_user_chip"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp" />
|
||||||
|
|
||||||
<!-- Created At -->
|
<!-- Created At -->
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvEventCreatedAt"
|
android:id="@+id/tvEventCreatedAt"
|
||||||
|
@ -166,5 +208,4 @@
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
android:id="@+id/colorIndicator"
|
android:id="@+id/colorIndicator"
|
||||||
android:layout_width="8dp"
|
android:layout_width="8dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:background="@android:color/holo_blue_dark"
|
android:background="@color/category_indicator_color"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
@ -40,20 +40,39 @@
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/colorIndicator"
|
app:layout_constraintStart_toEndOf="@id/colorIndicator"
|
||||||
app:layout_constraintEnd_toStartOf="@id/btnDeleteCategory" />
|
app:layout_constraintEnd_toStartOf="@id/buttonContainer" />
|
||||||
|
|
||||||
<!-- Delete Button -->
|
<!-- Button Container -->
|
||||||
<ImageButton
|
<LinearLayout
|
||||||
android:id="@+id/btnDeleteCategory"
|
android:id="@+id/buttonContainer"
|
||||||
android:layout_width="40dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="40dp"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
android:orientation="horizontal"
|
||||||
android:src="@drawable/ic_trashbin"
|
android:gravity="center"
|
||||||
android:contentDescription="@string/delete_category"
|
|
||||||
app:tint="?android:attr/textColorSecondary"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
|
<!-- Edit Button -->
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnEditCategory"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@drawable/ic_pencil"
|
||||||
|
android:contentDescription="@string/edit_category"
|
||||||
|
app:tint="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
|
<!-- 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" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
|
@ -17,6 +17,6 @@
|
||||||
app:chipIconTint="?android:attr/textColorSecondary"
|
app:chipIconTint="?android:attr/textColorSecondary"
|
||||||
app:chipBackgroundColor="@android:color/transparent"
|
app:chipBackgroundColor="@android:color/transparent"
|
||||||
app:chipSurfaceColor="@android:color/transparent"
|
app:chipSurfaceColor="@android:color/transparent"
|
||||||
app:chipStrokeColor="@android:color/holo_blue_dark"
|
app:chipStrokeColor="@color/category_indicator_color"
|
||||||
app:closeIconEnabled="false"
|
app:closeIconEnabled="false"
|
||||||
tools:text="Work" />
|
tools:text="Work" />
|
||||||
|
|
|
@ -1,37 +1,24 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<com.google.android.material.chip.Chip
|
||||||
<LinearLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
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"
|
||||||
|
android:id="@+id/removableChip"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:background="@drawable/bg_category_chip"
|
|
||||||
android:backgroundTint="@android:color/holo_blue_dark"
|
|
||||||
android:paddingStart="8dp"
|
android:paddingStart="8dp"
|
||||||
android:paddingEnd="4dp"
|
android:paddingEnd="8dp"
|
||||||
android:paddingTop="4dp"
|
android:paddingTop="4dp"
|
||||||
android:paddingBottom="4dp">
|
android:paddingBottom="4dp"
|
||||||
|
android:textSize="12sp"
|
||||||
<!-- Text for the category -->
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
<TextView
|
app:chipIcon="@drawable/ic_tag"
|
||||||
android:id="@+id/chipText"
|
app:chipIconTint="?android:attr/textColorSecondary"
|
||||||
android:layout_width="wrap_content"
|
app:chipBackgroundColor="@android:color/transparent"
|
||||||
android:layout_height="wrap_content"
|
app:chipSurfaceColor="@android:color/transparent"
|
||||||
android:textSize="12sp"
|
app:chipStrokeColor="@android:color/holo_blue_dark"
|
||||||
android:textColor="?android:attr/textColorPrimary"
|
app:chipStrokeWidth="1dp"
|
||||||
tools:text="Work" />
|
app:closeIconEnabled="true"
|
||||||
|
app:closeIcon="@drawable/ic_close"
|
||||||
<!-- X button to remove the category -->
|
app:closeIconTint="?android:attr/textColorSecondary"
|
||||||
<ImageButton
|
tools:text="Work" />
|
||||||
android:id="@+id/removeButton"
|
|
||||||
android:layout_width="20dp"
|
|
||||||
android:layout_height="20dp"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:contentDescription="@string/remove_category"
|
|
||||||
android:src="@drawable/ic_close"
|
|
||||||
app:tint="?android:attr/textColorPrimary" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
|
@ -7,5 +7,6 @@
|
||||||
<color name="splash_dark_background">#121212</color> <!-- Dark gray -->
|
<color name="splash_dark_background">#121212</color> <!-- Dark gray -->
|
||||||
|
|
||||||
<color name="event_indicator_color">#0035D0</color>
|
<color name="event_indicator_color">#0035D0</color>
|
||||||
|
<color name="category_indicator_color">#0035D0</color>
|
||||||
<color name="unreadIndicator">#bb6633</color>
|
<color name="unreadIndicator">#bb6633</color>
|
||||||
</resources>
|
</resources>
|
|
@ -44,6 +44,7 @@
|
||||||
<string name="remove_category">Remove the category</string>
|
<string name="remove_category">Remove the category</string>
|
||||||
<string name="add_category">Add category</string>
|
<string name="add_category">Add category</string>
|
||||||
<string name="update_event">Update Event</string>
|
<string name="update_event">Update Event</string>
|
||||||
|
<string name="update_category">Update Category</string>
|
||||||
<string name="leave_invited_event">Leave invited event</string>
|
<string name="leave_invited_event">Leave invited event</string>
|
||||||
<string name="open_app_settings">Open Settings</string>
|
<string name="open_app_settings">Open Settings</string>
|
||||||
<string name="open_notifications">Open Notifications</string>
|
<string name="open_notifications">Open Notifications</string>
|
||||||
|
@ -87,4 +88,16 @@
|
||||||
<string name="event_details">Event Details</string>
|
<string name="event_details">Event Details</string>
|
||||||
<string name="category_name">Category Name</string>
|
<string name="category_name">Category Name</string>
|
||||||
<string name="open_categories">Open category management</string>
|
<string name="open_categories">Open category management</string>
|
||||||
|
<string name="event_start_time">"Start: %1$s"</string>
|
||||||
|
<string name="event_end_time">End: %1$s</string>
|
||||||
|
<string name="event_created_at">Created on: %1$s</string>
|
||||||
|
<string name="categories_section">Categories:</string>
|
||||||
|
<string name="attendees_section">Attendees:</string>
|
||||||
|
<string name="no_attendees_placeholder">This event has no attendees.</string>
|
||||||
|
<string name="no_categories_placeholder">This event has no categories.</string>
|
||||||
|
<string name="event_owner_section">Event owner:</string>
|
||||||
|
<string name="no_invited_categories_placeholder">You can\'t see categories for invited events</string>
|
||||||
|
<string name="edit_category">Edit category</string>
|
||||||
|
<string name="please_enter_a_category_name">Please enter a category name</string>
|
||||||
|
<string name="failed_to_save_category">Failed to save category: %1$s</string>
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in a new issue