diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 105682e23..c5ff89c45 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,7 +2,10 @@ + + = Build.VERSION_CODES.N) { - Html.fromHtml(this, Html.FROM_HTML_MODE_LEGACY).toString() - } else { - Html.fromHtml(this).toString() - } +fun String.htmlToSpanned() : Spanned { + return HtmlCompat.fromHtml(this, FROM_HTML_MODE_LEGACY) } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/todolist/model/History.kt b/android/app/src/main/java/com/example/todolist/model/History.kt index a3a00bcf0..299987f96 100644 --- a/android/app/src/main/java/com/example/todolist/model/History.kt +++ b/android/app/src/main/java/com/example/todolist/model/History.kt @@ -6,7 +6,7 @@ data class History( val title: String, val nowStatus: String, val beforeStatus: String?, - val createData: String, + val createDateTime: String, ) enum class ActionType(val action: String) { diff --git a/android/app/src/main/java/com/example/todolist/model/Task.kt b/android/app/src/main/java/com/example/todolist/model/Task.kt index d39aa4400..0d7e3b34f 100644 --- a/android/app/src/main/java/com/example/todolist/model/Task.kt +++ b/android/app/src/main/java/com/example/todolist/model/Task.kt @@ -2,7 +2,7 @@ package com.example.todolist.model data class Task( val title: String, - val content: String, + val contents: String, val status: Status, val author: String = "Android" ) diff --git a/android/app/src/main/java/com/example/todolist/model/request/ModifyTaskRequest.kt b/android/app/src/main/java/com/example/todolist/model/request/ModifyTaskRequest.kt new file mode 100644 index 000000000..ee2685219 --- /dev/null +++ b/android/app/src/main/java/com/example/todolist/model/request/ModifyTaskRequest.kt @@ -0,0 +1,9 @@ +package com.example.todolist.model.request + +data class ModifyTaskRequest( + val id: Int, + val title: String, + val contents: String, + val author: String = "Android", + val status: String +) diff --git a/android/app/src/main/java/com/example/todolist/model/request/MoveTaskRequest.kt b/android/app/src/main/java/com/example/todolist/model/request/MoveTaskRequest.kt new file mode 100644 index 000000000..c8bd8c212 --- /dev/null +++ b/android/app/src/main/java/com/example/todolist/model/request/MoveTaskRequest.kt @@ -0,0 +1,8 @@ +package com.example.todolist.model.request + +import com.example.todolist.model.Status + +data class MoveTaskRequest( + val beforeStatus: Status, + val nowStatus: Status +) diff --git a/android/app/src/main/java/com/example/todolist/model/request/TaskRequest.kt b/android/app/src/main/java/com/example/todolist/model/request/TaskRequest.kt new file mode 100644 index 000000000..61fcac171 --- /dev/null +++ b/android/app/src/main/java/com/example/todolist/model/request/TaskRequest.kt @@ -0,0 +1,8 @@ +package com.example.todolist.model.request + +data class TaskRequest( + val title: String, + val contents: String, + val status: String, + val author: String +) diff --git a/android/app/src/main/java/com/example/todolist/model/response/TasksResponse.kt b/android/app/src/main/java/com/example/todolist/model/response/TasksResponse.kt index 25ec6896d..c8a0dda76 100644 --- a/android/app/src/main/java/com/example/todolist/model/response/TasksResponse.kt +++ b/android/app/src/main/java/com/example/todolist/model/response/TasksResponse.kt @@ -1,15 +1,28 @@ -package com.example.todolist.model +package com.example.todolist.model.response + +import com.example.todolist.model.Status +import com.google.gson.annotations.SerializedName data class TasksResponse( + @SerializedName("todoCollection") val todo: MutableList, + @SerializedName("inProgressCollection") val inProgress: MutableList, + @SerializedName("doneCollection") val done: MutableList, ) +data class CommonResponse( + val status: Int, + @SerializedName("resources") + val taskDetailResponse: TaskDetailResponse, +) + data class TaskDetailResponse( val id: Int, val title: String, - val content: String, + val contents: String, val status: Status, val author: String = "Android", + val updateDateTime: String? = null, ) \ No newline at end of file diff --git a/android/app/src/main/java/com/example/todolist/network/Result.kt b/android/app/src/main/java/com/example/todolist/network/Result.kt new file mode 100644 index 000000000..1bc88db84 --- /dev/null +++ b/android/app/src/main/java/com/example/todolist/network/Result.kt @@ -0,0 +1,6 @@ +package com.example.todolist.network + +sealed class Result { + data class Success(val data: T) : Result() + data class Error(val error: String) : Result() +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/todolist/network/RetrofitAPI.kt b/android/app/src/main/java/com/example/todolist/network/RetrofitAPI.kt new file mode 100644 index 000000000..4caaabef1 --- /dev/null +++ b/android/app/src/main/java/com/example/todolist/network/RetrofitAPI.kt @@ -0,0 +1,30 @@ +package com.example.todolist.network + +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +object RetrofitAPI { + private const val BASE_URL = "http://www.louie-03.com/" + + private val okHttpClient: OkHttpClient by lazy { + OkHttpClient.Builder() + .addInterceptor(HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + }) + .build() + } + + private val retrofit: Retrofit by lazy { + Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build() + } + + val service: Service by lazy { + retrofit.create(Service::class.java) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/todolist/network/Service.kt b/android/app/src/main/java/com/example/todolist/network/Service.kt new file mode 100644 index 000000000..222fc93d0 --- /dev/null +++ b/android/app/src/main/java/com/example/todolist/network/Service.kt @@ -0,0 +1,38 @@ +package com.example.todolist.network + +import com.example.todolist.model.History +import com.example.todolist.model.Task +import com.example.todolist.model.request.ModifyTaskRequest +import com.example.todolist.model.request.MoveTaskRequest +import com.example.todolist.model.response.CommonResponse +import com.example.todolist.model.response.TasksResponse +import retrofit2.Response +import retrofit2.http.* + +interface Service { + + @Headers("Content-Type: application/json") + @GET("cards") + suspend fun loadTasks(): Response + + @Headers("Content-Type: application/json") + @POST("cards") + suspend fun saveTask(@Body cardInfo: Task): Response + + @Headers("Content-Type: application/json") + @PATCH("cards/{id}") + suspend fun modifyTask( + @Path("id") id: Int, + @Body modifyTaskRequest: ModifyTaskRequest + ): Response + + @Headers("Content-Type: application/json") + @GET("activity-logs") + suspend fun loadHistory(): Response> + + @DELETE("cards/{id}") + suspend fun deleteTask(@Path("id") id: Int): Response + + @PATCH("cards/{id}/move") + suspend fun moveTask(@Path("id") id: Int, @Body request: MoveTaskRequest): Response +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/todolist/repository/TaskDataSource.kt b/android/app/src/main/java/com/example/todolist/repository/TaskDataSource.kt new file mode 100644 index 000000000..bee814e25 --- /dev/null +++ b/android/app/src/main/java/com/example/todolist/repository/TaskDataSource.kt @@ -0,0 +1,25 @@ +package com.example.todolist.repository + +import com.example.todolist.model.History +import com.example.todolist.model.Status +import com.example.todolist.model.Task +import com.example.todolist.model.request.ModifyTaskRequest +import com.example.todolist.model.request.MoveTaskRequest +import com.example.todolist.model.response.CommonResponse +import com.example.todolist.model.response.TaskDetailResponse +import com.example.todolist.model.response.TasksResponse + +interface TaskDataSource { + + suspend fun loadTasks(): TasksResponse? + + suspend fun addTask(cardData: Task): CommonResponse? + + suspend fun modifyTask(modifyTaskRequest: ModifyTaskRequest): CommonResponse? + + suspend fun loadHistory(): List? + + suspend fun deleteTask(id: Int): CommonResponse? + + suspend fun moveTask(request: MoveTaskRequest, id: Int): CommonResponse? +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/todolist/repository/TaskRemoteDataSource.kt b/android/app/src/main/java/com/example/todolist/repository/TaskRemoteDataSource.kt new file mode 100644 index 000000000..ab4cd1d37 --- /dev/null +++ b/android/app/src/main/java/com/example/todolist/repository/TaskRemoteDataSource.kt @@ -0,0 +1,44 @@ +package com.example.todolist.repository + +import com.example.todolist.model.History +import com.example.todolist.model.Status +import com.example.todolist.model.Task +import com.example.todolist.model.request.ModifyTaskRequest +import com.example.todolist.model.request.MoveTaskRequest +import com.example.todolist.model.response.CommonResponse +import com.example.todolist.model.response.TaskDetailResponse +import com.example.todolist.model.response.TasksResponse +import com.example.todolist.network.RetrofitAPI + +class TaskRemoteDataSource : TaskDataSource { + + override suspend fun loadTasks(): TasksResponse? { + val response = RetrofitAPI.service.loadTasks() + return if (response.isSuccessful) response.body() else null + } + + override suspend fun addTask(cardData: Task): CommonResponse? { + val response = RetrofitAPI.service.saveTask(cardData) + return if (response.isSuccessful) response.body() else null + } + + override suspend fun modifyTask(modifyTaskRequest: ModifyTaskRequest): CommonResponse? { + val response = RetrofitAPI.service.modifyTask(modifyTaskRequest.id, modifyTaskRequest) + return if (response.isSuccessful) response.body() else null + } + + override suspend fun loadHistory(): List? { + val response = RetrofitAPI.service.loadHistory() + return if (response.isSuccessful) response.body() else null + } + + override suspend fun deleteTask(id: Int): CommonResponse? { + val response = RetrofitAPI.service.deleteTask(id) + return if (response.isSuccessful) response.body() else null + } + + override suspend fun moveTask(request: MoveTaskRequest, id: Int): CommonResponse? { + val response = RetrofitAPI.service.moveTask(id, request) + return if (response.isSuccessful) response.body() else null + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/todolist/repository/TaskRemoteRepository.kt b/android/app/src/main/java/com/example/todolist/repository/TaskRemoteRepository.kt new file mode 100644 index 000000000..136b7469c --- /dev/null +++ b/android/app/src/main/java/com/example/todolist/repository/TaskRemoteRepository.kt @@ -0,0 +1,64 @@ +package com.example.todolist.repository + +import com.example.todolist.model.History +import com.example.todolist.model.Status +import com.example.todolist.network.Result +import com.example.todolist.model.Task +import com.example.todolist.model.request.ModifyTaskRequest +import com.example.todolist.model.request.MoveTaskRequest +import com.example.todolist.model.response.CommonResponse +import com.example.todolist.model.response.TaskDetailResponse +import com.example.todolist.model.response.TasksResponse + +class TaskRemoteRepository( + private val taskRemoteDataSource: TaskRemoteDataSource, +) { + + suspend fun loadTask(): Result { + val response = taskRemoteDataSource.loadTasks() + response?.let { + return Result.Success(it) + } + return Result.Error("error") + } + + suspend fun addTask(cardData: Task): Result { + val response = taskRemoteDataSource.addTask(cardData) + response?.let { + return Result.Success(it) + } + return Result.Error("error") + } + + suspend fun modifyTask(modifyTaskRequest: ModifyTaskRequest): Result { + val response = taskRemoteDataSource.modifyTask(modifyTaskRequest) + response?.let { + return Result.Success(it) + } + return Result.Error("error") + } + + suspend fun loadHistory(): Result> { + val response = taskRemoteDataSource.loadHistory() + response?.let { + return Result.Success(it) + } + return Result.Error("error") + } + + suspend fun deleteTask(id: Int): Result { + val response = taskRemoteDataSource.deleteTask(id) + response?.let { + return Result.Success(it.taskDetailResponse) + } + return Result.Error("error") + } + + suspend fun moveTask(task: TaskDetailResponse, status: Status): Result { + val response = taskRemoteDataSource.moveTask(MoveTaskRequest(task.status, status), task.id) + response?.let { + return Result.Success(it.taskDetailResponse) + } + return Result.Error("error") + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/todolist/repository/TaskRepository.kt b/android/app/src/main/java/com/example/todolist/repository/TaskRepository.kt index 7ebdf038a..0a3aba239 100644 --- a/android/app/src/main/java/com/example/todolist/repository/TaskRepository.kt +++ b/android/app/src/main/java/com/example/todolist/repository/TaskRepository.kt @@ -1,6 +1,9 @@ package com.example.todolist.repository import com.example.todolist.model.* +import com.example.todolist.model.response.TaskDetailResponse +import com.example.todolist.model.response.TasksResponse +import java.util.* class TaskRepository { @@ -10,32 +13,43 @@ class TaskRepository { private val tasks = TasksResponse( mutableListOf( TaskDetailResponse(1, "GitHub 공부하기", "add, commit, push", Status.TODO, "Android"), - TaskDetailResponse(2, + TaskDetailResponse( + 2, "블로그에 포스팅할 것", "• GitHub 공부내용\n• 모던 자바스크립트 1장 공부내용", Status.TODO, - "Android"), - TaskDetailResponse(3, "HTML/CSS", "input 태그 실습", Status.TODO, "Android")), + "Android" + ), + TaskDetailResponse(3, "HTML/CSS", "input 태그 실습", Status.TODO, "Android") + ), mutableListOf( - TaskDetailResponse(1, + TaskDetailResponse( + 4, "GitHub 공부하기", "add, commit, push", Status.IN_PROGRESS, - "Android"), - TaskDetailResponse(2, + "Android" + ), + TaskDetailResponse( + 5, "블로그에 포스팅할 것", "• GitHub 공부내용\n• 모던 자바스크립트 1장 공부내용", Status.IN_PROGRESS, - "Android"), - TaskDetailResponse(3, "HTML/CSS", "input 태그 실습", Status.IN_PROGRESS, "Android")), + "Android" + ), + TaskDetailResponse(6, "HTML/CSS", "input 태그 실습", Status.IN_PROGRESS, "Android") + ), mutableListOf( - TaskDetailResponse(1, "GitHub 공부하기", "add, commit, push", Status.DONE, "Android"), - TaskDetailResponse(2, + TaskDetailResponse(7, "GitHub 공부하기", "add, commit, push", Status.DONE, "Android"), + TaskDetailResponse( + 8, "블로그에 포스팅할 것", "• GitHub 공부내용\n• 모던 자바스크립트 1장 공부내용", Status.DONE, - "Android"), - TaskDetailResponse(3, "HTML/CSS", "input 태그 실습", Status.DONE, "Android")), + "Android" + ), + TaskDetailResponse(9, "HTML/CSS", "input 태그 실습", Status.DONE, "Android") + ), ) fun getTasks(): TasksResponse { @@ -44,28 +58,91 @@ class TaskRepository { fun addTask(task: Task): TasksResponse { when (task.status) { - Status.TODO -> tasks.todo.add(TaskDetailResponse(todoIndex++, - task.title, - task.content, - task.status)) - Status.IN_PROGRESS -> tasks.inProgress.add(TaskDetailResponse(inProgressIndex++, - task.title, - task.content, - task.status)) - Status.DONE -> tasks.done.add(TaskDetailResponse(doneIndex++, - task.title, - task.content, - task.status)) + Status.TODO -> tasks.todo.add( + 0, TaskDetailResponse( + todoIndex++, + task.title, + task.contents, + task.status + ) + ) + Status.IN_PROGRESS -> tasks.inProgress.add( + 0, TaskDetailResponse( + inProgressIndex++, + task.title, + task.contents, + task.status + ) + ) + Status.DONE -> tasks.done.add( + 0, TaskDetailResponse( + doneIndex++, + task.title, + task.contents, + task.status + ) + ) + } + return tasks + } + + fun updateTask(task: TaskDetailResponse): TasksResponse { + when (task.status) { + Status.TODO -> { + val originalTask = tasks.todo.find { task.id == it.id } + val index = tasks.todo.indexOf(originalTask) + tasks.todo[index] = task + } + Status.IN_PROGRESS -> { + val originalTask = tasks.inProgress.find { task.id == it.id } + val index = tasks.inProgress.indexOf(originalTask) + tasks.inProgress[index] = task + } + Status.DONE -> { + val originalTask = tasks.done.find { task.id == it.id } + val index = tasks.done.indexOf(originalTask) + tasks.done[index] = task + } + } + return tasks + } + + fun moveDone(task: TaskDetailResponse): TasksResponse { + if (task.status != Status.DONE) { + tasks.done.add(0, task.copy(status = Status.DONE)) + deleteTask(task) + } + return tasks + } + + fun deleteTask(task: TaskDetailResponse): TasksResponse { + when (task.status) { + Status.TODO -> tasks.todo.remove(task) + Status.IN_PROGRESS -> tasks.inProgress.remove(task) + else -> tasks.done.remove(task) + } + return tasks + } + + fun swap( + currentList: List, + fromPosition: Int, + toPosition: Int, + ): TasksResponse { + when (currentList) { + tasks.todo -> Collections.swap(tasks.todo, fromPosition, toPosition) + tasks.inProgress -> Collections.swap(tasks.inProgress, fromPosition, toPosition) + tasks.done -> Collections.swap(tasks.done, fromPosition, toPosition) } return tasks } fun getHistory(): List { return listOf( - History(1, ActionType.MOVE, "HTML/CSS공부하기", "해야할 일", "하고 있는 일", "2022-04-05 21:19:00"), - History(2, ActionType.ADD, "HTML/CSS공부하기", "해야할 일", null, "2022-04-05 21:05:03"), - History(3, ActionType.REMOVE, "HTML/CSS공부하기", "해야할 일", null, "2022-04-05 21:05:03"), - History(4, ActionType.UPDATE, "HTML/CSS공부하기", "해야할 일", null, "2022-04-05 21:05:03"), + History(4, ActionType.MOVE, "HTML/CSS공부하기", "해야할 일", "하고 있는 일", "2022-04-05 21:19:00"), + History(3, ActionType.ADD, "HTML/CSS공부하기", "해야할 일", null, "2022-04-05 21:05:03"), + History(2, ActionType.REMOVE, "HTML/CSS공부하기", "해야할 일", null, "2022-04-05 21:05:03"), + History(1, ActionType.UPDATE, "HTML/CSS공부하기", "해야할 일", null, "2022-04-05 21:05:03"), ) } } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/todolist/ui/DragListener.kt b/android/app/src/main/java/com/example/todolist/ui/DragListener.kt new file mode 100644 index 000000000..6715bbc47 --- /dev/null +++ b/android/app/src/main/java/com/example/todolist/ui/DragListener.kt @@ -0,0 +1,78 @@ +package com.example.todolist.ui + +import android.view.DragEvent +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.example.todolist.R + +class DragListener : View.OnDragListener { + + override fun onDrag(view: View, event: DragEvent): Boolean { + when (event.action) { + DragEvent.ACTION_DROP -> { + var positionTarget = -1 + val viewSource = event.localState as View? + val viewId = view.id + val taskItem = R.id.cl_main + val rvTodo = R.id.rv_todo + val rvInProgress = R.id.rv_in_progress + val rvDone = R.id.rv_done + + when (viewId) { + taskItem, rvTodo, rvInProgress, rvDone -> { + val target: RecyclerView + when (viewId) { + rvTodo -> target = view.rootView.findViewById(rvTodo) as RecyclerView + rvInProgress -> target = + view.rootView.findViewById(rvInProgress) as RecyclerView + rvDone -> target = view.rootView.findViewById(rvDone) as RecyclerView + else -> { + target = view.parent as RecyclerView + positionTarget = view.tag as Int + } + } + + if (viewSource != null) { + val source = viewSource.parent as RecyclerView + val adapterSource = source.adapter as TaskAdapter + val adapterTarget = target.adapter as TaskAdapter + val positionSource = source.getChildAdapterPosition(viewSource) + val sourceData = adapterSource.currentList[positionSource] + if (adapterSource == adapterTarget) { + if (positionTarget < 0) { + adapterSource.remove(positionSource, sourceData) + when (target.id) { + rvTodo -> adapterTarget.add(1, -1, sourceData) + rvInProgress -> adapterTarget.add(2, -1, sourceData) + rvDone -> adapterTarget.add(3, -1, sourceData) + } + return true + } + + val targetPosition = target.getChildAdapterPosition(view) + adapterTarget.onItemMove(positionSource, targetPosition) + return true + } + + adapterSource.remove(positionSource, sourceData) + if (positionTarget >= 0) { + when (target.id) { + rvTodo -> adapterTarget.add(1, positionTarget, sourceData) + rvInProgress -> adapterTarget.add(2, positionTarget, sourceData) + rvDone -> adapterTarget.add(3, positionTarget, sourceData) + } + } else { + when (target.id) { + rvTodo -> adapterTarget.add(1, -1, sourceData) + rvInProgress -> adapterTarget.add(2, -1, sourceData) + rvDone -> adapterTarget.add(3, -1, sourceData) + } + } + } + } + } + } + } + return true + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/todolist/ui/ItemTouchCallback.kt b/android/app/src/main/java/com/example/todolist/ui/ItemTouchCallback.kt new file mode 100644 index 000000000..83b21af82 --- /dev/null +++ b/android/app/src/main/java/com/example/todolist/ui/ItemTouchCallback.kt @@ -0,0 +1,105 @@ +package com.example.todolist.ui + +import android.graphics.Canvas +import android.view.View +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import kotlin.math.max +import kotlin.math.min + +class ItemTouchCallback(private var clamp: Float) : ItemTouchHelper.Callback() { + + private var currentPosition: Int? = null + private var previousPosition: Int? = null + private var currentDX = 0f + + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + ): Int { + return makeMovementFlags( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, + ItemTouchHelper.START or ItemTouchHelper.END + ) + } + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder, + ): Boolean = false + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} + + override fun getSwipeEscapeVelocity(defaultValue: Float): Float { + return defaultValue * 10 + } + + override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float { + val isClamped = (viewHolder as TaskAdapter.TaskViewHolder).getTag() + viewHolder.setTag(!isClamped && currentDX <= -clamp) + return 2f + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + currentDX = 0f + previousPosition = viewHolder.adapterPosition + getDefaultUIUtil().clearView((viewHolder as TaskAdapter.TaskViewHolder).getView()) + } + + override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { + viewHolder?.let { + currentPosition = viewHolder.adapterPosition + getDefaultUIUtil().onSelected((viewHolder as TaskAdapter.TaskViewHolder).getView()) + } + } + + override fun onChildDraw( + c: Canvas, + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + dX: Float, + dY: Float, + actionState: Int, + isCurrentlyActive: Boolean, + ) { + val taskViewHolder = viewHolder as TaskAdapter.TaskViewHolder + if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { + val view = taskViewHolder.getView() + if (dX < 0) taskViewHolder.setVisibility(View.VISIBLE) + else if (dX > 0) taskViewHolder.setVisibility(View.GONE) + val isClamped = taskViewHolder.getTag() + val x = clampViewPositionHorizontal(view, dX, isClamped, isCurrentlyActive) + + currentDX = x + getDefaultUIUtil().onDraw(c, recyclerView, view, x, dY, actionState, isCurrentlyActive) + } + } + + private fun clampViewPositionHorizontal( + view: View, + dX: Float, + isClamped: Boolean, + isCurrentlyActive: Boolean, + ): Float { + val min: Float = -view.width.toFloat() / 2 + val max: Float = 0f + val x = if (isClamped) { + if (isCurrentlyActive) dX - clamp else -clamp + } else { + dX + } + return min(max(min, x), max) + } + + fun removePreviousClamp(recyclerView: RecyclerView) { + if (currentPosition == previousPosition) return + previousPosition?.let { + val viewHolder = recyclerView.findViewHolderForAdapterPosition(it) ?: return + val taskViewHolder = viewHolder as TaskAdapter.TaskViewHolder + taskViewHolder.getView().translationX = 0f + taskViewHolder.setTag(false) + previousPosition = null + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/todolist/ui/ItemTouchHelperListener.kt b/android/app/src/main/java/com/example/todolist/ui/ItemTouchHelperListener.kt new file mode 100644 index 000000000..083a8508e --- /dev/null +++ b/android/app/src/main/java/com/example/todolist/ui/ItemTouchHelperListener.kt @@ -0,0 +1,13 @@ +package com.example.todolist.ui + +import com.example.todolist.model.response.TaskDetailResponse + +interface ItemTouchHelperListener { + fun onItemMove(fromPosition: Int, toPosition: Int): Boolean + + fun onItemSwipe(position: Int) + + fun add(type: Int, index: Int, task: TaskDetailResponse) + + fun remove(index: Int, task: TaskDetailResponse) +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/todolist/ui/MainActivity.kt b/android/app/src/main/java/com/example/todolist/ui/MainActivity.kt index 5d31f5f0b..7ebed5f42 100644 --- a/android/app/src/main/java/com/example/todolist/ui/MainActivity.kt +++ b/android/app/src/main/java/com/example/todolist/ui/MainActivity.kt @@ -2,23 +2,26 @@ package com.example.todolist.ui import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import android.util.Log import android.view.* -import android.widget.PopupMenu import android.widget.Toast import androidx.activity.viewModels import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.ItemTouchHelper import com.example.todolist.R import com.example.todolist.databinding.ActivityMainBinding import com.example.todolist.model.Status +import com.example.todolist.model.response.TaskDetailResponse import com.example.todolist.ui.common.ViewModelFactory -class MainActivity : AppCompatActivity(), PopupMenu.OnMenuItemClickListener { - private val viewModel: TaskViewModel by viewModels { ViewModelFactory() } +class MainActivity : AppCompatActivity(), TaskAdapter.DialogListener { + private val viewModel: TaskRemoteViewModel by viewModels { ViewModelFactory() } private lateinit var binding: ActivityMainBinding private val historyAdapter: HistoryAdapter by lazy { HistoryAdapter() } - private val toDoAdapter: TaskAdapter by lazy { TaskAdapter() } - private val inProgressAdapter: TaskAdapter by lazy { TaskAdapter() } - private val doneAdapter: TaskAdapter by lazy { TaskAdapter() } + private val toDoAdapter: TaskAdapter by lazy { TaskAdapter(viewModel, this) } + private val inProgressAdapter: TaskAdapter by lazy { TaskAdapter(viewModel, this) } + private val doneAdapter: TaskAdapter by lazy { TaskAdapter(viewModel, this) } + private val clamp: Float = 170f override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -32,40 +35,79 @@ class MainActivity : AppCompatActivity(), PopupMenu.OnMenuItemClickListener { historyAdapter.submitList(histories) } + viewModel.error.observe(this) { + Toast.makeText(this, getString(R.string.error_message), Toast.LENGTH_SHORT).show() + } + with(binding) { includeTodo.btnAdd.setOnClickListener { - TaskDialogFragment(Status.TODO).show(supportFragmentManager, "todoDialog") + NewTaskDialogFragment(Status.TODO).show(supportFragmentManager, "todoDialog") } includeInProgress.btnAdd.setOnClickListener { - TaskDialogFragment(Status.IN_PROGRESS).show(supportFragmentManager, "inProgressDialog") + NewTaskDialogFragment(Status.IN_PROGRESS).show( + supportFragmentManager, + "inProgressDialog" + ) } includeDone.btnAdd.setOnClickListener { - TaskDialogFragment(Status.DONE).show(supportFragmentManager, "doneDialog") + NewTaskDialogFragment(Status.DONE).show(supportFragmentManager, "doneDialog") } } - binding.includeTodo.rvTodo.adapter = toDoAdapter + val todoItemTouchCallback = ItemTouchCallback(clamp) + with(binding.includeTodo.rvTodo) { + ItemTouchHelper(todoItemTouchCallback).attachToRecyclerView(this) + adapter = toDoAdapter + setOnDragListener(DragListener()) + setOnTouchListener { view, _ -> + todoItemTouchCallback.removePreviousClamp(this) + view.performClick() + false + } + } viewModel.todoTask.observe(this) { todoTask -> - toDoAdapter.submitList(todoTask) + toDoAdapter.submitList(todoTask.toList()) } - binding.includeInProgress.rvInProgress.adapter = inProgressAdapter + val inProgressItemTouchCallback = ItemTouchCallback(clamp) + with(binding.includeInProgress.rvInProgress) { + ItemTouchHelper(inProgressItemTouchCallback).attachToRecyclerView(this) + adapter = inProgressAdapter + setOnDragListener(DragListener()) + setOnTouchListener { view, _ -> + inProgressItemTouchCallback.removePreviousClamp(this) + view.performClick() + false + } + } viewModel.inProgressTask.observe(this) { inProgressTask -> - inProgressAdapter.submitList(inProgressTask) + inProgressAdapter.submitList(inProgressTask.toList()) } + + binding.includeDone.rvDone.adapter = doneAdapter + val doneItemTouchCallback = ItemTouchCallback(clamp) + with(binding.includeDone.rvDone) { + ItemTouchHelper(doneItemTouchCallback).attachToRecyclerView(this) + adapter = doneAdapter + setOnDragListener(DragListener()) + setOnTouchListener { view, _ -> + doneItemTouchCallback.removePreviousClamp(this) + view.performClick() + false + } + } viewModel.doneTask.observe(this) { doneTask -> - doneAdapter.submitList(doneTask) + doneAdapter.submitList(doneTask.toList()) } - } private fun onDrawerEvent() { binding.btnDrawer.setOnClickListener { - viewModel.loadDummyData() + viewModel.loadHistory() binding.dlDrawer.openDrawer(Gravity.RIGHT) } @@ -74,20 +116,7 @@ class MainActivity : AppCompatActivity(), PopupMenu.OnMenuItemClickListener { } } - private fun showPopup(view: View) { - val wrapper = ContextThemeWrapper(this, R.style.PopupWindow) - val popup = PopupMenu(wrapper, view) - popup.menuInflater.inflate(R.menu.popup, popup.menu) - popup.setOnMenuItemClickListener(this) - popup.show() - } - - override fun onMenuItemClick(item: MenuItem?): Boolean { - when (item?.itemId) { - R.id.popup_go_done -> Toast.makeText(this, "popup_go_done", Toast.LENGTH_LONG).show() - R.id.popup_modify -> Toast.makeText(this, "popup_modify", Toast.LENGTH_LONG).show() - R.id.popup_delete -> Toast.makeText(this, "popup_delete", Toast.LENGTH_LONG).show() - } - return item != null + override fun updateDialog(task: TaskDetailResponse) { + UpdateTaskDialogFragment(task).show(supportFragmentManager, "updateDialog") } } diff --git a/android/app/src/main/java/com/example/todolist/ui/TaskDialogFragment.kt b/android/app/src/main/java/com/example/todolist/ui/NewTaskDialogFragment.kt similarity index 85% rename from android/app/src/main/java/com/example/todolist/ui/TaskDialogFragment.kt rename to android/app/src/main/java/com/example/todolist/ui/NewTaskDialogFragment.kt index 9f9a70972..ed5683182 100644 --- a/android/app/src/main/java/com/example/todolist/ui/TaskDialogFragment.kt +++ b/android/app/src/main/java/com/example/todolist/ui/NewTaskDialogFragment.kt @@ -5,6 +5,7 @@ import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.text.Editable import android.text.TextWatcher +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -15,9 +16,9 @@ import com.example.todolist.databinding.DialogNewCardBinding import com.example.todolist.model.Status import com.example.todolist.model.Task -class TaskDialogFragment(private val status: Status) : DialogFragment() { +class NewTaskDialogFragment(private val status: Status) : DialogFragment() { private lateinit var binding: DialogNewCardBinding - private val viewModel: TaskViewModel by activityViewModels() + private val viewModel: TaskRemoteViewModel by activityViewModels() private var titleFlag = false private var contentsFlag = false @@ -42,30 +43,27 @@ class TaskDialogFragment(private val status: Status) : DialogFragment() { binding.btnRegister.setOnClickListener { when (status) { - Status.TODO -> { - val task = Task( + Status.TODO -> viewModel.addTask( + Task( binding.etTitle.text.toString(), binding.etContents.text.toString(), Status.TODO ) - viewModel.addTodoTask(task) - } - Status.IN_PROGRESS -> { - val task = Task( + ) + Status.IN_PROGRESS -> viewModel.addTask( + Task( binding.etTitle.text.toString(), binding.etContents.text.toString(), Status.IN_PROGRESS ) - viewModel.addInProgressTask(task) - } - else -> { - val task = Task( + ) + Status.DONE -> viewModel.addTask( + Task( binding.etTitle.text.toString(), binding.etContents.text.toString(), Status.DONE ) - viewModel.addDoneTask(task) - } + ) } dismiss() } diff --git a/android/app/src/main/java/com/example/todolist/ui/TaskAdapter.kt b/android/app/src/main/java/com/example/todolist/ui/TaskAdapter.kt index 70647bd09..89957c213 100644 --- a/android/app/src/main/java/com/example/todolist/ui/TaskAdapter.kt +++ b/android/app/src/main/java/com/example/todolist/ui/TaskAdapter.kt @@ -1,14 +1,27 @@ package com.example.todolist.ui -import android.view.LayoutInflater -import android.view.ViewGroup +import android.os.Build +import android.view.* +import android.widget.PopupMenu +import androidx.annotation.RequiresApi import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import com.example.todolist.R import com.example.todolist.databinding.ItemTaskBinding -import com.example.todolist.model.TaskDetailResponse +import com.example.todolist.model.Status +import com.example.todolist.model.request.MoveTaskRequest +import com.example.todolist.model.response.TaskDetailResponse -class TaskAdapter : ListAdapter(TaskDiffCallback) { +class TaskAdapter( + private val viewModel: TaskRemoteViewModel, + private val listener: DialogListener, +) : ListAdapter(TaskDiffCallback), + ItemTouchHelperListener { + + interface DialogListener { + fun updateDialog(task: TaskDetailResponse) + } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TaskViewHolder { val binding = ItemTaskBinding.inflate(LayoutInflater.from(parent.context), parent, false) @@ -17,23 +30,107 @@ class TaskAdapter : ListAdapter( override fun onBindViewHolder(holder: TaskViewHolder, position: Int) { holder.bind(getItem(position)) + holder.itemView.tag = position } - class TaskViewHolder(private val binding: ItemTaskBinding) : - RecyclerView.ViewHolder(binding.root) { + inner class TaskViewHolder(private val binding: ItemTaskBinding) : + RecyclerView.ViewHolder(binding.root), PopupMenu.OnMenuItemClickListener, View.OnTouchListener { + + private lateinit var task: TaskDetailResponse + fun bind(task: TaskDetailResponse) { + this.task = task binding.task = task binding.executePendingBindings() + itemView.setOnTouchListener(this) + itemView.setOnDragListener(DragListener()) + binding.clDelete.setOnClickListener { onItemSwipe(layoutPosition) } + itemView.setOnLongClickListener { + showPopup(it) + true + } } + + private fun showPopup(view: View) { + val wrapper = ContextThemeWrapper(view.context, R.style.PopupWindow) + val popup = PopupMenu(wrapper, view) + popup.menuInflater.inflate(R.menu.menu_popup, popup.menu) + popup.setOnMenuItemClickListener(this) + popup.show() + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.popup_go_done -> viewModel.moveDone(task, Status.DONE) + R.id.popup_modify -> listener.updateDialog(task) + R.id.popup_delete -> viewModel.deleteTask(task) + } + return item != null + } + + fun getView(): View = binding.swipeView + + private fun getDeleteView(): View = binding.clDelete + + fun setVisibility(visibility: Int) { + getDeleteView().visibility = visibility + } + + fun getTag(): Boolean = binding.swipeView.tag as? Boolean ?: false + + fun setTag(isClamped: Boolean) { + binding.swipeView.tag = isClamped + } + + @RequiresApi(Build.VERSION_CODES.N) + override fun onTouch(view: View?, event: MotionEvent?): Boolean { + val shadowBuilder = View.DragShadowBuilder(view) + val isDrag = getTag() + when(event?.action) { + MotionEvent.ACTION_DOWN -> { } + MotionEvent.ACTION_MOVE -> { + if(!isDrag) { + view?.startDragAndDrop(null, shadowBuilder, view , 0) + return true + } + } + MotionEvent.ACTION_UP -> return true + } + view?.performClick() + return false + } + } + + override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean { + viewModel.swapTask(currentList, fromPosition, toPosition) + return true + } + + override fun onItemSwipe(position: Int) { + viewModel.deleteTask(getItem(position)) + } + + override fun add(type: Int, index: Int, task: TaskDetailResponse) { + viewModel.addTask(type, index, task) + } + + override fun remove(index: Int, task: TaskDetailResponse) { + viewModel.remove(index, task) } } object TaskDiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: TaskDetailResponse, newItem: TaskDetailResponse): Boolean { + override fun areItemsTheSame( + oldItem: TaskDetailResponse, + newItem: TaskDetailResponse, + ): Boolean { return oldItem.id == newItem.id } - override fun areContentsTheSame(oldItem: TaskDetailResponse, newItem: TaskDetailResponse): Boolean { + override fun areContentsTheSame( + oldItem: TaskDetailResponse, + newItem: TaskDetailResponse, + ): Boolean { return oldItem == newItem } diff --git a/android/app/src/main/java/com/example/todolist/ui/TaskRemoteViewModel.kt b/android/app/src/main/java/com/example/todolist/ui/TaskRemoteViewModel.kt new file mode 100644 index 000000000..fc3f23a99 --- /dev/null +++ b/android/app/src/main/java/com/example/todolist/ui/TaskRemoteViewModel.kt @@ -0,0 +1,263 @@ +package com.example.todolist.ui + +import androidx.lifecycle.LiveData +import com.example.todolist.network.Result +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.todolist.model.History +import com.example.todolist.model.Status +import com.example.todolist.model.Task +import com.example.todolist.model.request.ModifyTaskRequest +import com.example.todolist.model.response.TaskDetailResponse +import com.example.todolist.repository.TaskRemoteRepository +import kotlinx.coroutines.launch +import java.util.* + +class TaskRemoteViewModel(private val taskRemoteRepository: TaskRemoteRepository) : ViewModel() { + + private val _history = MutableLiveData>() + val history: LiveData> + get() = _history + + private var todoItem: MutableList = mutableListOf() + private val _todoTask = MutableLiveData>() + val todoTask: LiveData> + get() = _todoTask + + private var inProgressItem: MutableList = mutableListOf() + private val _inProgressTask = MutableLiveData>() + val inProgressTask: LiveData> + get() = _inProgressTask + + private var doneItem: MutableList = mutableListOf() + private val _doneTask = MutableLiveData>() + val doneTask: LiveData> + get() = _doneTask + + private val _error = MutableLiveData() + val error: LiveData + get() = _error + + init { + loadTasks() + } + + private fun loadTasks() { + viewModelScope.launch { + when (val tasks = taskRemoteRepository.loadTask()) { + is Result.Success -> { + todoItem = tasks.data.todo + _todoTask.value = tasks.data.todo + + inProgressItem = tasks.data.inProgress + _inProgressTask.value = tasks.data.inProgress + + doneItem = tasks.data.done + _doneTask.value = tasks.data.done + } + is Result.Error -> _error.value = tasks.error + } + } + } + + fun addTask(task: Task) { + viewModelScope.launch { + when (val tasks = taskRemoteRepository.addTask(task)) { + is Result.Success -> { + when (tasks.data.taskDetailResponse.status) { + Status.TODO -> { + todoItem.add(0, tasks.data.taskDetailResponse) + _todoTask.value = todoItem + } + Status.IN_PROGRESS -> { + inProgressItem.add(0, tasks.data.taskDetailResponse) + _inProgressTask.value = inProgressItem + } + Status.DONE -> { + doneItem.add(0, tasks.data.taskDetailResponse) + _doneTask.value = doneItem + } + } + } + is Result.Error -> _error.value = tasks.error + } + } + } + + fun modifyTask(modifyTaskRequest: ModifyTaskRequest) { + viewModelScope.launch { + when (val tasks = taskRemoteRepository.modifyTask(modifyTaskRequest)) { + is Result.Success -> { + when (tasks.data.taskDetailResponse.status) { + Status.TODO -> { + val originalTask = _todoTask.value?.find { resources -> + modifyTaskRequest.id == resources.id + } + _todoTask.value?.indexOf(originalTask)?.let { index -> + todoItem[index] = tasks.data.taskDetailResponse + _todoTask.value = todoItem + } + } + Status.IN_PROGRESS -> { + val originalTask = + _inProgressTask.value?.find { resources -> modifyTaskRequest.id == resources.id } + _inProgressTask.value?.indexOf(originalTask)?.let { index -> + inProgressItem[index] = tasks.data.taskDetailResponse + _inProgressTask.value = inProgressItem + } + } + Status.DONE -> { + val originalTask = + _doneTask.value?.find { resources -> modifyTaskRequest.id == resources.id } + _doneTask.value?.indexOf(originalTask)?.let { index -> + doneItem[index] = tasks.data.taskDetailResponse + _doneTask.value = doneItem + } + } + } + } + } + } + } + + fun loadHistory() { + viewModelScope.launch { + when (val history = taskRemoteRepository.loadHistory()) { + is Result.Success -> { + _history.value = history.data + } + is Result.Error -> { + _error.value = history.error + } + } + } + } + + fun deleteTask(task: TaskDetailResponse) { + viewModelScope.launch { + when (val deleteTask = taskRemoteRepository.deleteTask(task.id)) { + is Result.Success -> { + when (deleteTask.data.status) { + Status.TODO -> { + val originalTask = todoItem.find { it.id == deleteTask.data.id } + val index = todoItem.indexOf(originalTask) + if (index != -1) todoItem.removeAt(index) + _todoTask.value = todoItem + } + Status.IN_PROGRESS -> { + val originalTask = inProgressItem.find { it.id == deleteTask.data.id } + val index = inProgressItem.indexOf(originalTask) + if (index != -1) inProgressItem.removeAt(index) + _inProgressTask.value = inProgressItem + } + Status.DONE -> { + val originalTask = doneItem.find { it.id == deleteTask.data.id } + val index = doneItem.indexOf(originalTask) + if (index != -1) doneItem.removeAt(index) + _doneTask.value = doneItem + } + } + } + is Result.Error -> { + _error.value = deleteTask.error + } + } + } + } + + fun moveDone(task: TaskDetailResponse, status: Status) { + viewModelScope.launch { + when (val updateTask = taskRemoteRepository.moveTask(task, status)) { + is Result.Success -> { + when (task.status) { + Status.TODO -> { + todoItem.remove(task) + _todoTask.value = todoItem + } + Status.IN_PROGRESS -> { + inProgressItem.remove(task) + _inProgressTask.value = inProgressItem + } + Status.DONE -> { + doneItem.remove(task) + _doneTask.value = doneItem + } + } + + when (updateTask.data.status) { + Status.TODO -> { + todoItem.add(0, updateTask.data) + _todoTask.value = todoItem + } + Status.IN_PROGRESS -> { + inProgressItem.add(0, updateTask.data) + _inProgressTask.value = inProgressItem + } + Status.DONE -> { + doneItem.add(0, updateTask.data) + _doneTask.value = doneItem + } + } + } + is Result.Error -> { + _error.value = updateTask.error + } + } + } + } + + fun swapTask(currentList: List, fromPosition: Int, toPosition: Int) { + when (currentList) { + todoItem -> { + Collections.swap(todoItem, fromPosition, toPosition) + _todoTask.value = todoItem + } + inProgressItem -> { + Collections.swap(inProgressItem, fromPosition, toPosition) + _inProgressTask.value = inProgressItem + } + doneItem -> { + Collections.swap(doneItem, fromPosition, toPosition) + _doneTask.value = doneItem + } + } + } + + fun addTask(type: Int, index: Int, task: TaskDetailResponse) { + when(type) { + 1 -> { + if(index == -1) todoItem.add(task.copy(status = Status.TODO)) + else todoItem.add(index, task.copy(status = Status.TODO)) + _todoTask.value = todoItem + } + 2 -> { + if(index == -1) inProgressItem.add(task.copy(status = Status.IN_PROGRESS)) + else inProgressItem.add(index, task.copy(status = Status.IN_PROGRESS)) + _inProgressTask.value = inProgressItem + } + else -> { + if(index == -1) doneItem.add(task.copy(status = Status.DONE)) + else doneItem.add(index, task.copy(status = Status.DONE)) + _doneTask.value = doneItem + } + } + } + + fun remove(index: Int, task: TaskDetailResponse) { + when(task.status) { + Status.TODO -> { + todoItem.removeAt(index) + _todoTask.value = todoItem + } + Status.IN_PROGRESS -> { + inProgressItem.removeAt(index) + _inProgressTask.value = inProgressItem + } + else -> { + doneItem.removeAt(index) + _doneTask.value = doneItem + } + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/todolist/ui/TaskViewModel.kt b/android/app/src/main/java/com/example/todolist/ui/TaskViewModel.kt index cf8b27d61..f22923df1 100644 --- a/android/app/src/main/java/com/example/todolist/ui/TaskViewModel.kt +++ b/android/app/src/main/java/com/example/todolist/ui/TaskViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.example.todolist.model.* +import com.example.todolist.model.response.TaskDetailResponse import com.example.todolist.repository.TaskRepository class TaskViewModel(private val repository: TaskRepository) : ViewModel() { @@ -53,4 +54,40 @@ class TaskViewModel(private val repository: TaskRepository) : ViewModel() { val tasks = repository.addTask(task) _doneTask.value = tasks.done } + + fun moveDone(task: TaskDetailResponse) { + val tasks = repository.moveDone(task) + _todoTask.value = tasks.todo + _inProgressTask.value = tasks.inProgress + _doneTask.value = tasks.done + } + + fun deleteTask(task: TaskDetailResponse) { + val tasks = repository.deleteTask(task) + _todoTask.value = tasks.todo + _inProgressTask.value = tasks.inProgress + _doneTask.value = tasks.done + } + + fun updateTodoTask(task: TaskDetailResponse) { + val tasks = repository.updateTask(task) + _todoTask.value = tasks.todo + } + + fun updateInProgressTask(task: TaskDetailResponse) { + val tasks = repository.updateTask(task) + _inProgressTask.value = tasks.inProgress + } + + fun updateDoneTask(task: TaskDetailResponse) { + val tasks = repository.updateTask(task) + _doneTask.value = tasks.done + } + + fun swapTask(currentList: List, fromPosition: Int, toPosition: Int) { + val tasks = repository.swap(currentList, fromPosition, toPosition) + _todoTask.value = tasks.todo + _inProgressTask.value = tasks.inProgress + _doneTask.value = tasks.done + } } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/todolist/ui/UpdateTaskDialogFragment.kt b/android/app/src/main/java/com/example/todolist/ui/UpdateTaskDialogFragment.kt new file mode 100644 index 000000000..df9df53c4 --- /dev/null +++ b/android/app/src/main/java/com/example/todolist/ui/UpdateTaskDialogFragment.kt @@ -0,0 +1,93 @@ +package com.example.todolist.ui + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.Window +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import com.example.todolist.databinding.DialogUpdateCardBinding +import com.example.todolist.model.request.ModifyTaskRequest +import com.example.todolist.model.response.TaskDetailResponse + +class UpdateTaskDialogFragment(private val task: TaskDetailResponse) : DialogFragment() { + private lateinit var binding: DialogUpdateCardBinding + private val viewModel: TaskRemoteViewModel by activityViewModels() + private var titleFlag = false + private var contentsFlag = false + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + binding = DialogUpdateCardBinding.inflate(inflater, container, false) + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) // 다이얼로그의 곡선 주변에 배경색을 맞춰주는 코드 + dialog?.window?.requestFeature(Window.FEATURE_NO_TITLE) + dialog?.setCanceledOnTouchOutside(false) // 다이얼로그 외부의 영역 터치 시 취소 불가능 + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + binding.btnCancel.setOnClickListener { dismiss() } + + binding.etTitle.addTextChangedListener(titleListener) + binding.etContents.addTextChangedListener(contentsListener) + binding.etTitle.setText(task.title) + binding.etContents.setText(task.contents) + + binding.btnUpdate.setOnClickListener { + val modifyTaskRequest = ModifyTaskRequest( + task.id, + binding.etTitle.text.toString(), + binding.etContents.text.toString(), + "Android", + task.status.toString() + ) + viewModel.modifyTask(modifyTaskRequest) + dismiss() + } + } + + private val titleListener = object : TextWatcher { + override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} + + override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} + + override fun afterTextChanged(s: Editable?) { + if (s != null) { + titleFlag = when { + s.isEmpty() -> false + else -> true + } + } + flagCheck() + } + } + + private val contentsListener = object : TextWatcher { + override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} + + override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} + + override fun afterTextChanged(s: Editable?) { + if (s != null) { + contentsFlag = when { + s.isEmpty() -> false + else -> true + } + } + flagCheck() + } + } + + fun flagCheck() { + binding.btnUpdate.isEnabled = titleFlag && contentsFlag + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/todolist/ui/common/TextBindingAdapters.kt b/android/app/src/main/java/com/example/todolist/ui/common/TextBindingAdapters.kt index 5e92d4669..fea5bd71a 100644 --- a/android/app/src/main/java/com/example/todolist/ui/common/TextBindingAdapters.kt +++ b/android/app/src/main/java/com/example/todolist/ui/common/TextBindingAdapters.kt @@ -1,11 +1,10 @@ package com.example.todolist.ui.common -import android.os.Build -import android.text.Html +import android.annotation.SuppressLint import android.widget.TextView import androidx.databinding.BindingAdapter import com.example.todolist.R -import com.example.todolist.common.htmlToString +import com.example.todolist.common.htmlToSpanned import com.example.todolist.model.ActionType.* import com.example.todolist.model.History import java.text.SimpleDateFormat @@ -13,21 +12,30 @@ import java.text.SimpleDateFormat @BindingAdapter("formatting") fun stringFormat(view: TextView, history: History) { val (_, action, title, nowStatus, beforeStatus, _) = history + val now = when (nowStatus) { + "TODO" -> "해야할 일" + "IN_PROGRESS" -> "하고 있는 일" + else -> "완료한 일" + } + val before = when (beforeStatus) { + "TODO" -> "해야할 일" + "IN_PROGRESS" -> "하고 있는 일" + else -> "완료한 일" + } + when (action) { ADD -> view.text = - view.context.getString(R.string.action_default, nowStatus, title, "등록").htmlToString() + view.context.getString(R.string.action_default, now, title, "등록").htmlToSpanned() REMOVE -> view.text = - view.context.getString(R.string.action_default, nowStatus, title, "삭제").htmlToString() + view.context.getString(R.string.action_default, now, title, "삭제").htmlToSpanned() UPDATE -> view.text = - view.context.getString(R.string.action_default, nowStatus, title, "변경").htmlToString() - MOVE -> view.text = view.context.getString(R.string.action_move, - title, - beforeStatus, - nowStatus, - "이동").htmlToString() + view.context.getString(R.string.action_default, now, title, "변경").htmlToSpanned() + MOVE -> view.text = + view.context.getString(R.string.action_move, title, before, now, "이동").htmlToSpanned() } } +@SuppressLint("SimpleDateFormat") @BindingAdapter("formattingDate") fun dateFormat(view: TextView, stringDate: String) { val format = SimpleDateFormat(view.context.getString(R.string.date_format)) diff --git a/android/app/src/main/java/com/example/todolist/ui/common/ViewModelFactory.kt b/android/app/src/main/java/com/example/todolist/ui/common/ViewModelFactory.kt index aa51b866c..e5537782b 100644 --- a/android/app/src/main/java/com/example/todolist/ui/common/ViewModelFactory.kt +++ b/android/app/src/main/java/com/example/todolist/ui/common/ViewModelFactory.kt @@ -2,13 +2,20 @@ package com.example.todolist.ui.common import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.example.todolist.repository.TaskRemoteDataSource +import com.example.todolist.repository.TaskRemoteRepository import com.example.todolist.repository.TaskRepository +import com.example.todolist.ui.TaskRemoteViewModel import com.example.todolist.ui.TaskViewModel -class ViewModelFactory() : ViewModelProvider.Factory { +class ViewModelFactory : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return when { modelClass.isAssignableFrom(TaskViewModel::class.java) -> TaskViewModel(TaskRepository()) as T + modelClass.isAssignableFrom(TaskRemoteViewModel::class.java) -> { + val repository = TaskRemoteRepository(TaskRemoteDataSource()) + TaskRemoteViewModel(repository) as T + } else -> throw IllegalAccessException("Failed to create ViewModel: ${modelClass.name}") } } diff --git a/android/app/src/main/res/drawable/item_background_red.xml b/android/app/src/main/res/drawable/item_background_red.xml new file mode 100644 index 000000000..730dbefbf --- /dev/null +++ b/android/app/src/main/res/drawable/item_background_red.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout-sw720dp-land/activity_main.xml b/android/app/src/main/res/layout-sw720dp-land/activity_main.xml index cbd1cbe67..4e475800a 100644 --- a/android/app/src/main/res/layout-sw720dp-land/activity_main.xml +++ b/android/app/src/main/res/layout-sw720dp-land/activity_main.xml @@ -5,10 +5,9 @@ xmlns:tools="http://schemas.android.com/tools"> - + type="com.example.todolist.ui.TaskRemoteViewModel" /> + type="com.example.todolist.ui.TaskRemoteViewModel" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/item_history.xml b/android/app/src/main/res/layout/item_history.xml index 54f72ada9..7f0f04a46 100644 --- a/android/app/src/main/res/layout/item_history.xml +++ b/android/app/src/main/res/layout/item_history.xml @@ -49,7 +49,7 @@ + type="com.example.todolist.model.response.TaskDetailResponse" /> - - + android:layout_height="wrap_content"> - + app:layout_constraintTop_toTopOf="parent"> - + + + + app:layout_constraintTop_toTopOf="parent"> + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/view_done.xml b/android/app/src/main/res/layout/view_done.xml index fafe36514..b51b0a8c5 100644 --- a/android/app/src/main/res/layout/view_done.xml +++ b/android/app/src/main/res/layout/view_done.xml @@ -7,7 +7,7 @@ + type="com.example.todolist.ui.TaskRemoteViewModel" /> + type="com.example.todolist.ui.TaskRemoteViewModel" /> + type="com.example.todolist.ui.TaskRemoteViewModel" /> + android:title="@string/label_popup_delete" /> \ No newline at end of file diff --git a/android/app/src/main/res/values-night/strings.xml b/android/app/src/main/res/values-night/strings.xml index 5c89a2487..2d1edbd6f 100644 --- a/android/app/src/main/res/values-night/strings.xml +++ b/android/app/src/main/res/values-night/strings.xml @@ -8,19 +8,25 @@ <b>%s</b>에 <b>%s</b>를 <b>%s</b>하였습니다. <b>%s</b>를 <b>%s</b>에서 <b>%s</b>로 <b>%s</b>하였습니다. - yyyy-MM-dd HH:mm:ss + yyyy-MM-dd\'T\'HH:mm:ss 해야 할 일 + 하고 있는 일 + 완료한 일 add_button 새로운 카드 추가 + 카드 수정 등록 + 수정 취소 제목을 입력하세요 내용을 입력하세요 author by %s - 완료한 일로 이동 수정하기 - 삭제하기 + 삭제하기 + 삭제 + + 서버와의 연결이 실패했습니다. \ No newline at end of file diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index 4234664d2..08bee8ec2 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -14,4 +14,5 @@ #FFFFFFFF #0075DE #86C6FF + #FF0000 \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 8536906be..bd6c36dcf 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -7,14 +7,16 @@ <b>%s</b>에 <b>%s</b>를 <b>%s</b>하였습니다. <b>%s</b>를 <b>%s</b>에서 <b>%s</b>로 <b>%s</b>하였습니다. - yyyy-MM-dd HH:mm:ss + yyyy-MM-dd\'T\'HH:mm:ss 해야 할 일 하고 있는 일 완료한 일 add_button 새로운 카드 추가 + 카드 수정 등록 + 수정 취소 제목을 입력하세요 내용을 입력하세요 @@ -22,5 +24,8 @@ author by %s 완료한 일로 이동 수정하기 - 삭제하기 + 삭제하기 + 삭제 + + 서버와의 연결이 실패했습니다. \ No newline at end of file diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index b22d6c086..938bf6010 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -22,6 +22,10 @@ 14sp + +