Compare commits

..

No commits in common. "110f356d78691c40bef775ca6d6ecf3bddc88cd8" and "9c2703831327578add93fe7e8bc1883c1c354551" have entirely different histories.

15 changed files with 34 additions and 431 deletions

View file

@ -51,7 +51,6 @@ dependencies {
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.auth0.android:jwtdecode:2.0.1")
implementation(libs.androidx.recyclerview)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)

View file

@ -1,32 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<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
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.KanbanCloneAndroid"
android:usesCleartextTraffic="true">
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.KanbanCloneAndroid"
android:usesCleartextTraffic="true" >
<activity
android:name=".ui.CreateProjectActivity"
android:exported="false" />
android:name="com.campusaula.edbole.kanban_clone_android.LoginActivity"
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:name="com.campusaula.edbole.kanban_clone_android.MainActivity"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View file

@ -1,31 +1,25 @@
package com.campusaula.edbole.kanban_clone_android.ui
package com.campusaula.edbole.kanban_clone_android
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 emailInput : androidx.appcompat.widget.AppCompatEditText
private lateinit var passwordInput : androidx.appcompat.widget.AppCompatEditText
private lateinit var loginButton : AppCompatButton
private lateinit var logonButton : AppCompatButton
private lateinit var loginButton : androidx.appcompat.widget.AppCompatButton
private lateinit var logonButton : androidx.appcompat.widget.AppCompatButton
private lateinit var retrofit : Retrofit
private lateinit var api : ApiService
@ -53,14 +47,14 @@ class LoginActivity : AppCompatActivity() {
val password = passwordInput.text.toString()
if (email.isEmpty() && password.isEmpty()) {
Toast.makeText(this, "Please enter email and password", Toast.LENGTH_SHORT).show()
android.widget.Toast.makeText(this, "Please enter email and password", android.widget.Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
lifecycleScope.launch{
try {
val loginResponse = api.login(
UserLogin(
com.campusaula.edbole.kanban_clone_android.kanban.UserLogin(
email = email,
password = password
)
@ -73,14 +67,10 @@ class LoginActivity : AppCompatActivity() {
// 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()
android.widget.Toast.makeText(this@LoginActivity, "Auth cookie guardada", android.widget.Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this@LoginActivity, "Login OK pero no se encontró cookie de auth", Toast.LENGTH_SHORT).show()
android.widget.Toast.makeText(this@LoginActivity, "Login OK pero no se encontró cookie de auth", android.widget.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
@ -95,14 +85,14 @@ class LoginActivity : AppCompatActivity() {
// clear stored cookies for base host
RetrofitInstance.clearCookiesForHost(baseHost)
Toast.makeText(this@LoginActivity, "Login failed (401): $errMsg", Toast.LENGTH_SHORT).show()
android.widget.Toast.makeText(this@LoginActivity, "Login failed (401): $errMsg", android.widget.Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this@LoginActivity, "Login failed: ${loginResponse.code()}", Toast.LENGTH_SHORT).show()
android.widget.Toast.makeText(this@LoginActivity, "Login failed: ${loginResponse.code()}", android.widget.Toast.LENGTH_SHORT).show()
}
}
} catch (ex: Exception){
Toast.makeText(this@LoginActivity, "Login failed: ${ex.message}", Toast.LENGTH_SHORT).show()
android.widget.Toast.makeText(this@LoginActivity, "Login failed: ${ex.message}", android.widget.Toast.LENGTH_SHORT).show()
}
}

View file

@ -1,17 +1,16 @@
package com.campusaula.edbole.kanban_clone_android.ui
package com.campusaula.edbole.kanban_clone_android
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.campusaula.edbole.kanban_clone_android.R
class CreateProjectActivity : AppCompatActivity() {
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_create_project)
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)

View file

@ -8,11 +8,6 @@ class Project{
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(

View file

@ -11,7 +11,7 @@ interface ApiService {
@POST("auth/login/")
suspend fun login(@Body userLogin: UserLogin): Response<LoginResponse>
@GET("me/logout/")
@POST("me/logout/")
suspend fun logout(): Response<Unit>
@DELETE("me/delete-me/")

View file

@ -1,101 +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.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:8000"
)
// 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()
}
}
}
}
}

View file

@ -1,92 +0,0 @@
package com.campusaula.edbole.kanban_clone_android.ui
import android.health.connect.datatypes.units.Percentage
import android.os.Bundle
import android.util.Log
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 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.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.coroutines.launch
class ProjectDetailActivity : AppCompatActivity() {
private lateinit var api: ApiService
private lateinit var projectTitleText : TextView
private lateinit var projectDescriptionText : TextView
private lateinit var completedPercentageText: TextView
private lateinit var returnActionButton: FloatingActionButton
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)
projectTitleText = findViewById(R.id.projectTitleText)
projectDescriptionText = findViewById(R.id.projectDescriptionText)
completedPercentageText = findViewById(R.id.completedPercentageText)
returnActionButton = findViewById(R.id.returnActionButton)
returnActionButton.setOnClickListener { finish() }
val projectId : Int = intent.getIntExtra("project_id", -1)
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 tasks: List<Task> = project.tasks
val totalTasks: Int = tasks.size
val perTaskPercentage = if (totalTasks > 0) (1.0 / totalTasks)*100 else 0.0
for (task in tasks) {
if (task.status == TaskStatus.COMPLETED) {
percentageFinished += perTaskPercentage
}
}
completedPercentageText.text = "Completed: ${"%.2f".format(percentageFinished * 100)}%"
} 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()
}
}
}

View file

@ -1,44 +0,0 @@
package com.campusaula.edbole.kanban_clone_android.ui
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 nameTv: TextView = itemView.findViewById(R.id.projectName)
private val descTv: TextView = itemView.findViewById(R.id.projectDescription)
fun bind(project: Project) {
nameTv.text = project.id.toString() + " " + project.name
descTv.text = project.description
}
}
}

View file

@ -1,10 +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">
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -5,7 +5,7 @@
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.campusaula.edbole.kanban_clone_android.ui.LoginActivity">
tools:context="com.campusaula.edbole.kanban_clone_android.LoginActivity">
<EditText
android:layout_width="350dp"

View file

@ -5,50 +5,15 @@
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.campusaula.edbole.kanban_clone_android.ui.MainActivity">
tools:context="com.campusaula.edbole.kanban_clone_android.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Logged in as: "
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="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
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="1dp"
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" />
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,56 +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="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:text="Project Name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/projectTitleText"
android:padding="12dp"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="4dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintEnd_toEndOf="parent"
android:textSize="24sp" />
<TextView
android:text="Completed: 100%"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/completedPercentageText"
app:layout_constraintTop_toBottomOf="@+id/projectTitleText"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintEnd_toEndOf="parent"
android:paddingRight="12dp"
android:paddingLeft="12dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/projectDescriptionText"
app:layout_constraintTop_toBottomOf="@+id/completedPercentageText"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintEnd_toEndOf="parent"
android:text="Project description"
android:padding="12dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -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>

View file

@ -10,7 +10,6 @@ activity = "1.12.2"
constraintlayout = "2.2.1"
okhttp = "4.11.0"
retrofit = "2.9.0"
recyclerview = "1.4.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -25,7 +24,6 @@ okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhtt
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]
android-application = { id = "com.android.application", version.ref = "agp" }