refactor: move network logic into ViewModels and externalize strings
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
98b15c748a
commit
43d1a6013b
@ -22,9 +22,14 @@ android {
|
|||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
|
isMinifyEnabled = true
|
||||||
optimization {
|
optimization {
|
||||||
enable = false
|
enable = false
|
||||||
}
|
}
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@ -36,6 +41,9 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
|
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||||
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
implementation(libs.androidx.fragment.ktx)
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
implementation(libs.okhttp)
|
implementation(libs.okhttp)
|
||||||
implementation(libs.gson)
|
implementation(libs.gson)
|
||||||
|
|||||||
@ -147,7 +147,7 @@ class JobFormFragment : Fragment() {
|
|||||||
private fun scrapeUrl() {
|
private fun scrapeUrl() {
|
||||||
val url = seekUrlInput.text?.toString()?.trim() ?: ""
|
val url = seekUrlInput.text?.toString()?.trim() ?: ""
|
||||||
if (url.isEmpty()) {
|
if (url.isEmpty()) {
|
||||||
seekUrlInput.error = "Enter a URL"
|
seekUrlInput.error = getString(R.string.enter_url)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
showLoading(true)
|
showLoading(true)
|
||||||
|
|||||||
@ -14,27 +14,25 @@ import androidx.credentials.CustomCredential
|
|||||||
import androidx.credentials.GetCredentialRequest
|
import androidx.credentials.GetCredentialRequest
|
||||||
import androidx.credentials.GetCredentialResponse
|
import androidx.credentials.GetCredentialResponse
|
||||||
import androidx.credentials.exceptions.GetCredentialException
|
import androidx.credentials.exceptions.GetCredentialException
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.activity.viewModels
|
||||||
import com.example.resbuilder.R
|
import com.example.resbuilder.R
|
||||||
import com.example.resbuilder.data.remote.ApiClient
|
|
||||||
import com.google.android.gms.auth.api.signin.GoogleSignIn
|
|
||||||
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
|
|
||||||
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
|
|
||||||
import com.google.android.gms.common.api.ApiException
|
|
||||||
import com.google.android.gms.tasks.Task
|
|
||||||
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
|
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
|
||||||
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
|
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
|
||||||
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
|
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
import com.google.android.material.progressindicator.CircularProgressIndicator
|
import com.google.android.material.progressindicator.CircularProgressIndicator
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
|
|
||||||
class LoginActivity : AppCompatActivity() {
|
class LoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private val viewModel: LoginViewModel by viewModels()
|
||||||
|
|
||||||
private lateinit var googleSignInButton: MaterialButton
|
private lateinit var googleSignInButton: MaterialButton
|
||||||
private lateinit var progress: CircularProgressIndicator
|
private lateinit var progress: CircularProgressIndicator
|
||||||
private lateinit var credentialManager: CredentialManager
|
private lateinit var credentialManager: CredentialManager
|
||||||
@ -45,10 +43,6 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
private val webClientId = "416332591622-godddvcplbobkhv6uvabg94pvtukp61u.apps.googleusercontent.com"
|
private val webClientId = "416332591622-godddvcplbobkhv6uvabg94pvtukp61u.apps.googleusercontent.com"
|
||||||
private val androidClientId = "416332591622-41ioels091g0am6v5m1catf0tt69ohk4.apps.googleusercontent.com"
|
private val androidClientId = "416332591622-41ioels091g0am6v5m1catf0tt69ohk4.apps.googleusercontent.com"
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "LoginActivity"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_login)
|
setContentView(R.layout.activity_login)
|
||||||
@ -62,25 +56,36 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
googleSignInLauncher = registerForActivityResult(
|
googleSignInLauncher = registerForActivityResult(
|
||||||
ActivityResultContracts.StartIntentSenderForResult()
|
ActivityResultContracts.StartIntentSenderForResult()
|
||||||
) { result ->
|
) { result ->
|
||||||
Log.w(TAG, "Unexpected activity result: ${result.resultCode}")
|
|
||||||
showLoading(false)
|
showLoading(false)
|
||||||
Toast.makeText(this, "Sign-in interrupted", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.sign_in_interrupted, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
observeAuthState()
|
||||||
|
|
||||||
showLoading(true)
|
showLoading(true)
|
||||||
checkExistingAuth()
|
viewModel.checkExistingAuth()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkExistingAuth() {
|
private fun observeAuthState() {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
val user = withContext(Dispatchers.IO) { ApiClient.getMe() }
|
viewModel.state.collect { state ->
|
||||||
showLoading(false)
|
showLoading(state.loading)
|
||||||
if (user.authenticated) {
|
state.error?.let {
|
||||||
|
Toast.makeText(this@LoginActivity, it, Toast.LENGTH_LONG).show()
|
||||||
|
viewModel.setLoading(false)
|
||||||
|
}
|
||||||
|
state.user?.let { user ->
|
||||||
|
if (state.authenticated) {
|
||||||
|
Toast.makeText(
|
||||||
|
this@LoginActivity,
|
||||||
|
getString(R.string.welcome_user, user.name ?: user.email),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
launchMain()
|
launchMain()
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
}
|
||||||
showLoading(false)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,14 +95,8 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
googleSignInButton.isEnabled = !loading
|
googleSignInButton.isEnabled = !loading
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private fun launchGoogleSignIn() {
|
private fun launchGoogleSignIn() {
|
||||||
showLoading(true)
|
showLoading(true)
|
||||||
lifecycleScope.launch {
|
|
||||||
Log.d(TAG, "Starting Credential Manager flow")
|
|
||||||
Log.d(TAG, "Web client ID: $webClientId")
|
|
||||||
Log.d(TAG, "Android client ID: $androidClientId")
|
|
||||||
val googleIdOption = GetGoogleIdOption.Builder()
|
val googleIdOption = GetGoogleIdOption.Builder()
|
||||||
.setFilterByAuthorizedAccounts(false)
|
.setFilterByAuthorizedAccounts(false)
|
||||||
.setServerClientId(webClientId)
|
.setServerClientId(webClientId)
|
||||||
@ -109,23 +108,19 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
.addCredentialOption(googleIdOption)
|
.addCredentialOption(googleIdOption)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
viewModel.viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "Calling getCredential...")
|
|
||||||
val result = credentialManager.getCredential(
|
val result = credentialManager.getCredential(
|
||||||
request = request,
|
request = request,
|
||||||
context = this@LoginActivity
|
context = this@LoginActivity
|
||||||
)
|
)
|
||||||
Log.d(TAG, "Credential retrieved successfully")
|
|
||||||
handleCredentialResponse(result)
|
handleCredentialResponse(result)
|
||||||
} catch (e: GetCredentialException) {
|
} catch (e: GetCredentialException) {
|
||||||
Log.e(TAG, "Credential Manager failed: ${e.javaClass.simpleName}", e)
|
|
||||||
Log.e(TAG, "Error message: ${e.message}")
|
|
||||||
Log.e(TAG, "Error code: ${e.cause}")
|
|
||||||
showLoading(false)
|
showLoading(false)
|
||||||
val errorMsg = when (e) {
|
val errorMsg = when (e) {
|
||||||
is androidx.credentials.exceptions.NoCredentialException -> "No Google accounts found on device"
|
is androidx.credentials.exceptions.NoCredentialException -> getString(R.string.no_google_accounts)
|
||||||
is androidx.credentials.exceptions.GetCredentialCancellationException -> "Sign-in cancelled"
|
is androidx.credentials.exceptions.GetCredentialCancellationException -> getString(R.string.sign_in_cancelled)
|
||||||
else -> "Sign-in failed: ${e.message}"
|
else -> getString(R.string.sign_in_failed, e.message)
|
||||||
}
|
}
|
||||||
Toast.makeText(this@LoginActivity, errorMsg, Toast.LENGTH_LONG).show()
|
Toast.makeText(this@LoginActivity, errorMsg, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
@ -141,56 +136,18 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
val idToken = googleIdTokenCredential.idToken
|
val idToken = googleIdTokenCredential.idToken
|
||||||
if (idToken.isNullOrEmpty()) {
|
if (idToken.isNullOrEmpty()) {
|
||||||
showLoading(false)
|
showLoading(false)
|
||||||
Toast.makeText(this, "No ID token from Google", Toast.LENGTH_LONG).show()
|
Toast.makeText(this, R.string.no_id_token, Toast.LENGTH_LONG).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
exchangeToken(idToken)
|
viewModel.exchangeToken(idToken)
|
||||||
} catch (e: GoogleIdTokenParsingException) {
|
} catch (e: GoogleIdTokenParsingException) {
|
||||||
showLoading(false)
|
showLoading(false)
|
||||||
Toast.makeText(this, "Invalid Google ID token: ${e.message}", Toast.LENGTH_LONG).show()
|
Toast.makeText(this, getString(R.string.invalid_google_id_token, e.message), Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
showLoading(false)
|
showLoading(false)
|
||||||
Toast.makeText(this, "Unexpected credential type", Toast.LENGTH_LONG).show()
|
Toast.makeText(this, R.string.unexpected_credential_type, Toast.LENGTH_LONG).show()
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleSignInError(message: String) {
|
|
||||||
showLoading(false)
|
|
||||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun exchangeToken(idToken: String) {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
try {
|
|
||||||
val token = withContext(Dispatchers.IO) { ApiClient.exchangeGoogleToken(idToken) }
|
|
||||||
ApiClient.setToken(token)
|
|
||||||
val user = withContext(Dispatchers.IO) { ApiClient.getMe() }
|
|
||||||
showLoading(false)
|
|
||||||
if (user.authenticated) {
|
|
||||||
Toast.makeText(
|
|
||||||
this@LoginActivity,
|
|
||||||
"Welcome ${user.name ?: user.email}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
launchMain()
|
|
||||||
} else {
|
|
||||||
Toast.makeText(
|
|
||||||
this@LoginActivity,
|
|
||||||
"Server rejected the token",
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showLoading(false)
|
|
||||||
Log.e(TAG, "Token exchange failed", e)
|
|
||||||
Toast.makeText(
|
|
||||||
this@LoginActivity,
|
|
||||||
e.message ?: "Token exchange failed",
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -200,10 +157,6 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun generateNonce(): String {
|
private fun generateNonce(): String {
|
||||||
val secureRandom = SecureRandom()
|
val secureRandom = SecureRandom()
|
||||||
val bytes = ByteArray(32)
|
val bytes = ByteArray(32)
|
||||||
|
|||||||
@ -0,0 +1,62 @@
|
|||||||
|
package com.example.resbuilder.ui
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.example.resbuilder.data.model.User
|
||||||
|
import com.example.resbuilder.data.remote.ApiClient
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
data class LoginUiState(
|
||||||
|
val loading: Boolean = false,
|
||||||
|
val authenticated: Boolean = false,
|
||||||
|
val user: User? = null,
|
||||||
|
val error: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
class LoginViewModel : ViewModel() {
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(LoginUiState())
|
||||||
|
val state: StateFlow<LoginUiState> = _state.asStateFlow()
|
||||||
|
|
||||||
|
fun checkExistingAuth() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val user = withContext(Dispatchers.IO) { ApiClient.getMe() }
|
||||||
|
_state.value = LoginUiState(loading = false, authenticated = user.authenticated, user = user)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
_state.value = LoginUiState(loading = false, error = "check auth failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exchangeToken(idToken: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.value = _state.value.copy(loading = true, error = null)
|
||||||
|
try {
|
||||||
|
val token = withContext(Dispatchers.IO) { ApiClient.exchangeGoogleToken(idToken) }
|
||||||
|
ApiClient.setToken(token)
|
||||||
|
val user = withContext(Dispatchers.IO) { ApiClient.getMe() }
|
||||||
|
_state.value = LoginUiState(
|
||||||
|
loading = false,
|
||||||
|
authenticated = user.authenticated,
|
||||||
|
user = user
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.value = LoginUiState(loading = false, error = e.message ?: "Token exchange failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLoading(loading: Boolean) {
|
||||||
|
_state.value = _state.value.copy(loading = loading)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consumeAuth() {
|
||||||
|
_state.value = _state.value.copy(authenticated = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,6 @@
|
|||||||
package com.example.resbuilder.ui
|
package com.example.resbuilder.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@ -11,15 +9,18 @@ import android.widget.ListView
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.example.resbuilder.R
|
import com.example.resbuilder.R
|
||||||
import com.example.resbuilder.data.model.Job
|
|
||||||
import com.example.resbuilder.data.model.JobSummary
|
import com.example.resbuilder.data.model.JobSummary
|
||||||
import com.example.resbuilder.data.remote.ApiClient
|
|
||||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class MyResumesFragment : Fragment() {
|
class MyResumesFragment : Fragment() {
|
||||||
|
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val viewModel: MyResumesViewModel by viewModels()
|
||||||
|
|
||||||
private lateinit var emptyView: TextView
|
private lateinit var emptyView: TextView
|
||||||
private lateinit var progress: LinearProgressIndicator
|
private lateinit var progress: LinearProgressIndicator
|
||||||
@ -37,71 +38,54 @@ class MyResumesFragment : Fragment() {
|
|||||||
|
|
||||||
listView.setOnItemClickListener { _, _, position, _ ->
|
listView.setOnItemClickListener { _, _, position, _ ->
|
||||||
val item = listView.adapter.getItem(position) as JobSummary
|
val item = listView.adapter.getItem(position) as JobSummary
|
||||||
openJob(item.job_id)
|
viewModel.openJob(item.job_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
loadJobs()
|
observeState()
|
||||||
|
viewModel.loadJobs()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadJobs() {
|
private fun observeState() {
|
||||||
showLoading(true)
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
Thread {
|
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
try {
|
viewModel.state.collect { state ->
|
||||||
val resp = ApiClient.getMyJobs()
|
progress.visibility = if (state.loading) View.VISIBLE else View.GONE
|
||||||
handler.post {
|
|
||||||
showLoading(false)
|
state.error?.let {
|
||||||
bindJobs(resp.jobs)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
handler.post {
|
|
||||||
showLoading(false)
|
|
||||||
emptyView.text = getString(R.string.my_resumes_error)
|
emptyView.text = getString(R.string.my_resumes_error)
|
||||||
emptyView.visibility = View.VISIBLE
|
emptyView.visibility = View.VISIBLE
|
||||||
Toast.makeText(requireContext(), e.message ?: getString(R.string.my_resumes_error), Toast.LENGTH_LONG).show()
|
Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show()
|
||||||
}
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindJobs(jobs: List<JobSummary>) {
|
if (!state.loading && state.jobs.isNotEmpty()) {
|
||||||
if (jobs.isEmpty()) {
|
emptyView.visibility = View.GONE
|
||||||
|
listView.visibility = View.VISIBLE
|
||||||
|
listView.adapter = JobListAdapter(requireContext(), state.jobs)
|
||||||
|
} else if (!state.loading && state.error == null) {
|
||||||
emptyView.text = getString(R.string.my_resumes_empty)
|
emptyView.text = getString(R.string.my_resumes_empty)
|
||||||
emptyView.visibility = View.VISIBLE
|
emptyView.visibility = View.VISIBLE
|
||||||
listView.visibility = View.GONE
|
listView.visibility = View.GONE
|
||||||
return
|
|
||||||
}
|
|
||||||
listView.visibility = View.VISIBLE
|
|
||||||
listView.adapter = JobListAdapter(requireContext(), jobs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openJob(jobId: String) {
|
state.openedJob?.let { job ->
|
||||||
showLoading(true)
|
viewModel.consumeOpenedJob()
|
||||||
Thread {
|
openJobResult(job)
|
||||||
try {
|
|
||||||
val job = ApiClient.getJob(jobId)
|
|
||||||
handler.post {
|
|
||||||
showLoading(false)
|
|
||||||
if (job == null) {
|
|
||||||
Toast.makeText(requireContext(), R.string.my_resumes_error, Toast.LENGTH_LONG).show()
|
|
||||||
return@post
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openJobResult(job: com.example.resbuilder.data.model.Job) {
|
||||||
when (job.status.uppercase()) {
|
when (job.status.uppercase()) {
|
||||||
"COMPLETED" -> {
|
"COMPLETED" -> {
|
||||||
val type = job.type ?: JobFormFragment.TYPE_RESUME
|
val type = job.type ?: JobFormFragment.TYPE_RESUME
|
||||||
replaceWith(ResultFragment.newInstance(job, type))
|
replaceWith(ResultFragment.newInstance(job, type))
|
||||||
}
|
}
|
||||||
"FAILED" -> Toast.makeText(requireContext(), job.error ?: getString(R.string.status_failed), Toast.LENGTH_LONG).show()
|
"FAILED" -> Toast.makeText(requireContext(), job.error ?: getString(R.string.status_failed), Toast.LENGTH_LONG).show()
|
||||||
else -> replaceWith(JobStatusFragment.newInstance(jobId, job.type ?: JobFormFragment.TYPE_RESUME))
|
else -> replaceWith(JobStatusFragment.newInstance(job.job_id ?: "", job.type ?: JobFormFragment.TYPE_RESUME))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
handler.post {
|
|
||||||
showLoading(false)
|
|
||||||
Toast.makeText(requireContext(), e.message ?: getString(R.string.my_resumes_error), Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun replaceWith(fragment: Fragment) {
|
private fun replaceWith(fragment: Fragment) {
|
||||||
requireActivity().supportFragmentManager.beginTransaction()
|
requireActivity().supportFragmentManager.beginTransaction()
|
||||||
@ -110,15 +94,6 @@ class MyResumesFragment : Fragment() {
|
|||||||
.commitAllowingStateLoss()
|
.commitAllowingStateLoss()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showLoading(loading: Boolean) {
|
|
||||||
progress.visibility = if (loading) View.VISIBLE else View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
handler.removeCallbacksAndMessages(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
private class JobListAdapter(
|
private class JobListAdapter(
|
||||||
context: android.content.Context,
|
context: android.content.Context,
|
||||||
private val jobs: List<JobSummary>
|
private val jobs: List<JobSummary>
|
||||||
|
|||||||
@ -0,0 +1,54 @@
|
|||||||
|
package com.example.resbuilder.ui
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.example.resbuilder.data.model.Job
|
||||||
|
import com.example.resbuilder.data.model.JobSummary
|
||||||
|
import com.example.resbuilder.data.remote.ApiClient
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
data class MyResumesUiState(
|
||||||
|
val loading: Boolean = false,
|
||||||
|
val jobs: List<JobSummary> = emptyList(),
|
||||||
|
val error: String? = null,
|
||||||
|
val openedJob: Job? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
class MyResumesViewModel : ViewModel() {
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(MyResumesUiState())
|
||||||
|
val state: StateFlow<MyResumesUiState> = _state.asStateFlow()
|
||||||
|
|
||||||
|
fun loadJobs() {
|
||||||
|
_state.value = _state.value.copy(loading = true, error = null)
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val jobs = withContext(Dispatchers.IO) { ApiClient.getMyJobs().jobs }
|
||||||
|
_state.value = _state.value.copy(loading = false, jobs = jobs)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.value = _state.value.copy(loading = false, error = e.message ?: "load failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openJob(jobId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.value = _state.value.copy(loading = true, error = null)
|
||||||
|
try {
|
||||||
|
val job = withContext(Dispatchers.IO) { ApiClient.getJob(jobId) }
|
||||||
|
_state.value = _state.value.copy(loading = false, openedJob = job)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.value = _state.value.copy(loading = false, error = e.message ?: "open failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consumeOpenedJob() {
|
||||||
|
_state.value = _state.value.copy(openedJob = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,14 @@
|
|||||||
<string name="checking_auth">Checking authentication…</string>
|
<string name="checking_auth">Checking authentication…</string>
|
||||||
<string name="welcome_subtitle">AI-powered resume and cover letter builder</string>
|
<string name="welcome_subtitle">AI-powered resume and cover letter builder</string>
|
||||||
<string name="sign_in_terms">By signing in, you agree to our Terms of Service and Privacy Policy</string>
|
<string name="sign_in_terms">By signing in, you agree to our Terms of Service and Privacy Policy</string>
|
||||||
|
<string name="sign_in_interrupted">Sign-in interrupted</string>
|
||||||
|
<string name="no_id_token">No ID token from Google</string>
|
||||||
|
<string name="invalid_google_id_token">Invalid Google ID token: %1$s</string>
|
||||||
|
<string name="unexpected_credential_type">Unexpected credential type</string>
|
||||||
|
<string name="no_google_accounts">No Google accounts found on device</string>
|
||||||
|
<string name="sign_in_cancelled">Sign-in cancelled</string>
|
||||||
|
<string name="sign_in_failed">Sign-in failed: %1$s</string>
|
||||||
|
<string name="welcome_user">Welcome %1$s</string>
|
||||||
|
|
||||||
<!-- Main -->
|
<!-- Main -->
|
||||||
<string name="app_title">resBuilder</string>
|
<string name="app_title">resBuilder</string>
|
||||||
@ -28,6 +36,7 @@
|
|||||||
<string name="generate_cover_letter">Generate Cover Letter</string>
|
<string name="generate_cover_letter">Generate Cover Letter</string>
|
||||||
<string name="please_log_in">Please log in to continue</string>
|
<string name="please_log_in">Please log in to continue</string>
|
||||||
<string name="fields_required">Please fill in all fields</string>
|
<string name="fields_required">Please fill in all fields</string>
|
||||||
|
<string name="enter_url">Enter a URL</string>
|
||||||
|
|
||||||
<!-- Status -->
|
<!-- Status -->
|
||||||
<string name="status_queued">Queued</string>
|
<string name="status_queued">Queued</string>
|
||||||
|
|||||||
@ -13,9 +13,15 @@ googlePlayServicesAuth = "20.7.0"
|
|||||||
credentialManager = "1.5.0-beta01"
|
credentialManager = "1.5.0-beta01"
|
||||||
googleid = "1.1.1"
|
googleid = "1.1.1"
|
||||||
kotlinxCoroutines = "1.9.0"
|
kotlinxCoroutines = "1.9.0"
|
||||||
|
lifecycleViewmodel = "2.8.7"
|
||||||
|
lifecycleRuntime = "2.8.7"
|
||||||
|
fragmentKtx = "1.8.5"
|
||||||
|
|
||||||
[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" }
|
||||||
|
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodel" }
|
||||||
|
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntime" }
|
||||||
|
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user