mirror of
https://github.com/a-mayb3/KanbanCloneAndroid.git
synced 2026-03-21 18:15:38 +01:00
Compare commits
No commits in common. "409b7770e01c2ec06605a4cfa85fcf6c8ed7fb19" and "6a291355662c8cf298a0e5c090c909af52d2b3c0" have entirely different histories.
409b7770e0
...
6a29135566
39 changed files with 33 additions and 2995 deletions
5
.idea/.gitignore
generated
vendored
5
.idea/.gitignore
generated
vendored
|
|
@ -1,5 +0,0 @@
|
||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
|
|
@ -3,8 +3,10 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.campusaula.edbole.kanban_clone_android"
|
namespace = "com.campusaula.edbole.KanbanCloneAndroid"
|
||||||
compileSdk = 36
|
compileSdk {
|
||||||
|
version = release(36)
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.campusaula.edbole.KanbanCloneAndroid"
|
applicationId = "com.campusaula.edbole.KanbanCloneAndroid"
|
||||||
|
|
@ -37,19 +39,6 @@ dependencies {
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
implementation(libs.androidx.activity)
|
implementation(libs.androidx.activity)
|
||||||
implementation(libs.androidx.constraintlayout)
|
implementation(libs.androidx.constraintlayout)
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
|
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
|
|
||||||
|
|
||||||
implementation("com.squareup.okhttp3:okhttp:4.11.0")
|
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
|
|
||||||
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
|
||||||
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
|
||||||
|
|
||||||
implementation("com.auth0.android:jwtdecode:2.0.1")
|
|
||||||
implementation(libs.androidx.recyclerview)
|
|
||||||
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.campusaula.edbole.kanban_clone_android
|
package com.campusaula.edbole.KanbanCloneAndroid
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
|
@ -2,9 +2,6 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
|
@ -13,31 +10,9 @@
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.KanbanCloneAndroid"
|
android:theme="@style/Theme.KanbanCloneAndroid">
|
||||||
android:usesCleartextTraffic="true">
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.CreateProjectActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="false" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.ProjectEditActivity"
|
|
||||||
android:exported="false" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.CollaboratorAddActivity"
|
|
||||||
android:exported="false" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.TaskAddActivity"
|
|
||||||
android:exported="false" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.TaskEditActivity"
|
|
||||||
android:exported="false" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.ProjectDetailActivity"
|
|
||||||
android:exported="false" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.LoginActivity"
|
|
||||||
android:exported="false" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.MainActivity"
|
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.campusaula.edbole.KanbanCloneAndroid
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
setContentView(R.layout.activity_main)
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
||||||
|
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
package com.campusaula.edbole.kanban_clone_android.kanban
|
|
||||||
|
|
||||||
data class LoginResponse(
|
|
||||||
val message: String?,
|
|
||||||
val user: LoginUser?
|
|
||||||
)
|
|
||||||
|
|
||||||
data class LoginUser(
|
|
||||||
val id: String?,
|
|
||||||
val name: String?,
|
|
||||||
val email: String?
|
|
||||||
)
|
|
||||||
|
|
||||||
// Error response from the API (e.g. 401 Unauthorized)
|
|
||||||
data class ErrorResponse(
|
|
||||||
val detail: String?
|
|
||||||
)
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
package com.campusaula.edbole.kanban_clone_android.kanban
|
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
|
|
||||||
class Project{
|
|
||||||
val id: Int = 0
|
|
||||||
val name: String = ""
|
|
||||||
val description: String = ""
|
|
||||||
val users: List<User> = emptyList()
|
|
||||||
val tasks: List<Task> = emptyList()
|
|
||||||
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return "Project(id=$id, name='$name', description='$description', users=$users, tasks=$tasks)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ProjectBase(
|
|
||||||
@SerializedName("id")
|
|
||||||
val id : Int,
|
|
||||||
@SerializedName("name")
|
|
||||||
val name : String,
|
|
||||||
@SerializedName("description")
|
|
||||||
val description : String
|
|
||||||
)
|
|
||||||
|
|
||||||
data class ProjectCreate(
|
|
||||||
@SerializedName("name")
|
|
||||||
val name : String,
|
|
||||||
@SerializedName("description")
|
|
||||||
val description : String
|
|
||||||
)
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
package com.campusaula.edbole.kanban_clone_android.kanban
|
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
|
|
||||||
enum class TaskStatus {
|
|
||||||
@SerializedName("pending")
|
|
||||||
PENDING,
|
|
||||||
@SerializedName("in_progress")
|
|
||||||
IN_PROGRESS,
|
|
||||||
@SerializedName("completed")
|
|
||||||
COMPLETED,
|
|
||||||
@SerializedName("failed")
|
|
||||||
FAILED,
|
|
||||||
@SerializedName("stashed")
|
|
||||||
STASHED
|
|
||||||
}
|
|
||||||
|
|
||||||
class Task {
|
|
||||||
val id: Int = 0
|
|
||||||
val title: String = ""
|
|
||||||
val description: String = ""
|
|
||||||
var status: TaskStatus = TaskStatus.PENDING
|
|
||||||
val project: Project? = null
|
|
||||||
}
|
|
||||||
|
|
||||||
data class TaskBase(
|
|
||||||
@SerializedName("id")
|
|
||||||
val id : Int,
|
|
||||||
|
|
||||||
@SerializedName("title")
|
|
||||||
val title : String,
|
|
||||||
|
|
||||||
@SerializedName("description")
|
|
||||||
val description : String,
|
|
||||||
|
|
||||||
@SerializedName("status")
|
|
||||||
val status: TaskStatus
|
|
||||||
)
|
|
||||||
|
|
||||||
data class TaskUpdate(
|
|
||||||
@SerializedName("title")
|
|
||||||
val title : String?,
|
|
||||||
|
|
||||||
@SerializedName("description")
|
|
||||||
val description : String?,
|
|
||||||
|
|
||||||
@SerializedName("status")
|
|
||||||
val status: TaskStatus?
|
|
||||||
)
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
package com.campusaula.edbole.kanban_clone_android.kanban
|
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
|
|
||||||
class User {
|
|
||||||
val id: Int = 0
|
|
||||||
val name : String = ""
|
|
||||||
val email: String = ""
|
|
||||||
val password: String = ""
|
|
||||||
val projects: List<Project> = emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
data class UserBase (
|
|
||||||
@SerializedName("id")
|
|
||||||
val id: Int,
|
|
||||||
@SerializedName("name")
|
|
||||||
val name: String,
|
|
||||||
@SerializedName("email")
|
|
||||||
val email: String
|
|
||||||
)
|
|
||||||
|
|
||||||
data class ProjectUser(
|
|
||||||
@SerializedName("id")
|
|
||||||
val id: Int,
|
|
||||||
@SerializedName("name")
|
|
||||||
val name: String,
|
|
||||||
@SerializedName("email")
|
|
||||||
val email: String,
|
|
||||||
@SerializedName("projects")
|
|
||||||
val projects: List<ProjectBase>
|
|
||||||
)
|
|
||||||
|
|
||||||
data class UserLogin (
|
|
||||||
@SerializedName("email")
|
|
||||||
val email: String,
|
|
||||||
@SerializedName("password")
|
|
||||||
val password: String
|
|
||||||
)
|
|
||||||
|
|
||||||
data class UserCreate (
|
|
||||||
@SerializedName("name")
|
|
||||||
val name: String,
|
|
||||||
@SerializedName("email")
|
|
||||||
val email: String,
|
|
||||||
@SerializedName("password")
|
|
||||||
val password: String
|
|
||||||
)
|
|
||||||
|
|
||||||
data class UserUpdatePassword(
|
|
||||||
@SerializedName("password")
|
|
||||||
val password: String,
|
|
||||||
@SerializedName("new_password")
|
|
||||||
val newPassword: String
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
package com.campusaula.edbole.kanban_clone_android.network
|
|
||||||
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.kanban.*
|
|
||||||
import retrofit2.Response
|
|
||||||
import retrofit2.http.*
|
|
||||||
|
|
||||||
interface ApiService {
|
|
||||||
@GET("/ping")
|
|
||||||
suspend fun ping(): Response<Unit>
|
|
||||||
|
|
||||||
@POST("auth/login/")
|
|
||||||
suspend fun login(@Body userLogin: UserLogin): Response<LoginResponse>
|
|
||||||
|
|
||||||
@GET("me/logout/")
|
|
||||||
suspend fun logout(): Response<Unit>
|
|
||||||
|
|
||||||
@DELETE("me/delete-me/")
|
|
||||||
suspend fun deleteMe(): Response<Unit>
|
|
||||||
|
|
||||||
@GET("me/")
|
|
||||||
suspend fun getMe(): Response<ProjectUser>
|
|
||||||
|
|
||||||
@GET("users/{user_id}/")
|
|
||||||
suspend fun getUserById(@Path("user_id") userId: Int): Response<UserBase>
|
|
||||||
|
|
||||||
@GET("users/{user_id}/projects/")
|
|
||||||
suspend fun getUserProjectsByUserId(@Path("user_id") userId: Int): Response<List<ProjectBase>>
|
|
||||||
|
|
||||||
@POST("users/")
|
|
||||||
suspend fun createUser(@Body userLogin: UserCreate): Response<UserBase>
|
|
||||||
|
|
||||||
// Projects endpoints
|
|
||||||
|
|
||||||
@GET("projects/")
|
|
||||||
suspend fun getAllProjects(): Response<List<Project>>
|
|
||||||
|
|
||||||
@GET("projects/{project_id}/")
|
|
||||||
suspend fun getProjectById(@Path("project_id") projectId: Int): Response<Project>
|
|
||||||
|
|
||||||
@GET("projects/{project_id}/users/")
|
|
||||||
suspend fun getProjectUsers(@Path("project_id") projectId: Int): Response<List<UserBase>>
|
|
||||||
|
|
||||||
@POST("projects/{project_id}/users/")
|
|
||||||
suspend fun addProjectCollaborator(
|
|
||||||
@Path("project_id") projectId: Int,
|
|
||||||
@Body email: Map<String, String>
|
|
||||||
): Response<Unit>
|
|
||||||
|
|
||||||
@DELETE("projects/{project_id}/users/{user_id}/")
|
|
||||||
suspend fun removeProjectCollaborator(
|
|
||||||
@Path("project_id") projectId: Int,
|
|
||||||
@Path("user_id") userId: Int
|
|
||||||
): Response<Unit>
|
|
||||||
|
|
||||||
@POST("projects/")
|
|
||||||
suspend fun createProject(@Body projectCreate: ProjectCreate): Response<ProjectBase>
|
|
||||||
|
|
||||||
@PUT("projects/{project_id}/")
|
|
||||||
suspend fun updateProject(@Path("project_id") projectId: Int, @Body projectCreate: ProjectCreate): Response<ProjectBase>
|
|
||||||
|
|
||||||
@DELETE("projects/{project_id}/")
|
|
||||||
suspend fun deleteProject(@Path("project_id") projectId: Int): Response<Unit>
|
|
||||||
|
|
||||||
// Tasks endpoints
|
|
||||||
|
|
||||||
@GET("projects/{project_id}/tasks/")
|
|
||||||
suspend fun getProjectTasks(@Path("project_id") projectId: Int): Response<List<Task>>
|
|
||||||
|
|
||||||
@POST("projects/{project_id}/tasks/")
|
|
||||||
suspend fun createTask(@Path("project_id") projectId: Int, @Body taskBase: TaskBase): Response<TaskBase>
|
|
||||||
|
|
||||||
@PUT("projects/{project_id}/tasks/{task_id}/")
|
|
||||||
suspend fun updateProjectTask(
|
|
||||||
@Path("project_id") projectId: Int,
|
|
||||||
@Path("task_id") taskId: Int,
|
|
||||||
@Body taskUpdate: TaskUpdate
|
|
||||||
): Response<Unit>
|
|
||||||
|
|
||||||
@DELETE("projects/{project_id}/tasks/{task_id}/")
|
|
||||||
suspend fun deleteProjectTask(
|
|
||||||
@Path("project_id") projectId: Int,
|
|
||||||
@Path("task_id") taskId: Int
|
|
||||||
): Response<Unit>
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,186 +0,0 @@
|
||||||
package com.campusaula.edbole.kanban_clone_android.network
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import okhttp3.Cookie
|
|
||||||
import okhttp3.CookieJar
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
import android.util.Base64
|
|
||||||
import com.auth0.android.jwt.JWT
|
|
||||||
|
|
||||||
|
|
||||||
private data class StoredCookie(
|
|
||||||
val name: String,
|
|
||||||
val value: String,
|
|
||||||
val expiresAt: Long,
|
|
||||||
val domain: String,
|
|
||||||
val path: String,
|
|
||||||
val secure: Boolean,
|
|
||||||
val httpOnly: Boolean,
|
|
||||||
val hostOnly: Boolean,
|
|
||||||
val iat: Long? = null,
|
|
||||||
val exp: Long? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
class AuthCookieJar(
|
|
||||||
context: Context,
|
|
||||||
private val authCookieNames: Set<String> = setOf("access_token", "session", "auth", "auth_token", "JSESSIONID")
|
|
||||||
) : CookieJar {
|
|
||||||
|
|
||||||
private val prefs = context.applicationContext.getSharedPreferences("auth_cookie_prefs", Context.MODE_PRIVATE)
|
|
||||||
private val lock = Any()
|
|
||||||
private val gson = Gson()
|
|
||||||
private val knownHosts = ConcurrentHashMap.newKeySet<String>()
|
|
||||||
|
|
||||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
|
||||||
if (cookies.isEmpty()) return
|
|
||||||
val hostKey = url.host
|
|
||||||
synchronized(lock) {
|
|
||||||
val existing = prefs.getStringSet(hostKey, emptySet())?.toMutableSet() ?: mutableSetOf()
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
for (cookie in cookies) {
|
|
||||||
if (cookie.expiresAt <= now) continue
|
|
||||||
existing.removeIf { it.startsWith("${'$'}{cookie.name}|${'$'}{cookie.path}|") }
|
|
||||||
|
|
||||||
// default stored expiresAt comes from cookie.expiresAt (already in ms)
|
|
||||||
var storedExpires = cookie.expiresAt
|
|
||||||
var iatVal: Long? = null
|
|
||||||
var expVal: Long? = null
|
|
||||||
|
|
||||||
// if this is an auth cookie, try to decode JWT payload to obtain exp/iat
|
|
||||||
if (cookie.name in authCookieNames) {
|
|
||||||
try {
|
|
||||||
val (expSec, iatSec) = decodeJwtExpIat(cookie.value)
|
|
||||||
if (expSec != null) {
|
|
||||||
expVal = expSec
|
|
||||||
// convert to millis
|
|
||||||
storedExpires = expSec * 1000L
|
|
||||||
}
|
|
||||||
if (iatSec != null) {
|
|
||||||
iatVal = iatSec
|
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
|
||||||
// ignore, keep cookie.expiresAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val stored = StoredCookie(
|
|
||||||
name = cookie.name,
|
|
||||||
value = cookie.value,
|
|
||||||
expiresAt = storedExpires,
|
|
||||||
domain = cookie.domain,
|
|
||||||
path = cookie.path,
|
|
||||||
secure = cookie.secure,
|
|
||||||
httpOnly = cookie.httpOnly,
|
|
||||||
hostOnly = cookie.hostOnly,
|
|
||||||
iat = iatVal,
|
|
||||||
exp = expVal
|
|
||||||
)
|
|
||||||
// serialize to json explicitly and use it
|
|
||||||
val json = gson.toJson(stored)
|
|
||||||
existing.add("${cookie.name}|${cookie.path}|${json}")
|
|
||||||
}
|
|
||||||
prefs.edit { putStringSet(hostKey, existing) }
|
|
||||||
knownHosts.add(hostKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
|
||||||
val hostKey = url.host
|
|
||||||
val result = ArrayList<Cookie>()
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
synchronized(lock) {
|
|
||||||
val set = prefs.getStringSet(hostKey, emptySet()) ?: emptySet()
|
|
||||||
val newSet = mutableSetOf<String>()
|
|
||||||
for (s in set) {
|
|
||||||
try {
|
|
||||||
val jsonStart = s.indexOf('{')
|
|
||||||
val json = if (jsonStart >= 0) s.substring(jsonStart) else s
|
|
||||||
val stored = gson.fromJson(json, StoredCookie::class.java)
|
|
||||||
if (stored.expiresAt <= now) continue
|
|
||||||
val builder = Cookie.Builder()
|
|
||||||
.name(stored.name)
|
|
||||||
.value(stored.value)
|
|
||||||
.expiresAt(stored.expiresAt)
|
|
||||||
.path(stored.path)
|
|
||||||
if (stored.hostOnly) builder.hostOnlyDomain(stored.domain) else builder.domain(stored.domain)
|
|
||||||
if (stored.secure) builder.secure()
|
|
||||||
if (stored.httpOnly) builder.httpOnly()
|
|
||||||
val cookie = builder.build()
|
|
||||||
if (cookie.matches(url)) {
|
|
||||||
result.add(cookie)
|
|
||||||
}
|
|
||||||
newSet.add(s)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
// skip malformed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prefs.edit { putStringSet(hostKey, newSet) }
|
|
||||||
if (newSet.isNotEmpty()) knownHosts.add(hostKey)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveFromSetCookieHeader(setCookieHeader: String, requestUrl: String) {
|
|
||||||
val url = requestUrl.toHttpUrlOrNull() ?: return
|
|
||||||
val lines = setCookieHeader.split('\n').map { it.trim() }.filter { it.isNotEmpty() }
|
|
||||||
val parsed = mutableListOf<Cookie>()
|
|
||||||
for (line in lines) {
|
|
||||||
Cookie.parse(url, line)?.let { parsed.add(it) }
|
|
||||||
}
|
|
||||||
if (parsed.isNotEmpty()) saveFromResponse(url, parsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCookieHeaderForUrl(urlString: String): String? {
|
|
||||||
val url = urlString.toHttpUrlOrNull() ?: return null
|
|
||||||
val cookies = loadForRequest(url)
|
|
||||||
if (cookies.isEmpty()) return null
|
|
||||||
return cookies.joinToString("; ") { "${'$'}{it.name}=${'$'}{it.value}" }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAuthCookieForUrl(urlString: String): String? {
|
|
||||||
val url = urlString.toHttpUrlOrNull() ?: return null
|
|
||||||
val host = url.host
|
|
||||||
val set = prefs.getStringSet(host, emptySet()) ?: return null
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
for (s in set) {
|
|
||||||
try {
|
|
||||||
val jsonStart = s.indexOf('{')
|
|
||||||
val json = if (jsonStart >= 0) s.substring(jsonStart) else s
|
|
||||||
val stored = gson.fromJson(json, StoredCookie::class.java)
|
|
||||||
if (stored.expiresAt <= now) continue
|
|
||||||
if (stored.name in authCookieNames) return stored.value
|
|
||||||
} catch (_: Exception) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Remove all cookies stored for the given host. */
|
|
||||||
fun clearCookiesForHost(host: String) {
|
|
||||||
synchronized(lock) {
|
|
||||||
prefs.edit { remove(host) }
|
|
||||||
knownHosts.remove(host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decodeJwtExpIat(token: String): Pair<Long?, Long?> {
|
|
||||||
return try {
|
|
||||||
val jwt = JWT(token)
|
|
||||||
// expiresAt / issuedAt -> java.util.Date?
|
|
||||||
val expSec = jwt.expiresAt?.time?.div(1000) // segundos desde epoch
|
|
||||||
val iatSec = jwt.issuedAt?.time?.div(1000)
|
|
||||||
Pair(expSec, iatSec)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Pair(null, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun padBase64(b64: String): String {
|
|
||||||
val rem = b64.length % 4
|
|
||||||
return if (rem == 0) b64 else b64 + "=".repeat(4 - rem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
package com.campusaula.edbole.kanban_clone_android.network
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
object RetrofitInstance {
|
|
||||||
private const val BASE_URL = "http://10.0.2.2:8000/"
|
|
||||||
|
|
||||||
@Volatile
|
|
||||||
private var retrofit: Retrofit? = null
|
|
||||||
private var cookieJar: AuthCookieJar? = null
|
|
||||||
|
|
||||||
private val logging = HttpLoggingInterceptor().apply {
|
|
||||||
level = HttpLoggingInterceptor.Level.BODY
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getRetrofit(context: Context): Retrofit {
|
|
||||||
return retrofit ?: synchronized(this) {
|
|
||||||
retrofit ?: buildRetrofit(context.applicationContext).also { retrofit = it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildRetrofit(context: Context): Retrofit {
|
|
||||||
cookieJar = AuthCookieJar(context)
|
|
||||||
|
|
||||||
val client = OkHttpClient.Builder()
|
|
||||||
.cookieJar(cookieJar!!)
|
|
||||||
.addInterceptor(logging)
|
|
||||||
.connectTimeout(30, TimeUnit.SECONDS)
|
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return Retrofit.Builder()
|
|
||||||
.baseUrl(BASE_URL)
|
|
||||||
.client(client)
|
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Helper: obtiene el valor de la cookie de autenticación para una URL. */
|
|
||||||
fun getAuthCookieForUrl(url: String): String? {
|
|
||||||
return cookieJar?.getAuthCookieForUrl(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCookieHeaderForUrl(url: String): String? {
|
|
||||||
return cookieJar?.getCookieHeaderForUrl(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearCookiesForHost(host: String) {
|
|
||||||
cookieJar?.clearCookiesForHost(host)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cookieJarInstance(): AuthCookieJar? = cookieJar
|
|
||||||
}
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
package com.campusaula.edbole.kanban_clone_android.ui
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.util.Patterns
|
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.R
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.network.ApiService
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.network.RetrofitInstance
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class CollaboratorAddActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
private lateinit var api: ApiService
|
|
||||||
|
|
||||||
private lateinit var returnActionButton: FloatingActionButton
|
|
||||||
private lateinit var collaboratorEmailInput: EditText
|
|
||||||
private lateinit var addCollaboratorButton: Button
|
|
||||||
|
|
||||||
private var projectId: Int = -1
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
enableEdgeToEdge()
|
|
||||||
setContentView(R.layout.activity_collaborator_add)
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
|
||||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
|
||||||
insets
|
|
||||||
}
|
|
||||||
|
|
||||||
api = RetrofitInstance.getRetrofit(applicationContext).create(ApiService::class.java)
|
|
||||||
|
|
||||||
// Get project ID from intent
|
|
||||||
projectId = intent.getIntExtra("project_id", -1)
|
|
||||||
|
|
||||||
if (projectId == -1) {
|
|
||||||
Toast.makeText(this, "Error: Invalid project ID", Toast.LENGTH_SHORT).show()
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize views
|
|
||||||
returnActionButton = findViewById(R.id.returnActionButton)
|
|
||||||
collaboratorEmailInput = findViewById(R.id.collaboratorEmailInput)
|
|
||||||
addCollaboratorButton = findViewById(R.id.addCollaboratorButton)
|
|
||||||
|
|
||||||
// Set up button listeners
|
|
||||||
returnActionButton.setOnClickListener {
|
|
||||||
finish()
|
|
||||||
|
|
||||||
val intent = Intent(this@CollaboratorAddActivity, ProjectDetailActivity::class.java)
|
|
||||||
intent.putExtra("project_id", projectId)
|
|
||||||
startActivity(intent)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
addCollaboratorButton.setOnClickListener {
|
|
||||||
addCollaborator()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addCollaborator() {
|
|
||||||
val user_email = collaboratorEmailInput.text.toString().trim()
|
|
||||||
|
|
||||||
// Validate email
|
|
||||||
if (user_email.isEmpty()) {
|
|
||||||
Toast.makeText(this, "Email cannot be empty", Toast.LENGTH_SHORT).show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Patterns.EMAIL_ADDRESS.matcher(user_email).matches()) {
|
|
||||||
Toast.makeText(this, "Please enter a valid email address", Toast.LENGTH_SHORT).show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
try {
|
|
||||||
Log.d("CollaboratorAddActivity", "Adding collaborator: $user_email")
|
|
||||||
val emailBody = mapOf("user_email" to user_email)
|
|
||||||
val response = api.addProjectCollaborator(projectId, emailBody)
|
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
Log.d("CollaboratorAddActivity", "Collaborator added successfully")
|
|
||||||
Toast.makeText(
|
|
||||||
this@CollaboratorAddActivity,
|
|
||||||
"Collaborator added successfully",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
setResult(RESULT_OK)
|
|
||||||
finish()
|
|
||||||
|
|
||||||
// Reopen ProjectDetailActivity to show the new collaborator
|
|
||||||
val intent = Intent(this@CollaboratorAddActivity, ProjectDetailActivity::class.java)
|
|
||||||
intent.putExtra("project_id", projectId)
|
|
||||||
startActivity(intent)
|
|
||||||
} else {
|
|
||||||
val errorBody = response.errorBody()?.string()
|
|
||||||
Log.e("CollaboratorAddActivity", "Error adding collaborator: $errorBody")
|
|
||||||
Toast.makeText(
|
|
||||||
this@CollaboratorAddActivity,
|
|
||||||
"Error adding collaborator: ${response.code()}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("CollaboratorAddActivity", "Exception adding collaborator: ${e.message}")
|
|
||||||
Toast.makeText(
|
|
||||||
this@CollaboratorAddActivity,
|
|
||||||
"Failed to add collaborator: ${e.message}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
package com.campusaula.edbole.kanban_clone_android.ui
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.R
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.kanban.ProjectCreate
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.network.ApiService
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.network.RetrofitInstance
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
|
|
||||||
class CreateProjectActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
private lateinit var api: ApiService
|
|
||||||
|
|
||||||
private lateinit var returnActionButton : FloatingActionButton
|
|
||||||
private lateinit var newProjectName : EditText
|
|
||||||
private lateinit var newProjectDescription : EditText
|
|
||||||
private lateinit var newProjectCreateButton : Button
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
enableEdgeToEdge()
|
|
||||||
setContentView(R.layout.activity_create_project)
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
|
||||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
|
||||||
insets
|
|
||||||
}
|
|
||||||
|
|
||||||
api = RetrofitInstance.getRetrofit(applicationContext).create(ApiService::class.java)
|
|
||||||
|
|
||||||
returnActionButton = findViewById(R.id.returnActionButton)
|
|
||||||
returnActionButton.setOnClickListener {
|
|
||||||
finish()
|
|
||||||
|
|
||||||
val intent = Intent(this@CreateProjectActivity, MainActivity::class.java)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
newProjectName = findViewById(R.id.newProjectName)
|
|
||||||
newProjectDescription = findViewById(R.id.newProjectDescription)
|
|
||||||
|
|
||||||
newProjectCreateButton = findViewById(R.id.newProjectCreateButton)
|
|
||||||
newProjectCreateButton.setOnClickListener {
|
|
||||||
val projectName = newProjectName.text.toString().trim()
|
|
||||||
val projectDescription = newProjectDescription.text.toString().trim()
|
|
||||||
|
|
||||||
if (projectName.isEmpty()) {
|
|
||||||
Toast.makeText(this, "Project name cannot be empty", Toast.LENGTH_SHORT).show()
|
|
||||||
return@setOnClickListener
|
|
||||||
}
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
try {
|
|
||||||
Log.d("CreateProjectActivity", "Creating project: $projectName")
|
|
||||||
val projectCreate = ProjectCreate(projectName, projectDescription)
|
|
||||||
val response = api.createProject(projectCreate)
|
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
Log.d("CreateProjectActivity", "Project created successfully: ${response.body()}")
|
|
||||||
Toast.makeText(
|
|
||||||
this@CreateProjectActivity,
|
|
||||||
"Project created successfully",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
finish()
|
|
||||||
|
|
||||||
// Volver a MainActivity para ver el nuevo proyecto
|
|
||||||
val intent = Intent(this@CreateProjectActivity, MainActivity::class.java)
|
|
||||||
startActivity(intent)
|
|
||||||
} else {
|
|
||||||
val errorBody = response.errorBody()?.string()
|
|
||||||
Log.e("CreateProjectActivity", "Error creating project: $errorBody")
|
|
||||||
Toast.makeText(
|
|
||||||
this@CreateProjectActivity,
|
|
||||||
"Error creating project: ${response.code()}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("CreateProjectActivity", "Exception creating project: ${e.message}")
|
|
||||||
Toast.makeText(
|
|
||||||
this@CreateProjectActivity,
|
|
||||||
"Failed to create project: ${e.message}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
package com.campusaula.edbole.kanban_clone_android.ui
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.appcompat.widget.AppCompatButton
|
|
||||||
import androidx.appcompat.widget.AppCompatEditText
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.R
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.network.ApiService
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.network.RetrofitInstance
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.kanban.ErrorResponse
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.kanban.UserLogin
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
|
|
||||||
class LoginActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
private lateinit var emailInput : AppCompatEditText
|
|
||||||
private lateinit var passwordInput : AppCompatEditText
|
|
||||||
|
|
||||||
private lateinit var loginButton : AppCompatButton
|
|
||||||
private lateinit var logonButton : AppCompatButton
|
|
||||||
|
|
||||||
private lateinit var retrofit : Retrofit
|
|
||||||
private lateinit var api : ApiService
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
enableEdgeToEdge()
|
|
||||||
setContentView(R.layout.activity_login)
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
|
||||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
|
||||||
insets
|
|
||||||
}
|
|
||||||
|
|
||||||
retrofit = RetrofitInstance.getRetrofit(applicationContext)
|
|
||||||
api = retrofit.create(ApiService::class.java)
|
|
||||||
|
|
||||||
emailInput = findViewById(R.id.emailInput)
|
|
||||||
passwordInput = findViewById(R.id.passwordInput)
|
|
||||||
loginButton = findViewById(R.id.loginButton)
|
|
||||||
logonButton = findViewById(R.id.logonButton)
|
|
||||||
|
|
||||||
loginButton.setOnClickListener {
|
|
||||||
val email = emailInput.text.toString()
|
|
||||||
val password = passwordInput.text.toString()
|
|
||||||
|
|
||||||
if (email.isEmpty() && password.isEmpty()) {
|
|
||||||
Toast.makeText(this, "Please enter email and password", Toast.LENGTH_SHORT).show()
|
|
||||||
return@setOnClickListener
|
|
||||||
}
|
|
||||||
|
|
||||||
lifecycleScope.launch{
|
|
||||||
try {
|
|
||||||
val loginResponse = api.login(
|
|
||||||
UserLogin(
|
|
||||||
email = email,
|
|
||||||
password = password
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val baseUrl = retrofit.baseUrl().toString()
|
|
||||||
val baseHost = retrofit.baseUrl().host
|
|
||||||
|
|
||||||
if (loginResponse.isSuccessful) {
|
|
||||||
// Después del login exitoso OkHttp/CookieJar habrá guardado las cookies.
|
|
||||||
val authValue = RetrofitInstance.getAuthCookieForUrl(baseUrl)
|
|
||||||
if (authValue != null) {
|
|
||||||
Toast.makeText(this@LoginActivity, "Auth cookie guardada", Toast.LENGTH_SHORT).show()
|
|
||||||
} else {
|
|
||||||
Toast.makeText(this@LoginActivity, "Login OK pero no se encontró cookie de auth", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
// Navegar a MainActivity
|
|
||||||
val intent = Intent(this@LoginActivity, MainActivity::class.java)
|
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
if (loginResponse.code() == 401) {
|
|
||||||
// parse error body if possible
|
|
||||||
val errBody = loginResponse.errorBody()?.string()
|
|
||||||
val gson = Gson()
|
|
||||||
val errMsg = try {
|
|
||||||
val err = gson.fromJson(errBody, ErrorResponse::class.java)
|
|
||||||
err.detail ?: "Unauthorized"
|
|
||||||
} catch (_: Exception) {
|
|
||||||
errBody ?: "Unauthorized"
|
|
||||||
}
|
|
||||||
// clear stored cookies for base host
|
|
||||||
RetrofitInstance.clearCookiesForHost(baseHost)
|
|
||||||
|
|
||||||
Toast.makeText(this@LoginActivity, "Login failed (401): $errMsg", Toast.LENGTH_SHORT).show()
|
|
||||||
} else {
|
|
||||||
Toast.makeText(this@LoginActivity, "Login failed: ${loginResponse.code()}", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (ex: Exception){
|
|
||||||
Toast.makeText(this@LoginActivity, "Login failed: ${ex.message}", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
package com.campusaula.edbole.kanban_clone_android.ui
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.R
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.kanban.Project
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.network.ApiService
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.network.RetrofitInstance
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.ui.adapters.ProjectItemAdapter
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
private lateinit var api: ApiService
|
|
||||||
private lateinit var projectList : List<Project>
|
|
||||||
|
|
||||||
private lateinit var loggedInAs: TextView
|
|
||||||
private lateinit var logoutButton: Button
|
|
||||||
private lateinit var addProjectActionButton: FloatingActionButton
|
|
||||||
private lateinit var projectsRecyclerView: RecyclerView
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
enableEdgeToEdge()
|
|
||||||
setContentView(R.layout.activity_main)
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
|
||||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
|
||||||
insets
|
|
||||||
}
|
|
||||||
|
|
||||||
api = RetrofitInstance.getRetrofit(applicationContext).create(ApiService::class.java)
|
|
||||||
projectList = emptyList()
|
|
||||||
|
|
||||||
/* Activity components */
|
|
||||||
loggedInAs = findViewById(R.id.loggedInAs)
|
|
||||||
logoutButton = findViewById(R.id.logoutButton)
|
|
||||||
addProjectActionButton = findViewById(R.id.addProjectActionButton)
|
|
||||||
projectsRecyclerView = findViewById(R.id.projectsRecyclerView)
|
|
||||||
projectsRecyclerView.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this)
|
|
||||||
val adapter = ProjectItemAdapter(projectList) { project ->
|
|
||||||
val intent = Intent(this, ProjectDetailActivity::class.java)
|
|
||||||
intent.putExtra("project_id", project.id)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
projectsRecyclerView.adapter = adapter
|
|
||||||
|
|
||||||
addProjectActionButton.setOnClickListener {
|
|
||||||
val intent = Intent(this, CreateProjectActivity::class.java)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Getting the logged-in user info */
|
|
||||||
lifecycleScope.launch{
|
|
||||||
|
|
||||||
val getMe = api.getMe()
|
|
||||||
if (getMe.isSuccessful){
|
|
||||||
val user = getMe.body()
|
|
||||||
loggedInAs.text = "Logged in as: ${user?.name}"
|
|
||||||
projectList = api.getAllProjects().body()!!
|
|
||||||
adapter.submitList(projectList)
|
|
||||||
} else {
|
|
||||||
val intent = Intent(this@MainActivity, LoginActivity::class.java)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
logoutButton.setOnClickListener {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
val logoutResponse = api.logout()
|
|
||||||
if (logoutResponse.isSuccessful) {
|
|
||||||
// Clear cookies for the API host
|
|
||||||
RetrofitInstance.clearCookiesForHost(
|
|
||||||
"10.0.2.2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Navigate back to the login screen
|
|
||||||
val intent =
|
|
||||||
Intent(this@MainActivity, LoginActivity::class.java)
|
|
||||||
startActivity(intent)
|
|
||||||
finish() // Optional: close the MainActivity so it's removed from the back stack
|
|
||||||
} else {
|
|
||||||
Toast.makeText(
|
|
||||||
this@MainActivity,
|
|
||||||
"Logout failed",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,255 +0,0 @@
|
||||||
package com.campusaula.edbole.kanban_clone_android.ui
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.health.connect.datatypes.units.Percentage
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.R
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.kanban.Project
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.kanban.Task
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.kanban.TaskStatus
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.network.ApiService
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.network.RetrofitInstance
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.ui.adapters.ProjectCollaboratorAdapter
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.ui.adapters.ProjectTaskAdapter
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class ProjectDetailActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
private lateinit var api: ApiService
|
|
||||||
|
|
||||||
private lateinit var returnActionButton: FloatingActionButton
|
|
||||||
private lateinit var addTaskButton: Button
|
|
||||||
private lateinit var addCollaboratorButton: Button
|
|
||||||
private lateinit var editProjectButton: Button
|
|
||||||
private lateinit var deleteProjectButton: Button
|
|
||||||
|
|
||||||
private lateinit var taskListRecycler: RecyclerView
|
|
||||||
private lateinit var collaboratorListRecycler: RecyclerView
|
|
||||||
private lateinit var collaboratorListAdapter: ProjectCollaboratorAdapter
|
|
||||||
private lateinit var taskListAdapter: ProjectTaskAdapter
|
|
||||||
|
|
||||||
|
|
||||||
private lateinit var projectTitleText : TextView
|
|
||||||
private lateinit var projectDescriptionText : TextView
|
|
||||||
private lateinit var completedPercentageText: TextView
|
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
enableEdgeToEdge()
|
|
||||||
setContentView(R.layout.activity_project_detail)
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
|
||||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
|
||||||
insets
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
api = RetrofitInstance.getRetrofit(applicationContext).create(ApiService::class.java)
|
|
||||||
val projectId = intent.getIntExtra("project_id", -1)
|
|
||||||
|
|
||||||
projectTitleText = findViewById(R.id.projectTitleText)
|
|
||||||
projectDescriptionText = findViewById(R.id.projectDescriptionText)
|
|
||||||
completedPercentageText = findViewById(R.id.completedPercentageText)
|
|
||||||
|
|
||||||
returnActionButton = findViewById(R.id.returnActionButton)
|
|
||||||
returnActionButton.setOnClickListener { finish() }
|
|
||||||
|
|
||||||
addTaskButton = findViewById(R.id.addTaskButton)
|
|
||||||
addTaskButton.setOnClickListener {
|
|
||||||
val intent = Intent(this, TaskAddActivity::class.java)
|
|
||||||
intent.putExtra("project_id", projectId)
|
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
addCollaboratorButton = findViewById(R.id.addCollaboratorButton)
|
|
||||||
addCollaboratorButton.setOnClickListener {
|
|
||||||
val intent = Intent(this, CollaboratorAddActivity::class.java)
|
|
||||||
intent.putExtra("project_id", projectId)
|
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
taskListRecycler = findViewById(R.id.taskListRecycler)
|
|
||||||
taskListRecycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this)
|
|
||||||
taskListAdapter = ProjectTaskAdapter(emptyList(), api, projectId, {
|
|
||||||
updateCompletionRate()
|
|
||||||
}, {
|
|
||||||
finish()
|
|
||||||
})
|
|
||||||
taskListRecycler.adapter = taskListAdapter
|
|
||||||
|
|
||||||
collaboratorListRecycler = findViewById(R.id.collaboratorListRecycler)
|
|
||||||
collaboratorListRecycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this)
|
|
||||||
collaboratorListAdapter = ProjectCollaboratorAdapter(emptyList(), api, projectId) {
|
|
||||||
updateCollaboratorList()
|
|
||||||
}
|
|
||||||
collaboratorListRecycler.adapter = collaboratorListAdapter
|
|
||||||
|
|
||||||
// Danger Zone buttons
|
|
||||||
editProjectButton = findViewById(R.id.editProjectButton)
|
|
||||||
editProjectButton.setOnClickListener {
|
|
||||||
val intent = Intent(this, ProjectEditActivity::class.java)
|
|
||||||
intent.putExtra("project_id", projectId)
|
|
||||||
intent.putExtra("project_name", projectTitleText.text.toString())
|
|
||||||
intent.putExtra("project_description", projectDescriptionText.text.toString())
|
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteProjectButton = findViewById(R.id.deleteProjectButton)
|
|
||||||
deleteProjectButton.setOnClickListener {
|
|
||||||
deleteProject(projectId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (projectId > 0) {
|
|
||||||
Log.d("ProjectDetailActivity", "Received project ID: $projectId")
|
|
||||||
lifecycleScope.launch {
|
|
||||||
try {
|
|
||||||
val projectResponse = api.getProjectById(projectId)
|
|
||||||
|
|
||||||
if (projectResponse.isSuccessful && projectResponse.body() != null) {
|
|
||||||
Log.d("ProjectDetailActivity", "Fetched project: ${projectResponse.body()!!.name}")
|
|
||||||
val project = projectResponse.body()!!
|
|
||||||
|
|
||||||
|
|
||||||
Log.d("ProjectDetailActivity", "Displaying project details for: $project")
|
|
||||||
|
|
||||||
projectTitleText.text = project.name
|
|
||||||
projectDescriptionText.text = project.description
|
|
||||||
|
|
||||||
var percentageFinished = 0.0;
|
|
||||||
val collaborators = project.users
|
|
||||||
val tasks: List<Task> = project.tasks
|
|
||||||
val totalTasks: Int = tasks.size
|
|
||||||
|
|
||||||
var completedTasks = 0
|
|
||||||
for (task in tasks) {
|
|
||||||
if (task.status == TaskStatus.COMPLETED) {
|
|
||||||
completedTasks++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
percentageFinished = if (totalTasks > 0) (completedTasks.toDouble() / totalTasks.toDouble()) * 100 else 0.0
|
|
||||||
completedPercentageText.text = "Completed: ${"%.2f".format(percentageFinished)}%"
|
|
||||||
|
|
||||||
taskListAdapter.submitList(tasks.toMutableList())
|
|
||||||
collaboratorListAdapter.submitList(collaborators.toMutableList())
|
|
||||||
|
|
||||||
|
|
||||||
} else {
|
|
||||||
Log.e("ProjectDetailActivity", "Failed to fetch project: ${projectResponse.code()} - ${projectResponse.message()}")
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("ProjectDetailActivity", "Error fetching project", e)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Log.e("ProjectDetailActivity", "No project ID found in intent")
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateCompletionRate() {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
try {
|
|
||||||
val projectId = intent.getIntExtra("project_id", -1)
|
|
||||||
val projectResponse = api.getProjectById(projectId)
|
|
||||||
|
|
||||||
if (projectResponse.isSuccessful && projectResponse.body() != null) {
|
|
||||||
val project = projectResponse.body()!!
|
|
||||||
val tasks: List<Task> = project.tasks
|
|
||||||
val totalTasks: Int = tasks.size
|
|
||||||
|
|
||||||
var completedTasks = 0
|
|
||||||
for (task in tasks) {
|
|
||||||
if (task.status == TaskStatus.COMPLETED) {
|
|
||||||
completedTasks++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val percentageFinished = if (totalTasks > 0) (completedTasks.toDouble() / totalTasks.toDouble()) * 100 else 0.0
|
|
||||||
completedPercentageText.text = "Completed: ${"%.2f".format(percentageFinished)}%"
|
|
||||||
|
|
||||||
// Actualizar la lista de tareas también
|
|
||||||
taskListAdapter.submitList(tasks.toMutableList())
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("ProjectDetailActivity", "Error updating completion rate", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateCollaboratorList() {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
try {
|
|
||||||
val projectId = intent.getIntExtra("project_id", -1)
|
|
||||||
val projectResponse = api.getProjectById(projectId)
|
|
||||||
|
|
||||||
if (projectResponse.isSuccessful && projectResponse.body() != null) {
|
|
||||||
val project = projectResponse.body()!!
|
|
||||||
val collaborators = project.users
|
|
||||||
|
|
||||||
collaboratorListAdapter.submitList(collaborators.toMutableList())
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("ProjectDetailActivity", "Error updating collaborator list", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun deleteProject(projectId: Int) {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
try {
|
|
||||||
Log.d("ProjectDetailActivity", "Deleting project: $projectId")
|
|
||||||
val response = api.deleteProject(projectId)
|
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
Log.d("ProjectDetailActivity", "Project deleted successfully")
|
|
||||||
android.widget.Toast.makeText(
|
|
||||||
this@ProjectDetailActivity,
|
|
||||||
"Project deleted successfully",
|
|
||||||
android.widget.Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
finish()
|
|
||||||
|
|
||||||
// Volver a MainActivity
|
|
||||||
val intent = Intent(this@ProjectDetailActivity, MainActivity::class.java)
|
|
||||||
startActivity(intent)
|
|
||||||
} else {
|
|
||||||
val errorBody = response.errorBody()?.string()
|
|
||||||
Log.e("ProjectDetailActivity", "Error deleting project: $errorBody")
|
|
||||||
android.widget.Toast.makeText(
|
|
||||||
this@ProjectDetailActivity,
|
|
||||||
"Error deleting project: ${response.code()}",
|
|
||||||
android.widget.Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("ProjectDetailActivity", "Exception deleting project: ${e.message}")
|
|
||||||
android.widget.Toast.makeText(
|
|
||||||
this@ProjectDetailActivity,
|
|
||||||
"Failed to delete project: ${e.message}",
|
|
||||||
android.widget.Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
package com.campusaula.edbole.kanban_clone_android.ui
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.R
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.kanban.ProjectCreate
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.network.ApiService
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.network.RetrofitInstance
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class ProjectEditActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
private lateinit var api: ApiService
|
|
||||||
|
|
||||||
private lateinit var returnActionButton: FloatingActionButton
|
|
||||||
private lateinit var projectNameInput: EditText
|
|
||||||
private lateinit var projectDescriptionInput: EditText
|
|
||||||
private lateinit var saveProjectButton: Button
|
|
||||||
|
|
||||||
private var projectId: Int = -1
|
|
||||||
private var currentName: String = ""
|
|
||||||
private var currentDescription: String = ""
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
enableEdgeToEdge()
|
|
||||||
setContentView(R.layout.activity_project_edit)
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
|
||||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
|
||||||
insets
|
|
||||||
}
|
|
||||||
|
|
||||||
api = RetrofitInstance.getRetrofit(applicationContext).create(ApiService::class.java)
|
|
||||||
|
|
||||||
// Get project data from intent
|
|
||||||
projectId = intent.getIntExtra("project_id", -1)
|
|
||||||
currentName = intent.getStringExtra("project_name") ?: ""
|
|
||||||
currentDescription = intent.getStringExtra("project_description") ?: ""
|
|
||||||
|
|
||||||
if (projectId == -1) {
|
|
||||||
Toast.makeText(this, "Error: Invalid project ID", Toast.LENGTH_SHORT).show()
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize views
|
|
||||||
returnActionButton = findViewById(R.id.returnActionButton)
|
|
||||||
projectNameInput = findViewById(R.id.projectNameInput)
|
|
||||||
projectDescriptionInput = findViewById(R.id.projectDescriptionInput)
|
|
||||||
saveProjectButton = findViewById(R.id.saveProjectButton)
|
|
||||||
|
|
||||||
// Populate fields with current project data
|
|
||||||
projectNameInput.setText(currentName)
|
|
||||||
projectDescriptionInput.setText(currentDescription)
|
|
||||||
|
|
||||||
// Set up button listeners
|
|
||||||
returnActionButton.setOnClickListener {
|
|
||||||
finish()
|
|
||||||
|
|
||||||
val intent = Intent(this@ProjectEditActivity, ProjectDetailActivity::class.java)
|
|
||||||
intent.putExtra("project_id", projectId)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
saveProjectButton.setOnClickListener {
|
|
||||||
saveProject()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveProject() {
|
|
||||||
val newName = projectNameInput.text.toString().trim()
|
|
||||||
val newDescription = projectDescriptionInput.text.toString().trim()
|
|
||||||
|
|
||||||
if (newName.isEmpty()) {
|
|
||||||
Toast.makeText(this, "Project name cannot be empty", Toast.LENGTH_SHORT).show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
try {
|
|
||||||
Log.d("ProjectEditActivity", "Updating project: $projectId")
|
|
||||||
val projectCreate = ProjectCreate(
|
|
||||||
name = newName,
|
|
||||||
description = newDescription
|
|
||||||
)
|
|
||||||
|
|
||||||
val response = api.updateProject(projectId, projectCreate)
|
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
Log.d("ProjectEditActivity", "Project updated successfully")
|
|
||||||
Toast.makeText(
|
|
||||||
this@ProjectEditActivity,
|
|
||||||
"Project updated successfully",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
setResult(RESULT_OK)
|
|
||||||
finish()
|
|
||||||
|
|
||||||
// Reopen ProjectDetailActivity to show the updated project
|
|
||||||
val intent = Intent(this@ProjectEditActivity, ProjectDetailActivity::class.java)
|
|
||||||
intent.putExtra("project_id", projectId)
|
|
||||||
startActivity(intent)
|
|
||||||
} else {
|
|
||||||
val errorBody = response.errorBody()?.string()
|
|
||||||
Log.e("ProjectEditActivity", "Error updating project: $errorBody")
|
|
||||||
Toast.makeText(
|
|
||||||
this@ProjectEditActivity,
|
|
||||||
"Error updating project: ${response.code()}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("ProjectEditActivity", "Exception updating project: ${e.message}")
|
|
||||||
Toast.makeText(
|
|
||||||
this@ProjectEditActivity,
|
|
||||||
"Failed to update project: ${e.message}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
package com.campusaula.edbole.kanban_clone_android.ui
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.Spinner
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.R
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.kanban.TaskBase
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.kanban.TaskStatus
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.network.ApiService
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.network.RetrofitInstance
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class TaskAddActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
private lateinit var api: ApiService
|
|
||||||
|
|
||||||
private lateinit var returnActionButton: FloatingActionButton
|
|
||||||
private lateinit var taskTitleInput: EditText
|
|
||||||
private lateinit var taskDescriptionInput: EditText
|
|
||||||
private lateinit var taskStatusSpinner: Spinner
|
|
||||||
private lateinit var createTaskButton: Button
|
|
||||||
|
|
||||||
private var projectId: Int = -1
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
enableEdgeToEdge()
|
|
||||||
setContentView(R.layout.activity_task_add)
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
|
||||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
|
||||||
insets
|
|
||||||
}
|
|
||||||
|
|
||||||
api = RetrofitInstance.getRetrofit(applicationContext).create(ApiService::class.java)
|
|
||||||
|
|
||||||
// Get project ID from intent
|
|
||||||
projectId = intent.getIntExtra("project_id", -1)
|
|
||||||
|
|
||||||
if (projectId == -1) {
|
|
||||||
Toast.makeText(this, "Error: Invalid project ID", Toast.LENGTH_SHORT).show()
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize views
|
|
||||||
returnActionButton = findViewById(R.id.returnActionButton)
|
|
||||||
taskTitleInput = findViewById(R.id.taskTitleInput)
|
|
||||||
taskDescriptionInput = findViewById(R.id.taskDescriptionInput)
|
|
||||||
taskStatusSpinner = findViewById(R.id.taskStatusSpinner)
|
|
||||||
createTaskButton = findViewById(R.id.createTaskButton)
|
|
||||||
|
|
||||||
// Set default status to PENDING (index 0)
|
|
||||||
taskStatusSpinner.setSelection(0)
|
|
||||||
|
|
||||||
// Set up button listeners
|
|
||||||
returnActionButton.setOnClickListener {
|
|
||||||
finish()
|
|
||||||
|
|
||||||
val intent = Intent(this@TaskAddActivity, ProjectDetailActivity::class.java)
|
|
||||||
intent.putExtra("project_id", projectId)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
createTaskButton.setOnClickListener {
|
|
||||||
createTask()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createTask() {
|
|
||||||
val title = taskTitleInput.text.toString().trim()
|
|
||||||
val description = taskDescriptionInput.text.toString().trim()
|
|
||||||
val status = TaskStatus.entries[taskStatusSpinner.selectedItemPosition]
|
|
||||||
|
|
||||||
if (title.isEmpty()) {
|
|
||||||
Toast.makeText(this, "Title cannot be empty", Toast.LENGTH_SHORT).show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
try {
|
|
||||||
Log.d("TaskAddActivity", "Creating task: $title")
|
|
||||||
val taskBase = TaskBase(
|
|
||||||
id = 0, // ID will be assigned by the server
|
|
||||||
title = title,
|
|
||||||
description = description,
|
|
||||||
status = status
|
|
||||||
)
|
|
||||||
|
|
||||||
val response = api.createTask(projectId, taskBase)
|
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
Log.d("TaskAddActivity", "Task created successfully")
|
|
||||||
Toast.makeText(
|
|
||||||
this@TaskAddActivity,
|
|
||||||
"Task created successfully",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
setResult(RESULT_OK)
|
|
||||||
finish()
|
|
||||||
|
|
||||||
// Reopen ProjectDetailActivity to show the new task
|
|
||||||
val intent = Intent(this@TaskAddActivity, ProjectDetailActivity::class.java)
|
|
||||||
intent.putExtra("project_id", projectId)
|
|
||||||
startActivity(intent)
|
|
||||||
} else {
|
|
||||||
val errorBody = response.errorBody()?.string()
|
|
||||||
Log.e("TaskAddActivity", "Error creating task: $errorBody")
|
|
||||||
Toast.makeText(
|
|
||||||
this@TaskAddActivity,
|
|
||||||
"Error creating task: ${response.code()}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("TaskAddActivity", "Exception creating task: ${e.message}")
|
|
||||||
Toast.makeText(
|
|
||||||
this@TaskAddActivity,
|
|
||||||
"Failed to create task: ${e.message}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,189 +0,0 @@
|
||||||
package com.campusaula.edbole.kanban_clone_android.ui
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.Spinner
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.R
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.kanban.TaskStatus
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.kanban.TaskUpdate
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.network.ApiService
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.network.RetrofitInstance
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class TaskEditActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
private lateinit var api: ApiService
|
|
||||||
|
|
||||||
private lateinit var returnActionButton: FloatingActionButton
|
|
||||||
private lateinit var taskTitleInput: EditText
|
|
||||||
private lateinit var taskDescriptionInput: EditText
|
|
||||||
private lateinit var taskStatusSpinner: Spinner
|
|
||||||
private lateinit var saveTaskButton: Button
|
|
||||||
private lateinit var deleteTaskButton: Button
|
|
||||||
|
|
||||||
private var projectId: Int = -1
|
|
||||||
private var taskId: Int = -1
|
|
||||||
private var currentTitle: String = ""
|
|
||||||
private var currentDescription: String = ""
|
|
||||||
private var currentStatus: TaskStatus = TaskStatus.PENDING
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
enableEdgeToEdge()
|
|
||||||
setContentView(R.layout.activity_task_edit)
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
|
||||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
|
||||||
insets
|
|
||||||
}
|
|
||||||
|
|
||||||
api = RetrofitInstance.getRetrofit(applicationContext).create(ApiService::class.java)
|
|
||||||
|
|
||||||
// Get task and project IDs from intent
|
|
||||||
projectId = intent.getIntExtra("project_id", -1)
|
|
||||||
taskId = intent.getIntExtra("task_id", -1)
|
|
||||||
currentTitle = intent.getStringExtra("task_title") ?: ""
|
|
||||||
currentDescription = intent.getStringExtra("task_description") ?: ""
|
|
||||||
val statusString = intent.getStringExtra("task_status") ?: "PENDING"
|
|
||||||
currentStatus = TaskStatus.valueOf(statusString)
|
|
||||||
|
|
||||||
if (projectId == -1 || taskId == -1) {
|
|
||||||
Toast.makeText(this, "Error: Invalid task or project ID", Toast.LENGTH_SHORT).show()
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize views
|
|
||||||
returnActionButton = findViewById(R.id.returnActionButton)
|
|
||||||
taskTitleInput = findViewById(R.id.taskTitleInput)
|
|
||||||
taskDescriptionInput = findViewById(R.id.taskDescriptionInput)
|
|
||||||
taskStatusSpinner = findViewById(R.id.taskStatusSpinner)
|
|
||||||
saveTaskButton = findViewById(R.id.saveTaskButton)
|
|
||||||
deleteTaskButton = findViewById(R.id.deleteTaskButton)
|
|
||||||
|
|
||||||
// Populate fields with current task data
|
|
||||||
taskTitleInput.setText(currentTitle)
|
|
||||||
taskDescriptionInput.setText(currentDescription)
|
|
||||||
taskStatusSpinner.setSelection(TaskStatus.entries.indexOf(currentStatus))
|
|
||||||
|
|
||||||
// Set up button listeners
|
|
||||||
returnActionButton.setOnClickListener {
|
|
||||||
finish()
|
|
||||||
|
|
||||||
val intent = Intent(this@TaskEditActivity, ProjectDetailActivity::class.java)
|
|
||||||
intent.putExtra("project_id", projectId)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
saveTaskButton.setOnClickListener {
|
|
||||||
saveTask()
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteTaskButton.setOnClickListener {
|
|
||||||
deleteTask()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveTask() {
|
|
||||||
val newTitle = taskTitleInput.text.toString().trim()
|
|
||||||
val newDescription = taskDescriptionInput.text.toString().trim()
|
|
||||||
val newStatus = TaskStatus.entries[taskStatusSpinner.selectedItemPosition]
|
|
||||||
|
|
||||||
if (newTitle.isEmpty()) {
|
|
||||||
Toast.makeText(this, "Title cannot be empty", Toast.LENGTH_SHORT).show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
try {
|
|
||||||
Log.d("TaskEditActivity", "Updating task: $taskId")
|
|
||||||
val taskUpdate = TaskUpdate(
|
|
||||||
title = if (newTitle != currentTitle) newTitle else null,
|
|
||||||
description = if (newDescription != currentDescription) newDescription else null,
|
|
||||||
status = if (newStatus != currentStatus) newStatus else null
|
|
||||||
)
|
|
||||||
|
|
||||||
val response = api.updateProjectTask(projectId, taskId, taskUpdate)
|
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
Log.d("TaskEditActivity", "Task updated successfully")
|
|
||||||
Toast.makeText(
|
|
||||||
this@TaskEditActivity,
|
|
||||||
"Task updated successfully",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
setResult(RESULT_OK)
|
|
||||||
finish()
|
|
||||||
val intent = Intent(this@TaskEditActivity, ProjectDetailActivity::class.java)
|
|
||||||
intent.putExtra("project_id", projectId)
|
|
||||||
startActivity(intent)
|
|
||||||
} else {
|
|
||||||
val errorBody = response.errorBody()?.string()
|
|
||||||
Log.e("TaskEditActivity", "Error updating task: $errorBody")
|
|
||||||
Toast.makeText(
|
|
||||||
this@TaskEditActivity,
|
|
||||||
"Error updating task: ${response.code()}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("TaskEditActivity", "Exception updating task: ${e.message}")
|
|
||||||
Toast.makeText(
|
|
||||||
this@TaskEditActivity,
|
|
||||||
"Failed to update task: ${e.message}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun deleteTask() {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
try {
|
|
||||||
Log.d("TaskEditActivity", "Deleting task: $taskId")
|
|
||||||
val response = api.deleteProjectTask(projectId, taskId)
|
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
Log.d("TaskEditActivity", "Task deleted successfully")
|
|
||||||
Toast.makeText(
|
|
||||||
this@TaskEditActivity,
|
|
||||||
"Task deleted successfully",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
setResult(RESULT_OK)
|
|
||||||
finish()
|
|
||||||
|
|
||||||
// Reopen ProjectDetailActivity after deleting the task
|
|
||||||
val intent = Intent(this@TaskEditActivity, ProjectDetailActivity::class.java)
|
|
||||||
intent.putExtra("project_id", projectId)
|
|
||||||
startActivity(intent)
|
|
||||||
} else {
|
|
||||||
val errorBody = response.errorBody()?.string()
|
|
||||||
Log.e("TaskEditActivity", "Error deleting task: $errorBody")
|
|
||||||
Toast.makeText(
|
|
||||||
this@TaskEditActivity,
|
|
||||||
"Error deleting task: ${response.code()}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("TaskEditActivity", "Exception deleting task: ${e.message}")
|
|
||||||
Toast.makeText(
|
|
||||||
this@TaskEditActivity,
|
|
||||||
"Failed to delete task: ${e.message}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
package com.campusaula.edbole.kanban_clone_android.ui.adapters
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.R
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.kanban.User
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.network.ApiService
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class ProjectCollaboratorAdapter(
|
|
||||||
private var collaborators: List<User>,
|
|
||||||
private val apiService: ApiService,
|
|
||||||
private val projectId: Int,
|
|
||||||
private val onCollaboratorRemoved: () -> Unit = {}
|
|
||||||
) : RecyclerView.Adapter<ProjectCollaboratorAdapter.ViewHolder>() {
|
|
||||||
|
|
||||||
private val adapterScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
||||||
|
|
||||||
fun submitList(newList: List<User>) {
|
|
||||||
collaborators = newList.toMutableList()
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
||||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_collaborator, parent, false)
|
|
||||||
return ViewHolder(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
holder.bind(collaborators[position])
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int = collaborators.size
|
|
||||||
|
|
||||||
fun onDestroy() {
|
|
||||||
adapterScope.cancel("Adapter destroyed")
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
|
||||||
private val collaboratorNameText: TextView = itemView.findViewById(R.id.collaboratorNameText)
|
|
||||||
private val collaboratorEmailText: TextView = itemView.findViewById(R.id.collaboratorEmailText)
|
|
||||||
private val removeCollaboratorButton: Button = itemView.findViewById(R.id.removeCollaboratorButton)
|
|
||||||
|
|
||||||
fun bind(collaborator: User) {
|
|
||||||
collaboratorNameText.text = collaborator.name
|
|
||||||
collaboratorEmailText.text = collaborator.email
|
|
||||||
|
|
||||||
removeCollaboratorButton.setOnClickListener {
|
|
||||||
adapterScope.launch {
|
|
||||||
try {
|
|
||||||
Log.d("ProjectCollaboratorAdapter", "Removing collaborator: ${collaborator.id}")
|
|
||||||
val response = apiService.removeProjectCollaborator(projectId, collaborator.id)
|
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
Log.d("ProjectCollaboratorAdapter", "Collaborator removed successfully: ${collaborator.id}")
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
Toast.makeText(
|
|
||||||
itemView.context,
|
|
||||||
"Collaborator removed: ${collaborator.name}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
onCollaboratorRemoved()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val errorBody = response.errorBody()?.string()
|
|
||||||
Log.e("ProjectCollaboratorAdapter", "Error removing collaborator: $errorBody")
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
Toast.makeText(
|
|
||||||
itemView.context,
|
|
||||||
"Error removing collaborator: ${response.code()}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("ProjectCollaboratorAdapter", "Exception removing collaborator: ${e.message}")
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
Toast.makeText(
|
|
||||||
itemView.context,
|
|
||||||
"Failed to remove collaborator: ${e.message}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
package com.campusaula.edbole.kanban_clone_android.ui.adapters
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.R
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.kanban.Project
|
|
||||||
|
|
||||||
class ProjectItemAdapter(
|
|
||||||
private var items: List<Project>,
|
|
||||||
private val onItemClick: ((Project) -> Unit)? = null
|
|
||||||
) : RecyclerView.Adapter<ProjectItemAdapter.ViewHolder>() {
|
|
||||||
|
|
||||||
fun submitList(newList: List<Project>) {
|
|
||||||
items = newList
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
||||||
val view = LayoutInflater.from(parent.context)
|
|
||||||
.inflate(R.layout.item_project, parent, false)
|
|
||||||
return ViewHolder(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
val project = items[position]
|
|
||||||
holder.bind(project)
|
|
||||||
holder.itemView.setOnClickListener { onItemClick?.invoke(project) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int = items.size
|
|
||||||
|
|
||||||
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
|
||||||
private val nameTextView: TextView = itemView.findViewById(R.id.projectName)
|
|
||||||
private val descriptionTextView: TextView = itemView.findViewById(R.id.projectDescription)
|
|
||||||
|
|
||||||
fun bind(project: Project) {
|
|
||||||
nameTextView.text = project.name
|
|
||||||
descriptionTextView.text = project.description
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
package com.campusaula.edbole.kanban_clone_android.ui.adapters
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.AdapterView
|
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.Spinner
|
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.R
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.kanban.Task
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.kanban.TaskStatus
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.kanban.TaskUpdate
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.network.ApiService
|
|
||||||
import com.campusaula.edbole.kanban_clone_android.ui.TaskEditActivity
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class ProjectTaskAdapter(
|
|
||||||
private var tasks: List<Task>,
|
|
||||||
private val apiService: ApiService,
|
|
||||||
private val projectId: Int,
|
|
||||||
private val onTaskUpdated: () -> Unit = {},
|
|
||||||
private val onEditTask: () -> Unit = {}
|
|
||||||
) : RecyclerView.Adapter<ProjectTaskAdapter.ViewHolder>() {
|
|
||||||
|
|
||||||
private val adapterScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
||||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_task, parent, false)
|
|
||||||
return ViewHolder(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
holder.bind(tasks[position])
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int = tasks.size
|
|
||||||
|
|
||||||
fun submitList(newList: List<Task>) {
|
|
||||||
tasks = newList
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancelar el CoroutineScope cuando el adaptador ya no sea necesario
|
|
||||||
fun onDestroy() {
|
|
||||||
adapterScope.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
|
||||||
private val taskTitleText: TextView = itemView.findViewById(R.id.taskTitleText)
|
|
||||||
private val taskDescriptionText: TextView = itemView.findViewById(R.id.taskDescriptionText)
|
|
||||||
private val editTaskButton: Button = itemView.findViewById(R.id.editTaskButton)
|
|
||||||
private val taskStatusPicker: Spinner = itemView.findViewById(R.id.taskStatusDropdown)
|
|
||||||
|
|
||||||
fun bind(task: Task) {
|
|
||||||
taskTitleText.text = task.title
|
|
||||||
taskDescriptionText.text = task.description
|
|
||||||
|
|
||||||
editTaskButton.setOnClickListener {
|
|
||||||
val intent = Intent(itemView.context, TaskEditActivity::class.java)
|
|
||||||
intent.putExtra("project_id", projectId)
|
|
||||||
intent.putExtra("task_id", task.id)
|
|
||||||
intent.putExtra("task_title", task.title)
|
|
||||||
intent.putExtra("task_description", task.description)
|
|
||||||
intent.putExtra("task_status", task.status.name)
|
|
||||||
itemView.context.startActivity(intent)
|
|
||||||
onEditTask() // Cerrar la actividad
|
|
||||||
}
|
|
||||||
|
|
||||||
taskStatusPicker.setSelection(TaskStatus.entries.indexOf(task.status))
|
|
||||||
|
|
||||||
taskStatusPicker.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
|
||||||
override fun onItemSelected(
|
|
||||||
parent: AdapterView<*>?,
|
|
||||||
view: View?,
|
|
||||||
position: Int,
|
|
||||||
id: Long
|
|
||||||
) {
|
|
||||||
val selectedStatus: TaskStatus = TaskStatus.entries[position]
|
|
||||||
task.status = selectedStatus
|
|
||||||
|
|
||||||
adapterScope.launch {
|
|
||||||
try {
|
|
||||||
Log.d("ProjectTaskAdapter", "Sending PUT request for task: ${task.id}")
|
|
||||||
Log.d("ProjectTaskAdapter", "Project ID: $projectId")
|
|
||||||
Log.d("ProjectTaskAdapter", "TaskUpdate data: ${TaskUpdate(null, null, task.status)}")
|
|
||||||
|
|
||||||
val response = apiService.updateProjectTask(
|
|
||||||
projectId,
|
|
||||||
task.id,
|
|
||||||
TaskUpdate(null, null, task.status)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
Log.d("ProjectTaskAdapter", "Task updated successfully: ${task.id}")
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
onTaskUpdated()
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val errorBody = response.errorBody()?.string()
|
|
||||||
Log.e("ProjectTaskAdapter", "Error response: $errorBody")
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
Toast.makeText(
|
|
||||||
itemView.context,
|
|
||||||
"Error updating task: ${response.code()} - $errorBody",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("ProjectTaskAdapter", "Exception updating task: ${e.message}")
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
Toast.makeText(
|
|
||||||
itemView.context,
|
|
||||||
"Failed to update task: ${e.message}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
|
||||||
// No action needed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/main"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".ui.CollaboratorAddActivity">
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/returnActionButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="@dimen/fab_margin_end"
|
|
||||||
android:layout_marginBottom="@dimen/fab_margin_bottom"
|
|
||||||
android:contentDescription="Return"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:srcCompat="@android:drawable/ic_menu_revert" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingBottom="@dimen/bottom_padding_for_fab">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/titleLabel"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Add Collaborator"
|
|
||||||
android:textSize="@dimen/text_size_title"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:padding="@dimen/padding_standard"
|
|
||||||
android:layout_marginTop="@dimen/margin_tiny" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/collaboratorEmailLabel"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Email Address:"
|
|
||||||
android:textSize="@dimen/text_size_subtitle"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:layout_marginTop="@dimen/margin_medium"
|
|
||||||
android:paddingLeft="@dimen/padding_standard"
|
|
||||||
android:paddingRight="@dimen/padding_standard" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/collaboratorEmailInput"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="Enter collaborator email"
|
|
||||||
android:inputType="textEmailAddress"
|
|
||||||
android:padding="@dimen/padding_standard"
|
|
||||||
android:layout_marginLeft="@dimen/margin_standard"
|
|
||||||
android:layout_marginRight="@dimen/margin_standard"
|
|
||||||
android:layout_marginTop="@dimen/margin_small" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/addCollaboratorButton"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Add Collaborator"
|
|
||||||
android:backgroundTint="@color/primary_green"
|
|
||||||
android:layout_marginTop="@dimen/margin_large"
|
|
||||||
android:layout_marginLeft="@dimen/margin_standard"
|
|
||||||
android:layout_marginRight="@dimen/margin_standard" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout 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"
|
|
||||||
android:id="@+id/main"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".ui.CreateProjectActivity">
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:clickable="true"
|
|
||||||
android:contentDescription="return back to home"
|
|
||||||
android:focusable="true"
|
|
||||||
app:srcCompat="@android:drawable/ic_menu_revert"
|
|
||||||
android:id="@+id/returnActionButton"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
android:layout_marginBottom="@dimen/fab_margin_bottom"
|
|
||||||
android:layout_marginEnd="@dimen/fab_margin_end"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingBottom="@dimen/bottom_padding_for_fab">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:text="Create a new project..."
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/activityTitle"
|
|
||||||
android:padding="@dimen/padding_standard"
|
|
||||||
android:textSize="@dimen/text_size_title"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:layout_marginTop="@dimen/margin_tiny" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:text="Project name:"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/newProjectNameLabel"
|
|
||||||
android:textSize="@dimen/text_size_subtitle"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:layout_marginTop="@dimen/margin_medium"
|
|
||||||
android:paddingLeft="@dimen/padding_standard"
|
|
||||||
android:paddingRight="@dimen/padding_standard" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/newProjectName"
|
|
||||||
android:hint="My new project!"
|
|
||||||
android:inputType="text"
|
|
||||||
android:padding="@dimen/padding_standard"
|
|
||||||
android:layout_marginLeft="@dimen/margin_standard"
|
|
||||||
android:layout_marginRight="@dimen/margin_standard"
|
|
||||||
android:layout_marginTop="@dimen/margin_small" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:text="Project description:"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/newProjectDescriptionLabel"
|
|
||||||
android:textSize="@dimen/text_size_subtitle"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:layout_marginTop="@dimen/margin_medium"
|
|
||||||
android:paddingLeft="@dimen/padding_standard"
|
|
||||||
android:paddingRight="@dimen/padding_standard" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/newProjectDescription"
|
|
||||||
android:hint="This is my new super-special project that will change the world!!!"
|
|
||||||
android:inputType="textMultiLine"
|
|
||||||
android:minLines="3"
|
|
||||||
android:maxLines="5"
|
|
||||||
android:gravity="start|top"
|
|
||||||
android:padding="@dimen/padding_standard"
|
|
||||||
android:layout_marginLeft="@dimen/margin_standard"
|
|
||||||
android:layout_marginRight="@dimen/margin_standard"
|
|
||||||
android:layout_marginTop="@dimen/margin_small" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:text="Create new project"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/newProjectCreateButton"
|
|
||||||
android:backgroundTint="@color/primary_green"
|
|
||||||
android:layout_marginTop="@dimen/margin_large"
|
|
||||||
android:layout_marginLeft="@dimen/margin_standard"
|
|
||||||
android:layout_marginRight="@dimen/margin_standard" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout 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"
|
|
||||||
android:id="@+id/main"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context="com.campusaula.edbole.kanban_clone_android.ui.LoginActivity">
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:layout_width="350dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:inputType="textEmailAddress"
|
|
||||||
android:ems="10"
|
|
||||||
android:id="@+id/emailInput"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintHorizontal_bias="0.491"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
android:hint="usuario@ejemplo.es"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
android:layout_marginTop="224dp" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:layout_width="350dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:inputType="textPassword"
|
|
||||||
android:ems="10"
|
|
||||||
android:id="@+id/passwordInput"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintHorizontal_bias="0.491"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/emailInput"
|
|
||||||
android:hint="tu-contraseña" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:text="Iniciar sesión"
|
|
||||||
android:layout_width="228dp"
|
|
||||||
android:layout_height="46dp"
|
|
||||||
android:id="@+id/loginButton"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/passwordInput"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
app:layout_constraintHorizontal_bias="0.502" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:text="... o registrate ahora"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/logonButton"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintHorizontal_bias="0.5"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
style="@style/Widget.Material3.Button.TextButton" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
@ -5,50 +5,15 @@
|
||||||
android:id="@+id/main"
|
android:id="@+id/main"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context="com.campusaula.edbole.kanban_clone_android.ui.MainActivity">
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Logged in as: "
|
android:text="Hello World!"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
android:id="@+id/loggedInAs"
|
|
||||||
android:textAlignment="textStart"
|
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
|
||||||
app:layout_constraintVertical_bias="0.0"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:layout_marginStart="16dp" />
|
|
||||||
|
|
||||||
<Button
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
android:text="Not you? Log out here..."
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="33dp"
|
|
||||||
android:id="@+id/logoutButton"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/loggedInAs"
|
|
||||||
app:layout_constraintStart_toStartOf="@+id/loggedInAs"
|
|
||||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
|
||||||
android:textSize="8sp" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
tools:layout_editor_absoluteX="0dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/logoutButton"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:id="@+id/projectsRecyclerView" />
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:clickable="true"
|
|
||||||
app:srcCompat="@android:drawable/ic_input_add"
|
|
||||||
android:id="@+id/addProjectActionButton"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
@ -1,193 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout 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"
|
|
||||||
android:id="@+id/main"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".ui.ProjectDetailActivity">
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:clickable="true"
|
|
||||||
app:srcCompat="@android:drawable/ic_menu_revert"
|
|
||||||
android:id="@+id/returnActionButton"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
android:layout_marginBottom="@dimen/fab_margin_bottom"
|
|
||||||
android:layout_marginEnd="@dimen/fab_margin_end"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
|
||||||
|
|
||||||
<ScrollView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:gravity="start|top">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:text="Project Name"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/projectTitleText"
|
|
||||||
android:padding="@dimen/padding_standard"
|
|
||||||
android:layout_marginTop="@dimen/margin_tiny"
|
|
||||||
android:textSize="24sp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:text="Project description:"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/projectDescriptionLabel"
|
|
||||||
android:layout_marginTop="@dimen/margin_medium"
|
|
||||||
android:paddingLeft="@dimen/padding_standard"
|
|
||||||
android:paddingRight="@dimen/padding_standard"
|
|
||||||
android:labelFor="@id/projectDescriptionText"
|
|
||||||
android:textSize="@dimen/text_size_title"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/projectDescriptionText"
|
|
||||||
android:text="Project description"
|
|
||||||
android:padding="@dimen/padding_standard"
|
|
||||||
android:gravity="start|top" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:text="Completed: 100%"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/completedPercentageText"
|
|
||||||
android:paddingRight="@dimen/padding_standard"
|
|
||||||
android:paddingLeft="@dimen/padding_standard"
|
|
||||||
android:layout_marginTop="@dimen/margin_medium"
|
|
||||||
android:textSize="@dimen/text_size_subtitle" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/margin_large"
|
|
||||||
android:id="@+id/taskLinearLayout">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:text="Tasks:"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/taskListTitle"
|
|
||||||
android:paddingLeft="@dimen/padding_standard"
|
|
||||||
android:textSize="@dimen/text_size_title"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/taskListRecycler"
|
|
||||||
android:padding="8dp"
|
|
||||||
android:scrollbars="none"
|
|
||||||
android:isScrollContainer="false" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:text="Add a task..."
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/addTaskButton"
|
|
||||||
android:layout_marginLeft="@dimen/margin_standard"
|
|
||||||
android:layout_marginRight="@dimen/margin_standard"
|
|
||||||
android:backgroundTint="@color/primary_green" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/collaboratorsLinearLayout"
|
|
||||||
android:gravity="top|center_vertical"
|
|
||||||
android:layout_marginTop="@dimen/margin_large">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:text="Collaborators:"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/collaboratorListTitle"
|
|
||||||
android:paddingLeft="@dimen/padding_standard"
|
|
||||||
android:textSize="@dimen/text_size_title"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/collaboratorListRecycler"
|
|
||||||
android:padding="8dp"
|
|
||||||
android:scrollbars="none"
|
|
||||||
android:isScrollContainer="false" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:text="Add a new collaborator..."
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/addCollaboratorButton"
|
|
||||||
android:layout_marginLeft="@dimen/margin_standard"
|
|
||||||
android:layout_marginRight="@dimen/margin_standard"
|
|
||||||
android:backgroundTint="@color/primary_green" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/dangerZoneLinearLayout"
|
|
||||||
android:layout_marginTop="@dimen/margin_xlarge"
|
|
||||||
android:paddingBottom="16dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:text="Danger Zone"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/dangerZoneTitle"
|
|
||||||
android:paddingLeft="@dimen/padding_standard"
|
|
||||||
android:paddingRight="@dimen/padding_standard"
|
|
||||||
android:textSize="@dimen/text_size_title"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:textColor="@color/danger_red" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:text="These actions cannot be undone. Proceed with caution."
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/dangerZoneWarning"
|
|
||||||
android:paddingLeft="@dimen/padding_standard"
|
|
||||||
android:paddingRight="@dimen/padding_standard"
|
|
||||||
android:layout_marginTop="@dimen/margin_small"
|
|
||||||
android:textSize="@dimen/text_size_body"
|
|
||||||
android:textColor="@color/text_secondary" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:text="Edit Project"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/editProjectButton"
|
|
||||||
android:layout_marginLeft="@dimen/margin_standard"
|
|
||||||
android:layout_marginRight="@dimen/margin_standard"
|
|
||||||
android:layout_marginTop="@dimen/margin_medium"
|
|
||||||
android:backgroundTint="@color/warning_orange" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:text="Delete Project"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/deleteProjectButton"
|
|
||||||
android:layout_marginLeft="@dimen/margin_standard"
|
|
||||||
android:layout_marginRight="@dimen/margin_standard"
|
|
||||||
android:layout_marginTop="@dimen/margin_small"
|
|
||||||
android:backgroundTint="@color/danger_red" />
|
|
||||||
</LinearLayout>
|
|
||||||
</LinearLayout>
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/main"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".ui.ProjectEditActivity">
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/returnActionButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="@dimen/fab_margin_end"
|
|
||||||
android:layout_marginBottom="@dimen/fab_margin_bottom"
|
|
||||||
android:contentDescription="Return"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:srcCompat="@android:drawable/ic_menu_revert" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingBottom="@dimen/bottom_padding_for_fab">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/titleLabel"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Edit Project"
|
|
||||||
android:textSize="@dimen/text_size_title"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:padding="@dimen/padding_standard"
|
|
||||||
android:layout_marginTop="@dimen/margin_tiny" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/projectNameLabel"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Project Name:"
|
|
||||||
android:textSize="@dimen/text_size_subtitle"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:layout_marginTop="@dimen/margin_medium"
|
|
||||||
android:paddingLeft="@dimen/padding_standard"
|
|
||||||
android:paddingRight="@dimen/padding_standard" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/projectNameInput"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="Enter project name"
|
|
||||||
android:inputType="text"
|
|
||||||
android:padding="@dimen/padding_standard"
|
|
||||||
android:layout_marginLeft="@dimen/margin_standard"
|
|
||||||
android:layout_marginRight="@dimen/margin_standard"
|
|
||||||
android:layout_marginTop="@dimen/margin_small" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/projectDescriptionLabel"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Description:"
|
|
||||||
android:textSize="@dimen/text_size_subtitle"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:layout_marginTop="@dimen/margin_medium"
|
|
||||||
android:paddingLeft="@dimen/padding_standard"
|
|
||||||
android:paddingRight="@dimen/padding_standard" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/projectDescriptionInput"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="Enter project description"
|
|
||||||
android:inputType="textMultiLine"
|
|
||||||
android:minLines="3"
|
|
||||||
android:maxLines="5"
|
|
||||||
android:padding="@dimen/padding_standard"
|
|
||||||
android:layout_marginLeft="@dimen/margin_standard"
|
|
||||||
android:layout_marginRight="@dimen/margin_standard"
|
|
||||||
android:layout_marginTop="@dimen/margin_small"
|
|
||||||
android:gravity="start|top" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/saveProjectButton"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Save Changes"
|
|
||||||
android:backgroundTint="@color/primary_green"
|
|
||||||
android:layout_marginTop="@dimen/margin_large"
|
|
||||||
android:layout_marginLeft="@dimen/margin_standard"
|
|
||||||
android:layout_marginRight="@dimen/margin_standard" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/main"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".ui.TaskAddActivity">
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/returnActionButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="@dimen/fab_margin_end"
|
|
||||||
android:layout_marginBottom="@dimen/fab_margin_bottom"
|
|
||||||
android:contentDescription="Return"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:srcCompat="@android:drawable/ic_menu_revert" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingBottom="@dimen/bottom_padding_for_fab">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/titleLabel"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Add New Task"
|
|
||||||
android:textSize="@dimen/text_size_title"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:padding="@dimen/padding_standard"
|
|
||||||
android:layout_marginTop="@dimen/margin_tiny" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/taskTitleLabel"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Title:"
|
|
||||||
android:textSize="@dimen/text_size_subtitle"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:layout_marginTop="@dimen/margin_medium"
|
|
||||||
android:paddingLeft="@dimen/padding_standard"
|
|
||||||
android:paddingRight="@dimen/padding_standard" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/taskTitleInput"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="Enter task title"
|
|
||||||
android:inputType="text"
|
|
||||||
android:padding="@dimen/padding_standard"
|
|
||||||
android:layout_marginLeft="@dimen/margin_standard"
|
|
||||||
android:layout_marginRight="@dimen/margin_standard"
|
|
||||||
android:layout_marginTop="@dimen/margin_small" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/taskDescriptionLabel"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Description:"
|
|
||||||
android:textSize="@dimen/text_size_subtitle"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:layout_marginTop="@dimen/margin_medium"
|
|
||||||
android:paddingLeft="@dimen/padding_standard"
|
|
||||||
android:paddingRight="@dimen/padding_standard" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/taskDescriptionInput"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="Enter task description"
|
|
||||||
android:inputType="textMultiLine"
|
|
||||||
android:minLines="3"
|
|
||||||
android:maxLines="5"
|
|
||||||
android:padding="@dimen/padding_standard"
|
|
||||||
android:layout_marginLeft="@dimen/margin_standard"
|
|
||||||
android:layout_marginRight="@dimen/margin_standard"
|
|
||||||
android:layout_marginTop="@dimen/margin_small"
|
|
||||||
android:gravity="start|top" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/taskStatusLabel"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Status:"
|
|
||||||
android:textSize="@dimen/text_size_subtitle"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:layout_marginTop="@dimen/margin_medium"
|
|
||||||
android:paddingLeft="@dimen/padding_standard"
|
|
||||||
android:paddingRight="@dimen/padding_standard" />
|
|
||||||
|
|
||||||
<Spinner
|
|
||||||
android:id="@+id/taskStatusSpinner"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:entries="@array/task_status_options"
|
|
||||||
android:padding="@dimen/padding_standard"
|
|
||||||
android:layout_marginLeft="@dimen/margin_standard"
|
|
||||||
android:layout_marginRight="@dimen/margin_standard"
|
|
||||||
android:layout_marginTop="@dimen/margin_small" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/createTaskButton"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Create Task"
|
|
||||||
android:backgroundTint="@color/primary_green"
|
|
||||||
android:layout_marginTop="@dimen/margin_large"
|
|
||||||
android:layout_marginLeft="@dimen/margin_standard"
|
|
||||||
android:layout_marginRight="@dimen/margin_standard" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
@ -1,127 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/main"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".ui.TaskEditActivity">
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/returnActionButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="@dimen/fab_margin_end"
|
|
||||||
android:layout_marginBottom="@dimen/fab_margin_bottom"
|
|
||||||
android:contentDescription="Return"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:srcCompat="@android:drawable/ic_menu_revert" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingBottom="@dimen/bottom_padding_for_fab">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/titleLabel"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Edit Task"
|
|
||||||
android:textSize="@dimen/text_size_title"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:padding="@dimen/padding_standard"
|
|
||||||
android:layout_marginTop="@dimen/margin_tiny" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/taskTitleLabel"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Title:"
|
|
||||||
android:textSize="@dimen/text_size_subtitle"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:layout_marginTop="@dimen/margin_medium"
|
|
||||||
android:paddingLeft="@dimen/padding_standard"
|
|
||||||
android:paddingRight="@dimen/padding_standard" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/taskTitleInput"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="Enter task title"
|
|
||||||
android:inputType="text"
|
|
||||||
android:padding="@dimen/padding_standard"
|
|
||||||
android:layout_marginLeft="@dimen/margin_standard"
|
|
||||||
android:layout_marginRight="@dimen/margin_standard"
|
|
||||||
android:layout_marginTop="@dimen/margin_small" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/taskDescriptionLabel"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Description:"
|
|
||||||
android:textSize="@dimen/text_size_subtitle"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:layout_marginTop="@dimen/margin_medium"
|
|
||||||
android:paddingLeft="@dimen/padding_standard"
|
|
||||||
android:paddingRight="@dimen/padding_standard" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/taskDescriptionInput"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="Enter task description"
|
|
||||||
android:inputType="textMultiLine"
|
|
||||||
android:minLines="3"
|
|
||||||
android:maxLines="5"
|
|
||||||
android:padding="@dimen/padding_standard"
|
|
||||||
android:layout_marginLeft="@dimen/margin_standard"
|
|
||||||
android:layout_marginRight="@dimen/margin_standard"
|
|
||||||
android:layout_marginTop="@dimen/margin_small"
|
|
||||||
android:gravity="start|top" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/taskStatusLabel"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Status:"
|
|
||||||
android:textSize="@dimen/text_size_subtitle"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:layout_marginTop="@dimen/margin_medium"
|
|
||||||
android:paddingLeft="@dimen/padding_standard"
|
|
||||||
android:paddingRight="@dimen/padding_standard" />
|
|
||||||
|
|
||||||
<Spinner
|
|
||||||
android:id="@+id/taskStatusSpinner"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:entries="@array/task_status_options"
|
|
||||||
android:padding="@dimen/padding_standard"
|
|
||||||
android:layout_marginLeft="@dimen/margin_standard"
|
|
||||||
android:layout_marginRight="@dimen/margin_standard"
|
|
||||||
android:layout_marginTop="@dimen/margin_small" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/saveTaskButton"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Save Changes"
|
|
||||||
android:backgroundTint="@color/primary_green"
|
|
||||||
android:layout_marginTop="@dimen/margin_large"
|
|
||||||
android:layout_marginLeft="@dimen/margin_standard"
|
|
||||||
android:layout_marginRight="@dimen/margin_standard" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/deleteTaskButton"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Delete Task"
|
|
||||||
android:backgroundTint="@color/danger_red"
|
|
||||||
android:layout_marginTop="@dimen/margin_medium"
|
|
||||||
android:layout_marginLeft="@dimen/margin_standard"
|
|
||||||
android:layout_marginRight="@dimen/margin_standard" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:gravity="center_horizontal|end"
|
|
||||||
android:layout_margin="8dp"
|
|
||||||
android:id="@+id/linearLayout">
|
|
||||||
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintVertical_bias="1.0"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
android:layout_marginStart="4dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/collaboratorNameText"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Collaborator Name"
|
|
||||||
android:textSize="18dp"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:text="example@email.com"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/collaboratorEmailText"
|
|
||||||
android:textStyle="italic" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/removeCollaboratorButton"
|
|
||||||
android:layout_width="50dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
style="@style/Widget.Material3.Button.Icon"
|
|
||||||
app:icon="@android:drawable/ic_delete"
|
|
||||||
android:backgroundTint="@color/transparent_red"
|
|
||||||
app:iconTint="@color/danger_red"
|
|
||||||
android:layout_marginEnd="4dp"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintVertical_bias="0.333" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="8dp"
|
|
||||||
android:layout_margin="8dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/projectName"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Project name"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:textSize="16sp"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/projectDescription"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Project description"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="2"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/projectName"
|
|
||||||
android:layout_marginTop="4dp" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="start|top"
|
|
||||||
android:layout_margin="4dp"
|
|
||||||
android:id="@+id/linearLayout2">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="331dp"
|
|
||||||
android:layout_height="50dp"
|
|
||||||
android:id="@+id/linearLayout3"
|
|
||||||
tools:layout_editor_absoluteY="4dp"
|
|
||||||
tools:layout_editor_absoluteX="4dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/taskTitleText"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="Task Title"
|
|
||||||
android:textSize="20dp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:paddingBottom="4dp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/taskDescriptionText"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="2"
|
|
||||||
android:text="Task Description"
|
|
||||||
android:textSize="14sp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/taskStatusLinearLayout"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/linearLayout3"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
android:layout_margin="4dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:text="STATUS:"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:id="@+id/taskStatusLabel"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:textAlignment="textStart"
|
|
||||||
android:gravity="start|center_vertical" />
|
|
||||||
|
|
||||||
<Spinner
|
|
||||||
android:id="@+id/taskStatusDropdown"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:entries="@array/task_status_options"
|
|
||||||
android:layout_marginStart="16dp"
|
|
||||||
android:spinnerMode="dropdown"
|
|
||||||
android:dropDownWidth="wrap_content" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:layout_width="50dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/editTaskButton"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintVertical_bias="0.5"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:icon="@android:drawable/ic_menu_edit"
|
|
||||||
style="@style/Widget.Material3.Button.IconButton"
|
|
||||||
app:iconTint="#0C0808" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string-array name="task_status_options">
|
|
||||||
<item>Pending</item>
|
|
||||||
<item>In progress</item>
|
|
||||||
<item>Completed</item>
|
|
||||||
<item>Stashed</item>
|
|
||||||
<item>Failed</item>
|
|
||||||
</string-array>
|
|
||||||
</resources>
|
|
||||||
|
|
@ -2,15 +2,4 @@
|
||||||
<resources>
|
<resources>
|
||||||
<color name="black">#FF000000</color>
|
<color name="black">#FF000000</color>
|
||||||
<color name="white">#FFFFFFFF</color>
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
|
||||||
<!-- Primary Colors -->
|
|
||||||
<color name="primary_green">#469E29</color>
|
|
||||||
<color name="danger_red">#FF0000</color>
|
|
||||||
<color name="warning_orange">#FF9800</color>
|
|
||||||
|
|
||||||
<!-- Text Colors -->
|
|
||||||
<color name="text_secondary">#666666</color>
|
|
||||||
|
|
||||||
<!-- Background Colors -->
|
|
||||||
<color name="transparent_red">#24FF5757</color>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<!-- Paddings -->
|
|
||||||
<dimen name="padding_standard">12dp</dimen>
|
|
||||||
<dimen name="padding_small">8dp</dimen>
|
|
||||||
|
|
||||||
<!-- Margins -->
|
|
||||||
<dimen name="margin_standard">12dp</dimen>
|
|
||||||
<dimen name="margin_medium">16dp</dimen>
|
|
||||||
<dimen name="margin_large">24dp</dimen>
|
|
||||||
<dimen name="margin_xlarge">32dp</dimen>
|
|
||||||
<dimen name="margin_small">8dp</dimen>
|
|
||||||
<dimen name="margin_tiny">4dp</dimen>
|
|
||||||
|
|
||||||
<!-- FAB Margins -->
|
|
||||||
<dimen name="fab_margin_bottom">17dp</dimen>
|
|
||||||
<dimen name="fab_margin_end">18dp</dimen>
|
|
||||||
|
|
||||||
<!-- Bottom Padding for FAB -->
|
|
||||||
<dimen name="bottom_padding_for_fab">80dp</dimen>
|
|
||||||
|
|
||||||
<!-- Text Sizes -->
|
|
||||||
<dimen name="text_size_title">24sp</dimen>
|
|
||||||
<dimen name="text_size_subtitle">18sp</dimen>
|
|
||||||
<dimen name="text_size_body">14sp</dimen>
|
|
||||||
</resources>
|
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.campusaula.edbole.kanban_clone_android
|
package com.campusaula.edbole.KanbanCloneAndroid
|
||||||
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -8,9 +8,6 @@ appcompat = "1.7.1"
|
||||||
material = "1.13.0"
|
material = "1.13.0"
|
||||||
activity = "1.12.2"
|
activity = "1.12.2"
|
||||||
constraintlayout = "2.2.1"
|
constraintlayout = "2.2.1"
|
||||||
okhttp = "4.11.0"
|
|
||||||
retrofit = "2.9.0"
|
|
||||||
recyclerview = "1.4.0"
|
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
|
|
@ -21,11 +18,7 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version
|
||||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||||
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
|
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
|
||||||
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||||
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
|
||||||
okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
|
|
||||||
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
|
||||||
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
|
|
||||||
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
|
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue